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/components/cell/cell_design_params.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
cell_base_design_params = {
"A_cell",
"membrane_thickness",
"operating_temperature",
"anode_pressure",
"cathode_pressure",
"R_elec",
}

params_per_cell = {
"PEMCell": {"i_0a", "i_0c", "alpha_a", "alpha_c", "b_combined", "i_0combined", "f1", "f2"},
}


def get_cell_params_for_model(cell_model_name: str | None) -> list[str]:
if cell_model_name is None:
raise ValueError(f"{cell_model_name} was not input")
if cell_model_name not in params_per_cell:
raise ValueError(f"{cell_model_name} is not a valid cell model")
return list(params_per_cell.get(cell_model_name) | cell_base_design_params)
9 changes: 6 additions & 3 deletions electrolyzer/components/cell/pem_cell.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,20 @@ def setup(self):

super().setup()

# self.add_input("H2_demand", val=0.0, shape_by_conn=True, units="g/s")
# self.add_output("I_demand", val=0.0, copy_shape="H2_demand", units="A")
# self.add_output("J_demand", val=0.0, copy_shape="H2_demand", units="A/(cm**2)")
# Design parameters
if self.config.kinetics_method == "per_electrode":
self.add_input("i_0a", val=self.config.i_0a, shape=1, units="A/(cm**2)")
self.add_input("i_0c", val=self.config.i_0c, shape=1, units="A/(cm**2)")
self.add_input("alpha_a", val=self.config.alpha_a, shape=1, units="unitless")
self.add_input("alpha_c", val=self.config.alpha_c, shape=1, units="unitless")
self.add_input("b_combined", val=0, shape=1, units="V")
self.add_input("i_0combined", val=0, shape=1, units="A/(cm**2)")
else:
# b_combined is in V/decade
self.add_input("i_0a", val=0, shape=1, units="A/(cm**2)")
self.add_input("i_0c", val=0, shape=1, units="A/(cm**2)")
self.add_input("alpha_a", val=0, shape=1, units="unitless")
self.add_input("alpha_c", val=0, shape=1, units="unitless")
self.add_input("b_combined", val=self.config.b_combined, shape=1, units="V")
self.add_input("i_0combined", val=self.config.i0_combined, shape=1, units="A/(cm**2)")

Expand Down
53 changes: 38 additions & 15 deletions electrolyzer/core/bert.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from electrolyzer.core.file_utils import load_yaml
from electrolyzer.core.supported_models import supported_models
from electrolyzer.components.cell.cell_design_params import get_cell_params_for_model


class State(IntEnum):
Expand Down Expand Up @@ -43,6 +44,18 @@ def load_config(self, config_input):
self.plant_config = {"simulation": simulation_config}
self.n_clusters = system_config["n_clusters"]
self.control_var = system_config["control_variable"]
if "cell" in config:
# clusters have identical cell models
self.identical_cells = True
else:
self.identical_cells = False
msg = (
"The ability to have clusters with different cell designs is not yet "
"available. Please ensure your config has a ``cell`` section with "
"the cell model and design parameters"
)
raise NotImplementedError(msg)

self.config = config
if self.control_var == "power":
self.control_passed_var = "P"
Expand Down Expand Up @@ -84,24 +97,34 @@ def create_controller(self):

def create_components(self):
#
# Step 0: Create cluster groups

# Get the design parameters of the cell
cell_design_params = get_cell_params_for_model(self.config["cell"].get("model", None))

# Step 1: Create cluster groups
clusters = []
cluster_i = 0
cluster_group = self.plant.add_subsystem(f"Cluster{cluster_i}", om.Group())

# NOTE: cell design params should only be promoted if all the clusters are identical
if self.identical_cells:
cluster_group = self.plant.add_subsystem(
f"Cluster{cluster_i}", om.Group(), promotes=cell_design_params
)
else:
cluster_group = self.plant.add_subsystem(f"Cluster{cluster_i}", om.Group())
clusters.append(cluster_group)

# Step 1: Create controller cluster connector components
pre_translator = self.create_controller_cluster_connector()
# Step 2: Create controller cluster connector components
pre_translator = self.create_controller_cluster_connector(cell_design_params)
# Translator has scale down + power to current conversion
translator = self.create_controller_translator()
# Step 2: Create the simulate block of a cluster
simulator = self.create_cluster_simulation_block()
# Step 3: Create the simulate block of a cluster
simulator = self.create_cluster_simulation_block(cell_design_params)

cluster_group.add_subsystem(
"converter", pre_translator, promotes=["A_cell", "I_min", "I_max"]
)
promotion_vars = [*cell_design_params, "I_min", "I_max"]
cluster_group.add_subsystem("converter", pre_translator, promotes=promotion_vars)
cluster_group.add_subsystem("translator", translator, promotes=["n_stacks", "n_cells"])
cluster_group.add_subsystem("simulation", simulator, promotes=["I_min", "I_max", "A_cell"])
cluster_group.add_subsystem("simulation", simulator, promotes=promotion_vars)

# simulation.dynamics gets I_min and I_max from the converter outputs
# Connect the converter bounds to the
Expand All @@ -115,7 +138,7 @@ def create_components(self):

self.clusters = clusters

def create_cluster_simulation_block(self):
def create_cluster_simulation_block(self, cell_design_params):
simulation = om.Group()

cell_nom = self.create_cell_model()
Expand All @@ -124,9 +147,9 @@ def create_cluster_simulation_block(self):
dynamics = self.create_component("dynamics")

simulation.add_subsystem("dynamics", dynamics, promotes=["I_min", "I_max"])
simulation.add_subsystem("cell_nominal", cell_nom, promotes=["A_cell"])
simulation.add_subsystem("cell_nominal", cell_nom, promotes=cell_design_params)
simulation.add_subsystem("degradation", degradation)
simulation.add_subsystem("cell_real", cell_real, promotes=["A_cell"])
simulation.add_subsystem("cell_real", cell_real, promotes=cell_design_params)

# connect dynamics current output to nominal cell current input
simulation.connect("dynamics.I_out", "cell_nominal.I_in")
Expand Down Expand Up @@ -174,7 +197,7 @@ def create_controller_translator(self):

return translator

def create_controller_cluster_connector(self):
def create_controller_cluster_connector(self, cell_design_params):
pre_converter_grp = om.Group()
bounds_comp = self.create_bounds_component()
pre_converter_grp.add_subsystem(
Expand All @@ -185,7 +208,7 @@ def create_controller_cluster_connector(self):
)

cell = self.create_cell_model()
pre_converter_grp.add_subsystem("ref_cell", cell, promotes_inputs=["A_cell"])
pre_converter_grp.add_subsystem("ref_cell", cell, promotes_inputs=cell_design_params)

coeff_comp = self.create_component("control_command_converter", model_key="coeff_model")
pre_converter_grp.add_subsystem("p2i", coeff_comp, promotes_inputs=["I_ref_points"])
Expand Down
64 changes: 64 additions & 0 deletions electrolyzer/test/test_om_examples.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import copy

import numpy as np
import pytest
Expand All @@ -25,6 +26,7 @@ def test_example_00_no_controller(subtests):
bert.run()
i_estimated = bert.model.get_val("Cluster0.translator.command_to_current.I_command", units="A")
i_actual = bert.model.get_val("Cluster0.converter.I_ref_points", units="A")

i_error = i_estimated - i_actual

with subtests.test("100 cells per stack"):
Expand All @@ -35,3 +37,65 @@ def test_example_00_no_controller(subtests):

with subtests.test("I-V Curve fit error is less than 0.105 A"):
assert np.all(np.abs(i_error) < 0.105)

with subtests.test("Initial operating temperature"):
assert pytest.approx(80.0, rel=1e-6) == bert.model.get_val(
"operating_temperature", units="degC"
)

V_initial = copy.deepcopy(
bert.model.get_val("Cluster0.simulation.cell_real.V_cell_out", units="V")
)
coeff_initial = copy.deepcopy(
bert.model.get_val("Cluster0.translator.command_to_current.curve_coeffs", units="A/W")
)
V_ref_initial = copy.deepcopy(
bert.model.get_val("Cluster0.converter.ref_cell.V_cell_out", units="V")
)
P_ref_initial = copy.deepcopy(
bert.model.get_val("Cluster0.converter.ref_cell.P_cell_out", units="W")
)
with subtests.test("Initial reference rated power"):
assert (
pytest.approx(4467.1560, rel=1e-6)
== bert.model.get_val("Cluster0.converter.ref_cell.P_cell_out", units="W")[-1]
)
with subtests.test("Initial curve coefficients"):
expected_initial_coeff = np.array(
[7.08472908e-10, -1.70727901e-05, 4.78002528e-01, 2.34225327e00, -1.42414827e01]
)
assert pytest.approx(expected_initial_coeff, rel=1e-6, abs=1e-8) == coeff_initial
with subtests.test("Initial reference rated voltage"):
assert (
pytest.approx(2.233578003273652, rel=1e-6)
== bert.model.get_val("Cluster0.converter.ref_cell.V_cell_out", units="V")[-1]
)

# Change one of the cell design parameters
bert.model.set_val("operating_temperature", 60.0, units="degC")
bert.run()
V_new = bert.model.get_val("Cluster0.simulation.cell_real.V_cell_out", units="V")
coeff_new = bert.model.get_val(
"Cluster0.translator.command_to_current.curve_coeffs", units="A/W"
)
V_ref_new = bert.model.get_val("Cluster0.converter.ref_cell.V_cell_out", units="V")
P_ref_new = bert.model.get_val("Cluster0.converter.ref_cell.P_cell_out", units="W")

with subtests.test("Reference power points changed"):
P_ref_diff = np.abs(P_ref_initial - P_ref_new)
assert np.all(P_ref_diff < 91.0)
assert np.all(P_ref_diff > 0.20)

with subtests.test("Reference voltage points changed"):
V_ref_diff = np.abs(V_ref_initial - V_ref_new)
assert np.all(V_ref_diff < 0.046)
assert np.all(V_ref_diff > 0.0004)

with subtests.test("Real simulation voltage changed"):
assert not all(k for k in np.isclose(V_new, V_initial, rtol=1e-6, atol=1e-6))

with subtests.test("Curve coefficients changed"):
expected_coeff = np.array(
[8.30333109e-10, -1.92246906e-05, 4.75331901e-01, 2.52511351e00, -1.62209719e01]
)
assert pytest.approx(expected_coeff, rel=1e-6, abs=1e-8) == coeff_new
11 changes: 6 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,18 @@ branch = true
source = ["electrolyzer/*"]
omit = [
"setup.py",
"tests/*"
"tests/*",
"*/test/*",
"*/test_*.py"
]

[tool.pytest.ini_options]
python_files = [
"tests/*.py",
"*/test/*",
"*/test_*.py"
]
testpaths = [
"test/*.py",
"test/glue_code/*.py",
]



[tool.isort]
Expand Down
Loading