Skip to content
Merged
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
20 changes: 20 additions & 0 deletions electrolyzer/connectors/degradation_combiner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import openmdao.api as om


class CombineDegradation(om.ExplicitComponent):
"""Combine cell voltage with the cell degradation"""

def initialize(self):
pass

def setup(self):
self.add_input("V_cell_deg", val=0.0, shape_by_conn=True, units="V")
self.add_input("V_cell", val=0.0, copy_shape="V_cell_deg", units="V")
self.add_input("I_actual", val=0.0, copy_shape="V_cell_deg", units="A")

self.add_output("V_cell_total", val=0.0, copy_shape="V_cell_deg", units="V")
self.add_output("P_cell_total", val=0.0, copy_shape="V_cell_deg", units="W")

def compute(self, inputs, outputs):
outputs["V_cell_total"] = inputs["V_cell_deg"] + inputs["V_cell"]
outputs["P_cell_total"] = inputs["I_actual"] * outputs["V_cell_total"]
4 changes: 2 additions & 2 deletions electrolyzer/connectors/series_scalar.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def setup(self):


class SplitAcrossSerialComponents(GenericSeriesConverter):
"""Scale power down"""
"""Split values across components connected in series"""

def compute(self, inputs, outputs):
for o_name in outputs.keys():
Expand All @@ -49,7 +49,7 @@ def compute(self, inputs, outputs):


class CombineSerialComponents(GenericSeriesConverter):
"""Scale power down"""
"""Add values across components connected in series"""

def setup(self):
super().setup()
Expand Down
45 changes: 44 additions & 1 deletion electrolyzer/core/bert.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from electrolyzer.connectors.series_scalar import (
CombineSerialComponents, # , SplitAcrossSerialComponents
)
from electrolyzer.connectors.degradation_combiner import CombineDegradation
from electrolyzer.components.cell.cell_design_params import get_cell_params_for_model
from electrolyzer.components.classifiers.cell_classifier import CellClassification

Expand Down Expand Up @@ -137,7 +138,8 @@ def create_components(self):
"classifier", cluster_classifier, promotes=["I_min", "I_max", "n_cells", "n_stacks"]
)
cluster_group.add_subsystem("translator", translator, promotes=["n_stacks", "n_cells"])
cluster_group.add_subsystem("simulation", simulator, promotes=promotion_vars)
sim_prom_vars = [*promotion_vars, "n_stacks", "n_cells"]
cluster_group.add_subsystem("simulation", simulator, promotes=sim_prom_vars)

# simulation.dynamics gets I_min and I_max from the converter outputs
# Connect the converter bounds to the
Expand Down Expand Up @@ -172,10 +174,29 @@ def create_cluster_simulation_block(self, cell_design_params):
degradation = self.create_component("degradation")
dynamics = self.create_component("dynamics")

cell_scale_up = CombineSerialComponents(
scaling_component="cells", n_comps=self.config["stack"]["n_cells"]
)
stack_scale_up = CombineSerialComponents(
scaling_component="stacks", n_comps=self.config["cluster"]["n_stacks"]
)
degradation_combiner = CombineDegradation()

simulation.add_subsystem("dynamics", dynamics, promotes=["I_min", "I_max"])
simulation.add_subsystem("cell_nominal", cell_nom, promotes=cell_design_params)
simulation.add_subsystem("degradation", degradation)
simulation.add_subsystem("cell_real", cell_real, promotes=cell_design_params)
simulation.add_subsystem("degradation_combiner", degradation_combiner)
simulation.add_subsystem("scale_cell_to_stack", cell_scale_up, promotes_inputs=["n_cells"])

scale_up_base_vars = ["J", "P", "H2", "O2", "V"] # todo: add in I?
cluster_out_prom_vars = [(f"{v}_out", f"Cluster_{v}") for v in scale_up_base_vars]
simulation.add_subsystem(
"scale_stack_to_cluster",
stack_scale_up,
promotes_inputs=["n_stacks"],
promotes_outputs=cluster_out_prom_vars,
)

# connect dynamics current output to nominal cell current input
simulation.connect("dynamics.I_out", "cell_nominal.I_in")
Expand All @@ -187,6 +208,28 @@ def create_cluster_simulation_block(self, cell_design_params):
simulation.connect("cell_nominal.V_cell_out", "degradation.V_cell_nominal")
# connect the degraded current to the cell voltage
simulation.connect("degradation.I_actual", "cell_real.I_in")

# combine the degradation results at the cell level
simulation.connect("cell_real.V_cell_out", "degradation_combiner.V_cell")
simulation.connect("degradation.V_cell_degraded", "degradation_combiner.V_cell_deg")
simulation.connect("degradation.I_actual", "degradation_combiner.I_actual")

# connect the outputs from the real cell to the cell scale-up components
simulation.connect("cell_real.J_out", "scale_cell_to_stack.J_in")
simulation.connect("degradation.I_actual", "scale_cell_to_stack.I_in")
# TODO: in the future, add a losses component and connect the outputs from that to the scale-up
simulation.connect("cell_real.H2_cell_out", "scale_cell_to_stack.H2_in")
simulation.connect("cell_real.O2_cell_out", "scale_cell_to_stack.O2_in")

# connect the power and voltage from the degradation combiner to the scale-up components
simulation.connect("degradation_combiner.V_cell_total", "scale_cell_to_stack.V_in")
simulation.connect("degradation_combiner.P_cell_total", "scale_cell_to_stack.P_in")

# Scale up from stack to cluster level

for var in scale_up_base_vars:
simulation.connect(f"scale_cell_to_stack.{var}_out", f"scale_stack_to_cluster.{var}_in")

return simulation

def create_controller_translator(self):
Expand Down
46 changes: 46 additions & 0 deletions electrolyzer/test/test_om_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,49 @@ def test_example_00_no_controller(subtests):
pytest.approx(7.491416242830744, rel=1e-6)
== bert.model.get_val("Cluster0.classifier.H2_max", units="kg/h")[0]
)

with subtests.test("Cluster H2 Production"):
cell_h2 = bert.model.get_val(
"Cluster0.simulation.cell_real.H2_cell_out", units="kg/h"
).sum()

assert (
pytest.approx(cell_h2 * scale_fac, rel=1e-6)
== bert.model.get_val("Cluster0.simulation.Cluster_H2", units="kg/h").sum()
)
with subtests.test("Cluster H2 Production (value)"):
assert (
pytest.approx(81.49985544233095, rel=1e-6)
== bert.model.get_val("Cluster0.simulation.Cluster_H2", units="kg/h").sum()
)

with subtests.test("Cluster Voltage"):
cell_voltage = bert.model.get_val(
"Cluster0.simulation.degradation_combiner.V_cell_total", units="V"
)[-1]

assert (
pytest.approx(cell_voltage * scale_fac, rel=1e-6)
== bert.model.get_val("Cluster0.simulation.Cluster_V", units="V")[-1]
)
with subtests.test("Cluster Voltage (value)"):
cell_voltage = bert.model.get_val(
"Cluster0.simulation.degradation_combiner.V_cell_total", units="V"
)[-1]

assert (
pytest.approx(227.1918107704954, rel=1e-6)
== bert.model.get_val("Cluster0.simulation.Cluster_V", units="V")[-1]
)

with subtests.test("Degradation power"):
V_cell_bol = bert.model.get_val("Cluster0.simulation.cell_nominal.V_cell_out", units="V")
V_cell_deg = bert.model.get_val(
"Cluster0.simulation.degradation.V_cell_degraded", units="V"
)
I_deg = bert.model.get_val("Cluster0.simulation.degradation.I_actual", units="A")
I_bol = bert.model.get_val("Cluster0.simulation.degradation.I_in", units="A")
P_cell_bol = I_bol * V_cell_bol
P_cell_deg = I_deg * (V_cell_deg + V_cell_bol)
assert np.allclose(P_cell_bol, P_cell_deg)
assert np.allclose(P_cell_bol * scale_fac, P_cell_deg * scale_fac)
Loading