diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index b6679e83..201fcfc5 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1 +1,2 @@ f728f7fc0e523b6bf33592dd41661cb331efa924 +e5d8bb62af96a0900aa77b348ac076033d38c0fc diff --git a/CHANGELOG.md b/CHANGELOG.md index f732802f..51aa57da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## v0.13.1 - 7 January 2026 + +- Updates the project capacity check for a true 6 decimal precision check. +- Breaks apart all long error messages so they are defined outside of the error call. + ## v0.13 - 23 December 2025 ### Default Data Now Available diff --git a/wombat/__init__.py b/wombat/__init__.py index 780ae6df..bde190d0 100644 --- a/wombat/__init__.py +++ b/wombat/__init__.py @@ -4,4 +4,4 @@ from wombat.core.library import create_library_structure, load_yaml -__version__ = "0.13" +__version__ = "0.13.1" diff --git a/wombat/core/data_classes.py b/wombat/core/data_classes.py index a24800f4..ab7b2e72 100644 --- a/wombat/core/data_classes.py +++ b/wombat/core/data_classes.py @@ -372,17 +372,17 @@ def annual_date_range( """ # Check the year bounds if end_year < start_year: - raise ValueError( - f"The end_year ({start_year}) is later than the start_year ({end_year})." - ) + msg = f"The end_year ({start_year}) is later than the start_year ({end_year})." + raise ValueError(msg) # Check the month, date combination bounds start = datetime.datetime(2022, start_month, start_day) end = datetime.datetime(2022, end_month, end_day) if end < start: - raise ValueError( + msg = ( f"The starting month/day combination: {start}, is after the ending: {end}." ) + raise ValueError(msg) # Create a list of arrays of date ranges for each year start = datetime.datetime(1, start_month, start_day) @@ -470,22 +470,25 @@ def check_start_stop_dates( if value is None: if start_date is None: return - raise ValueError( + msg = ( "A starting date was provided, but no ending date was provided" f" for `{attribute.name}`." ) + raise ValueError(msg) if start_date is None: - raise ValueError( + msg = ( "An ending date was provided, but no starting date was provided" f" for `{start_name}`." ) + raise ValueError(msg) if start_date == value: - raise ValueError( + msg = ( f"Starting date (`{start_name}`={start_date} and ending date" f" (`{attribute.name}`={value}) cannot be the same date." ) + raise ValueError(msg) def convert_maintenance_list(value: list[dict], self_) -> list[Maintenance]: @@ -547,10 +550,11 @@ def validate_0_1_inclusive( The input value for `speed_reduction_factor`. """ if value < 0 or value > 1: - raise ValueError( + msg = ( f"Input for {attribute.name} must be between 0 and 1, inclusive, not:" f" {value=}." ) + raise ValueError(msg) def to_datetime(value: str | datetime.datetime) -> datetime.datetime: @@ -627,10 +631,11 @@ def from_dict(cls, data: dict): ] undefined = sorted(set(required_inputs) - set(kwargs)) if undefined: - raise AttributeError( + msg = ( f"The class defintion for {cls.__name__} is missing the following" f" inputs: {undefined}" ) + raise AttributeError(msg) return cls(**kwargs) @@ -1216,9 +1221,8 @@ def _compare_dates( original_start = getattr(self, f"{which}_start") original_end = getattr(self, f"{which}_end") else: - raise ValueError( - "`which` must be one of 'reduced_speed' or 'non_operational'." - ) + msg = "`which` must be one of 'reduced_speed' or 'non_operational'." + raise ValueError(msg) if original_start is not None: if new_start is not None: @@ -1284,16 +1288,16 @@ def set_non_operational_dates( # Check that the input year range is valid if not isinstance(start_year, int): - raise ValueError( - f"Input to `start_year`: {start_year}, must be an integer." - ) + msg = f"Input to `start_year`: {start_year}, must be an integer." + raise ValueError(msg) if not isinstance(end_year, int): raise ValueError(f"Input to `end_year`: {end_year}, must be an integer.") if end_year < start_year: - raise ValueError( + msg = ( "`start_year`: {start_year}, must less than or equal to the" f" `end_year`: {end_year}" ) + raise ValueError(msg) # Create the date range dates = annualized_date_range( @@ -1357,16 +1361,16 @@ def set_reduced_speed_parameters( # Check that the input year range is valid if not isinstance(start_year, int): - raise ValueError( - f"Input to `start_year`: {start_year}, must be an integer." - ) + msg = f"Input to `start_year`: {start_year}, must be an integer." + raise ValueError(msg) if not isinstance(end_year, int): raise ValueError(f"Input to `end_year`: {end_year}, must be an integer.") if end_year < start_year: - raise ValueError( + msg = ( "`start_year`: {start_year}, must less than or equal to the" f" `end_year`: {end_year}" ) + raise ValueError(msg) # Create the date range dates = annualized_date_range( @@ -1770,16 +1774,18 @@ def _validate_threshold( """Ensure a valid threshold is provided for a given ``strategy``.""" if self.strategy == "downtime": if value <= 0 or value >= 1: - raise ValueError( + msg = ( "Downtime-based strategies must have a ``strategy_threshold``", "between 0 and 1, non-inclusive!", ) + raise ValueError(msg) if self.strategy == "requests": if value <= 0: - raise ValueError( + msg = ( "Requests-based strategies must have a ``strategy_threshold``", "greater than 0!", ) + raise ValueError(msg) def __attrs_post_init__(self) -> None: """Post-initialization hook.""" @@ -1865,10 +1871,11 @@ def __attrs_post_init__(self): self, "strategy", clean_string_input(self.data_dict["strategy"]) ) if self.strategy not in VALID_STRATEGIES: - raise ValueError( + msg = ( f"ServiceEquipment strategy should be one of {VALID_STRATEGIES};" f" input: {self.strategy}." ) + raise ValueError(msg) def determine_type( self, diff --git a/wombat/core/environment.py b/wombat/core/environment.py index f989dcde..bef8102f 100644 --- a/wombat/core/environment.py +++ b/wombat/core/environment.py @@ -173,9 +173,8 @@ def __init__( if not 0 <= self.workday_end <= 24: raise ValueError("workday_end must be a valid 24hr time.") if self.workday_end <= self.workday_start: - raise ValueError( - "Work shifts must end after they start ({self.workday_start}hrs)." - ) + msg = "Work shifts must end after they start ({self.workday_start}hrs)." + raise ValueError(msg) self.port_distance = port_distance self.weather = self._weather_setup(weather_file, start_year, end_year) @@ -491,10 +490,11 @@ def _weather_setup( if start_year is None: pass elif start_year > self.end_year: - raise ValueError( + msg = ( f"'start_year' ({start_year}) occurs after the last available year" f" in the weather data (range: {self.end_year})" ) + raise ValueError(msg) else: # Filter for the provided, validated starting year and update the attribute weather = ( @@ -508,16 +508,18 @@ def _weather_setup( if end_year is None: pass elif start_year is None and end_year < self.start_year: - raise ValueError( + msg = ( f"The provided 'end_year' ({end_year}) is before the start_year" f" ({self.start_year})" ) + raise ValueError(msg) elif start_year is not None: if end_year < start_year: - raise ValueError( + msg = ( f"The provided 'end_year' ({end_year}) is before the start_year" f" ({start_year})" ) + raise ValueError(msg) else: # Filter for the provided, validated ending year and update weather = weather.filter(pl.col("datetime").dt.year() <= end_year) @@ -647,9 +649,8 @@ def log_action( """ valid_locations = ("site", "system", "port", "enroute", "na") if location not in valid_locations: - raise ValueError( - f"Event logging `location` must be one of: {valid_locations}" - ) + msg = f"Event logging `location` must be one of: {valid_locations}" + raise ValueError(msg) total_labor_cost = hourly_labor_cost + salary_labor_cost total_cost = total_labor_cost + equipment_cost + materials_cost now = self.simulation_time diff --git a/wombat/core/mixins.py b/wombat/core/mixins.py index b6a6308a..f3fb2bcb 100644 --- a/wombat/core/mixins.py +++ b/wombat/core/mixins.py @@ -81,9 +81,8 @@ def _check_working_hours(self, which: str) -> None: start = self.port.settings.workday_start end = self.port.settings.workday_end else: - raise ValueError( - "Can only set the workday settings from a 'port' or 'env'." - ) + msg = "Can only set the workday settings from a 'port' or 'env'." + raise ValueError(msg) self.settings._set_environment_shift( *check_working_hours( diff --git a/wombat/core/post_processor.py b/wombat/core/post_processor.py index 1a5cb1da..8d956c1a 100644 --- a/wombat/core/post_processor.py +++ b/wombat/core/post_processor.py @@ -422,9 +422,8 @@ def time_based_availability(self, frequency: str, by: str) -> pd.DataFrame: by = by.lower().strip() if by not in ("windfarm", "turbine", "electrolyzer"): - raise ValueError( - '`by` must be one of "windfarm", "turbine", or "electrolyzer".' - ) + msg = '`by` must be one of "windfarm", "turbine", or "electrolyzer".' + raise ValueError(msg) by_windfarm = by == "windfarm" by_electrolyzer = by == "electrolyzer" @@ -498,9 +497,8 @@ def production_based_availability(self, frequency: str, by: str) -> pd.DataFrame by = by.lower().strip() if by not in ("windfarm", "turbine", "electrolyzer"): - raise ValueError( - '`by` must be one of "windfarm", "turbine", or "electrolyzer".' - ) + msg = '`by` must be one of "windfarm", "turbine", or "electrolyzer".' + raise ValueError(msg) by_windfarm = by == "windfarm" by_electrolyzer = by == "electrolyzer" @@ -592,9 +590,8 @@ def capacity_factor(self, which: str, frequency: str, by: str) -> pd.DataFrame: by = by.lower().strip() if by not in ("windfarm", "turbine", "electrolyzer"): - raise ValueError( - '`by` must be one of "windfarm", "turbine", or "electrolyzer".' - ) + msg = '`by` must be one of "windfarm", "turbine", or "electrolyzer".' + raise ValueError(msg) by_windfarm = by == "windfarm" by_electrolyzer = by == "electrolyzer" @@ -665,9 +662,8 @@ def task_completion_rate(self, which: str, frequency: str) -> float | pd.DataFra """ which = which.lower().strip() if which not in ("scheduled", "unscheduled", "both"): - raise ValueError( - '``which`` must be one of "scheduled", "unscheduled", or "both".' - ) + msg = '``which`` must be one of "scheduled", "unscheduled", or "both".' + raise ValueError(msg) frequency = _check_frequency(frequency, which="all") @@ -972,10 +968,11 @@ def vessel_crew_hours_at_sea( raise ValueError("``by_equipment`` must be one of ``True`` or ``False``") if not isinstance(vessel_crew_assumption, dict): - raise ValueError( + msg = ( "`vessel_crew_assumption` must be a dictionary of vessel name (keys)" " and number of crew (values)" ) + raise ValueError(msg) # Filter by the at sea indicators and required columns at_sea = self.events @@ -1593,9 +1590,8 @@ def emissions( if missing := set(self.service_equipment_names).difference( [*emissions_factors] ): - raise KeyError( - f"`emissions_factors` is missing the following keys: {missing}" - ) + msg = f"`emissions_factors` is missing the following keys: {missing}" + raise KeyError(msg) valid_categories = ("transit", "maneuvering", "idle at port", "idle at site") emissions_categories = list( @@ -1606,10 +1602,11 @@ def emissions( len(set(valid_categories).difference(emissions_input.keys())) > 0 or len(set(emissions_input.values())) > 1 ): - raise KeyError( + msg = ( "Each servicing equipment's emissions factors must have inputs for:" f"{valid_categories}" ) + raise KeyError(msg) # Create the agent/duration subset equipment_usage = ( @@ -1909,9 +1906,8 @@ def project_fixed_costs(self, frequency: str, resolution: str) -> pd.DataFrame: resolution = resolution.lower().strip() if resolution not in ("low", "medium", "high"): - raise ValueError( - '``resolution`` must be one of "low", "medium", or "high".' - ) + msg = '``resolution`` must be one of "low", "medium", or "high".' + raise ValueError(msg) # Get the appropriate values and convert to the currency base keys = self.fixed_costs.resolution[resolution] diff --git a/wombat/core/repair_management.py b/wombat/core/repair_management.py index 0ec18129..f8c3b764 100644 --- a/wombat/core/repair_management.py +++ b/wombat/core/repair_management.py @@ -525,9 +525,8 @@ def invalidate_system( system.servicing_queue = self.env.event() self.invalid_systems.append(system.id) else: - raise RuntimeError( - f"{self.env.simulation_time} {system.id} already being serviced" - ) + msg = f"{self.env.simulation_time} {system.id} already being serviced" + raise RuntimeError(msg) if tow: self.systems_in_tow.append(system.id) _ = self.systems_waiting_for_tow.pop( @@ -577,9 +576,8 @@ def interrupt_system( system.servicing = self.env.event() self.reset_subassembly_processes(system, subassembly_full_reset) else: - raise RuntimeError( - f"{self.env.simulation_time} {system.id} already being serviced" - ) + msg = f"{self.env.simulation_time} {system.id} already being serviced" + raise RuntimeError(msg) def register_repair(self, repair: RepairRequest) -> Generator: """Registers the repair as complete with the repair managiner. @@ -615,10 +613,11 @@ def enable_requests_for_system( Set to True if this is for a tow-to-port request. """ if system.servicing.triggered: - raise RuntimeError( + msg = ( f"{self.env.simulation_time} Repairs were already completed" f" at {system.id}" ) + raise RuntimeError(msg) _ = self.invalid_systems.pop(self.invalid_systems.index(system.id)) if tow: _ = self.systems_in_tow.pop(self.systems_in_tow.index(system.id)) diff --git a/wombat/core/service_equipment.py b/wombat/core/service_equipment.py index 80498f49..ddf143b2 100644 --- a/wombat/core/service_equipment.py +++ b/wombat/core/service_equipment.py @@ -121,9 +121,8 @@ def validate_end_points(start: str, end: str, *, no_intrasite: bool = False) -> ``no_intrasite`` is set to ``True``. """ if start not in ("port", "site", "system"): - raise ValueError( - "``start`` location must be one of 'port', 'site', or 'system'!" - ) + msg = "``start`` location must be one of 'port', 'site', or 'system'!" + raise ValueError(msg) if end not in ("port", "site", "system"): raise ValueError("``end`` location must be one of 'port', 'site', or 'system'!") if no_intrasite and (start == end == "system"): @@ -823,10 +822,11 @@ def weather_delay(self, hours: int | float, **kwargs) -> Generator[Event, Any, A ``hours``. """ if hours < 0: - raise ValueError( + msg = ( f"`hours` must be greater than 0 for {self.name} to process" " a weather delay" ) + raise ValueError(msg) if hours == 0: return @@ -1334,9 +1334,8 @@ def mooring_connection( elif which == "reconnect": hours_to_process = self.settings.reconnection_hours else: - raise ValueError( - f"Only `unmoor` and `reconnect` are allowable inputs, not {which}" - ) + msg = f"Only `unmoor` and `reconnect` are allowable inputs, not {which}" + raise ValueError(msg) salary_cost = self.calculate_salary_cost(hours_to_process) hourly_cost = self.calculate_hourly_cost(hours_to_process) diff --git a/wombat/core/simulation_api.py b/wombat/core/simulation_api.py index 555c9e07..d69c4b45 100644 --- a/wombat/core/simulation_api.py +++ b/wombat/core/simulation_api.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math import datetime from copy import deepcopy from typing import TYPE_CHECKING @@ -269,10 +270,11 @@ def _create_configuration( if isinstance(value, Configuration): object.__setattr__(self, attribute.name, value) else: - raise TypeError( + msg = ( "``config`` must be a dictionary, valid file path to a yaml-enocoded", "dictionary, or ``Configuration`` object!", ) + raise TypeError(msg) @classmethod def from_config( @@ -313,9 +315,8 @@ def from_config( if isinstance(config, dict): config = Configuration.from_dict(config) if not isinstance(config, Configuration): - raise TypeError( - "``config`` must be a dictionary or ``Configuration`` object!" - ) + msg = "``config`` must be a dictionary or ``Configuration`` object!" + raise TypeError(msg) if TYPE_CHECKING: assert isinstance(config, Configuration) # mypy helper return cls( # type: ignore @@ -431,18 +432,21 @@ def _setup_simulation(self): for service_equipment in tugboats: name = service_equipment.name # type: ignore if name in self.service_equipment: - raise ValueError( + msg = ( f"Servicing equipment `{name}` already exists, please use" " unique names for all servicing equipment." ) + raise ValueError(msg) self.service_equipment[name] = service_equipment # type: ignore - if self.config.project_capacity * 1000 != round(self.windfarm.capacity, 6): - raise ValueError( + _project_capacity = self.config.project_capacity * 1000 + if not math.isclose(_project_capacity, self.windfarm.capacity, abs_tol=1e-6): + msg = ( f"Input `project_capacity`: {self.config.project_capacity:,.6f} MW is" f" not equal to the sum of turbine capacities:" f" {self.windfarm.capacity / 1000:,.6f} MW" ) + raise ValueError(msg) def run( self, diff --git a/wombat/windfarm/system/system.py b/wombat/windfarm/system/system.py index d69c88b2..4a84ced3 100644 --- a/wombat/windfarm/system/system.py +++ b/wombat/windfarm/system/system.py @@ -160,10 +160,11 @@ def _create_subassemblies(self, subassembly_data: dict) -> None: self.subassemblies.append(getattr(self, name)) if self.subassemblies == []: - raise ValueError( + msg = ( "At least one subassembly definition requred for ", f"ID: {self.id}, Name: {self.name}.", ) + raise ValueError(msg) self.env.log_action( agent=self.name, diff --git a/wombat/windfarm/windfarm.py b/wombat/windfarm/windfarm.py index b73c93b2..9a64f8b0 100644 --- a/wombat/windfarm/windfarm.py +++ b/wombat/windfarm/windfarm.py @@ -233,10 +233,11 @@ def _create_systems(self) -> None: name = data["subassembly"] node_type = data["type"] if name == "": - raise ValueError( + msg = ( "A 'subassembly' file must be specified for all nodes in the" " windfarm layout!" ) + raise ValueError(msg) # Read in unique system configuration files only once, and reference # the existing dictionary when possible to reduce I/O @@ -253,9 +254,8 @@ def _create_systems(self) -> None: ) raise ValueError(msg) if (subassembly_dict := self._inputs[node_type].get(name)) is None: # type: ignore - raise KeyError( - f"No configuration provided for {node_type}: {name}" - ) + msg = f"No configuration provided for {node_type}: {name}" + raise KeyError(msg) self.configs[node_type][name] = subassembly_dict # Create the turbine or substation simulation object @@ -286,10 +286,11 @@ def _create_cables(self) -> None: # Check that the cable data is provided if name == "": - raise ValueError( + msg = ( "An 'upstream_cable' file must be specified for all nodes in the" " windfarm layout!" ) + raise ValueError(msg) # Read in unique cable configuration files once to reduce I/O if (cable_dict := self.configs["cable"].get(name)) is None: