From fd035dc4f76b7f35ad74bd5dc4a4d7b4f9770ec1 Mon Sep 17 00:00:00 2001 From: ZackTully Date: Mon, 13 Mar 2023 11:36:05 -0600 Subject: [PATCH 01/10] minor changes to deg calc dt details --- electrolyzer/stack.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/electrolyzer/stack.py b/electrolyzer/stack.py index e550954..b4c1e70 100644 --- a/electrolyzer/stack.py +++ b/electrolyzer/stack.py @@ -85,8 +85,12 @@ class Stack(FromDictMixin): stack_on: bool = field(init=False, default=False) stack_waiting: bool = field(init=False, default=False) + # [s] 10 minute base turn on delay, for large time steps + base_turn_on_delay: float = 600 + # [s] 10 minute time delay for PEM electrolyzer startup procedure - turn_on_delay: float = 600 + # (set in __attrs_post_init__) + turn_on_delay: float = field(init=False) # keep track of when the stack was last turned on turn_on_time: float = field(init=False, default=0) @@ -111,6 +115,9 @@ class Stack(FromDictMixin): # state space, (set in __attrs_post_init) DTSS: NDArrayFloat = field(init=False) + # whether 1st order dynamics should be ignored according to dt size + ignore_dynamics: bool = field(init=False, default=False) + def __attrs_post_init__(self) -> None: # Stack parameters # #################### @@ -123,6 +130,16 @@ def __attrs_post_init__(self) -> None: # Stack dynamics # ################## + # If the time step is bigger than the 1st order time constant, ignore dynamics + if self.dt > self.tau: + self.ignore_dynamics = True + + # Remove turn on delay for large time steps + if self.dt > 2 * self.base_turn_on_delay: + self.turn_on_delay = 0 + else: + self.turn_on_delay = self.base_turn_on_delay + self.wait_time = self.turn_on_time # [kW] nameplate power rating @@ -309,11 +326,16 @@ def update_dynamics(self, H2_mfr_ss, stack_state): This is really just a filter on the steady state mfr from time step to time step """ - x_k = stack_state - x_kp1 = self.DTSS[0] * x_k + self.DTSS[1] * H2_mfr_ss - y_kp1 = self.DTSS[2] * x_k + self.DTSS[3] * H2_mfr_ss - next_state = x_kp1 - H2_mfr_actual = y_kp1 + + if not self.ignore_dynamics: + x_k = stack_state + x_kp1 = self.DTSS[0] * x_k + self.DTSS[1] * H2_mfr_ss + y_kp1 = self.DTSS[2] * x_k + self.DTSS[3] * H2_mfr_ss + next_state = x_kp1 + H2_mfr_actual = y_kp1 + else: + H2_mfr_actual = H2_mfr_ss + next_state = self.stack_state return next_state, H2_mfr_actual @@ -335,7 +357,7 @@ def update_status(self): return if self.stack_waiting: - if (self.turn_on_time + self.wait_time) < self.time: + if (self.turn_on_time + self.wait_time) <= self.time: self.stack_waiting = False self.stack_on = True From 5d5271f94cb36d68636a58b359d9bbfd6da4f04a Mon Sep 17 00:00:00 2001 From: ZackTully Date: Mon, 13 Mar 2023 12:16:34 -0600 Subject: [PATCH 02/10] update wait_time init and test_regression --- electrolyzer/stack.py | 10 ++++++---- tests/glue_code/test_run_electrolyzer.py | 3 ++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/electrolyzer/stack.py b/electrolyzer/stack.py index b4c1e70..2c9c130 100644 --- a/electrolyzer/stack.py +++ b/electrolyzer/stack.py @@ -101,9 +101,6 @@ class Stack(FromDictMixin): # wait time for partial startup procedure (set in __attrs_post_init) wait_time: float = field(init=False) - # # [s] simulation time step - # dt: float = 1 - # [s] total time of simulation time: float = field(init=False, default=0) @@ -140,7 +137,12 @@ def __attrs_post_init__(self) -> None: else: self.turn_on_delay = self.base_turn_on_delay - self.wait_time = self.turn_on_time + self.wait_time = np.min( + [ + (self.turn_on_time - self.turn_off_time), + self.turn_on_delay, + ] + ) # [kW] nameplate power rating self.stack_rating_kW = self.stack_rating_kW or self.calc_stack_power( diff --git a/tests/glue_code/test_run_electrolyzer.py b/tests/glue_code/test_run_electrolyzer.py index 656ff3c..e913e1e 100644 --- a/tests/glue_code/test_run_electrolyzer.py +++ b/tests/glue_code/test_run_electrolyzer.py @@ -14,6 +14,7 @@ from electrolyzer import Supervisor from electrolyzer.glue_code.run_electrolyzer import run_electrolyzer + turbine_rating = 3.4 # MW # Create cosine test signal @@ -87,7 +88,7 @@ def test_regression(result): _, df = result # Test total kg H2 produced - assert_almost_equal(df["kg_rate"].sum(), 222.87991746974592, decimal=4) + assert_almost_equal(df["kg_rate"].sum(), 222.8930364856318, decimal=4) # Test degradation state of stacks degradation = df[[col for col in df if "deg" in col]] From 41b275788fc66e991888b09a205a80b4b6e6d70f Mon Sep 17 00:00:00 2001 From: ZackTully Date: Mon, 13 Mar 2023 14:58:32 -0600 Subject: [PATCH 03/10] revisit stack init and turn off test --- electrolyzer/stack.py | 7 +++++-- tests/test_stack.py | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/electrolyzer/stack.py b/electrolyzer/stack.py index 2c9c130..67058d2 100644 --- a/electrolyzer/stack.py +++ b/electrolyzer/stack.py @@ -93,10 +93,10 @@ class Stack(FromDictMixin): turn_on_delay: float = field(init=False) # keep track of when the stack was last turned on - turn_on_time: float = field(init=False, default=0) + turn_on_time: float = field(init=False) # keep track of when the stack was last turned off - turn_off_time: float = field(init=False, default=-1000) + turn_off_time: float = field(init=False) # wait time for partial startup procedure (set in __attrs_post_init) wait_time: float = field(init=False) @@ -137,6 +137,9 @@ def __attrs_post_init__(self) -> None: else: self.turn_on_delay = self.base_turn_on_delay + self.turn_on_time = 0 + self.turn_off_time = -self.turn_on_delay + self.wait_time = np.min( [ (self.turn_on_time - self.turn_off_time), diff --git a/tests/test_stack.py b/tests/test_stack.py index 6665cc4..efa2d8e 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -66,8 +66,8 @@ def test_init(mocker): assert stack.stack_waiting is False assert stack.turn_on_delay == 600 assert stack.turn_on_time == 0 - assert stack.turn_off_time == -1000 - assert stack.wait_time == stack.turn_on_time + assert stack.turn_off_time < 0 + assert stack.wait_time > 0 assert stack.dt == 1.0 assert stack.time == 0 assert stack.tau == 5.0 @@ -337,6 +337,7 @@ def test_turn_stack_off(stack: Stack): assert stack.cycle_count == 0 stack.stack_on = True + stack.time = 800 stack.turn_stack_off() assert stack.turn_off_time == stack.time assert stack.stack_on is False From 4c0ad84367fe18618f2fa3a373a65c78ec5e2286 Mon Sep 17 00:00:00 2001 From: Zachary Tully <107644545+ZackTully@users.noreply.github.com> Date: Mon, 13 Mar 2023 15:28:29 -0600 Subject: [PATCH 04/10] Update test_run_electrolyzer.py --- tests/glue_code/test_run_electrolyzer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/glue_code/test_run_electrolyzer.py b/tests/glue_code/test_run_electrolyzer.py index e913e1e..26dc717 100644 --- a/tests/glue_code/test_run_electrolyzer.py +++ b/tests/glue_code/test_run_electrolyzer.py @@ -14,7 +14,6 @@ from electrolyzer import Supervisor from electrolyzer.glue_code.run_electrolyzer import run_electrolyzer - turbine_rating = 3.4 # MW # Create cosine test signal From 923b8d18036586ae1dc713391ea8623ffb1b0352 Mon Sep 17 00:00:00 2001 From: ZackTully Date: Fri, 17 Mar 2023 18:32:56 -0600 Subject: [PATCH 05/10] add time step test --- electrolyzer/stack.py | 8 +++---- tests/glue_code/test_run_electrolyzer.py | 28 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/electrolyzer/stack.py b/electrolyzer/stack.py index 67058d2..69ec677 100644 --- a/electrolyzer/stack.py +++ b/electrolyzer/stack.py @@ -332,15 +332,15 @@ def update_dynamics(self, H2_mfr_ss, stack_state): This is really just a filter on the steady state mfr from time step to time step """ - if not self.ignore_dynamics: + if self.ignore_dynamics: + H2_mfr_actual = H2_mfr_ss + next_state = self.stack_state + else: x_k = stack_state x_kp1 = self.DTSS[0] * x_k + self.DTSS[1] * H2_mfr_ss y_kp1 = self.DTSS[2] * x_k + self.DTSS[3] * H2_mfr_ss next_state = x_kp1 H2_mfr_actual = y_kp1 - else: - H2_mfr_actual = H2_mfr_ss - next_state = self.stack_state return next_state, H2_mfr_actual diff --git a/tests/glue_code/test_run_electrolyzer.py b/tests/glue_code/test_run_electrolyzer.py index 26dc717..cbe8e44 100644 --- a/tests/glue_code/test_run_electrolyzer.py +++ b/tests/glue_code/test_run_electrolyzer.py @@ -14,6 +14,7 @@ from electrolyzer import Supervisor from electrolyzer.glue_code.run_electrolyzer import run_electrolyzer + turbine_rating = 3.4 # MW # Create cosine test signal @@ -54,6 +55,33 @@ def test_run_electrolyzer_dict(): run_electrolyzer(bad_input, []) +def test_degradation_dt(): + """Larger time steps should undercalculate degradation""" + + model_input = val.load_modeling_yaml(fname_input_modeling) + + # initialize with dt = 1 + model_input["electrolyzer"]["dt"] = 1 + res1 = run_electrolyzer(model_input, power_test_signal) + _, df1 = res1 + deg1 = df1[[col for col in df1 if "deg" in col]] + + # initialize with dt = 60 + model_input["electrolyzer"]["dt"] = 60 + res60 = run_electrolyzer(model_input, power_test_signal) + _, df60 = res60 + deg60 = df60[[col for col in df60 if "deg" in col]] + + # initialize with dt = 3600 + model_input["electrolyzer"]["dt"] = 3600 + res3600 = run_electrolyzer(model_input, power_test_signal) + _, df3600 = res3600 + deg3600 = df3600[[col for col in df3600 if "deg" in col]] + + assert all(deg3600 < deg60) + assert all(deg60 < deg1) + + def test_result_df(result): """An electrolyzer run should return a `DataFrame` with time series output.""" sup, df = result From b40cc627acd439fad1a19d7f10ece271d3afe2d9 Mon Sep 17 00:00:00 2001 From: ZackTully Date: Mon, 27 Mar 2023 15:11:53 -0600 Subject: [PATCH 06/10] commit to save work --- tests/test_stack.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/test_stack.py b/tests/test_stack.py index efa2d8e..f270284 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -383,3 +383,28 @@ def test_calc_electrolysis_efficiency(stack: Stack): stack.stack_rating_kW, H2_mfr2 * 3600 ) assert eta_values2[0] < eta_values[0] + + +def test_degradation_time_scaling(): + + stack_dict = { + "n_cells": 100, + "cell_area": 1000, + "temperature": 60, + "max_current": 2000, + "dt": 1, + } + + stack1 = Stack.from_dict(stack_dict) + + stack_dict["dt"] = 60 + + stack60 = Stack.from_dict(stack_dict) + + stack_dict["dt"] = 3600 + + stack3600 = Stack.from_dict(stack_dict) + + # temporary sloppy coding. I am coming back to fix this later + assert stack1.cell_area == stack60.cell_area + assert stack60.cell_area == stack3600.cell_area From 88517e1b92e8b345eaaeeac2e68f5026ffeb95ba Mon Sep 17 00:00:00 2001 From: ZackTully Date: Thu, 30 Mar 2023 15:17:25 -0600 Subject: [PATCH 07/10] dt test --- tests/test_stack.py | 35 +++++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/tests/test_stack.py b/tests/test_stack.py index f270284..dee4db0 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -385,7 +385,7 @@ def test_calc_electrolysis_efficiency(stack: Stack): assert eta_values2[0] < eta_values[0] -def test_degradation_time_scaling(): +def test_dt_behavior(): stack_dict = { "n_cells": 100, @@ -396,15 +396,38 @@ def test_degradation_time_scaling(): } stack1 = Stack.from_dict(stack_dict) + stack1.cell_voltage = 1.5 stack_dict["dt"] = 60 - stack60 = Stack.from_dict(stack_dict) + stack60.cell_voltage = 1.5 stack_dict["dt"] = 3600 - stack3600 = Stack.from_dict(stack_dict) + stack3600.cell_voltage = 1.5 + + # Check that timescale specific attributes were initialized correctly + assert stack1.dt == 1 + assert stack60.dt == 60 + assert stack3600.dt == 3600 + + assert not stack1.ignore_dynamics + assert stack60.ignore_dynamics + assert stack3600.ignore_dynamics + + assert stack1.turn_on_delay == 600 + assert stack60.turn_on_delay == 600 + assert stack3600.turn_on_delay == 0 + + assert stack1.wait_time > 0 + assert stack60.wait_time > 0 + assert stack3600.wait_time == 0 + + # The steady degfradation calculation should change with the change in dt + stack1.calc_steady_degradation() + stack60.calc_steady_degradation() + stack3600.calc_steady_degradation() - # temporary sloppy coding. I am coming back to fix this later - assert stack1.cell_area == stack60.cell_area - assert stack60.cell_area == stack3600.cell_area + assert stack1.d_s == 2.126068935e-10 + assert stack60.d_s == 1.2756413610000001e-08 + assert stack3600.d_s == 7.653848166e-07 From bcf1fca230d29735c3a34b635de7a8f70e7b279f Mon Sep 17 00:00:00 2001 From: ZackTully Date: Thu, 30 Mar 2023 15:22:41 -0600 Subject: [PATCH 08/10] dt test --- tests/test_stack.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_stack.py b/tests/test_stack.py index dee4db0..0f737da 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -386,7 +386,6 @@ def test_calc_electrolysis_efficiency(stack: Stack): def test_dt_behavior(): - stack_dict = { "n_cells": 100, "cell_area": 1000, From 41b0f5b89be8ae89504d7a659c11014473c17313 Mon Sep 17 00:00:00 2001 From: ZackTully Date: Thu, 30 Mar 2023 15:55:14 -0600 Subject: [PATCH 09/10] stack.update_dynamics with different dt --- electrolyzer/stack.py | 4 ++-- tests/test_stack.py | 29 +++++++++++++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/electrolyzer/stack.py b/electrolyzer/stack.py index 69ec677..bc325e1 100644 --- a/electrolyzer/stack.py +++ b/electrolyzer/stack.py @@ -339,8 +339,8 @@ def update_dynamics(self, H2_mfr_ss, stack_state): x_k = stack_state x_kp1 = self.DTSS[0] * x_k + self.DTSS[1] * H2_mfr_ss y_kp1 = self.DTSS[2] * x_k + self.DTSS[3] * H2_mfr_ss - next_state = x_kp1 - H2_mfr_actual = y_kp1 + next_state = x_kp1[0][0] + H2_mfr_actual = y_kp1[0][0] return next_state, H2_mfr_actual diff --git a/tests/test_stack.py b/tests/test_stack.py index 0f737da..ee2277e 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -410,10 +410,6 @@ def test_dt_behavior(): assert stack60.dt == 60 assert stack3600.dt == 3600 - assert not stack1.ignore_dynamics - assert stack60.ignore_dynamics - assert stack3600.ignore_dynamics - assert stack1.turn_on_delay == 600 assert stack60.turn_on_delay == 600 assert stack3600.turn_on_delay == 0 @@ -430,3 +426,28 @@ def test_dt_behavior(): assert stack1.d_s == 2.126068935e-10 assert stack60.d_s == 1.2756413610000001e-08 assert stack3600.d_s == 7.653848166e-07 + + # stack.update_dynamics() should only perform state space calculations if dt is + # small enough + assert not stack1.ignore_dynamics + assert stack60.ignore_dynamics + assert stack3600.ignore_dynamics + + H2_mfr = 0.0015 + stack_state = 0.001 + + stack1.stack_state = stack_state + stack60.stack_state = stack_state + stack3600.stack_state = stack_state + + next_state1, H2_mfr1 = stack1.update_dynamics(H2_mfr, stack_state) + next_state60, H2_mfr60 = stack60.update_dynamics(H2_mfr, stack_state) + next_state3600, H2_mfr3600 = stack3600.update_dynamics(H2_mfr, stack_state) + + assert H2_mfr1 != H2_mfr + assert H2_mfr60 == H2_mfr + assert H2_mfr3600 == H2_mfr + + assert next_state1 != stack_state + assert next_state60 == stack_state + assert next_state3600 == stack_state From ad698f3dce07000ecf615edfd5fed8c0c6855532 Mon Sep 17 00:00:00 2001 From: Zachary Tully <107644545+ZackTully@users.noreply.github.com> Date: Thu, 30 Mar 2023 17:35:29 -0600 Subject: [PATCH 10/10] Update test_run_electrolyzer.py --- tests/glue_code/test_run_electrolyzer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/glue_code/test_run_electrolyzer.py b/tests/glue_code/test_run_electrolyzer.py index 4515577..ffa920d 100644 --- a/tests/glue_code/test_run_electrolyzer.py +++ b/tests/glue_code/test_run_electrolyzer.py @@ -15,8 +15,6 @@ from electrolyzer.inputs.validation import load_modeling_yaml from electrolyzer.glue_code.optimization import calc_rated_system - - turbine_rating = 3.4 # MW # Create cosine test signal