From 912f2e9fd920627fe49305ece8d6bc3d80322a8f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 6 Aug 2026 12:04:06 -0600 Subject: [PATCH 1/9] updated example to run for 1 year --- electrolyzer/test/test_om_examples.py | 5 ++++- examples/example_00_refactor/bert_config.yaml | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/electrolyzer/test/test_om_examples.py b/electrolyzer/test/test_om_examples.py index 7dd135f..8b937b1 100644 --- a/electrolyzer/test/test_om_examples.py +++ b/electrolyzer/test/test_om_examples.py @@ -6,6 +6,7 @@ from electrolyzer import BERT_EXAMPLE_DIR from electrolyzer.core.bert import BERT +from electrolyzer.core.file_utils import load_yaml def test_example_00_no_controller(subtests): @@ -13,7 +14,9 @@ def test_example_00_no_controller(subtests): os.chdir(example_fpath) config_fpath = example_fpath / "bert_config.yaml" - bert = BERT(config_fpath, make_n2=False) + config = load_yaml(config_fpath) + config["simulation"]["n_timesteps"] = 20 + bert = BERT(config, make_n2=False) bert.run() scale_fac = bert.model.get_val("Cluster0.n_stacks", units="unitless") * bert.model.get_val( diff --git a/examples/example_00_refactor/bert_config.yaml b/examples/example_00_refactor/bert_config.yaml index 472aeba..bc92f93 100644 --- a/examples/example_00_refactor/bert_config.yaml +++ b/examples/example_00_refactor/bert_config.yaml @@ -1,6 +1,6 @@ simulation: dt: 3600 # seconds - n_timesteps: 20 # number of timesteps, UNUSED + n_timesteps: 8760 # number of timesteps # electrolyzer: system: n_clusters: 1 From fd3c5470249ba5740ef88fc241ad55933f2d2fea Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:03:43 -0600 Subject: [PATCH 2/9] moved cluster group creation/connection to a separate method --- electrolyzer/core/bert.py | 90 +++++++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/electrolyzer/core/bert.py b/electrolyzer/core/bert.py index 36d11f3..a43106a 100644 --- a/electrolyzer/core/bert.py +++ b/electrolyzer/core/bert.py @@ -98,31 +98,11 @@ def run(self): def post_process(self): pass - def create_cluster_components(self): - pass - - def create_controller(self): - controller = self.create_controller_component() - self.plant.add_subsystem("controller", controller) - - def create_components(self): - # - + def create_cluster_group(self): # 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 - - # 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) + cluster_group = om.Group() # Step 2: Create controller cluster connector components pre_translator = self.create_controller_cluster_connector(cell_design_params) @@ -156,6 +136,72 @@ def create_components(self): f"converter.ref_cell.{var}_cell_out", f"classifier.cell_classifier.{var}_in" ) + return cluster_group + + def create_controller(self): + controller = self.create_controller_component() + self.plant.add_subsystem("controller", controller) + + def create_components(self): + # + + # 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_comp = self.create_cluster_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) + if self.identical_cells: + cluster_group = self.plant.add_subsystem( + f"Cluster{cluster_i}", cluster_comp, promotes=cell_design_params + ) + else: + cluster_group = self.plant.add_subsystem(f"Cluster{cluster_i}", cluster_comp) + clusters.append(cluster_group) + + # # Step 2: Create controller cluster connector components + # pre_translator = self.create_controller_cluster_connector(cell_design_params) + # cluster_classifier = self.create_cluster_classification_component() + # # Translator has scale down + power to current conversion + # translator = self.create_controller_translator() + # # Step 3: Create the simulate block of a cluster + # simulator = self.create_cluster_simulation_block(cell_design_params) + + # promotion_vars = [*cell_design_params, "I_min", "I_max"] + # cluster_group.add_subsystem("converter", pre_translator, promotes=promotion_vars) + # cluster_group.add_subsystem( + # "classifier", cluster_classifier, promotes=["I_min", "I_max", "n_cells", "n_stacks"] + # ) + # cluster_group.add_subsystem("translator", translator, promotes=["n_stacks", "n_cells"]) + # 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 + # cluster_group.connect( + # "converter.p2i.curve_coeffs", "translator.command_to_current.curve_coeffs" + # ) + # cluster_group.connect("translator.command_to_current.I_command", "simulation.dynamics.I_in") + + # # connect converter group stuff to classification block + # cluster_group.connect("converter.I_ref_points", "classifier.I_ref_points") + # cluster_group.connect("converter.ref_cell.J_out", "classifier.cell_classifier.J_in") + # for var in ["P", "H2", "O2", "V"]: + # cluster_group.connect( + # f"converter.ref_cell.{var}_cell_out", f"classifier.cell_classifier.{var}_in" + # ) + # Connect controller to cluster self.plant.connect( "controller.P_command", f"Cluster{cluster_i}.translator.cluster_to_stack.P_in" From 9a0e78055b4c28abfc1fe67ab032f23f7a3a887a Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:04:39 -0600 Subject: [PATCH 3/9] removed commented out code --- electrolyzer/core/bert.py | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/electrolyzer/core/bert.py b/electrolyzer/core/bert.py index a43106a..1a6e978 100644 --- a/electrolyzer/core/bert.py +++ b/electrolyzer/core/bert.py @@ -155,13 +155,6 @@ def create_components(self): cluster_comp = self.create_cluster_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) if self.identical_cells: cluster_group = self.plant.add_subsystem( f"Cluster{cluster_i}", cluster_comp, promotes=cell_design_params @@ -170,38 +163,6 @@ def create_components(self): cluster_group = self.plant.add_subsystem(f"Cluster{cluster_i}", cluster_comp) clusters.append(cluster_group) - # # Step 2: Create controller cluster connector components - # pre_translator = self.create_controller_cluster_connector(cell_design_params) - # cluster_classifier = self.create_cluster_classification_component() - # # Translator has scale down + power to current conversion - # translator = self.create_controller_translator() - # # Step 3: Create the simulate block of a cluster - # simulator = self.create_cluster_simulation_block(cell_design_params) - - # promotion_vars = [*cell_design_params, "I_min", "I_max"] - # cluster_group.add_subsystem("converter", pre_translator, promotes=promotion_vars) - # cluster_group.add_subsystem( - # "classifier", cluster_classifier, promotes=["I_min", "I_max", "n_cells", "n_stacks"] - # ) - # cluster_group.add_subsystem("translator", translator, promotes=["n_stacks", "n_cells"]) - # 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 - # cluster_group.connect( - # "converter.p2i.curve_coeffs", "translator.command_to_current.curve_coeffs" - # ) - # cluster_group.connect("translator.command_to_current.I_command", "simulation.dynamics.I_in") - - # # connect converter group stuff to classification block - # cluster_group.connect("converter.I_ref_points", "classifier.I_ref_points") - # cluster_group.connect("converter.ref_cell.J_out", "classifier.cell_classifier.J_in") - # for var in ["P", "H2", "O2", "V"]: - # cluster_group.connect( - # f"converter.ref_cell.{var}_cell_out", f"classifier.cell_classifier.{var}_in" - # ) - # Connect controller to cluster self.plant.connect( "controller.P_command", f"Cluster{cluster_i}.translator.cluster_to_stack.P_in" From 45d2c0645f903413fdbe4d0010b98ce0b180ee1a Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:52:36 -0600 Subject: [PATCH 4/9] updated so connections between controller and clusters are done in separate method --- electrolyzer/core/bert.py | 35 +++++++++++++++++++++------ electrolyzer/test/test_om_examples.py | 2 +- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/electrolyzer/core/bert.py b/electrolyzer/core/bert.py index 1a6e978..0c7de4d 100644 --- a/electrolyzer/core/bert.py +++ b/electrolyzer/core/bert.py @@ -12,9 +12,7 @@ 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 - - -# from electrolyzer.components.classifiers.system_performance import SystemPerformance +from electrolyzer.components.classifiers.system_performance import SystemPerformance class State(IntEnum): @@ -40,6 +38,7 @@ def __init__(self, config_input, make_n2=True): self.create_controller() self.create_components() + self.connect_system() self.create_recorder(self.prob) @@ -142,6 +141,10 @@ def create_controller(self): controller = self.create_controller_component() self.plant.add_subsystem("controller", controller) + def create_performance_aggregator(self): + perf_mod = SystemPerformance(n_clusters=self.n_clusters) + return perf_mod + def create_components(self): # @@ -164,15 +167,25 @@ def create_components(self): clusters.append(cluster_group) # Connect controller to cluster - self.plant.connect( - "controller.P_command", f"Cluster{cluster_i}.translator.cluster_to_stack.P_in" - ) + # self.plant.connect( + # f"controller.{self.control_passed_var}_command_{cluster_i}", + # f"Cluster{cluster_i}.translator.cluster_to_stack.{self.control_passed_var}_in" + # ) # cluster_group.connect("converter.I_ref_points", "classifier." # cluster_group.connect("converter.ref_cell.") self.clusters = clusters + def connect_system(self): + # Connect controller to cluster + + for cluster_i in range(0, self.n_clusters, 1): + self.plant.connect( + f"controller.{self.control_passed_var}_command_{cluster_i}", + f"Cluster{cluster_i}.translator.cluster_to_stack.{self.control_passed_var}_in", + ) + def create_cluster_simulation_block(self, cell_design_params): simulation = om.Group() @@ -423,8 +436,16 @@ def create_controller_component(self): n_timesteps = int(self.plant_config["simulation"]["n_timesteps"]) if "control_model" not in self.system_config: ivc_comp = om.IndepVarComp( - name=f"{self.control_passed_var}_command", val=np.full(n_timesteps, 40.0), units="W" + name=f"{self.control_passed_var}_command_0", + val=np.full(n_timesteps, 40.0), + units="W", ) + if self.n_clusters > 1: + msg = ( + "Cannot run multiple clusters without a control model. " + "Please specify a control model" + ) + raise NotImplementedError(msg) return ivc_comp controller_name = self.system_config["control_model"] controller_model = self.supported_models(controller_name) diff --git a/electrolyzer/test/test_om_examples.py b/electrolyzer/test/test_om_examples.py index 8b937b1..5b15454 100644 --- a/electrolyzer/test/test_om_examples.py +++ b/electrolyzer/test/test_om_examples.py @@ -25,7 +25,7 @@ def test_example_00_no_controller(subtests): p_cell_ref = bert.model.get_val("Cluster0.converter.ref_cell.P_cell_out", units="W") p_system_ref = p_cell_ref * scale_fac - bert.model.set_val("controller.P_command", p_system_ref, units="W") + bert.model.set_val("controller.P_command_0", p_system_ref, units="W") 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") From 1d25d4571275174b6818c0928f43ac21b412e7fe Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 10 Aug 2026 14:58:10 -0600 Subject: [PATCH 5/9] updated framework to handle multiple clusters and fixed controller --- .../openloop/simple_openloop_control.py | 10 +++-- electrolyzer/core/bert.py | 31 +++++++------ electrolyzer/test/test_om_examples.py | 45 +++++++++++++++++++ 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/electrolyzer/control/openloop/simple_openloop_control.py b/electrolyzer/control/openloop/simple_openloop_control.py index 5c72859..c375658 100644 --- a/electrolyzer/control/openloop/simple_openloop_control.py +++ b/electrolyzer/control/openloop/simple_openloop_control.py @@ -14,10 +14,10 @@ def initialize(self): self.options.declare("plant_config", types=dict, default={}) self.options.declare("tech_config", types=dict, default={}) self.options.declare("n_clusters", types=int) - self.options.declare("control_variable", types=str, values=["power", "hydrogen"]) + self.options.declare("control_variable", values=["power", "hydrogen"]) def setup(self): - # self.n_timesteps = self.options["plant_config"]["simulation"]["n_timesteps"] + self.n_timesteps = self.options["plant_config"]["simulation"]["n_timesteps"] # self.dt = self.options["plant_config"]["simulation"]["dt"] # self.config = OLControlConfig.from_dict(self.options["tech_config"]["control_parameters"]) self.n_clusters = self.options["n_clusters"] @@ -25,12 +25,14 @@ def setup(self): if self.control_cmd == "power": # output_cmd_fmt = "power_cmd_{ci}" - self.add_input("P_command", val=0.0, shape_by_conn=True, units="kW") + # self.add_input("P_command", val=0.0, shape_by_conn=True, units="kW") + self.add_input("P_command", val=0.0, shape=self.n_timesteps, units="kW") for ci in range(self.n_clusters): self.add_output(f"P_command_{ci}", val=0.0, copy_shape="P_command", units="kW") else: # output_cmd_fmt = "hydrogen_cmd_{ci}" - self.add_input("H2_command", val=0.0, shape_by_conn=True, units="kg/h") + # self.add_input("H2_command", val=0.0, shape_by_conn=True, units="kg/h") + self.add_input("H2_command", val=0.0, shape=self.n_timesteps, units="kg/h") for ci in range(self.n_clusters): self.add_output(f"H2_command_{ci}", val=0.0, copy_shape="H2_command", units="kg/h") diff --git a/electrolyzer/core/bert.py b/electrolyzer/core/bert.py index 0c7de4d..03518e9 100644 --- a/electrolyzer/core/bert.py +++ b/electrolyzer/core/bert.py @@ -38,6 +38,7 @@ def __init__(self, config_input, make_n2=True): self.create_controller() self.create_components() + self.connect_system() self.create_recorder(self.prob) @@ -143,6 +144,8 @@ def create_controller(self): def create_performance_aggregator(self): perf_mod = SystemPerformance(n_clusters=self.n_clusters) + self.plant.add_subsystem("system_timeseries", perf_mod) + return perf_mod def create_components(self): @@ -153,18 +156,18 @@ def create_components(self): # Step 1: Create cluster groups clusters = [] - cluster_i = 0 - - cluster_comp = self.create_cluster_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}", cluster_comp, promotes=cell_design_params - ) - else: - cluster_group = self.plant.add_subsystem(f"Cluster{cluster_i}", cluster_comp) - clusters.append(cluster_group) + # cluster_i = 0 + for cluster_i in range(self.n_clusters): + cluster_comp = self.create_cluster_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}", cluster_comp, promotes=cell_design_params + ) + else: + cluster_group = self.plant.add_subsystem(f"Cluster{cluster_i}", cluster_comp) + clusters.append(cluster_group) # Connect controller to cluster # self.plant.connect( @@ -181,10 +184,12 @@ def connect_system(self): # Connect controller to cluster for cluster_i in range(0, self.n_clusters, 1): + # Connect controller to cluster self.plant.connect( f"controller.{self.control_passed_var}_command_{cluster_i}", f"Cluster{cluster_i}.translator.cluster_to_stack.{self.control_passed_var}_in", ) + # connect the clusters to a system performance component def create_cluster_simulation_block(self, cell_design_params): simulation = om.Group() @@ -448,7 +453,7 @@ def create_controller_component(self): raise NotImplementedError(msg) return ivc_comp controller_name = self.system_config["control_model"] - controller_model = self.supported_models(controller_name) + controller_model = self.supported_models.get(controller_name) controller = controller_model( plant_config=self.plant_config, tech_config=self.system_config, diff --git a/electrolyzer/test/test_om_examples.py b/electrolyzer/test/test_om_examples.py index 5b15454..7e5908b 100644 --- a/electrolyzer/test/test_om_examples.py +++ b/electrolyzer/test/test_om_examples.py @@ -190,3 +190,48 @@ def test_example_00_no_controller(subtests): 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) + + +def test_example_00_with_controller(subtests): + example_fpath = BERT_EXAMPLE_DIR / "example_00_refactor" + os.chdir(example_fpath) + + n_clusters = 2 + + config_fpath = example_fpath / "bert_config.yaml" + config = load_yaml(config_fpath) + config["simulation"]["n_timesteps"] = 20 + config["system"]["n_clusters"] = n_clusters + config["system"]["control_model"] = "OLBasicSplit" + bert = BERT(config, make_n2=False) + bert.run() + + scale_fac = bert.model.get_val("Cluster0.n_stacks", units="unitless") * bert.model.get_val( + "Cluster0.n_cells", units="unitless" + ) + + p_cell_ref = bert.model.get_val("Cluster0.converter.ref_cell.P_cell_out", units="W") + p_system_ref = p_cell_ref * scale_fac * n_clusters + bert.model.set_val("controller.P_command", p_system_ref, units="W") + bert.run() + + 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] + ) + + coeff_initial = copy.deepcopy( + bert.model.get_val("Cluster0.translator.command_to_current.curve_coeffs", units="A/W") + ) + + 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] + ) From 309a95c2f83f0498096944495d3b127408f27538 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 10 Aug 2026 16:17:21 -0600 Subject: [PATCH 6/9] added system performance components --- electrolyzer/core/bert.py | 24 ++++++++++++++++++++---- electrolyzer/test/test_om_examples.py | 22 ++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/electrolyzer/core/bert.py b/electrolyzer/core/bert.py index 03518e9..c9ed708 100644 --- a/electrolyzer/core/bert.py +++ b/electrolyzer/core/bert.py @@ -38,6 +38,7 @@ def __init__(self, config_input, make_n2=True): self.create_controller() self.create_components() + self.create_performance_aggregator() self.connect_system() @@ -143,10 +144,11 @@ def create_controller(self): self.plant.add_subsystem("controller", controller) def create_performance_aggregator(self): - perf_mod = SystemPerformance(n_clusters=self.n_clusters) - self.plant.add_subsystem("system_timeseries", perf_mod) + ts_perf_mod = SystemPerformance(n_clusters=self.n_clusters) + self.plant.add_subsystem("system_timeseries", ts_perf_mod) - return perf_mod + perf_mod = SystemPerformance(n_clusters=self.n_clusters) + self.plant.add_subsystem("system_ub", perf_mod) def create_components(self): # @@ -189,7 +191,21 @@ def connect_system(self): f"controller.{self.control_passed_var}_command_{cluster_i}", f"Cluster{cluster_i}.translator.cluster_to_stack.{self.control_passed_var}_in", ) - # connect the clusters to a system performance component + + for cluster_i in range(0, self.n_clusters, 1): + # connect the clusters to a system performance component + # connect the classifier component and the simulation component + for var in ["P", "H2", "O2", "V"]: + self.plant.connect( + # part of scale_stack_to_cluster + f"Cluster{cluster_i}.simulation.Cluster_{var}", + f"system_timeseries.{var}_in_{cluster_i}", + ) + self.plant.connect( + # part of classifier.stack_to_cluster_ub + f"Cluster{cluster_i}.classifier.{var}_max", + f"system_ub.{var}_in_{cluster_i}", + ) def create_cluster_simulation_block(self, cell_design_params): simulation = om.Group() diff --git a/electrolyzer/test/test_om_examples.py b/electrolyzer/test/test_om_examples.py index 7e5908b..312ee3e 100644 --- a/electrolyzer/test/test_om_examples.py +++ b/electrolyzer/test/test_om_examples.py @@ -235,3 +235,25 @@ def test_example_00_with_controller(subtests): pytest.approx(2.233578003273652, rel=1e-6) == bert.model.get_val("Cluster0.converter.ref_cell.V_cell_out", units="V")[-1] ) + # TODO: also test Cluster1 + + with subtests.test("Cluster H2 production"): + assert ( + pytest.approx(82.38478954465917, rel=1e-6) + == bert.model.get_val("Cluster0.simulation.Cluster_H2", units="kg/h").sum() + ) + assert ( + pytest.approx(82.38478954465917, rel=1e-6) + == bert.model.get_val("Cluster1.simulation.Cluster_H2", units="kg/h").sum() + ) + + with subtests.test("System H2 Production"): + assert ( + pytest.approx(164.76957908931834, rel=1e-6) + == bert.model.get_val("system_timeseries.H2_out", units="kg/h").sum() + ) + + with subtests.test("System Rated H2 Production"): + assert pytest.approx(14.982832485661488, rel=1e-6) == bert.model.get_val( + "system_ub.H2_out", units="kg/h" + ) From 0ded5c1d49342fe6c2c8f22083ea3f34e08fce0c Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 10 Aug 2026 17:22:16 -0600 Subject: [PATCH 7/9] updated bert so it can be easily ran within an openmdao group --- electrolyzer/core/bert.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/electrolyzer/core/bert.py b/electrolyzer/core/bert.py index c9ed708..8b3b971 100644 --- a/electrolyzer/core/bert.py +++ b/electrolyzer/core/bert.py @@ -23,18 +23,22 @@ class State(IntEnum): class BERT: - def __init__(self, config_input, make_n2=True): + def __init__(self, config_input, make_n2=True, as_problem=True): self.create_n2 = make_n2 self.supported_models = supported_models.copy() # read in config file; it's a yaml dict that looks like this: self.load_config(config_input) - self.prob = om.Problem(reports=False) - self.model = self.prob.model - plant_group = om.Group() - # Create the plant model group and add components - self.plant = self.model.add_subsystem("plant", plant_group, promotes=["*"]) + if as_problem: + self.prob = om.Problem(reports=False) + self.model = self.prob.model + plant_group = om.Group() + + # Create the plant model group and add components + self.plant = self.model.add_subsystem("plant", plant_group, promotes=["*"]) + else: + self.plant = om.Group() self.create_controller() self.create_components() @@ -42,7 +46,8 @@ def __init__(self, config_input, make_n2=True): self.connect_system() - self.create_recorder(self.prob) + if as_problem: + self.create_recorder(self.prob) self.state = State.INITIALIZED From 3c8cee229f61906395a96b3564774d9bba4865c7 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 11 Aug 2026 14:44:34 -0600 Subject: [PATCH 8/9] added system performance component --- .../classifiers/system_performance.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 electrolyzer/components/classifiers/system_performance.py diff --git a/electrolyzer/components/classifiers/system_performance.py b/electrolyzer/components/classifiers/system_performance.py new file mode 100644 index 0000000..da1aad0 --- /dev/null +++ b/electrolyzer/components/classifiers/system_performance.py @@ -0,0 +1,42 @@ +import numpy as np +import openmdao.api as om + + +class SystemPerformance(om.ExplicitComponent): + """Connect clusters""" + + def initialize(self): + self.options.declare("n_clusters", types=(int, float), default=1.0) + + def setup(self): + self.vars_to_units = { + # "J": "A/(cm**2)", + # "I": "A", + "P": "W", + "H2": "kg/s", + "O2": "kg/s", + # "H2O": "kg/s", + "V": "V", + # "V_deg": "V" + } + + ref_shape = None + for v, u in self.vars_to_units.items(): + for i in range(0, int(self.options["n_clusters"])): + if ref_shape is None: + self.add_input(f"{v}_in_{i}", val=0.0, shape_by_conn=True, units=u) + ref_shape = f"{v}_in_{i}" + else: + self.add_input(f"{v}_in_{i}", val=0.0, copy_shape=ref_shape, units=u) + self.add_output(f"{v}_out", val=0.0, copy_shape=ref_shape, units=u) + + self.ref_shape = ref_shape + + def compute(self, inputs, outputs): + n_timesteps = len(inputs[self.ref_shape]) + + for v in self.vars_to_units.keys(): + var_cnt = np.zeros(n_timesteps) + for i in range(0, int(self.options["n_clusters"])): + var_cnt += inputs[f"{v}_in_{i}"] + outputs[f"{v}_out"] = var_cnt From 6273a9805d9e510e3071e132d01a99fe852a44c9 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:29:33 -0600 Subject: [PATCH 9/9] updated to promote n_cells and n_stacks to plant level and fixed dynamics to reflect off-hours in current --- .../components/cluster/simple_dynamics.py | 3 +- electrolyzer/core/bert.py | 4 +- electrolyzer/test/test_om_examples.py | 38 +++++++++++++------ 3 files changed, 30 insertions(+), 15 deletions(-) diff --git a/electrolyzer/components/cluster/simple_dynamics.py b/electrolyzer/components/cluster/simple_dynamics.py index 9db7958..6ade131 100644 --- a/electrolyzer/components/cluster/simple_dynamics.py +++ b/electrolyzer/components/cluster/simple_dynamics.py @@ -40,6 +40,5 @@ def compute(self, inputs, outputs): # But a separate output should be used to reflect show partial losses (warm-up delay) i_out = np.clip(inputs["I_in"], a_min=inputs["I_min"], a_max=inputs["I_max"]) - # TODO: add start-up delay - outputs["I_out"] = i_out + outputs["I_out"] = i_out * on_off_status outputs["on_off_status"] = on_off_status diff --git a/electrolyzer/core/bert.py b/electrolyzer/core/bert.py index 8b3b971..cdee455 100644 --- a/electrolyzer/core/bert.py +++ b/electrolyzer/core/bert.py @@ -160,7 +160,7 @@ def create_components(self): # Get the design parameters of the cell cell_design_params = get_cell_params_for_model(self.config["cell"].get("model", None)) - + stack_design_params = [*cell_design_params, "n_stacks", "n_cells"] # Step 1: Create cluster groups clusters = [] # cluster_i = 0 @@ -170,7 +170,7 @@ def create_components(self): # 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}", cluster_comp, promotes=cell_design_params + f"Cluster{cluster_i}", cluster_comp, promotes=stack_design_params ) else: cluster_group = self.plant.add_subsystem(f"Cluster{cluster_i}", cluster_comp) diff --git a/electrolyzer/test/test_om_examples.py b/electrolyzer/test/test_om_examples.py index 312ee3e..cfa7726 100644 --- a/electrolyzer/test/test_om_examples.py +++ b/electrolyzer/test/test_om_examples.py @@ -19,8 +19,8 @@ def test_example_00_no_controller(subtests): bert = BERT(config, make_n2=False) bert.run() - scale_fac = bert.model.get_val("Cluster0.n_stacks", units="unitless") * bert.model.get_val( - "Cluster0.n_cells", units="unitless" + scale_fac = bert.model.get_val("n_stacks", units="unitless") * bert.model.get_val( + "n_cells", units="unitless" ) p_cell_ref = bert.model.get_val("Cluster0.converter.ref_cell.P_cell_out", units="W") @@ -33,10 +33,10 @@ def test_example_00_no_controller(subtests): i_error = i_estimated - i_actual with subtests.test("100 cells per stack"): - assert pytest.approx(100.0, rel=1e-6) == bert.model.get_val("Cluster0.n_cells")[0] + assert pytest.approx(100.0, rel=1e-6) == bert.model.get_val("n_cells")[0] with subtests.test("1 stack per cluster"): - assert pytest.approx(1.0, rel=1e-6) == bert.model.get_val("Cluster0.n_stacks")[0] + assert pytest.approx(1.0, rel=1e-6) == bert.model.get_val("n_stacks")[0] with subtests.test("I-V Curve fit error is less than 0.105 A"): assert np.all(np.abs(i_error) < 0.105) @@ -120,14 +120,14 @@ def test_example_00_no_controller(subtests): ) assert ( pytest.approx( - cell_rated_power * bert.model.get_val("Cluster0.n_cells", units="unitless"), + cell_rated_power * bert.model.get_val("n_cells", units="unitless"), rel=1e-6, ) == stack_rated_power ) assert pytest.approx( bert.model.get_val("Cluster0.classifier.P_max", units="kW"), rel=1e-6 - ) == stack_rated_power * bert.model.get_val("Cluster0.n_stacks", units="unitless") + ) == stack_rated_power * bert.model.get_val("n_stacks", units="unitless") with subtests.test("Rated conversion efficiency"): assert pytest.approx(60.84498639, rel=1e-6) == bert.model.get_val( @@ -206,8 +206,8 @@ def test_example_00_with_controller(subtests): bert = BERT(config, make_n2=False) bert.run() - scale_fac = bert.model.get_val("Cluster0.n_stacks", units="unitless") * bert.model.get_val( - "Cluster0.n_cells", units="unitless" + scale_fac = bert.model.get_val("n_stacks", units="unitless") * bert.model.get_val( + "n_cells", units="unitless" ) p_cell_ref = bert.model.get_val("Cluster0.converter.ref_cell.P_cell_out", units="W") @@ -237,19 +237,35 @@ def test_example_00_with_controller(subtests): ) # TODO: also test Cluster1 + with subtests.test("Cluster0 on/off status"): + assert ( + 19.0 + == bert.model.get_val( + "Cluster0.simulation.dynamics.on_off_status", units="unitless" + ).sum() + ) + + with subtests.test("Cluster1 on/off status"): + assert ( + 19.0 + == bert.model.get_val( + "Cluster1.simulation.dynamics.on_off_status", units="unitless" + ).sum() + ) + with subtests.test("Cluster H2 production"): assert ( - pytest.approx(82.38478954465917, rel=1e-6) + pytest.approx(81.64025077453496, rel=1e-6) == bert.model.get_val("Cluster0.simulation.Cluster_H2", units="kg/h").sum() ) assert ( - pytest.approx(82.38478954465917, rel=1e-6) + pytest.approx(81.64025077453496, rel=1e-6) == bert.model.get_val("Cluster1.simulation.Cluster_H2", units="kg/h").sum() ) with subtests.test("System H2 Production"): assert ( - pytest.approx(164.76957908931834, rel=1e-6) + pytest.approx(163.28050154906992, rel=1e-6) == bert.model.get_val("system_timeseries.H2_out", units="kg/h").sum() )