Skip to content

Commit

Permalink
feat(api): add thermal timeseries generation (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
salemsd authored Jan 13, 2025
1 parent 97672ff commit 8208053
Show file tree
Hide file tree
Showing 10 changed files with 89 additions and 20 deletions.
6 changes: 6 additions & 0 deletions src/antares/craft/exceptions/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,3 +360,9 @@ def __init__(self, study_id: str, output_id: str, mc_type: str, object_type: str
f"Could not create {mc_type}/{object_type} aggregate for study {study_id}, output {output_id}: " + message
)
super().__init__(self.message)


class ThermalTimeseriesGenerationError(Exception):
def __init__(self, study_id: str, message: str) -> None:
self.message = f"Could not generate thermal timeseries for study {study_id}: " + message
super().__init__(self.message)
3 changes: 3 additions & 0 deletions src/antares/craft/model/study.py
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,9 @@ def delete_output(self, output_name: str) -> None:
self._study_service.delete_output(output_name)
self._outputs.pop(output_name)

def generate_thermal_timeseries(self) -> None:
self._study_service.generate_thermal_timeseries()


def _verify_study_already_exists(study_directory: Path) -> None:
if study_directory.exists():
Expand Down
20 changes: 2 additions & 18 deletions src/antares/craft/service/api_services/run_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
SimulationFailedError,
SimulationTimeOutError,
TaskFailedError,
TaskTimeOutError,
)
from antares.craft.model.simulation import AntaresSimulationParameters, Job, JobStatus
from antares.craft.service.api_services.utils import wait_task_completion
from antares.craft.service.base_services import BaseRunService


Expand Down Expand Up @@ -94,7 +94,7 @@ def _wait_unzip_output(self, ref_id: str, job: Job, time_out: int) -> None:
response = self._wrapper.post(url, json=payload)
tasks = response.json()
task_id = self._get_unarchiving_task_id(job, tasks)
self._wait_task_completion(task_id, repeat_interval, time_out)
wait_task_completion(self._base_url, self._wrapper, task_id, repeat_interval, time_out)
except (APIError, TaskFailedError) as e:
raise AntaresSimulationUnzipError(self.study_id, job.job_id, e.message) from e

Expand All @@ -105,19 +105,3 @@ def _get_unarchiving_task_id(self, job: Job, tasks: list[dict[str, Any]]) -> str
if output_id == job.output_id:
return task["id"]
raise AntaresSimulationUnzipError(self.study_id, job.job_id, "Could not find task for unarchiving job")

def _wait_task_completion(self, task_id: str, repeat_interval: int, time_out: int) -> None:
url = f"{self._base_url}/tasks/{task_id}"

start_time = time.time()
task_result = None
while not task_result:
if time.time() - start_time > time_out:
raise TaskTimeOutError(task_id, time_out)
response = self._wrapper.get(url)
task = response.json()
task_result = task["result"]
time.sleep(repeat_interval)

if not task_result["success"]:
raise TaskFailedError(task_id)
13 changes: 13 additions & 0 deletions src/antares/craft/service/api_services/study_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
StudyDeletionError,
StudySettingsUpdateError,
StudyVariantCreationError,
TaskFailedError,
TaskTimeOutError,
ThermalTimeseriesGenerationError,
)
from antares.craft.model.binding_constraint import BindingConstraint
from antares.craft.model.output import Output
Expand All @@ -34,6 +37,7 @@
from antares.craft.model.settings.study_settings import StudySettings
from antares.craft.model.settings.thematic_trimming import ThematicTrimmingParameters
from antares.craft.model.settings.time_series import TimeSeriesParameters
from antares.craft.service.api_services.utils import wait_task_completion
from antares.craft.service.base_services import BaseOutputService, BaseStudyService

if TYPE_CHECKING:
Expand Down Expand Up @@ -162,3 +166,12 @@ def delete_output(self, output_name: str) -> None:
self._wrapper.delete(url)
except APIError as e:
raise OutputDeletionError(self.study_id, output_name, e.message) from e

def generate_thermal_timeseries(self) -> None:
url = f"{self._base_url}/studies/{self.study_id}/timeseries/generate"
try:
response = self._wrapper.put(url)
task_id = response.json()
wait_task_completion(self._base_url, self._wrapper, task_id)
except (APIError, TaskFailedError, TaskTimeOutError) as e:
raise ThermalTimeseriesGenerationError(self.study_id, e.message)
2 changes: 1 addition & 1 deletion src/antares/craft/service/api_services/thermal_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def get_thermal_matrix(self, thermal_cluster: ThermalCluster, ts_name: ThermalCl
/ "thermal"
/ keyword
/ f"{thermal_cluster.area_id}"
/ f"{thermal_cluster.name.lower()}"
/ f"{thermal_cluster.id.lower()}"
/ ts_name.value
)
return get_matrix(self._base_url, self.study_id, self._wrapper, path.as_posix())
Expand Down
22 changes: 22 additions & 0 deletions src/antares/craft/service/api_services/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
# SPDX-License-Identifier: MPL-2.0
#
# This file is part of the Antares project.
import time

import pandas as pd

from antares.craft.api_conf.request_wrapper import RequestWrapper
from antares.craft.exceptions.exceptions import TaskFailedError, TaskTimeOutError


def upload_series(base_url: str, study_id: str, wrapper: RequestWrapper, series: pd.DataFrame, path: str) -> None:
Expand All @@ -31,3 +33,23 @@ def get_matrix(base_url: str, study_id: str, wrapper: RequestWrapper, series_pat
else:
dataframe = pd.DataFrame(data=json_df["data"], columns=json_df["columns"])
return dataframe


def wait_task_completion(
base_url: str, wrapper: RequestWrapper, task_id: str, repeat_interval: int = 5, time_out: int = 172800
) -> None:
url = f"{base_url}/tasks/{task_id}"

start_time = time.time()
task_result = None

while not task_result:
if time.time() - start_time > time_out:
raise TaskTimeOutError(task_id, time_out)
response = wrapper.get(url)
task = response.json()
task_result = task["result"]
time.sleep(repeat_interval)

if not task_result["success"]:
raise TaskFailedError(task_id)
4 changes: 4 additions & 0 deletions src/antares/craft/service/base_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,10 @@ def delete_output(self, output_name: str) -> None:
"""
pass

@abstractmethod
def generate_thermal_timeseries(self) -> None:
pass


class BaseRenewableService(ABC):
@abstractmethod
Expand Down
3 changes: 3 additions & 0 deletions src/antares/craft/service/local_services/study_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ def delete_outputs(self) -> None:

def delete_output(self, output_name: str) -> None:
raise NotImplementedError

def generate_thermal_timeseries(self) -> None:
raise NotImplementedError
22 changes: 22 additions & 0 deletions tests/antares/services/api_services/test_study_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
StudyCreationError,
StudySettingsUpdateError,
StudyVariantCreationError,
ThermalTimeseriesGenerationError,
)
from antares.craft.model.area import Area, AreaProperties, AreaUi
from antares.craft.model.binding_constraint import (
Expand Down Expand Up @@ -648,3 +649,24 @@ def test_delete_outputs(self):
mocker.get(outputs_url, json={"description": error_message}, status_code=404)
with pytest.raises(OutputsRetrievalError, match=error_message):
self.study.delete_outputs()

def test_generate_thermal_timeseries_success(self):
with requests_mock.Mocker() as mocker:
url = f"https://antares.com/api/v1/studies/{self.study_id}/timeseries/generate"
task_id = "task-5678"
mocker.put(url, json=task_id, status_code=200)

task_url = f"https://antares.com/api/v1/tasks/{task_id}"
mocker.get(task_url, json={"result": {"success": True}}, status_code=200)

with patch("antares.craft.service.api_services.utils.wait_task_completion", return_value=None):
self.study.generate_thermal_timeseries()

def test_generate_thermal_timeseries_failure(self):
with requests_mock.Mocker() as mocker:
url = f"https://antares.com/api/v1/studies/{self.study_id}/timeseries/generate"
error_message = f"Thermal timeseries generation failed for study {self.study_id}"
mocker.put(url, json={"description": error_message}, status_code=404)

with pytest.raises(ThermalTimeseriesGenerationError, match=error_message):
self.study.generate_thermal_timeseries()
14 changes: 13 additions & 1 deletion tests/integration/test_web_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,24 @@ def test_creation_lifecycle(self, antares_web: AntaresWebDesktop):

# test thermal cluster creation with default values
thermal_name = "Cluster_test %?"
thermal_fr = area_fr.create_thermal_cluster(thermal_name)
thermal_fr = area_fr.create_thermal_cluster(thermal_name, ThermalClusterProperties(nominal_capacity=1000))
assert thermal_fr.name == thermal_name.lower()
# AntaresWeb has id issues for thermal/renewable clusters,
# so we force the name in lowercase to avoid issues.
assert thermal_fr.id == "cluster_test"

# ===== Test generate thermal timeseries =====
study.generate_thermal_timeseries()
thermal_timeseries = thermal_fr.get_series_matrix()
assert isinstance(
thermal_timeseries,
pd.DataFrame,
)
assert thermal_timeseries.shape == (8760, 1)
assert (
(thermal_timeseries == 1000).all().all()
) # first all() returns a one column matrix with booleans, second all() checks that they're all true

# test thermal cluster creation with properties
thermal_name = "gaz_be"
thermal_properties = ThermalClusterProperties(efficiency=55)
Expand Down

0 comments on commit 8208053

Please sign in to comment.