Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api-ui): add inflow structure form #1919

Merged
merged 10 commits into from
Feb 1, 2024
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
Loading