Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f6b1c18
add initial prototype for weibull reliability
RHammond2 Aug 10, 2026
19e3e1c
simplify relibaility model non openmdao model
RHammond2 Aug 11, 2026
6bcd000
Merge branch 'develop' into feature/reliability
RHammond2 Aug 11, 2026
fccb5bd
add model retrieval and remove extrasfrom baseclasses
RHammond2 Aug 11, 2026
6340023
Merge branch 'feature/reliability' of https://github.com/RHammond2/H2…
RHammond2 Aug 11, 2026
e02b34d
Merge branch 'develop' into feature/reliability
RHammond2 Aug 12, 2026
58bdff9
make downtime and reliability separate models
RHammond2 Aug 17, 2026
4283ab5
update sampling and fix interval availability
RHammond2 Aug 18, 2026
f5e61da
update downtime for matrix and generation helpers
RHammond2 Aug 19, 2026
dce308e
make weibull model definiton multi-component-compatible
RHammond2 Aug 19, 2026
2463e78
getting setup partially working
RHammond2 Aug 19, 2026
efaf665
update docstrings and add fixed downtime
RHammond2 Aug 19, 2026
e4bfcb8
fix small bugs and ensure matrix works for weibull
RHammond2 Aug 19, 2026
6388656
system availability
RHammond2 Aug 19, 2026
081386b
move calculate_availability to base and simplify broadcasting
RHammond2 Aug 19, 2026
e5cbdd9
convert fixexd interval to componentizable model and update n_compone…
RHammond2 Aug 20, 2026
4e326bb
update fixed downtime
RHammond2 Aug 20, 2026
b3544be
update lognormal downtime model
RHammond2 Aug 20, 2026
c809904
add burn-in and fix bugs in n_components setting
RHammond2 Aug 20, 2026
2bde5e3
update use_reliability
RHammond2 Aug 20, 2026
1d9112f
merge develop and fix conflicts
RHammond2 Aug 20, 2026
33f80a3
add validator for array variation of validators.ge
RHammond2 Aug 20, 2026
8568a2c
standardize dimensionality update
RHammond2 Aug 20, 2026
7e9d473
minor reorg
RHammond2 Aug 20, 2026
b4a0ff5
add missing return
RHammond2 Aug 20, 2026
40ac1c9
add shape checking validator
RHammond2 Aug 20, 2026
823ea19
remove extra shape check
RHammond2 Aug 20, 2026
ff76626
Merge branch 'develop' into feature/reliability
RHammond2 Aug 21, 2026
6850587
move downtime event definition to base class and add array_gt
RHammond2 Aug 21, 2026
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
11 changes: 11 additions & 0 deletions h2integrate/converters/natural_gas/natural_gas_cc_ct.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from h2integrate.core.utilities import BaseConfig, merge_shared_inputs
from h2integrate.core.validators import gt_zero, gte_zero
from h2integrate.reliability.models import WeibullReliabilityModel
from h2integrate.core.model_baseclasses import (
CostModelBaseClass,
CostModelBaseConfig,
Expand Down Expand Up @@ -67,6 +68,8 @@ def initialize(self):
self.commodity = "electricity"
self.commodity_rate_units = "MW"
self.commodity_amount_units = "MW*h"
self.reliability_model = None
self.use_reliability = False

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these be parameters in the NaturalGasPerformanceConfig? So a user can input reliability_model and use_reliability? Where the __attrs_post_init__ checks that reliability_model is provided if use_reliability is True?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good question, it didn't dawn on me that a user could want to provide a definition, but not use it. Though it could be easier to iterate on a problem by simply turning it on/off instead of commenting out a whole section of the inputs. I'll also add this to the to do section.


def setup(self):
super().setup()
Expand All @@ -75,6 +78,12 @@ def setup(self):
merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"),
additional_cls_name=self.__class__.__name__,
)
if self.options["tech_config"]["model_inputs"]["reliability"]:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to use self.options["tech_config"]["model_inputs"].get("reliability", False) here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not exactly, but that did get me thinking on a better way to use use_reliability, so I appreciate the question!

self.reliability_model = WeibullReliabilityModel.from_dict(
merge_shared_inputs(self.options["tech_config"]["model_inputs"], "reliability"),
additional_cls_name=self.__class__.__name__,
)
self.use_reliability = True

# Add natural gas consumed output
self.add_output(
Expand Down Expand Up @@ -154,6 +163,8 @@ def compute(self, inputs, outputs):
inputs["electricity_command_value"],
)
natural_gas_demand = electricity_command_value * heat_rate_mmbtu_per_mwh
if self.use_reliability:
natural_gas_demand * self.reliability_model.availability

# available feedstock, saturated at maximum system feedstock consumption
natural_gas_available = np.where(
Expand Down
Empty file.
94 changes: 94 additions & 0 deletions h2integrate/reliability/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import numpy as np
from attrs import field, define, validators

from h2integrate.core.utilities import BaseConfig


N_TIMESTEPS = 8760


def create_reliability(config: dict):
"""Retrieves and initializes a matching reliability model."""
name = config.pop("reliability")
match name:
case "WeibullReliabilityModel":
return WeibullReliabilityModel.from_dict(config)
case _:
raise NotImplementedError(f"{name} is not a valid model name")


@define(kw_only=True)
class WeibullReliabilityModel(BaseConfig):
r"""Basic reliability model for operating/not operating statuses.

Assumes a full operational shutdown with zero ramping of production for an hourly, 1 year
simulation.

Args:
scale (float): Also referred to as :math:`\lambda` or :math:`\alpha`. Determines
the scale of distribution, and is equivalent to the mean time
between failure in years (MTBF), or 1 / annual failure rate.
shape (float): Also referred to as ``k`` or :math:`\beta`. A value less than 1
corresponds to a decreasing hazard rate over time (break-in period failures);
a value greater than 1 corresponds to an increasing hazard rate over time (
aging/wear-out failures); and a value of 1 corresponds to a constant hazard
rate over time (exponential distribution).
downtime (float): Average amount of downtime per failure.

Attributes:
rng (np.random._generator.Generator): NumPy random generator object.

TODO:
- how to pass n_timesteps through from plant?
- stabilize random generator/determine how to manage random seeding across library
"""

scale: float = field(validator=validators.instance_of(float))
shape: float = field(validator=validators.instance_of(float))
downtime: float = field(validator=validators.gt(1))
rng: np.random._generator.Generator = field(
default=np.random.default_rng(),
init=False,
validator=validators.instance_of(np.random._generator.Generator),
)
availability: np.ndarray = field(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we make n_timesteps a configuration parameter? Then we could have availability be initialized in __attrs_post_init__ as np.ones(n_timesteps)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generation of availability is now done in calculate_availability to make it easier to automate, and adding the n_timesteps to the to do section.

default=np.ones(N_TIMESTEPS), init=False, validator=validators.instance_of(np.ndarray)
)
downtime_per_event: np.ndarray = field(
default=np.zeros(N_TIMESTEPS), init=False, validator=validators.instance_of(np.ndarray)
)

def __attrs_post_init__(self):
self.create_downtime_events()
self.calculate_availability()

def create_downtime_events(self):
"""Creates a ``time_to_failure`` and ``downtime_per_event`` array based on the distributions
described in ``WeibullReliabilityConfig``.
"""
# NOTE: Arrays are default length 30 to ensure enough events are created for a 1-year
# simulation without burdening the memory usage.
self.time_to_failures = np.ceil(
self.config.rng.weibul(self.config.shape, size=30) * self.config.scale * N_TIMESTEPS
).astype(int)
downtime_per_event = np.ceil(np.rng.normal(loc=self.config.downtime, size=30)).astype(int)
self.downtime_per_event = np.where(downtime_per_event >= 1, downtime_per_event, 1)

def calculate_availability(self):
"""Determine the timing and duration of outages for a single year of simulation time."""
accumulated = 0
Comment thread
RHammond2 marked this conversation as resolved.
Outdated
while accumulated < N_TIMESTEPS:
event, self.time_to_failures = self.time_to_failures[0], self.time_to_failures[1:]
duration, self.downtime_per_event = (
self.downtime_per_event[0],
self.downtime_per_event[1:],
)
if event + accumulated > N_TIMESTEPS:
break

start = accumulated + event
end = start + duration
self.availability[start:end] = 0
accumulated = start
if not self.time_to_failures:
self.create_downtime_events()
Loading