2929import rainflow
3030from scipy import interpolate
3131
32+ from h2integrate .tools .constants import O2_MW
33+
3234
3335np .set_printoptions (threshold = sys .maxsize )
3436
@@ -174,7 +176,7 @@ def run(self, input_external_power_kw):
174176 cluster_cycling = np .array (cluster_cycling )
175177
176178 # how much to reduce h2 by based on cycling status
177- h2_multiplier = np .where (cluster_cycling > 0 , startup_ratio , 1 )
179+ production_multiplier = np .where (cluster_cycling > 0 , startup_ratio , 1 )
178180 # number of "stacks" on at a single time
179181 self .n_stacks_op = self .max_stacks * self .cluster_status
180182 # n_stacks_op is now either number of pem per cluster or 0 if cluster is off!
@@ -218,10 +220,15 @@ def run(self, input_external_power_kw):
218220 # h20_gal_used_system=self.water_supply(h2_kg_hr_system_init)
219221 p_consumed_max , rated_h2_hr = self .rated_h2_prod ()
220222 # scales h2 production to account for start-up time if going from off->on
221- h2_kg_hr_system = h2_kg_hr_system_init * h2_multiplier
223+ h2_kg_hr_system = h2_kg_hr_system_init * production_multiplier
222224
223225 h20_gal_used_system = self .water_supply (h2_kg_hr_system )
224226
227+ # Get oxygen production and rated used
228+ rated_o2_hr = self .rated_o2_prod ()
229+ o2_kg_hr_system_init = self .o2_production_rate (stack_current , self .n_stacks_op )
230+ o2_kg_hr_system = o2_kg_hr_system_init * production_multiplier
231+
225232 pem_cf = np .sum (h2_kg_hr_system ) / (rated_h2_hr * len (input_power_kw ) * self .max_stacks )
226233 efficiency = self .system_efficiency (input_power_kw , stack_current ) # Efficiency as %-HHV
227234
@@ -231,6 +238,7 @@ def run(self, input_external_power_kw):
231238 h2_results ["hydrogen production no start-up time" ] = h2_kg_hr_system_init
232239 h2_results ["hydrogen_hourly_production" ] = h2_kg_hr_system
233240 h2_results ["water_hourly_usage_kg" ] = h20_gal_used_system * 3.79
241+ h2_results ["oxygen_hourly_production" ] = o2_kg_hr_system
234242 h2_results ["electrolyzer_total_efficiency_perc" ] = efficiency
235243 h2_results ["kwh_per_kgH2" ] = input_power_kw / h2_kg_hr_system
236244 h2_results ["Power Consumed [kWh]" ] = system_power_consumed
@@ -246,13 +254,17 @@ def run(self, input_external_power_kw):
246254 p_consumed_max * self .max_stacks
247255 )
248256 h2_results_aggregates ["Cluster Rated H2 Production [kg/hr]" ] = rated_h2_hr * self .max_stacks
257+ h2_results_aggregates ["Cluster Rated O2 Production [kg/hr]" ] = rated_o2_hr * self .max_stacks
249258 h2_results_aggregates ["gal H20 per kg H2" ] = np .sum (h20_gal_used_system ) / np .sum (
250259 h2_kg_hr_system
251260 )
252261 h2_results_aggregates ["Stack Rated Efficiency [kWh/kg]" ] = p_consumed_max / rated_h2_hr
253262 h2_results_aggregates ["Cluster Rated H2 Production [kg/yr]" ] = (
254263 rated_h2_hr * len (input_power_kw ) * self .max_stacks
255264 )
265+ h2_results_aggregates ["Cluster Rated O2 Production [kg/yr]" ] = (
266+ rated_o2_hr * len (input_power_kw ) * self .max_stacks
267+ )
256268 h2_results_aggregates ["Operational Time / Simulation Time (ratio)" ] = (
257269 self .percent_of_sim_operating
258270 ) # added
@@ -367,13 +379,17 @@ def make_yearly_performance_dict(self, power_in_kW, V_deg, V_cell, I_op, grid_co
367379 cluster_cycling = [0 , * list (np .diff (self .cluster_status ))] # no delay at beginning of sim
368380 cluster_cycling = np .array (cluster_cycling )
369381 startup_ratio = 1 - (600 / 3600 ) # TODO: don't have this hard-coded
370- h2_multiplier = np .where (cluster_cycling > 0 , startup_ratio , 1 )
382+ production_multiplier = np .where (cluster_cycling > 0 , startup_ratio , 1 )
371383
372384 _ , rated_h2_pr_stack_BOL = self .rated_h2_prod ()
385+ rated_o2_pr_stack_BOL = self .rated_o2_prod ()
373386 rated_h2_pr_sim = rated_h2_pr_stack_BOL * self .max_stacks * sim_length
387+ rated_o2_pr_sim = rated_o2_pr_stack_BOL * self .max_stacks * sim_length
374388
375389 kg_h2_pr_sim = np .zeros (int (self .plant_life_years ))
390+ kg_o2_pr_sim = np .zeros (int (self .plant_life_years ))
376391 capfac_per_sim = np .zeros (int (self .plant_life_years ))
392+ o2_capfac_per_sim = np .zeros (int (self .plant_life_years ))
377393 d_sim = np .zeros (int (self .plant_life_years ))
378394 power_pr_yr_kWh = np .zeros (int (self .plant_life_years ))
379395 Vdeg0 = 0
@@ -396,20 +412,28 @@ def make_yearly_performance_dict(self, power_in_kW, V_deg, V_cell, I_op, grid_co
396412 power_in_kW , V_cell , V_deg_pr_sim
397413 )
398414 h2_kg_hr_system_init = self .h2_production_rate (stack_current , self .n_stacks_op )
415+ o2_kg_hr_system_init = self .o2_production_rate (stack_current , self .n_stacks_op )
399416 # total_sim_input_power = self.max_stacks*np.sum(power_in_kW)
400417 power_pr_yr_kWh [i ] = self .max_stacks * np .sum (power_in_kW )
401418 else :
402419 h2_kg_hr_system_init = self .h2_production_rate (I_op , self .n_stacks_op )
403420 h2_kg_hr_system_init = h2_kg_hr_system_init * np .ones (len (power_in_kW ))
421+
422+ o2_kg_hr_system_init = self .o2_production_rate (I_op , self .n_stacks_op )
423+ o2_kg_hr_system_init = o2_kg_hr_system_init * np .ones (len (power_in_kW ))
424+
404425 annual_power_consumed_kWh = (
405426 self .max_stacks * I_op * (V_cell + V_deg_pr_sim ) * self .N_cells / 1000
406427 )
407428 # total_sim_input_power = np.sum(annual_power_consumed_kWh)
408429 power_pr_yr_kWh [i ] = np .sum (annual_power_consumed_kWh )
409430
410- h2_kg_hr_system = h2_kg_hr_system_init * h2_multiplier
431+ h2_kg_hr_system = h2_kg_hr_system_init * production_multiplier
432+ o2_kg_hr_system = o2_kg_hr_system_init * production_multiplier
433+ kg_o2_pr_sim [i ] = np .sum (o2_kg_hr_system )
411434 kg_h2_pr_sim [i ] = np .sum (h2_kg_hr_system )
412435 capfac_per_sim [i ] = np .sum (h2_kg_hr_system ) / rated_h2_pr_sim
436+ o2_capfac_per_sim [i ] = np .sum (o2_kg_hr_system ) / rated_o2_pr_sim
413437 d_sim [i ] = V_deg_pr_sim [sim_length - 1 ]
414438 Vdeg0 = V_deg_pr_sim [sim_length - 1 ]
415439 performance_by_year = {}
@@ -427,7 +451,8 @@ def make_yearly_performance_dict(self, power_in_kW, V_deg, V_cell, I_op, grid_co
427451 zip (year , self .eta_h2_hhv / (power_pr_yr_kWh / kg_h2_pr_sim ))
428452 )
429453 performance_by_year ["Annual Energy Used [kWh/year]" ] = dict (zip (year , power_pr_yr_kWh ))
430-
454+ performance_by_year ["Annual O2 Production [kg/year]" ] = dict (zip (year , kg_o2_pr_sim ))
455+ performance_by_year ["O2 Capacity Factor [-]" ] = dict (zip (year , o2_capfac_per_sim ))
431456 return performance_by_year
432457
433458 def reset_uptime_degradation_rate (self , uptime_hours_until_eol ):
@@ -606,6 +631,22 @@ def rated_h2_prod(self):
606631 max_h2_stack_kg = self .h2_production_rate (I_max , 1 )
607632 return P_consumed_stack_kw , max_h2_stack_kg
608633
634+ def rated_o2_prod (self ):
635+ """Calculates the oxygen production per stack at the rated current in kg
636+
637+ Returns:
638+ float: rated oxygen production in kg/dt
639+ """
640+ i = self .output_dict ["BOL Efficiency Curve Info" ].index [
641+ self .output_dict ["BOL Efficiency Curve Info" ]["Power Sent [kWh]" ]
642+ == self .stack_rating_kW
643+ ]
644+ I_max = self .output_dict ["BOL Efficiency Curve Info" ]["Current" ].iloc [i ].values [0 ]
645+ # I_max = calc_current((self.stack_rating_kW,self.T_C),*self.curve_coeff)
646+
647+ max_o2_stack_kg = self .o2_production_rate (I_max , 1 )
648+ return max_o2_stack_kg
649+
609650 def external_power_supply (self , input_external_power_kw ):
610651 """
611652 External power source (grid or REG) which will need to be stepped
@@ -892,6 +933,33 @@ def h2_production_rate(self, stack_current, n_stacks_op):
892933
893934 return h2_produced_kg_hr_system
894935
936+ def o2_production_rate (self , stack_current , n_stacks_op ):
937+ """Calculate the oxygen production rate of the cluster without warm-up losses.
938+ These equations are based on Faraday's law:
939+
940+ O2 [mol/sec] = eta_F*(n_cells*I)/(4F)
941+
942+ where eta_F is the faradaic efficiency. This can be found in Equation 12 of
943+ `Simple PEM water electrolyser model and experimental validation <http://dx.doi.org/10.1016/j.ijhydene.2011.09.027>`_.
944+
945+ Args:
946+ stack_current (float | np.array): current of the stack in A
947+ n_stacks_op (int | np.array[int]): number of stacks that are operating in the stack.
948+
949+ Returns:
950+ float | np.array: oxygen production profile of the cluster in kg/dt
951+ """
952+ # Calculate the faradaic efficiency
953+ n_Tot = self .faradaic_efficiency (stack_current )
954+ # Faraday's law
955+ o2_production_rate = n_Tot * ((self .N_cells * stack_current ) / (4 * self .F )) # mol/s
956+ # O2_MW is in g/mol
957+ # mol/s * g/mol = g/s
958+ o2_production_rate_g_s = o2_production_rate * O2_MW
959+ o2_produced_kg_dt = o2_production_rate_g_s * self .dt / 1000
960+ o2_produced_kg_dt_system = o2_produced_kg_dt * n_stacks_op
961+ return o2_produced_kg_dt_system
962+
895963 def water_supply (self , h2_kg_hr ):
896964 """
897965 Calculate water supply rate based system efficiency and H2 production
@@ -920,7 +988,7 @@ def run_grid_connected_workaround(self, power_input_signal, current_signal):
920988 cluster_cycling = np .array (cluster_cycling )
921989 power_per_stack = np .where (self .n_stacks_op > 0 , power_input_signal / self .n_stacks_op , 0 )
922990
923- h2_multiplier = np .where (cluster_cycling > 0 , startup_ratio , 1 )
991+ production_multiplier = np .where (cluster_cycling > 0 , startup_ratio , 1 )
924992 self .n_stacks_op = self .max_stacks * self .cluster_status
925993
926994 V_init = self .cell_design (self .T_C , current_signal )
@@ -936,7 +1004,9 @@ def run_grid_connected_workaround(self, power_input_signal, current_signal):
9361004
9371005 h2_kg_hr_system_init = self .h2_production_rate (current_signal , self .n_stacks_op )
9381006 p_consumed_max , rated_h2_hr = self .rated_h2_prod ()
939- h2_kg_hr_system = h2_kg_hr_system_init * h2_multiplier # scales h2 production to account
1007+ h2_kg_hr_system = (
1008+ h2_kg_hr_system_init * production_multiplier
1009+ ) # scales h2 production to account
9401010 # for start-up time if going from off->on
9411011 h20_gal_used_system = self .water_supply (h2_kg_hr_system )
9421012
0 commit comments