Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Enable `PySAMWindPlantPerformanceModel` to accept more than 300 turbines by overriding the default maximum in the PySAM model. [PR 831](https://github.com/NatLabRockies/H2Integrate/pull/831)
- Add `PySAMWavePerformanceModel` and `WaveResource` to wrap PySAM MhkWave as an H2I performance model, replacing the HOPP wave module in example 09. [PR 825](https://github.com/NatLabRockies/H2Integrate/pull/825)
- Replace HOPP with native H2I wind, solar, and battery models in example 11. Adds `percent_load_missed` and `curtailment_percent` outputs to `DemandComponentBase`, allows zero capacity in wind/solar/battery performance models. [PR 826](https://github.com/NatLabRockies/H2Integrate/pull/826)
- Replaces all custom attrs validators in `h2integrate.core.validators` with built in attrs validators. [PR 835](https://github.com/NatLabRockies/H2Integrate/pull/835)

## 0.9 [August 10, 2026]

Expand Down
31 changes: 17 additions & 14 deletions docs/developer_guide/technology_model_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ Here is an example of a solar PV performance model.

```python
import PySAM.Pvwattsv8 as Pvwatts
from attrs import field, define
from attrs import field, define, validators

from h2integrate.core.utilities import BaseConfig, merge_shared_inputs
from h2integrate.core.validators import contains, range_val_or_none
from h2integrate.converters.solar.solar_baseclass import SolarPerformanceBaseClass


Expand All @@ -29,11 +28,17 @@ class PYSAMSolarPlantPerformanceModelDesignConfig(BaseConfig):
"""

pv_capacity_kWdc: float = field()
dc_ac_ratio: float = field(default=None, validator=range_val_or_none(0.0, 2.0))
tilt: float = field(default=None, validator=range_val_or_none(0.0, 90.0))
dc_ac_ratio: float = field(
default=None,
validator=validators.optional((validators.ge(0), validators.le(2)))
)
tilt: float = field(
default=None,
validator=validators.optional((validators.ge(0), validators.le(90)))
)
config_name: str = field(
default="PVWattsSingleOwner",
validator=contains(["PVWattsSingleOwner", "PVWattsCommercial"]), # truncated
validator=validators.in_(["PVWattsSingleOwner", "PVWattsCommercial"]), # truncated
)


Expand Down Expand Up @@ -107,21 +112,20 @@ model always reports costs in a fixed dollar-year), inherit the config from
`BaseConfig` and pin `cost_year` to a constant:

```python
from attrs import field, define
from attrs import field, define, validators

from h2integrate.core.utilities import BaseConfig, merge_shared_inputs
from h2integrate.core.validators import gt_zero, must_equal
from h2integrate.core.model_baseclasses import CostModelBaseClass


@define(kw_only=True)
class ReverseOsmosisCostModelConfig(BaseConfig):
# Config values come from tech_config['model_inputs']['cost_parameters']
# or tech_config['model_inputs']['shared_parameters'].
freshwater_kg_per_hour: float = field(validator=gt_zero)
freshwater_density: float = field(validator=gt_zero)
freshwater_kg_per_hour: float = field(validator=validators.gt(0))
freshwater_density: float = field(validator=validators.gt(0))
# cost_year is fixed because this model always reports 2013 USD.
cost_year: int = field(default=2013, converter=int, validator=must_equal(2013))
cost_year: int = field(default=2013, converter=int, validator=validators.in_([2013]))


class ReverseOsmosisCostModel(CostModelBaseClass):
Expand All @@ -148,17 +152,16 @@ inherit the config from `CostModelBaseConfig` instead. `CostModelBaseConfig`
adds a required `cost_year` field, forcing the user to supply it:

```python
from attrs import field, define
from attrs import field, define, validators

from h2integrate.core.utilities import merge_shared_inputs
from h2integrate.core.validators import gt_zero
from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig


@define(kw_only=True)
class ATBUtilityPVCostModelConfig(CostModelBaseConfig):
capex_per_kWac: float | int = field(validator=gt_zero)
opex_per_kWac_per_year: float | int = field(validator=gt_zero)
capex_per_kWac: float | int = field(validator=validators.gt(0))
opex_per_kWac_per_year: float | int = field(validator=validators.gt(0))
# ``cost_year`` is inherited from CostModelBaseConfig and is user-provided.


Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import numpy as np
import openmdao.api as om
from attrs import field, define
from attrs import field, define, validators

from h2integrate.core.utilities import BaseConfig
from h2integrate.core.validators import gt_zero, range_val


@define(kw_only=True)
class SimpleLCOFinanceConfig(BaseConfig):
discount_rate: float = field(validator=range_val(0, 1))
plant_life: int = field(converter=int, validator=gt_zero)
discount_rate: float = field(validator=(validators.ge(0), validators.le(1)))
plant_life: int = field(converter=int, validator=validators.gt(0))


class SimpleLCOFinance(om.ExplicitComponent):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import pyomo.environ as pyo
from attrs import field, define
from attrs import field, define, validators
from pyomo.network import Port

from h2integrate.core.utilities import merge_shared_inputs
from h2integrate.core.validators import gt_zero, range_val
from h2integrate.control.control_rules.pyomo_rule_baseclass import (
PyomoRuleBaseClass,
PyomoRuleBaseConfig,
Expand All @@ -12,13 +11,15 @@

@define(kw_only=True)
class PyomoStorageRuleBaseConfig(PyomoRuleBaseConfig):
max_capacity: float = field(validator=gt_zero)
max_capacity: float = field(validator=validators.gt(0))

min_soc_fraction: float = field(default=0.1, validator=range_val(0, 1))
max_soc_fraction: float = field(default=0.9, validator=range_val(0, 1))
min_soc_fraction: float = field(default=0.1, validator=(validators.ge(0), validators.le(1)))
max_soc_fraction: float = field(default=0.9, validator=(validators.ge(0), validators.le(1)))

charge_efficiency: float = field(default=0.938, validator=range_val(0, 1))
discharge_efficiency: float = field(default=0.938, validator=range_val(0, 1))
charge_efficiency: float = field(default=0.938, validator=(validators.ge(0), validators.le(1)))
discharge_efficiency: float = field(
default=0.938, validator=(validators.ge(0), validators.le(1))
)


class PyomoRuleStorageBaseclass(PyomoRuleBaseClass):
Expand Down
29 changes: 21 additions & 8 deletions h2integrate/control/control_strategies/openloop_control_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from attrs import field, define, validators

from h2integrate.core.utilities import BaseConfig
from h2integrate.core.validators import gte_zero, range_val_or_none


@define(kw_only=True)
Expand Down Expand Up @@ -64,15 +63,29 @@ class OpenLoopControlBaseConfig(BaseConfig):
require_storage_parameters: ClassVar[bool] = False

max_capacity: float | None = field(default=None)
max_soc_fraction: float | None = field(default=None, validator=range_val_or_none(0, 1))
min_soc_fraction: float | None = field(default=None, validator=range_val_or_none(0, 1))
init_soc_fraction: float | None = field(default=None, validator=range_val_or_none(0, 1))
max_charge_rate: float | None = field(default=None, validator=validators.optional(gte_zero))
max_soc_fraction: float | None = field(
default=None, validator=validators.optional((validators.ge(0), validators.le(1)))
)
min_soc_fraction: float | None = field(
default=None, validator=validators.optional((validators.ge(0), validators.le(1)))
)
init_soc_fraction: float | None = field(
default=None, validator=validators.optional((validators.ge(0), validators.le(1)))
)
max_charge_rate: float | None = field(
default=None, validator=validators.optional(validators.ge(0))
)
charge_equals_discharge: bool = field(default=True)
max_discharge_rate: float | None = field(default=None)
charge_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1))
discharge_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1))
round_trip_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1))
charge_efficiency: float | None = field(
default=None, validator=validators.optional((validators.ge(0), validators.le(1)))
)
discharge_efficiency: float | None = field(
default=None, validator=validators.optional((validators.ge(0), validators.le(1)))
)
round_trip_efficiency: float | None = field(
default=None, validator=validators.optional((validators.ge(0), validators.le(1)))
)

def __attrs_post_init__(self):
if self.commodity_amount_units is None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@
import numpy as np
import openmdao.api as om
import pyomo.environ as pyomo
from attrs import field, define
from attrs import field, define, validators

from h2integrate.core.utilities import BaseConfig
from h2integrate.core.validators import range_val


if TYPE_CHECKING: # to avoid circular imports
Expand Down Expand Up @@ -55,9 +54,9 @@ class PyomoStorageControllerBaseConfig(BaseConfig):
"""

max_capacity: float = field()
max_soc_fraction: float = field(validator=range_val(0, 1))
min_soc_fraction: float = field(validator=range_val(0, 1))
init_soc_fraction: float = field(validator=range_val(0, 1))
max_soc_fraction: float = field(validator=(validators.ge(0), validators.le(1)))
min_soc_fraction: float = field(validator=(validators.ge(0), validators.le(1)))
init_soc_fraction: float = field(validator=(validators.ge(0), validators.le(1)))
n_control_window_hours: int = field()
commodity: str = field()
commodity_rate_units: str = field()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@

import numpy as np
import pyomo.environ as pyomo
from attrs import field, define
from attrs import field, define, validators

from h2integrate.core.utilities import merge_shared_inputs
from h2integrate.core.validators import range_val_or_none
from h2integrate.control.control_strategies.pyomo_storage_controller_baseclass import (
PyomoStorageControllerBaseClass,
PyomoStorageControllerBaseConfig,
Expand Down Expand Up @@ -33,9 +32,15 @@ class HeuristicLoadFollowingStorageControllerConfig(PyomoStorageControllerBaseCo
provided.
"""

charge_efficiency: float = field(validator=range_val_or_none(0, 1), default=None)
discharge_efficiency: float = field(validator=range_val_or_none(0, 1), default=None)
round_trip_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1))
charge_efficiency: float = field(
validator=validators.optional((validators.ge(0), validators.le(1))), default=None
)
discharge_efficiency: float = field(
validator=validators.optional((validators.ge(0), validators.le(1))), default=None
)
round_trip_efficiency: float | None = field(
default=None, validator=validators.optional((validators.ge(0), validators.le(1)))
)

def __attrs_post_init__(self):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@

import numpy as np
import pyomo.environ as pyomo
from attrs import field, define
from attrs import field, define, validators
from pyomo.util.check_units import assert_units_consistent

from h2integrate.core.utilities import merge_shared_inputs
from h2integrate.core.validators import range_val
from h2integrate.control.control_rules.plant_dispatch_model import PyomoDispatchPlantModel
from h2integrate.control.control_strategies.controller_opt_problem_state import DispatchProblemState
from h2integrate.control.control_strategies.pyomo_storage_controller_baseclass import (
Expand Down Expand Up @@ -64,13 +63,17 @@ class OptimizedDispatchStorageControllerConfig(PyomoStorageControllerBaseConfig)
"""

max_charge_rate: int | float = field()
charge_efficiency: float = field(validator=range_val(0, 1), default=None)
discharge_efficiency: float = field(validator=range_val(0, 1), default=None)
charge_efficiency: float = field(validator=(validators.ge(0), validators.le(1)), default=None)
discharge_efficiency: float = field(
validator=(validators.ge(0), validators.le(1)), default=None
)
cost_per_production: float = field(default=None)
cost_per_charge: float = field(default=None)
cost_per_discharge: float = field(default=None)
commodity_met_value: float = field(default=None)
time_weighting_factor: float = field(validator=range_val(0, 1), default=0.995)
time_weighting_factor: float = field(
validator=(validators.ge(0), validators.le(1)), default=0.995
)
time_duration: float = field(default=1.0) # hours

def make_dispatch_inputs(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

import numpy as np
import pandas as pd
from attrs import field, define
from attrs import field, define, validators

from h2integrate.core.utilities import merge_shared_inputs, build_time_series_from_plant_config
from h2integrate.core.validators import contains, has_required_keys
from h2integrate.control.control_strategies.openloop_control_base import (
OpenLoopControlBase,
OpenLoopControlBaseConfig,
Expand Down Expand Up @@ -61,15 +60,51 @@ class PeakLoadManagementHeuristicOpenLoopStorageControllerConfig(OpenLoopControl

demand_profile_upstream: int | float | list | None = field()
dispatch_priority_demand_profile: str = field(
validator=contains(["demand_profile", "demand_profile_upstream"]),
validator=validators.in_(["demand_profile", "demand_profile_upstream"]),
)
n_override_events: int | None = field(default=None)
override_events_period: int | str | None = field(default=None)
peak_range: dict = field(validator=has_required_keys(["start", "end"]))
advance_discharge_period: dict = field(validator=has_required_keys(["units", "val"]))
delay_charge_period: dict = field(validator=has_required_keys(["units", "val"]))
peak_range: dict = field(
validator=validators.deep_mapping(
key_validator=validators.in_(["start", "end"]),
mapping_validator=(
validators.instance_of(dict),
validators.min_len(2),
validators.max_len(2),
),
)
)
advance_discharge_period: dict = field(
validator=validators.deep_mapping(
key_validator=validators.in_(["units", "val"]),
mapping_validator=(
validators.instance_of(dict),
validators.min_len(2),
validators.max_len(2),
),
)
)
delay_charge_period: dict = field(
validator=validators.deep_mapping(
key_validator=validators.in_(["units", "val"]),
mapping_validator=(
validators.instance_of(dict),
validators.min_len(2),
validators.max_len(2),
),
)
)
allow_charge_in_peak_range: bool = field(default=True)
min_peak_proximity: dict = field(validator=has_required_keys(["units", "val"]))
min_peak_proximity: dict = field(
validator=validators.deep_mapping(
key_validator=validators.in_(["units", "val"]),
mapping_validator=(
validators.instance_of(dict),
validators.min_len(2),
validators.max_len(2),
),
)
)

def __attrs_post_init__(self):
super().__attrs_post_init__()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@
import numpy as np
import pandas as pd
import pyomo.environ as pyomo
from attrs import field, define
from attrs import field, define, validators
from pyomo.opt import SolverStatus, TerminationCondition

from h2integrate.core.utilities import merge_shared_inputs, build_time_series_from_plant_config
from h2integrate.core.validators import range_val
from h2integrate.control.control_strategies.controller_opt_problem_state import DispatchProblemState
from h2integrate.control.control_strategies.pyomo_storage_controller_baseclass import (
SolverOptions,
Expand Down Expand Up @@ -79,11 +78,13 @@ class PeakLoadManagementOptimizedControllerConfig(PyomoStorageControllerBaseConf
peak_window: dict = field()
performance_incentive: float = field(default=None)
performance_incentive_per_event: float = field(default=None)
charge_efficiency: float = field(validator=range_val(0, 1), default=1.0)
discharge_efficiency: float = field(validator=range_val(0, 1), default=1.0)
charge_efficiency: float = field(validator=(validators.ge(0), validators.le(1)), default=1.0)
discharge_efficiency: float = field(validator=(validators.ge(0), validators.le(1)), default=1.0)
n_max_events: int = field(default=10)
n_control_window_hours: float = field(default=24.0)
signal_threshold_percentile: float = field(default=0.0, validator=range_val(0, 100))
signal_threshold_percentile: float = field(
default=0.0, validator=(validators.ge(0), validators.le(100))
)
event_duration: dict = field(default=None)
min_peak_separation: dict = field(default=None)

Expand Down
Loading
Loading