Skip to content
42 changes: 42 additions & 0 deletions electrolyzer/components/classifiers/system_performance.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 1 addition & 2 deletions electrolyzer/components/cluster/simple_dynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 6 additions & 4 deletions electrolyzer/control/openloop/simple_openloop_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,25 @@ 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"]
self.control_cmd = self.options["control_variable"]

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")

Expand Down
128 changes: 91 additions & 37 deletions electrolyzer/core/bert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -25,23 +23,31 @@ 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()
self.create_performance_aggregator()

self.connect_system()

self.create_recorder(self.prob)
if as_problem:
self.create_recorder(self.prob)

self.state = State.INITIALIZED

Expand Down Expand Up @@ -98,31 +104,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)
Expand Down Expand Up @@ -156,16 +142,76 @@ 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_performance_aggregator(self):
ts_perf_mod = SystemPerformance(n_clusters=self.n_clusters)
self.plant.add_subsystem("system_timeseries", ts_perf_mod)

perf_mod = SystemPerformance(n_clusters=self.n_clusters)
self.plant.add_subsystem("system_ub", perf_mod)

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
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=stack_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(
"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):
# 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",
)

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()

Expand Down Expand Up @@ -416,11 +462,19 @@ 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)
controller_model = self.supported_models.get(controller_name)
controller = controller_model(
plant_config=self.plant_config,
tech_config=self.system_config,
Expand Down
Loading
Loading