Skip to content

Commit

Permalink
feat(api-ui): add Inflow Structure form in Hydro (#1919)
Browse files Browse the repository at this point in the history
Merge pull request #1919 from AntaresSimulatorTeam/feature/983-add-inflow-pattern-form
  • Loading branch information
laurent-laporte-pro authored Feb 1, 2024
2 parents a2060ae + 4283833 commit a5ab20d
Show file tree
Hide file tree
Showing 11 changed files with 403 additions and 22 deletions.
53 changes: 53 additions & 0 deletions antarest/study/business/areas/hydro_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@
from antarest.study.storage.storage_service import StudyStorageService
from antarest.study.storage.variantstudy.model.command.update_config import UpdateConfig

INFLOW_PATH = "input/hydro/prepro/{area_id}/prepro/prepro"


class InflowStructure(FormFieldsBaseModel):
"""Represents the inflow structure values in the hydraulic configuration."""

# NOTE: Currently, there is only one field for the inflow structure model
# due to the scope of hydro config requirements, it may change.
inter_monthly_correlation: float = Field(
default=0.5,
ge=0,
le=1,
description="Average correlation between the energy of a month and that of the next month",
title="Inter-monthly correlation",
)


class ManagementOptionsFormFields(FormFieldsBaseModel):
inter_daily_breakdown: Optional[float] = Field(ge=0)
Expand Down Expand Up @@ -121,3 +137,40 @@ def set_field_values(
if len(commands) > 0:
file_study = self.storage_service.get_storage(study).get_raw(study)
execute_or_add_commands(study, file_study, commands, self.storage_service)

# noinspection SpellCheckingInspection
def get_inflow_structure(self, study: Study, area_id: str) -> InflowStructure:
"""
Retrieves inflow structure values for a specific area within a study.
Returns:
InflowStructure: The inflow structure values.
"""
# NOTE: Focusing on the single field "intermonthly-correlation" due to current model scope.
path = INFLOW_PATH.format(area_id=area_id)
file_study = self.storage_service.get_storage(study).get_raw(study)
inter_monthly_correlation = file_study.tree.get(path.split("/")).get("intermonthly-correlation", 0.5)
return InflowStructure(inter_monthly_correlation=inter_monthly_correlation)

# noinspection SpellCheckingInspection
def update_inflow_structure(self, study: Study, area_id: str, values: InflowStructure) -> None:
"""
Updates inflow structure values for a specific area within a study.
Args:
study: The study instance to update the inflow data for.
area_id: The area identifier to update data for.
values: The new inflow structure values to be updated.
Raises:
RequestValidationError: If the provided `values` parameter is None or invalid.
"""
# NOTE: Updates only "intermonthly-correlation" due to current model scope.
path = INFLOW_PATH.format(area_id=area_id)
command = UpdateConfig(
target=path,
data={"intermonthly-correlation": values.inter_monthly_correlation},
command_context=self.storage_service.variant_study_service.command_factory.command_context,
)
file_study = self.storage_service.get_storage(study).get_raw(study)
execute_or_add_commands(study, file_study, [command], self.storage_service)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@
from antarest.study.storage.rawstudy.model.filesystem.ini_file_node import IniFileNode


# noinspection SpellCheckingInspection
class InputHydroPreproAreaPrepro(IniFileNode):
"""
Configuration for the hydraulic Inflow Structure:
- intermonthly-correlation: Average correlation between the energy of a month and
that of the next month (inter-monthly correlation).
See: https://antares-simulator.readthedocs.io/en/latest/reference-guide/04-active_windows/#hydro
"""

def __init__(self, context: ContextServer, config: FileStudyTreeConfig):
types = {"prepro": {"intermonthly-correlation": float}}
IniFileNode.__init__(self, context, config, types)
44 changes: 43 additions & 1 deletion antarest/study/web/study_data_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from antarest.study.business.advanced_parameters_management import AdvancedParamsFormFields
from antarest.study.business.allocation_management import AllocationFormFields, AllocationMatrix
from antarest.study.business.area_management import AreaCreationDTO, AreaInfoDTO, AreaType, AreaUI, LayerInfoDTO
from antarest.study.business.areas.hydro_management import ManagementOptionsFormFields
from antarest.study.business.areas.hydro_management import InflowStructure, ManagementOptionsFormFields
from antarest.study.business.areas.properties_management import PropertiesFormFields
from antarest.study.business.areas.renewable_management import (
RenewableClusterCreation,
Expand Down Expand Up @@ -423,6 +423,48 @@ def set_hydro_form_values(

study_service.hydro_manager.set_field_values(study, data, area_id)

# noinspection SpellCheckingInspection
@bp.get(
"/studies/{uuid}/areas/{area_id}/hydro/inflow-structure",
tags=[APITag.study_data],
summary="Get inflow structure values",
response_model=InflowStructure,
)
def get_inflow_structure(
uuid: str,
area_id: str,
current_user: JWTUser = Depends(auth.get_current_user),
) -> InflowStructure:
"""Get the configuration for the hydraulic inflow structure of the given area."""
logger.info(
msg=f"Getting inflow structure values for area {area_id} of study {uuid}",
extra={"user": current_user.id},
)
params = RequestParameters(user=current_user)
study = study_service.check_study_access(uuid, StudyPermissionType.READ, params)
return study_service.hydro_manager.get_inflow_structure(study, area_id)

@bp.put(
"/studies/{uuid}/areas/{area_id}/hydro/inflow-structure",
tags=[APITag.study_data],
summary="Update inflow structure values",
response_model=InflowStructure,
)
def update_inflow_structure(
uuid: str,
area_id: str,
values: InflowStructure,
current_user: JWTUser = Depends(auth.get_current_user),
) -> None:
"""Update the configuration for the hydraulic inflow structure of the given area."""
logger.info(
msg=f"Updating inflow structure values for area {area_id} of study {uuid}",
extra={"user": current_user.id},
)
params = RequestParameters(user=current_user)
study = study_service.check_study_access(uuid, StudyPermissionType.WRITE, params)
return study_service.hydro_manager.update_inflow_structure(study, area_id, values)

@bp.put(
"/studies/{uuid}/matrix",
tags=[APITag.study_data],
Expand Down
155 changes: 155 additions & 0 deletions tests/integration/study_data_blueprint/test_hydro_inflow_structure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from http import HTTPStatus
from unittest.mock import ANY

import pytest
from starlette.testclient import TestClient


@pytest.mark.unit_test
class TestHydroInflowStructure:
"""
Test the end points related to hydraulic inflow-structure.
Those tests use the "examples/studies/STA-mini.zip" Study,
which contains the following areas: ["de", "es", "fr", "it"].
"""

def test_get_inflow_structure(
self,
client: TestClient,
user_access_token: str,
study_id: str,
):
user_header = {"Authorization": f"Bearer {user_access_token}"}
area_id = "fr"

# ====================
# Use case : RAW study
# ====================

# Check that the default values are returned
res = client.get(
f"/v1/studies/{study_id}/areas/{area_id}/hydro/inflow-structure",
headers=user_header,
)
assert res.status_code == HTTPStatus.OK, res.json()
actual = res.json()
expected = {"interMonthlyCorrelation": 0.5}
assert actual == expected

# Update the values
obj = {"interMonthlyCorrelation": 0.8}
res = client.put(
f"/v1/studies/{study_id}/areas/{area_id}/hydro/inflow-structure",
headers=user_header,
json=obj,
)
assert res.status_code == HTTPStatus.OK, res.json()

# Check that the right values are returned
res = client.get(
f"/v1/studies/{study_id}/areas/{area_id}/hydro/inflow-structure",
headers=user_header,
)
assert res.status_code == HTTPStatus.OK, res.json()
actual = res.json()
expected = {"interMonthlyCorrelation": 0.8}
assert actual == expected

# ========================
# Use case : Variant study
# ========================

# Create a managed study from the RAW study.
res = client.post(
f"/v1/studies/{study_id}/copy",
headers={"Authorization": f"Bearer {user_access_token}"},
params={"dest": "Clone", "with_outputs": False, "use_task": False},
)
res.raise_for_status()
managed_id = res.json()
assert managed_id is not None

# Create a variant from the managed study.
res = client.post(
f"/v1/studies/{managed_id}/variants",
headers={"Authorization": f"Bearer {user_access_token}"},
params={"name": "Variant"},
)
res.raise_for_status()
variant_id = res.json()
assert variant_id is not None

# Check that the return values match the RAW study values
res = client.get(
f"/v1/studies/{variant_id}/areas/{area_id}/hydro/inflow-structure",
headers=user_header,
)
assert res.status_code == HTTPStatus.OK, res.json()
actual = res.json()
expected = {"interMonthlyCorrelation": 0.8}
assert actual == expected

# Update the values
obj = {"interMonthlyCorrelation": 0.9}
res = client.put(
f"/v1/studies/{variant_id}/areas/{area_id}/hydro/inflow-structure",
headers=user_header,
json=obj,
)
assert res.status_code == HTTPStatus.OK, res.json()

# Check that the right values are returned
res = client.get(
f"/v1/studies/{variant_id}/areas/{area_id}/hydro/inflow-structure",
headers=user_header,
)
assert res.status_code == HTTPStatus.OK, res.json()
actual = res.json()
expected = {"interMonthlyCorrelation": 0.9}
assert actual == expected

# Check the variant commands
res = client.get(
f"/v1/studies/{variant_id}/commands",
headers=user_header,
)
assert res.status_code == HTTPStatus.OK, res.json()
actual = res.json()
assert len(actual) == 2
expected = {
"id": ANY,
"action": "update_config",
"args": {
"target": "input/hydro/prepro/fr/prepro/prepro",
"data": {"intermonthly-correlation": 0.9},
},
"version": 1,
}
assert actual[1] == expected

def test_update_inflow_structure__invalid_values(
self,
client: TestClient,
user_access_token: str,
study_id: str,
):
user_header = {"Authorization": f"Bearer {user_access_token}"}
area_id = "fr"

# Update the values with invalid values
obj = {"interMonthlyCorrelation": 1.1}
res = client.put(
f"/v1/studies/{study_id}/areas/{area_id}/hydro/inflow-structure",
headers=user_header,
json=obj,
)
assert res.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, res.json()

obj = {"interMonthlyCorrelation": -0.1}
res = client.put(
f"/v1/studies/{study_id}/areas/{area_id}/hydro/inflow-structure",
headers=user_header,
json=obj,
)
assert res.status_code == HTTPStatus.UNPROCESSABLE_ENTITY, res.json()
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useOutletContext } from "react-router";
import { StudyMetadata } from "../../../../../../../../common/types";
import useAppSelector from "../../../../../../../../redux/hooks/useAppSelector";
import { getCurrentAreaId } from "../../../../../../../../redux/selectors";
import Form from "../../../../../../../common/Form";
import {
type InflowStructureFields,
getInflowStructureFields,
updateInflowStructureFields,
} from "./utils";
import NumberFE from "../../../../../../../common/fieldEditors/NumberFE";
import { SubmitHandlerPlus } from "../../../../../../../common/Form/types";
import { useTranslation } from "react-i18next";

function InflowStructure() {
const [t] = useTranslation();
const { study } = useOutletContext<{ study: StudyMetadata }>();
const areaId = useAppSelector(getCurrentAreaId);

////////////////////////////////////////////////////////////////
// Event handlers
////////////////////////////////////////////////////////////////

const handleSubmit = (data: SubmitHandlerPlus<InflowStructureFields>) => {
return updateInflowStructureFields(study.id, areaId, data.values);
};

////////////////////////////////////////////////////////////////
// JSX
////////////////////////////////////////////////////////////////

return (
<Form
key={study.id + areaId}
config={{
defaultValues: () => getInflowStructureFields(study.id, areaId),
}}
onSubmit={handleSubmit}
miniSubmitButton
enableUndoRedo
sx={{ display: "flex", alignItems: "center", ".Form__Footer": { p: 0 } }}
>
{({ control }) => (
<NumberFE
label="Inter-Monthly Correlation"
name="interMonthlyCorrelation"
control={control}
rules={{
min: {
value: 0,
message: t("form.field.minValue", { 0: 0 }),
},
max: {
value: 1,
message: t("form.field.maxValue", { 0: 1 }),
},
}}
inputProps={{ step: 0.1 }}
size="small"
sx={{ width: 180 }}
/>
)}
</Form>
);
}

export default InflowStructure;
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { type StudyMetadata } from "../../../../../../../../common/types";
import client from "../../../../../../../../services/api/client";

////////////////////////////////////////////////////////////////
// Types
////////////////////////////////////////////////////////////////

export interface InflowStructureFields {
interMonthlyCorrelation: number;
}

////////////////////////////////////////////////////////////////
// Utils
////////////////////////////////////////////////////////////////

function makeRequestURL(studyId: StudyMetadata["id"], areaId: string): string {
return `v1/studies/${studyId}/areas/${areaId}/hydro/inflow-structure`;
}

export async function getInflowStructureFields(
studyId: StudyMetadata["id"],
areaId: string,
): Promise<InflowStructureFields> {
const res = await client.get(makeRequestURL(studyId, areaId));
return res.data;
}

export function updateInflowStructureFields(
studyId: StudyMetadata["id"],
areaId: string,
values: InflowStructureFields,
): Promise<void> {
return client.put(makeRequestURL(studyId, areaId), values);
}
Loading

0 comments on commit a5ab20d

Please sign in to comment.