Add new PEM and NG solid oxide fuel cell models#794
Conversation
elenya-grant
left a comment
There was a problem hiding this comment.
hi Gen! I really only looked at the PEM_h2_fuel_cell.py method but wanted to give some preliminary feedback. My main thoughts are:
- I think keeping track of what's calculated at the cell level vs stack level vs the system level is a bit confusing in the
compute()method and thecalc_currentfunction. I think some extra in-line comments would be helpful for improving clarity. - I think that there are some mis-matches on units (kg/hr vs kg/dt)
- I think the looping in the
compute()method could be removed. - using
Ifor current andJfor current density would help me follow the logic!
Overall - this is a great start! I haven't dug into the doc page or the examples or tests yet but would love to on a re-review! Let me know if you want to chat about any of my comments sometime! Happy to hop on a call!
| self.f_c = 96485.33 # Faraday's constant in A/mol | ||
| self.M_H2 = 0.002016 # Molar mass of H2 in kg/mol | ||
| self.M_O2 = 0.032 # Molar mass of O2 in kg/mol | ||
| self.M_H2O = 0.018 # Molar mass of H2O in kg/mol | ||
| self.Tref = 298.15 # Standard room temperature in K [25 deg Celsius] | ||
| self.cp_H2 = 14300 # Specific heat of H2 in J/(kg*K) | ||
| self.cp_air = 1005 # Specific heat of air in J/(kg*K) | ||
| self.cp_H2O = 4184 # Specific heat of water in J/(kg*K) | ||
| self.cp_N2 = 1040 # Specific heat of nitrogen in J/(kg*K) | ||
| self.cp_O2 = 918 # Specific heat of oxygen in J/(kg*K) | ||
| self.hhv_h2 = 141.8 * 1e6 # Higher heating value of hydrogen in J/kg | ||
| self.hhv_air = 0 # No higher heating value of air | ||
| self.hhv_H2O = 2260 # Higher heating value of water in J/kg |
There was a problem hiding this comment.
some of these already exist in h2integrate/tools/constants.py. I think you could remove these as attributes and import the constants instead. Some of the constants that exist in constants.py are:
- constants.py var = your variable equivalent
f_c=faradayH_MW=M_H2*1e3/2O2_MW=M_O2*1e3H_MW*2 + (O2_MW/2)=M_H2O/1e3HHV_H2_MJ_PER_KG=hhv_h2/1e6
I think the other constants you have here could be moved to constants.py also!
| desc="Electricity command value for PEM fuel cell", | ||
| ) | ||
|
|
||
| def compute(self, inputs, outputs): |
There was a problem hiding this comment.
I don't think that you need to set any variables as attributes in the compute() method. I think that most instances of self.var = something could be changed to var=something then use var instead of self.var elsewhere.
There was a problem hiding this comment.
I did this locally and it seems like only f_c, M_H2, M_O2, and M_H2O are used.
| # fuel_cell_efficiency_hhv: float = field(validator=range_val(0, 1)) | ||
|
|
||
|
|
||
| def calc_current(power_ref, cell_area, n_cells, stack_number): |
There was a problem hiding this comment.
I may not be fully appreciating what this function does, but I don't think the power_curve part is needed, power = current*voltage. I have some ideas on possible revisions but add those in later.
There was a problem hiding this comment.
I think what's partially confusing about this method is what scale it should be on. like power_ref is the power to the entire system. Then power is divided among stacks. Every cell in the stack has the same current and voltage. below is what I tried to do instead and it gave similar results as your method:
def calc_current_refactor(power_to_system, cell_area, n_cells, n_stacks):
# note: power_to_system should be in W
J_curve = np.array([
0.0356,
0.05413333,
0.0796,
0.11366667,
0.244,
0.454,
]) # A/cm2
voltage_curve = np.array([
0.987,
0.936,
0.884,
0.838,
0.786,
0.736])
# function to calculate voltage from current density
V_coefs = np.polyfit(J_curve, voltage_curve, 5)
V_J_curve = np.poly1d(V_coefs)
# function to calculate current density from power
I_curve = J_curve*cell_area
P_curve = I_curve*(n_cells*voltage_curve)
J_coefs = np.polyfit(P_curve, J_curve, 5)
J_P_curve = np.poly1d(J_coefs)
power_per_stack = power_to_system/n_stacks
stack_current_density = J_P_curve(power_per_stack)
stack_current = stack_current_density*cell_area
cell_voltage = V_J_curve(stack_current_density)
return stack_current, cell_voltage, V_J_curve| I_cell = max(power_I_curve(power_density), 0) | ||
| V_cell = V_I_curve(I_cell) | ||
| I_cell = I_cell * cell_area |
There was a problem hiding this comment.
I may need to work out the units for myself to check the logic here but I think that if you're doing this:
I_cell = I_cell * cell_areathat makes me think that current_curve is actually current density (A/cm2), even though the comment says A. could you clarify whether the I_cell being returned is current or current density and similar question for current_curve?
There was a problem hiding this comment.
You're right, the current curve is for current density! I'll clarify that!
| "oxygen_consumed", | ||
| val=0.0, | ||
| shape=self.n_timesteps, | ||
| units="kg/h", |
There was a problem hiding this comment.
I think the units are f"kg/({self.dt}*s)" for the hydrogen consumed, oxygen consumed, and the water output.
| ) | ||
|
|
||
| # Calculate hydrogen and oxygen consumed | ||
| H2_consumed_rate = ((I_cell * self.N_series * self.M_H2) / (2.0 * self.f_c)) * ( |
There was a problem hiding this comment.
I think it may be nice to split this line up a bit:
h2_consumption_rate_stack =(n_cells * I_cell) / (2 * faradays) # mol/s
h2_consumption_rate_system = n_stacks*h2_consumption_rate_stack*H2_MW/1e3 # kg/s
h2_consumed = h2_consumption_rate_system*self.dt| h2o_generated = np.zeros(self.n_timesteps) | ||
| commodity_out = np.zeros(self.n_timesteps) | ||
|
|
||
| for i in range(self.n_timesteps): |
There was a problem hiding this comment.
I think this loop could be removed?
There was a problem hiding this comment.
here's a potentially alternative approach to the looping
power_demanded = np.clip(inputs[f"{self.commodity}_command_value"], a_min=0.0, a_max=inputs["system_capacity"])
i_stack, v_cell, V_J_curve = calc_current_refactor(power_demanded*1e3, cell_active_area, n_cells, self.config.n_stacks)
h2_in_kg_per_s = inputs["hydrogen_in"]/3600
o2_in_kg_per_s = inputs["oxygen_in"]/3600
# stack current
i_from_h2 = (h2_in_kg_per_s*2*f_c)/(M_H2*self.config.n_stacks*n_cells)
i_from_o2 = (o2_in_kg_per_s*4*f_c)/(M_O2*self.config.n_stacks*n_cells)
i_op = np.minimum.reduce([i_stack, i_from_h2, i_from_o2])
j_op = i_op/cell_active_area
v_op = V_J_curve(j_op)
power_out = v_op*n_cells*i_op*self.config.n_stacks/1e3 # kW
H2_consumed_rate = ((i_op * M_H2) / (2.0 * f_c)) * (
3600 * self.config.n_stacks * n_cells
) # kg/hr
O2_consumed_rate = ((i_op * M_O2) / (4.0 * f_c)) * (
3600 * self.config.n_stacks * n_cells
) # kg/hrnote that calc_current_refactor is a function that I put in an above comment.
| # is n_cells = N_series? | ||
| self.N_series = 1 | ||
| self.stack_size = self.config.system_capacity_kw / self.config.n_stacks | ||
| self.cell_active_area = 400 # [cm^2] from Battelle (https://www.energy.gov/sites/prod/files/2018/02/f49/fcto_battelle_mfg_cost_analysis_1%20_to_25kw_pp_chp_fc_systems_jan2017_0.pdf) |
There was a problem hiding this comment.
should cell active area be recalculated so that stack_size = n_cells*cell_active_area*power_dens?
| # print(self.stack_size, self.n_cells) | ||
|
|
||
| # TODO: | ||
| if H2_consumed_rate > H2in or O2_consumed_rate > O2in: |
There was a problem hiding this comment.
H2in and H2_consumed_rate won't be in the same units for non-hourly timesteps. H2_consumed_rate is in kg/dt but H2in is in kg/h.
Co-authored-by: elenya-grant <116225007+elenya-grant@users.noreply.github.com>
Add new PEM and NG fuel cell models
This PR adds two new fuel cell models to H2I:
Each of these models include an I-V curve and basic reaction modeling to determine the feedstocks consumed.
Section 1: Type of Contribution
Section 2: Draft PR Checklist
TODO:
Type of Reviewer Feedback Requested (on Draft PR)
Structural feedback:
There is some potential overlap here with the steam natural gas reforming model that already exists in the hydrogen folder. In the SO NG fuel cell model, the fuel cell itself still uses a reaction of hydrogen and oxygen, but the natural gas is put through a steam reforming process before the deconstructed H_2 and CO are fed into the fuel cell. The addition of other things in the fuel cell input other than pure hydrogen does affect the fuel cell performance, so I think it's a worthwhile addition to the code on its own. In the model added, natural gas goes into the model, and electricity, water, and carbon dioxide come out.
Let me know your thoughts on possible overlap issues!
Implementation feedback:
Are these models where you would expect them to be in the code?
Thoughts on Solid Oxide Natural Gas model (SONG)? Should it be NGSO (natural gas solid oxide) instead?
Other feedback:
Are these being worked on/needed in/for other projects?
Section 3: General PR Checklist
docs/files are up-to-date, or added when necessaryCHANGELOG.md"A complete thought. [PR XYZ]((https://github.com/NatLabRockies/H2Integrate/pull/XYZ)", where
XYZshould be replaced with the actual number.Section 4: Related Issues
Section 5: Impacted Areas of the Software
Section 5.1: New Files
path/to/file.extensionmethod1: What and why something was changed in one sentence or less.Section 5.2: Modified Files
path/to/file.extensionmethod1: What and why something was changed in one sentence or less.Section 6: Additional Supporting Information
Section 7: Test Results, if applicable
Section 8 (Optional): New Model Checklist
docs/developer_guide/coding_guidelines.mdattrsclass to define theConfigto load in attributes for the modelBaseConfigorCostModelBaseConfiginitialize()method,setup()method,compute()methodCostModelBaseClasssupported_models.pycreate_financial_modelinh2integrate_model.pytest_all_examples.pydocs/user_guide/model_overview.mddocs/section<model_name>.mdis added to the_toc.ymlgenerate_class_hierarchy.pyto update the class hierarchy diagram indocs/developer_guide/class_structure.md