diff --git a/doc/source/generated/rubem.core.Model.rst b/doc/source/generated/rubem.core.Model.rst deleted file mode 100644 index 5d5d90a..0000000 --- a/doc/source/generated/rubem.core.Model.rst +++ /dev/null @@ -1,26 +0,0 @@ -rubem.core.Model -================ - -.. currentmodule:: rubem.core - -.. autoclass:: Model - :members: - :show-inheritance: - :inherited-members: - :special-members: __call__, __add__, __mul__ - - - - .. rubric:: Methods - - .. autosummary:: - :nosignatures: - - ~Model.load - ~Model.run - - - - - - \ No newline at end of file diff --git a/doc/source/userguide.rst b/doc/source/userguide.rst index 82bd1eb..1fc3525 100644 --- a/doc/source/userguide.rst +++ b/doc/source/userguide.rst @@ -32,10 +32,10 @@ Digital Elevation Map (DEM) Mandatory path to Digital Elevation Map (DEM) file `[masl] `_ in PCRaster map format :file:`*.map`: this map contains topographic ground elevation in meters. Must be a valid file path to a PCRaster map format :file:`*.map` file. :ref:`See more. ` - .. code-block:: dosini - - [RASTERS] - dem = /Dataset/UIGCRB/input/maps/dem/dem.map +.. code-block:: dosini + + [RASTERS] + dem = /Dataset/UIGCRB/input/maps/dem/dem.map Mask of Catchment (Clone) `````````````````````````` @@ -53,7 +53,7 @@ Local Drain Direction (LDD) Optional path to Local Drain Direction (LDD) file in PCRaster map format :file:`*.map`. This map contains the local flow direction of each cell in the catchment. Must be a valid file path to a PCRaster map format :file:`*.map` file. :ref:`See more. ` -.. note:: +.. warning:: If not specified in the simulation configuration, it will be automatically generated using ``lddcreate`` from `PCRaster `_. @@ -100,30 +100,55 @@ Mandatory cell dimension value in meters. Value has to correspond to the pixel r Simulation Period ````````````````` +.. warning:: + + All dates must be valid and fall within between the time period of the dataset input time range. + Start Date '''''''''' -Mandatory date of the first time step of the simulation scenario (day, month and year of the start period of simulation scenario). Start date must be before the end date. +Mandatory date of the first time step of the simulation scenario (day, month and year of the start period of simulation scenario). .. code-block:: dosini [SIM_TIME] start = 01/01/2000 +.. warning:: + + The start date must be less than the :ref:`end date. ` + End Date '''''''' -Mandatory date of the last time step of the simulation scenario (day, month and year of the last period of simulation scenario). End date must be after the start date. +Mandatory date of the last time step of the simulation scenario (day, month and year of the last period of simulation scenario). .. code-block:: dosini [SIM_TIME] end = 01/08/2000 -.. note:: +.. warning:: + + The end date must be greater than the :ref:`start date. ` + +Alignment Date +'''''''''''''' + +Optional date of the alignment time step of the simulation scenario (day, month and year of the alignment period of simulation scenario). If not specified, the alignment date will be the same as the :ref:`start date. ` + +.. code-block:: dosini - Both dates must be valid and fall within between the time period of the dataset input time range. The ``end`` date must be greater than the ``start`` date. + [SIM_TIME] + alignment = 01/01/2000 +.. warning:: + + The alignment date must be before the :ref:`start date. ` + +.. note:: + + In certain scenarios, modelers may need to initiate simulations from a date other than the one corresponding to file ``*.001``. This will allow the transformation of time steps accordingly, ensuring alignment with the desired starting date for the simulation. Soil Parameters ---------------- @@ -152,6 +177,7 @@ Mandatory path to a tabular file with values :raw-html:`[g/cm3]` of B ```````````````````````````````````````````````````````````````````````````````` Mandatory path to a tabular file with values [mm/month] of saturated hydraulic conductivity for each soil class. Must be a valid path to an existing text file :file:`*.txt` or comma-separated values (CSV) file :file:`*.csv`. :ref:`See more. ` + .. code-block:: dosini [TABLES] diff --git a/rubem/_dynamic_model.py b/rubem/_dynamic_model.py index 1f93e91..3c51a1e 100644 --- a/rubem/_dynamic_model.py +++ b/rubem/_dynamic_model.py @@ -195,13 +195,13 @@ def dynamic(self): """ current_timestep = self.currentStep current_date = self.config.simulation_period.start_date + relativedelta( - months=current_timestep - 1 + months=(current_timestep - self.config.simulation_period.first_step) ) self.logger.info( "Cycle %s of %s (%s)", current_timestep, self.config.simulation_period.last_step, - current_date.strftime("%d/%m/%Y"), + current_date.strftime("%b/%Y"), ) print(f"## Timestep {current_timestep} of {self.config.simulation_period.last_step}") @@ -210,7 +210,6 @@ def dynamic(self): current_ndvi = self.__readmap_series_wrapper( files_partial_path=self.config.raster_series.ndvi, dynamic_readmap_func=self.readmap, - supress_errors=True, ) self.previous_ndvi = current_ndvi except RuntimeError: @@ -226,7 +225,6 @@ def dynamic(self): current_landuse = self.__readmap_series_wrapper( files_partial_path=self.config.raster_series.landuse, dynamic_readmap_func=self.readmap, - supress_errors=True, ) self.previous_landuse = current_landuse except RuntimeError: diff --git a/rubem/configuration/model_configuration.py b/rubem/configuration/model_configuration.py index b8e3147..7787379 100644 --- a/rubem/configuration/model_configuration.py +++ b/rubem/configuration/model_configuration.py @@ -68,8 +68,15 @@ def __init__( self.logger.debug("Loading configuration...") self.simulation_period = SimulationPeriod( - datetime.strptime(self.__get_setting("SIM_TIME", "start"), "%d/%m/%Y").date(), - datetime.strptime(self.__get_setting("SIM_TIME", "end"), "%d/%m/%Y").date(), + start=datetime.strptime(self.__get_setting("SIM_TIME", "start"), "%d/%m/%Y"), + end=datetime.strptime(self.__get_setting("SIM_TIME", "end"), "%d/%m/%Y"), + alignment=( + datetime.strptime( + self.__get_setting("SIM_TIME", "alignment", optional=True), "%d/%m/%Y" + ) + if self.__get_setting("SIM_TIME", "alignment", optional=True) + else None + ), ) self.grid = RasterGrid(float(self.__get_setting("GRID", "grid"))) self.calibration_parameters = CalibrationParameters( diff --git a/rubem/configuration/simulation_period.py b/rubem/configuration/simulation_period.py index bbc8e84..5a3b3d5 100644 --- a/rubem/configuration/simulation_period.py +++ b/rubem/configuration/simulation_period.py @@ -1,5 +1,8 @@ -from datetime import date +from datetime import date, datetime import logging +from typing import Optional, Union + +DATE_FORMAT = "%d/%m/%Y" class SimulationPeriod: @@ -7,29 +10,56 @@ class SimulationPeriod: Represents a period of time for simulation. :param start: The start date of the simulation period. - :type start: date + :type start: Union[date, datetime] :param end: The end date of the simulation period. - :type end: date + :type end: Union[date, datetime] + + :param alignment: The date to align the simulation period to. If not provided, the start date is used. + :type alignment: Optional[Union[date, datetime]] :raises ValueError: If the start date is not before the end date. """ - def __init__(self, start: date, end: date): + def __init__( + self, + start: Union[date, datetime], + end: Union[date, datetime], + alignment: Optional[Union[date, datetime]] = None, + ): self.logger = logging.getLogger(__name__) + if start >= end: - self.logger.error("Start date must be before end date.") - raise ValueError("Start date must be before end date.") + self.logger.error( + "Start date (%s) must be before end date (%s).", + start.strftime(DATE_FORMAT), + end.strftime(DATE_FORMAT), + ) + raise ValueError( + f"Start date ({start.strftime(DATE_FORMAT)}) must be before end date ({end.strftime(DATE_FORMAT)})." + ) + self.start_date = start self.end_date = end - self.total_steps = ( - (self.end_date.year - self.start_date.year) * 12 - + (self.end_date.month - self.start_date.month) - + 1 - ) - self.first_step = 1 # PCRaster Dynamic Framework uses 1-based indexing - self.last_step = self.total_steps + if not alignment: + self.logger.info("No alignment date provided. Using start date as alignment.") + alignment = self.start_date + + if alignment > self.start_date: + self.logger.error( + "Alignment date (%s) is after start date (%s).", + alignment.strftime(DATE_FORMAT), + self.start_date.strftime(DATE_FORMAT), + ) + raise ValueError( + f"Alignment date ({alignment.strftime(DATE_FORMAT)}) must be before start date ({self.start_date.strftime(DATE_FORMAT)})." + ) + + self.first_step = (start.year - alignment.year) * 12 + (start.month - alignment.month) + 1 + self.last_step = (end.year - alignment.year) * 12 + (end.month - alignment.month) + 1 + + self.total_steps = self.last_step - self.first_step + 1 def __str__(self) -> str: return f"{self.start_date} to {self.end_date}" diff --git a/tests/unit/configuration/test_simulation_period.py b/tests/unit/configuration/test_simulation_period.py index 2e6f948..0c217ea 100644 --- a/tests/unit/configuration/test_simulation_period.py +++ b/tests/unit/configuration/test_simulation_period.py @@ -45,3 +45,36 @@ def test_simulation_period_step_calculation( assert sp.first_step == first_step assert sp.last_step == last_step assert sp.total_steps == total_steps + + @pytest.mark.unit + @pytest.mark.parametrize( + "start, end, alignment, expected_first_step, expected_last_step, expected_total_steps", + [ + (date(2024, 1, 1), date(2024, 3, 31), date(2024, 1, 1), 1, 3, 3), + (date(2024, 1, 1), date(2024, 12, 31), date(2019, 1, 1), 61, 72, 12), + (date(2020, 1, 1), date(2024, 12, 31), date(2020, 1, 1), 1, 60, 60), + (date(2021, 1, 1), date(2024, 3, 31), date(2019, 1, 1), 25, 63, 39), + (date(2022, 1, 1), date(2024, 12, 31), date(2019, 1, 1), 37, 72, 36), + (date(2020, 1, 1), date(2024, 12, 31), date(2019, 1, 1), 13, 72, 60), + ], + ) + def test_simulation_period_alignment( + self, start, end, alignment, expected_first_step, expected_last_step, expected_total_steps + ): + sp = SimulationPeriod(start=start, end=end, alignment=alignment) + assert sp.first_step == expected_first_step + assert sp.last_step == expected_last_step + assert sp.total_steps == expected_total_steps + + @pytest.mark.unit + @pytest.mark.parametrize( + "start, end, alignment", + [ + (date(2024, 1, 1), date(2024, 3, 31), date(2024, 1, 2)), + (date(2024, 1, 1), date(2024, 12, 31), date(2024, 12, 31)), + (date(2020, 1, 1), date(2024, 12, 31), date(2025, 1, 1)), + ], + ) + def test_simulation_period_alignment_bad_args(self, start, end, alignment): + with pytest.raises(Exception): + SimulationPeriod(start=start, end=end, alignment=alignment)