diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f2eef77..498748d50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - Improved readability of the postprocessing printout by simplifying numerical representation, especially for years - Added grid converter performance and cost model which can be used to buy, sell, or buy and sell electricity to/from the grid - Add open-loop load demand controllers: `DemandOpenLoopConverterController` and `FlexibleDemandOpenLoopConverterController` +- Removed a large portion of the old GreenHEART code that was no longer being used ## 0.4.0 [October 1, 2025] diff --git a/docs/_toc.yml b/docs/_toc.yml index 213d1d650..1f79d32b4 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -39,6 +39,7 @@ parts: - file: technology_models/pysam_battery.md - file: technology_models/geologic_hydrogen.md - file: technology_models/grid + - file: technology_models/hydrogen_storage.md - caption: Resource Models chapters: diff --git a/docs/developer_guide/adding_a_new_technology.md b/docs/developer_guide/adding_a_new_technology.md index 15cc7fdd0..053c2e821 100644 --- a/docs/developer_guide/adding_a_new_technology.md +++ b/docs/developer_guide/adding_a_new_technology.md @@ -165,13 +165,14 @@ Here's what the updated `supported_models.py` file looks like with our new solar from h2integrate.converters.solar.solar_pysam import PYSAMSolarPlantPerformanceComponent supported_models = { - 'pysam_solar_plant_performance' : PYSAMSolarPlantPerformanceComponent, - - 'pem_electrolyzer_performance': ElectrolyzerPerformanceModel, - 'pem_electrolyzer_cost': ElectrolyzerCostModel, - - 'eco_pem_electrolyzer_performance': ECOElectrolyzerPerformanceModel, - 'eco_pem_electrolyzer_cost': ECOElectrolyzerCostModel, + "pysam_solar_plant_performance" : PYSAMSolarPlantPerformanceComponent, + + "run_of_river_hydro_performance": RunOfRiverHydroPerformanceModel, + "run_of_river_hydro_cost": RunOfRiverHydroCostModel, + "eco_pem_electrolyzer_performance": ECOElectrolyzerPerformanceModel, + "singlitico_electrolyzer_cost": SingliticoCostModel, + "basic_electrolyzer_cost": BasicElectrolyzerCostModel, + "custom_electrolyzer_cost": CustomElectrolyzerCostModel, ... } diff --git a/docs/technology_models/hydrogen_storage.md b/docs/technology_models/hydrogen_storage.md new file mode 100644 index 000000000..6cf844182 --- /dev/null +++ b/docs/technology_models/hydrogen_storage.md @@ -0,0 +1,49 @@ +# Bulk Hydrogen Storage Cost Model + +## Storage Types + +H2Integrate models at least three types of bulk hydrogen storage technologies: + +- **Underground Pipe Storage**: Hydrogen stored in underground pipeline networks +- **Lined Rock Caverns (LRC)**: Hydrogen stored in rock caverns with engineered linings +- **Salt Caverns**: Hydrogen stored in solution-mined salt caverns + +These storage options provide different cost-capacity relationships suitable for various scales of hydrogen production and distribution. + +## Cost Correlations + +The bulk hydrogen storage costs are modeled as functions of storage capacity using exponential correlations: + +$$Cost = \exp(a(\ln(m))^2 - b\ln(m) + c)$$ + +where $m$ is the useable amount of H₂ stored in tonnes. + +## Installed Capital Cost and Lifetime Storage Cost + +The figures below show how storage costs scale with capacity for different storage technologies: + +![Installed capital cost scaling](images/installed_capital_cost_h2.png) + +*Figure 1a: Installed capital cost (\$/kg-H₂) as a function of usable hydrogen storage capacity* + +![Lifetime storage cost scaling](images/lifetime_storage_cost_h2.png) + +*Figure 1b: Lifetime storage cost (\$/kg-H₂-stored) as a function of usable hydrogen storage capacity* + +## Cost Correlation Coefficients + +### Capital Cost Coefficients (Figure 1a) + +| Storage | a | b | c | +|--------------------------------|----------|---------|--------| +| Underground pipe storage | 0.004161 | 0.06036 | 6.4581 | +| Underground lined rock caverns | 0.095803 | 1.5868 | 10.332 | +| Underground salt caverns | 0.092548 | 1.6432 | 10.161 | + +### Annual Cost Coefficients (Figure 1b) + +| Storage | a | b | c | +|--------------------------------|----------|---------|--------| +| Underground pipe storage | 0.001559 | 0.03531 | 4.5183 | +| Underground lined rock caverns | 0.092286 | 1.5565 | 8.4658 | +| Underground salt caverns | 0.085863 | 1.5574 | 8.1606 | diff --git a/docs/technology_models/images/installed_capital_cost_h2.png b/docs/technology_models/images/installed_capital_cost_h2.png new file mode 100644 index 000000000..fcbfde560 Binary files /dev/null and b/docs/technology_models/images/installed_capital_cost_h2.png differ diff --git a/docs/technology_models/images/lifetime_storage_cost_h2.png b/docs/technology_models/images/lifetime_storage_cost_h2.png new file mode 100644 index 000000000..63a63c36c Binary files /dev/null and b/docs/technology_models/images/lifetime_storage_cost_h2.png differ diff --git a/docs/user_guide/how_to_set_up_an_analysis.ipynb b/docs/user_guide/how_to_set_up_an_analysis.ipynb index 6f28035c2..4a7687e79 100644 --- a/docs/user_guide/how_to_set_up_an_analysis.ipynb +++ b/docs/user_guide/how_to_set_up_an_analysis.ipynb @@ -93,7 +93,6 @@ " hydrogen_dmd:\n", " n_clusters: 13\n", " cluster_rating_MW: 40\n", - " pem_control_type: basic\n", " eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol\n", " uptime_hours_until_eol: 77600 #number of 'on' hours until electrolyzer reaches eol\n", " include_degradation_penalty: True #include degradation\n", diff --git a/docs/user_guide/model_overview.md b/docs/user_guide/model_overview.md index 39d3b19a0..da1768ac0 100644 --- a/docs/user_guide/model_overview.md +++ b/docs/user_guide/model_overview.md @@ -53,6 +53,10 @@ The inputs, outputs, and corresponding technology that are currently available i | `desal` | water | electricity | | `natural_gas` | electricity | natural gas | +```{note} +When the Primary Commodity is electricity, those converters are considered electricity producing technologies and their electricity production is summed for financial calculations. +``` + (transport)= ## Transport `Transport` models are used to either: @@ -149,10 +153,8 @@ Below summarizes the available performance, cost, and financial models for each - combined performance and cost: + `'wombat'` - performance models: - + `'pem_electrolyzer_performance'` + `'eco_pem_electrolyzer_performance'` - cost models: - + `'pem_electrolyzer_cost'` + `'singlitico_electrolyzer_cost'` + `'basic_electrolyzer_cost'` - `geoh2_well_subsurface`: geologic hydrogen well subsurface diff --git a/examples/01_onshore_steel_mn/tech_config.yaml b/examples/01_onshore_steel_mn/tech_config.yaml index ca026ff43..4be21380b 100644 --- a/examples/01_onshore_steel_mn/tech_config.yaml +++ b/examples/01_onshore_steel_mn/tech_config.yaml @@ -29,7 +29,6 @@ technologies: hydrogen_dmd: n_clusters: 18 cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000. #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation @@ -68,102 +67,100 @@ technologies: shared_parameters: capacity_factor: 0.9 plant_capacity_mtpy: 1000000. - cost_parameters: operational_year: 2035 - o2_heat_integration: false + o2_heat_integration: True lcoh: 7.37 - inflation_rate: installation_time: 36 # months inflation_rate: 0.0 # 0 for nominal analysis - feedstocks: - oxygen_market_price: 0.0 # 0.03 $/kgO2 if `o2_heat_integration` == 1 - unused_oxygen: 395 - lime_unitcost: 122.1 - carbon_unitcost: 236.97 - electricity_cost: 48.92 - iron_ore_pellet_unitcost: 207.35 - oxygen_market_price: 0.03 - raw_water_unitcost: 0.59289 - iron_ore_consumption: 1.62927 - raw_water_consumption: 0.80367 - lime_consumption: 0.01812 - carbon_consumption: 0.0538 - hydrogen_consumption: 0.06596 - natural_gas_consumption: 0.71657 - electricity_consumption: 0.5502 - slag_disposal_unitcost: 37.63 - slag_production: 0.17433 - maintenance_materials_unitcost: 7.72 - natural_gas_prices: - "2035": 3.76232 - "2036": 3.776032 - "2037": 3.812906 - "2038": 3.9107960000000004 - "2039": 3.865776 - "2040": 3.9617400000000003 - "2041": 4.027136 - "2042": 4.017166 - "2043": 3.9715339999999997 - "2044": 3.924314 - "2045": 3.903287 - "2046": 3.878192 - "2047": 3.845413 - "2048": 3.813366 - "2049": 3.77735 - "2050": 3.766164 - "2051": 3.766164 - "2052": 3.766164 - "2053": 3.766164 - "2054": 3.766164 - "2055": 3.766164 - "2056": 3.766164 - "2057": 3.766164 - "2058": 3.766164 - "2059": 3.766164 - "2060": 3.766164 - "2061": 3.766164 - "2062": 3.766164 - "2063": 3.766164 - "2064": 3.766164 - finances: - # plant_life: 30 - grid_prices: - "2035": 89.42320514456621 - "2036": 89.97947569251141 - "2037": 90.53574624045662 - "2038": 91.09201678840184 - "2039": 91.64828733634704 - "2040": 92.20455788429224 - "2041": 89.87291235917809 - "2042": 87.54126683406393 - "2043": 85.20962130894978 - "2044": 82.87797578383562 - "2045": 80.54633025872147 - "2046": 81.38632144593608 - "2047": 82.22631263315068 - "2048": 83.0663038203653 - "2049": 83.90629500757991 - "2050": 84.74628619479452 - "2051": 84.74628619479452 - "2052": 84.74628619479452 - "2053": 84.74628619479452 - "2054": 84.74628619479452 - "2055": 84.74628619479452 - "2056": 84.74628619479452 - "2057": 84.74628619479452 - "2058": 84.74628619479452 - "2059": 84.74628619479452 - "2060": 84.74628619479452 - "2061": 84.74628619479452 - "2062": 84.74628619479452 - "2063": 84.74628619479452 - "2064": 84.74628619479452 - - # Additional parameters passed to ProFAST - financial_assumptions: - "total income tax rate": 0.2574 - "capital gains tax rate": 0.15 - "leverage after tax nominal discount rate": 0.10893 - "debt equity ratio of initial financing": 0.624788 - "debt interest rate": 0.050049 + # Feedstock parameters (flattened) + excess_oxygen: 395 + lime_unitcost: 122.1 + lime_transport_cost: 0.0 + carbon_unitcost: 236.97 + carbon_transport_cost: 0.0 + electricity_cost: 48.92 + iron_ore_pellet_unitcost: 207.35 + iron_ore_pellet_transport_cost: 0.0 + oxygen_market_price: 0.03 + raw_water_unitcost: 0.59289 + iron_ore_consumption: 1.62927 + raw_water_consumption: 0.80367 + lime_consumption: 0.01812 + carbon_consumption: 0.0538 + hydrogen_consumption: 0.06596 + natural_gas_consumption: 0.71657 + electricity_consumption: 0.5502 + slag_disposal_unitcost: 37.63 + slag_production: 0.17433 + maintenance_materials_unitcost: 7.72 + natural_gas_prices: + "2035": 3.76232 + "2036": 3.776032 + "2037": 3.812906 + "2038": 3.9107960000000004 + "2039": 3.865776 + "2040": 3.9617400000000003 + "2041": 4.027136 + "2042": 4.017166 + "2043": 3.9715339999999997 + "2044": 3.924314 + "2045": 3.903287 + "2046": 3.878192 + "2047": 3.845413 + "2048": 3.813366 + "2049": 3.77735 + "2050": 3.766164 + "2051": 3.766164 + "2052": 3.766164 + "2053": 3.766164 + "2054": 3.766164 + "2055": 3.766164 + "2056": 3.766164 + "2057": 3.766164 + "2058": 3.766164 + "2059": 3.766164 + "2060": 3.766164 + "2061": 3.766164 + "2062": 3.766164 + "2063": 3.766164 + "2064": 3.766164 + # Financial parameters (flattened) + grid_prices: + "2035": 89.42320514456621 + "2036": 89.97947569251141 + "2037": 90.53574624045662 + "2038": 91.09201678840184 + "2039": 91.64828733634704 + "2040": 92.20455788429224 + "2041": 89.87291235917809 + "2042": 87.54126683406393 + "2043": 85.20962130894978 + "2044": 82.87797578383562 + "2045": 80.54633025872147 + "2046": 81.38632144593608 + "2047": 82.22631263315068 + "2048": 83.0663038203653 + "2049": 83.90629500757991 + "2050": 84.74628619479452 + "2051": 84.74628619479452 + "2052": 84.74628619479452 + "2053": 84.74628619479452 + "2054": 84.74628619479452 + "2055": 84.74628619479452 + "2056": 84.74628619479452 + "2057": 84.74628619479452 + "2058": 84.74628619479452 + "2059": 84.74628619479452 + "2060": 84.74628619479452 + "2061": 84.74628619479452 + "2062": 84.74628619479452 + "2063": 84.74628619479452 + "2064": 84.74628619479452 + # Financial assumptions + financial_assumptions: + "total income tax rate": 0.2574 + "capital gains tax rate": 0.15 + "leverage after tax nominal discount rate": 0.10893 + "debt equity ratio of initial financing": 0.624788 + "debt interest rate": 0.050049 diff --git a/examples/02_texas_ammonia/tech_config.yaml b/examples/02_texas_ammonia/tech_config.yaml index 5c22caf07..76eb972fc 100644 --- a/examples/02_texas_ammonia/tech_config.yaml +++ b/examples/02_texas_ammonia/tech_config.yaml @@ -29,7 +29,6 @@ technologies: hydrogen_dmd: n_clusters: 16 cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 10 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000 #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/03_methanol/co2_hydrogenation/tech_config_co2h.yaml b/examples/03_methanol/co2_hydrogenation/tech_config_co2h.yaml index f90f58190..43158f3ef 100644 --- a/examples/03_methanol/co2_hydrogenation/tech_config_co2h.yaml +++ b/examples/03_methanol/co2_hydrogenation/tech_config_co2h.yaml @@ -28,7 +28,6 @@ technologies: hydrogen_dmd: n_clusters: 4 cluster_rating_MW: 39 - pem_control_type: 'basic' eol_eff_percent_loss: 10 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000 #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/03_methanol/co2_hydrogenation_doc/tech_config_co2h.yaml b/examples/03_methanol/co2_hydrogenation_doc/tech_config_co2h.yaml index d412ccde2..2f3573552 100644 --- a/examples/03_methanol/co2_hydrogenation_doc/tech_config_co2h.yaml +++ b/examples/03_methanol/co2_hydrogenation_doc/tech_config_co2h.yaml @@ -36,7 +36,6 @@ technologies: hydrogen_dmd: n_clusters: 4 cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 10 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000 #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/05_wind_h2_opt/tech_config.yaml b/examples/05_wind_h2_opt/tech_config.yaml index a1226db2e..17625aef9 100644 --- a/examples/05_wind_h2_opt/tech_config.yaml +++ b/examples/05_wind_h2_opt/tech_config.yaml @@ -45,7 +45,6 @@ technologies: hydrogen_dmd: cluster_rating_MW: 20 n_clusters: 25 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000. #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/08_wind_electrolyzer/tech_config.yaml b/examples/08_wind_electrolyzer/tech_config.yaml index 546554de7..e74dba46a 100644 --- a/examples/08_wind_electrolyzer/tech_config.yaml +++ b/examples/08_wind_electrolyzer/tech_config.yaml @@ -46,7 +46,6 @@ technologies: hydrogen_dmd: n_clusters: 13 #should be 12.5 to get 500 MW cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000. #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/10_electrolyzer_om/tech_config.yaml b/examples/10_electrolyzer_om/tech_config.yaml index b699cdfe4..238cfc356 100644 --- a/examples/10_electrolyzer_om/tech_config.yaml +++ b/examples/10_electrolyzer_om/tech_config.yaml @@ -44,7 +44,6 @@ technologies: hydrogen_dmd: n_clusters: 1 cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000. #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/12_ammonia_synloop/tech_config.yaml b/examples/12_ammonia_synloop/tech_config.yaml index 13b3bc4e9..5fcb1f4a2 100644 --- a/examples/12_ammonia_synloop/tech_config.yaml +++ b/examples/12_ammonia_synloop/tech_config.yaml @@ -29,7 +29,6 @@ technologies: hydrogen_dmd: n_clusters: 16 cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 10 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000 #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/14_wind_hydrogen_dispatch/inputs/tech_config.yaml b/examples/14_wind_hydrogen_dispatch/inputs/tech_config.yaml index 1228fa326..c8af0c941 100644 --- a/examples/14_wind_hydrogen_dispatch/inputs/tech_config.yaml +++ b/examples/14_wind_hydrogen_dispatch/inputs/tech_config.yaml @@ -45,7 +45,6 @@ technologies: hydrogen_dmd: n_clusters: 18 cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000. #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/15_wind_solar_electrolyzer/tech_config.yaml b/examples/15_wind_solar_electrolyzer/tech_config.yaml index 19ccd1ee2..f955555be 100644 --- a/examples/15_wind_solar_electrolyzer/tech_config.yaml +++ b/examples/15_wind_solar_electrolyzer/tech_config.yaml @@ -81,7 +81,6 @@ technologies: hydrogen_dmd: n_clusters: 18 cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000. #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/17_splitter_wind_doc_h2/tech_config.yaml b/examples/17_splitter_wind_doc_h2/tech_config.yaml index ea576a146..d518934ec 100644 --- a/examples/17_splitter_wind_doc_h2/tech_config.yaml +++ b/examples/17_splitter_wind_doc_h2/tech_config.yaml @@ -68,7 +68,6 @@ technologies: hydrogen_dmd: n_clusters: 4 #should be 12.5 to get 500 MW cluster_rating_MW: 20 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000. #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/examples/20_solar_electrolyzer_doe/tech_config.yaml b/examples/20_solar_electrolyzer_doe/tech_config.yaml index cef47354f..4289076c3 100644 --- a/examples/20_solar_electrolyzer_doe/tech_config.yaml +++ b/examples/20_solar_electrolyzer_doe/tech_config.yaml @@ -42,7 +42,6 @@ technologies: hydrogen_dmd: n_clusters: 18 cluster_rating_MW: 10 - pem_control_type: 'basic' eol_eff_percent_loss: 10 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 80000. #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/h2integrate/converters/hydrogen/basic_cost_model.py b/h2integrate/converters/hydrogen/basic_cost_model.py index bb7071b2a..d91d793d2 100644 --- a/h2integrate/converters/hydrogen/basic_cost_model.py +++ b/h2integrate/converters/hydrogen/basic_cost_model.py @@ -1,11 +1,11 @@ +import warnings + +import numpy as np from attrs import field, define from h2integrate.core.utilities import CostModelBaseConfig, merge_shared_inputs from h2integrate.core.validators import gt_zero, contains, must_equal from h2integrate.converters.hydrogen.electrolyzer_baseclass import ElectrolyzerCostBaseClass -from h2integrate.simulation.technologies.hydrogen.electrolysis.H2_cost_model import ( - basic_H2_cost_model, -) @define @@ -49,11 +49,11 @@ def setup(self): def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # unpack inputs - plant_config = self.options["plant_config"] + self.options["plant_config"] - total_hydrogen_produced = float(inputs["total_hydrogen_produced"]) - electrolyzer_size_mw = inputs["electrolyzer_rating_mw"][0] - useful_life = plant_config["plant"]["plant_life"] + electrolyzer_size_mw = float(inputs["electrolyzer_size_mw"][0]) + electrical_generation_timeseries_kw = inputs["electricity_in"] + electrolyzer_capex_kw = self.config.electrolyzer_capex # run hydrogen production cost model - from hopp examples if self.config.location == "onshore": @@ -61,25 +61,92 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): else: offshore = 1 - ( - electrolyzer_total_capital_cost, - electrolyzer_OM_cost, - electrolyzer_capex_kw, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) = basic_H2_cost_model( - self.config.electrolyzer_capex, - self.config.time_between_replacement, - electrolyzer_size_mw, - useful_life, - inputs["electricity_in"], - total_hydrogen_produced, - 0.0, - 0.0, - include_refurb_in_opex=False, - offshore=offshore, + # Basic cost modeling for a PEM electrolyzer. + # Looking at cost projections for PEM electrolyzers over years 2022, 2025, 2030, 2035. + # Electricity costs are calculated outside of hydrogen cost model + + # Basic information in our analysis + kw_continuous = electrolyzer_size_mw * 1000 + + # Capacity factor + avg_generation = np.mean(electrical_generation_timeseries_kw) # Avg Generation + cap_factor = avg_generation / kw_continuous + + if cap_factor > 1.0: + cap_factor = 1.0 + warnings.warn( + "Electrolyzer capacity factor would be greater than 1 with provided energy profile." + " Capacity factor has been reduced to 1 for electrolyzer cost estimate purposes." + ) + + # Hydrogen Production Cost From PEM Electrolysis - 2019 (HFTO Program Record) + # https://www.hydrogen.energy.gov/pdfs/19009_h2_production_cost_pem_electrolysis_2019.pdf + + # Capital costs provide by Hydrogen Production Cost From PEM Electrolysis - 2019 (HFTO + # Program Record) + mechanical_bop_cost = 36 # [$/kW] for a compressor + electrical_bop_cost = 82 # [$/kW] for a rectifier + + # Installed capital cost + stack_installation_factor = 12 / 100 # [%] for stack cost + elec_installation_factor = 12 / 100 # [%] and electrical BOP + + # scale installation fraction if offshore (see Singlitico 2021 https://doi.org/10.1016/j.rset.2021.100005) + stack_installation_factor *= 1 + offshore + elec_installation_factor *= 1 + offshore + + # mechanical BOP install cost = 0% + + # Indirect capital cost as a percentage of installed capital cost + site_prep = 2 / 100 # [%] + engineering_design = 10 / 100 # [%] + project_contingency = 15 / 100 # [%] + permitting = 15 / 100 # [%] + land = 250000 # [$] + + total_direct_electrolyzer_cost_kw = ( + (electrolyzer_capex_kw * (1 + stack_installation_factor)) + + mechanical_bop_cost + + (electrical_bop_cost * (1 + elec_installation_factor)) ) + # Assign CapEx for electrolyzer from capacity based installed CapEx + electrolyzer_total_installed_capex = ( + total_direct_electrolyzer_cost_kw * electrolyzer_size_mw * 1000 + ) + + # Add indirect capital costs + electrolyzer_total_capital_cost = ( + ( + (site_prep + engineering_design + project_contingency + permitting) + * electrolyzer_total_installed_capex + ) + + land + + electrolyzer_total_installed_capex + ) + + # O&M costs + # https://www.sciencedirect.com/science/article/pii/S2542435121003068 + # for 700 MW electrolyzer (https://www.hydrogen.energy.gov/pdfs/19009_h2_production_cost_pem_electrolysis_2019.pdf) + h2_FOM_kg = 0.24 # [$/kg] + + # linearly scaled current central fixed O&M for a 700MW electrolyzer up to a + # 1000MW electrolyzer + scaled_h2_FOM_kg = h2_FOM_kg * electrolyzer_size_mw / 700 + + h2_FOM_kWh = scaled_h2_FOM_kg / 55.5 # [$/kWh] used 55.5 kWh/kg for efficiency + fixed_OM = h2_FOM_kWh * 8760 # [$/kW-y] + property_tax_insurance = 1.5 / 100 # [% of Cap/y] + variable_OM = 1.30 # [$/MWh] + + # Total O&M costs [% of installed cap/year] + total_OM_costs = ( + fixed_OM + (property_tax_insurance * total_direct_electrolyzer_cost_kw) + ) / total_direct_electrolyzer_cost_kw + ( + variable_OM / 1000 * 8760 * (cap_factor / total_direct_electrolyzer_cost_kw) + ) + + electrolyzer_OM_cost = electrolyzer_total_installed_capex * total_OM_costs # Capacity based + outputs["CapEx"] = electrolyzer_total_capital_cost outputs["OpEx"] = electrolyzer_OM_cost diff --git a/h2integrate/converters/hydrogen/eco_tools_pem_electrolyzer.py b/h2integrate/converters/hydrogen/eco_tools_pem_electrolyzer.py deleted file mode 100644 index de64729f0..000000000 --- a/h2integrate/converters/hydrogen/eco_tools_pem_electrolyzer.py +++ /dev/null @@ -1,142 +0,0 @@ -from attrs import field, define - -from h2integrate.core.utilities import BaseConfig, merge_shared_inputs -from h2integrate.core.validators import gt_zero, contains -from h2integrate.tools.eco.utilities import ceildiv -from h2integrate.converters.hydrogen.electrolyzer_baseclass import ElectrolyzerPerformanceBaseClass -from h2integrate.simulation.technologies.hydrogen.electrolysis.run_h2_PEM import run_h2_PEM - - -@define -class ECOElectrolyzerPerformanceModelConfig(BaseConfig): - """ - Configuration class for the ECOElectrolyzerPerformanceModel. - - Args: - sizing (dict): A dictionary containing the following model sizing parameters: - - resize_for_enduse (bool): Flag to adjust the electrolyzer based on the enduse. - - size_for (str): Determines the sizing strategy, either "BOL" (generous), or - "EOL" (conservative). - - hydrogen_dmd (#TODO): #TODO - n_clusters (int): number of electrolyzer clusters within the system. - location (str): The location of the electrolyzer; options include "onshore" or "offshore". - cluster_rating_MW (float): The rating of the clusters that the electrolyzer is grouped - into, in MW. - pem_control_type (str): The control strategy to be used by the electrolyzer. - eol_eff_percent_loss (float): End-of-life (EOL) defined as a percent change in efficiency - from beginning-of-life (BOL). - uptime_hours_until_eol (int): Number of "on" hours until the electrolyzer reaches EOL. - include_degradation_penalty (bool): Flag to include degradation of the electrolyzer due to - operational hours, ramping, and on/off power cycles. - turndown_ratio (float): The ratio at which the electrolyzer will shut down. - electrolyzer_capex (int): $/kW overnight installed capital costs for a 1 MW system in - 2022 USD/kW (DOE hydrogen program record 24005 Clean Hydrogen Production Cost Scenarios - with PEM Electrolyzer Technology 05/20/24) #TODO: convert to refs - (https://www.hydrogen.energy.gov/docs/hydrogenprogramlibraries/pdfs/24005-clean-hydrogen-production-cost-pem-electrolyzer.pdf?sfvrsn=8cb10889_1) - """ - - sizing: dict = field() - n_clusters: int = field(validator=gt_zero) - location: str = field(validator=contains(["onshore", "offshore"])) - cluster_rating_MW: float = field(validator=gt_zero) - pem_control_type: str = field(validator=contains(["basic"])) - eol_eff_percent_loss: float = field(validator=gt_zero) - uptime_hours_until_eol: int = field(validator=gt_zero) - include_degradation_penalty: bool = field() - turndown_ratio: float = field(validator=gt_zero) - electrolyzer_capex: int = field() - - -class ECOElectrolyzerPerformanceModel(ElectrolyzerPerformanceBaseClass): - """ - An OpenMDAO component that wraps the PEM electrolyzer model. - Takes electricity input and outputs hydrogen and oxygen generation rates. - """ - - def setup(self): - super().setup() - self.config = ECOElectrolyzerPerformanceModelConfig.from_dict( - merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), - strict=False, - ) - self.add_output("efficiency", val=0.0, desc="Average efficiency of the electrolyzer") - self.add_output( - "rated_h2_production_kg_pr_hr", - val=0.0, - units="kg/h", - desc="Rated hydrogen production of system in kg/hour", - ) - - self.add_input( - "n_clusters", - val=self.config.n_clusters, - units="unitless", - desc="number of electrolyzer clusters in the system", - ) - - self.add_output( - "electrolyzer_size_mw", - val=0.0, - units="MW", - desc="Size of the electrolyzer in MW", - ) - - def compute(self, inputs, outputs): - plant_life = self.options["plant_config"]["plant"]["plant_life"] - electrolyzer_size_mw = inputs["n_clusters"][0] * self.config.cluster_rating_MW - electrolyzer_capex_kw = self.config.electrolyzer_capex - - # # IF GRID CONNECTED - # if plant_config["plant"]["grid_connection"]: - # # NOTE: if grid-connected, it assumes that hydrogen demand is input and there is not - # # multi-cluster control strategies. - # This capability exists at the cluster level, not at the - # # system level. - # if config["sizing"]["hydrogen_dmd"] is not None: - # grid_connection_scenario = "grid-only" - # hydrogen_production_capacity_required_kgphr = config[ - # "sizing" - # ]["hydrogen_dmd"] - # energy_to_electrolyzer_kw = [] - # else: - # grid_connection_scenario = "off-grid" - # hydrogen_production_capacity_required_kgphr = [] - # energy_to_electrolyzer_kw = np.ones(8760) * electrolyzer_size_mw * 1e3 - # # IF NOT GRID CONNECTED - # else: - hydrogen_production_capacity_required_kgphr = [] - grid_connection_scenario = "off-grid" - energy_to_electrolyzer_kw = inputs["electricity_in"] - - n_pem_clusters = int(ceildiv(electrolyzer_size_mw, self.config.cluster_rating_MW)) - - electrolyzer_actual_capacity_MW = n_pem_clusters * self.config.cluster_rating_MW - ## run using greensteel model - pem_param_dict = { - "eol_eff_percent_loss": self.config.eol_eff_percent_loss, - "uptime_hours_until_eol": self.config.uptime_hours_until_eol, - "include_degradation_penalty": self.config.include_degradation_penalty, - "turndown_ratio": self.config.turndown_ratio, - } - - H2_Results, h2_ts, h2_tot, power_to_electrolyzer_kw = run_h2_PEM( - electrical_generation_timeseries=energy_to_electrolyzer_kw, - electrolyzer_size=electrolyzer_size_mw, - useful_life=plant_life, - n_pem_clusters=n_pem_clusters, - pem_control_type=self.config.pem_control_type, - electrolyzer_direct_cost_kw=electrolyzer_capex_kw, - user_defined_pem_param_dictionary=pem_param_dict, - grid_connection_scenario=grid_connection_scenario, # if not offgrid, assumes steady h2 demand in kgphr for full year # noqa: E501 - hydrogen_production_capacity_required_kgphr=hydrogen_production_capacity_required_kgphr, - debug_mode=False, - verbose=False, - ) - - # Assuming `h2_results` includes hydrogen and oxygen rates per timestep - outputs["hydrogen_out"] = H2_Results["Hydrogen Hourly Production [kg/hr]"] - outputs["total_hydrogen_produced"] = H2_Results["Life: Annual H2 production [kg/year]"] - outputs["efficiency"] = H2_Results["Sim: Average Efficiency [%-HHV]"] - outputs["time_until_replacement"] = H2_Results["Time Until Replacement [hrs]"] - outputs["rated_h2_production_kg_pr_hr"] = H2_Results["Rated BOL: H2 Production [kg/hr]"] - outputs["electrolyzer_size_mw"] = electrolyzer_actual_capacity_MW diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index 23583c325..f69d9475c 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -1,30 +1,52 @@ +import math + from attrs import field, define -from h2integrate.core.utilities import BaseConfig, CostModelBaseConfig, merge_shared_inputs -from h2integrate.core.validators import must_equal -from h2integrate.converters.hydrogen.electrolyzer_baseclass import ( - ElectrolyzerCostBaseClass, - ElectrolyzerPerformanceBaseClass, -) -from h2integrate.simulation.technologies.hydrogen.electrolysis import ( - PEM_H2_LT_electrolyzer_Clusters, -) -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_costs_Singlitico_model import ( - PEMCostsSingliticoModel, -) +from h2integrate.core.utilities import BaseConfig, merge_shared_inputs +from h2integrate.core.validators import gt_zero, contains +from h2integrate.converters.hydrogen.pem_model.run_h2_PEM import run_h2_PEM +from h2integrate.converters.hydrogen.electrolyzer_baseclass import ElectrolyzerPerformanceBaseClass @define -class ElectrolyzerPerformanceModelConfig(BaseConfig): - cluster_size_mw: float = field() - plant_life: int = field() - eol_eff_percent_loss: float = field() - uptime_hours_until_eol: int = field() +class ECOElectrolyzerPerformanceModelConfig(BaseConfig): + """ + Configuration class for the ECOElectrolyzerPerformanceModel. + + Args: + sizing (dict): A dictionary containing the following model sizing parameters: + - resize_for_enduse (bool): Flag to adjust the electrolyzer based on the enduse. + - size_for (str): Determines the sizing strategy, either "BOL" (generous), or + "EOL" (conservative). + - hydrogen_dmd (#TODO): #TODO + n_clusters (int): number of electrolyzer clusters within the system. + location (str): The location of the electrolyzer; options include "onshore" or "offshore". + cluster_rating_MW (float): The rating of the clusters that the electrolyzer is grouped + into, in MW. + eol_eff_percent_loss (float): End-of-life (EOL) defined as a percent change in efficiency + from beginning-of-life (BOL). + uptime_hours_until_eol (int): Number of "on" hours until the electrolyzer reaches EOL. + include_degradation_penalty (bool): Flag to include degradation of the electrolyzer due to + operational hours, ramping, and on/off power cycles. + turndown_ratio (float): The ratio at which the electrolyzer will shut down. + electrolyzer_capex (int): $/kW overnight installed capital costs for a 1 MW system in + 2022 USD/kW (DOE hydrogen program record 24005 Clean Hydrogen Production Cost Scenarios + with PEM Electrolyzer Technology 05/20/24) #TODO: convert to refs + (https://www.hydrogen.energy.gov/docs/hydrogenprogramlibraries/pdfs/24005-clean-hydrogen-production-cost-pem-electrolyzer.pdf?sfvrsn=8cb10889_1) + """ + + sizing: dict = field() + n_clusters: int = field(validator=gt_zero) + location: str = field(validator=contains(["onshore", "offshore"])) + cluster_rating_MW: float = field(validator=gt_zero) + eol_eff_percent_loss: float = field(validator=gt_zero) + uptime_hours_until_eol: int = field(validator=gt_zero) include_degradation_penalty: bool = field() - turndown_ratio: float = field() + turndown_ratio: float = field(validator=gt_zero) + electrolyzer_capex: int = field() -class ElectrolyzerPerformanceModel(ElectrolyzerPerformanceBaseClass): +class ECOElectrolyzerPerformanceModel(ElectrolyzerPerformanceBaseClass): """ An OpenMDAO component that wraps the PEM electrolyzer model. Takes electricity input and outputs hydrogen and oxygen generation rates. @@ -32,70 +54,68 @@ class ElectrolyzerPerformanceModel(ElectrolyzerPerformanceBaseClass): def setup(self): super().setup() - self.config = ElectrolyzerPerformanceModelConfig.from_dict( - merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") + self.config = ECOElectrolyzerPerformanceModelConfig.from_dict( + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=False, ) - self.electrolyzer = PEM_H2_LT_electrolyzer_Clusters( - self.config.cluster_size_mw, - self.config.plant_life, - self.config.eol_eff_percent_loss, - self.config.uptime_hours_until_eol, - self.config.include_degradation_penalty, - self.config.turndown_ratio, + self.add_output("efficiency", val=0.0, desc="Average efficiency of the electrolyzer") + self.add_output( + "rated_h2_production_kg_pr_hr", + val=0.0, + units="kg/h", + desc="Rated hydrogen production of system in kg/hour", ) - self.add_input("cluster_size", val=1.0, units="MW") - def compute(self, inputs, outputs): - # Run the PEM electrolyzer model using the input power signal - self.electrolyzer.max_stacks = inputs["cluster_size"] - h2_results, h2_results_aggregates = self.electrolyzer.run(inputs["electricity_in"]) - - # Assuming `h2_results` includes hydrogen and oxygen rates per timestep - outputs["hydrogen_out"] = h2_results["hydrogen_hourly_production"] - outputs["total_hydrogen_produced"] = h2_results_aggregates["Total H2 Production [kg]"] - - -@define -class ElectrolyzeCostModelConfig(CostModelBaseConfig): - cluster_size_mw: float = field() - electrolyzer_cost: float = field() - cost_year: int = field(default=2021, converter=int, validator=must_equal(2021)) - - -class ElectrolyzerCostModel(ElectrolyzerCostBaseClass): - """ - An OpenMDAO component that computes the cost of a PEM electrolyzer cluster - using PEMCostsSinglicitoModel which outputs costs in 2021 USD. - """ - - def setup(self): - self.cost_model = PEMCostsSingliticoModel(elec_location=1) - # Define inputs: electrolyzer capacity and reference cost - self.config = ElectrolyzeCostModelConfig.from_dict( - merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") + self.add_input( + "n_clusters", + val=self.config.n_clusters, + units="unitless", + desc="number of electrolyzer clusters in the system", ) - super().setup() - self.add_input( - "P_elec", - val=self.config.cluster_size_mw, + self.add_output( + "electrolyzer_size_mw", + val=0.0, units="MW", - desc="Nominal capacity of the electrolyzer", + desc="Size of the electrolyzer in MW", ) - self.add_input( - "RC_elec", - val=self.config.electrolyzer_cost, - units="MUSD/GW", - desc="Reference cost of the electrolyzer", - ) - - def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - # Call the cost model to compute costs - P_elec = inputs["P_elec"] * 1.0e-3 # Convert MW to GW - RC_elec = inputs["RC_elec"] - cost_model = self.cost_model - capex, opex = cost_model.run(P_elec, RC_elec) + def compute(self, inputs, outputs): + plant_life = self.options["plant_config"]["plant"]["plant_life"] + electrolyzer_size_mw = inputs["n_clusters"][0] * self.config.cluster_rating_MW + electrolyzer_capex_kw = self.config.electrolyzer_capex + + hydrogen_production_capacity_required_kgphr = [] + grid_connection_scenario = "off-grid" + energy_to_electrolyzer_kw = inputs["electricity_in"] + + n_pem_clusters = int(math.ceil(electrolyzer_size_mw / self.config.cluster_rating_MW)) + + electrolyzer_actual_capacity_MW = n_pem_clusters * self.config.cluster_rating_MW + pem_param_dict = { + "eol_eff_percent_loss": self.config.eol_eff_percent_loss, + "uptime_hours_until_eol": self.config.uptime_hours_until_eol, + "include_degradation_penalty": self.config.include_degradation_penalty, + "turndown_ratio": self.config.turndown_ratio, + } + + H2_Results, h2_ts, h2_tot, power_to_electrolyzer_kw = run_h2_PEM( + electrical_generation_timeseries=energy_to_electrolyzer_kw, + electrolyzer_size=electrolyzer_size_mw, + useful_life=plant_life, + n_pem_clusters=n_pem_clusters, + electrolyzer_direct_cost_kw=electrolyzer_capex_kw, + user_defined_pem_param_dictionary=pem_param_dict, + grid_connection_scenario=grid_connection_scenario, # if not offgrid, assumes steady h2 demand in kgphr for full year # noqa: E501 + hydrogen_production_capacity_required_kgphr=hydrogen_production_capacity_required_kgphr, + debug_mode=False, + verbose=False, + ) - outputs["CapEx"] = capex * 1.0e-6 # Convert to MUSD - outputs["OpEx"] = opex * 1.0e-6 # Convert to MUSD + # Assuming `h2_results` includes hydrogen and oxygen rates per timestep + outputs["hydrogen_out"] = H2_Results["Hydrogen Hourly Production [kg/hr]"] + outputs["total_hydrogen_produced"] = H2_Results["Life: Annual H2 production [kg/year]"] + outputs["efficiency"] = H2_Results["Sim: Average Efficiency [%-HHV]"] + outputs["time_until_replacement"] = H2_Results["Time Until Replacement [hrs]"] + outputs["rated_h2_production_kg_pr_hr"] = H2_Results["Rated BOL: H2 Production [kg/hr]"] + outputs["electrolyzer_size_mw"] = electrolyzer_actual_capacity_MW diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_H2_LT_electrolyzer_Clusters.py b/h2integrate/converters/hydrogen/pem_model/PEM_H2_LT_electrolyzer_Clusters.py similarity index 87% rename from h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_H2_LT_electrolyzer_Clusters.py rename to h2integrate/converters/hydrogen/pem_model/PEM_H2_LT_electrolyzer_Clusters.py index eeefa4b1c..cd09dc742 100644 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_H2_LT_electrolyzer_Clusters.py +++ b/h2integrate/converters/hydrogen/pem_model/PEM_H2_LT_electrolyzer_Clusters.py @@ -27,7 +27,6 @@ import pandas as pd import rainflow from scipy import interpolate -from matplotlib import pyplot as plt np.set_printoptions(threshold=sys.maxsize) @@ -992,202 +991,3 @@ def run_grid_connected_workaround(self, power_input_signal, current_signal): h2_results_aggregates["Time until replacement [hours]"] = time_until_replacement return h2_results, h2_results_aggregates - - -if __name__ == "__main__": - cluster_size_mw = 1 # MW - plant_life = 30 # years - electrolyzer_model_parameters = { - "eol_eff_percent_loss": 10, # percent - for calculating EOL voltage and steady deg rate - "uptime_hours_until_eol": 77600, # for calculating steady deg rate - "include_degradation_penalty": True, - "turndown_ratio": 0.1, - "curve_coeff": [ - 4.0519644766515644e-08, - -0.00026186723338675105, - 3.8985774154190334, - 7.615382921418666, - -20.075110413404484, - 1.0, - ], - } - # Create PEM and initialize parameters - pem = PEM_H2_Clusters(cluster_size_mw, plant_life, **electrolyzer_model_parameters) - # ----- Run off-grid case ----- - # Make a mock input power signal - hours_in_year = 8760 - power_signal_kW_ramp_up = np.arange(0, 1000 * cluster_size_mw, 20) - power_signal_kW_ramp_down = np.flip(power_signal_kW_ramp_up) - power_signal_kW_ramp_updown = np.concatenate( - (power_signal_kW_ramp_down, power_signal_kW_ramp_up) - ) - n_repeats = int(np.ceil(hours_in_year / len(power_signal_kW_ramp_updown))) - power_signal_kW = np.tile(power_signal_kW_ramp_updown, n_repeats) - # Run the electrolyzer and get outputs - h2_results, h2_results_aggregates = pem.run(power_signal_kW) - - # ----- Run grid-connected case ----- - h2_kg_hr_system_required = 12 # kg-H2/hr - target_h2_per_year = h2_kg_hr_system_required * hours_in_year - power_required_kW, stack_current_signal = pem.grid_connected_func(h2_kg_hr_system_required) - h2_results_grid, h2_results_aggregates_grid = pem.run_grid_connected_workaround( - power_required_kW, stack_current_signal - ) - - # Refurbishment Schedule (RS) [MW/year] and Year of Replacement (YOR) - offgrid_RS = np.array( - list( - h2_results_aggregates["Performance By Year"][ - "Refurbishment Schedule [MW replaced/year]" - ].values() - ) - ) - offgrid_YOR = np.argwhere(offgrid_RS > 0)[:, 0] - grid_RS = np.array( - list( - h2_results_aggregates_grid["Performance By Year"][ - "Refurbishment Schedule [MW replaced/year]" - ].values() - ) - ) - grid_YOR = np.argwhere(grid_RS > 0)[:, 0] - # ----- Plot and compare results ----- - year_of_operation = list( - h2_results_aggregates["Performance By Year"]["Capacity Factor [-]"].keys() - ) - fig, ax = plt.subplots(nrows=3, ncols=1, sharex=True, figsize=[6.4, 7.2]) - - # Annual Hydrogen Produced (AH2) [metric tons H2/year] - offgrid_AH2 = ( - np.array( - list( - h2_results_aggregates["Performance By Year"][ - "Annual H2 Production [kg/year]" - ].values() - ) - ) - / 1000 - ) - grid_AH2 = ( - np.array( - list( - h2_results_aggregates_grid["Performance By Year"][ - "Annual H2 Production [kg/year]" - ].values() - ) - ) - / 1000 - ) - - ax[0].plot(year_of_operation, offgrid_AH2, color="b", ls="-", lw=2, label="off-grid") - ax[0].plot(year_of_operation, grid_AH2, color="r", ls="--", lw=2, label="grid-connected") - ax[0].vlines( - offgrid_YOR, - np.zeros(len(offgrid_YOR)), - offgrid_AH2[offgrid_YOR], - colors="skyblue", - ls="-", - lw=0.5, - ) - ax[0].vlines( - grid_YOR, - np.zeros(len(grid_YOR)), - grid_AH2[grid_YOR], - colors="tomato", - ls="--", - lw=0.5, - ) - ax[0].set_ylabel("Annual Hydrogen Produced\n[metric tons/year]") - ax[0].legend(loc="upper right") - ax[0].set_ylim((0.8 * np.min([offgrid_AH2, grid_AH2]), 1.2 * np.max([offgrid_AH2, grid_AH2]))) - - # Annual Energy Used (AEU) [MWh/year] - offgrid_AEU = ( - np.array( - list( - h2_results_aggregates["Performance By Year"][ - "Annual Energy Used [kWh/year]" - ].values() - ) - ) - / 1000 - ) - grid_AEU = ( - np.array( - list( - h2_results_aggregates_grid["Performance By Year"][ - "Annual Energy Used [kWh/year]" - ].values() - ) - ) - / 1000 - ) - ax[1].plot(year_of_operation, offgrid_AEU, color="b", ls="-", lw=2, label="_off-grid") - ax[1].plot(year_of_operation, grid_AEU, color="r", ls="--", lw=2, label="_grid-connected") - ax[1].vlines( - offgrid_YOR, - np.zeros(len(offgrid_YOR)), - offgrid_AEU[offgrid_YOR], - colors="skyblue", - ls="-", - lw=0.5, - label="off-grid stack replacement", - ) - ax[1].vlines( - grid_YOR, - np.zeros(len(grid_YOR)), - grid_AEU[grid_YOR], - colors="tomato", - ls="--", - lw=0.5, - label="grid-connected stack replacement", - ) - ax[1].set_ylabel("Annual Energy Used\n[MWh/year]") - ax[1].legend(loc="upper right") - ax[1].set_ylim((0.8 * np.min([offgrid_AEU, grid_AEU]), 1.2 * np.max([offgrid_AEU, grid_AEU]))) - - # Annual Average Conversion Efficiency (AACE) [kWh/kg] - offgrid_AACE = np.array( - list( - h2_results_aggregates["Performance By Year"][ - "Annual Average Efficiency [kWh/kg]" - ].values() - ) - ) - grid_AACE = np.array( - list( - h2_results_aggregates_grid["Performance By Year"][ - "Annual Average Efficiency [kWh/kg]" - ].values() - ) - ) - ax[2].plot(year_of_operation, offgrid_AACE, color="b", ls="-", lw=2, label="_off-grid") - ax[2].plot(year_of_operation, grid_AACE, color="r", ls="--", lw=2, label="_grid-connected") - ax[2].vlines( - offgrid_YOR, - np.zeros(len(offgrid_YOR)), - offgrid_AACE[offgrid_YOR], - colors="skyblue", - ls="-", - lw=0.5, - label="off-grid stack replacement", - ) - ax[2].vlines( - grid_YOR, - np.zeros(len(grid_YOR)), - grid_AACE[grid_YOR], - colors="tomato", - ls="--", - lw=0.5, - label="grid-connected stack replacement", - ) - ax[2].set_ylabel("Annual Average Efficiency\n[kWh/kg]") - ax[2].set_xlabel("Year of Operation") - ax[2].set_xlim((0, plant_life)) - ax[2].set_ylim( - ( - 0.95 * np.min([offgrid_AACE, grid_AACE]), - 1.05 * np.max([offgrid_AACE, grid_AACE]), - ) - ) - fig.tight_layout() diff --git a/h2integrate/simulation/technologies/hydrogen/desal/__init__.py b/h2integrate/converters/hydrogen/pem_model/__init__.py similarity index 100% rename from h2integrate/simulation/technologies/hydrogen/desal/__init__.py rename to h2integrate/converters/hydrogen/pem_model/__init__.py diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/run_PEM_master.py b/h2integrate/converters/hydrogen/pem_model/run_PEM_main.py similarity index 66% rename from h2integrate/simulation/technologies/hydrogen/electrolysis/run_PEM_master.py rename to h2integrate/converters/hydrogen/pem_model/run_PEM_main.py index 29fa22de5..3873ca07b 100644 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/run_PEM_master.py +++ b/h2integrate/converters/hydrogen/pem_model/run_PEM_main.py @@ -5,10 +5,7 @@ import pandas as pd from pyomo.environ import * # FIXME: no * imports, delete whole comment when fixed # noqa: F403 -from h2integrate.simulation.technologies.hydrogen.electrolysis.optimization_utils_linear import ( - optimize, -) -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer_Clusters import ( # noqa: E501 +from h2integrate.converters.hydrogen.pem_model.PEM_H2_LT_electrolyzer_Clusters import ( PEM_H2_Clusters as PEMClusters, ) @@ -99,13 +96,10 @@ def run_grid_connected_pem(self, system_size_mw, hydrogen_production_capacity_re # h2_df_tot = pd.DataFrame(h2_tot, index=list(h2_tot.keys()), columns=['Cluster #0']) return pd.DataFrame(h2_df_ts), pd.DataFrame(h2_df_tot) - def run(self, optimize=False): + def run(self): # TODO: add control type as input! clusters = self.create_clusters() # initialize clusters - if optimize: - power_to_clusters = self.optimize_power_split() # run Sanjana's code - else: - power_to_clusters = self.even_split_power() + power_to_clusters = self.even_split_power() h2_df_ts = pd.DataFrame() h2_df_tot = pd.DataFrame() @@ -141,69 +135,6 @@ def run(self, optimize=False): return h2_df_ts, h2_df_tot # return h2_dict_ts, h2_df_tot - def optimize_power_split(self): - number_of_stacks = self.num_clusters - rated_power = self.cluster_cap_mw * 1000 - tf = 96 - n_times_to_run = int(np.ceil(self.T / tf)) - df = pd.DataFrame({"Wind + PV Generation": self.input_power_kw}) - P_ = None - I_ = None - Tr_ = None - AC = 1 - F_tot = 1 - diff = 0 - - for start_time in range(n_times_to_run): - print(f"Optimizing {number_of_stacks} stacks tarting {start_time*tf}hr/{self.T}hr") - if start_time == 0: - df["Wind + PV Generation"] = df["Wind + PV Generation"].replace(0, np.nan) - df = df.interpolate() - - P_wind_t = df["Wind + PV Generation"][ - (start_time * tf) : ((start_time * tf) + tf) - ].values - start = time.time() - if P_ is not None: - P_ = P_[: len(P_wind_t), :] - I_ = I_[: len(P_wind_t), :] - Tr_ = Tr_[: len(P_wind_t), :] - P_tot_opt, P_, H2f, I_, Tr_, P_wind_t, AC, F_tot = optimize( - P_wind_t, - T=(len(P_wind_t)), - n_stacks=(number_of_stacks), - c_wp=0, - c_sw=self.switching_cost, - rated_power=rated_power, - P_init=P_, - I_init=I_, - T_init=Tr_, - AC_init=AC, - F_tot_init=F_tot, - ) - - diff += time.time() - start - if type(AC).__module__ != "numpy": - AC = np.array(AC) - F_tot = np.array(F_tot) - if start_time == 0: - P_full = P_ - H2f_full = H2f - I_full = I_ - Tr_full = np.sum(Tr_, axis=0) - AC_full = AC - F_tot_full = F_tot - - else: - P_full = np.vstack((P_full, P_)) - H2f_full = np.vstack((H2f_full, H2f)) - I_full = np.vstack((I_full, I_)) - Tr_full = np.vstack((Tr_full, np.sum(Tr_, axis=0))) - AC_full = np.vstack((AC_full, (AC))) - F_tot_full = np.vstack((F_tot_full, (F_tot))) - - return np.transpose(P_full) - def even_split_power(self): start = time.perf_counter() # determine how much power to give each cluster @@ -254,36 +185,3 @@ def create_clusters(self): if self.verbose: print(f"Took {round(end - start, 3)} sec to run the create clusters") return stacks - - -if __name__ == "__main__": - system_size_mw = 1000 - num_clusters = 20 - cluster_cap_mw = system_size_mw / num_clusters - stack_rating_kw = 1000 - cluster_min_power_kw = 0.1 * stack_rating_kw * cluster_cap_mw - num_steps = 200 - power_rampup = np.arange( - cluster_min_power_kw, system_size_mw * stack_rating_kw, cluster_min_power_kw - ) - - plant_life = 30 - electrolyzer_model_parameters = { - "eol_eff_percent_loss": 10, - "uptime_hours_until_eol": 77600, - "include_degradation_penalty": True, - "turndown_ratio": 0.1, - } - # power_rampup = np.linspace(cluster_min_power_kw,system_size_mw*1000,num_steps) - power_rampdown = np.flip(power_rampup) - power_in = np.concatenate((power_rampup, power_rampdown)) - pem = run_PEM_clusters( - power_in, - system_size_mw, - num_clusters, - plant_life, - electrolyzer_model_parameters, - ) - - h2_ts, h2_tot = pem.run() - # pem.clusters[0].cell_design(80,1920*2) diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_PEM.py b/h2integrate/converters/hydrogen/pem_model/run_h2_PEM.py similarity index 84% rename from h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_PEM.py rename to h2integrate/converters/hydrogen/pem_model/run_h2_PEM.py index 1672e9433..e44ef7ff3 100644 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_PEM.py +++ b/h2integrate/converters/hydrogen/pem_model/run_h2_PEM.py @@ -1,12 +1,8 @@ import numpy as np import pandas as pd -from h2integrate.simulation.technologies.hydrogen.electrolysis.run_PEM_master import ( - run_PEM_clusters, -) -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer_Clusters import ( # noqa: E501 - eta_h2_hhv, -) +from h2integrate.converters.hydrogen.pem_model.run_PEM_main import run_PEM_clusters +from h2integrate.converters.hydrogen.pem_model.PEM_H2_LT_electrolyzer_Clusters import eta_h2_hhv def clean_up_final_outputs(h2_tot, h2_ts): @@ -61,7 +57,6 @@ def run_h2_PEM( electrolyzer_size, useful_life, n_pem_clusters, - pem_control_type, electrolyzer_direct_cost_kw, user_defined_pem_param_dictionary, grid_connection_scenario, @@ -86,10 +81,7 @@ def run_h2_PEM( electrolyzer_size, hydrogen_production_capacity_required_kgphr ) else: - if pem_control_type == "optimize": - h2_ts, h2_tot = pem.run(optimize=True) - else: - h2_ts, h2_tot = pem.run() + h2_ts, h2_tot = pem.run() # dictionaries of performance during each year of simulation, # good to use for a more accurate financial analysis annual_avg_performance = combine_cluster_annual_performance_info(h2_tot) @@ -245,41 +237,3 @@ def run_h2_PEM( H2_Results.update({"# Stacks Never Used": n_stacks_new}) return H2_Results, h2_ts, h2_tot, energy_input_to_electrolyzer - - -def run_h2_PEM_IVcurve( - energy_to_electrolyzer, - electrolyzer_size_mw, - kw_continuous, - electrolyzer_capex_kw, - lcoe, - adjusted_installed_cost, - useful_life, - net_capital_costs=0, -): - # electrical_generation_timeseries = combined_pv_wind_storage_power_production_hopp - electrical_generation_timeseries = np.zeros_like(energy_to_electrolyzer) - electrical_generation_timeseries[:] = energy_to_electrolyzer[:] - - # system_rating = electrolyzer_size - H2_Results, H2A_Results = ( - kernel_PEM_IVcurve( # FIXME: undefined, delete whole comment when fixed # noqa: F821 - electrical_generation_timeseries, - electrolyzer_size_mw, - useful_life, - kw_continuous, - electrolyzer_capex_kw, - lcoe, - adjusted_installed_cost, - net_capital_costs, - ) - ) - - H2_Results["hydrogen_annual_output"] = H2_Results["hydrogen_annual_output"] - H2_Results["cap_factor"] = H2_Results["cap_factor"] - - print(f"Total power input to electrolyzer: {np.sum(electrical_generation_timeseries)}") - print("Hydrogen Annual Output (kg): {}".format(H2_Results["hydrogen_annual_output"])) - print("Water Consumption (kg) Total: {}".format(H2_Results["water_annual_usage"])) - - return H2_Results, H2A_Results # , electrical_generation_timeseries diff --git a/h2integrate/converters/hydrogen/singlitico_cost_model.py b/h2integrate/converters/hydrogen/singlitico_cost_model.py index 03ebff382..d668e8e18 100644 --- a/h2integrate/converters/hydrogen/singlitico_cost_model.py +++ b/h2integrate/converters/hydrogen/singlitico_cost_model.py @@ -3,9 +3,6 @@ from h2integrate.core.utilities import CostModelBaseConfig, merge_shared_inputs from h2integrate.core.validators import contains, must_equal from h2integrate.converters.hydrogen.electrolyzer_baseclass import ElectrolyzerCostBaseClass -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_costs_Singlitico_model import ( - PEMCostsSingliticoModel, -) @define @@ -46,28 +43,78 @@ def setup(self): ) def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - electrolyzer_size_mw = inputs["electrolyzer_size_mw"][0] + electrolyzer_size_mw = float(inputs["electrolyzer_size_mw"][0]) # run hydrogen production cost model - from hopp examples if self.config.location == "onshore": - offshore = 0 + elec_location = 0 else: - offshore = 1 + elec_location = 1 P_elec = electrolyzer_size_mw * 1e-3 # [GW] RC_elec = self.config.electrolyzer_capex # [USD/kW] - pem_offshore = PEMCostsSingliticoModel(elec_location=offshore) + # PEM costs based on Singlitico et al. 2021 + # Values for CapEX & OpEx taken from paper, Table B.2, PEMEL. + # Installation costs include land, contingency, contractors, legal fees, construction, + # engineering, yard improvements, buildings, electrics, piping, instrumentation, + # and installation and grid connection. + IF = 0.33 # installation fraction [% RC_elec] + RP_elec = 10 # reference power [MW] + + # Choose the scale factor based on electrolyzer size + if P_elec < 10 / 10**3: + SF_elec = -0.21 # scale factor, -0.21 for <10MW, -0.14 for >10MW + else: + SF_elec = -0.14 # scale factor, -0.21 for <10MW, -0.14 for >10MW + + # If electrolyzer capacity is >100MW, fix unit cost to 100MW electrolyzer as economies of + # scale stop at sizes above this, according to assumption in paper. + if P_elec > 100 / 10**3: + P_elec_cost_per_unit_calc = 0.1 + else: + P_elec_cost_per_unit_calc = P_elec + + # Calculate CapEx for a single electrolyzer + # Return the cost of a single electrolyzer of the specified capacity in millions of USD + # MUSD = GW * MUSD/GW * - * GW * MW/GW / MW ** - + capex_musd = ( + P_elec_cost_per_unit_calc + * RC_elec + * (1 + IF * elec_location) + * ((P_elec_cost_per_unit_calc * 10**3 / RP_elec) ** SF_elec) + ) + capex_per_unit = capex_musd / P_elec_cost_per_unit_calc + electrolyzer_capital_cost_musd = capex_per_unit * P_elec + + # Calculate OpEx for a single electrolyzer + # If electrolyzer capacity is >100MW, fix unit cost to 100MW electrolyzer + P_elec_opex = P_elec + if P_elec > 100 / 10**3: + P_elec_opex = 0.1 + + # Including material cost for planned and unplanned maintenance, labor cost in central + # Europe, which all depend on a system scale. Excluding the cost of electricity and the + # stack replacement, calculated separately. + # MUSD*MW MUSD * - * - * GW * MW/GW + opex_elec_eq = ( + electrolyzer_capital_cost_musd + * (1 - IF * (1 + elec_location)) + * 0.0344 + * (P_elec_opex * 10**3) ** -0.155 + ) + + # Covers the other operational expenditure related to the facility level. This includes site + # management, land rent and taxes, administrative fees (insurance, legal fees...), and site + # maintenance. + # MUSD MUSD + opex_elec_neq = 0.04 * electrolyzer_capital_cost_musd * IF * (1 + elec_location) - ( - electrolyzer_capital_cost_musd, - electrolyzer_om_cost_musd, - ) = pem_offshore.run(P_elec, RC_elec) + electrolyzer_om_cost_musd = opex_elec_eq + opex_elec_neq - electrolyzer_total_capital_cost = ( - electrolyzer_capital_cost_musd * 1e6 - ) # convert from M USD to USD - electrolyzer_OM_cost = electrolyzer_om_cost_musd * 1e6 # convert from M USD to USD + # Convert from M USD to USD + electrolyzer_total_capital_cost = electrolyzer_capital_cost_musd * 1e6 + electrolyzer_OM_cost = electrolyzer_om_cost_musd * 1e6 outputs["CapEx"] = electrolyzer_total_capital_cost outputs["OpEx"] = electrolyzer_OM_cost diff --git a/h2integrate/converters/hydrogen/test/test_basic_cost_model.py b/h2integrate/converters/hydrogen/test/test_basic_cost_model.py new file mode 100644 index 000000000..6b3969f78 --- /dev/null +++ b/h2integrate/converters/hydrogen/test/test_basic_cost_model.py @@ -0,0 +1,136 @@ +import numpy as np +import openmdao.api as om +from pytest import approx + +from h2integrate.converters.hydrogen.basic_cost_model import BasicElectrolyzerCostModel + + +class TestBasicH2Costs: + electrolyzer_size_mw = 100 + h2_annual_output = 500 + nturbines = 10 + n_timesteps = 500 + electrical_generation_timeseries = ( + electrolyzer_size_mw * (np.sin(range(0, n_timesteps))) * 0.5 + electrolyzer_size_mw * 0.5 + ) + + per_turb_electrolyzer_size_mw = electrolyzer_size_mw / nturbines + per_turb_h2_annual_output = h2_annual_output / nturbines + per_turb_electrical_generation_timeseries = electrical_generation_timeseries / nturbines + + elec_capex = 600 # $/kW + time_between_replacement = 80000 # hours + useful_life = 30 # years + + def _create_problem(self, location, electrolyzer_size_mw, electrical_generation_timeseries): + """Helper method to create and set up an OpenMDAO problem.""" + prob = om.Problem() + prob.model.add_subsystem( + "basic_cost_model", + BasicElectrolyzerCostModel( + plant_config={ + "plant": { + "plant_life": self.useful_life, + "simulation": { + "n_timesteps": self.n_timesteps, + }, + }, + }, + tech_config={ + "model_inputs": { + "cost_parameters": { + "location": location, + "electrolyzer_capex": self.elec_capex, + "time_between_replacement": self.time_between_replacement, + }, + } + }, + ), + promotes=["*"], + ) + prob.setup() + prob.set_val("electricity_in", electrical_generation_timeseries, units="kW") + prob.set_val("electrolyzer_size_mw", electrolyzer_size_mw, units="MW") + return prob + + def test_on_turbine_capex(self): + prob = self._create_problem( + "offshore", + self.per_turb_electrolyzer_size_mw, + self.per_turb_electrical_generation_timeseries, + ) + prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg/year") + prob.run_model() + + per_turb_electrolyzer_total_capital_cost = prob["CapEx"] + electrolyzer_total_capital_cost = per_turb_electrolyzer_total_capital_cost * self.nturbines + + assert electrolyzer_total_capital_cost == approx(127698560.0) + + def test_on_platform_capex(self): + prob = self._create_problem( + "offshore", self.electrolyzer_size_mw, self.electrical_generation_timeseries + ) + prob.set_val("total_hydrogen_produced", self.h2_annual_output, units="kg/year") + prob.run_model() + + electrolyzer_total_capital_cost = prob["CapEx"] + + assert electrolyzer_total_capital_cost == approx(125448560.0) + + def test_on_land_capex(self): + prob = self._create_problem( + "onshore", + self.per_turb_electrolyzer_size_mw, + self.per_turb_electrical_generation_timeseries, + ) + prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg/year") + prob.run_model() + + per_turb_electrolyzer_total_capital_cost = prob["CapEx"] + electrolyzer_total_capital_cost = per_turb_electrolyzer_total_capital_cost * self.nturbines + + assert electrolyzer_total_capital_cost == approx(116077280.00000003) + + def test_on_turbine_opex(self): + prob = self._create_problem( + "offshore", + self.per_turb_electrolyzer_size_mw, + self.per_turb_electrical_generation_timeseries, + ) + prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg/year") + prob.run_model() + + per_turb_electrolyzer_OM_cost = prob["OpEx"] + electrolyzer_OM_cost = per_turb_electrolyzer_OM_cost * self.nturbines + + assert electrolyzer_OM_cost == approx(1377207.4599629682) + + def test_on_platform_opex(self): + prob = self._create_problem( + "offshore", self.electrolyzer_size_mw, self.electrical_generation_timeseries + ) + prob.set_val("total_hydrogen_produced", self.h2_annual_output, units="kg/year") + prob.run_model() + + electrolyzer_OM_cost = prob["OpEx"] + + assert electrolyzer_OM_cost == approx(1864249.9310054395) + + def test_on_land_opex(self): + prob = self._create_problem( + "onshore", + self.per_turb_electrolyzer_size_mw, + self.per_turb_electrical_generation_timeseries, + ) + prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg/year") + prob.run_model() + + per_turb_electrolyzer_OM_cost = prob["OpEx"] + electrolyzer_OM_cost = per_turb_electrolyzer_OM_cost * self.nturbines + + assert electrolyzer_OM_cost == approx(1254447.4599629682) + + +if __name__ == "__main__": + test_set = TestBasicH2Costs() diff --git a/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py b/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py new file mode 100644 index 000000000..abe06af03 --- /dev/null +++ b/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py @@ -0,0 +1,109 @@ +import numpy as np +import openmdao.api as om +from pytest import approx + +from h2integrate.converters.hydrogen.singlitico_cost_model import SingliticoCostModel + + +TOL = 1e-3 + +BASELINE = np.array( + [ + # onshore, [capex, opex] + [ + [50.7105172052493, 1.2418205567631722], + ], + # offshore, [capex, opex] + [ + [67.44498788298158, 2.16690312809502], + ], + ] +) + + +class TestSingliticoCostModel: + P_elec_mw = 100.0 # [MW] + RC_elec = 700 # [USD/kW] + + def _create_problem(self, location): + """Helper method to create and set up an OpenMDAO problem.""" + prob = om.Problem() + prob.model.add_subsystem( + "singlitico_cost_model", + SingliticoCostModel( + plant_config={ + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + }, + }, + }, + tech_config={ + "model_inputs": { + "cost_parameters": { + "location": location, + "electrolyzer_capex": self.RC_elec, + }, + } + }, + ), + promotes=["*"], + ) + prob.setup() + prob.set_val("electrolyzer_size_mw", self.P_elec_mw, units="MW") + prob.set_val("electricity_in", np.ones(8760) * self.P_elec_mw, units="kW") + prob.set_val("total_hydrogen_produced", 1000.0, units="kg/year") + return prob + + def test_calc_capex_onshore(self): + prob = self._create_problem("onshore") + prob.run_model() + + capex_musd = prob["CapEx"] / 1e6 + assert capex_musd == approx(BASELINE[0][0][0], TOL) + + def test_calc_capex_offshore(self): + prob = self._create_problem("offshore") + prob.run_model() + + capex_musd = prob["CapEx"] / 1e6 + assert capex_musd == approx(BASELINE[1][0][0], TOL) + + def test_calc_opex_onshore(self): + prob = self._create_problem("onshore") + prob.run_model() + + opex_musd = prob["OpEx"] / 1e6 + assert opex_musd == approx(BASELINE[0][0][1], TOL) + + def test_calc_opex_offshore(self): + prob = self._create_problem("offshore") + prob.run_model() + + opex_musd = prob["OpEx"] / 1e6 + assert opex_musd == approx(BASELINE[1][0][1], TOL) + + def test_run_onshore(self): + prob = self._create_problem("onshore") + prob.run_model() + + capex_musd = prob["CapEx"] / 1e6 + opex_musd = prob["OpEx"] / 1e6 + + assert capex_musd == approx(BASELINE[0][0][0], TOL) + assert opex_musd == approx(BASELINE[0][0][1], TOL) + + def test_run_offshore(self): + prob = self._create_problem("offshore") + prob.run_model() + + capex_musd = prob["CapEx"] / 1e6 + opex_musd = prob["OpEx"] / 1e6 + + assert capex_musd == approx(BASELINE[1][0][0], TOL) + assert opex_musd == approx(BASELINE[1][0][1], TOL) + + +if __name__ == "__main__": + test_set = TestSingliticoCostModel() diff --git a/h2integrate/converters/hydrogen/test/test_wombat_model.py b/h2integrate/converters/hydrogen/test/test_wombat_model.py index c54aa3586..7654a37ce 100644 --- a/h2integrate/converters/hydrogen/test/test_wombat_model.py +++ b/h2integrate/converters/hydrogen/test/test_wombat_model.py @@ -31,7 +31,6 @@ def test_wombat_model_outputs(subtests): }, "n_clusters": 1, "cluster_rating_MW": 40, - "pem_control_type": "basic", "eol_eff_percent_loss": 13, "uptime_hours_until_eol": 80000.0, "include_degradation_penalty": True, @@ -93,7 +92,6 @@ def test_wombat_error(subtests): }, "n_clusters": 0.75, "cluster_rating_MW": 40, - "pem_control_type": "basic", "eol_eff_percent_loss": 13, "uptime_hours_until_eol": 80000.0, "include_degradation_penalty": True, diff --git a/h2integrate/converters/hydrogen/wombat_model.py b/h2integrate/converters/hydrogen/wombat_model.py index 2b61ab265..f2abfd248 100644 --- a/h2integrate/converters/hydrogen/wombat_model.py +++ b/h2integrate/converters/hydrogen/wombat_model.py @@ -6,7 +6,7 @@ from wombat.core.library import load_yaml from h2integrate.core.utilities import merge_shared_inputs -from h2integrate.converters.hydrogen.eco_tools_pem_electrolyzer import ( +from h2integrate.converters.hydrogen.pem_electrolyzer import ( ECOElectrolyzerPerformanceModel, ECOElectrolyzerPerformanceModelConfig, ) diff --git a/h2integrate/simulation/technologies/iron/iron.py b/h2integrate/converters/iron/iron.py similarity index 100% rename from h2integrate/simulation/technologies/iron/iron.py rename to h2integrate/converters/iron/iron.py diff --git a/h2integrate/converters/iron/iron_mine.py b/h2integrate/converters/iron/iron_mine.py index ec21c1b15..4a941af94 100644 --- a/h2integrate/converters/iron/iron_mine.py +++ b/h2integrate/converters/iron/iron_mine.py @@ -5,21 +5,17 @@ from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import contains, range_val -from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.tools.inflation.inflate import inflate_cpi -from h2integrate.simulation.technologies.iron.iron import ( +from h2integrate.converters.iron.iron import ( IronCostModelConfig, IronPerformanceModelConfig, IronPerformanceModelOutputs, run_iron_cost_model, run_size_iron_plant_performance, ) -from h2integrate.simulation.technologies.iron.martin_ore.variable_om_cost import ( - martin_ore_variable_om_cost, -) -from h2integrate.simulation.technologies.iron.rosner_ore.variable_om_cost import ( - rosner_ore_variable_om_cost, -) +from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.tools.inflation.inflate import inflate_cpi +from h2integrate.converters.iron.martin_ore.variable_om_cost import martin_ore_variable_om_cost +from h2integrate.converters.iron.rosner_ore.variable_om_cost import rosner_ore_variable_om_cost @define diff --git a/h2integrate/converters/iron/iron_plant.py b/h2integrate/converters/iron/iron_plant.py index 52f4ec3ec..1fd52a1c8 100644 --- a/h2integrate/converters/iron/iron_plant.py +++ b/h2integrate/converters/iron/iron_plant.py @@ -5,16 +5,16 @@ from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import contains -from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci -from h2integrate.simulation.technologies.iron.iron import ( +from h2integrate.converters.iron.iron import ( IronCostModelConfig, IronPerformanceModelConfig, IronPerformanceModelOutputs, run_iron_cost_model, run_size_iron_plant_performance, ) -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs @define diff --git a/h2integrate/converters/iron/iron_transport.py b/h2integrate/converters/iron/iron_transport.py index 45d021f04..9f248e9a7 100644 --- a/h2integrate/converters/iron/iron_transport.py +++ b/h2integrate/converters/iron/iron_transport.py @@ -8,7 +8,7 @@ from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import contains, range_val from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs @define @@ -80,12 +80,7 @@ def compute(self, inputs, outputs): lon = self.options["plant_config"].get("site", {}).get("longitude") site_location = (lat, lon) shipping_coord_fpath = ( - ROOT_DIR - / "simulation" - / "technologies" - / "iron" - / "martin_transport" - / "shipping_coords.csv" + ROOT_DIR / "converters" / "iron" / "martin_transport" / "shipping_coords.csv" ) shipping_locations = pd.read_csv(shipping_coord_fpath, index_col="Unnamed: 0") diff --git a/h2integrate/converters/iron/iron_wrapper.py b/h2integrate/converters/iron/iron_wrapper.py index 2fd32ebc0..48546707f 100644 --- a/h2integrate/converters/iron/iron_wrapper.py +++ b/h2integrate/converters/iron/iron_wrapper.py @@ -7,11 +7,9 @@ import h2integrate.tools.profast_reverse_tools as rev_pf_tools from h2integrate.core.utilities import CostModelBaseConfig, merge_shared_inputs from h2integrate.core.validators import contains, range_val +from h2integrate.converters.iron.iron import run_iron_full_model from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.simulation.technologies.iron.iron import run_iron_full_model -from h2integrate.simulation.technologies.iron.martin_transport.iron_transport import ( - calc_iron_ship_cost, -) +from h2integrate.converters.iron.martin_transport.iron_transport import calc_iron_ship_cost @define diff --git a/h2integrate/simulation/technologies/iron/load_top_down_coeffs.py b/h2integrate/converters/iron/load_top_down_coeffs.py similarity index 100% rename from h2integrate/simulation/technologies/iron/load_top_down_coeffs.py rename to h2integrate/converters/iron/load_top_down_coeffs.py diff --git a/h2integrate/converters/iron/martin_mine_cost_model.py b/h2integrate/converters/iron/martin_mine_cost_model.py index 80df24baf..ba1d2aa9e 100644 --- a/h2integrate/converters/iron/martin_mine_cost_model.py +++ b/h2integrate/converters/iron/martin_mine_cost_model.py @@ -91,9 +91,7 @@ def setup(self): desc="Iron ore pellets produced", ) - coeff_fpath = ( - ROOT_DIR / "simulation" / "technologies" / "iron" / "martin_ore" / "cost_coeffs.csv" - ) + coeff_fpath = ROOT_DIR / "converters" / "iron" / "martin_ore" / "cost_coeffs.csv" # martin ore performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) diff --git a/h2integrate/converters/iron/martin_mine_perf_model.py b/h2integrate/converters/iron/martin_mine_perf_model.py index 8fb48f0af..83e498ff9 100644 --- a/h2integrate/converters/iron/martin_mine_perf_model.py +++ b/h2integrate/converters/iron/martin_mine_perf_model.py @@ -108,9 +108,7 @@ def setup(self): desc="Total iron ore pellets produced anually", ) - coeff_fpath = ( - ROOT_DIR / "simulation" / "technologies" / "iron" / "martin_ore" / "perf_coeffs.csv" - ) + coeff_fpath = ROOT_DIR / "converters" / "iron" / "martin_ore" / "perf_coeffs.csv" # martin ore performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) self.coeff_df = self.format_coeff_df(coeff_df, self.config.mine) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/__init__.py b/h2integrate/converters/iron/martin_ore/__init__.py similarity index 100% rename from h2integrate/simulation/technologies/hydrogen/h2_storage/__init__.py rename to h2integrate/converters/iron/martin_ore/__init__.py diff --git a/h2integrate/simulation/technologies/iron/martin_ore/cost_coeffs.csv b/h2integrate/converters/iron/martin_ore/cost_coeffs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/martin_ore/cost_coeffs.csv rename to h2integrate/converters/iron/martin_ore/cost_coeffs.csv diff --git a/h2integrate/simulation/technologies/iron/martin_ore/cost_inputs.csv b/h2integrate/converters/iron/martin_ore/cost_inputs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/martin_ore/cost_inputs.csv rename to h2integrate/converters/iron/martin_ore/cost_inputs.csv diff --git a/h2integrate/simulation/technologies/iron/martin_ore/cost_model.py b/h2integrate/converters/iron/martin_ore/cost_model.py similarity index 100% rename from h2integrate/simulation/technologies/iron/martin_ore/cost_model.py rename to h2integrate/converters/iron/martin_ore/cost_model.py diff --git a/h2integrate/simulation/technologies/iron/martin_ore/finance_model.py b/h2integrate/converters/iron/martin_ore/finance_model.py similarity index 98% rename from h2integrate/simulation/technologies/iron/martin_ore/finance_model.py rename to h2integrate/converters/iron/martin_ore/finance_model.py index 134d456bb..2ccc88950 100644 --- a/h2integrate/simulation/technologies/iron/martin_ore/finance_model.py +++ b/h2integrate/converters/iron/martin_ore/finance_model.py @@ -2,7 +2,7 @@ import ProFAST from h2integrate.tools.inflation.inflate import inflate_cpi -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs def main(config): diff --git a/h2integrate/simulation/technologies/iron/martin_ore/perf_coeffs.csv b/h2integrate/converters/iron/martin_ore/perf_coeffs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/martin_ore/perf_coeffs.csv rename to h2integrate/converters/iron/martin_ore/perf_coeffs.csv diff --git a/h2integrate/simulation/technologies/iron/martin_ore/perf_inputs.csv b/h2integrate/converters/iron/martin_ore/perf_inputs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/martin_ore/perf_inputs.csv rename to h2integrate/converters/iron/martin_ore/perf_inputs.csv diff --git a/h2integrate/simulation/technologies/iron/martin_ore/perf_model.py b/h2integrate/converters/iron/martin_ore/perf_model.py similarity index 100% rename from h2integrate/simulation/technologies/iron/martin_ore/perf_model.py rename to h2integrate/converters/iron/martin_ore/perf_model.py diff --git a/h2integrate/simulation/technologies/iron/martin_ore/variable_om_cost.py b/h2integrate/converters/iron/martin_ore/variable_om_cost.py similarity index 93% rename from h2integrate/simulation/technologies/iron/martin_ore/variable_om_cost.py rename to h2integrate/converters/iron/martin_ore/variable_om_cost.py index b1c0ed102..65b1b7a64 100644 --- a/h2integrate/simulation/technologies/iron/martin_ore/variable_om_cost.py +++ b/h2integrate/converters/iron/martin_ore/variable_om_cost.py @@ -1,6 +1,6 @@ import numpy as np -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs def martin_ore_variable_om_cost(mine_name, cost_df, analysis_start, cost_year, plant_life): diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/lined_rock_cavern/__init__.py b/h2integrate/converters/iron/martin_transport/__init__.py similarity index 100% rename from h2integrate/simulation/technologies/hydrogen/h2_storage/lined_rock_cavern/__init__.py rename to h2integrate/converters/iron/martin_transport/__init__.py diff --git a/h2integrate/simulation/technologies/iron/martin_transport/iron_transport.py b/h2integrate/converters/iron/martin_transport/iron_transport.py similarity index 98% rename from h2integrate/simulation/technologies/iron/martin_transport/iron_transport.py rename to h2integrate/converters/iron/martin_transport/iron_transport.py index 2734a1348..eda5c9f16 100644 --- a/h2integrate/simulation/technologies/iron/martin_transport/iron_transport.py +++ b/h2integrate/converters/iron/martin_transport/iron_transport.py @@ -7,7 +7,7 @@ import pandas as pd import geopy.distance -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs CD = Path(__file__).parent diff --git a/h2integrate/simulation/technologies/iron/martin_transport/shipping_coords.csv b/h2integrate/converters/iron/martin_transport/shipping_coords.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/martin_transport/shipping_coords.csv rename to h2integrate/converters/iron/martin_transport/shipping_coords.csv diff --git a/h2integrate/simulation/technologies/iron/model_locations.yaml b/h2integrate/converters/iron/model_locations.yaml similarity index 62% rename from h2integrate/simulation/technologies/iron/model_locations.yaml rename to h2integrate/converters/iron/model_locations.yaml index c38eb2b09..9a1b2d509 100644 --- a/h2integrate/simulation/technologies/iron/model_locations.yaml +++ b/h2integrate/converters/iron/model_locations.yaml @@ -1,53 +1,53 @@ performance: martin_ore: compatible_tech: ['h2_dri','ng_dri'] - model: 'h2integrate.simulation.technologies.iron.martin_ore.perf_model' + model: 'h2integrate.converters.iron.martin_ore.perf_model' inputs: 'perf_inputs.csv' coeffs: 'perf_coeffs.csv' rosner: compatible_tech: ['h2_dri','ng_dri'] - model: 'h2integrate.simulation.technologies.iron.rosner.perf_model' + model: 'h2integrate.converters.iron.rosner.perf_model' inputs: 'perf_inputs.csv' coeffs: 'perf_coeffs.csv' cost: martin_ore: compatible_tech: ['h2_dri','ng_dri'] - model: 'h2integrate.simulation.technologies.iron.martin_ore.cost_model' + model: 'h2integrate.converters.iron.martin_ore.cost_model' inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' rosner: compatible_tech: ['h2_dri','ng_dri'] - model: 'h2integrate.simulation.technologies.iron.rosner.cost_model' + model: 'h2integrate.converters.iron.rosner.cost_model' inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' stinn: compatible_tech: ['blast_furnace', 'ng_dri', 'h2_dri', 'moe', 'ahe', 'mse'] - model: 'h2integrate.simulation.technologies.iron.stinn.cost_model' + model: 'h2integrate.converters.iron.stinn.cost_model' inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' peters: compatible_tech: ['blast_furnace', 'ng_dri', 'h2_dri', 'moe', 'ahe', 'mse'] - model: 'h2integrate.simulation.technologies.iron.peters.opex_model' + model: 'h2integrate.converters.iron.peters.opex_model' inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' finance: martin_ore: compatible_tech: ['h2_dri','ng_dri'] - model: 'h2integrate.simulation.technologies.iron.martin_ore.finance_model' + model: 'h2integrate.converters.iron.martin_ore.finance_model' inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' rosner: compatible_tech: ['h2_dri','ng_dri'] - model: 'h2integrate.simulation.technologies.iron.rosner.finance_model' + model: 'h2integrate.converters.iron.rosner.finance_model' inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' rosner_ore: compatible_tech: ['h2_dri','ng_dri'] - model: 'h2integrate.simulation.technologies.iron.rosner_ore.finance_model' + model: 'h2integrate.converters.iron.rosner_ore.finance_model' inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' rosner_override: compatible_tech: ['h2_dri','ng_dri'] - model: 'h2integrate.simulation.technologies.iron.rosner_override.finance_model' + model: 'h2integrate.converters.iron.rosner_override.finance_model' inputs: 'cost_inputs.csv' coeffs: 'cost_coeffs.csv' diff --git a/h2integrate/converters/iron/old_input/h2integrate_config_modular.yaml b/h2integrate/converters/iron/old_input/h2integrate_config_modular.yaml index f616a0f37..7b5b3c1b5 100644 --- a/h2integrate/converters/iron/old_input/h2integrate_config_modular.yaml +++ b/h2integrate/converters/iron/old_input/h2integrate_config_modular.yaml @@ -61,7 +61,6 @@ electrolyzer: hydrogen_dmd: rating: 1160 # MW cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 77600 #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/h2integrate/simulation/technologies/iron/peters/cost_coeffs.csv b/h2integrate/converters/iron/peters/cost_coeffs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/peters/cost_coeffs.csv rename to h2integrate/converters/iron/peters/cost_coeffs.csv diff --git a/h2integrate/simulation/technologies/iron/peters/cost_inputs.csv b/h2integrate/converters/iron/peters/cost_inputs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/peters/cost_inputs.csv rename to h2integrate/converters/iron/peters/cost_inputs.csv diff --git a/h2integrate/simulation/technologies/iron/peters/cost_model.py b/h2integrate/converters/iron/peters/cost_model.py similarity index 100% rename from h2integrate/simulation/technologies/iron/peters/cost_model.py rename to h2integrate/converters/iron/peters/cost_model.py diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/mch/__init__.py b/h2integrate/converters/iron/rosner/__init__.py similarity index 100% rename from h2integrate/simulation/technologies/hydrogen/h2_storage/mch/__init__.py rename to h2integrate/converters/iron/rosner/__init__.py diff --git a/h2integrate/simulation/technologies/iron/rosner/cost_coeffs.csv b/h2integrate/converters/iron/rosner/cost_coeffs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/rosner/cost_coeffs.csv rename to h2integrate/converters/iron/rosner/cost_coeffs.csv diff --git a/h2integrate/simulation/technologies/iron/rosner/cost_inputs.csv b/h2integrate/converters/iron/rosner/cost_inputs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/rosner/cost_inputs.csv rename to h2integrate/converters/iron/rosner/cost_inputs.csv diff --git a/h2integrate/simulation/technologies/iron/rosner/cost_model.py b/h2integrate/converters/iron/rosner/cost_model.py similarity index 99% rename from h2integrate/simulation/technologies/iron/rosner/cost_model.py rename to h2integrate/converters/iron/rosner/cost_model.py index cf5262c15..5e0977d84 100644 --- a/h2integrate/simulation/technologies/iron/rosner/cost_model.py +++ b/h2integrate/converters/iron/rosner/cost_model.py @@ -10,7 +10,7 @@ import pandas as pd from h2integrate.core.utilities import load_yaml -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs CD = Path(__file__).parent diff --git a/h2integrate/simulation/technologies/iron/rosner/finance_model.py b/h2integrate/converters/iron/rosner/finance_model.py similarity index 99% rename from h2integrate/simulation/technologies/iron/rosner/finance_model.py rename to h2integrate/converters/iron/rosner/finance_model.py index 5f835011c..46032d3bd 100644 --- a/h2integrate/simulation/technologies/iron/rosner/finance_model.py +++ b/h2integrate/converters/iron/rosner/finance_model.py @@ -3,7 +3,7 @@ import h2integrate.tools.profast_tools as pf_tools from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs def main(config): diff --git a/h2integrate/simulation/technologies/iron/rosner/perf_coeffs.csv b/h2integrate/converters/iron/rosner/perf_coeffs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/rosner/perf_coeffs.csv rename to h2integrate/converters/iron/rosner/perf_coeffs.csv diff --git a/h2integrate/simulation/technologies/iron/rosner/perf_inputs.csv b/h2integrate/converters/iron/rosner/perf_inputs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/rosner/perf_inputs.csv rename to h2integrate/converters/iron/rosner/perf_inputs.csv diff --git a/h2integrate/simulation/technologies/iron/rosner/perf_model.py b/h2integrate/converters/iron/rosner/perf_model.py similarity index 100% rename from h2integrate/simulation/technologies/iron/rosner/perf_model.py rename to h2integrate/converters/iron/rosner/perf_model.py diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/salt_cavern/__init__.py b/h2integrate/converters/iron/rosner_ore/__init__.py similarity index 100% rename from h2integrate/simulation/technologies/hydrogen/h2_storage/salt_cavern/__init__.py rename to h2integrate/converters/iron/rosner_ore/__init__.py diff --git a/h2integrate/simulation/technologies/iron/rosner_ore/finance_model.py b/h2integrate/converters/iron/rosner_ore/finance_model.py similarity index 98% rename from h2integrate/simulation/technologies/iron/rosner_ore/finance_model.py rename to h2integrate/converters/iron/rosner_ore/finance_model.py index 5427b28bd..5fc3724f1 100644 --- a/h2integrate/simulation/technologies/iron/rosner_ore/finance_model.py +++ b/h2integrate/converters/iron/rosner_ore/finance_model.py @@ -2,7 +2,7 @@ import ProFAST from h2integrate.tools.inflation.inflate import inflate_cpi -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs def main(config): diff --git a/h2integrate/simulation/technologies/iron/rosner_ore/variable_om_cost.py b/h2integrate/converters/iron/rosner_ore/variable_om_cost.py similarity index 93% rename from h2integrate/simulation/technologies/iron/rosner_ore/variable_om_cost.py rename to h2integrate/converters/iron/rosner_ore/variable_om_cost.py index 15d8fd442..bd0c6e535 100644 --- a/h2integrate/simulation/technologies/iron/rosner_ore/variable_om_cost.py +++ b/h2integrate/converters/iron/rosner_ore/variable_om_cost.py @@ -1,6 +1,6 @@ import numpy as np -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs def rosner_ore_variable_om_cost(mine_name, cost_df, analysis_start, cost_year, plant_life): diff --git a/h2integrate/simulation/technologies/hydrogen/h2_transport/__init__.py b/h2integrate/converters/iron/rosner_override/__init__.py similarity index 100% rename from h2integrate/simulation/technologies/hydrogen/h2_transport/__init__.py rename to h2integrate/converters/iron/rosner_override/__init__.py diff --git a/h2integrate/simulation/technologies/iron/rosner_override/finance_model.py b/h2integrate/converters/iron/rosner_override/finance_model.py similarity index 100% rename from h2integrate/simulation/technologies/iron/rosner_override/finance_model.py rename to h2integrate/converters/iron/rosner_override/finance_model.py diff --git a/h2integrate/simulation/technologies/iron/stinn/cost_coeffs.csv b/h2integrate/converters/iron/stinn/cost_coeffs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/stinn/cost_coeffs.csv rename to h2integrate/converters/iron/stinn/cost_coeffs.csv diff --git a/h2integrate/simulation/technologies/iron/stinn/cost_model.py b/h2integrate/converters/iron/stinn/cost_model.py similarity index 100% rename from h2integrate/simulation/technologies/iron/stinn/cost_model.py rename to h2integrate/converters/iron/stinn/cost_model.py diff --git a/h2integrate/simulation/technologies/iron/top_down_coeffs.csv b/h2integrate/converters/iron/top_down_coeffs.csv similarity index 100% rename from h2integrate/simulation/technologies/iron/top_down_coeffs.csv rename to h2integrate/converters/iron/top_down_coeffs.csv diff --git a/h2integrate/converters/steel/electric_arc_furnance.py b/h2integrate/converters/steel/electric_arc_furnance.py index b45058592..137618379 100644 --- a/h2integrate/converters/steel/electric_arc_furnance.py +++ b/h2integrate/converters/steel/electric_arc_furnance.py @@ -5,16 +5,16 @@ from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import contains -from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci -from h2integrate.simulation.technologies.iron.iron import ( +from h2integrate.converters.iron.iron import ( IronCostModelConfig, IronPerformanceModelConfig, IronPerformanceModelOutputs, run_iron_cost_model, run_size_iron_plant_performance, ) -from h2integrate.simulation.technologies.iron.load_top_down_coeffs import load_top_down_coeffs +from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci +from h2integrate.converters.iron.load_top_down_coeffs import load_top_down_coeffs @define diff --git a/h2integrate/converters/steel/steel.py b/h2integrate/converters/steel/steel.py index ba8fdab27..782e9fda8 100644 --- a/h2integrate/converters/steel/steel.py +++ b/h2integrate/converters/steel/steel.py @@ -1,3 +1,4 @@ +import ProFAST from attrs import field, define from h2integrate.core.utilities import BaseConfig, merge_shared_inputs @@ -6,14 +7,6 @@ SteelCostBaseClass, SteelPerformanceBaseClass, ) -from h2integrate.simulation.technologies.steel.steel import ( - Feedstocks, - SteelCostModelConfig, - SteelFinanceModelConfig, - run_steel_model, - run_steel_cost_model, - run_steel_finance_model, -) @define @@ -28,9 +21,6 @@ class SteelPerformanceModel(SteelPerformanceBaseClass): Computes annual steel production based on plant capacity and capacity factor. """ - def initialize(self): - super().initialize() - def setup(self): super().setup() self.config = SteelPerformanceModelConfig.from_dict( @@ -38,10 +28,7 @@ def setup(self): ) def compute(self, inputs, outputs): - steel_production_mtpy = run_steel_model( - self.config.plant_capacity_mtpy, - self.config.capacity_factor, - ) + steel_production_mtpy = self.config.plant_capacity_mtpy * self.config.capacity_factor outputs["steel"] = steel_production_mtpy / len(inputs["electricity_in"]) @@ -54,10 +41,35 @@ class SteelCostAndFinancialModelConfig(BaseConfig): capacity_factor: float = field() o2_heat_integration: bool = field() lcoh: float = field() - feedstocks: dict = field() # TODO: build validator for this large dictionary - finances: dict = field() # TODO: build validator for this large dictionary + natural_gas_prices: dict = field() + + # Financial parameters - flattened from the nested structure + grid_prices: dict = field() + financial_assumptions: dict = field() cost_year: int = field(default=2022, converter=int, validator=must_equal(2022)) + # Feedstock parameters - flattened from the nested structure + excess_oxygen: float = field(default=395) + lime_unitcost: float = field(default=122.1) + lime_transport_cost: float = field(default=0.0) + carbon_unitcost: float = field(default=236.97) + carbon_transport_cost: float = field(default=0.0) + electricity_cost: float = field(default=48.92) + iron_ore_pellet_unitcost: float = field(default=207.35) + iron_ore_pellet_transport_cost: float = field(default=0.0) + oxygen_market_price: float = field(default=0.03) + raw_water_unitcost: float = field(default=0.59289) + iron_ore_consumption: float = field(default=1.62927) + raw_water_consumption: float = field(default=0.80367) + lime_consumption: float = field(default=0.01812) + carbon_consumption: float = field(default=0.0538) + hydrogen_consumption: float = field(default=0.06596) + natural_gas_consumption: float = field(default=0.71657) + electricity_consumption: float = field(default=0.5502) + slag_disposal_unitcost: float = field(default=37.63) + slag_production: float = field(default=0.17433) + maintenance_materials_unitcost: float = field(default=7.72) + class SteelCostAndFinancialModel(SteelCostBaseClass): """ @@ -65,58 +77,428 @@ class SteelCostAndFinancialModel(SteelCostBaseClass): Includes CapEx, OpEx, and byproduct credits. """ - def initialize(self): - super().initialize() - def setup(self): self.config = SteelCostAndFinancialModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") ) super().setup() - # TODO Bring the steel cost model config and feedstock classes into new h2integrate - self.cost_config = SteelCostModelConfig( - operational_year=self.config.operational_year, - feedstocks=Feedstocks(**self.config.feedstocks), - plant_capacity_mtpy=self.config.plant_capacity_mtpy, - lcoh=self.config.lcoh, - ) - # TODO Review whether to split plant and finance_parameters configs or combine somehow self.add_input("steel_production_mtpy", val=0.0, units="t/year") - self.add_output("LCOS", val=0.0, units="USD/t") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - config = self.cost_config - - config.lcoh = inputs["LCOH"] + # Update config with runtime inputs + self.config.lcoh = inputs["LCOH"] if inputs["electricity_cost"] > 0: - self.config.feedstocks.update({"electricity_cost": inputs["electricity_cost"][0]}) - - cost_model_outputs = run_steel_cost_model(config) - - outputs["CapEx"] = cost_model_outputs.total_plant_cost - outputs["OpEx"] = cost_model_outputs.total_fixed_operating_cost - - # TODO Bring this config dict into new_h2integrate from old h2integrate - finance_config = SteelFinanceModelConfig( - plant_life=self.options["plant_config"]["plant"]["plant_life"], - plant_capacity_mtpy=self.config.plant_capacity_mtpy, - plant_capacity_factor=self.config.capacity_factor, - steel_production_mtpy=inputs["steel_production_mtpy"], - lcoh=config.lcoh, - grid_prices=self.config.finances["grid_prices"], - feedstocks=Feedstocks(**self.config.feedstocks), - costs=cost_model_outputs, - o2_heat_integration=self.config.o2_heat_integration, - financial_assumptions=self.config.finances["financial_assumptions"], - install_years=int(self.config.installation_time / 12), - gen_inflation=self.config.inflation_rate, - save_plots=False, - show_plots=False, - output_dir="./output/", - design_scenario_id=0, - ) - - finance_model_outputs = run_steel_finance_model(finance_config) - outputs["LCOS"] = finance_model_outputs.sol.get("price") + self.config.electricity_cost = inputs["electricity_cost"][0] + + # Calculate steel production costs directly + model_year_CEPCI = 816.0 # 2022 + equation_year_CEPCI = 708.8 # 2021 + + capex_eaf_casting = ( + model_year_CEPCI + / equation_year_CEPCI + * 352191.5237 + * self.config.plant_capacity_mtpy**0.456 + ) + capex_shaft_furnace = ( + model_year_CEPCI + / equation_year_CEPCI + * 489.68061 + * self.config.plant_capacity_mtpy**0.88741 + ) + capex_oxygen_supply = ( + model_year_CEPCI + / equation_year_CEPCI + * 1715.21508 + * self.config.plant_capacity_mtpy**0.64574 + ) + if self.config.o2_heat_integration: + capex_h2_preheating = ( + model_year_CEPCI + / equation_year_CEPCI + * (1 - 0.4) + * (45.69123 * self.config.plant_capacity_mtpy**0.86564) + ) + capex_cooling_tower = ( + model_year_CEPCI + / equation_year_CEPCI + * (1 - 0.3) + * (2513.08314 * self.config.plant_capacity_mtpy**0.63325) + ) + else: + capex_h2_preheating = ( + model_year_CEPCI + / equation_year_CEPCI + * 45.69123 + * self.config.plant_capacity_mtpy**0.86564 + ) + capex_cooling_tower = ( + model_year_CEPCI + / equation_year_CEPCI + * 2513.08314 + * self.config.plant_capacity_mtpy**0.63325 + ) + + capex_piping = ( + model_year_CEPCI + / equation_year_CEPCI + * 11815.72718 + * self.config.plant_capacity_mtpy**0.59983 + ) + capex_elec_instr = ( + model_year_CEPCI + / equation_year_CEPCI + * 7877.15146 + * self.config.plant_capacity_mtpy**0.59983 + ) + capex_buildings_storage_water = ( + model_year_CEPCI + / equation_year_CEPCI + * 1097.81876 + * self.config.plant_capacity_mtpy**0.8 + ) + capex_misc = ( + model_year_CEPCI + / equation_year_CEPCI + * 7877.1546 + * self.config.plant_capacity_mtpy**0.59983 + ) + + total_plant_cost = ( + capex_eaf_casting + + capex_shaft_furnace + + capex_oxygen_supply + + capex_h2_preheating + + capex_cooling_tower + + capex_piping + + capex_elec_instr + + capex_buildings_storage_water + + capex_misc + ) + + # Fixed O&M Costs + labor_cost_annual_operation = ( + 69375996.9 + * ((self.config.plant_capacity_mtpy / 365 * 1000) ** 0.25242) + / ((1162077 / 365 * 1000) ** 0.25242) + ) + labor_cost_maintenance = 0.00863 * total_plant_cost + labor_cost_admin_support = 0.25 * (labor_cost_annual_operation + labor_cost_maintenance) + + property_tax_insurance = 0.02 * total_plant_cost + + total_fixed_operating_cost = ( + labor_cost_annual_operation + + labor_cost_maintenance + + labor_cost_admin_support + + property_tax_insurance + ) + + # Owner's (Installation) Costs + labor_cost_fivemonth = ( + 5 + / 12 + * (labor_cost_annual_operation + labor_cost_maintenance + labor_cost_admin_support) + ) + + (self.config.maintenance_materials_unitcost * self.config.plant_capacity_mtpy / 12) + ( + self.config.plant_capacity_mtpy + * ( + self.config.raw_water_consumption * self.config.raw_water_unitcost + + self.config.lime_consumption + * (self.config.lime_unitcost + self.config.lime_transport_cost) + + self.config.carbon_consumption + * (self.config.carbon_unitcost + self.config.carbon_transport_cost) + + self.config.iron_ore_consumption + * ( + self.config.iron_ore_pellet_unitcost + + self.config.iron_ore_pellet_transport_cost + ) + ) + / 12 + ) + + ( + self.config.plant_capacity_mtpy + * self.config.slag_disposal_unitcost + * self.config.slag_production + / 12 + ) + + ( + self.config.plant_capacity_mtpy + * ( + self.config.hydrogen_consumption * self.config.lcoh * 1000 + + self.config.natural_gas_consumption + * self.config.natural_gas_prices[str(self.config.operational_year)] + + self.config.electricity_consumption * self.config.electricity_cost + ) + / 12 + ) + two_percent_tpc = 0.02 * total_plant_cost + + fuel_consumables_60day_supply_cost = ( + self.config.plant_capacity_mtpy + * ( + self.config.raw_water_consumption * self.config.raw_water_unitcost + + self.config.lime_consumption + * (self.config.lime_unitcost + self.config.lime_transport_cost) + + self.config.carbon_consumption + * (self.config.carbon_unitcost + self.config.carbon_transport_cost) + + self.config.iron_ore_consumption + * ( + self.config.iron_ore_pellet_unitcost + + self.config.iron_ore_pellet_transport_cost + ) + ) + / 365 + * 60 + ) + + spare_parts_cost = 0.005 * total_plant_cost + land_cost = 0.775 * self.config.plant_capacity_mtpy + misc_owners_costs = 0.15 * total_plant_cost + + installation_cost = ( + labor_cost_fivemonth + + two_percent_tpc + + fuel_consumables_60day_supply_cost + + spare_parts_cost + + misc_owners_costs + ) + + outputs["CapEx"] = total_plant_cost + outputs["OpEx"] = total_fixed_operating_cost + + # Run finance model directly using ProFAST + pf = ProFAST.ProFAST("blank") + + # Apply all params passed through from config + for param, val in self.config.financial_assumptions.items(): + pf.set_params(param, val) + + analysis_start = int([*self.config.grid_prices][0]) - int( + self.config.installation_time / 12 + ) + plant_life = self.options["plant_config"]["plant"]["plant_life"] + + # Fill these in - can have most of them as 0 also + pf.set_params( + "commodity", + { + "name": "Steel", + "unit": "metric tons", + "initial price": 1000, + "escalation": self.config.inflation_rate, + }, + ) + pf.set_params("capacity", self.config.plant_capacity_mtpy / 365) # units/day + pf.set_params("maintenance", {"value": 0, "escalation": self.config.inflation_rate}) + pf.set_params("analysis start year", analysis_start) + pf.set_params("operating life", plant_life) + pf.set_params("installation months", self.config.installation_time) + pf.set_params( + "installation cost", + { + "value": installation_cost, + "depr type": "Straight line", + "depr period": 4, + "depreciable": False, + }, + ) + pf.set_params("non depr assets", land_cost) + pf.set_params( + "end of proj sale non depr assets", + land_cost * (1 + self.config.inflation_rate) ** plant_life, + ) + pf.set_params("demand rampup", 5.3) + pf.set_params("long term utilization", self.config.capacity_factor) + pf.set_params("credit card fees", 0) + pf.set_params("sales tax", 0) + pf.set_params("license and permit", {"value": 00, "escalation": self.config.inflation_rate}) + pf.set_params("rent", {"value": 0, "escalation": self.config.inflation_rate}) + pf.set_params("property tax and insurance", 0) + pf.set_params("admin expense", 0) + pf.set_params("sell undepreciated cap", True) + pf.set_params("tax losses monetized", True) + pf.set_params("general inflation rate", self.config.inflation_rate) + pf.set_params("debt type", "Revolving debt") + pf.set_params("cash onhand", 1) + + # Add capital items to ProFAST + pf.add_capital_item( + name="EAF & Casting", + cost=capex_eaf_casting, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + pf.add_capital_item( + name="Shaft Furnace", + cost=capex_shaft_furnace, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + pf.add_capital_item( + name="Oxygen Supply", + cost=capex_oxygen_supply, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + pf.add_capital_item( + name="H2 Pre-heating", + cost=capex_h2_preheating, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + pf.add_capital_item( + name="Cooling Tower", + cost=capex_cooling_tower, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + pf.add_capital_item( + name="Piping", + cost=capex_piping, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + pf.add_capital_item( + name="Electrical & Instrumentation", + cost=capex_elec_instr, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + pf.add_capital_item( + name="Buildings, Storage, Water Service", + cost=capex_buildings_storage_water, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + pf.add_capital_item( + name="Other Miscellaneous Costs", + cost=capex_misc, + depr_type="MACRS", + depr_period=7, + refurb=[0], + ) + + # Add fixed costs + pf.add_fixed_cost( + name="Annual Operating Labor Cost", + usage=1, + unit="$/year", + cost=labor_cost_annual_operation, + escalation=self.config.inflation_rate, + ) + pf.add_fixed_cost( + name="Maintenance Labor Cost", + usage=1, + unit="$/year", + cost=labor_cost_maintenance, + escalation=self.config.inflation_rate, + ) + pf.add_fixed_cost( + name="Administrative & Support Labor Cost", + usage=1, + unit="$/year", + cost=labor_cost_admin_support, + escalation=self.config.inflation_rate, + ) + pf.add_fixed_cost( + name="Property tax and insurance", + usage=1, + unit="$/year", + cost=property_tax_insurance, + escalation=0.0, + ) + + # Add feedstocks + pf.add_feedstock( + name="Maintenance Materials", + usage=1.0, + unit="Units per metric ton of steel", + cost=self.config.maintenance_materials_unitcost, + escalation=self.config.inflation_rate, + ) + pf.add_feedstock( + name="Raw Water Withdrawal", + usage=self.config.raw_water_consumption, + unit="metric tons of water per metric ton of steel", + cost=self.config.raw_water_unitcost, + escalation=self.config.inflation_rate, + ) + pf.add_feedstock( + name="Lime", + usage=self.config.lime_consumption, + unit="metric tons of lime per metric ton of steel", + cost=(self.config.lime_unitcost + self.config.lime_transport_cost), + escalation=self.config.inflation_rate, + ) + pf.add_feedstock( + name="Carbon", + usage=self.config.carbon_consumption, + unit="metric tons of carbon per metric ton of steel", + cost=(self.config.carbon_unitcost + self.config.carbon_transport_cost), + escalation=self.config.inflation_rate, + ) + pf.add_feedstock( + name="Iron Ore", + usage=self.config.iron_ore_consumption, + unit="metric tons of iron ore per metric ton of steel", + cost=( + self.config.iron_ore_pellet_unitcost + self.config.iron_ore_pellet_transport_cost + ), + escalation=self.config.inflation_rate, + ) + pf.add_feedstock( + name="Hydrogen", + usage=self.config.hydrogen_consumption, + unit="metric tons of hydrogen per metric ton of steel", + cost=self.config.lcoh * 1000, + escalation=self.config.inflation_rate, + ) + pf.add_feedstock( + name="Natural Gas", + usage=self.config.natural_gas_consumption, + unit="GJ-LHV per metric ton of steel", + cost=self.config.natural_gas_prices, + escalation=self.config.inflation_rate, + ) + pf.add_feedstock( + name="Electricity", + usage=self.config.electricity_consumption, + unit="MWh per metric ton of steel", + cost=self.config.grid_prices, + escalation=self.config.inflation_rate, + ) + pf.add_feedstock( + name="Slag Disposal", + usage=self.config.slag_production, + unit="metric tons of slag per metric ton of steel", + cost=self.config.slag_disposal_unitcost, + escalation=self.config.inflation_rate, + ) + + pf.add_coproduct( + name="Oxygen sales", + usage=self.config.excess_oxygen, + unit="kg O2 per metric ton of steel", + cost=self.config.oxygen_market_price, + escalation=self.config.inflation_rate, + ) + + # Solve + sol = pf.solve_price() + + outputs["LCOS"] = sol.get("price") diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index 69b7ac663..1735d99e9 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -54,10 +54,7 @@ ReverseOsmosisPerformanceModel, ) from h2integrate.converters.hydrogen.basic_cost_model import BasicElectrolyzerCostModel -from h2integrate.converters.hydrogen.pem_electrolyzer import ( - ElectrolyzerCostModel, - ElectrolyzerPerformanceModel, -) +from h2integrate.converters.hydrogen.pem_electrolyzer import ECOElectrolyzerPerformanceModel from h2integrate.converters.solar.atb_res_com_pv_cost import ATBResComPVCostModel from h2integrate.converters.solar.atb_utility_pv_cost import ATBUtilityPVCostModel from h2integrate.resource.wind.nrel_developer_wtk_api import WTKNRELDeveloperAPIWindResource @@ -93,9 +90,6 @@ GOESFullDiscSolarAPI, GOESAggregatedSolarAPI, ) -from h2integrate.converters.hydrogen.eco_tools_pem_electrolyzer import ( - ECOElectrolyzerPerformanceModel, -) from h2integrate.converters.water_power.hydro_plant_run_of_river import ( RunOfRiverHydroCostModel, RunOfRiverHydroPerformanceModel, @@ -165,8 +159,6 @@ "atb_comm_res_pv_cost": ATBResComPVCostModel, "run_of_river_hydro_performance": RunOfRiverHydroPerformanceModel, "run_of_river_hydro_cost": RunOfRiverHydroCostModel, - "pem_electrolyzer_performance": ElectrolyzerPerformanceModel, - "pem_electrolyzer_cost": ElectrolyzerCostModel, "eco_pem_electrolyzer_performance": ECOElectrolyzerPerformanceModel, "singlitico_electrolyzer_cost": SingliticoCostModel, "basic_electrolyzer_cost": BasicElectrolyzerCostModel, diff --git a/h2integrate/simulation/technologies/hydrogen/__init__.py b/h2integrate/simulation/technologies/hydrogen/__init__.py deleted file mode 100644 index 0f6efc5fb..000000000 --- a/h2integrate/simulation/technologies/hydrogen/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from h2integrate.simulation.technologies.hydrogen.h2_transport.h2_compression import Compressor diff --git a/h2integrate/simulation/technologies/hydrogen/desal/desal_model.py b/h2integrate/simulation/technologies/hydrogen/desal/desal_model.py deleted file mode 100644 index 4dc57d64d..000000000 --- a/h2integrate/simulation/technologies/hydrogen/desal/desal_model.py +++ /dev/null @@ -1,133 +0,0 @@ -## High-Pressure Reverse Osmosis Desalination Model -""" -Python model of High-Pressure Reverse Osmosis Desalination (HPRO). - -Reverse Osmosis (RO) is a membrane separation process. No heating or phase change is necessary. -The majority of energy required is for pressurizing the feed water. - -A typical RO system is made up of the following basic components: -Pre-treatment: Removes suspended solids and microorganisms through sterilization, fine filtration - and adding chemicals to inhibit precipitation. -High-pressure pump: Supplies the pressure needed to enable the water to pass through the membrane - (pressure ranges from 54 to 80 bar for seawater). -Membrane Modules: Membrane assembly consists of a pressure vessel and the membrane. Either sprial - wound membranes or hollow fiber membranes are used. -Post-treatment: Consists of sterilization, stabilization, mineral enrichment and pH adjustment of - product water. -Energy recovery system: A system where a portion of the pressure energy of the brine is recovered. -""" - -import numpy as np - - -def RO_desal( - net_power_supply_kW, - desal_sys_size, - useful_life, - plant_life, - water_recovery_ratio=0.30, - energy_conversion_factor=4.2, - high_pressure_pump_efficency=0.70, - pump_pressure_kPa=5366, - energy_recovery=0.40, -): - """ - Calculates the fresh water flow rate (m^3/hr) as - a function of supplied power (kW) in RO desal. - Calculates CAPEX (USD), OPEX (USD/yr), annual cash flows - based on system's rated capacity (m^3/hr). - - Args: - net_power_supply_kW (list): Hourly power input [kW]. - desal_sys_size (float): Desired fresh water flow rate [m^3/hr]. - useful_life (int): Useful life of desal system [years]. - plant_life (int): Years of plant operation [years]. - - Assumed values: - Common set points from: - https://www.sciencedirect.com/science/article/abs/pii/S0011916409008443 - water_recovery_ratio (float): 0.30 - energy_conversion_factor (float): 4.2 - high_pressure_pump_efficency (float): 0.70 - pump_pressure_kPa (float): 5366 (kept static for simplicity) - energy_recovery (float): 0.40 - Assumed energy savings by energy recovery device to be 40% of total energy - https://www.sciencedirect.com/science/article/pii/S0360544210005578?casa_token=aEz_d_LiSgYAAAAA:88Xa6uHMTZee-djvJIF9KkhpuZmwZCLPHNiThmcwv9k9RC3H17JuSoRWI-l92rrTl_E3kO4oOA - - TODO: - Modify water recovery to vary based on salinity. - SWRO: Sea water Reverse Osmosis, water >18,000 ppm - SWRO energy_conversion_factor range 2.5 to 4.2 kWh/m^3 - Modify pressure through RO process - - BWRO: Brakish water Reverse Osmosis, water < 18,000 ppm - BWRO energy_conversion_factor range 1.0 to 1.5 kWh/m^3 - Source: https://www.sciencedirect.com/science/article/pii/S0011916417321057 - """ - # net_power_supply_kW = np.array(net_power_supply_kW) - - desal_power_max = desal_sys_size * energy_conversion_factor # kW - - # Modify power to not exceed system's power maximum (100% rated power capacity) or - # minimum (approx 50% rated power capacity --> affects filter fouling below this level) - net_power_for_desal = [] - operational_flags = [] - feed_water_flowrate = [] - fresh_water_flowrate = [] - for power_at_time_step in net_power_supply_kW: - if power_at_time_step > desal_power_max: - current_net_power_available = desal_power_max - operational_flag = 2 - elif (0.5 * desal_power_max) <= power_at_time_step <= desal_power_max: - current_net_power_available = power_at_time_step - operational_flag = 1 - elif power_at_time_step <= 0.5 * desal_power_max: - current_net_power_available = 0 - operational_flag = 0 - - # Append Operational Flags to a list - operational_flags.append(operational_flag) - # Create list of net power available for desal at each timestep - net_power_for_desal.append(current_net_power_available) - - # Create list of feedwater flowrates based on net power available for desal - # https://www.sciencedirect.com/science/article/abs/pii/S0011916409008443 - instantaneous_feed_water_flowrate = ( - ((current_net_power_available * (1 + energy_recovery)) * high_pressure_pump_efficency) - / pump_pressure_kPa - * 3600 - ) # m^3/hr - - instantaneous_fresh_water_flowrate = ( - instantaneous_feed_water_flowrate * water_recovery_ratio - ) # m^3/hr - - feed_water_flowrate.append(instantaneous_feed_water_flowrate) - fresh_water_flowrate.append(instantaneous_fresh_water_flowrate) - - """Values for CAPEX and OPEX given as $/(kg/s) - Source: https://www.nrel.gov/docs/fy16osti/66073.pdf - Assumed density of recovered water = 997 kg/m^3""" - - desal_capex = 32894 * (997 * desal_sys_size / 3600) # Output in USD - - desal_opex = 4841 * (997 * desal_sys_size / 3600) # Output in USD/yr - - """ - Assumed useful life = payment period for capital expenditure. - compressor amortization interest = 3% - """ - - return ( - fresh_water_flowrate, - feed_water_flowrate, - operational_flags, - desal_capex, - desal_opex, - ) - - -if __name__ == "__main__": - Power = np.array([446, 500, 183, 200, 250, 100]) - test = RO_desal(Power, 300, 30, 30) - print(test) diff --git a/h2integrate/simulation/technologies/hydrogen/desal/desal_model_eco.py b/h2integrate/simulation/technologies/hydrogen/desal/desal_model_eco.py deleted file mode 100644 index dd0f09172..000000000 --- a/h2integrate/simulation/technologies/hydrogen/desal/desal_model_eco.py +++ /dev/null @@ -1,136 +0,0 @@ -################## needed addition ###################### -""" -Description: This file already contains a desal model, but we need an estimate of the desal unit - size, particularly mass and footprint (m^2) -Sources: - - [1] Singlitico 2021 (use this as a jumping off point, I think there may be other good sources - available) - - [2] See sources in existing model below and the model itself -Args: - - electrolyzer_rating (float): electrolyzer rating in MW - - input and output values from RO_desal() below - - others may be added as needed -Returns (can be from separate functions and/or methods as it makes sense): - - mass (float): approximate mass of the desalination system (kg or metric tons) - - footprint (float): approximate area required for the desalination system (m^2) -""" - - -#################### existing model ######################## - -## High-Pressure Reverse Osmosis Desalination Model -""" -Python model of High-Pressure Reverse Osmosis Desalination (HPRO). - -Reverse Osmosis (RO) is a membrane separation process. No heating or phase change is necessary. -The majority of energy required is for pressurizing the feed water. - -A typical RO system is made up of the following basic components: -Pre-treatment: Removes suspended solids and microorganisms through sterilization, fine filtration - and adding chemicals to inhibit precipitation. -High-pressure pump: Supplies the pressure needed to enable the water to pass through the membrane - (pressure ranges from 54 to 80 bar for seawater). -Membrane Modules: Membrane assembly consists of a pressure vessel and the membrane. Either sprial - wound membranes or hollow fiber membranes are used. -Post-treatment: Consists of sterilization, stabilization, mineral enrichment and pH adjustment of - product water. -Energy recovery system: A system where a portion of the pressure energy of the brine is recovered. - -Costs are in 2013 dollars -""" - - -def RO_desal_eco(freshwater_kg_per_hr, salinity): - """ - param: freshwater_kg_per_hr: Maximum freshwater requirements of system [kg/hr] - - param: salinity: (str) "Seawater" >18,000 ppm or "Brackish" <18,000 ppm - - output: feedwater_m3_per_hr: feedwater flow rate [m^3/hr] - - output: desal_power: reqired power [kW] - - output: desal_capex: Capital cost [USD] - - output: desal_opex: OPEX (USD/yr) - - Costs from: https://pdf.sciencedirectassets.com/271370/1-s2.0-S0011916408X00074/1-s2.0-S0011916408002683/main.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEcaCXVzLWVhc3QtMSJGMEQCIBNfL%2Frp%2BWpMGUW7rWBm3dkXztvOFIdswOdqI23VkBTGAiALG4NJuAiUkzKnukw233sXHF1OFBPnogJP1ZkboPkaiSrVBAjA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAUaDDA1OTAwMzU0Njg2NSIMWZ%2Fh3cDnrPjUJMleKqkELlVPKjinHYk85KwguMS3panLr1RRD9qkoxIASocYCbkvKLE9xW%2BT8QMCtEaH3Is7NRZ2Efc6YFQiO0DHbRzXYTfgz6Er5qqvSAFTrfgp%2B5bB3NYvtDI3kEGH%2F%2BOrEiL8iDK9TmgUjojvnKt86zidswBSDWrzclxcLrw6dfsqZf6dVjJT2g3Cyy8LKnP9vc33tCbACRLeszW1Zce%2BTlBbON22W%2FJq0qLcXDxI9JpRDqL8T%2Fo7SsetEif2DWovTLnv%2B%2FX2tJotFp630ZTVpd37ukGtanjAr5pl0nHgjnUtOJVtNksHQwc8XElFpBGKEXmvRo2uZJFd%2BeNtPEB1dWIZlZul6B8%2BJ7D%2FSPJsclPfpkMU92YUStQpw4Mc%2FOJFCILFyb4416DsL6PVWsdcYu9bbry8c0hQGZlE7oXTFoUy9SKdpEOguXAUi3X4JxjZisy3esVH8zNS3%2FiFsNr2FkTB6MLaSjSKj344AuDCkQYZ7CnenAiCHgf4a2tSnfiXzAvAFnpeQkr4iCnZOQ4Eis6L3fVRpWlluX5HUpbvUMN6rvtmAzq0APJn1b3NmFHy4ORoemTGvmI%2FHTRYKuAu257XBMe7X1qAJlnmpt6yGXrelXCz%2FmUvmbT1SzxETA5ss4KR0OM4YdXNnFLUrsV44ZkUM%2B8FlwZr%2F%2FePjz4QeG4ApR821IYTyre3%2FY%2BBZxaMs5AcXKiTHGwfE7CDi%2BQQ7CnDKk0lleZcas6kxzDl9%2BmeBjqqAeZhBVwd5sEx6aDGxAQC0eWpux6HauoVfuPOCkkv621szF0kTBqcoOlJlJav4eUPW4efAzBremirjiRLI2GdP72lVqXz9oaCg5NFXeKJAWbWkLdzHnDOu8ecSUPn%2F0jcR2IO2mznLspx6wKQA%2BAPEVGgptkwZtDqHcw8FNx7Q8tWJ1C4qL1bEMl0%2FatDXOHiJfuzCFp4%2B4uijTNfpVXO%2BzYQuNJA7ZNUMroa&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230201T155950Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=ASIAQ3PHCVTY7RLVF2MG%2F20230201%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=a3770ee910f7f78c94bb84206538810ca03f7a653183191b3794c633b9e3f08f&hash=2e8904ff0d2a6ef567a5894d5bb773524bf1a90bc3ed88d8592e3f9d4cc3c531&host=68042c943591013ac2b2430a89b270f6af2c76d8dfd086a07176afe7c76c2c61&pii=S0011916408002683&tid=spdf-27339dc5-0d03-4078-a244-c049a9bb014d&sid=50eb5802654ba84dc80a5675e9bbf644ed4dgxrqa&type=client&tsoh=d3d3LnNjaWVuY2VkaXJlY3QuY29t&ua=0f1650585c05065559515c&rr=792be5868a1a8698&cc=us - - A desal system capacity is given as desired freshwater flow rate [m^3/hr] - """ - - freshwater_density = 997 # [kg/m^3] - freshwater_m3_per_hr = freshwater_kg_per_hr / freshwater_density - desal_capacity = freshwater_m3_per_hr - - if salinity == "Seawater": - # SWRO: Sea Water Reverse Osmosis, water >18,000 ppm - # Water recovery - recovery_ratio = 0.5 # https://www.usbr.gov/research/dwpr/reportpdfs/report072.pdf - feedwater_m3_per_hr = freshwater_m3_per_hr / recovery_ratio - - # Power required - energy_conversion_factor = ( - 4.0 # [kWh/m^3] SWRO energy_conversion_factor range 2.5 to 4.0 kWh/m^3 - ) - # https://www.sciencedirect.com/science/article/pii/S0011916417321057 - desal_power = freshwater_m3_per_hr * energy_conversion_factor - - elif salinity == "Brackish": - # BWRO: Brakish water Reverse Osmosis, water < 18,000 ppm - # Water recovery - recovery_ratio = 0.75 # https://www.usbr.gov/research/dwpr/reportpdfs/report072.pdf - feedwater_m3_per_hr = freshwater_m3_per_hr / recovery_ratio - - # Power required - energy_conversion_factor = ( - 1.5 # [kWh/m^3] BWRO energy_conversion_factor range 1.0 to 1.5 kWh/m^3 - ) - # https://www.sciencedirect.com/science/article/pii/S0011916417321057 - - desal_power = freshwater_m3_per_hr * energy_conversion_factor - - else: - raise Exception("Salinity parameter must be set to Brackish or Seawater") - - # Costing - # https://www.nrel.gov/docs/fy16osti/66073.pdf - desal_capex = 32894 * (freshwater_density * desal_capacity / 3600) # [USD] - - desal_opex = 4841 * (freshwater_density * desal_capacity / 3600) # [USD/yr] - - """Mass and Footprint - Based on Commercial Industrial RO Systems - https://www.appliedmembranes.com/s-series-seawater-reverse-osmosis-systems-2000-to-100000-gpd.html - - All Mass and Footprint Estimates are estimated from Largest RO System: - S-308F - -436 m^3/day - -6330 kg - -762 cm (L) x 112 cm (D) x 183 cm (H) - - 436 m^3/day = 18.17 m^3/hr = 8.5 m^2, 6330 kg - 1 m^3/hr = .467 m^2, 346.7 kg - - Voltage Codes - 460 or 480v/ 3ph/ 60 Hz - """ - desal_mass_kg = freshwater_m3_per_hr * 346.7 # [kg] - desal_size_m2 = freshwater_m3_per_hr * 0.467 # [m^2] - - return ( - desal_capacity, - feedwater_m3_per_hr, - desal_power, - desal_capex, - desal_opex, - desal_mass_kg, - desal_size_m2, - ) - - -if __name__ == "__main__": - desal_freshwater_kg_hr = 75000 - salinity = "Brackish" - test = RO_desal_eco(desal_freshwater_kg_hr, salinity) - print(test) diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/H2_cost_model.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/H2_cost_model.py deleted file mode 100644 index 74b347122..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/H2_cost_model.py +++ /dev/null @@ -1,313 +0,0 @@ -import warnings - -import numpy as np - - -def basic_H2_cost_model( - electrolyzer_capex_kw, - time_between_replacement, - electrolyzer_size_mw, - useful_life, - electrical_generation_timeseries_kw, - hydrogen_annual_output, - PTC_USD_kg, - ITC_perc, - include_refurb_in_opex=False, - offshore=0, -): - """ - Basic cost modeling for a PEM electrolyzer. - Looking at cost projections for PEM electrolyzers over years 2022, 2025, 2030, 2035. - Electricity costs are calculated outside of hydrogen cost model - - Needs: - Scaling factor for off-shore electrolysis - Verifying numbers are appropriate for simplified cash flows - Verify how H2 PTC works/factors into cash flows - - If offshore = 1, then additional cost scaling is added to account for added difficulties for - offshore installation, offshore=0 means onshore - """ - - # Basic information in our analysis - kw_continuous = electrolyzer_size_mw * 1000 - - # Capacity factor - avg_generation = np.mean(electrical_generation_timeseries_kw) # Avg Generation - # print("avg_generation: ", avg_generation) - cap_factor = avg_generation / kw_continuous - - if cap_factor > 1.0: - cap_factor = 1.0 - warnings.warn( - "Electrolyzer capacity factor would be greater than 1 with provided energy profile." - " Capacity factor has been reduced to 1 for electrolyzer cost estimate purposes." - ) - - # print(cap_factor) - # if cap_factor != approx(1.0): - # raise(ValueError("Capacity factor must equal 1")) - # print("cap_factor",cap_factor) - - # #Apply PEM Cost Estimates based on year based on GPRA pathway (H2New) - # if atb_year == 2022: - # electrolyzer_capex_kw = 1100 #[$/kW capacity] stack capital cost - # time_between_replacement = 40000 #[hrs] - # elif atb_year == 2025: - # electrolyzer_capex_kw = 300 - # time_between_replacement = 80000 #[hrs] - # elif atb_year == 2030: - # electrolyzer_capex_kw = 150 - # time_between_replacement = 80000 #[hrs] - # elif atb_year == 2035: - # electrolyzer_capex_kw = 100 - # time_between_replacement = 80000 #[hrs] - - # Hydrogen Production Cost From PEM Electrolysis - 2019 (HFTO Program Record) - # https://www.hydrogen.energy.gov/pdfs/19009_h2_production_cost_pem_electrolysis_2019.pdf - - # Capital costs provide by Hydrogen Production Cost From PEM Electrolysis - 2019 (HFTO - # Program Record) - stack_capital_cost = 342 # [$/kW] - mechanical_bop_cost = 36 # [$/kW] for a compressor - electrical_bop_cost = 82 # [$/kW] for a rectifier - - # Installed capital cost - stack_installation_factor = 12 / 100 # [%] for stack cost - elec_installation_factor = 12 / 100 # [%] and electrical BOP - - # scale installation fraction if offshore (see Singlitico 2021 https://doi.org/10.1016/j.rset.2021.100005) - stack_installation_factor *= 1 + offshore - elec_installation_factor *= 1 + offshore - - # mechanical BOP install cost = 0% - - # Indirect capital cost as a percentage of installed capital cost - site_prep = 2 / 100 # [%] - engineering_design = 10 / 100 # [%] - project_contingency = 15 / 100 # [%] - permitting = 15 / 100 # [%] - land = 250000 # [$] - - stack_replacment_cost = 15 / 100 # [% of installed capital cost] - fixed_OM = 0.24 # [$/kg H2] - - program_record = False - - # Chose to use numbers provided by GPRA pathways - if program_record: - total_direct_electrolyzer_cost_kw = ( - (stack_capital_cost * (1 + stack_installation_factor)) - + mechanical_bop_cost - + (electrical_bop_cost * (1 + elec_installation_factor)) - ) - else: - total_direct_electrolyzer_cost_kw = ( - (electrolyzer_capex_kw * (1 + stack_installation_factor)) - + mechanical_bop_cost - + (electrical_bop_cost * (1 + elec_installation_factor)) - ) - - # Assign CapEx for electrolyzer from capacity based installed CapEx - electrolyzer_total_installed_capex = ( - total_direct_electrolyzer_cost_kw * electrolyzer_size_mw * 1000 - ) - - # Add indirect capital costs - electrolyzer_total_capital_cost = ( - ( - (site_prep + engineering_design + project_contingency + permitting) - * electrolyzer_total_installed_capex - ) - + land - + electrolyzer_total_installed_capex - ) - - # O&M costs - # https://www.sciencedirect.com/science/article/pii/S2542435121003068 - # for 700 MW electrolyzer (https://www.hydrogen.energy.gov/pdfs/19009_h2_production_cost_pem_electrolysis_2019.pdf) - h2_FOM_kg = 0.24 # [$/kg] - - # linearly scaled current central fixed O&M for a 700MW electrolyzer up to a 1000MW electrolyzer - scaled_h2_FOM_kg = h2_FOM_kg * electrolyzer_size_mw / 700 - - h2_FOM_kWh = scaled_h2_FOM_kg / 55.5 # [$/kWh] used 55.5 kWh/kg for efficiency - fixed_OM = h2_FOM_kWh * 8760 # [$/kW-y] - property_tax_insurance = 1.5 / 100 # [% of Cap/y] - variable_OM = 1.30 # [$/MWh] - - # Amortized refurbishment expense [$/MWh] - if not include_refurb_in_opex: - amortized_refurbish_cost = 0.0 - else: - amortized_refurbish_cost = ( - (total_direct_electrolyzer_cost_kw * stack_replacment_cost) - * max(((useful_life * 8760 * cap_factor) / time_between_replacement - 1), 0) - / useful_life - / 8760 - / cap_factor - * 1000 - ) - - # Total O&M costs [% of installed cap/year] - total_OM_costs = ( - fixed_OM + (property_tax_insurance * total_direct_electrolyzer_cost_kw) - ) / total_direct_electrolyzer_cost_kw + ( - (variable_OM + amortized_refurbish_cost) - / 1000 - * 8760 - * (cap_factor / total_direct_electrolyzer_cost_kw) - ) - - capacity_based_OM = True - if capacity_based_OM: - electrolyzer_OM_cost = electrolyzer_total_installed_capex * total_OM_costs # Capacity based - else: - electrolyzer_OM_cost = ( - fixed_OM * hydrogen_annual_output - ) # Production based - likely not very accurate - - # Add in electrolyzer repair schedule (every 7 years) - # Use if not using time between replacement given in hours - # Currently not added into further calculations - electrolyzer_repair_schedule = [] - counter = 1 - for year in range(0, useful_life): - if year == 0: - electrolyzer_repair_schedule = np.append(electrolyzer_repair_schedule, [0]) - - elif counter % time_between_replacement == 0: - electrolyzer_repair_schedule = np.append(electrolyzer_repair_schedule, [1]) - - else: - electrolyzer_repair_schedule = np.append(electrolyzer_repair_schedule, [0]) - counter += 1 - electrolyzer_repair_schedule * (stack_replacment_cost * electrolyzer_total_installed_capex) - - # Include Hydrogen PTC from the Inflation Reduction Act (range $0.60 - $3/kg-H2) - h2_tax_credit = [0] * useful_life - h2_tax_credit[0:10] = [hydrogen_annual_output * PTC_USD_kg] * 10 - - # Include ITC from IRA (range 0% - 50%) - # ITC is expressed as a percentage of the total installed cost which reduces the annual tax - # liabiity in year one of the project cash flow. - h2_itc = (ITC_perc / 100) * electrolyzer_total_installed_capex - cf_h2_itc = [0] * 30 - cf_h2_itc[1] = h2_itc - - return ( - electrolyzer_total_capital_cost, - electrolyzer_OM_cost, - electrolyzer_capex_kw, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) - - -if __name__ == "__main__": - import matplotlib.pyplot as plt - - # plot a sweep of sizes for OPEX and CAPEX - - electrolyzer_capex_kw = 1300 # $/kW - time_between_replacement = 65000 # hours - electrolyzer_sizes_mw = np.arange(1, 1000) - useful_life = 30 # years - atb_year = 2025 - # electrical_generation_timeseries_kw = np.sin(np.arange(0,24*365)*1E-3)*0.5E6 + 0.6E6 - hydrogen_annual_output = 0 - - # for distributed - ndivs = [2, 5, 10] - - opex = [] - capex = [] - opex_distributed = np.zeros((len(ndivs), len(electrolyzer_sizes_mw))) - capex_distributed = np.zeros((len(ndivs), len(electrolyzer_sizes_mw))) - - for i, electrolyzer_size_mw in enumerate(electrolyzer_sizes_mw): - electrical_generation_timeseries_kw = electrolyzer_size_mw * 1000 * np.ones(365 * 24) - - # centralized - _, electrolyzer_total_capital_cost, electrolyzer_OM_cost, _, _, _, _ = basic_H2_cost_model( - electrolyzer_capex_kw, - time_between_replacement, - electrolyzer_size_mw, - useful_life, - electrical_generation_timeseries_kw, - hydrogen_annual_output, - 0, - 0, - include_refurb_in_opex=False, - offshore=0, - ) - - opex.append(electrolyzer_OM_cost) - capex.append(electrolyzer_total_capital_cost) - - for j, div in enumerate(ndivs): - # divided - electrolyzer_size_mw_distributed = electrolyzer_size_mw / div - electrical_generation_timeseries_kw_distibuted = ( - electrical_generation_timeseries_kw / div - ) - - ( - _, - electrolyzer_capital_cost_distributed, - electrolyzer_OM_cost_distributed, - electrolyzer_capex_kw_distributed, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) = basic_H2_cost_model( - electrolyzer_capex_kw, - time_between_replacement, - electrolyzer_size_mw_distributed, - useful_life, - electrical_generation_timeseries_kw_distibuted, - hydrogen_annual_output, - 0, - 0, - include_refurb_in_opex=False, - offshore=0, - ) - # print(opex_distributed) - opex_distributed[j, i] = electrolyzer_OM_cost_distributed * div - capex_distributed[j, i] = electrolyzer_capital_cost_distributed * div - - fig, ax = plt.subplots(1, 2, figsize=(6, 3)) - ax[0].plot(electrolyzer_sizes_mw, np.asarray(capex) * 1e-6, label="Centralized") - ax[1].plot(electrolyzer_sizes_mw, np.asarray(opex) * 1e-6, label="Centralized") - - for i, div in enumerate(ndivs): - # dims(capex_distributed) - ax[0].plot( - electrolyzer_sizes_mw, - np.asarray(capex_distributed[i]) * 1e-6, - "--", - label=f"{div} Divisions", - ) - ax[1].plot( - electrolyzer_sizes_mw, - np.asarray(opex_distributed[i]) * 1e-6, - "--", - label=f"{div} Divisions", - ) - - ax[0].set(ylabel="CAPEX (M USD)", xlabel="Electrolyzer Size (MW)") - ax[1].set(ylabel="Annual OPEX (M USD)", xlabel="Electrolyzer Size (MW)") - plt.legend(frameon=False) - plt.tight_layout() - plt.show() - - ## plot divided energy signals - fig, ax = plt.subplots(1) - ax.plot(electrical_generation_timeseries_kw, label="1") - for div in ndivs: - ax.plot(electrical_generation_timeseries_kw / div, label=f"{div}") - - ax.set(xlabel="Hour", ylabel="Power (MW)") - plt.tight_layout() - plt.show() diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/BOP_efficiency_BOL.csv b/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/BOP_efficiency_BOL.csv deleted file mode 100644 index df25ab18e..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/BOP_efficiency_BOL.csv +++ /dev/null @@ -1,87 +0,0 @@ -operating_ratio,efficiency -0.937732223,4.285714286 -0.933250382,4.258241758 -0.929665402,4.230769231 -0.925182329,4.217032967 -0.920700488,4.18956044 -0.916217415,4.175824176 -0.911735574,4.148351648 -0.904564382,4.107142857 -0.896495097,4.07967033 -0.887531415,4.024725275 -0.878566501,3.983516484 -0.870498448,3.942307692 -0.862430395,3.901098901 -0.856154832,3.873626374 -0.847189918,3.832417582 -0.837328143,3.791208791 -0.827466368,3.75 -0.818501454,3.708791209 -0.805950328,3.653846154 -0.796985414,3.612637363 -0.786226778,3.571428571 -0.775468142,3.53021978 -0.757537082,3.461538462 -0.743192234,3.406593407 -0.730641108,3.351648352 -0.717193121,3.296703297 -0.703743902,3.255494505 -0.687605332,3.200549451 -0.674156113,3.159340659 -0.661603755,3.118131868 -0.646362046,3.063186813 -0.632017198,3.008241758 -0.614979303,2.980769231 -0.588977726,2.898351648 -0.570147341,2.857142857 -0.550420096,2.815934066 -0.530694082,2.760989011 -0.513656187,2.733516484 -0.490341497,2.692307692 -0.475096092,2.678571429 -0.449089588,2.651098901 -0.415906963,2.637362637 -0.388104272,2.637362637 -0.372856404,2.651098901 -0.348638693,2.678571429 -0.329802149,2.706043956 -0.309169418,2.760989011 -0.286741734,2.82967033 -0.266102843,2.953296703 -0.245462721,3.090659341 -0.228410043,3.228021978 -0.207766225,3.406593407 -0.194302222,3.53021978 -0.181735081,3.653846154 -0.173653477,3.763736264 -0.167365594,3.873626374 -0.16107648,3.997252747 -0.153889272,4.134615385 -0.146698369,4.313186813 -0.138608141,4.519230769 -0.131417237,4.697802198 -0.125120731,4.903846154 -0.120620411,5.082417582 -0.114320209,5.32967033 -0.110717982,5.494505495 -0.107116986,5.645604396 -0.103513527,5.824175824 -0.099907604,6.03021978 -0.097199773,6.222527473 -0.093595082,6.414835165 -0.089987927,6.634615385 -0.086377076,6.895604396 -0.084566107,7.087912088 -0.081858276,7.28021978 -0.079147982,7.5 -0.078235106,7.678571429 -0.076422904,7.884615385 -0.074610703,8.090659341 -0.070994924,8.406593407 -0.069177795,8.667582418 -0.067361898,8.914835165 -0.064645444,9.203296703 -0.06372764,9.436813187 -0.061910511,9.697802198 -0.061894496,9.876373626 -0.060088454,10.01373626 diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py deleted file mode 100644 index 084217480..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py +++ /dev/null @@ -1,136 +0,0 @@ -from pathlib import Path - -import numpy as np -import pandas as pd -import scipy.optimize - -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_tools import ( - get_electrolyzer_BOL_efficiency, -) - - -file_path = Path(__file__).parent - - -def calc_efficiency_curve(operating_ratio, a, b, c, d): - """Calculates efficiency [kWh/kg] given operation ratio with flattened end curves. - - Efficiency curve and general equation structure from Wang et. al (2023). See README.md - in PEM_BOP directory for more details. - - Wang, X.; Star, A.G.; Ahluwalia, R.K. Performance of Polymer Electrolyte Membrane Water - Electrolysis Systems: Configuration, Stack Materials, Turndown and Efficiency. Energies 2023, - 16, 4964. https://doi.org/10.3390/en16134964 - - Args: - operating_ratio (list or np.array): Operation ratios. - a (float): Coefficient a. - b (float): Coefficient b. - c (float): Coefficient c. - d (float): Coefficient d. - - Returns: - efficiency (list or np.array): Efficiency of electrolyzer BOP in kWh/kg. - """ - efficiency = a + b * operating_ratio + c * operating_ratio**2 + d / operating_ratio - - return efficiency - - -def calc_efficiency( - operating_ratio, efficiency, min_ratio, max_ratio, min_efficiency, max_efficiency -): - """Adjust efficiency list to not go above minimum or maximum operating ratios in - BOP_efficiency_BOL.csv - - Args: - operating_ratio (list or np.array): Operation ratios. - efficiency (list or np.array): Efficiencies calculated using curve fit. - min_ratio (float): Minimum operating ratio from the CSV. - max_ratio (float): Maximum operating ratio from the CSV. - min_efficiency (float): Efficiency at the minimum operating ratio. - max_efficiency (float): Efficiency at the maximum operating ratio. - - Returns: - efficiency (list or np.array): Efficiencies limited with minimum and maximum values - in kWh/kg. - """ - efficiency = np.where(operating_ratio <= min_ratio, min_efficiency, efficiency) - - efficiency = np.where(operating_ratio >= max_ratio, max_efficiency, efficiency) - return efficiency - - -def calc_curve_coefficients(): - """Calculates curve coefficients from BOP_efficiency_BOL.csv""" - df = pd.read_csv(file_path / "BOP_efficiency_BOL.csv") - operating_ratios = df["operating_ratio"].values - efficiency = df["efficiency"].values - - # Get min and max operating ratios - min_ratio_idx = df["operating_ratio"].idxmin() # Index of minimum operating ratio - max_ratio_idx = df["operating_ratio"].idxmax() # Index of maximum operating ratio - - # Get the efficiency at the min and max operating ratios - min_efficiency = df["efficiency"].iloc[min_ratio_idx] - max_efficiency = df["efficiency"].iloc[max_ratio_idx] - - # Get the actual min and max ratios - min_ratio = df["operating_ratio"].iloc[min_ratio_idx] - max_ratio = df["operating_ratio"].iloc[max_ratio_idx] - - curve_coeff, curve_cov = scipy.optimize.curve_fit( - calc_efficiency_curve, operating_ratios, efficiency, p0=(1.0, 1.0, 1.0, 1.0) - ) - return curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency - - -def pem_bop( - power_profile_to_electrolyzer_kw, - electrolyzer_rated_mw, - electrolyzer_turn_down_ratio, -): - """ - Calculate PEM balance of plant energy consumption at the beginning-of-life - based on power provided to the electrolyzer. - - Args: - power_profile_to_electrolyzer_kw (list or np.array): Power profile to electrolyzer in kW. - electrolyzer_rated_mw (float): The rating of the PEM electrolyzer in MW. - electrolyzer_turn_down_ratio (float): The electrolyzer turndown ratio. - - Returns: - energy_consumption_bop_kwh (list or np.array): Energy consumed by electrolyzer BOP in kWh. - """ - operating_ratios = power_profile_to_electrolyzer_kw / (electrolyzer_rated_mw * 1e3) - - curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency = calc_curve_coefficients() - - efficiencies = calc_efficiency_curve( - operating_ratios, - *curve_coeff, - ) # kwh/kg - - efficiencies = calc_efficiency( - operating_ratios, - efficiencies, - min_ratio, - max_ratio, - min_efficiency, - max_efficiency, - ) - - BOL_efficiency = get_electrolyzer_BOL_efficiency() # kwh/kg - - BOL_kg = (electrolyzer_rated_mw * 1000) / BOL_efficiency # kg/hr - - energy_consumption_bop_kwh = efficiencies * BOL_kg # kwh - - energy_consumption_bop_kwh = np.where( - power_profile_to_electrolyzer_kw - < electrolyzer_turn_down_ratio * electrolyzer_rated_mw * 1000, - 0, - energy_consumption_bop_kwh, - ) - - return energy_consumption_bop_kwh diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md b/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md deleted file mode 100644 index 267bf00f1..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Proton Exchange Membrane Water Electrolysis Balance-of-Plant - -This balance-of-plant (BOP) model is derived from Wang et. al (2023). It is represented as an overall BOP efficiency curve (kWh/kg) at different operating ratios at the beginning-of-life (BOL) of the electrolyzer. The operating ratios are the percentage of rated power provided to the electrolyzer. - -The BOP curve is largely dominated by electrical losses from the power electronics which includes a transformer and a rectifier to condition alternating current power, but there are additional mechanical and hydrogen losses. - -The model in H2Integrate calculates a curve fit based on the provided CSV it also limits the calculated efficiencies to the maximum and minimum operating ratio values in the CSV, making the overall function a piecewise implementation. - -**NOTE**: BOP assumes AC current as an input and assumes power electronics for AC to DC conversion. BOP efficiency curve has not been optimized for economies of scale or other electrical infrastructure connections. - - -Citation for BOP model. Losses from BOP are shown in Figure 8. in Wang et. al (2023). -``` -@Article{en16134964, -AUTHOR = {Wang, Xiaohua and Star, Andrew G. and Ahluwalia, Rajesh K.}, -TITLE = {Performance of Polymer Electrolyte Membrane Water Electrolysis Systems: Configuration, Stack Materials, Turndown and Efficiency}, -JOURNAL = {Energies}, -VOLUME = {16}, -YEAR = {2023}, -NUMBER = {13}, -ARTICLE-NUMBER = {4964}, -URL = {https://www.mdpi.com/1996-1073/16/13/4964}, -ISSN = {1996-1073}, -DOI = {10.3390/en16134964} -} -``` diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_H2_LT_electrolyzer.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_H2_LT_electrolyzer.py deleted file mode 100644 index 2b964b6ec..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_H2_LT_electrolyzer.py +++ /dev/null @@ -1,516 +0,0 @@ -## Low-Temperature PEM Electrolyzer Model -""" -Python model of H2 PEM low-temp electrolyzer. - -Quick Hydrogen Physics: - -1 kg H2 <-> 11.1 N-m3 <-> 33.3 kWh (LHV) <-> 39.4 kWh (HHV) - -High mass energy density (1 kg H2= 3,77 l gasoline) -Low volumetric density (1 Nm³ H2= 0,34 l gasoline - -Hydrogen production from water electrolysis (~5 kWh/Nm³ H2) - -Power:1 MW electrolyser <-> 200 Nm³/h H2 <-> ±18 kg/h H2 -Energy:+/-55 kWh of electricity --> 1 kg H2 <-> 11.1 Nm³ <-> ±10 liters -demineralized water - -Power production from a hydrogen PEM fuel cell from hydrogen (+/-50% -efficiency): -Energy: 1 kg H2 --> 16 kWh -""" - -import sys -import math - -import numpy as np -import pandas as pd -from matplotlib import pyplot as plt - - -np.set_printoptions(threshold=sys.maxsize) - - -class PEM_electrolyzer_LT: - """ - Create an instance of a low-temperature PEM Electrolyzer System. Each - stack in the electrolyzer system in this model is rated at 1 MW_DC. - - Parameters - _____________ - np_array P_input_external_kW - 1-D array of time-series external power supply - - string voltage_type - Nature of voltage supplied to electrolyzer from the external power - supply ['variable' or 'constant] - - float power_supply_rating_MW - Rated power of external power supply - - Returns - _____________ - - """ - - def __init__(self, input_dict, output_dict): - self.input_dict = input_dict - self.output_dict = output_dict - - # array of input power signal - self.input_dict["P_input_external_kW"] = input_dict["P_input_external_kW"] - self.electrolyzer_system_size_MW = input_dict["electrolyzer_system_size_MW"] - - # self.input_dict['voltage_type'] = 'variable' # not yet implemented - self.input_dict["voltage_type"] = "constant" - self.stack_input_voltage_DC = 250 - - # Assumptions: - self.min_V_cell = 1.62 # Only used in variable voltage scenario - self.p_s_h2_bar = 31 # H2 outlet pressure - self.stack_input_current_lower_bound = 500 - self.stack_rating_kW = 1000 # 1 MW - self.cell_active_area = 1250 - self.N_cells = 130 - - # Constants: - self.moles_per_g_h2 = 0.49606 - self.V_TN = 1.48 # Thermo-neutral Voltage (Volts) - self.F = 96485 # Faraday's Constant (C/mol) - self.R = 8.314 # Ideal Gas Constant (J/mol/K) - - self.external_power_supply() - - def external_power_supply(self): - """ - External power source (grid or REG) which will need to be stepped - down and converted to DC power for the electrolyzer. - - Please note, for a wind farm as the electrolyzer's power source, - the model assumes variable power supplied to the stack at fixed - voltage (fixed voltage, variable power and current) - - TODO: extend model to accept variable voltage, current, and power - This will replicate direct DC-coupled PV system operating at MPP - """ - power_converter_efficiency = 0.95 - if self.input_dict["voltage_type"] == "constant": - self.input_dict["P_input_external_kW"] = np.where( - self.input_dict["P_input_external_kW"] > (self.electrolyzer_system_size_MW * 1000), - (self.electrolyzer_system_size_MW * 1000), - self.input_dict["P_input_external_kW"], - ) - - self.output_dict["curtailed_P_kW"] = np.where( - self.input_dict["P_input_external_kW"] > (self.electrolyzer_system_size_MW * 1000), - ( - self.input_dict["P_input_external_kW"] - - (self.electrolyzer_system_size_MW * 1000) - ), - 0, - ) - - self.output_dict["current_input_external_Amps"] = ( - self.input_dict["P_input_external_kW"] * 1000 * power_converter_efficiency - ) / (self.stack_input_voltage_DC * self.system_design()) - - self.output_dict["stack_current_density_A_cm2"] = ( - self.output_dict["current_input_external_Amps"] / self.cell_active_area - ) - - self.output_dict["current_input_external_Amps"] = np.where( - self.output_dict["current_input_external_Amps"] - < self.stack_input_current_lower_bound, - 0, - self.output_dict["current_input_external_Amps"], - ) - - else: - pass # TODO: extend model to variable voltage and current source - - def system_design(self): - """ - For now, system design is solely a function of max. external power - supply; i.e., a rated power supply of 50 MW means that the electrolyzer - system developed by this model is also rated at 50 MW - - TODO: Extend model to include this capability. - Assume that a PEM electrolyzer behaves as a purely resistive load - in a circuit, and design the configuration of the entire electrolyzer - system - which may consist of multiple stacks connected together in - series, parallel, or a combination of both. - """ - h2_production_multiplier = (self.electrolyzer_system_size_MW * 1000) / self.stack_rating_kW - self.output_dict["electrolyzer_system_size_MW"] = self.electrolyzer_system_size_MW - return h2_production_multiplier - - def cell_design(self): - """ - Creates an I-V (polarization) curve of each cell in a stack. - - Please note that this method is currently not used in the model. It - will be used once the electrolyzer model is expanded to variable - voltage supply as well as implementation of the self.system_design() - method - - Motivation: - - The most common representation of the electrolyzer performance is the - polarization curve that represents the relation between the current density - and the voltage (V): - Source: https://www.sciencedirect.com/science/article/pii/S0959652620312312 - - V = N_c(E_cell + V_Act,c + V_Act,a + iR_cell) - - where N_c is the number of electrolyzer cells,E_cell is the open circuit - voltage VAct,and V_Act,c are the anode and cathode activation over-potentials, - i is the current density and iRcell is the electrolyzer cell resistance - (ohmic losses). - - Use this to make a V vs. A (Amperes/cm2) graph which starts at 1.23V because - thermodynamic reaction of water formation/splitting dictates that standard - electrode potential has a ∆G of 237 kJ/mol (where: ∆H = ∆G + T∆S) - """ - - # Cell level inputs: - N_cells = 130 - self.cell_active_area / N_cells - (self.stack_rating_kW * 1000) / N_cells - - # V_cell_max = 3.0 #Volts - # V_cell_I_density_max = 2.50 #mA/cm2 - E_rev = 1.23 # (in Volts) Reversible potential at 25degC - T_C = 80 # Celsius - T_K = T_C + 273.15 # in Kelvins - # E_cell == Open Circuit Voltage - E_cell = ( - 1.5184 - - (1.5421 * (10 ** (-3)) * T_K) - + (9.523 * (10 ** (-5)) * T_K * math.log(T_K)) - + (9.84 * (10 ** (-8)) * (T_K**2)) - ) - # V_act = V_act_c + V_Act_a - R = 8.314 # Ideal Gas Constant (J/mol/K) - i = self.output_dict["stack_current_density_A_cm2"] - F = 96485 # Faraday's Constant (C/mol) - - # Following coefficient values obtained from Yigit and Selamet (2016) - - # https://www.sciencedirect.com/science/article/pii/S0360319916318341?via%3Dihub - a_a = 2 # Anode charge transfer coefficient - a_c = 0.5 # Cathode charge transfer coefficient - i_o_a = 2 * (10 ** (-7)) - i_o_c = 2 * (10 ** (-3)) - V_act = (((R * T_K) / (a_a * F)) * np.arcsinh(i / (2 * i_o_a))) + ( - ((R * T_K) / (a_c * F)) * np.arcsinh(i / (2 * i_o_c)) - ) - lambda_water_content = ((-2.89556 + (0.016 * T_K)) + 1.625) / 0.1875 - delta = 0.0003 # membrane thickness (cm) - assuming a 3-µm thick membrane - sigma = ((0.005139 * lambda_water_content) - 0.00326) * math.exp( - 1268 * ((1 / 303) - (1 / T_K)) - ) # Material thickness # material conductivity - R_cell = delta / sigma - V_cell = E_cell + V_act + (i * R_cell) - V_cell = np.where(V_cell < E_rev, E_rev, V_cell) - N_cells * V_cell # Stack operational voltage - - def dynamic_operation(self): - """ - Model the electrolyzer's realistic response/operation under variable RE - - TODO: add this capability to the model - """ - # When electrolyzer is already at or near its optimal operation - # temperature (~80degC) - - def water_electrolysis_efficiency(self): - """ - https://www.sciencedirect.com/science/article/pii/S2589299119300035#b0500 - - According to the first law of thermodynamics energy is conserved. - Thus, the conversion efficiency calculated from the yields of - converted electrical energy into chemical energy. Typically, - water electrolysis efficiency is calculated by the higher heating - value (HHV) of hydrogen. Since the electrolysis process water is - supplied to the cell in liquid phase efficiency can be calculated by: - - n_T = V_TN / V_cell - - where, V_TN is the thermo-neutral voltage (min. required V to - electrolyze water) - - Parameters - ______________ - - Returns - ______________ - - """ - - n_T = self.V_TN / (self.stack_input_voltage_DC / self.N_cells) - return n_T - - def faradaic_efficiency(self): - """ - Text background from: - [https://www.researchgate.net/publication/344260178_Faraday%27s_ - Efficiency_Modeling_of_a_Proton_Exchange_Membrane_Electrolyzer_ - Based_on_Experimental_Data] - - In electrolyzers, Faraday's efficiency is a relevant parameter to - assess the amount of hydrogen generated according to the input - energy and energy efficiency. Faraday's efficiency expresses the - faradaic losses due to the gas crossover current. The thickness - of the membrane and operating conditions (i.e., temperature, gas - pressure) may affect the Faraday's efficiency. - - Equation for n_F obtained from: - https://www.sciencedirect.com/science/article/pii/S0360319917347237#bib27 - - Parameters - ______________ - float f_1 - Coefficient - value at operating temperature of 80degC (mA2/cm4) - - float f_2 - Coefficient - value at operating temp of 80 degC (unitless) - - np_array current_input_external_Amps - 1-D array of current supplied to electrolyzer stack from external - power source - - - Returns - ______________ - - float n_F - Faradaic efficiency (unitless) - - """ - f_1 = 250 # Coefficient (mA2/cm4) - f_2 = 0.996 # Coefficient (unitless) - I_cell = self.output_dict["current_input_external_Amps"] * 1000 - - # Faraday efficiency - n_F = ( - ((I_cell / self.cell_active_area) ** 2) - / (f_1 + ((I_cell / self.cell_active_area) ** 2)) - ) * f_2 - - return n_F - - def compression_efficiency(self): - """ - In industrial contexts, the remaining hydrogen should be stored at - certain storage pressures that vary depending on the intended - application. In the case of subsequent compression, pressure-volume - work, Wc, must be performed. The additional pressure-volume work can - be related to the heating value of storable hydrogen. Then, the total - efficiency reduces by the following factor: - https://www.mdpi.com/1996-1073/13/3/612/htm - - Due to reasons of material properties and operating costs, large - amounts of gaseous hydrogen are usually not stored at pressures - exceeding 100 bar in aboveground vessels and 200 bar in underground - storages - https://www.sciencedirect.com/science/article/pii/S0360319919310195 - - Partial pressure of H2(g) calculated using: - The hydrogen partial pressure is calculated as a difference between - the cathode pressure, 101,325 Pa, and the water saturation - pressure - [Source: Energies2018,11,3273; doi:10.3390/en11123273] - - """ - n_limC = 0.825 # Limited efficiency of gas compressors (unitless) - H_LHV = 241 # Lower heating value of H2 (kJ/mol) - K = 1.4 # Average heat capacity ratio (unitless) - C_c = 2.75 # Compression factor (ratio of pressure after and before compression) - n_F = self.faradaic_efficiency() - j = self.output_dict["stack_current_density_A_cm2"] - n_x = ((1 - n_F) * j) * self.cell_active_area - n_h2 = j * self.cell_active_area - Z = 1 # [Assumption] Average compressibility factor (unitless) - T_in_C = 80 # Assuming electrolyzer operates at 80degC - T_in = 273.15 + T_in_C # (Kelvins) Assuming electrolyzer operates at 80degC - W_1_C = ( - (K / (K - 1)) - * ((n_h2 - n_x) / self.F) - * self.R - * T_in - * Z - * ((C_c ** ((K - 1) / K)) - 1) - ) # Single stage compression - - # Calculate partial pressure of H2 at the cathode: - A = 8.07131 - B = 1730.63 - C = 233.426 - p_h2o_sat = 10 ** (A - (B / (C + T_in_C))) # Pa - p_cat = 101325 # Cathode pressure (Pa) - p_h2_cat = p_cat - p_h2o_sat - p_s_h2_Pa = self.p_s_h2_bar * 1e5 - - s_C = math.log10(p_s_h2_Pa / p_h2_cat) / math.log10(C_c) - W_C = round(s_C) * W_1_C # Pressure-Volume work - energy reqd. for compression - net_energy_carrier = n_h2 - n_x # C/s - net_energy_carrier = np.where((n_h2 - n_x) == 0, 1, net_energy_carrier) - n_C = 1 - ((W_C / (((net_energy_carrier) / self.F) * H_LHV * 1000)) * (1 / n_limC)) - n_C = np.where((n_h2 - n_x) == 0, 0, n_C) - return n_C - - def total_efficiency(self): - """ - Aside from efficiencies accounted for in this model - (water_electrolysis_efficiency, faradaic_efficiency, and - compression_efficiency) all process steps such as gas drying above - 2 bar or water pumping can be assumed as negligible. Ultimately, the - total efficiency or system efficiency of a PEM electrolysis system is: - - n_T = n_p_h2 * n_F_h2 * n_c_h2 - https://www.mdpi.com/1996-1073/13/3/612/htm - """ - n_p_h2 = self.water_electrolysis_efficiency() - n_F_h2 = self.faradaic_efficiency() - n_c_h2 = self.compression_efficiency() - - n_T = n_p_h2 * n_F_h2 * n_c_h2 - self.output_dict["total_efficiency"] = n_T - return n_T - - def h2_production_rate(self): - """ - H2 production rate calculated using Faraday's Law of Electrolysis - (https://www.sciencedirect.com/science/article/pii/S0360319917347237#bib27) - - Parameters - _____________ - - float f_1 - Coefficient - value at operating temperature of 80degC (mA2/cm4) - - float f_2 - Coefficient - value at operating temp of 80 degC (unitless) - - np_array - 1-D array of current supplied to electrolyzer stack from external - power source - - - Returns - _____________ - - """ - # Single stack calculations: - n_Tot = self.total_efficiency() - h2_production_rate = n_Tot * ( - (self.N_cells * self.output_dict["current_input_external_Amps"]) / (2 * self.F) - ) # mol/s - h2_production_rate_g_s = h2_production_rate / self.moles_per_g_h2 - h2_produced_kg_hr = ( - h2_production_rate_g_s * 3.6 * 72.55 / 55.5 - ) ## TEMPORARY CORRECTION APPLIED FOR PEM EFFICIENCY to reach expected 55.5kwh/kg value - - self.output_dict["stack_h2_produced_kg_hr"] = h2_produced_kg_hr - - # Total electrolyzer system calculations: - h2_produced_kg_hr_system = self.system_design() * h2_produced_kg_hr - # h2_produced_kg_hr_system = h2_produced_kg_hr - self.output_dict["h2_produced_kg_hr_system"] = h2_produced_kg_hr_system - - return h2_produced_kg_hr_system - - def degradation(self): - """ - TODO - Add a time component to the model - for degradation -> - https://www.hydrogen.energy.gov/pdfs/progress17/ii_b_1_peters_2017.pdf - """ - pass - - def water_supply(self): - """ - Calculate water supply rate based system efficiency and H2 production - rate - TODO: Add this capability to the model - """ - water_used_kg_hr_system = self.h2_production_rate() * 10 - self.output_dict["water_used_kg_hr"] = water_used_kg_hr_system - self.output_dict["water_used_kg_annual"] = np.sum(water_used_kg_hr_system) - - def h2_storage(self): - """ - Model to estimate Ideal Isorthermal H2 compression at 70degC - https://www.sciencedirect.com/science/article/pii/S036031991733954X - - The amount of hydrogen gas stored under pressure can be estimated - using the van der Waals equation - - p = [(nRT)/(V-nb)] - [a * ((n^2) / (V^2))] - - where p is pressure of the hydrogen gas (Pa), n the amount of - substance (mol), T the temperature (K), and V the volume of storage - (m3). The constants a and b are called the van der Waals coefficients, - which for hydrogen are 2.45 x 10^2 Pa m6mol-2 and 26.61 x 10^6 , - respectively. - """ - - pass - - -if __name__ == "__main__": - # Example on how to use this model: - in_dict = {} - in_dict["electrolyzer_system_size_MW"] = 15 - out_dict = {} - - electricity_profile = pd.read_csv("sample_wind_electricity_profile.csv") - in_dict["P_input_external_kW"] = electricity_profile.iloc[:, 1].to_numpy() - - el = PEM_electrolyzer_LT(in_dict, out_dict) - el.h2_production_rate() - print( - "Hourly H2 production by stack (kg/hr): ", - out_dict["stack_h2_produced_kg_hr"][0:50], - ) - print( - "Hourly H2 production by system (kg/hr): ", - out_dict["h2_produced_kg_hr_system"][0:50], - ) - fig, axs = plt.subplots(2, 2) - fig.suptitle( - "PEM H2 Electrolysis Results for " - + str(out_dict["electrolyzer_system_size_MW"]) - + " MW System" - ) - - axs[0, 0].plot(out_dict["stack_h2_produced_kg_hr"]) - axs[0, 0].set_title("Hourly H2 production by stack") - axs[0, 0].set_ylabel("kg_h2 / hr") - axs[0, 0].set_xlabel("Hour") - - axs[0, 1].plot(out_dict["h2_produced_kg_hr_system"]) - axs[0, 1].set_title("Hourly H2 production by system") - axs[0, 1].set_ylabel("kg_h2 / hr") - axs[0, 1].set_xlabel("Hour") - - axs[1, 0].plot(in_dict["P_input_external_kW"]) - axs[1, 0].set_title("Hourly Energy Supplied by Wind Farm (kWh)") - axs[1, 0].set_ylabel("kWh") - axs[1, 0].set_xlabel("Hour") - - total_efficiency = out_dict["total_efficiency"] - system_h2_eff = (1 / total_efficiency) * 33.3 - system_h2_eff = np.where(total_efficiency == 0, 0, system_h2_eff) - - axs[1, 1].plot(system_h2_eff) - axs[1, 1].set_title("Total Stack Energy Usage per mass net H2") - axs[1, 1].set_ylabel("kWh_e/kg_h2") - axs[1, 1].set_xlabel("Hour") - - plt.show() - print("Annual H2 production (kg): ", np.sum(out_dict["h2_produced_kg_hr_system"])) - print("Annual energy production (kWh): ", np.sum(in_dict["P_input_external_kW"])) - print( - "H2 generated (kg) per kWH of energy generated by wind farm: ", - np.sum(out_dict["h2_produced_kg_hr_system"]) / np.sum(in_dict["P_input_external_kW"]), - ) diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_costs_Singlitico_model.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_costs_Singlitico_model.py deleted file mode 100644 index 48077ef85..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_costs_Singlitico_model.py +++ /dev/null @@ -1,201 +0,0 @@ -""" -Author: Christopher Bay -Date: 01/24/2023 -Institution: National Renewable Energy Laboratory -Description: This file implements electrolzyer CapEx and OpEx models from [1]. The exact extent of - what is included in the costs is unclear in [1]. Source [2] (cited by [1]) states that - "equipment costs include the electrolyser system, the filling centre or compressor skids and - storage systems". -Sources: - - [1] Singlitico, Alessandro, Jacob Østergaard, and Spyros Chatzivasileiadis. "Onshore, offshore - or in-turbine electrolysis? Techno-economic overview of alternative integration designs for - green hydrogen production into Offshore Wind Power Hubs." Renewable and Sustainable Energy - Transition 1 (2021): 100005. - - [2] [E. Tractebel , H. Engie , Study on early business cases for h2 in energy storage and more - broadly power to h2 applications, EU Comm, 2017, p. 228 .] - https://hsweb.hs.uni-hamburg.de/projects/star-formation/hydrogen/P2H_Full_Study_FCHJU.pdf -""" - -from __future__ import annotations - - -class PEMCostsSingliticoModel: - def __init__( - self, - elec_location: int, - ): - """ - Initialize object for PEM costs based on [1]. - - Args: - elec_location (int): Parameter for indicating the electrolyzer location; - 0 is for onshore, 1 is for offshore or in-turbine. - """ - # Values for CapEX & OpEx taken from [1], Table B.2, PEMEL. - # Installation costs include land, contingency, contractors, legal fees, construction, - # engineering, yard improvements, buildings, electrics, piping, instrumentation, - # and installation and grid connection. - self.IF = 0.33 # installation fraction [% RC_elec] - self.RP_elec = 10 # reference power [MW] - - # Values for OpEx taken from [1], Table B.3, PEMEL. - self.RP_SR = 5 # reference power [MW] - self.RU_SR = 0.41 # reference cost share [%], for a reference power, RP_SR, of 5MW - self.P_stack_max_bar = 2 # average max size [MW] - self.SF_SR_0 = 0.11 # average scale factor - - # NOTE: 1 for offshore or in-turbine electrolyzer location, 0 for onshore; from [1], - self.OS = elec_location - # Table B.1 notes for CapEx_el - - # NOTE: This is used in the stack replacement cost code that is currently commented out; - # more work needs to be done to make sure this is set and used correctly. - # self.P_elec_bar = 1 * 10**3 # scaled max [MW] from [1], Table B.1 notes forOpEx_elec_eq - - # NOTE: This is used in the stack replacement cost code that is currently commented out. - # self.OH_max = 85000 # Lifetime maximum operating hours [h], taken from [1], Table 1, PEMEL - - def run( - self, - P_elec: float, - RC_elec: float, - ) -> tuple: - """ - Computes the CapEx and OpEx costs for a single electrolyzer. - - Args: - P_elec (float): Nominal capacity of the electrolyzer [GW]. - RC_elec (float): Reference cost of the electrolyzer [MUSD/GW] for a 10 MW electrolyzer - plant installed. - - Returns: - tuple: CapEx and OpEx costs for a single electrolyzer. - """ - capex = self.calc_capex(P_elec, RC_elec) - - opex = self.calc_opex(P_elec, capex) - - return capex, opex - - def calc_capex( - self, - P_elec: float, - RC_elec: float, - ) -> float: - """ - CapEx for a single electrolyzer, given the electrolyzer capacity and reference cost. - Equation from [1], Table B.1, CapEx_EL. For in-turbine electrolyzers, - it is assumed that the maximum electrolyzer size is equal to the turbine rated capacity. - - NOTE: If the single electrolyzer capacity exceeds 100MW, the CapEx becomes fixed at the cost - of a 100MW system, due to decreasing economies of scale (based on assumption from [1]). As - such, if you use the output to calculate a cost per unit of electrolyzer, you will need to - divide the cost by 100MW and not the user-specified size of the electrolyzer for sizes above - 100MW. - - Args: - P_elec (float): Nominal capacity of the electrolyzer [GW]. - RC_elec (float): Reference cost of the electrolyzer [MUSD/GW]. - - Returns: - float: CapEx for electrolyzer [MUSD]. - """ - # Choose the scale factor based on electrolyzer size, [1], Table B.2. - if P_elec < 10 / 10**3: - self.SF_elec = -0.21 # scale factor, -0.21 for <10MW, -0.14 for >10MW - else: - self.SF_elec = -0.14 # scale factor, -0.21 for <10MW, -0.14 for >10MW - - # If electrolyzer capacity is >100MW, fix unit cost to 100MW electrolyzer as economies of - # scale stop at sizes above this, according to assumption in [1]. - if P_elec > 100 / 10**3: - P_elec_cost_per_unit_calc = 0.1 - else: - P_elec_cost_per_unit_calc = P_elec - - # Return the cost of a single electrolyzer of the specified capacity in millions of USD (or - # the supplied currency). - # MUSD = GW * MUSD/GW * - * GW * MW/GW / MW ** - - cost = ( - P_elec_cost_per_unit_calc - * RC_elec - * (1 + self.IF * self.OS) - * ((P_elec_cost_per_unit_calc * 10**3 / self.RP_elec) ** self.SF_elec) - ) - cost_per_unit = cost / P_elec_cost_per_unit_calc - - return cost_per_unit * P_elec - - def calc_opex( - self, - P_elec: float, - capex_elec: float, - RC_elec: float | None = None, - OH: float | None = None, - ) -> float: - """ - OpEx for a single electrolyzer, given the electrolyzer capacity and reference cost. - Equations from [1], Table B.1, OpEx_elec_eq and OpEx_elec_neq. - The returned OpEx cost include equipment and non-equipment costs, but excludes the stack - replacement cost. - - NOTE: If the single electrolyzer capacity exceeds 100MW, the OpEx becomes fixed at the cost - of a 100MW system, due to decreasing economies of scale (based on assumption from [1]). - As such, if you use the output to calculate a cost per unit of electrolyzer, you will need - to divide the cost by 100MW and not the user-specified size of the electrolyzer for sizes - above 100 MW. - - NOTE: Code for the stack replacement cost is included below, but does not currently match - results from [1]. DO NOT USE in the current form. - - Args: - P_elec (float): Nominal capacity of the electrolyzer [GW]. - capex_elec (float): CapEx for electrolyzer [MUSD]. - RC_elec (float, optional): Reference cost of the electrolyzer [MUSD/GW]. Defaults to - None. Not currently used. - OH (float, optional): Operating hours [h]. Defaults to None. Not currently used. - - Returns: - float: OpEx for electrolyzer [MUSD]. - """ - # If electrolyzer capacity is >100MW, fix unit cost to 100MW electrolyzer as economies of - # scale stop at sizes above this, according to assumption in [1]. - if P_elec > 100 / 10**3: - P_elec = 0.1 - - # Including material cost for planned and unplanned maintenance, labor cost in central - # Europe, which all depend on a system scale. Excluding the cost of electricity and the - # stack replacement, calculated separately. Scaled maximum to P_elec_bar = 1 GW. - # MUSD*MW MUSD * - * - * GW * MW/GW - opex_elec_eq = ( - capex_elec * (1 - self.IF * (1 + self.OS)) * 0.0344 * (P_elec * 10**3) ** -0.155 - ) - - # Covers the other operational expenditure related to the facility level. This includes site - # management, land rent and taxes, administrative fees (insurance, legal fees...), and site - # maintenance. - # MUSD MUSD - opex_elec_neq = 0.04 * capex_elec * self.IF * (1 + self.OS) - - # NOTE: The stack replacement costs below don't match the results in [1] supplementary - # materials. - # ***DO NOT USE*** stack replacement cost in its current form. - - # Choose the scale factor based on electrolyzer size, [1], Table B.2. - # if P_elec < 10 / 10**3: - # self.SF_elec = -0.21 # scale factor, -0.21 for <10MW, -0.14 for >10MW - # else: - # self.SF_elec = -0.14 # scale factor, -0.21 for <10MW, -0.14 for >10MW - - # Approximation of stack costs and replacement cost depending on the electrolyzer equipment - # costs. - # Paid only the year in which the replacement is needed. - # MUSD/GW % * MUSD/GW * - * MW / MW ** - - # RC_SR = self.RU_SR * RC_elec * (1 - self.IF) * (self.RP_SR / self.RP_elec) ** self.SF_elec - # # - - * MW / MW - # SF_SR = 1 - (1 - self.SF_SR_0) * np.exp(-self.P_elec_bar / self.P_stack_max_bar) - # # SF_SR = 1 - (1 - self.SF_SR_0) * np.exp(-P_elec * 10**3 / self.P_stack_max_bar) - # # MUSD GW * MUSD/GW * GW * MW/GW MW ** - * h / h - # opex_elec_sr = P_elec * RC_SR * (P_elec * 10**3 / self.RP_SR) ** SF_SR * OH / self.OH_max - - return opex_elec_eq + opex_elec_neq diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_costs_custom.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_costs_custom.py deleted file mode 100644 index eb02807a0..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_costs_custom.py +++ /dev/null @@ -1,24 +0,0 @@ -def calc_custom_electrolysis_capex_fom(electrolyzer_capacity_kW, electrolyzer_config): - """Calculates electrolyzer total installed capex and fixed O&M based on user-input values. - - Only used if h2integrate_config["electrolyzer"]["cost_model"] is set to "basic_custom" - Requires additional inputs in h2integrate_config["electrolyzer"]: - - fixed_om_per_kw: electrolyzer fixed o&m in $/kW-year - - electrolyzer_capex: electrolyzer capex in $/kW - - Args: - electrolyzer_capacity_kW (float or int): electrolyzer capacity in kW - electrolyzer_config (dict): ``h2integrate_config["electrolyzer"]`` - - Returns: - 2-element tuple containing - - - **capex** (float): electrolyzer overnight capex in $ - - **fixed_om** (float): electrolyzer fixed O&M in $/year - """ - electrolyzer_capex = electrolyzer_config["electrolyzer_capex"] * electrolyzer_capacity_kW - if "fixed_om_per_kw" in electrolyzer_config.keys(): - electrolyzer_fopex = electrolyzer_config["fixed_om_per_kw"] * electrolyzer_capacity_kW - else: - electrolyzer_fopex = 0.0 - return electrolyzer_capex, electrolyzer_fopex diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_electrolyzer_IVcurve.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_electrolyzer_IVcurve.py deleted file mode 100644 index 79d575a6e..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_electrolyzer_IVcurve.py +++ /dev/null @@ -1,657 +0,0 @@ -## Low-Temperature PEM Electrolyzer Model -""" -Python model of H2 PEM low-temp electrolyzer. - -Quick Hydrogen Physics: - -1 kg H2 <-> 11.1 N-m3 <-> 33.3 kWh (LHV) <-> 39.4 kWh (HHV) - -High mass energy density (1 kg H2= 3,77 l gasoline) -Low volumetric density (1 Nm³ H2= 0,34 l gasoline - -Hydrogen production from water electrolysis (~5 kWh/Nm³ H2) - -Power:1 MW electrolyser <-> 200 Nm³/h H2 <-> ±18 kg/h H2 -Energy:+/-55 kWh of electricity --> 1 kg H2 <-> 11.1 Nm³ <-> ±10 liters -demineralized water - -Power production from a hydrogen PEM fuel cell from hydrogen (+/-50% -efficiency): -Energy: 1 kg H2 --> 16 kWh -""" - -import sys -import math - -import numpy as np -import scipy -import pandas as pd -from matplotlib import pyplot as plt - - -np.set_printoptions(threshold=sys.maxsize) - - -def calc_current( - P_T, p1, p2, p3, p4, p5, p6 -): # calculates i-v curve coefficients given the stack power and stack temp - pwr, tempc = P_T - i_stack = ( - p1 * (pwr**2) + p2 * (tempc**2) + (p3 * pwr * tempc) + (p4 * pwr) + (p5 * tempc) + (p6) - ) - return i_stack - - -class PEM_electrolyzer_LT: - """ - Create an instance of a low-temperature PEM Electrolyzer System. Each - stack in the electrolyzer system in this model is rated at 1 MW_DC. - - Parameters - _____________ - np_array P_input_external_kW - 1-D array of time-series external power supply - - string voltage_type - Nature of voltage supplied to electrolyzer from the external power - supply ['variable' or 'constant] - - float power_supply_rating_MW - Rated power of external power supply - - Returns - _____________ - - """ - - def __init__(self, input_dict, output_dict): - self.input_dict = input_dict - self.output_dict = output_dict - - # array of input power signal - self.input_dict["P_input_external_kW"] = input_dict["P_input_external_kW"] - self.electrolyzer_system_size_MW = input_dict["electrolyzer_system_size_MW"] - - # self.input_dict['voltage_type'] = 'variable' # not yet implemented - self.input_dict["voltage_type"] = "constant" - self.stack_input_voltage_DC = 250 - - # Assumptions: - self.min_V_cell = 1.62 # Only used in variable voltage scenario - self.p_s_h2_bar = 31 # H2 outlet pressure - - # any current below this amount (10% rated) will saturate the H2 production to zero, used to - # be 500 (12.5% of rated) - self.stack_input_current_lower_bound = 400 # [A] - self.stack_rating_kW = 1000 # 1 MW - self.cell_active_area = 1250 # [cm^2] - self.N_cells = 130 - - # PEM electrolyzers have a max current density of approx 2 A/cm^2 so max is 2*cell_area - self.max_cell_current = 2 * self.cell_active_area - - # Constants: - self.moles_per_g_h2 = 0.49606 # [1/weight_h2] - self.V_TN = 1.48 # Thermo-neutral Voltage (Volts) in standard conditions - self.F = 96485.34 # Faraday's Constant (C/mol) or [As/mol] - self.R = 8.314 # Ideal Gas Constant (J/mol/K) - - # Additional Constants - self.T_C = 80 # stack temperature in [C] - self.mmHg_2_Pa = 133.322 # convert between mmHg to Pa - self.patmo = 101325 # atmospheric pressure [Pa] - self.mmHg_2_atm = self.mmHg_2_Pa / self.patmo # convert from mmHg to atm - - self.curve_coeff = self.iv_curve() # this initializes the I-V curve to calculate current - self.external_power_supply() - - def external_power_supply(self): - """ - External power source (grid or REG) which will need to be stepped - down and converted to DC power for the electrolyzer. - - Please note, for a wind farm as the electrolyzer's power source, - the model assumes variable power supplied to the stack at fixed - voltage (fixed voltage, variable power and current) - - TODO: extend model to accept variable voltage, current, and power - This will replicate direct DC-coupled PV system operating at MPP - """ - power_converter_efficiency = ( - 1.0 # this used to be 0.95 but feel free to change as you'd like - ) - if self.input_dict["voltage_type"] == "constant": - self.input_dict["P_input_external_kW"] = np.where( - self.input_dict["P_input_external_kW"] > (self.electrolyzer_system_size_MW * 1000), - (self.electrolyzer_system_size_MW * 1000), - self.input_dict["P_input_external_kW"], - ) - - self.output_dict["curtailed_P_kW"] = np.where( - self.input_dict["P_input_external_kW"] > (self.electrolyzer_system_size_MW * 1000), - ( - self.input_dict["P_input_external_kW"] - - (self.electrolyzer_system_size_MW * 1000) - ), - 0, - ) - - # Current used to be calculated as Power/Voltage but now it uses the IV curve - # self.output_dict['current_input_external_Amps'] = \ - # (self.input_dict['P_input_external_kW'] * 1000 * - # power_converter_efficiency) / (self.stack_input_voltage_DC * - # self.system_design()) - - self.output_dict["current_input_external_Amps"] = calc_current( - ( - ( - (self.input_dict["P_input_external_kW"] * power_converter_efficiency) - / self.system_design() - ), - self.T_C, - ), - *self.curve_coeff, - ) - - self.output_dict["stack_current_density_A_cm2"] = ( - self.output_dict["current_input_external_Amps"] / self.cell_active_area - ) - - self.output_dict["current_input_external_Amps"] = np.where( - self.output_dict["current_input_external_Amps"] - < self.stack_input_current_lower_bound, - 0, - self.output_dict["current_input_external_Amps"], - ) - - else: - pass # TODO: extend model to variable voltage and current source - - def iv_curve(self): - """ - This is a new function that creates the I-V curve to calculate current based - on input power and electrolyzer temperature - - current range is 0: max_cell_current+10 -> PEM have current density approx = 2 A/cm^2 - - temperature range is 40 degC : rated_temp+5 -> temperatures for PEM are usually within - 60-80degC - - calls cell_design() which calculates the cell voltage - """ - current_range = np.arange(0, self.max_cell_current + 10, 10) - temp_range = np.arange(40, self.T_C + 5, 5) - idx = 0 - powers = np.zeros(len(current_range) * len(temp_range)) - currents = np.zeros(len(current_range) * len(temp_range)) - temps_C = np.zeros(len(current_range) * len(temp_range)) - for i in range(len(current_range)): - for t in range(len(temp_range)): - powers[idx] = ( - current_range[i] - * self.cell_design(temp_range[t], current_range[i]) - * self.N_cells - * (1e-3) - ) # stack power - currents[idx] = current_range[i] - temps_C[idx] = temp_range[t] - idx = idx + 1 - - curve_coeff, curve_cov = scipy.optimize.curve_fit( - calc_current, (powers, temps_C), currents, p0=(1.0, 1.0, 1.0, 1.0, 1.0, 1.0) - ) # updates IV curve coeff - return curve_coeff - - def system_design(self): - """ - For now, system design is solely a function of max. external power - supply; i.e., a rated power supply of 50 MW means that the electrolyzer - system developed by this model is also rated at 50 MW - - TODO: Extend model to include this capability. - Assume that a PEM electrolyzer behaves as a purely resistive load - in a circuit, and design the configuration of the entire electrolyzer - system - which may consist of multiple stacks connected together in - series, parallel, or a combination of both. - """ - h2_production_multiplier = (self.electrolyzer_system_size_MW * 1000) / self.stack_rating_kW - self.output_dict["electrolyzer_system_size_MW"] = self.electrolyzer_system_size_MW - return h2_production_multiplier - - def cell_design(self, Stack_T, Stack_Current): - """ - - Please note that this method is currently not used in the model. It - will be used once the electrolyzer model is expanded to variable - voltage supply as well as implementation of the self.system_design() - method - - Motivation: - - The most common representation of the electrolyzer performance is the - polarization curve that represents the relation between the current density - and the voltage (V): - Source: https://www.sciencedirect.com/science/article/pii/S0959652620312312 - - V = N_c(E_cell + V_Act,c + V_Act,a + iR_cell) - - where N_c is the number of electrolyzer cells,E_cell is the open circuit - voltage VAct,and V_Act,c are the anode and cathode activation over-potentials, - i is the current density and iRcell is the electrolyzer cell resistance - (ohmic losses). - - Use this to make a V vs. A (Amperes/cm2) graph which starts at 1.23V because - thermodynamic reaction of water formation/splitting dictates that standard - electrode potential has a ∆G of 237 kJ/mol (where: ∆H = ∆G + T∆S) - - 10/31/2022 - ESG: https://www.sciencedirect.com/science/article/pii/S0360319906000693 - -> calculates cell voltage to make IV curve (called by iv_curve) - Another good source for the equations used in this function: - https://www.sciencedirect.com/science/article/pii/S0360319918309017 - - """ - - # Cell level inputs: - - E_rev0 = ( - 1.229 # (in Volts) Reversible potential at 25degC - Nerst Equation (see Note below) - ) - # E_th = 1.48 # (in Volts) Thermoneutral potential at 25degC - No longer used - - T_K = Stack_T + 273.15 # in Kelvins - # E_cell == Open Circuit Voltage - used to be a static variable, now calculated - # NOTE: E_rev is unused right now, E_rev0 is the general nerst equation for operating at 25 - # deg C at atmospheric pressure (whereas we will be operating at higher temps). From the - # literature above, it appears that E_rev0 is more correct - # https://www.sciencedirect.com/science/article/pii/S0360319911021380 - ( - 1.5184 - - (1.5421 * (10 ** (-3)) * T_K) - + (9.523 * (10 ** (-5)) * T_K * math.log(T_K)) - + (9.84 * (10 ** (-8)) * (T_K**2)) - ) - - # Calculate partial pressure of H2 at the cathode: - # Uses Antoine formula (see link below) - # p_h2o_sat calculation taken from compression efficiency calculation - # https://www.omnicalculator.com/chemistry/vapour-pressure-of-water#antoine-equation - A = 8.07131 - B = 1730.63 - C = 233.426 - - p_h2o_sat_mmHg = 10 ** ( - A - (B / (C + Stack_T)) - ) # vapor pressure of water in [mmHg] using Antoine formula - p_h20_sat_atm = p_h2o_sat_mmHg * self.mmHg_2_atm # convert mmHg to atm - - # could also use Arden-Buck equation (see below). Arden Buck and Antoine equations give - # barely different pressures for the temperatures we're looking, however, the differences - # between the two become more substantial at higher temps - - # p_h20_sat_pa=((0.61121*math.exp((18.678-(Stack_T/234.5))*(Stack_T/(257.14+Stack_T))))*1e+3) #ARDEN BUCK # noqa: E501 - # p_h20_sat_atm=p_h20_sat_pa/self.patmo - - # Cell reversible voltage kind of explain in Equations (12)-(15) of below source - # https://www.sciencedirect.com/science/article/pii/S0360319906000693 - # OR see equation (8) in the source below - # https://www.sciencedirect.com/science/article/pii/S0360319917309278?via%3Dihub - E_cell = E_rev0 + ((self.R * T_K) / (2 * self.F)) * ( - np.log((1 - p_h20_sat_atm) * math.sqrt(1 - p_h20_sat_atm)) - ) # 1 value is atmoshperic pressure in atm - i = Stack_Current / self.cell_active_area # i is cell current density - - # Following coefficient values obtained from Yigit and Selamet (2016) - - # https://www.sciencedirect.com/science/article/pii/S0360319916318341?via%3Dihub - a_a = 2 # Anode charge transfer coefficient - a_c = 0.5 # Cathode charge transfer coefficient - i_o_a = 2 * (10 ** (-7)) # anode exchange current density - i_o_c = 2 * (10 ** (-3)) # cathode exchange current density - - # below is the activation energy for anode and cathode - see - # https://www.sciencedirect.com/science/article/pii/S0360319911021380 - V_act = (((self.R * T_K) / (a_a * self.F)) * np.arcsinh(i / (2 * i_o_a))) + ( - ((self.R * T_K) / (a_c * self.F)) * np.arcsinh(i / (2 * i_o_c)) - ) - - # equation 13 and 12 for lambda_water_content and sigma: - # from https://www.sciencedirect.com/science/article/pii/S0360319917309278?via%3Dihub - lambda_water_content = ((-2.89556 + (0.016 * T_K)) + 1.625) / 0.1875 - - # reasonable membrane thickness of 180-µm NOTE: this will likely decrease in the future - delta = 0.018 # [cm] - - sigma = ((0.005139 * lambda_water_content) - 0.00326) * math.exp( - 1268 * ((1 / 303) - (1 / T_K)) - ) # membrane proton conductivity [S/cm] - - R_cell = delta / sigma # ionic resistance [ohms] - R_elec = 3.5 * ( - 10 ** (-5) - ) # [ohms] from Table 1 in https://journals.utm.my/jurnalteknologi/article/view/5213/3557 - V_cell = E_cell + V_act + (i * (R_cell + R_elec)) # cell voltage [V] - # NOTE: R_elec is to account for the electronic resistance measured between stack terminals - # in open-circuit conditions - # Supposedly, removing it shouldn't lead to large errors - # calculation for it: http://www.electrochemsci.org/papers/vol7/7043314.pdf - - # V_stack = self.N_cells * V_cell # Stack operational voltage -> this is combined in iv_calc for power rather than here # noqa: E501 - - return V_cell - - def dynamic_operation(self): # UNUSED - """ - Model the electrolyzer's realistic response/operation under variable RE - - TODO: add this capability to the model - """ - # When electrolyzer is already at or near its optimal operation - # temperature (~80degC) - - def water_electrolysis_efficiency(self): # UNUSED - """ - https://www.sciencedirect.com/science/article/pii/S2589299119300035#b0500 - - According to the first law of thermodynamics energy is conserved. - Thus, the conversion efficiency calculated from the yields of - converted electrical energy into chemical energy. Typically, - water electrolysis efficiency is calculated by the higher heating - value (HHV) of hydrogen. Since the electrolysis process water is - supplied to the cell in liquid phase efficiency can be calculated by: - - n_T = V_TN / V_cell - - where, V_TN is the thermo-neutral voltage (min. required V to - electrolyze water) - - Parameters - ______________ - - Returns - ______________ - - """ - # From the source listed in this function ... - # n_T=V_TN/V_cell NOT what's below which is input voltage -> this should call cell_design() - n_T = self.V_TN / (self.stack_input_voltage_DC / self.N_cells) - return n_T - - def faradaic_efficiency(self): # ONLY EFFICIENCY CONSIDERED RIGHT NOW - """ - Text background from: - [https://www.researchgate.net/publication/344260178_Faraday%27s_ - Efficiency_Modeling_of_a_Proton_Exchange_Membrane_Electrolyzer_ - Based_on_Experimental_Data] - - In electrolyzers, Faraday's efficiency is a relevant parameter to - assess the amount of hydrogen generated according to the input - energy and energy efficiency. Faraday's efficiency expresses the - faradaic losses due to the gas crossover current. The thickness - of the membrane and operating conditions (i.e., temperature, gas - pressure) may affect the Faraday's efficiency. - - Equation for n_F obtained from: - https://www.sciencedirect.com/science/article/pii/S0360319917347237#bib27 - - Parameters - ______________ - float f_1 - Coefficient - value at operating temperature of 80degC (mA2/cm4) - - float f_2 - Coefficient - value at operating temp of 80 degC (unitless) - - np_array current_input_external_Amps - 1-D array of current supplied to electrolyzer stack from external - power source - - - Returns - ______________ - - float n_F - Faradaic efficiency (unitless) - - """ - f_1 = 250 # Coefficient (mA2/cm4) - f_2 = 0.996 # Coefficient (unitless) - I_cell = self.output_dict["current_input_external_Amps"] * 1000 - - # Faraday efficiency - n_F = ( - ((I_cell / self.cell_active_area) ** 2) - / (f_1 + ((I_cell / self.cell_active_area) ** 2)) - ) * f_2 - - return n_F - - def compression_efficiency(self): # UNUSED AND MAY HAVE ISSUES - # Should this only be used if we plan on storing H2? - """ - In industrial contexts, the remaining hydrogen should be stored at - certain storage pressures that vary depending on the intended - application. In the case of subsequent compression, pressure-volume - work, Wc, must be performed. The additional pressure-volume work can - be related to the heating value of storable hydrogen. Then, the total - efficiency reduces by the following factor: - https://www.mdpi.com/1996-1073/13/3/612/htm - - Due to reasons of material properties and operating costs, large - amounts of gaseous hydrogen are usually not stored at pressures - exceeding 100 bar in aboveground vessels and 200 bar in underground - storages - https://www.sciencedirect.com/science/article/pii/S0360319919310195 - - Partial pressure of H2(g) calculated using: - The hydrogen partial pressure is calculated as a difference between - the cathode pressure, 101,325 Pa, and the water saturation - pressure - [Source: Energies2018,11,3273; doi:10.3390/en11123273] - - """ - n_limC = 0.825 # Limited efficiency of gas compressors (unitless) - H_LHV = 241 # Lower heating value of H2 (kJ/mol) - K = 1.4 # Average heat capacity ratio (unitless) - C_c = 2.75 # Compression factor (ratio of pressure after and before compression) - n_F = self.faradaic_efficiency() - j = self.output_dict["stack_current_density_A_cm2"] - n_x = ((1 - n_F) * j) * self.cell_active_area - n_h2 = j * self.cell_active_area - Z = 1 # [Assumption] Average compressibility factor (unitless) - T_in = 273.15 + self.T_C # (Kelvins) Assuming electrolyzer operates at 80degC - W_1_C = ( - (K / (K - 1)) - * ((n_h2 - n_x) / self.F) - * self.R - * T_in - * Z - * ((C_c ** ((K - 1) / K)) - 1) - ) # Single stage compression - - # Calculate partial pressure of H2 at the cathode: This is the Antoine formula (see link - # below) - # https://www.omnicalculator.com/chemistry/vapour-pressure-of-water#antoine-equation - A = 8.07131 - B = 1730.63 - C = 233.426 - p_h2o_sat = 10 ** (A - (B / (C + self.T_C))) # [mmHg] - p_cat = 101325 # Cathode pressure (Pa) - # Fixed unit bug between mmHg and Pa - - p_h2_cat = p_cat - (p_h2o_sat * self.mmHg_2_Pa) # convert mmHg to Pa - p_s_h2_Pa = self.p_s_h2_bar * 1e5 - - s_C = math.log10(p_s_h2_Pa / p_h2_cat) / math.log10(C_c) - W_C = round(s_C) * W_1_C # Pressure-Volume work - energy reqd. for compression - net_energy_carrier = n_h2 - n_x # C/s - net_energy_carrier = np.where((n_h2 - n_x) == 0, 1, net_energy_carrier) - n_C = 1 - ((W_C / (((net_energy_carrier) / self.F) * H_LHV * 1000)) * (1 / n_limC)) - n_C = np.where((n_h2 - n_x) == 0, 0, n_C) - return n_C - - def total_efficiency(self): - """ - Aside from efficiencies accounted for in this model - (water_electrolysis_efficiency, faradaic_efficiency, and - compression_efficiency) all process steps such as gas drying above - 2 bar or water pumping can be assumed as negligible. Ultimately, the - total efficiency or system efficiency of a PEM electrolysis system is: - - n_T = n_p_h2 * n_F_h2 * n_c_h2 - https://www.mdpi.com/1996-1073/13/3/612/htm - """ - # n_p_h2 = self.water_electrolysis_efficiency() #no longer considered - n_F_h2 = self.faradaic_efficiency() - # n_c_h2 = self.compression_efficiency() #no longer considered - - # n_T = n_p_h2 * n_F_h2 * n_c_h2 #No longer considers these other efficiencies - n_T = n_F_h2 - self.output_dict["total_efficiency"] = n_T - return n_T - - def h2_production_rate(self): - """ - H2 production rate calculated using Faraday's Law of Electrolysis - (https://www.sciencedirect.com/science/article/pii/S0360319917347237#bib27) - - Parameters - _____________ - - float f_1 - Coefficient - value at operating temperature of 80degC (mA2/cm4) - - float f_2 - Coefficient - value at operating temp of 80 degC (unitless) - - np_array - 1-D array of current supplied to electrolyzer stack from external - power source - - - Returns - _____________ - - """ - # Single stack calculations: - n_Tot = self.total_efficiency() - h2_production_rate = n_Tot * ( - (self.N_cells * self.output_dict["current_input_external_Amps"]) / (2 * self.F) - ) # mol/s - h2_production_rate_g_s = h2_production_rate / self.moles_per_g_h2 - h2_produced_kg_hr = h2_production_rate_g_s * 3.6 # Fixed: no more manual scaling - self.output_dict["stack_h2_produced_g_s"] = h2_production_rate_g_s - self.output_dict["stack_h2_produced_kg_hr"] = h2_produced_kg_hr - - # Total electrolyzer system calculations: - h2_produced_kg_hr_system = self.system_design() * h2_produced_kg_hr - # h2_produced_kg_hr_system = h2_produced_kg_hr - self.output_dict["h2_produced_kg_hr_system"] = h2_produced_kg_hr_system - - return h2_produced_kg_hr_system, h2_production_rate_g_s - - def degradation(self): - """ - TODO - Add a time component to the model - for degradation -> - https://www.hydrogen.energy.gov/pdfs/progress17/ii_b_1_peters_2017.pdf - """ - pass - - def water_supply(self): - """ - Calculate water supply rate based system efficiency and H2 production - rate - TODO: Add this capability to the model - - The 10x multiple is likely too low. See Lampert, David J., Cai, Hao, Wang, Zhichao, - Keisman, Jennifer, Wu, May, Han, Jeongwoo, Dunn, Jennifer, Sullivan, John L., - Elgowainy, Amgad, Wang, Michael, & Keisman, Jennifer. Development of a Life Cycle Inventory - of Water Consumption Associated with the Production of Transportation Fuels. United States. - https://doi.org/10.2172/1224980 - """ - # ratio of water_used:h2_kg_produced depends on power source - # h20_kg:h2_kg with PV 22-126:1 or 18-25:1 without PV but considering water - # deminersalisation stoichometrically its just 9:1 but ... theres inefficiencies in the - # water purification process - h2_produced_kg_hr_system, h2_production_rate_g_s = self.h2_production_rate() - water_used_kg_hr_system = h2_produced_kg_hr_system * 10 - self.output_dict["water_used_kg_hr"] = water_used_kg_hr_system - self.output_dict["water_used_kg_annual"] = np.sum(water_used_kg_hr_system) - - def h2_storage(self): - """ - Model to estimate Ideal Isorthermal H2 compression at 70degC - https://www.sciencedirect.com/science/article/pii/S036031991733954X - - The amount of hydrogen gas stored under pressure can be estimated - using the van der Waals equation - - p = [(nRT)/(V-nb)] - [a * ((n^2) / (V^2))] - - where p is pressure of the hydrogen gas (Pa), n the amount of - substance (mol), T the temperature (K), and V the volume of storage - (m3). The constants a and b are called the van der Waals coefficients, - which for hydrogen are 2.45 x 10^-2 Pa m6mol^-2 and 26.61 x 10^-6 , - respectively. - """ - - pass - - -if __name__ == "__main__": - # Example on how to use this model: - in_dict = {} - in_dict["electrolyzer_system_size_MW"] = 15 - out_dict = {} - - electricity_profile = pd.read_csv("sample_wind_electricity_profile.csv") - in_dict["P_input_external_kW"] = electricity_profile.iloc[:, 1].to_numpy() - - el = PEM_electrolyzer_LT(in_dict, out_dict) - el.h2_production_rate() - print( - "Hourly H2 production by stack (kg/hr): ", - out_dict["stack_h2_produced_kg_hr"][0:50], - ) - print( - "Hourly H2 production by system (kg/hr): ", - out_dict["h2_produced_kg_hr_system"][0:50], - ) - fig, axs = plt.subplots(2, 2) - fig.suptitle( - "PEM H2 Electrolysis Results for " - + str(out_dict["electrolyzer_system_size_MW"]) - + " MW System" - ) - - axs[0, 0].plot(out_dict["stack_h2_produced_kg_hr"]) - axs[0, 0].set_title("Hourly H2 production by stack") - axs[0, 0].set_ylabel("kg_h2 / hr") - axs[0, 0].set_xlabel("Hour") - - axs[0, 1].plot(out_dict["h2_produced_kg_hr_system"]) - axs[0, 1].set_title("Hourly H2 production by system") - axs[0, 1].set_ylabel("kg_h2 / hr") - axs[0, 1].set_xlabel("Hour") - - axs[1, 0].plot(in_dict["P_input_external_kW"]) - axs[1, 0].set_title("Hourly Energy Supplied by Wind Farm (kWh)") - axs[1, 0].set_ylabel("kWh") - axs[1, 0].set_xlabel("Hour") - - total_efficiency = out_dict["total_efficiency"] - system_h2_eff = (1 / total_efficiency) * 33.3 - system_h2_eff = np.where(total_efficiency == 0, 0, system_h2_eff) - - axs[1, 1].plot(system_h2_eff) - axs[1, 1].set_title("Total Stack Energy Usage per mass net H2") - axs[1, 1].set_ylabel("kWh_e/kg_h2") - axs[1, 1].set_xlabel("Hour") - - plt.show() - print("Annual H2 production (kg): ", np.sum(out_dict["h2_produced_kg_hr_system"])) - print("Annual energy production (kWh): ", np.sum(in_dict["P_input_external_kW"])) - print( - "H2 generated (kg) per kWH of energy generated by wind farm: ", - np.sum(out_dict["h2_produced_kg_hr_system"]) / np.sum(in_dict["P_input_external_kW"]), - ) diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_tools.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_tools.py deleted file mode 100644 index efce5947f..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/PEM_tools.py +++ /dev/null @@ -1,59 +0,0 @@ -import numpy as np - -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer_Clusters import ( # noqa: E501 - PEM_H2_Clusters as PEMClusters, -) - - -def create_1MW_reference_PEM(curve_coeff=None): - pem_param_dict = { - "eol_eff_percent_loss": 10, - "uptime_hours_until_eol": 77600, - "include_degradation_penalty": True, - "turndown_ratio": 0.1, - "curve_coeff": curve_coeff, - } - pem = PEMClusters(cluster_size_mw=1, plant_life=30, **pem_param_dict) - return pem - - -def get_electrolyzer_BOL_efficiency(): - pem_1MW = create_1MW_reference_PEM() - bol_eff = pem_1MW.output_dict["BOL Efficiency Curve Info"]["Efficiency [kWh/kg]"].values[-1] - - return np.round(bol_eff, 2) - - -def size_electrolyzer_for_hydrogen_demand( - hydrogen_production_capacity_required_kgphr, - size_for="BOL", - electrolyzer_degradation_power_increase=None, -): - electrolyzer_energy_kWh_per_kg_estimate_BOL = get_electrolyzer_BOL_efficiency() - if size_for == "BOL": - electrolyzer_capacity_MW = ( - hydrogen_production_capacity_required_kgphr - * electrolyzer_energy_kWh_per_kg_estimate_BOL - / 1000 - ) - elif size_for == "EOL": - electrolyzer_energy_kWh_per_kg_estimate_EOL = ( - electrolyzer_energy_kWh_per_kg_estimate_BOL - * (1 + electrolyzer_degradation_power_increase) - ) - electrolyzer_capacity_MW = ( - hydrogen_production_capacity_required_kgphr - * electrolyzer_energy_kWh_per_kg_estimate_EOL - / 1000 - ) - - return electrolyzer_capacity_MW - - -def check_capacity_based_on_clusters(electrolyzer_capacity_BOL_MW, cluster_cap_mw): - if electrolyzer_capacity_BOL_MW % cluster_cap_mw == 0: - n_pem_clusters_max = electrolyzer_capacity_BOL_MW // cluster_cap_mw - else: - n_pem_clusters_max = int(np.ceil(np.ceil(electrolyzer_capacity_BOL_MW) / cluster_cap_mw)) - electrolyzer_size_mw = n_pem_clusters_max * cluster_cap_mw - return electrolyzer_size_mw diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/__init__.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/__init__.py deleted file mode 100644 index 1b79c00b3..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -import h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer_Clusters -import h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_tools -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_costs_Singlitico_model import ( - PEMCostsSingliticoModel, -) - -# FIXME: duplicative imports -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_electrolyzer_IVcurve import ( - PEM_electrolyzer_LT, -) -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer import ( - PEM_electrolyzer_LT, # FIXME: duplicative import, delete whole comment when fixed # noqa: F811 -) diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/optimization_utils_linear.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/optimization_utils_linear.py deleted file mode 100644 index f417834b7..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/optimization_utils_linear.py +++ /dev/null @@ -1,200 +0,0 @@ -import time -from pathlib import Path - -import numpy as np -from pyomo.environ import * # FIXME: no * imports, delete whole comment when fixed # noqa: F403 - - -def optimize( - P_wind_t, - T=50, - n_stacks=3, - c_wp=0, - c_sw=12, - rated_power=500, - dt=1, - P_init=None, - I_init=None, - T_init=None, - AC_init=0, - F_tot_init=0, -): - model = ConcreteModel() # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - # Things to solve: - C_INV = 1.47e6 - LT = 90000 - - # Initializations - if P_init is None: - P_init = [0] * (n_stacks * T) - else: - P_init = P_init.flatten() - if I_init is None: - I_init = [0] * (n_stacks * T) - else: - I_init = I_init.flatten() - if T_init is None: - T_init = [0] * (n_stacks * T) - else: - T_init = T_init.flatten() - - model.p = Var( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - list(range(n_stacks * T)), - bounds=(-1e-2, rated_power), - initialize=P_init, - ) - model.I = Var( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - list(range(n_stacks * T)), - within=Binary, # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - initialize=I_init, # .astype(int), - ) - model.T = Var( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - list(range(n_stacks * T)), - within=Binary, # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - initialize=T_init, # .astype(int), - ) - model.AC = Var( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - [0], bounds=(1e-3, 1.2 * rated_power * n_stacks * T), initialize=float(AC_init) - ) - model.F_tot = Var( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - [0], bounds=(1e-3, 8 * rated_power * n_stacks * T), initialize=float(F_tot_init) - ) - model.eps = Param( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - initialize=1, mutable=True - ) - - C_WP = c_wp * np.ones( - T, - ) # could vary with time - C_SW = c_sw * np.ones( - T, - ) # could vary with time - - P_max = rated_power - P_min = 0.1 * rated_power - - def obj(model): - return model.AC[0] - model.eps * model.F_tot[0] - - def physical_constraint_AC(model): - AC = 0 - for t in range(T): - for stack in range(n_stacks): - AC = ( - AC - + C_WP[t] * model.p[t * n_stacks + stack] - + C_SW[t] * model.T[t * n_stacks + stack] - ) - return model.AC[0] == AC + C_INV * n_stacks / LT - - def physical_constraint_F_tot(model): - """Objective function""" - F_tot = 0 - for t in range(T): - for stack in range(n_stacks): - F_tot = ( - F_tot - + ( - 0.0145 * model.p[t * n_stacks + stack] - + 0.3874 * model.I[t * n_stacks + stack] * rated_power / 500 - ) - * dt - ) - return model.F_tot[0] == F_tot - - def power_constraint(model, t): - """Make sure sum of stack powers is below available wind power.""" - power_full_stack = 0 - for stack in range(n_stacks): - power_full_stack = power_full_stack + model.p[t * n_stacks + stack] - return power_full_stack <= P_wind_t[t] - - def safety_bounds_lower(model, t, stack): - """Make sure input powers don't exceed safety bounds.""" - return P_min * model.I[t * n_stacks + stack] <= model.p[t * n_stacks + stack] - - def safety_bounds_upper(model, t, stack): - """Make sure input powers don't exceed safety bounds.""" - return P_max * model.I[t * n_stacks + stack] >= model.p[t * n_stacks + stack] - - def switching_constraint_pos(model, stack, t): - trans = model.I[t * n_stacks + stack] - model.I[(t - 1) * n_stacks + stack] - return model.T[t * n_stacks + stack] >= trans - - def switching_constraint_neg(model, stack, t): - trans = model.I[t * n_stacks + stack] - model.I[(t - 1) * n_stacks + stack] - return -model.T[t * n_stacks + stack] <= trans - - model.pwr_constraints = ( - ConstraintList() # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - ) - model.safety_constraints = ( - ConstraintList() # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - ) - model.switching_constraints = ( - ConstraintList() # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - ) - model.physical_constraints = ( - ConstraintList() # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - ) - - for t in range(T): - model.pwr_constraints.add(power_constraint(model, t)) - for stack in range(n_stacks): - model.safety_constraints.add(safety_bounds_lower(model, t, stack)) - model.safety_constraints.add(safety_bounds_upper(model, t, stack)) - - if t > 0: - model.switching_constraints.add(switching_constraint_pos(model, stack, t)) - model.switching_constraints.add(switching_constraint_neg(model, stack, t)) - model.physical_constraints.add(physical_constraint_F_tot(model)) - model.physical_constraints.add(physical_constraint_AC(model)) - model.objective = ( - Objective( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - expr=obj(model), - sense=minimize, # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - ) - ) - eps = 10 - cbc_path = Path(__file__).parent / "hybrid/PEM_Model_2Push/cbc.exe" - - # Use this if you have a Windows machine; also make sure that cbc.exe is in the same folder as - # this script - solver = SolverFactory( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - "cbc", executable=cbc_path - ) - - # Use this if you don't have a windows machine - # solver = SolverFactory("cbc") - - j = 1 - while eps > 1e-3: - start = time.process_time() - solver.solve(model) - print("time to solve", time.process_time() - start) - - model.eps = value( # FIXME: no * imports, delete whole comment when fixed # noqa: F405 - model.AC[0] / model.F_tot[0] - ) - eps = model.AC[0].value - model.eps.value * model.F_tot[0].value - j = j + 1 - - I = np.array([model.I[i].value for i in range(n_stacks * T)]) # noqa: E741 - I_ = np.reshape(I, (T, n_stacks)) - P = np.array([model.p[i].value for i in range(n_stacks * T)]) - P_ = np.reshape(P, (T, n_stacks)) - Tr = np.array([model.T[i].value for i in range(n_stacks * T)]).reshape((T, n_stacks)) - P_tot_opt = np.sum(P_, axis=1) - H2f = np.zeros((T, n_stacks)) - for stack in range(n_stacks): - H2f[:, stack] = (0.0145 * P_[:, stack] + 0.3874 * I_[:, stack] * rated_power / 500) * dt - return ( - P_tot_opt, - P_, - H2f, - I_, - Tr, - P_wind_t, - model.AC[0].value, - model.F_tot[0].value, - ) diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/pem_cost_tools.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/pem_cost_tools.py deleted file mode 100644 index c5d7d1e6d..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/pem_cost_tools.py +++ /dev/null @@ -1,201 +0,0 @@ -import numpy as np -from attrs import field, define - -from h2integrate.tools.profast_tools import create_years_of_operation - - -@define -class ElectrolyzerLCOHInputConfig: - """Calculates inputs for LCOH functions related to electrolyzer - performance outside of capex and opex. - - Args: - electrolyzer_physics_results (dict): results from run_electrolyzer_physics() - electrolyzer_config (dict): sub-dictionary of h2integrate_config - financial_analysis_start_year (int): analysis start year - installation_period_months (int|float|None): installation period in months. defaults to 36. - """ - - electrolyzer_physics_results: dict - electrolyzer_config: dict - financial_analysis_start_year: int - installation_period_months: int | float | None = field(default=36) - - electrolyzer_capacity_kW: int | float = field(init=False) - project_lifetime_years: int = field(init=False) - long_term_utilization: dict = field(init=False) - rated_capacity_kg_pr_day: float = field(init=False) - water_usage_gal_pr_kg: float = field(init=False) - - electrolyzer_annual_energy_usage_kWh: list[float] = field(init=False) - electrolyzer_eff_kWh_pr_kg: list[float] = field(init=False) - electrolyzer_annual_h2_production_kg: list[float] = field(init=False) - - refurb_cost_percent: list[float] = field(init=False) - replacement_schedule: list[float] = field(init=False) - - def __attrs_post_init__(self): - annual_performance = self.electrolyzer_physics_results["H2_Results"][ - "Performance Schedules" - ] - - #: electrolyzer system capacity in kW - self.electrolyzer_capacity_kW = self.electrolyzer_physics_results["H2_Results"][ - "system capacity [kW]" - ] - - #: int: lifetime of project in years - self.project_lifetime_years = len(annual_performance) - - #: float: electrolyzer beginnning-of-life rated H2 production capacity in kg/day - self.rated_capacity_kg_pr_day = ( - self.electrolyzer_physics_results["H2_Results"]["Rated BOL: H2 Production [kg/hr]"] * 24 - ) - - #: float: water usage in gallons of water per kg of H2 - self.water_usage_gal_pr_kg = self.electrolyzer_physics_results["H2_Results"][ - "Rated BOL: Gal H2O per kg-H2" - ] - #: list(float): annual energy consumed by electrolyzer per year of operation in kWh/year - self.electrolyzer_annual_energy_usage_kWh = annual_performance[ - "Annual Energy Used [kWh/year]" - ].to_list() - #: list(float): annual avg efficiency of electrolyzer for each year of operation in kWh/kg - self.electrolyzer_eff_kWh_pr_kg = annual_performance[ - "Annual Average Efficiency [kWh/kg]" - ].to_list() - #: list(float): annual hydrogen production for each year of operation in kg/year - self.electrolyzer_annual_h2_production_kg = annual_performance[ - "Annual H2 Production [kg/year]" - ] - #: dict: annual capacity factor of electrolyzer for each year of operation - self.long_term_utilization = self.make_lifetime_utilization() - - use_complex_refurb = False - if "complex_refurb" in self.electrolyzer_config.keys(): - if self.electrolyzer_config["complex_refurb"]: - use_complex_refurb = True - - # complex schedule assumes stacks are replaced in the year they reach end-of-life - if use_complex_refurb: - self.replacement_schedule = self.calc_complex_refurb_schedule() - self.refurb_cost_percent = list( - np.array( - self.replacement_schedule * self.electrolyzer_config["replacement_cost_percent"] - ) - ) - - # simple schedule assumes all stacks are replaced in the same year - else: - self.replacement_schedule = self.calc_simple_refurb_schedule() - self.refurb_cost_percent = list( - np.array( - self.replacement_schedule * self.electrolyzer_config["replacement_cost_percent"] - ) - ) - - def calc_simple_refurb_schedule(self): - """Calculate electrolyzer refurbishment schedule - assuming that all stacks are replaced in the same year. - - Returns: - list: list of years when stacks are replaced. - a value of 1 means stacks are replaced that year. - """ - annual_performance = self.electrolyzer_physics_results["H2_Results"][ - "Performance Schedules" - ] - refurb_simple = np.zeros(len(annual_performance)) - refurb_period = int( - round( - self.electrolyzer_physics_results["H2_Results"]["Time Until Replacement [hrs]"] - / 8760 - ) - ) - refurb_simple[refurb_period : len(annual_performance) : refurb_period] = 1.0 - - return refurb_simple - - def calc_complex_refurb_schedule(self): - """Calculate electrolyzer refurbishment schedule - stacks are replaced in the year they reach EOL. - - Returns: - list: list of years when stacks are replaced. values are are fraction of - the total installed capacity. - """ - annual_performance = self.electrolyzer_physics_results["H2_Results"][ - "Performance Schedules" - ] - refurb_complex = annual_performance["Refurbishment Schedule [MW replaced/year]"].values / ( - self.electrolyzer_capacity_kW / 1e3 - ) - return refurb_complex - - def make_lifetime_utilization(self): - """Make long term utilization dictionary for electrolyzer system. - - Returns: - dict: keys are years of operation and values are the capacity factor for that year. - """ - annual_performance = self.electrolyzer_physics_results["H2_Results"][ - "Performance Schedules" - ] - - years_of_operation = create_years_of_operation( - self.project_lifetime_years, - self.financial_analysis_start_year, - self.installation_period_months, - ) - - cf_per_year = annual_performance["Capacity Factor [-]"].to_list() - utilization_dict = dict(zip(years_of_operation, cf_per_year)) - return utilization_dict - - -def calc_electrolyzer_variable_om(electrolyzer_physics_results, h2integrate_config): - """Calculate variable O&M of electrolyzer system in $/kg-H2. - - Args: - electrolyzer_physics_results (dict): results from run_electrolyzer_physics() - h2integrate_config (:obj:`h2integrate_simulation.H2IntegrateSimulationConfig`): h2integrate - simulation config. - - Returns: - dict | float: electrolyzer variable o&m in $/kg-H2. - """ - electrolyzer_config = h2integrate_config["electrolyzer"] - annual_performance = electrolyzer_physics_results["H2_Results"]["Performance Schedules"] - - if "var_om" in electrolyzer_config.keys(): - electrolyzer_vopex_pr_kg = ( - electrolyzer_config["var_om"] - * annual_performance["Annual Average Efficiency [kWh/kg]"].values - ) - - if "financial_analysis_start_year" not in h2integrate_config["finance_parameters"]: - financial_analysis_start_year = h2integrate_config["project_parameters"][ - "financial_analysis_start_year" - ] - else: - financial_analysis_start_year = h2integrate_config["finance_parameters"][ - "financial_analysis_start_year" - ] - if "installation_time" not in h2integrate_config["project_parameters"]: - installation_period_months = 36 - else: - installation_period_months = h2integrate_config["project_parameters"][ - "installation_time" - ] - - years_of_operation = create_years_of_operation( - h2integrate_config["project_parameters"]["project_lifetime"], - financial_analysis_start_year, - installation_period_months, - ) - # $/kg-year - vopex_elec = dict(zip(years_of_operation, electrolyzer_vopex_pr_kg)) - - else: - vopex_elec = 0.0 - return vopex_elec diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/pem_mass_and_footprint.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/pem_mass_and_footprint.py deleted file mode 100644 index 1c6c0fbbc..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/pem_mass_and_footprint.py +++ /dev/null @@ -1,93 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from scipy.optimize import curve_fit - - -def _electrolyzer_footprint_data(): - """ - References: - [1] Bolhui, 2017 https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2020/Dec/IRENA_Green_hydrogen_cost_2020.pdf - - appears to include BOS - [2] Bourne, 2017 - - - [3] McPHy, 2018 (https://mcphy.com/en/equipment-services/electrolyzers/) - [4] Air Liquide 2021, Becancour Quebec - """ - - rating_mw = np.array([300, 100, 100, 20]) # [1], [2], [3], [4] - footprint_sqft = np.array([161500, 37700, 48500, 465000]) # [1], [2], [3], [4] - sqft_to_m2 = 0.092903 - footprint_m2 = footprint_sqft * sqft_to_m2 - - return rating_mw, footprint_m2 - - -def footprint(rating_mw): - """ - Estimate the area required for the electrolyzer equipment using a linear scaling - """ - - # from Singlitico 2021, Table 1 (ratio is given in m2/GW, so a conversion is used here forMW) - footprint_m2 = rating_mw * 48000 * (1 / 1e3) - - return footprint_m2 - - -def _electrolyzer_mass_data(): - """ - References: - [1] https://www.h-tec.com/en/products/detail/h-tec-pem-electrolyser-me450/me450/ - [2] https://www.nrel.gov/docs/fy19osti/70380.pdf - """ - - rating_mw = np.array([1, 1.25, 0.25, 45e-3, 40e-3, 28e-3, 14e-3, 14.4e-3, 7.2e-7]) - mass_kg = np.array([36e3, 17e3, 260, 900, 908, 858, 682, 275, 250]) - - return rating_mw, mass_kg - - -def _electrolyzer_mass_fit(x, m, b): - y = m * x + b - - return y - - -def mass(rating_mw): - """ - Estimate the electorlyzer mass given the electrolyzer rating based on data. - - Note: the largest electrolyzer data available was for 1.25 MW. Also, given the current fit, the - mass goes negative for very small electrolysis systems - """ - - rating_mw_fit, mass_kg_fit = _electrolyzer_mass_data() - - (m, b), pcov = curve_fit(_electrolyzer_mass_fit, rating_mw_fit, mass_kg_fit) - - mass_kg = _electrolyzer_mass_fit(rating_mw, m, b) - - return mass_kg - - -if __name__ == "__main__": - fig, ax = plt.subplots(1, 2) - rating_mw, footprint_m2 = _electrolyzer_footprint_data() - ax[0].scatter(rating_mw, footprint_m2, label="Data points") - - ratings = np.arange(0, 1000) - footprints = footprint(ratings) - - ax[0].plot(ratings, footprints, label="Scaling Factor") - ax[0].set(xlabel="Electrolyzer Rating (MW)", ylabel="Footprint (m$^2$)") - ax[0].legend(frameon=False) - print(rating_mw, footprint_m2) - - rating_mw, mass_kg = _electrolyzer_mass_data() - ax[1].scatter(rating_mw, np.multiply(mass_kg, 1e-3), label="Data points") - - ax[1].plot(ratings, mass(ratings) * 1e-3, label="Linear Fit") - ax[1].set(xlabel="Electrolyzer Rating (MW)", ylabel="Mass (metric tons)") - ax[1].legend(frameon=False) - plt.tight_layout() - plt.show() - print(rating_mw, np.divide(mass_kg, rating_mw)) diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_PEM_eco.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_PEM_eco.py deleted file mode 100644 index 74302b3ed..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_PEM_eco.py +++ /dev/null @@ -1,110 +0,0 @@ -import numpy as np -import examples.H2_Analysis.H2AModel as H2AModel - -from h2integrate.hydrogen.electrolysis.PEM_electrolyzer_IVcurve import PEM_electrolyzer_LT - - -def run_h2_PEM( - electrical_generation_timeseries, - electrolyzer_size, - kw_continuous, - forced_electrolyzer_cost_kw, - lcoe, - adjusted_installed_cost, - useful_life, - net_capital_costs, - voltage_type="constant", - stack_input_voltage_DC=250, - min_V_cell=1.62, - p_s_h2_bar=31, - stack_input_current_lower_bound=500, - cell_active_area=1250, - N_cells=130, - total_system_electrical_usage=55.5, -): - in_dict = {} - out_dict = {} - in_dict["P_input_external_kW"] = electrical_generation_timeseries - in_dict["electrolyzer_system_size_MW"] = electrolyzer_size - el = PEM_electrolyzer_LT(in_dict, out_dict) - - # el.power_supply_rating_MW = electrolyzer_size - # el.power_supply_rating_MW = power_supply_rating_MW - # print("electrolyzer size: ", electrolyzer_size) - # el.electrolyzer_system_size_MW = electrolyzer_size - # el.input_dict['voltage_type'] = voltage_type - # el.stack_input_voltage_DC = stack_input_voltage_DC - # el.stack_input_voltage_DC = - # Assumptions: - # el.min_V_cell = min_V_cell # Only used in variable voltage scenario - # el.p_s_h2_bar = p_s_h2_bar # H2 outlet pressure - # el.stack_input_current_lower_bound = stack_input_current_lower_bound - # el.cell_active_area = cell_active_area - # el.N_cells = N_cells - # print("running production rate") - # el.h2_production_rate() - - el.h2_production_rate() - el.water_supply() - - avg_generation = np.mean(electrical_generation_timeseries) # Avg Generation - # print("avg_generation: ", avg_generation) - cap_factor = avg_generation / kw_continuous - - hydrogen_hourly_production = out_dict["h2_produced_kg_hr_system"] - water_hourly_usage = out_dict["water_used_kg_hr"] - water_annual_usage = out_dict["water_used_kg_annual"] - electrolyzer_total_efficiency = out_dict["total_efficiency"] - # print('water annual: ', water_annual_usage) - # print("cap_factor: ", cap_factor) - - # Get Daily Hydrogen Production - Add Every 24 hours - i = 0 - daily_H2_production = [] - while i <= 8760: - x = sum(hydrogen_hourly_production[i : i + 24]) - daily_H2_production.append(x) - i = i + 24 - - avg_daily_H2_production = np.mean(daily_H2_production) # kgH2/day - hydrogen_annual_output = sum(hydrogen_hourly_production) # kgH2/year - # elec_remainder_after_h2 = combined_hybrid_curtailment_hopp - - H2A_Results = H2AModel.H2AModel( - cap_factor, - avg_daily_H2_production, - hydrogen_annual_output, - force_system_size=True, - forced_system_size=electrolyzer_size, - force_electrolyzer_cost=True, - forced_electrolyzer_cost_kw=forced_electrolyzer_cost_kw, - useful_life=useful_life, - ) - - feedstock_cost_h2_levelized_hopp = lcoe * total_system_electrical_usage / 100 # $/kg - # Hybrid Plant - levelized H2 Cost - HOPP - feedstock_cost_h2_via_net_cap_cost_lifetime_h2_hopp = adjusted_installed_cost / ( - hydrogen_annual_output * useful_life - ) # $/kgH2 - - # Total Hydrogen Cost ($/kgH2) - h2a_costs = H2A_Results["Total Hydrogen Cost ($/kgH2)"] - total_unit_cost_of_hydrogen = h2a_costs + feedstock_cost_h2_levelized_hopp - feedstock_cost_h2_via_net_cap_cost_lifetime_h2_reopt = net_capital_costs / ( - (kw_continuous / total_system_electrical_usage) * (8760 * useful_life) - ) - - H2_Results = { - "hydrogen_annual_output": hydrogen_annual_output, - "feedstock_cost_h2_levelized_hopp": feedstock_cost_h2_levelized_hopp, - "feedstock_cost_h2_via_net_cap_cost_lifetime_h2_hopp": feedstock_cost_h2_via_net_cap_cost_lifetime_h2_hopp, # noqa: E501 - "feedstock_cost_h2_via_net_cap_cost_lifetime_h2_reopt": feedstock_cost_h2_via_net_cap_cost_lifetime_h2_reopt, # noqa: E501 - "total_unit_cost_of_hydrogen": total_unit_cost_of_hydrogen, - "cap_factor": cap_factor, - "hydrogen_hourly_production": hydrogen_hourly_production, - "water_hourly_usage": water_hourly_usage, - "water_annual_usage": water_annual_usage, - "electrolyzer_total_efficiency": electrolyzer_total_efficiency, - } - - return H2_Results, H2A_Results diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_clusters.py b/h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_clusters.py deleted file mode 100644 index 4fe540ddb..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/run_h2_clusters.py +++ /dev/null @@ -1,233 +0,0 @@ -import sys - - -sys.path.append("") -import time -import warnings - -import numpy as np -import pandas as pd -from scipy import interpolate - -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer_Clusters import ( # noqa: E501 - PEM_H2_Clusters as PEMClusters, -) - - -# from PyOMO import ipOpt !! FOR SANJANA!! -warnings.filterwarnings("ignore") - -""" -Perform a LCOH analysis for an offshore wind + Hydrogen PEM system - -1. Offshore wind site locations and cost details (4 sites, $1300/kw capex + BOS cost which will - come from Orbit Runs)~ -2. Cost Scaling Based on Year (Have Weiser et. al report with cost scaling for fixed and floating - tech, will implement) -3. Cost Scaling Based on Plant Size (Shields et. Al report) -4. Future Model Development Required: -- Floating Electrolyzer Platform -""" - - -# -# --------------------------- -# -class run_PEM_clusters: - """Add description and stuff :)""" - - def __init__(self, electrical_power_signal, system_size_mw, num_clusters, verbose=True): - self.cluster_cap_mw = np.round(system_size_mw / num_clusters) - self.num_clusters = num_clusters - - self.stack_rating_kw = 1000 - self.stack_min_power_kw = 0.1 * self.stack_rating_kw - # self.num_available_pem=interconnection_size_mw - self.input_power_kw = electrical_power_signal - self.cluster_min_power = self.stack_min_power_kw * self.cluster_cap_mw - self.cluster_max_power = self.stack_rating_kw * self.cluster_cap_mw - - self.verbose = verbose - - def run(self): - clusters = self.create_clusters() - power_to_clusters = self.even_split_power() - h2_df_ts = pd.DataFrame() - h2_df_tot = pd.DataFrame() - # h2_dict_ts={} - # h2_dict_tot={} - - col_names = [] - start = time.perf_counter() - for ci in range(len(clusters)): - cl_name = f"Cluster #{ci}" - col_names.append(cl_name) - h2_ts, h2_tot = clusters[ci].run(power_to_clusters[ci]) - # h2_dict_ts['Cluster #{}'.format(ci)] = h2_ts - - h2_ts_temp = pd.Series(h2_ts, name=cl_name) - h2_tot_temp = pd.Series(h2_tot, name=cl_name) - if len(h2_df_tot) == 0: - # h2_df_ts=pd.concat([h2_df_ts,h2_ts_temp],axis=0,ignore_index=False) - h2_df_tot = pd.concat([h2_df_tot, h2_tot_temp], axis=0, ignore_index=False) - h2_df_tot.columns = col_names - - h2_df_ts = pd.concat([h2_df_ts, h2_ts_temp], axis=0, ignore_index=False) - h2_df_ts.columns = col_names - else: - # h2_df_ts = h2_df_ts.join(h2_ts_temp) - h2_df_tot = h2_df_tot.join(h2_tot_temp) - h2_df_tot.columns = col_names - - h2_df_ts = h2_df_ts.join(h2_ts_temp) - h2_df_ts.columns = col_names - - end = time.perf_counter() - if self.verbose: - print(f"Took {round(end - start, 3)} sec to run the RUN function") - return h2_df_ts, h2_df_tot - # return h2_dict_ts, h2_df_tot - - def optimize_power_split(self): - # Inputs: power signal, number of stacks, cost of switching (assumed constant) - # install PyOMO - #!!! Insert Sanjana's Code !!! - # - power_per_stack = [] - return power_per_stack # size - - def even_split_power(self): - start = time.perf_counter() - # determine how much power to give each cluster - num_clusters_on = np.floor(self.input_power_kw / self.cluster_min_power) - num_clusters_on = np.where( - num_clusters_on > self.num_clusters, self.num_clusters, num_clusters_on - ) - power_per_cluster = [ - self.input_power_kw[ti] / num_clusters_on[ti] if num_clusters_on[ti] > 0 else 0 - for ti, pwr in enumerate(self.input_power_kw) - ] - - power_per_to_active_clusters = np.array(power_per_cluster) - power_to_clusters = np.zeros((len(self.input_power_kw), self.num_clusters)) - for i, cluster_power in enumerate( - power_per_to_active_clusters - ): # np.arange(0,self.n_stacks,1): - clusters_off = self.num_clusters - int(num_clusters_on[i]) - no_power = np.zeros(clusters_off) - with_power = cluster_power * np.ones(int(num_clusters_on[i])) - tot_power = np.concatenate((with_power, no_power)) - power_to_clusters[i] = tot_power - - # power_to_clusters = np.repeat([power_per_cluster],self.num_clusters,axis=0) - end = time.perf_counter() - if self.verbose: - print(f"Took {round(end - start, 3)} sec to run basic_split_power function") - # rows are power, columns are stacks [300 x n_stacks] - - return np.transpose(power_to_clusters) - - def run_distributed_layout_power(self, wind_plant): - # need floris configuration! - x_load_percent = np.linspace(0.1, 1.0, 10) - - # ac2ac_transformer_eff = np.array( - # [90.63, 93.91, 95.63, 96.56, 97.19, 97.50, 97.66, 97.66, 97.66, 97.50] - # ) - ac2dc_rectification_eff = ( - np.array([96.54, 98.12, 98.24, 98.6, 98.33, 98.03, 97.91, 97.43, 97.04, 96.687]) / 100 - ) - dc2dc_rectification_eff = ( - np.array([91.46, 95.16, 96.54, 97.13, 97.43, 97.61, 97.61, 97.73, 97.67, 97.61]) / 100 - ) - rect_eff = ac2dc_rectification_eff * dc2dc_rectification_eff - f = interpolate.interp1d(x_load_percent, rect_eff) - start_idx = 0 - end_idx = 8760 - nTurbs = self.num_clusters - power_turbines = np.zeros((nTurbs, 8760)) - power_to_clusters = np.zeros((8760, self.num_clusters)) - ac2dc_rated_power_kw = wind_plant.turb_rating - - power_turbines[:, start_idx:end_idx] = ( - wind_plant._system_model.fi.get_turbine_powers().reshape((nTurbs, end_idx - start_idx)) - / 1000 - ) - power_to_clusters = (power_turbines) * (f(power_turbines / ac2dc_rated_power_kw)) - - # power_farm *((100 - 12.83)/100) / 1000 - - clusters = self.create_clusters() - - h2_df_ts = pd.DataFrame() - h2_df_tot = pd.DataFrame() - # h2_dict_ts={} - # h2_dict_tot={} - - col_names = [] - start = time.perf_counter() - for ci in range(len(clusters)): - cl_name = f"Cluster #{ci}" - col_names.append(cl_name) - h2_ts, h2_tot = clusters[ci].run(power_to_clusters[ci]) - # h2_dict_ts['Cluster #{}'.format(ci)] = h2_ts - - h2_ts_temp = pd.Series(h2_ts, name=cl_name) - h2_tot_temp = pd.Series(h2_tot, name=cl_name) - if len(h2_df_tot) == 0: - # h2_df_ts=pd.concat([h2_df_ts,h2_ts_temp],axis=0,ignore_index=False) - h2_df_tot = pd.concat([h2_df_tot, h2_tot_temp], axis=0, ignore_index=False) - h2_df_tot.columns = col_names - - h2_df_ts = pd.concat([h2_df_ts, h2_ts_temp], axis=0, ignore_index=False) - h2_df_ts.columns = col_names - else: - # h2_df_ts = h2_df_ts.join(h2_ts_temp) - h2_df_tot = h2_df_tot.join(h2_tot_temp) - h2_df_tot.columns = col_names - - h2_df_ts = h2_df_ts.join(h2_ts_temp) - h2_df_ts.columns = col_names - - end = time.perf_counter() - if self.verbose: - print(f"Took {round(end - start, 3)} sec to run the distributed PEM case function") - return h2_df_ts, h2_df_tot - - def max_h2_cntrl(self): - # run as many at lower power as possible - ... - - def min_deg_cntrl(self): - # run as few as possible - ... - - def create_clusters(self): - start = time.perf_counter() - # TODO fix the power input - don't make it required! - # in_dict={'dt':3600} - clusters = PEMClusters(cluster_size_mw=self.cluster_cap_mw) - stacks = [clusters] * self.num_clusters - end = time.perf_counter() - if self.verbose: - print(f"Took {round(end - start, 3)} sec to run the create clusters") - return stacks - - -if __name__ == "__main__": - system_size_mw = 1000 - num_clusters = 20 - cluster_cap_mw = system_size_mw / num_clusters - stack_rating_kw = 1000 - cluster_min_power_kw = 0.1 * stack_rating_kw * cluster_cap_mw - num_steps = 200 - power_rampup = np.arange( - cluster_min_power_kw, system_size_mw * stack_rating_kw, cluster_min_power_kw - ) - - # power_rampup = np.linspace(cluster_min_power_kw,system_size_mw*1000,num_steps) - power_rampdown = np.flip(power_rampup) - power_in = np.concatenate((power_rampup, power_rampdown)) - pem = run_PEM_clusters(power_in, system_size_mw, num_clusters) - - h2_ts, h2_tot = pem.run() diff --git a/h2integrate/simulation/technologies/hydrogen/electrolysis/test_opt.ipynb b/h2integrate/simulation/technologies/hydrogen/electrolysis/test_opt.ipynb deleted file mode 100644 index 8033a7e9f..000000000 --- a/h2integrate/simulation/technologies/hydrogen/electrolysis/test_opt.ipynb +++ /dev/null @@ -1,146 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The autoreload extension is already loaded. To reload it, use:\n", - " %reload_ext autoreload\n" - ] - } - ], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2\n", - "\n", - "from pyomo.environ import * # FIXME: no * imports, delete whole comment when fixed # noqa: F403\n", - "import numpy as np\n", - "from h2integrate.simulation.technologies.Electrolyzer_Models import run_PEM_clusters\n", - "\n", - "# from run_PEM_master import run_PEM_clusters" - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Took 1.021 sec to run the create clusters\n", - "Optimizing 20 stacks tarting 0hr/398hr\n", - "time to solve 0.8617440000000016\n", - "Optimizing 20 stacks tarting 219hr/398hr\n", - "time to solve 0.5715739999999983\n", - "Took 0.075 sec to run the RUN function\n" - ] - } - ], - "source": [ - "system_size_mw = 1000\n", - "num_clusters = 20\n", - "cluster_cap_mw = system_size_mw / num_clusters\n", - "stack_rating_kw = 1000\n", - "cluster_min_power_kw = 0.1 * stack_rating_kw * cluster_cap_mw\n", - "num_steps = 200\n", - "power_rampup = np.arange(\n", - " cluster_min_power_kw, system_size_mw * stack_rating_kw, cluster_min_power_kw\n", - ")\n", - "\n", - "plant_life = 30\n", - "deg_penalty = True\n", - "user_defined_electrolyzer_EOL_eff_drop = False\n", - "EOL_eff_drop = 13\n", - "user_defined_electrolyzer_BOL_kWh_per_kg = False\n", - "BOL_kWh_per_kg = []\n", - "electrolyzer_model_parameters = {\n", - " \"Modify BOL Eff\": user_defined_electrolyzer_BOL_kWh_per_kg,\n", - " \"BOL Eff [kWh/kg-H2]\": BOL_kWh_per_kg,\n", - " \"Modify EOL Degradation Value\": user_defined_electrolyzer_EOL_eff_drop,\n", - " \"EOL Rated Efficiency Drop\": EOL_eff_drop,\n", - "}\n", - "# power_rampup = np.linspace(cluster_min_power_kw,system_size_mw*1000,num_steps)\n", - "power_rampdown = np.flip(power_rampup)\n", - "power_in = np.concatenate((power_rampup, power_rampdown))\n", - "pem = run_PEM_clusters(\n", - " power_in,\n", - " system_size_mw,\n", - " num_clusters,\n", - " plant_life,\n", - " electrolyzer_model_parameters,\n", - " deg_penalty,\n", - ")\n", - "\n", - "h2_ts, h2_tot = pem.run(optimize=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "9.125" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "219 / 24" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3.9.2 ('aibias')", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.2" - }, - "orig_nbformat": 4, - "vscode": { - "interpreter": { - "hash": "006fa253eb2b24f25ab550317f005ab784f102b1e9b70f76ded10bb7ec2196b2" - } - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/Bulk Hydrogen Cost as Function of Capacity.docx b/h2integrate/simulation/technologies/hydrogen/h2_storage/Bulk Hydrogen Cost as Function of Capacity.docx deleted file mode 100644 index 4f103cdcd..000000000 Binary files a/h2integrate/simulation/technologies/hydrogen/h2_storage/Bulk Hydrogen Cost as Function of Capacity.docx and /dev/null differ diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/lined_rock_cavern/lined_rock_cavern.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/lined_rock_cavern/lined_rock_cavern.py deleted file mode 100644 index 92cc16ea0..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/lined_rock_cavern/lined_rock_cavern.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -Author: Kaitlin Brunik -Created: 7/20/2023 -Institution: National Renewable Energy Lab -Description: This file outputs capital and operational costs of lined rock cavern hydrogen storage. -It needs to be updated to with operational dynamics. -Costs are in 2018 USD - -Sources: - - [1] Papadias 2021: https://www.sciencedirect.com/science/article/pii/S0360319921030834?via%3Dihub - - [2] Papadias 2021: Bulk Hydrogen as Function of Capacity.docx documentation at - hopp/hydrogen/h2_storage - - [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet -""" - -import numpy as np - -from h2integrate.simulation.technologies.hydrogen.h2_transport.h2_compression import Compressor - - -class LinedRockCavernStorage: - """ - - Costs are in 2018 USD - """ - - def __init__(self, input_dict): - """ - Initialize LinedRockCavernStorage. - - Args: - input_dict (dict): - - h2_storage_kg (float): total capacity of hydrogen storage [kg] - - storage_duration_hrs (float): (optional if h2_storage_kg set) [hrs] - - flow_rate_kg_hr (float): (optional if h2_storage_kg set) [kg/hr] - - system_flow_rate (float): [kg/day] - - labor_rate (float): (optional, default: 37.40) [$2018/hr] - - insurance (float): (optional, default: 1%) [decimal percent] - - property_taxes (float): (optional, default: 1%) [decimal percent] - - licensing_permits (float): (optional, default: 0.01%) [decimal percent] - Returns: - - lined_rock_cavern_storage_capex_per_kg (float): the installed capital cost per kg h2 - in 2018 [USD/kg] - - installed_capex (float): the installed capital cost in 2018 [USD] (including - compressor) - - storage_compressor_capex (float): the installed capital cost in 2018 for the - compressor [USD] - - total_opex (float): the OPEX (annual, fixed) in 2018 excluding electricity costs - [USD/kg-yr] - - output_dict (dict): - - lined_rock_cavern_storage_capex (float): installed capital cost in 2018 [USD] - - lined_rock_cavern_storage_opex (float): OPEX (annual, fixed) in 2018 [USD/yr] - """ - self.input_dict = input_dict - self.output_dict = {} - - # inputs - if "h2_storage_kg" in input_dict: - self.h2_storage_kg = input_dict["h2_storage_kg"] # [kg] - elif "storage_duration_hrs" and "flow_rate_kg_hr" in input_dict: - self.h2_storage_kg = input_dict["storage_duration_hrs"] * input_dict["flow_rate_kg_hr"] - else: - raise Exception( - "input_dict must contain h2_storage_kg or storage_duration_hrs and flow_rate_kg_hr" - ) - - if "system_flow_rate" not in input_dict.keys(): - raise ValueError("system_flow_rate required for lined rock cavern storage model.") - else: - self.system_flow_rate = input_dict["system_flow_rate"] - - self.labor_rate = input_dict.get("labor_rate", 37.39817) # $(2018)/hr - self.insurance = input_dict.get("insurance", 1 / 100) # % of total capital investment - self.property_taxes = input_dict.get( - "property_taxes", 1 / 100 - ) # % of total capital investment - self.licensing_permits = input_dict.get( - "licensing_permits", 0.1 / 100 - ) # % of total capital investment - self.comp_om = input_dict.get( - "compressor_om", 4 / 100 - ) # % of compressor capital investment - self.facility_om = input_dict.get( - "facility_om", 1 / 100 - ) # % of facility capital investment minus compressor capital investment - - def lined_rock_cavern_capex(self): - """ - Calculates the installed capital cost of lined rock cavern hydrogen storage - Returns: - - lined_rock_cavern_storage_capex_per_kg (float): the installed capital cost per kg h2 - in 2018 [USD/kg] - - installed_capex (float): the installed capital cost in 2018 [USD] (including - compressor) - - storage_compressor_capex (float): the installed capital cost in 2018 for the - compressor [USD] - - output_dict (dict): - - lined_rock_cavern_storage_capex (float): installed capital cost in 2018 [USD] - """ - - # Installed capital cost - a = 0.095803 - b = 1.5868 - c = 10.332 - self.lined_rock_cavern_storage_capex_per_kg = np.exp( - a * (np.log(self.h2_storage_kg / 1000)) ** 2 - b * np.log(self.h2_storage_kg / 1000) + c - ) # 2019 [USD] from Papadias [2] - self.installed_capex = self.lined_rock_cavern_storage_capex_per_kg * self.h2_storage_kg - cepci_overall = 1.29 / 1.30 # Convert from $2019 to $2018 - self.installed_capex = cepci_overall * self.installed_capex - self.output_dict["lined_rock_cavern_storage_capex"] = self.installed_capex - - outlet_pressure = 200 # Max outlet pressure of lined rock cavern in [1] - n_compressors = 2 - storage_compressor = Compressor( - outlet_pressure, self.system_flow_rate, n_compressors=n_compressors - ) - storage_compressor.compressor_power() - motor_rating, power = storage_compressor.compressor_system_power() - if motor_rating > 1600: - n_compressors += 1 - storage_compressor = Compressor( - outlet_pressure, self.system_flow_rate, n_compressors=n_compressors - ) - storage_compressor.compressor_power() - motor_rating, power = storage_compressor.compressor_system_power() - comp_capex, comp_OM = storage_compressor.compressor_costs() - cepci = 1.36 / 1.29 # convert from $2016 to $2018 - self.comp_capex = comp_capex * cepci - return ( - self.lined_rock_cavern_storage_capex_per_kg, - self.installed_capex, - self.comp_capex, - ) - - def lined_rock_cavern_opex(self): - """ - Calculates the operation and maintenance costs excluding electricity costs for the lined - rock cavern hydrogen storage - - Returns: - - total_opex (float): the OPEX (annual, fixed) in 2018 excluding electricity costs - [USD/kg-yr] - - output_dict (dict): - - lined_rock_cavern_storage_opex (float): OPEX (annual, fixed) in 2018 [USD/yr] - """ - # Operations and Maintenace costs [3] - # Labor - # Base case is 1 operator, 24 hours a day, 7 days a week for a 100,000 kg/day average - # capacity facility. Scaling factor of 0.25 is used for other sized facilities - annual_hours = 8760 * (self.system_flow_rate / 100000) ** 0.25 - self.overhead = 0.5 - labor = (annual_hours * self.labor_rate) * (1 + self.overhead) # Burdened labor cost - insurance = self.insurance * self.installed_capex - property_taxes = self.property_taxes * self.installed_capex - licensing_permits = self.licensing_permits * self.installed_capex - comp_op_maint = self.comp_om * self.comp_capex - facility_op_maint = self.facility_om * (self.installed_capex - self.comp_capex) - - # O&M excludes electricity requirements - total_om = ( - labor - + insurance - + licensing_permits - + property_taxes - + comp_op_maint - + facility_op_maint - ) - self.output_dict["lined_rock_cavern_storage_opex"] = total_om - return total_om diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/mch/mch_cost.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/mch/mch_cost.py deleted file mode 100644 index 29127da91..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/mch/mch_cost.py +++ /dev/null @@ -1,142 +0,0 @@ -from attrs import field, define - - -@define -class MCHStorage: - """ - Cost model representing a toluene/methylcyclohexane (TOL/MCH) hydrogen storage system. - - Costs are in 2024 USD. - - Sources: - Breunig, H., Rosner, F., Saqline, S. et al. "Achieving gigawatt-scale green hydrogen - production and seasonal storage at industrial locations across the U.S." *Nat Commun* - **15**, 9049 (2024). https://doi.org/10.1038/s41467-024-53189-2 - - Args: - max_H2_production_kg_pr_hr (float): Maximum amount of hydrogen that may be - used to fill storage in kg/hr. - hydrogen_storage_capacity_kg (float): Hydrogen storage capacity in kilograms. - hydrogen_demand_kg_pr_hr (float): Hydrogen demand in kg/hr. - annual_hydrogen_stored_kg_pr_yr (float): Sum of hydrogen used to fill storage - in kg/year. - - Note: - Hydrogenation capacity (HC) should be sized to allow for peak hydrogen charge rate. - Dehydrogenation capacity (DC) sized to assume that end-use requires a consistent H2 supply. - Maximum storage capacity (MS) is the maximum consecutive quantity of H2 stored - with lowest frequency of discharge. - Annual hydrogen storage (AS) is the hydrogen curtailed from production into storage. - - """ - - max_H2_production_kg_pr_hr: float - hydrogen_storage_capacity_kg: float - hydrogen_demand_kg_pr_hr: float - annual_hydrogen_stored_kg_pr_yr: float - - #: dehydrogenation capacity [metric tonnes/day] - Dc: float = field(init=False) - - #: hydrogenation capacity [metric tonnes/day] - Hc: float = field(init=False) - - #: maximum storage capacity [metric tonnes] - Ms: float = field(init=False) - - #: annual hydrogen into storage [metric tonnes] - As: float = field(init=False) - - # overnight capital cost coefficients - occ_coeff = (54706639.43, 147074.25, 588779.05, 20825.39, 10.31) - - #: fixed O&M cost coefficients - foc_coeff = (3419384.73, 3542.79, 13827.02, 61.22, 0.0) - - #: variable O&M cost coefficients - voc_coeff = (711326.78, 1698.76, 6844.86, 36.04, 376.31) - - #: lcos cost coefficients for a capital charge factor of 0.0710 - lcos_coeff = (8014882.91, 15683.82, 62475.19, 1575.86, 377.04) - - #: hydrogen storage efficiency - eta = 0.9984 - - #: cost year associated with the costs in this model - cost_year = 2024 - - def __attrs_post_init__(self): - # Equation (3): DC = P_avg - self.Dc = self.hydrogen_demand_kg_pr_hr * 24 / 1e3 - - # Equation (2): HC = P_nameplate - P_avg - P_nameplate = self.max_H2_production_kg_pr_hr * 24 / 1e3 - self.Hc = P_nameplate - self.Dc - - # Equation (1): AS = sum(curtailed_h2) - self.As = self.annual_hydrogen_stored_kg_pr_yr / 1e3 - - # Defined in paragraph between Equation (2) and (3) - self.Ms = self.hydrogen_storage_capacity_kg / 1e3 - - def calc_cost_value(self, b0, b1, b2, b3, b4): - """ - Calculate the value of the cost function for the given coefficients. - - Args: - b0 (float): Coefficient representing the base cost. - b1 (float): Coefficient for the Hc (hydrogenation capacity) term. - b2 (float): Coefficient for the Dc (dehydrogenation capacity) term. - b3 (float): Coefficient for the Ms (maximum storage) term. - b4 (float): Coefficient for the As (annual hydrogen into storage) term. - Returns: - float: The calculated cost value based on the provided coefficients and attributes. - - """ - return b0 + (b1 * self.Hc) + (b2 * self.Dc) + (b3 * self.Ms) + b4 * self.As - - def run_costs(self): - """Calculate the costs of TOL/MCH hydrogen storage. - - Returns: - dict: dictionary of costs for TOL/MCH storage - """ - cost_results = { - "mch_capex": self.calc_cost_value(*self.occ_coeff), - "mch_opex": self.calc_cost_value(*self.foc_coeff), - "mch_variable_om": self.calc_cost_value(*self.voc_coeff), - "mch_cost_year": self.cost_year, - } - return cost_results - - def estimate_lcos(self): - """Estimate the levelized cost of hydrogen storage. Based on Equation (7) of the - reference article. - - Returns: - float: levelized cost of storage in $2024/kg-H2 stored - """ - - lcos_numerator = self.calc_cost_value(*self.lcos_coeff) - lcos_denom = self.As * self.eta * 1e3 - lcos_est = lcos_numerator / lcos_denom - return lcos_est - - def estimate_lcos_from_costs(self, ccf=0.0710): - """Estimate the levelized cost of hydrogen storage. Based on Equation (5) of the - reference article. - - Args: - ccf (float, optional): Capital charge factor. Defaults to 0.0710. - - Returns: - float: levelized cost of storage in $2024/kg-H2 stored - """ - - toc = self.calc_cost_value(*self.occ_coeff) - voc = self.calc_cost_value(*self.voc_coeff) - foc = self.calc_cost_value(*self.foc_coeff) - costs = (toc * ccf) + voc + foc - denom = self.As * self.eta * 1e3 - lcos_est = costs / denom - return lcos_est diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/README.md b/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/README.md deleted file mode 100644 index 1a042fde2..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/README.md +++ /dev/null @@ -1,117 +0,0 @@ - -# On-turbine hydrogen storage modeling - -## Implementation - -In this module, we create a model for storing hydrogen in a wind turbine tower -We follow, largely, the work of Kottenstette (see NREL/TP-500-34656), although -various assumptions in their study are not marked, and our goal is to flesh out -some of their assumptions. - -`PressurizedTower` is an object model that represents a pressurized wind turbine -tower with geometry specified on input. The Kottenstette work assumes a wind -turbine tower whose thickness is set by a constant diameter-thickness ratio, -which defaults to 320. The tower is specified by diameter/height pairs, between -which a linear taper is assumed. The material of the tower is assumed to be -steel with the following properties: - -- ultimate tensile strength: 636 MPa -- yield strength: 350 MPa -- welded joint efficiency: 0.80 (see ASME Boiler and Pressure Vessel Code for details) -- density: 7817 kg/m3 -- cost per kg: $1.50 - -These can be modified for alternative studies by variable access on the `PressurizedTower` object before running an analysis. Refer to the `__init__()` -function for definitions. Inner volume of the tower is computed by conic frustum -volume according to each section, assuming thin walls (s.t. $d \gg t$). Wall -material is computed by assuming the wall thickness is centered at the diameter -dimension (outer or inner thickness placement is available by specification). - -## Hydrogen storage - -### Wall increment - -When hydrogen is stored, a Goodman's equation thickness increment is assumed for -the vertical tower walls (see Kottenstette) in order to handle the additional -pressure stress contribution which is a zero-intercept linear function of -diameter, see `PressurizedTower.get_thickness_increment_const` for the leading -coefficient calculation. Hydrogen is assumed to be stored at the crossover -pressure where pressurized burst strength and aerodynamic moment fatigue are -balanced, see Kottenstette for theory and -`PressureizedTower.get_crossover_pressure` for implementation. - -### End cap sizing - -End caps are necessary for a pressure vessel tower, which are sized according to -the ASME Boiler and Pressure Vessel code. Caps are assumed to be welded with -flat pressure heads affixed by a rounded corner. Implementation in -`PressurizedTower.compute_cap_thickness` contains details on thickness -computation. Following Kottenstette, we use 2.66 \$/kg to cost the endcap -material. - -### Hydrogen - -A pressurized tower, then, is assumed to hold a volume of $\mathrm{H}_2$, stored -at pressure and the ambient temperature, and the ideal gas law is used to relate -the resulting mass of the stored hydrogen. - -### Summary and additional costs - -Above the baseline (non-pressurized) tower, hydrogen storage entails, then: -- increased wall thickness -- the addition of 2 pressure vessel endcaps -- additional fixed non-tower expenses, given in Kottenstette - - additional ladder - - conduit for weatherized wiring - - mainframe extension for safe addition of external ladder - - additional interior access door - - access mainway & nozzles for pressure vessel - -## Expenditure models - -### Capital expenditure (CapEx) model - -Capital expenses in addition to the baseline tower are given by: -- additional steel costs above baseline tower for wall reinforcement & pressure - end caps -- additional hardware requirements for/to facilitate hydrogen storage - -### Operational expenditure (OpEx) model - -Operational expenditure on pressure vessel is modeled roughly following the -relevant costs from `hopp/hydrogen/h2_storage/pressure_vessel`. The resulting -estimates are _rough_. Basically the annual operational expenditures are modeled -as: -$$ -OPEX= R_{\mathrm{maint}} \times CAPEX + \mathrm{Hours}_{\mathrm{staff}} \times \mathrm{Wage} -$$ -where $R_{\mathrm{maint}}$ is a maintenance rate. We assume: -- $R_{\mathrm{maint}}= 0.03$; i.e.: 3\% of the capital costs must be re-invested each year to cover maintenance -- $\mathrm{Wage}= \$36/\mathrm{hr}$ -- $\mathrm{Hours}= 60$: 60 man-hours of maintenance on the pressure vessel per year; this number very roughly derived from the other code - -## Unit testing - -Unit testing of this module consists of three tests under two approaches: -- comparison with simple geometries - - cylindrical tower - - conical tower -- comparison with Kottenstette results - - issue: Kottenstette results have some assumptions secreted away, so these - _at best_ are just to ensure the model remains in the ballpark of - the Kottenstette results - - specifically, Kottenstette's results table (Table 3 in NREL report) - imply a partial tower pressure vessel, inducing various - differences between this code and their results - - I can't figure out how to size the pressure vessel caps (heads) to get the - costs that are reported in Kottenstette - - tests against Table 3: - - traditional/non-pressurized tower: - - tower costs within 5% - - non-tower costs within 5% - - pressurized tower: - - wall costs within 10% - - _top cap within 100%_ - - _bottom cap within 200%_ - - non-tower cost within 10% - - capacity within 10% diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/__init__.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/__init__.py deleted file mode 100644 index 832c8d2d1..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from h2integrate.simulation.technologies.hydrogen.h2_storage.on_turbine.on_turbine_hydrogen_storage import ( - PressurizedTower, -) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/on_turbine_hydrogen_storage.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/on_turbine_hydrogen_storage.py deleted file mode 100644 index d8fca53f7..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/on_turbine/on_turbine_hydrogen_storage.py +++ /dev/null @@ -1,527 +0,0 @@ -""" -Author: Cory Frontin -Date: 23 Jan 2023 -Institution: National Renewable Energy Lab -Description: This file handles the cost, sizing, and pressure of on-turbine H2 storage - -To use this class, specify a turbine - -Costs are assumed to be in 2003 dollars [1] - -Sources: - - [1] Kottenstette 2003 (use their chosen favorite design) -Args: - - year (int): construction year - - turbine (dict): contains various information about the turbine, including tower_length, - section_diameters, and section_heights -API member functions: - - get_capex(): return the total additional capex necessary for H2 production, in 2003 dollars - - get_opex(): return the result of a simple model for operational expenditures for pressure - vessel, in 2003 dollars - - get_mass_empty(): return the total additional empty mass necessary for H2 production, in kg - - get_capacity_H2(): return the capacity mass of hydrogen @ operating pressure, ambient temp., - in kg - - get_pressure_H2() return the operating hydrogen pressure, in Pa -""" - -from __future__ import annotations - -import numpy as np - - -class PressurizedTower: - def __init__(self, year: int, turbine: dict): - # key inputs - self.year = year - self.turbine = turbine - - self.tower_length = turbine["tower_length"] # m - self.section_diameters = turbine["section_diameters"] # m - self.section_heights = turbine["section_heights"] # m - - # calculation settings - self.setting_volume_thickness_calc = "centered" # ['centered', 'outer', 'inner'] - - # constants/parameters - self.d_t_ratio = 320.0 # Kottenstette 2003 - self.thickness_top = self.section_diameters[-1] / self.d_t_ratio # m - self.thickness_bot = self.section_diameters[0] / self.d_t_ratio # m - self.ultimate_tensile_strength = 636e6 # Pa, Kottenstette 2003 - self.yield_strength = 350e6 # Pa, Kottenstette 2003 - self.welded_joint_efficiency = 0.80 # journal edition - # self.welded_joint_efficiency= 0.85 # double-welded butt joint w/ spot inspection (ASME) - self.density_steel = 7817.0 # kg/m^3 - self.gasconstant_H2 = 4126.0 # J/(kg K) - self.operating_temp = 25.0 # degC - - self.costrate_steel = 1.50 # $/kg - self.costrate_endcap = 2.66 # $/kg - - self.costrate_ladder = 32.80 # $/m - self.cost_door = 2000 # $ - self.cost_mainframe_extension = 6300 # $ - self.cost_nozzles_manway = 16000 # $ - self.costrate_conduit = 35 # $/m - - # based on pressure_vessel maintenance costs - self.wage = 36 # 2003 dollars (per hour worked) - self.staff_hours = 60 # hours - self.maintenance_rate = 0.03 # factor - - # set the operating pressure @ the crossover pressure - self.operating_pressure = PressurizedTower.get_crossover_pressure( - self.welded_joint_efficiency, self.ultimate_tensile_strength, self.d_t_ratio - ) - - def run(self): - # get the inner volume and traditional material volume, mass, cost - self.tower_inner_volume = self.get_volume_tower_inner() - ( - self.wall_material_volume_trad, - self.cap_bot_material_volume_trad, - self.cap_top_material_volume_trad, - ) = self.get_volume_tower_material(pressure=0.0) - self.wall_material_mass_trad = self.wall_material_volume_trad * self.density_steel - self.wall_material_cost_trad = self.wall_material_mass_trad * self.costrate_steel - self.cap_material_mass_trad = ( - self.cap_top_material_volume_trad + self.cap_bot_material_volume_trad - ) * self.density_steel - self.cap_material_cost_trad = self.cap_material_mass_trad * self.costrate_steel - self.nonwall_cost_trad = self.get_cost_nontower(traditional=True) - - ( - self.wall_material_volume, - self.cap_bot_material_volume, - self.cap_top_material_volume, - ) = self.get_volume_tower_material() - self.wall_material_mass = self.wall_material_volume * self.density_steel - self.wall_material_cost = self.wall_material_mass * self.costrate_steel - self.wall_material_mass = self.wall_material_volume * self.density_steel - self.cap_material_mass = ( - self.cap_bot_material_volume + self.cap_top_material_volume - ) * self.density_steel - self.cap_material_cost = self.cap_material_mass * self.costrate_endcap - self.nonwall_cost = self.get_cost_nontower() - - if False: - # print the inner volume and pressure-free material properties - print("operating pressure:", self.operating_pressure) - print("tower inner volume:", self.tower_inner_volume) - print() - print( - "tower wall material volume (non-pressurized):", - self.wall_material_volume_trad, - ) - print( - "tower wall material mass (non-pressurized):", - self.wall_material_mass_trad, - ) - print( - "tower wall material cost (non-pressurized):", - self.wall_material_cost_trad, - ) - print( - "tower cap material volume (non-pressurized):", - self.cap_top_material_volume_trad + self.cap_bot_material_volume_trad, - ) - print( - "tower cap material mass (non-pressurized):", - self.cap_material_mass_trad, - ) - print( - "tower cap material cost (non-pressurized):", - self.cap_material_cost_trad, - ) - print( - "tower total material cost (non-pressurized):", - self.wall_material_cost_trad + self.cap_material_cost_trad, - ) - - # print the changes to the structure - print() - print("tower wall material volume (pressurized):", self.wall_material_volume) - print("tower wall material mass (pressurized):", self.wall_material_mass) - print("tower wall material cost (pressurized):", self.wall_material_cost) - print() - print( - "tower cap material volume (pressurized):", - self.cap_bot_material_volume + self.cap_top_material_volume, - ) - print("tower cap material mass (pressurized):", self.cap_material_mass) - print( - "tower top cap material cost (pressurized):", - self.cap_top_material_volume * self.density_steel * self.costrate_endcap, - ) - print( - "tower bot cap material cost (pressurized):", - self.cap_bot_material_volume * self.density_steel * self.costrate_endcap, - ) - print("tower cap material cost (pressurized):", self.cap_material_cost) - print() - print("operating mass fraction:", self.get_operational_mass_fraction()) - print("nonwall cost (non-pressurized):", self.nonwall_cost_trad) - print("nonwall cost (pressurized):", self.nonwall_cost) - print( - "tower total material cost (pressurized):", - self.wall_material_cost + self.cap_material_cost, - ) - - print() - print( - "delta tower wall material cost:", - self.wall_material_cost - self.wall_material_cost_trad, - ) - print("empty mass:", self.get_mass_empty()) - print() - print("capex:", self.get_capex()) - print("opex:", self.get_opex()) - print("capacity (H2):", self.get_capacity_H2()) - - def get_volume_tower_inner(self): - """ - get the inner volume of the tower in m^3 - - assume t << d - """ - - # count the sections, all assumed conic frustum - Nsection = len(self.section_diameters) - 1 - - # loop over sections, calclulating volume of each - vol_section = np.zeros((Nsection,)) - for i_section in range(Nsection): - diameter_bot = self.section_diameters[i_section] # m - height_bot = self.section_heights[i_section] # m - diameter_top = self.section_diameters[i_section + 1] # m - height_top = self.section_heights[i_section + 1] # m - dh = np.abs(height_top - height_bot) # height of section, m - - vol_section[i_section] = PressurizedTower.compute_frustum_volume( - dh, diameter_bot, diameter_top - ) - - # total volume: sum of sections - return np.sum(vol_section) # m^3 - - def get_volume_tower_material(self, pressure: float | None = None): - """ - get the material volume of the tower in m^3 - - if pressurized, use pressure to set thickness increment due to pressurization - - assume t << d - - params: - - pressure: gauge pressure of H2 (defaults to design op. pressure) - returns: - - Vmat_wall: material volume of vertical tower - - Vmat_bot: material volume of bottom cap (nonzero only if pressurized) - - Vmat_top: material volume of top cap (nonzero only if pressurized) - """ - - # override pressure iff requested - if pressure is None: - pressure = self.operating_pressure - - # this is the linear constant s.t. delta t ~ alpha * d - alpha_dtp = PressurizedTower.get_thickness_increment_const( - pressure, self.ultimate_tensile_strength - ) # - - # loop over the sections of the tower - Nsection = len(self.section_diameters) - 1 - matvol_section = np.zeros((Nsection,)) - for i_section in range(Nsection): - d1 = self.section_diameters[i_section] - h1 = self.section_heights[i_section] - d2 = self.section_diameters[i_section + 1] - h2 = self.section_heights[i_section + 1] - - if self.setting_volume_thickness_calc == "centered": - Vouter = PressurizedTower.compute_frustum_volume( - h2 - h1, - d1 * (1 + (1 / self.d_t_ratio + alpha_dtp)), - d2 * (1 + (1 / self.d_t_ratio + alpha_dtp)), - ) - Vinner = PressurizedTower.compute_frustum_volume( - h2 - h1, - d1 * (1 - (1 / self.d_t_ratio + alpha_dtp)), - d2 * (1 - (1 / self.d_t_ratio + alpha_dtp)), - ) - elif self.setting_volume_thickness_calc == "outer": - Vouter = PressurizedTower.compute_frustum_volume( - h2 - h1, - d1 * (1 + 2 * (1 / self.d_t_ratio + alpha_dtp)), - d2 * (1 + 2 * (1 / self.d_t_ratio + alpha_dtp)), - ) - Vinner = PressurizedTower.compute_frustum_volume(h2 - h1, d1, d2) - elif self.setting_volume_thickness_calc == "inner": - Vouter = PressurizedTower.compute_frustum_volume(h2 - h1, d1, d2) - Vinner = PressurizedTower.compute_frustum_volume( - h2 - h1, - d1 * (1 - 2 * (1 / self.d_t_ratio + alpha_dtp)), - d2 * (1 - 2 * (1 / self.d_t_ratio + alpha_dtp)), - ) - - matvol_section[i_section] = Vouter - Vinner - - # compute wall volume by summing sections - Vmat_wall = np.sum(matvol_section) # m^3 - - if pressure == 0: - Vmat_bot = 0.0 - Vmat_top = 0.0 - else: - # compute caps as well: area by thickness - Vmat_bot = (np.pi / 4 * self.section_diameters[0] ** 2) * ( - PressurizedTower.compute_cap_thickness( - pressure, - self.section_diameters[0], # assume first is bottom - self.yield_strength, - efficiency_weld=self.welded_joint_efficiency, - ) - ) # m^3 - Vmat_top = ( - np.pi - / 4 - * self.section_diameters[-1] ** 2 - * ( - PressurizedTower.compute_cap_thickness( - pressure, - self.section_diameters[-1], # assume last is top - self.yield_strength, - efficiency_weld=self.welded_joint_efficiency, - ) - ) - ) # m^3 - - # total material volume - return (Vmat_wall, Vmat_bot, Vmat_top) # m^3 - - def get_mass_tower_material(self, pressure: float | None = None): - """ - get the material mass of the tower in m^3 - - if pressurized, use pressure to set thickness increment due to pressurization - - assume t << d - - params: - - pressure: gauge pressure of H2 (defaults to design op. pressure) - returns: - - Mmat_wall: material mass of vertical tower - - Mmat_bot: material mass of bottom cap - - Mmat_top: material mass of top cap - """ - - # pass through to volume calculator, multiplying by steel density - return [self.density_steel * x for x in self.get_volume_tower_material(pressure)] # kg - - def get_cost_tower_material(self, pressure: float | None = None): - """ - get the material cost of the tower in m^3 - - if pressurized, use pressure to set thickness increment due to pressurization - - assume t << d - - params: - - pressure: gauge pressure of H2 (defaults to design op. pressure) - returns: - - Vmat_wall: material cost of vertical tower - - Vmat_bot: material cost of bottom cap - - Vmat_top: material cost of top cap - """ - - if pressure == 0: - return [ - self.costrate_steel * x for x in self.get_mass_tower_material(pressure=pressure) - ] # 2003 dollars - else: - Mmat_wall, Mmat_bot, Mmat_top = self.get_mass_tower_material(pressure=pressure) - # use adjusted pressure cap cost - return [ - self.costrate_steel * Mmat_wall, - self.costrate_endcap * Mmat_bot, - self.costrate_endcap * Mmat_top, - ] # 2003 dollars - - def get_operational_mass_fraction(self): - """ - get the fraction of stored hydrogen to tower mass - - following Kottenstette - """ - - Sut = self.ultimate_tensile_strength - rho = self.density_steel - R = self.gasconstant_H2 - T = self.operating_temp + 273.15 # convert to K - - frac = Sut / (rho * R * T) - - return frac # nondim. - - def get_cost_nontower(self, traditional: bool = False, naive: bool = True): - nonwall_cost = 0 - if traditional: - nonwall_cost += self.tower_length * self.costrate_ladder # add ladder cost - nonwall_cost += self.cost_door # add door cost - else: - naive = True - if naive: - nonwall_cost += self.cost_mainframe_extension - nonwall_cost += 2 * self.cost_door - nonwall_cost += 2 * self.tower_length * self.costrate_ladder - nonwall_cost += self.cost_nozzles_manway - nonwall_cost += self.tower_length * self.costrate_conduit - else: - # adjust length b.c. conduit, one ladder must ride outside pressure vessel - adj_length = np.sqrt( - (self.section_diameters[0] - self.section_diameters[-1]) ** 2 - + self.tower_length**2 - ) - nonwall_cost += self.cost_mainframe_extension - nonwall_cost += 2 * self.cost_door - nonwall_cost += self.tower_length * self.costrate_ladder - nonwall_cost += adj_length * self.costrate_ladder - nonwall_cost += self.cost_nozzles_manway - nonwall_cost += adj_length * self.costrate_conduit - return nonwall_cost # 2003 dollars - - ### OFFICIAL OUTPUT INTERFACE - - def get_capex(self): - """return the total additional capex necessary for H2 production""" - capex_withH2 = self.get_cost_nontower() + np.sum(self.get_cost_tower_material()) - capex_without = self.get_cost_nontower(traditional=True) + np.sum( - self.get_cost_tower_material(pressure=0) - ) - return capex_withH2 - capex_without # 2003 dollars - - def get_opex(self): - """ - a simple model for operational expenditures for PV - - maintenance for pressure vessel based on an annual maintenance rate - against the vessel-specific capital expenditure, plus wages times staff - hours per year - """ - - return ( - self.get_capex() * self.maintenance_rate + self.wage * self.staff_hours - ) # 2003 dollars - - def get_mass_empty(self): - """return the total additional empty mass necessary for H2 production in kg""" - - Mtower_withH2 = np.sum(self.get_mass_tower_material()) - Mnontower_withH2 = 0.0 # not specified - - Mtower_without = np.sum(self.get_mass_tower_material(pressure=0)) - Mnontower_without = 0.0 # not specified - - Mtotal_withH2 = Mtower_withH2 + Mnontower_withH2 - Mtotal_without = Mtower_without + Mnontower_without - - return Mtotal_withH2 - Mtotal_without # kg - - def get_capacity_H2(self): - """get the ideal gas H2 capacity in kg""" - - Tabs = self.operating_temp + 273.15 - R = self.gasconstant_H2 - p = self.operating_pressure - V = self.get_volume_tower_inner() - - # ideal gas law - m_H2 = p * V / (R * Tabs) - - return m_H2 # kg - - def get_pressure_H2(self): - return self.operating_pressure # Pa, trivial, but for delivery - - ### STATIC FUNCTIONS - - @staticmethod - def compute_cap_thickness( - pressure, - diameter, - strength_yield, - safetyfactor_Sy=1.5, - efficiency_weld=0.80, - constant=0.10, - ): - """ - compute the necessary thickness for a pressure vessel cap - - $$ - t= d \\sqrt{\\frac{C P}{S E}} - $$ - with weld joint efficiency E, allowable stress S, pressure P, diameter - of pressure action d, edge restraint factor C - - assumed: - - C= 0.10: Fig-UG-34 of ASME Code S VII, div. 1, via Rao's _Companion - Guide to the ASME Boiler and Pressure Vessel Code_ (2009), - fig. 21.3. type of sketch (a) assumed - - E= 0.80: conservatively butt weld, inspected - - using the ASME pressure vessel code definitions, and values given in - Rao _Companion Guide to the ASME Boiler and Pressure Vessel Code_ (2009) - """ - - return diameter * np.sqrt( - constant * pressure / (efficiency_weld * strength_yield / safetyfactor_Sy) - ) - - @staticmethod - def compute_frustum_volume(height, base_diameter, top_diameter): - """ - return the volume of a frustum (truncated cone) - """ - return ( - np.pi - / 12.0 - * height - * (base_diameter**2 + base_diameter * top_diameter + top_diameter**2) - ) # volume units - - @staticmethod - def get_crossover_pressure( - welded_joint_efficiency: float, - ultimate_tensile_strength: float, - d_t_ratio: float, - ): - """ - get burst/fatigue crossover pressure - - following Kottenstette 2003 - """ - - # convert to nice variables - E = welded_joint_efficiency # nondim. - Sut = ultimate_tensile_strength # pressure units - d_over_t = d_t_ratio # length-per-length; assumed fixed in this study - - p_crossover = 4 * E * Sut / (7 * d_over_t * (1 - E / 7.0)) # pressure units - - return p_crossover # pressure units - - @staticmethod - def get_thickness_increment_const(pressure: float, ultimate_tensile_strength: float): - """ - compute Goodman equation-based thickness increment in m - - following Kottenstette 2003 - """ - - # convert to text variables - p = pressure - # r= diameter/2 - Sut = ultimate_tensile_strength - - alpha_dtp = 0.25 * p / Sut - - return alpha_dtp # length per length diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pipe_storage/__init__.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/pipe_storage/__init__.py deleted file mode 100644 index 4d3cf776e..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pipe_storage/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from h2integrate.simulation.technologies.hydrogen.h2_storage.pipe_storage.underground_pipe_storage import ( - UndergroundPipeStorage, -) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pipe_storage/underground_pipe_storage.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/pipe_storage/underground_pipe_storage.py deleted file mode 100644 index 5d9597f59..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pipe_storage/underground_pipe_storage.py +++ /dev/null @@ -1,177 +0,0 @@ -""" -Author: Kaitlin Brunik -Updated: 7/20/2023 -Institution: National Renewable Energy Lab -Description: This file outputs capital and operational costs of underground pipeline hydrogen -storage. It needs to be updated to with operational dynamics and physical size (footprint and mass). -Oversize pipe: pipe OD = 24'' schedule 60 [1] -Max pressure: 100 bar -Costs are in 2018 USD -Sources: - - [1] Papadias 2021: https://www.sciencedirect.com/science/article/pii/S0360319921030834?via%3Dihub - - [2] Papadias 2021: Bulk Hydrogen as Function of Capacity.docx documentation at - hopp/hydrogen/h2_storage - - [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet -""" - -import numpy as np - -from h2integrate.simulation.technologies.hydrogen.h2_transport.h2_compression import Compressor - - -class UndergroundPipeStorage: - """ - - Oversize pipe: pipe OD = 24'' schedule 60 - - Max pressure: 100 bar - - Costs are in 2018 USD - """ - - def __init__(self, input_dict): - """ - Initialize UndergroundPipeStorage. - - Args: - input_dict (dict): - - h2_storage_kg (float): total capacity of hydrogen storage [kg] - - storage_duration_hrs (float): (optional if h2_storage_kg set) [hrs] - - flow_rate_kg_hr (float): (optional if h2_storage_kg set) [kg/hr] - - compressor_output_pressure (float): 100 bar required [bar] - - system_flow_rate (float): [kg/day] - - labor_rate (float): (optional, default: 37.40) [$2018/hr] - - insurance (float): (optional, default: 1%) [decimal percent] - - property_taxes (float): (optional, default: 1%) [decimal percent] - - licensing_permits (float): (optional, default: 0.01%) [decimal percent] - Returns: - - pipe_storage_capex_per_kg (float): the installed capital cost per kg h2 in 2018 - [USD/kg] - - installed_capex (float): the installed capital cost in 2018 [USD] (including - compressor) - - storage_compressor_capex (float): the installed capital cost in 2018 for the - compressor [USD] - - total_opex (float): the OPEX (annual, fixed) in 2018 excluding electricity costs - [USD/kg-yr] - - output_dict (dict): - - pipe_storage_capex (float): installed capital cost in 2018 [USD] - - pipe_storage_opex (float): OPEX (annual, fixed) in 2018 [USD/yr] - """ - self.input_dict = input_dict - self.output_dict = {} - """""" - # inputs - if input_dict["compressor_output_pressure"] == 100: - self.compressor_output_pressure = input_dict["compressor_output_pressure"] # [bar] - else: - raise Exception( - "Error. compressor_output_pressure must = 100bar for pressure vessel storage." - ) - if "h2_storage_kg" in input_dict: - self.h2_storage_kg = input_dict["h2_storage_kg"] # [kg] - elif "storage_duration_hrs" and "flow_rate_kg_hr" in input_dict: - self.h2_storage_kg = input_dict["storage_duration_hrs"] * input_dict["flow_rate_kg_hr"] - else: - raise Exception( - "input_dict must contain h2_storage_kg or storage_duration_hrs and flow_rate_kg_hr" - ) - - if "system_flow_rate" not in input_dict.keys(): - raise ValueError("system_flow_rate required for underground pipe storage model.") - else: - self.system_flow_rate = input_dict["system_flow_rate"] - - self.labor_rate = input_dict.get("labor_rate", 37.39817) # $(2018)/hr - self.insurance = input_dict.get("insurance", 1 / 100) # % of total capital investment - self.property_taxes = input_dict.get( - "property_taxes", 1 / 100 - ) # % of total capital investment - self.licensing_permits = input_dict.get( - "licensing_permits", 0.1 / 100 - ) # % of total capital investment - self.comp_om = input_dict.get( - "compressor_om", 4 / 100 - ) # % of compressor capital investment - self.facility_om = input_dict.get( - "facility_om", 1 / 100 - ) # % of facility capital investment minus compressor capital investment - - def pipe_storage_capex(self): - """ - Calculates the installed capital cost of underground pipe hydrogen storage - Returns: - - pipe_storage_capex_per_kg (float): the installed capital cost per kg h2 in 2018 - [USD/kg] - - installed_capex (float): the installed capital cost in 2018 [USD] (including - compressor) - - storage_compressor_capex (float): the installed capital cost in 2018 for the - compressor [USD] - - output_dict (dict): - - pipe_storage_capex (float): installed capital cost in 2018 [USD] - """ - - # Installed capital cost - a = 0.0041617 - b = 0.060369 - c = 6.4581 - self.pipe_storage_capex_per_kg = np.exp( - a * (np.log(self.h2_storage_kg / 1000)) ** 2 - b * np.log(self.h2_storage_kg / 1000) + c - ) # 2019 [USD] from Papadias [2] - self.installed_capex = self.pipe_storage_capex_per_kg * self.h2_storage_kg - cepci_overall = 1.29 / 1.30 # Convert from $2019 to $2018 - self.installed_capex = cepci_overall * self.installed_capex - self.output_dict["pipe_storage_capex"] = self.installed_capex - - outlet_pressure = ( - self.compressor_output_pressure - ) # Max outlet pressure of underground pipe storage [1] - n_compressors = 2 - storage_compressor = Compressor( - outlet_pressure, self.system_flow_rate, n_compressors=n_compressors - ) - storage_compressor.compressor_power() - motor_rating, power = storage_compressor.compressor_system_power() - if motor_rating > 1600: - n_compressors += 1 - storage_compressor = Compressor( - outlet_pressure, self.system_flow_rate, n_compressors=n_compressors - ) - storage_compressor.compressor_power() - motor_rating, power = storage_compressor.compressor_system_power() - comp_capex, comp_OM = storage_compressor.compressor_costs() - cepci = 1.36 / 1.29 # convert from $2016 to $2018 - self.comp_capex = comp_capex * cepci - - return self.pipe_storage_capex_per_kg, self.installed_capex, self.comp_capex - - def pipe_storage_opex(self): - """ - Calculates the operation and maintenance costs excluding electricity costs for the - underground pipe hydrogen storage - - Returns: - - total_opex (float): the OPEX (annual, fixed) in 2018 excluding electricity costs - [USD/kg-yr] - - output_dict (dict): - - pipe_storage_opex (float): OPEX (annual, fixed) in 2018 [USD/yr] - """ - # Operations and Maintenace costs [3] - # Labor - # Base case is 1 operator, 24 hours a day, 7 days a week for a 100,000 kg/day average - # capacity facility. Scaling factor of 0.25 is used for other sized facilities - annual_hours = 8760 * (self.system_flow_rate / 100000) ** 0.25 - self.overhead = 0.5 - labor = (annual_hours * self.labor_rate) * (1 + self.overhead) # Burdened labor cost - insurance = self.insurance * self.installed_capex - property_taxes = self.property_taxes * self.installed_capex - licensing_permits = self.licensing_permits * self.installed_capex - comp_op_maint = self.comp_om * self.comp_capex - facility_op_maint = self.facility_om * (self.installed_capex - self.comp_capex) - - # O&M excludes electricity requirements - total_om = ( - labor - + insurance - + licensing_permits - + property_taxes - + comp_op_maint - + facility_op_maint - ) - self.output_dict["pipe_storage_opex"] = total_om - return total_om diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/__init__.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/__init__.py deleted file mode 100644 index 787241b9c..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from h2integrate.simulation.technologies.hydrogen.h2_storage.pressure_vessel.tankinator import ( - LinedTank, - MetalMaterial, - Tank, - TypeIIITank, - TypeITank, - TypeIVTank, -) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Compressed_all.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Compressed_all.py deleted file mode 100644 index 95c0f39ce..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Compressed_all.py +++ /dev/null @@ -1,255 +0,0 @@ -from __future__ import annotations - - -""" -Created on Mon Oct 17 20:08:09 2022 -@author: ppeng - -Revisions: -- 20221118: - Author: Jared J. Thomas - Description: - - Reformatted to be a class -""" - -""" -Model Revision Needed: storage space and mass -Description: This file should handle physical size (footprint and mass) needed for pressure vessel - storage -Sources: - - [1] ./README.md and other elements in this directory -Args: - - same as for the physics and cost model contained herein - - others may be added as needed -Returns:(can be from separate functions and/or methods as it makes sense): - - mass_empty (float): mass (approximate) for pressure vessel storage components ignoring stored - H2 - - footprint (float): area required for pressure vessel storage - - others may be added as needed -""" - - -from pathlib import Path - -import numpy as np - -from .Compressed_gas_function import CompressedGasFunction - - -class PressureVessel: - def __init__( - self, - Wind_avai=80, - H2_flow=200, - cdratio=1, - Energy_cost=0.07, - cycle_number=1, - parent_path=Path(__file__).parent, - spread_sheet_name="Tankinator.xlsx", - verbose=False, - ): - ########Key inputs########## - self.Wind_avai = Wind_avai # Wind availability in % - self.H2_flow = H2_flow # Flow rate of steel plants in metric ton/day - - # NOTE: Charge/discharge ratio, i.e. 2 means the charging is 2x faster than discharge - self.cdratio = cdratio - self.Energy_cost = Energy_cost # Renewable energy cost in $/kWh - - #######Other inputs######## - # NOTE: Equivalent cycle number for a year, only affects operation (the higher the number - # is the less effect there will be), set as now as I am not sure how the maximum storage - # capacity is determined and how the storage will be cycled - self.cycle_number = cycle_number - - _fn = parent_path / spread_sheet_name - self.compressed_gas_function = CompressedGasFunction(path_tankinator=_fn) - self.compressed_gas_function.verbose = verbose - - def run(self): - #####Run calculation######## - self.compressed_gas_function.func( - Wind_avai=self.Wind_avai, - H2_flow=self.H2_flow, - cdratio=self.cdratio, - Energy_cost=self.Energy_cost, - cycle_number=self.cycle_number, - ) - - ########Outputs################ - - ######Maximum equivalent storage capacity and duration - self.capacity_max = ( - self.compressed_gas_function.capacity_max - ) # This is the maximum equivalent H2 storage in kg - self.t_discharge_hr_max = ( - self.compressed_gas_function.t_discharge_hr_max - ) # This is tha maximum storage duration in kg - - ###Parameters for capital cost fitting for optimizing capital cost - self.a_fit_capex = self.compressed_gas_function.a_cap_fit - self.b_fit_capex = self.compressed_gas_function.b_cap_fit - self.c_fit_capex = self.compressed_gas_function.c_cap_fit - - # Parameters for operational cost fitting for optimizing capital cost - self.a_fit_opex = self.compressed_gas_function.a_op_fit - self.b_fit_opex = self.compressed_gas_function.b_op_fit - self.c_fit_opex = self.compressed_gas_function.c_op_fit - - def calculate_from_fit(self, capacity_kg): - capex_per_kg = self.compressed_gas_function.exp_log_fit( - [self.a_fit_capex, self.b_fit_capex, self.c_fit_capex], capacity_kg - ) - opex_per_kg = self.compressed_gas_function.exp_log_fit( - [self.a_fit_opex, self.b_fit_opex, self.c_fit_opex], capacity_kg - ) - energy_per_kg_h2 = self.compressed_gas_function.energy_function(capacity_kg) / capacity_kg - - # NOTE ON ENERGY: the energy value returned here is the energy used to fill the - # tanks initially for the first fill and so can be used as an approximation for the energy - # used on a per kg basis. - # If cycle_number > 1, the energy model output is incorrect. - - capex = capex_per_kg * capacity_kg - opex = opex_per_kg * capacity_kg - return capex, opex, energy_per_kg_h2 - - def get_tanks(self, capacity_kg): - """gets the number of tanks necessary""" - return np.ceil(capacity_kg / self.compressed_gas_function.m_H2_tank) - - def get_tank_footprint( - self, - capacity_kg, - upright: bool = True, - custom_packing: bool = False, - packing_ratio: float | None = None, - ): - """ - gets the footprint required for the H2 tanks - - assumes that packing is square (unless custom_packing is true) - - diameter D upright tank occupies D^2 - - diameter D, length L tank occupies D*L - - parameters: - - `upright`: place tanks vertically (default yes)? - - `custom_packing`: pack tanks at an alternate packing fraction? - - `packing_ratio`: ratio for custom packing, defaults to theoretical max (if known) - returns: - - `tank_footprint`: footprint of each tank in m^2 - - `array_footprint`: total footprint of all tanks in m^2 - """ - - tank_radius = self.compressed_gas_function.Router / 100 - tank_length = self.compressed_gas_function.Louter / 100 - Ntank = self.get_tanks(capacity_kg=capacity_kg) - - if upright: - tank_area = np.pi * tank_radius**2 - tank_footprint = 4 * tank_radius**2 - else: - tank_area = ( - np.pi * tank_radius**2 * ((tank_length - 2 * tank_radius) * (2 * tank_radius)) - ) - tank_footprint = tank_radius * tank_length - - if custom_packing: - if upright: - if packing_ratio is None: - packing_ratio = np.pi * np.sqrt(3.0) / 6.0 # default to tight packing - tank_footprint = tank_area * packing_ratio - else: - if packing_ratio is None: - raise NotImplementedError("tight packing ratio for cylinders isn't derived yet") - tank_footprint = tank_area * packing_ratio - - return (tank_footprint, Ntank * tank_footprint) - - def get_tank_mass(self, capacity_kg): - """ - gets the mass required for the H2 tanks - - returns - - `tank_mass`: mass of each tank - - `array_mass`: total mass of all tanks - """ - - tank_mass = self.compressed_gas_function.Mempty_tank - Ntank = self.get_tanks(capacity_kg=capacity_kg) - - return (tank_mass, Ntank * tank_mass) - - def plot(self): - self.compressed_gas_function.plot() - - def distributed_storage_vessels(self, capacity_total_tgt, N_sites): - """ - compute modified pressure vessel storage requirements for distributed - pressure vessels - - parameters: - - capacity_total_tgt: target gaseous H2 capacity in kilograms - - N_sites: number of sites (e.g. turbines) where pressure vessels will be placed - - returns: - - - """ - - # assume that the total target capacity is equally distributed across sites - capacity_site_tgt = capacity_total_tgt / N_sites - - # capex_centralized_total, opex_centralized_total, energy_kg_centralized_total= self.calculate_from_fit(capacity_total_tgt) # noqa: E501 - capex_site, opex_site, energy_kg_site = self.calculate_from_fit(capacity_site_tgt) - - # get the resulting capex & opex costs, incl. equivalent - capex_distributed_total = ( - N_sites * capex_site - ) # the cost for the total distributed storage facilities - opex_distributed_total = ( - N_sites * opex_site - ) # the cost for the total distributed storage facilities - - # get footprint stuff - area_footprint_site = self.get_tank_footprint(capacity_site_tgt)[1] - mass_tank_empty_site = self.get_tank_mass(capacity_site_tgt)[1] - - # return the outputs - return ( - capex_distributed_total, - opex_distributed_total, - energy_kg_site, - area_footprint_site, - mass_tank_empty_site, - capacity_site_tgt, - ) - - -if __name__ == "__main__": - storage = PressureVessel() - storage.run() - - capacity_req = 1e3 - print("tank type:", storage.compressed_gas_function.tank_type) - print("tank mass:", storage.get_tank_mass(capacity_req)[0]) - print("tank radius:", storage.compressed_gas_function.Router) - print("tank length:", storage.compressed_gas_function.Louter) - print( - "tank footprint (upright):", - storage.get_tank_footprint(capacity_req, upright=True)[0], - ) - print( - "tank footprint (flat):", - storage.get_tank_footprint(capacity_req, upright=False)[0], - ) - - print("\nnumber of tanks req'd:", storage.get_tanks(capacity_req)) - print( - "total footprint (upright):", - storage.get_tank_footprint(capacity_req, upright=True)[1], - ) - print( - "total footprint (flat):", - storage.get_tank_footprint(capacity_req, upright=False)[1], - ) - print("total mass:", storage.get_tank_mass(capacity_req)[1]) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Compressed_gas_function.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Compressed_gas_function.py deleted file mode 100644 index e4f95a4ee..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Compressed_gas_function.py +++ /dev/null @@ -1,791 +0,0 @@ -""" -Created on Fri Jan 15 15:06:21 2021 - -@author: ppeng -""" - -import math as math - -import numpy as np -import openpyxl as openpyxl -import matplotlib.pyplot as plt -from scipy.optimize import leastsq -from CoolProp.CoolProp import PropsSI - - -plt.rcParams.update({"font.size": 13}) - - -class CompressedGasFunction: - def __init__(self, path_tankinator): - # path to the excel spreadsheet to store material properties - self.wb_tankinator = openpyxl.load_workbook( - path_tankinator, data_only=True - ) # Add file name - - ################Other key inputs besides the main script########################## - self.MW_H2 = 2.02e-03 # molecular weight of H2 in kg/mol - - # NOTE: Important, if you change storage pressure, make sure to change it in the - # corresponding tab in Tankinator and save again - self.Pres = 350 # Define storage pressure in bar - self.Temp_c = 293 # Define storage temperature in K - self.Pin = 30 # Deinfe pressure out of electrolyzer in bar - self.Tin = 353 # Define temperature out of electrolyzer in K - self.T_amb = 295 - self.Pres3 = 35 # Define outlet pressure in bar - self.Temp3 = 353 # Define outlet temperature in K - - self.start_point = 10 # For setting the smallest capacity for fitting and plotting - - #################Economic parameters - self.CEPCI2007 = 525.4 - self.CEPCI2001 = 397 - self.CEPCI2017 = 567.5 - - self.CEPCI_current = 708 ####Change this value for current CEPCI - - self.wage = 36 - self.maintanance = 0.03 - self.Site_preparation = 100 # Site preparation in $/kg - - self.Tank_manufacturing = 1.8 # self.Markup for tank manufacturing - self.Markup = 1.5 # self.Markup for installation engineering/contingency - - #################Other minor input parameters############ - self.R = 8.314 # gas onstant m3*Pa/(molK) - self.Heat_Capacity_Wall = ( - 0.92 ##wall heat capacity at 298 K in kJ/kg*K for carbon fiber composite - ) - self.Efficiency_comp = 0.7 # Compressor efficiency - self.Efficiency_heater = 0.7 # Heat efficiency - - def exp_log_fit(self, var_op, capacity_1): - a_op = var_op[0] - b_op = var_op[1] - c_op = var_op[2] - - fit_op_kg = np.exp(a_op * (np.log(capacity_1)) ** 2 - b_op * np.log(capacity_1) + c_op) - - return fit_op_kg - - def residual_op(self, var_op, capacity_1, Op_c_Costs_kg): - fit_op_kg = self.exp_log_fit(var_op, capacity_1) - - return fit_op_kg - Op_c_Costs_kg - - def exp_fit(self, x, a, b): - return a * x**b - - def calculate_max_storage_capacity(self, Wind_avai, H2_flow, Release_efficiency): - # reference flow rate of steel plants in metric ton/day, in case in the future it is not - # 200 metric ton/day - H2_flow_ref = 200 - - capacity_max = ( - (0.8044 * Wind_avai**2 - 57.557 * Wind_avai + 4483.1) - * (H2_flow / H2_flow_ref) - / Release_efficiency - * 1000 - ) ###Total max equivalent storage capacity kg - - return capacity_max - - def calculate_max_storage_duration(self, Release_efficiency, H2_flow): - t_discharge_hr_max = ( - self.capacity_max / 1000 * Release_efficiency / H2_flow - ) ###This is the theoretical maximum storage duration - - return t_discharge_hr_max - - # TODO keep breaking this up so we can run the model without running the curve fit - def func( - self, - Wind_avai, - H2_flow, - cdratio, - Energy_cost, - cycle_number, - capacity_max_spec=None, - t_discharge_hr_max_spec=None, - ): - """ - Run the compressor and storage container cost models - - Wind_avai is only used for calculating the theoretical maximum storage capacity prior to - curve fitting - - H2_flow is (I think) the rate the H2 is being removed from the tank in metric ton/day - - cdratio is the charge/discharge ratio (1 means charge rate equals the discharge rate, 2 - means charge is 2x the discharge rate) - - Energy_cost is the renewable energy cost in $/kWh, or can be set to 0 to exclude energy - costs - - cycle number should just be left as 1 (see compressed_all.py) - """ - - ##############Calculation of storage capacity from duration############# - if 1 - self.Pres3 / self.Pres < 0.9: - Release_efficiency = 1 - self.Pres3 / self.Pres - else: - Release_efficiency = 0.9 - - if capacity_max_spec is None: - self.capacity_max = self.calculate_max_storage_capacity( - Wind_avai, H2_flow, Release_efficiency - ) - else: - self.capacity_max = capacity_max_spec - - if t_discharge_hr_max_spec is None: - self.t_discharge_hr_max = self.calculate_max_storage_duration( - Release_efficiency, H2_flow - ) - else: - self.t_discharge_hr_max = t_discharge_hr_max_spec - - if self.verbose: - print("Maximum capacity is", self.capacity_max, "kg H2") - print("Maximum storage duration is", self.t_discharge_hr_max, "hr") - - if self.Pres > 170: - ####Use this if use type IV tanks - tank_type = 4 - sheet_tankinator = self.wb_tankinator["type4_rev3"] # Add Sheet name - Vtank_c_cell = sheet_tankinator.cell(row=19, column=3) # tank internal volume in cm3 - Vtank_c = Vtank_c_cell.value / (10**6) # tank volume in m3 - m_c_wall_cell = sheet_tankinator.cell(row=55, column=3) - m_c_wall = m_c_wall_cell.value # Wall mass in kg - Mtank_c = m_c_wall # TODO why is this set but not used? - Louter_c_cell = sheet_tankinator.cell(row=36, column=3) - length_outer_c = Louter_c_cell.value # outer length of tank - Router_c_cell = sheet_tankinator.cell(row=37, column=3) - radius_outer_c = Router_c_cell.value # outer radius of tank - Cost_c_tank_cell = sheet_tankinator.cell(row=65, column=3) # Cost of one tank - Cost_c_tank = Cost_c_tank_cell.value ##Cost of the tank in $/tank - - if self.Pres <= 170: - ####Use this if use type I tanks - tank_type = 1 - sheet_tankinator = self.wb_tankinator["type1_rev3"] # Add Sheet nam - Vtank_c_cell = sheet_tankinator.cell(row=20, column=3) ##Tank's outer volume in cm^3 - Vtank_c = Vtank_c_cell.value / (10**6) # tank volume in m3 - m_c_wall_cell = sheet_tankinator.cell(row=188, column=3) - m_c_wall = m_c_wall_cell.value # Wall mass in kg - Mtank_c = m_c_wall # TODO why is this set but not used? - Louter_c_cell = sheet_tankinator.cell(row=184, column=3) - length_outer_c = Louter_c_cell.value - Router_c_cell = sheet_tankinator.cell(row=185, column=3) - radius_outer_c = Router_c_cell.value - Cost_c_tank_cell = sheet_tankinator.cell(row=193, column=3) # Cost of one tank - Cost_c_tank = Cost_c_tank_cell.value ##Cost of the tank in $/tank - - self.tank_type = tank_type - self.Vtank = Vtank_c - self.m_H2_tank = self.Vtank * PropsSI( - "D", "P", self.Pres * 10**5, "T", self.Temp_c, "Hydrogen" - ) - self.Mempty_tank = Mtank_c - self.Router = radius_outer_c - self.Louter = length_outer_c - - #####Define arrays for plotting and fitting - - self.t_discharge_hr_1 = np.linspace( - self.t_discharge_hr_max, self.t_discharge_hr_max / self.start_point, num=15 - ) - self.cost_kg = np.zeros(len(self.t_discharge_hr_1)) - cost_kg_tank = np.zeros(len(self.t_discharge_hr_1)) - cost_kg_comp = np.zeros(len(self.t_discharge_hr_1)) - cost_kg_ref = np.zeros(len(self.t_discharge_hr_1)) - cost_kg_heat = np.zeros(len(self.t_discharge_hr_1)) - self.number_of_tanks = np.zeros(len(self.t_discharge_hr_1)) - self.capacity_1 = np.zeros(len(self.t_discharge_hr_1)) - self.Op_c_Costs_kg = np.zeros(len(self.t_discharge_hr_1)) - self.total_energy_used_kwh = np.zeros(len(self.t_discharge_hr_1)) - - ########################################################################################### - ######################################################################################## - ############################################################################################ - ###############Starting detailed calculations################################# - ###############Stage 1 calculations################################# - - for i in range(0, len(self.t_discharge_hr_1 - 1)): - t_discharge_hr = self.t_discharge_hr_1[i] - capacity = ( - H2_flow * t_discharge_hr * 1000 / Release_efficiency - ) # Maximum capacity in kg H2 - - self.capacity_1[i] = capacity - - rgas = PropsSI( - "D", "P", self.Pres * 10**5, "T", self.Temp_c, "Hydrogen" - ) # h2 density in kg/m3 under storage conditions - H2_c_mass_gas_tank = Vtank_c * rgas # hydrogen mass per tank in kg - H2_c_mass_tank = H2_c_mass_gas_tank # Estimation of H2 amount per tank in kg - self.single_tank_h2_capacity_kg = H2_c_mass_tank - - number_c_of_tanks = np.ceil(capacity / H2_c_mass_tank) - self.number_of_tanks[i] = number_c_of_tanks - - # NOTE: This will be useful when changing to assume all tanks are full, but will cause - # the model to not perform well for small scales, where 1 tank makes a large difference - H2_c_Cap_Storage = H2_c_mass_tank * (number_c_of_tanks - 1) + capacity % H2_c_mass_tank - - #################Energy balance for adsorption (state 1 to state 2)######## - self.t_charge_hr = t_discharge_hr * (1 / cdratio) - - # NOTE: correcting first cycle, useful to size based on maximum power and also when - # calculating the operational cost - t_precondition_hr = self.t_charge_hr - m_c_flow_rate_1_2 = ( - H2_c_Cap_Storage / t_precondition_hr / 3600 - ) # mass flow rate in kg/s - Temp2 = self.Temp_c - Temp1_gas = self.Tin - Temp1_solid = self.T_amb - Pres2 = self.Pres * 10**5 - Pres1 = self.Pin * 10**5 - H_c_1_spec_g = ( - PropsSI("H", "P", Pres1, "T", Temp1_gas, "Hydrogen") / 1000 - ) # specific enthalpy of the gas under T1 P1 in kJ/kg - H_c_2_spec_g = ( - PropsSI("H", "P", Pres2, "T", Temp2, "Hydrogen") / 1000 - ) # specific enthalpy of the gas under T2 P2 in kJ/kg - H_c_1_gas = H2_c_Cap_Storage * H_c_1_spec_g - H_c_2_gas = H2_c_Cap_Storage * H_c_2_spec_g - deltaE_c_H2_1_2 = H_c_2_gas - H_c_1_gas - deltaE_c_Uwall_1_2 = ( - self.Heat_Capacity_Wall * (Temp2 - Temp1_solid) * m_c_wall * number_c_of_tanks - ) # Net energy/enthalpy change of adsorbent in kJ - deltaE_c_net_1_2 = ( - deltaE_c_H2_1_2 + deltaE_c_Uwall_1_2 - ) # Net energy/enthalpy change in kJ - deltaP_c_net_1_2 = deltaE_c_net_1_2 / self.t_charge_hr / 3600 # Net power change in kW - - #################Energy balance for desorption (state 2 to state 3)######## - Temp3_gas = self.Temp3 - Temp3_solid = Temp2 - self.Pres3 = self.Pres3 - Pres3_tank = self.Pres * (1 - Release_efficiency) * 10**5 * 10 - H_c_3_spec_g_fuel_cell = ( - PropsSI("H", "P", self.Pres3, "T", Temp3_gas, "Hydrogen") / 1000 - ) # specific enthalpy of the released gas in kJ/kg - H_c_3_spec_g_tank = ( - PropsSI("H", "P", Pres3_tank, "T", Temp2, "Hydrogen") / 1000 - ) # specific enthalpy of the remaining free volume gas in kJ/kg - H_c_3_gas = ( - H2_c_Cap_Storage * Release_efficiency * H_c_3_spec_g_fuel_cell - + H2_c_Cap_Storage * (1 - Release_efficiency) * H_c_3_spec_g_tank - ) # Total gas phase enthalpy in stage 3 in kJ - deltaE_c_H2_2_3 = H_c_3_gas - H_c_2_gas # Total h2 enthalpy change in kJ - deltaE_c_Uwall_2_3 = ( - self.Heat_Capacity_Wall * (Temp3_solid - Temp2) * m_c_wall * number_c_of_tanks - ) # kJ - deltaE_c_net_2_3 = ( - deltaE_c_H2_2_3 + deltaE_c_Uwall_2_3 - ) # Net enthalpy change during desorption - detlaP_c_net_2_3 = deltaE_c_net_2_3 / t_discharge_hr / 3600 - - ###############Energy balance for adsorption (state 4 to state 2)########## - m_c_flow_rate_4_2 = H2_c_Cap_Storage * Release_efficiency / self.t_charge_hr / 3600 - Temp4_tank = Temp2 - Pres4_tank = Pres3_tank - H_c_4_spec_g_electrolyzer = ( - PropsSI("H", "P", self.Pin, "T", self.Tin, "Hydrogen") / 1000 - ) # specific enthalpy of the released gas in kJ/kg - H_c_4_spec_g_tank = ( - PropsSI("H", "P", Pres4_tank, "T", Temp2 - 5, "Hydrogen") / 1000 - ) # specific enthalpy of the remaining free volume gas in kJ/kg - H_c_4_gas = ( - H2_c_Cap_Storage * Release_efficiency * H_c_4_spec_g_electrolyzer - + H2_c_Cap_Storage * (1 - Release_efficiency) * H_c_4_spec_g_tank - ) # Total gas phase enthalpy in stage 3 in kJ - deltaE_c_H2_4_2 = H_c_2_gas - H_c_4_gas # Total h2 enthalpy change in kJ - deltaE_c_Uwall_4_2 = ( - self.Heat_Capacity_Wall * (Temp2 - Temp4_tank) * m_c_wall * number_c_of_tanks - ) # kJ - deltaE_c_net_4_2 = ( - deltaE_c_H2_4_2 + deltaE_c_Uwall_4_2 - ) # Net enthalpy change during desorption - deltaP_c_net_4_2 = deltaE_c_net_4_2 / self.t_charge_hr / 3600 - - ########################################Costs for cycle 1 adsorption#################### - - ###############CAPITAL COSTS (sized based on cycle 1 requirements)###################### - - ###############################Compressor costs ### axial/centrifugal - if self.Pres >= self.Pin: - K = PropsSI( - "ISENTROPIC_EXPANSION_COEFFICIENT", - "P", - self.Pin * 10**5, - "T", - self.Tin, - "Hydrogen", - ) - P2nd = self.Pin * (self.Pres / self.Pin) ** (1 / 3) - P3rd = ( - self.Pin * (self.Pres / self.Pin) ** (1 / 3) * (self.Pres / self.Pin) ** (1 / 3) - ) - work_c_comp_1 = ( - K - / (K - 1) - * self.R - * self.Tin - / self.MW_H2 - * ((P2nd / self.Pin) ** ((K - 1) / K) - 1) - ) - work_c_comp_2 = ( - K - / (K - 1) - * self.R - * self.Tin - / self.MW_H2 - * ((P3rd / P2nd) ** ((K - 1) / K) - 1) - ) - work_c_comp_3 = ( - K - / (K - 1) - * self.R - * self.Tin - / self.MW_H2 - * ((self.Pres / P3rd) ** ((K - 1) / K) - 1) - ) - Work_c_comp = work_c_comp_1 + work_c_comp_2 + work_c_comp_3 - # Work_c_comp=K/(K-1)*self.R*self.Tin/self.MW_H2*((self.Pres/self.Pin)**((K-1)/K)-1) #mechanical energy required for compressor in J/kg (single stage) # noqa: E501 - Power_c_comp_1_2 = ( - Work_c_comp / 1000 * m_c_flow_rate_1_2 - ) # mechanical power of the pump in kW - Power_c_comp_4_2 = Work_c_comp / 1000 * m_c_flow_rate_4_2 - A_c_comp_1_2 = Power_c_comp_1_2 / self.Efficiency_comp # total power in kW - A_c_comp_4_2 = Power_c_comp_4_2 / self.Efficiency_comp # total power in kW - if A_c_comp_1_2 >= A_c_comp_4_2: - A_c_comp = A_c_comp_1_2 - else: - A_c_comp = A_c_comp_4_2 - - # print ('work of compressor is', Work_c_comp,'J/kg') - # print ('Adjusted storage capacity is', H2_c_Cap_Storage, 'kg') - # print ('flow rate is', m_c_flow_rate_1_2, 'and', m_c_flow_rate_4_2, 'kg/s') - # print('Total fluid power of compressor', A_c_comp, 'kW') - Number_c_Compressors = np.floor( - A_c_comp / 3000 - ) # Number of compressors excluding the last one - A_c_comp_1 = A_c_comp % 3000 # power of the last compressor - # print('Number of compressors', Number_c_Compressors+1) - k1 = 2.2897 - k2 = 1.3604 - k3 = -0.1027 - Compr_c_Cap_Cost = ( - 10 ** (k1 + k2 * np.log10(3000) + k3 * (np.log10(3000)) ** 2) - ) * Number_c_Compressors - Compr_c_Cap_Cost_1 = 10 ** ( - k1 + k2 * np.log10(A_c_comp_1) + k3 * (np.log10(A_c_comp_1)) ** 2 - ) - - compressor_energy_used_1 = Work_c_comp * H2_c_Cap_Storage * 2.8e-7 - compressor_energy_used_2 = ( - Work_c_comp * H2_c_Cap_Storage * Release_efficiency * 2.8e-7 - ) - - Compr_c_Energy_Costs_1 = ( - compressor_energy_used_1 * Energy_cost - ) # compressor electricity cost in cycle 1 - Compr_c_Energy_Costs_2 = ( - compressor_energy_used_2 * Energy_cost - ) # compressor electricity cost assuming in regular charging cycle - - Total_c_Compr_Cap_Cost = Compr_c_Cap_Cost + Compr_c_Cap_Cost_1 - Total_c_Compr_Cap_Cost = Total_c_Compr_Cap_Cost * ( - self.CEPCI_current / self.CEPCI2001 - ) ##Inflation - else: - Power_c_comp_1_2 = 0 # mechanical power of the pump in kW - Power_c_comp_4_2 = 0 - A_c_comp_1_2 = 0 # total power in kW - A_c_comp_4_2 = 0 # total power in kW - Work_c_comp = 0 - Compr_c_Cap_Cost = 0 - compressor_energy_used_1 = 0 - compressor_energy_used_2 = 0 - Compr_c_Energy_Costs_1 = 0 - Compr_c_Energy_Costs_2 = 0 - Total_c_Compr_Cap_Cost = 0 - - self.total_compressor_energy_used_kwh = ( - compressor_energy_used_1 # + compressor_energy_used_2 - ) - - # print ('Compressor energy cost is $', Compr_c_Energy_Costs) - # print ('refrigeration capcost for compressor is $') - # print('compressor capcost is $', Total_c_Compr_Cap_Cost) - # print("----------") - - ########################################Costs associated with storage tanks - - # print("Number of tanks is: ", number_c_of_tanks) - Storage_c_Tank_Cap_Costs = Cost_c_tank * number_c_of_tanks * self.Tank_manufacturing - Storage_c_Tank_Cap_Costs = Storage_c_Tank_Cap_Costs * ( - self.CEPCI_current / self.CEPCI2007 - ) ##Inflation - # print('Capcost for storage tank is', Storage_c_Tank_Cap_Costs) - # print("----------") - - ###############################Refrigeration costs estimation adsorption process - # print ('pre-conditioning time is', round (t_precondition_hr), 'hr') - Ref_c_P_net_1_2 = -( - deltaP_c_net_1_2 - Power_c_comp_1_2 - ) # Refrigeration power in kW from state 1 to state 2 (precondition) - Ref_c_P_net_4_2 = -( - deltaP_c_net_4_2 - Power_c_comp_4_2 - ) # Refrigeration power in kW from state 1 to state 2 (normal charging) - if Ref_c_P_net_1_2 >= Ref_c_P_net_4_2: - Net_c_Cooling_Power_Adsorption = Ref_c_P_net_1_2 # Net refrigeration power in kW - else: - Net_c_Cooling_Power_Adsorption = Ref_c_P_net_4_2 - # print( - # "Net Cooling power for refrigeration sizing is", - # Net_c_Cooling_Power_Adsorption, - # "kW", - # ) # Cooling power in kW - - if Net_c_Cooling_Power_Adsorption < 1000: - A1 = -3.53e-09 - A2 = -9.94e-06 - A3 = 3.30e-03 - nc = ( - (A1 * (self.Temp_c**3)) + (A2 * (self.Temp_c**2)) + A3 * self.Temp_c - ) # Carnot efficiency factor - COP = (self.Temp_c / (318 - self.Temp_c)) * nc # Coefficient of performance - B1 = 24000 - B2 = 3500 - B3 = 0.9 - Total_c_Refrig_Cap_Costs_adsorption = ( - B1 + (B2 * (Net_c_Cooling_Power_Adsorption / COP) ** B3) - ) * (self.CEPCI_current / 550.8) - else: - Total_c_Refrig_Cap_Costs_adsorption = ( - 2 - * 10**11 - * self.Temp_c**-2.077 - * (Net_c_Cooling_Power_Adsorption / 1000) ** 0.6 - ) - Total_c_Refrig_Cap_Costs_adsorption = Total_c_Refrig_Cap_Costs_adsorption * ( - self.CEPCI_current / self.CEPCI2017 - ) - - ####Utility for refrigeration - # NOTE: Utility in $/GJ, here, the utility is mostly for energy assumes 16.8 $/GJ - # (57 $/MWh) - Utility_c_ref = 4.07 * 10**7 * self.Temp_c ** (-2.669) - # Utility_c_refrigeration_1 = (self.CEPCI_current/self.CEPCI2017)*Utility_c_ref*-(deltaE_c_net_1_2-Work_c_comp*H2_c_Cap_Storage/1000)/1e6 # noqa: E501 - energy_consumption_refrigeration_1_kj = -( - deltaE_c_net_1_2 - Work_c_comp * H2_c_Cap_Storage / 1000 - ) # in kJ - - # NOTE: changed based on discussion with original author 20221216, energy separated - # out 20230317 - Utility_c_refrigeration_1 = ( - (Energy_cost / 0.057) * Utility_c_ref * energy_consumption_refrigeration_1_kj / 1e6 - ) - # print ('refrigerator capital cost for adsorption is $', Total_c_Refrig_Cap_Costs_adsorption) # noqa: E501 - # print("------------") - - # Utility_c_refrigeration_2 = ( - # (self.CEPCI_current / self.CEPCI2017) - # * Utility_c_ref - # * -(deltaE_c_net_4_2 - Work_c_comp * H2_c_Cap_Storage * Release_efficiency / 1000) - # / 1e6 - # ) - energy_consumption_refrigeration_2_kj = -( - deltaE_c_net_4_2 - Work_c_comp * H2_c_Cap_Storage * Release_efficiency / 1000 - ) # in kJ - - # NOTE: changed based on discussion with original author 20221216, energy separated - # out 20230317 - Utility_c_refrigeration_2 = ( - (Energy_cost / 0.057) * Utility_c_ref * energy_consumption_refrigeration_2_kj / 1e6 - ) - - # specify energy usage separately so energy usage can be used externally if desired - joule2watthour = 1.0 / 3600.0 # 3600 joules in a watt hour (as also 3600 kJ in a kWh) - energy_consumption_refrigeration_1_kwh = ( - energy_consumption_refrigeration_1_kj * joule2watthour - ) - (energy_consumption_refrigeration_2_kj * joule2watthour) - self.total_refrigeration_energy_used_kwh = ( - energy_consumption_refrigeration_1_kwh # + energy_consumption_refrigeration_2_kwh - ) - - if self.total_refrigeration_energy_used_kwh < 0: - raise (ValueError("energy usage must be greater than 0")) - ###############################Heating costs desorption process - k1 = 6.9617 - k2 = -1.48 - k3 = 0.3161 - Net_c_Heating_Power_Desorption = ( - detlaP_c_net_2_3 / self.Efficiency_heater - ) ## steam boiler power at 0.7 efficiency in kW - Number_c_Heaters = np.floor( - Net_c_Heating_Power_Desorption / 9400 - ) # Number of compressors excluding the last one - Heater_c_Power_1 = Net_c_Heating_Power_Desorption % 9400 # power of the last compressor - # print('Number of heaters', Number_c_Heaters+1) - Heater_c_Cap_Cost = ( - 10 ** (k1 + k2 * np.log10(9400) + k3 * (np.log10(9400)) ** 2) - ) * Number_c_Heaters - if Heater_c_Power_1 < 1000: - Heater_c_Cap_Cost_1 = ( - 10 ** (k1 + k2 * np.log10(1000) + k3 * (np.log10(1000)) ** 2) - ) * (Heater_c_Power_1 / 1000) - else: - Heater_c_Cap_Cost_1 = 10 ** ( - k1 + k2 * np.log10(Heater_c_Power_1) + k3 * (np.log10(Heater_c_Power_1)) ** 2 - ) - Total_c_Heater_Cap_Cost = Heater_c_Cap_Cost + Heater_c_Cap_Cost_1 - Total_c_Heater_Cap_Cost = Total_c_Heater_Cap_Cost * ( - self.CEPCI_current / self.CEPCI2001 - ) ##Inflation #TODO make inflation optional per user input - - # NOTE: Jared Thomas set to zero as per discussion with Peng Peng through Abhineet Gupta - # 20221215 was 13.28*deltaE_c_net_2_3/1e6 #$13.28/GJ for low pressure steam - Utility_c_Heater = 0 - - self.total_heating_energy_used_kwh = Net_c_Heating_Power_Desorption * t_discharge_hr - Total_c_Heating_Energy_Costs = self.total_heating_energy_used_kwh * Energy_cost - - # print('heater capcost is $', Total_c_Heater_Cap_Cost) - - #############Operational costs (sized based on cycle 1 requirements)#################### - Op_c_Costs_1 = ( - Compr_c_Energy_Costs_1 - + Utility_c_refrigeration_1 - + Utility_c_Heater - + Total_c_Heating_Energy_Costs - ) - Op_c_Costs_2 = ( - Compr_c_Energy_Costs_2 - + Utility_c_refrigeration_2 - + Utility_c_Heater - + Total_c_Heating_Energy_Costs - ) - Total_c_Cap_Costs = ( - Storage_c_Tank_Cap_Costs - + Total_c_Refrig_Cap_Costs_adsorption - + Total_c_Compr_Cap_Cost - + Total_c_Heater_Cap_Cost - ) - - # Op_c_Costs = ( - # ( - # Op_c_Costs_1 - # + Op_c_Costs_2 * (cycle_number - 1) - # + self.maintanance * Total_c_Cap_Costs - # + self.wage * 360 * 2 - # ) - # / cycle_number - # / capacity - # ) - # TODO check this. I changed the 2 to a 24 because it looks like it should be working hours in a year. - Op_c_Costs = ( - ( - Op_c_Costs_1 - + Op_c_Costs_2 * (cycle_number - 1) - + self.maintanance * Total_c_Cap_Costs - + self.wage * 360 * 2 - ) - / cycle_number - ) # checked, this was divided by capacity, but Peng Peng confirmed it was duplicating - # the following divisions by capacity - - ######################writing costs##################################################### - self.cost_kg[i] = (Total_c_Cap_Costs / capacity + self.Site_preparation) * self.Markup - cost_kg_tank[i] = Storage_c_Tank_Cap_Costs / capacity - cost_kg_comp[i] = Total_c_Compr_Cap_Cost / capacity - cost_kg_ref[i] = Total_c_Refrig_Cap_Costs_adsorption / capacity - cost_kg_heat[i] = Total_c_Heater_Cap_Cost / capacity - self.Op_c_Costs_kg[i] = Op_c_Costs / capacity - # print("\n Pressure Vessel Costs: ") - # print("cost_kg ") - # print("cost_kg_tank ") - # print("cost_kg_comp ") - # print("cost_kg_ref ") - # print("cost_kg_heat ") - ######################################## Total Energy Use (kWh) ###################### - self.total_energy_used_kwh[i] = ( - self.total_compressor_energy_used_kwh - + self.total_heating_energy_used_kwh - + self.total_refrigeration_energy_used_kwh - ) - - self.curve_fit() - - def curve_fit(self): - ################### plot prep ########### - self.plot_range = range(int(np.min(self.capacity_1)), int(np.max(self.capacity_1)), 100) - - ###################Fitting capital#################################################### - - var_cap = [0.01, 0.5, 5] # Initial guesses for the parameters, can be flexible - - varfinal_cap_fitted, success = leastsq( - self.residual_op, - var_cap, - args=(self.capacity_1, self.cost_kg), - maxfev=100000, - ) - - self.a_cap_fit = varfinal_cap_fitted[0] - self.b_cap_fit = varfinal_cap_fitted[1] - self.c_cap_fit = varfinal_cap_fitted[2] - - if self.verbose: - print("a_cap is", self.a_cap_fit) - print("b_cap is", self.b_cap_fit) - print("c_cap is", self.c_cap_fit) - print("***********") - - self.fitted_capex = self.exp_log_fit(varfinal_cap_fitted, self.plot_range) - - # popt, pcov = curve_fit(self.exp_fit, self.capacity_1, self.cost_kg, maxfev=100000) - - # self.a_cap_fit=popt[0] - # self.b_cap_fit=popt[1] - - # print ('a is', self.a_cap_fit) - # print ('b is', self.b_cap_fit) - # print ('***********') - - # self.fitted_kg = self.exp_fit(self.plot_range,self.a_cap_fit,self.b_cap_fit) - - ####################### fitting OpEx ################################# - var_op = [0.01, 0.5, 5] # Initial guesses for the parameters, can be flexible - - varfinal_op_fitted, success = leastsq( - self.residual_op, - var_op, - args=(self.capacity_1, self.Op_c_Costs_kg), - maxfev=100000, - ) - - self.a_op_fit = varfinal_op_fitted[0] - self.b_op_fit = varfinal_op_fitted[1] - self.c_op_fit = varfinal_op_fitted[2] - - if self.verbose: - print("a_op is", self.a_op_fit) - print("b_op is", self.b_op_fit) - print("c_op is", self.c_op_fit) - print("***********") - - self.fitted_op_kg = self.exp_log_fit(varfinal_op_fitted, self.plot_range) - - ##################### Fit energy usage ################################ - self.energy_coefficients = np.polyfit(self.capacity_1, self.total_energy_used_kwh, 1) - self.energy_function = np.poly1d(self.energy_coefficients) # kWh - self.fit_energy_wrt_capacity_kwh = self.energy_function(self.plot_range) - - def plot(self): - fig, ax = plt.subplots(2, 2, sharex=True, figsize=(10, 6)) - - ##################### CAPEX ####################### - ax[0, 0].scatter(self.capacity_1 * 1e-3, self.cost_kg, color="r", label="Calc") - ax[0, 0].plot(np.asarray(self.plot_range) * 1e-3, self.fitted_capex, label="Fit") - # ax[0,0].plot(self.capacity_1,cost_kg_tank, color='b', label = 'tank') - # ax[0,0].plot(self.capacity_1,cost_kg_comp, color='c', label = 'compressor') - # ax[0,0].plot(self.capacity_1,cost_kg_ref, color='m', label = 'refrigeration') - # ax[0,0].plot(self.capacity_1,cost_kg_heat, color='y', label = 'heater') - - np.round(self.a_cap_fit, 2) - np.round(self.b_cap_fit, 2) - # plt.ylim(0,np.amax(self.cost_kg)*2) - # equation_cap = 'y='+str(a_disp)+'x'+'^'+str(b_disp) - a_cap_fit_disp = np.round(self.a_cap_fit, 2) - b_cap_fit_disp = np.round(self.b_cap_fit, 2) - c_cap_fit_disp = np.round(self.c_cap_fit, 2) - equation_cap = ( - "y=" - + "exp(" - + str(a_cap_fit_disp) - + "(ln(x))^2\n-" - + str(b_cap_fit_disp) - + "ln(x)+" - + str(c_cap_fit_disp) - + ")" - ) - - ax[0, 0].annotate( - equation_cap, - xy=(np.amax(self.capacity_1) * 1e-3 * 0.4, np.amax(self.cost_kg) * 0.8), - ) - - ax[0, 0].set_ylabel("CAPEX ($/kg)") - ax[0, 0].legend(loc="best", frameon=False) - # plt.legend(loc='best') - # ax[0,0].title('Capital') - - ##################### OPEX ############################ - a_op_fit_disp = np.round(self.a_op_fit, 2) - b_op_fit_disp = np.round(self.b_op_fit, 2) - c_op_fit_disp = np.round(self.c_op_fit, 2) - - equation_op = ( - "y=" - + "exp(" - + str(a_op_fit_disp) - + "(ln(x))^2\n-" - + str(b_op_fit_disp) - + "ln(x)+" - + str(c_op_fit_disp) - + ")" - ) - - ax[0, 1].plot(np.asarray(self.plot_range) * 1e-3, self.fitted_op_kg, label="Fit") - ax[0, 1].scatter(self.capacity_1 * 1e-3, self.Op_c_Costs_kg, color="r", label="Calc") - ax[0, 1].set_ylabel("OPEX ($/kg)") - ax[0, 1].annotate( - equation_op, - xy=( - np.amax(self.capacity_1) * 1e-3 * 0.2, - np.amax(self.Op_c_Costs_kg) * 0.4, - ), - ) - ax[0, 1].legend(loc="best", frameon=False) - # plt.legend(loc='best') - # ax[0,1].title('Annual operational') - - ################## Energy ###################### - ax[1, 1].plot( - np.asarray(self.plot_range) * 1e-3, - self.fit_energy_wrt_capacity_kwh * 1e-6, - label="Fit", - ) - ax[1, 1].scatter( - self.capacity_1 * 1e-3, - self.total_energy_used_kwh * 1e-6, - color="r", - label="Calc", - ) - ax[1, 1].set_xlabel("Capacity (metric tons H2)") - ax[1, 1].set_ylabel("Energy Use (GWh)") - - equation_energy = ( - "y=" - + str(round(self.energy_coefficients[0], 2)) - + "x+" - + str(round(self.energy_coefficients[1], 2)) - ) - ax[1, 1].annotate(equation_energy, xy=(3000, 5)) - ax[1, 1].legend(loc="best", frameon=False) - ax[1, 1].legend(loc="best", frameon=False) - # ax[1,1].title('Annual operational') - - ################ Wrap Up ###################### - - ax[1, 0].set_xlabel("Capacity (metric tons H2)") - - plt.tight_layout() - plt.show() diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/README.md b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/README.md deleted file mode 100644 index d27f04793..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/README.md +++ /dev/null @@ -1,54 +0,0 @@ -# Compressed gas storage for wind-H2-steel project -- Author: Peng Peng (ppeng@lbl.gov) -- Date: 10/21/2022 -- Brief description: This script is for a high-level overall estimation of energy consumption for compressed gas H2 storage for a steel facility requiring 200 metric tons of hydrogen per day. - -## Required files: -1. Compressed_all.py -2. Compressed_gas_function.py -3. Tankinator_large.xlsx -## Key inputs: -1. Wind availability in % -2. Charge and discharge ratio -3. Flow rate -4. Energy (renewable) cost in $/kWh -## Key output: -1. Maximum equivalent storage capacity. -2. Maximum equivalent storage duration. -3. Fitting parameters for further calculations and optimizations. - 1. purchase capital storage cost ($/kg) vs. capacity - 2. annual operation cost ($/kg) vs. capacity. - -![](images/2022-11-18-15-30-34.png) -![](images/2022-11-18-15-31-18.png) - -## How to use: -1. Put all three files in one folder -2. Change the path of the Tankinator file -3. Change inputs and run Compressed_all.py - -## Notes: -1. Max storage capacity obtained from empirical relation from another project for 200 metric ton/day H2 flow into steel facility. See below. And is assumed to scale linearly with the steel facility. - - ![](images/2022-11-18-15-31-54.png) -2. Operation cost does not include electrolyzer. -3. Costs include the following dummy values, which can be changed later in the “Economic parameters” section (around line 64) in Compressed_gas_function.py - 1. Site preparation $100/kg H2 stored - 2. 80% markup for tank manufacturing based on materials - 3. 50% markup from purchase cost for engineering, installation, etc. - 4. labor @ 36 $/h, 360 days x2 - 5. annual maintenance @ 3% purchase capital -4. Capital cost is for 2021 CEPCI, which can be changed at the same place as above -5. The code is for 350 bar compressed gas storage. If change pressure make sure to change the pressure in the Tankinator file and save it again before running. -6. The main components included are storage tanks, compressors, refrigeration, and heater. -7. Calculations are based on high-level energy balances, detailed energy consumption should be performed with more comprehensive process simulation. -8. Heat transfer between the ambient, and friction losses are not included. -9. From the storage capacity (x) derived from the wind availability, the cost relation is derived from the Cost relation fitted from 0.1x to x. This is done because the fitting correlations do not work very well across a very large range (Let me know if you want to optimize for a larger range). This will lead to a small difference for the same capacity when the max changes. See the example below for different wind availabilities, the fitted values for 105 kg capacity are different. I tested different step sizes and ranges and found 0.1x-x, 15-25 steps tend to give the smallest difference for this fitting correlation. -![](images/2022-11-18-15-32-24.png) -![](images/2022-11-18-15-32-32.png) -10. Cost year for the model is 2021 with the chemical plant cost index set as 708. - -## Main references: -1. Geankoplis, C. J. "Transport processes and separation." Process Principles. Prentice Hall NJ, 2003. -2. Dicks, A. L. & Rand, D. A. J. in Fuel Cell Systems Explained 351-399 (Wiley, 2018). -3. Turton, R., Bailie, R. C., Whiting, W. B., & Shaeiwitz, J. A. (2018). Analysis, synthesis and design of chemical processes. Pearson Education. 5th edition. -4. Luyben, W. L. (2017). Estimating refrigeration costs at cryogenic temperatures. Computers & Chemical Engineering, 103, 144-150. diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Tankinator.xlsx b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Tankinator.xlsx deleted file mode 100644 index 93fcd6782..000000000 Binary files a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/Tankinator.xlsx and /dev/null differ diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/__init__.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/__init__.py deleted file mode 100644 index ca0310b72..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/compressed_gas_storage_model_20221021/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from h2integrate.simulation.technologies.hydrogen.h2_storage.pressure_vessel.compressed_gas_storage_model_20221021.Compressed_all import ( - PressureVessel, -) -from h2integrate.simulation.technologies.hydrogen.h2_storage.pressure_vessel.compressed_gas_storage_model_20221021.Compressed_gas_function import ( - CompressedGasFunction, -) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/material_properties.json b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/material_properties.json deleted file mode 100644 index 3d1d331bc..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/material_properties.json +++ /dev/null @@ -1,503 +0,0 @@ -{ - "316SS": { - "costrate_$kg": 4.92, - "density_kgccm": 0.008027, - "tables": { - "ultimate": { - "shear_bar": [ - 13896, - 13259.1, - 12575.88, - 11869.5, - 11348.4, - 10769.4, - 10248.3, - 9669.3, - 9148.2, - 8569.2, - 8048.1, - 7469.1, - 6948, - 6658.5, - 6369, - 5790, - 5170, - 5030, - 5000, - 4840, - 4520, - 3450, - 1860, - 800 - ], - "temp_degC": [ - -273, - -250, - -225, - -200, - -180, - -160, - -140, - -120, - -100, - -80, - -60, - -40, - -20, - -10, - 0, - 27, - 149, - 260, - 371, - 482, - 593, - 704, - 816, - 927 - ] - }, - "yield": { - "shear_bar": [ - 6413.793103, - 6099.213551, - 5784.633999, - 5517.241379, - 5465.517241, - 5120.689655, - 5034.482759, - 4850.574713, - 4605.363985, - 4360.153257, - 4114.942529, - 3931.034483, - 3103.448276, - 2758.62069, - 2758, - 2900, - 2010, - 1720, - 1590, - 1480, - 1400, - 1310, - 1100, - 1 - ], - "temp_degC": [ - -253.15, - -233.15, - -213.15, - -196.15, - -193.15, - -173.15, - -168.15, - -153.15, - -133.15, - -113.15, - -93.15, - -78.15, - -40.15, - -0.15, - 0, - 27, - 149, - 260, - 371, - 482, - 593, - 704, - 816, - 927 - ] - } - } - }, - "4130_cromoly": { - "costrate_$kg": 2.55, - "density_kgccm": 0.0077, - "tables": { - "ultimate": { - "shear_bar": [ - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963, - 8963 - ], - "temp_degC": [ - -253.15, - -233.15, - -213.15, - -196.15, - -193.15, - -173.15, - -168.15, - -153.15, - -133.15, - -113.15, - -93.15, - -78.15, - -40.15, - -0.15, - 0, - 427, - 538, - 649, - 650, - 1000 - ] - }, - "yield": { - "shear_bar": [ - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239, - 7239 - ], - "temp_degC": [ - -253.15, - -233.15, - -213.15, - -196.15, - -193.15, - -173.15, - -168.15, - -153.15, - -133.15, - -113.15, - -93.15, - -78.15, - -40.15, - -0.15, - 0, - 427, - 538, - 649, - 650, - 1000 - ] - } - } - }, - "6061_T6_Aluminum": { - "costrate_$kg": 4.45, - "density_kgccm": 0.002663, - "tables": { - "ultimate": { - "shear_bar": [ - 4504.747455, - 4217.42412, - 4041.31572, - 3874.477815, - 3753.96138, - 3614.93496, - 3512.959515, - 3438.82656, - 3373.933095, - 3327.58062, - 3244.17717, - 3142.201725, - 2975.366921, - 2780.717531, - 2613.873425, - 2382.148256, - 1872.348543, - 1065.942599, - 834.214329, - 621.0270495, - 426.3776595, - 268.8028083, - 203.9192649 - ], - "temp_degC": [ - -239.997348, - -214.77033, - -200.7547644, - -181.1337504, - -164.3157384, - -141.8911668, - -122.2701528, - -101.24736, - -80.2245672, - -56.3990502, - -25.56580596, - 8.0705514, - 57.123642, - 101.971674, - 130.0022496, - 144.0172596, - 172.0478352, - 214.0928652, - 228.1084308, - 246.327666, - 267.3504588, - 305.1912636, - 343.0320684 - ] - }, - "yield": { - "shear_bar": [ - 3871.227072, - 3829.511081, - 3787.795091, - 3746.079101, - 3696.00308, - 3654.28709, - 3595.879092, - 3545.831126, - 3512.447112, - 3487.423128, - 3445.707138, - 3403.991148, - 3370.635188, - 3345.611204, - 3312.22719, - 3278.843177, - 3245.487217, - 3220.463233, - 3187.079219, - 3162.055236, - 3137.031252, - 3111.979215, - 3086.955232, - 3061.931248, - 3036.907265, - 3020.215258, - 3003.523251, - 2995.191274, - 2986.831244, - 2970.167291, - 2970.167291, - 2953.475284, - 2945.115254, - 2928.451301, - 2911.759294, - 2895.067287, - 2878.37528, - 2861.683273, - 2853.351297, - 2836.65929, - 2811.635306, - 2803.294914, - 2778.265319, - 2761.578923, - 2736.549329, - 2711.519735, - 2694.833339, - 2669.803744, - 2644.77415, - 2603.05816, - 2569.685368, - 2544.655773, - 2511.282981, - 2477.910189, - 2444.537397 - ], - "temp_degC": [ - -251.9216667, - -250.2744444, - -246.9805556, - -243.6861111, - -240.3922222, - -235.4511111, - -230.51, - -225.5688889, - -222.2744444, - -218.9805556, - -214.0394444, - -210.745, - -205.8038889, - -200.8627778, - -195.9216667, - -190.9805556, - -186.0394444, - -181.0977778, - -174.51, - -169.5688889, - -162.9805556, - -156.3922222, - -148.1566667, - -139.9216667, - -128.3922222, - -116.8627778, - -106.9805556, - -95.45111111, - -85.56888889, - -75.68611111, - -65.80394444, - -55.92155556, - -46.03922222, - -36.15688889, - -29.56861111, - -19.68627222, - -9.803944444, - 0.078444444, - 6.666666667, - 13.25488889, - 21.49022222, - 26.43138889, - 33.01961111, - 39.60777778, - 47.84333333, - 56.07833333, - 62.66666667, - 69.255, - 77.49, - 87.37277778, - 95.60777778, - 103.8433333, - 112.0783333, - 120.3138889, - 125.255 - ] - } - } - }, - "HDPE": { - "costrate_$kg": 2.06, - "density_kgccm": 9.4587e-05 - }, - "carbon_fiber": { - "costrate_$kg": 30.65, - "density_kgccm": 0.001611, - "fiber_translation_efficiency": 0.8, - "layer_thickness_cm": 0.09144, - "min_layer": 3, - "shear_ultimate_bar": 15306 - }, - "liner_aluminum": { - "costrate_$kg": 4.45, - "density_kgccm": 0.002663, - "shear_ultimate_bar": 3103.0 - }, - "steel": { - "costrate_$kg": 3.01, - "density_kgccm": 0.00785, - "tables": { - "ultimate": { - "shear_bar": [ - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187, - 11187 - ], - "temp_degC": [ - -253.15, - -233.15, - -213.15, - -196.15, - -193.15, - -173.15, - -168.15, - -153.15, - -133.15, - -113.15, - -93.15, - -78.15, - -40.15, - -0.15, - 0, - 427, - 538, - 649, - 650, - 1000 - ] - }, - "yield": { - "shear_bar": [ - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043, - 10043 - ], - "temp_degC": [ - -253.15, - -233.15, - -213.15, - -196.15, - -193.15, - -173.15, - -168.15, - -153.15, - -133.15, - -113.15, - -93.15, - -78.15, - -40.15, - -0.15, - 0, - 427, - 538, - 649, - 650, - 1000 - ] - } - } - } -} diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/tankinator.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/tankinator.py deleted file mode 100644 index c71d02593..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/tankinator.py +++ /dev/null @@ -1,818 +0,0 @@ -""" -Author: Cory Frontin -Date: 23 Jan 2023 -Institution: National Renewable Energy Lab -Description: This file computes pressure vessel thickness, replacing Tankinator.xlsx -Sources: - - Tankinator.xlsx -""" - -from __future__ import annotations - -import json -from pathlib import Path - -import numpy as np -import scipy.optimize as opt - -from h2integrate.simulation.technologies.hydrogen.h2_storage.pressure_vessel import von_mises - - -class MetalMaterial: - """ - a class for the material properties for metals used in analysis - - :param metal_type: type of metal to use, must be defined in - material_properties.json - :type metal_type: str, must be in list of known materials - :param approx_method: method to approximate inexact table lookups - :type approx_method: str, must be in list of known methods - :raises NotImplementedError: if inputs are not found in known data - """ - - def __init__(self, metal_type: str, approx_method: str = "lookup"): - # make sure settings are correct - if approx_method not in ["nearest", "lookup", "interp"]: - msg = f"the requested approximation method ({approx_method}) is not implemented." - raise NotImplementedError(msg) - - with (Path(__file__).parent / "material_properties.json").open() as mmprop_file: - mmprop = json.load(mmprop_file) - if metal_type not in mmprop.keys(): - msg = ( - f"the requested metal/material ({metal_type}) has not been implemented in" - f" material_properties.json.\navailable types: {[*mmprop]}", - ) - raise NotImplementedError(msg) - self.mmprop = mmprop[metal_type] # select the relevant material property set - - # stash validated class data - self.approx_method = approx_method - self.metal_type = metal_type - - # density and cost per weight - self.density = self.mmprop["density_kgccm"] # kg/ccm - self.cost_rate = self.mmprop["costrate_$kg"] # cost per kg - - # nicely package an approximator function - def _get_approx_fun(approx_method): - if approx_method == "nearest": - - def nearest(xq, x, y): - x = np.asarray(x) - y = np.asarray(y) - idx_y = np.argmin(np.abs(x - xq)) - return y[idx_y] - - return nearest - elif approx_method == "lookup": - - def lookup(xq, x, y): - x = np.asarray(x) - y = np.asarray(y) - idx_yplus = np.argmin(np.abs(x[x < xq] - xq)) - return y[x < xq][idx_yplus] - - return lookup - elif approx_method == "interp": - return lambda xq, x, y: np.interp(xq, x, y) - else: - raise LookupError(f"approx method ({approx_method}) not found.") - - # stash functions to relate yield and ultimate shear (bar) to temp (degC) - def yield_shear_fun(self, T): - return MetalMaterial._get_approx_fun(self.approx_method)( - T, - self.mmprop["tables"]["yield"]["temp_degC"], - self.mmprop["tables"]["yield"]["shear_bar"], - ) - - def ultimate_shear_fun(self, T): - return MetalMaterial._get_approx_fun(self.approx_method)( - T, - self.mmprop["tables"]["ultimate"]["temp_degC"], - self.mmprop["tables"]["ultimate"]["shear_bar"], - ) - - -class Tank: - """ - a generalized class to size a pressurized gas tank - assumed to be cylindrical with hemispherical ends - - :param tank_type: type of tank to be used, which can take values I, III, IV - referring to all-metal, aluminum-lined carbon fiber, and HDPE-lined - carbon fiber, respectively, as - :type tank_type: int, must be 1, 3, or 4 - :param material: material that the pressure vessel is made of - :type material: str, must be in valid types - """ - - def __init__( - self, - tank_type: int, - yield_factor: float = 3.0 / 2.0, - ultimate_factor: float = 2.25, - ): - # unpack the key variables - if tank_type not in [1, 3, 4]: - msg = f"tank_type {tank_type} has not been implemented yet.\n" - raise NotImplementedError(msg) - self.tank_type = tank_type - - # if not (tank_type == 1): - # raise NotImplementedError("haven't done other classes yet. -CVF") - # self.liner= None - - # store fixed attributes - self.yield_factor = yield_factor - self.ultimate_factor = ultimate_factor - - # to start up: undefined geometry values - self.length_inner = None # inner total length of tank (m) - self.radius_inner = None # inner radius of tank (m) - - # operating conditions - self.operating_temperature = None - self.operating_pressure = None - - self.check_tol = 1e-10 # for validations - - # return functions for symmetry - def get_length_inner(self): - return self.length_inner - - def get_radius_inner(self): - return self.radius_inner - - def get_volume_inner(self): - """computes the inner volume""" - return Tank.compute_hemicylinder_volume(self.radius_inner, self.length_inner) - - def get_operating_temperature(self): - return self.operating_temperature - - def get_operating_pressure(self): - return self.operating_pressure - - # set functions: specify two of (length, radius, volume) - def set_length_radius(self, length_in, radius_in): - """ - set the pressure vessel dimensions by length and radius in cm and - compute the volume in ccm - """ - self.length_inner = length_in - self.radius_inner = radius_in - self.volume_inner = Tank.compute_hemicylinder_volume(radius_in, length_in) - - def set_length_volume(self, length_in, volume_in): - """ - set pressure vessel dimensions by length in cm and volume in ccm - - sets the length and volume of the pressure volume, backsolves for the - radius of the pressure volume - """ - self.length_inner = length_in - Rguess = length_in / 3 - r_opt = opt.fsolve( - lambda x: Tank.compute_hemicylinder_volume(x, length_in) - volume_in, Rguess - ) - assert ( - np.abs(Tank.compute_hemicylinder_volume(r_opt, length_in) - volume_in) / volume_in - <= self.check_tol - ) - self.radius_inner = float(r_opt) - - def set_radius_volume(self, radius_in, volume_in): - """ - set pressure vessel dimensions by radius in cm and volume in ccm - - sets the radius and volume of the pressure volume, backsolves for the - length of the pressure volume - """ - self.radius_inner = radius_in - Lguess = 3 * radius_in - L_opt = opt.fsolve( - lambda x: Tank.compute_hemicylinder_volume(radius_in, x) - volume_in, Lguess - ) - assert ( - np.abs(Tank.compute_hemicylinder_volume(radius_in, L_opt) - volume_in) / volume_in - <= self.check_tol - ) - self.length_inner = float(L_opt) - - def set_operating_temperature(self, temperature_in): - self.operating_temperature = temperature_in - - def set_operating_pressure(self, pressure_in): - self.operating_pressure = pressure_in - - # useful static methods - def compute_hemicylinder_volume(R: float, L: float) -> float: - assert L >= 2 * R # cylindrical tank with hemispherical ends - return np.pi * R**2 * (L - 2.0 * R / 3.0) - - def compute_hemicylinder_outer_length(L: float, t: float) -> float: - return L + 2 * t - - def compute_hemicylinder_outer_radius(R: float, t: float) -> float: - return R + t - - def check_thinwall(Rinner: float, t: float, thinwallratio_crit=10) -> bool: - return Rinner / t >= thinwallratio_crit - - -class TypeITank(Tank): - """ - a class I tank: metal shell tank - """ - - def __init__( - self, - material: str, - yield_factor: float = 3.0 / 2.0, - ultimate_factor: float = 2.25, - shear_approx="interp", - ): - super().__init__(1, yield_factor, ultimate_factor) - - self.material = MetalMaterial(material, approx_method=shear_approx) - - # initial geometry values undefined - self.thickness = None # thickness of tank - - # return functions for symmetry - def get_thickness(self): - return self.thickness - - # get the outer dimensions - def get_length_outer(self): - """returns the outer length of the pressure vessel in cm""" - if None in [self.length_inner, self.thickness]: - return None - return Tank.compute_hemicylinder_outer_length(self.length_inner, self.thickness) - - def get_radius_outer(self): - """returns the outer radius of the pressure vessel in cm""" - if None in [self.radius_inner, self.thickness]: - return None - return Tank.compute_hemicylinder_outer_radius(self.radius_inner, self.thickness) - - def get_volume_outer(self): - """ - returns the outer volume of the pressure vessel in ccm - """ - if None in [self.length_inner, self.radius_inner, self.thickness]: - return None - return Tank.compute_hemicylinder_volume(self.get_radius_outer(), self.get_length_outer()) - - def get_volume_metal(self): - """ - returns the (unsealed) displacement volume of the pressure vessel in ccm - """ - volume_inner = self.get_volume_inner() - volume_outer = self.get_volume_outer() - if None in [volume_inner, volume_outer]: - return None - assert volume_outer >= volume_inner - return volume_outer - volume_inner - - def get_mass_metal(self): - """returns the mass of the pressure vessel in kg""" - volume_metal = self.get_volume_metal() - if volume_metal is None: - return None - return self.material.density * volume_metal - - def get_cost_metal(self): - """ - returns the cost of the metal in the pressure vessel in dollars - """ - mass_metal = self.get_mass_metal() - if mass_metal is None: - return None - return self.material.cost_rate * mass_metal - - def get_gravimetric_tank_efficiency(self): - """ - returns the gravimetric tank efficiency: - $$ \frac{m_{metal}}{V_{inner}} $$ - in L/kg - """ - mass_metal = self.get_mass_metal() - volume_inner = self.get_volume_inner() - return (volume_inner / 1e3) / mass_metal - - def get_yield_thickness(self, pressure: float | None = None, temperature: float | None = None): - """ - gets the yield thickness - - returns the yield thickness given by: - $$ - t_y= \frac{p R_0}{S_y} \times SF_{yield} - $$ - with yield safety factor $SF_{yield}= 3/2$ by default - - temperature and pressure must be set in the class, or specified in this - function - - :param pressure: operating pressure, in bar - :type pressure: float - :param temperature: operating temperature, in degrees C - :type temperature: float - """ - - if (temperature is None) and (self.operating_temperature is None): - raise LookupError("you must specify an operating temperature.") - elif temperature is None: - temperature = self.operating_temperature - - if (pressure is None) and (self.operating_pressure is None): - raise LookupError("you must specify an operating pressure.") - elif pressure is None: - pressure = self.operating_pressure - - Sy = self.material.yield_shear_fun(temperature) - - thickness_yield = pressure * self.radius_inner / Sy * self.yield_factor - - return thickness_yield - - def get_ultimate_thickness( - self, pressure: float | None = None, temperature: float | None = None - ): - """ - get the ultimate thickness - - returns the ultimate thicnkess given by: - $$ - t_u= \frac{p R_0}{S_u} \times SF_{ultimate} - $$ - with ultimate safety factor $SF_{yield}= 2.25$ by default - - temperature and pressure must be set in the class, or specified in this - function - - :param pressure: operating pressure, in bar - :type pressure: float - :param temperature: operating temperature, in degrees C - :type temperature: float - """ - - if (temperature is None) and (self.operating_temperature is None): - raise LookupError("you must specify an operating temperature.") - elif temperature is None: - temperature = self.operating_temperature - - if (pressure is None) and (self.operating_pressure is None): - raise LookupError("you must specify an operating pressure.") - elif pressure is None: - pressure = self.operating_pressure - - Su = self.material.ultimate_shear_fun(temperature) - - thickness_ultimate = pressure * self.radius_inner / Su * self.ultimate_factor - - return thickness_ultimate - - def get_thickness_thinwall( - self, pressure: float | None = None, temperature: float | None = None - ): - """ - get the thickness based on thinwall assumptions - - maximum between yield and ultimate thickness - - temperature and pressure must be set in the class, or specified in this - function - - :param pressure: operating pressure, in bar - :type pressure: float - :param temperature: operating temperature, in degrees C - :type temperature: float - """ - - t_y = self.get_yield_thickness(pressure, temperature) - t_u = self.get_ultimate_thickness(pressure, temperature) - - thickness = max(t_y, t_u) - - return thickness - - def set_thickness_thinwall( - self, pressure: float | None = None, temperature: float | None = None - ): - """ - set the thickness based on thinwall assumptions - - maximum between yield and ultimate thickness - - temperature and pressure must be set in the class, or specified in this - function - - :param pressure: operating pressure, in bar - :type pressure: float - :param temperature: operating temperature, in degrees C - :type temperature: float - """ - - self.thickness = self.get_thickness_thinwall(pressure, temperature) - - def get_thickness_vonmises( - self, - pressure: float | None = None, - temperature: float | None = None, - max_cycle_iter: int = 10, - adj_fac_tol: float = 1e-6, - ): - """ - get the thickness based on a von Mises cycle - - temperature and pressure must be set in the class, or specified here - - :param pressure: operating pressure, in bar - :type pressure: float - :param temperature: operating temperature, in degrees C - :type temperature: float - :param max_cycle_iter: maximum iterations for von Mises cycle - :type max_cycle_iter: int - :param adj_fac_tol: tolerance for close enough wall thickness adjustment - factor - """ - - if (temperature is None) and (self.operating_temperature is None): - raise LookupError("you must specify an operating temperature.") - elif temperature is None: - temperature = self.operating_temperature - - if (pressure is None) and (self.operating_pressure is None): - raise LookupError("you must specify an operating pressure.") - elif pressure is None: - pressure = self.operating_pressure - - # get the limit shears - Sy = self.material.yield_shear_fun(temperature) - Su = self.material.ultimate_shear_fun(temperature) - - # start from the thinwall analysis - thickness_init = self.get_thickness_thinwall(pressure, temperature) - - # check to see if von Mises analysis is even needed - if (Tank.check_thinwall(self.radius_inner, thickness_init)) and ( - von_mises.wallThicknessAdjustmentFactor( - pressure, self.radius_inner + thickness_init, self.radius_inner, Sy, Su - ) - == 1.0 - ): - thickness_cycle = thickness_init # trivially satisfied - iter_cycle = -1 - print("trivially satisfied") - else: - print("running von mises cycle") - print(pressure, self.radius_inner, thickness_init, Sy, Su) - (thickness_cycle, WTAF_cycle, iter_cycle) = von_mises.cycle( - pressure, - self.radius_inner, - thickness_init, - Sy, - Su, - max_iter=max_cycle_iter, - WTAF_tol=adj_fac_tol, - ) - return thickness_cycle, iter_cycle - - def set_thickness_vonmises( - self, - pressure: float | None = None, - temperature: float | None = None, - max_cycle_iter: int = 10, - adj_fac_tol: float | None = 1e-6, - ): - """ - set the thickness based on a von Mises cycle - - temperature and pressure must be set in the class, or specified here - - :param pressure: operating pressure, in bar - :type pressure: float - :param temperature: operating temperature, in degrees C - :type temperature: float - :param max_cycle_iter: maximum iterations for von Mises cycle - :type max_cycle_iter: int - :param adj_fac_tol: tolerance for close enough wall thickness adjustment - factor - """ - - thickness, _ = self.get_thickness_vonmises( - pressure, temperature, max_cycle_iter, adj_fac_tol - ) - self.thickness = thickness - - -class LinedTank(Tank): - """ - a lined tank for Type III or Type III: aluminum-lined carbon fiber-jacketed tank - """ - - def __init__( - self, - tanktype: int, - load_bearing_liner, - liner_design_load_factor=0.21, - liner_thickness_min=0.3, - yield_factor: float = 3.0 / 2.0, - ultimate_factor: float = 2.25, - ): - super().__init__(tanktype, yield_factor, ultimate_factor) - - if tanktype not in [3, 4]: - raise NotImplementedError("unknown tank type.") - - self.load_bearing_liner = load_bearing_liner - self.liner_design_load_factor = liner_design_load_factor - - with (Path(__file__).parent / "material_properties.json").open() as mmprop_file: - mmprop = json.load(mmprop_file) - - # liner characteristics - if tanktype == 3: - self.shear_ultimate_liner = mmprop["liner_aluminum"]["shear_ultimate_bar"] - self.density_liner = mmprop["liner_aluminum"]["density_kgccm"] - self.costrate_liner = mmprop["liner_aluminum"]["costrate_$kg"] - else: - self.shear_ultimate_liner = None - self.density_liner = mmprop["HDPE"]["density_kgccm"] - self.costrate_liner = mmprop["HDPE"]["costrate_$kg"] - self.thickness_liner_min = liner_thickness_min - - # jacket characteristics - self.shear_ultimate_jacket = mmprop["carbon_fiber"]["shear_ultimate_bar"] - self.density_jacket = mmprop["carbon_fiber"]["density_kgccm"] - self.costrate_jacket = mmprop["carbon_fiber"]["costrate_$kg"] - self.fiber_translation_efficiency_jacket = mmprop["carbon_fiber"][ - "fiber_translation_efficiency" - ] - self.fiber_layer_thickness = mmprop["carbon_fiber"]["layer_thickness_cm"] - self.fiber_layer_min = mmprop["carbon_fiber"]["min_layer"] - - # thicknesses (to be computed) - self.thickness_liner = None - self.thickness_jacket = None - self.thickness_ideal_jacket = None - self.Nlayer_jacket = None - - def get_thicknesses_thinwall(self, pressure: float | None = None): - """???""" - - if (pressure is None) and (self.operating_pressure is None): - raise LookupError("you must specify an operating pressure.") - elif pressure is None: - pressure = self.operating_pressure - - # compute the liner thickness - pressure_burst_target = self.ultimate_factor * pressure - if self.tank_type != 4: - pressure_liner_target = pressure_burst_target * self.liner_design_load_factor - thickness_burst = pressure_liner_target * self.radius_inner / self.shear_ultimate_liner - thickness_liner = max(thickness_burst, self.thickness_liner_min) - else: - thickness_liner = self.thickness_liner_min - - # compute ideal jacket thickness - radius_liner = Tank.compute_hemicylinder_outer_radius(self.radius_inner, thickness_liner) - thickness_jacket_ideal = ( - pressure_burst_target - * radius_liner - / self.shear_ultimate_jacket - / self.fiber_translation_efficiency_jacket - ) - if self.load_bearing_liner: - # subdivide pressure if liner is load-bearing - pressure_liner = thickness_liner * self.shear_ultimate_liner / self.radius_inner - pressure_jacket = pressure_burst_target - pressure_liner - assert pressure_jacket >= 0 - thickness_jacket_ideal = ( - pressure_jacket - * radius_liner - / self.shear_ultimate_jacket - / self.fiber_translation_efficiency_jacket - ) - - # compute number of layers, real thickness - Nlayer_jacket = max( - self.fiber_layer_min, - int(np.ceil(thickness_jacket_ideal / self.fiber_layer_thickness)), - ) - thickness_jacket_real = Nlayer_jacket * self.fiber_layer_thickness - - return ( - thickness_liner, - thickness_jacket_ideal, - Nlayer_jacket, - thickness_jacket_real, - ) - - def set_thicknesses_thinwall(self, pressure: float | None = None): - """???""" - - if (pressure is None) and (self.operating_pressure is None): - raise LookupError("you must specify an operating pressure.") - elif pressure is None: - pressure = self.operating_pressure - - # pass the returns of the previous function - ( - self.thickness_liner, - self.thickness_ideal_jacket, - self.Nlayer_jacket, - self.thickness_jacket, - ) = self.get_thicknesses_thinwall(pressure) - - def get_safetyfactor_real_jacket(self): - """ - figure out the integer layer safety factor - """ - - if None in [self.thickness_jacket, self.thickness_ideal_jacket]: - return None - - sf_real = self.ultimate_factor * self.thickness_jacket / self.thickness_jacket_ideal - - return sf_real - - # get the liner dimensions - def get_length_liner(self): - """returns the outer length of the pressure vessel in cm""" - if None in [self.length_inner, self.thickness_liner]: - return None - return Tank.compute_hemicylinder_outer_length(self.length_inner, self.thickness_liner) - - def get_radius_liner(self): - """returns the outer radius of the pressure vessel in cm""" - if None in [self.radius_inner, self.thickness_liner]: - return None - return Tank.compute_hemicylinder_outer_radius(self.radius_inner, self.thickness_liner) - - def get_volume_outer_liner(self): - """ - returns the outer volume of the pressure vessel in ccm - """ - if None in [self.length_inner, self.radius_inner, self.thickness_liner]: - return None - return Tank.compute_hemicylinder_volume(self.get_radius_liner(), self.get_length_liner()) - - def get_volume_liner(self): - """ - returns the (unsealed) displacement volume of the liner in ccm - """ - volume_inner = self.get_volume_inner() - volume_outer_liner = self.get_volume_outer_liner() - if None in [volume_inner, volume_outer_liner]: - return None - assert volume_outer_liner >= volume_inner - return volume_outer_liner - volume_inner - - def get_mass_liner(self): - """ - returns the mass of the liner in kg - """ - volume_liner = self.get_volume_liner() - if volume_liner is None: - return None - return self.density_liner * volume_liner - - def get_cost_liner(self): - """ - returns the cost of the liner material in $ - """ - mass_liner = self.get_mass_liner() - if mass_liner is None: - return None - return self.costrate_liner * mass_liner - - # get the outer dimensions - def get_length_outer(self): - """returns the outer length of the pressure vessel in cm""" - if None in [self.length_inner, self.thickness_liner, self.thickness_jacket]: - return None - return Tank.compute_hemicylinder_outer_length( - self.length_inner, self.thickness_liner + self.thickness_jacket - ) - - def get_radius_outer(self): - """returns the outer radius of the pressure vessel in cm""" - if None in [self.radius_inner, self.thickness_liner, self.thickness_jacket]: - return None - return Tank.compute_hemicylinder_outer_radius( - self.radius_inner, self.thickness_liner + self.thickness_jacket - ) - - def get_volume_outer(self): - """ - returns the outer volume of the pressure vessel in ccm - """ - if None in [ - self.length_inner, - self.radius_inner, - self.thickness_liner, - self.thickness_jacket, - ]: - return None - return Tank.compute_hemicylinder_volume(self.get_radius_outer(), self.get_length_outer()) - - def get_volume_jacket(self): - """ - returns the (unsealed) displacement volume of the carbon fiber jacket in ccm - """ - volume_outer = self.get_volume_outer() - volume_outer_liner = self.get_volume_outer_liner() - if None in [volume_outer, volume_outer_liner]: - return None - assert volume_outer >= volume_outer_liner - return volume_outer - volume_outer_liner - - def get_mass_jacket(self): - """ - returns the mass of the carbon fiber jacket in kg - """ - volume_jacket = self.get_volume_jacket() - if volume_jacket is None: - return None - return self.density_jacket * volume_jacket - - def get_cost_jacket(self): - """ - returns the cost of the jacket material in $ - """ - mass_jacket = self.get_mass_jacket() - if mass_jacket is None: - return None - return self.costrate_jacket * mass_jacket - - def get_mass_tank(self): - """ - returns the mass of the empty tank in kg - """ - mass_liner = self.get_mass_liner() - mass_jacket = self.get_mass_jacket() - if None in [mass_liner, mass_jacket]: - return None - return mass_liner + mass_jacket - - def get_cost_tank(self): - """ - returns the material cost of the tank in $ - """ - cost_liner = self.get_cost_liner() - cost_jacket = self.get_cost_jacket() - if None in [cost_liner, cost_jacket]: - return None - return cost_liner + cost_jacket - - def get_gravimetric_tank_efficiency(self): - """ - returns the gravimetric tank efficiency: - $$ \frac{m_{tank}}{V_{inner}} $$ - in L/kg - """ - mass_tank = self.get_mass_tank() - volume_inner = self.get_volume_inner() - return (volume_inner / 1e3) / mass_tank - - -class TypeIIITank(LinedTank): - def __init__( - self, - conservative=False, - liner_design_load_factor=0.21, - liner_thickness_min=0.3, - yield_factor: float = 3 / 2, - ultimate_factor: float = 2.25, - ): - load_bearing_liner = not conservative # use load bearing liner iff not conservative - super().__init__( - 3, - load_bearing_liner, - liner_design_load_factor, - liner_thickness_min, - yield_factor, - ultimate_factor, - ) - - -class TypeIVTank(LinedTank): - def __init__( - self, - liner_design_load_factor=0.21, - liner_thickness=0.4, - yield_factor: float = 3 / 2, - ultimate_factor: float = 2.25, - ): - super().__init__( - 4, - False, - liner_design_load_factor, - liner_thickness, - yield_factor, - ultimate_factor, - ) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/von_mises.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/von_mises.py deleted file mode 100644 index e52794f32..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/pressure_vessel/von_mises.py +++ /dev/null @@ -1,97 +0,0 @@ -""" -Author: Cory Frontin -Date: 23 Jan 2023 -Institution: National Renewable Energy Lab -Description: This file computes von Mises quantities for hemicylindrical tanks, - replacing Tankinator.xlsx -Sources: - - Tankinator.xlsx -""" - -import numpy as np - - -def S1(p, Re, R0): # von Mises hoop stress - return p * (Re**2 + R0**2) / (Re**2 - R0**2) - - -def S2(p, Re, R0): # von Mises axial stress - return p * R0**2 / (Re**2 - R0**2) - - -def S3(p, Re, R0): # von Mises radial stress - return -p - - -def getPeakStresses(p, Re, R0, proof_factor=3.0 / 2.0, burst_factor=2.25): - aVM = np.sqrt(2) / 2 - bVM = (S2(p, Re, R0) - S1(p, Re, R0)) ** 2 - cVM = (S3(p, Re, R0) - S1(p, Re, R0)) ** 2 - dVM = (S3(p, Re, R0) - S2(p, Re, R0)) ** 2 - eVM = np.sqrt(bVM + cVM + dVM) - Sproof = proof_factor * aVM * eVM - Sburst = burst_factor * aVM * eVM - return (Sproof, Sburst) - - -def wallThicknessAdjustmentFactor( - p, Re, R0, Syield, Sultimate, proof_factor=3.0 / 2.0, burst_factor=2.25 -): - """ - get factor by which to increase thickness when von Mises stresses exceed - material yield safety margins - """ - Sproof, Sburst = getPeakStresses(p, Re, R0, proof_factor, burst_factor) - WTAF_proof = Sproof / Syield - WTAF_burst = Sburst / Sultimate - WTAF = max(WTAF_proof, WTAF_burst) - return WTAF - - -def iterate_thickness( - p, R0, thickness_in, Syield, Sultimate, proof_factor=3.0 / 2.0, burst_factor=2.25 -): - """ - apply the wall thickness adjustment factor, return it w/ new thickness - """ - - Router = R0 + thickness_in - WTAF = wallThicknessAdjustmentFactor( - p, Router, R0, Syield, Sultimate, proof_factor, burst_factor - ) - - return max(1.0, WTAF), max(1.0, WTAF) * thickness_in - - -def cycle( - p, - R0, - thickness_init, - Syield, - Sultimate, - proof_factor=3.0 / 2.0, - burst_factor=2.25, - max_iter=10, - WTAF_tol=1e-6, -): - """ - cycle to find a thickness that satisfies the von Mises criteria - """ - - # compute initial thickness, WTAF - thickness = thickness_init - WTAF = wallThicknessAdjustmentFactor( - p, R0 + thickness, R0, Syield, Sultimate, proof_factor, burst_factor - ) - - # iterate while WTAF is greater than zero - n_iter = 0 - while (WTAF - 1.0 > WTAF_tol) and (n_iter < max_iter): - n_iter += 1 # this cycle iteration number - - # get the next thickness - WTAF, thickness = iterate_thickness( - p, R0, thickness, Syield, Sultimate, proof_factor, burst_factor - ) - - return (thickness, WTAF, n_iter) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/salt_cavern/salt_cavern.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/salt_cavern/salt_cavern.py deleted file mode 100644 index 197ba41ef..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/salt_cavern/salt_cavern.py +++ /dev/null @@ -1,169 +0,0 @@ -""" -Author: Kaitlin Brunik -Created: 7/20/2023 -Institution: National Renewable Energy Lab -Description: This file outputs capital and operational costs of salt cavern hydrogen storage. -It needs to be updated to with operational dynamics. -Costs are in 2018 USD - -Sources: - - [1] Papadias 2021: https://www.sciencedirect.com/science/article/pii/S0360319921030834?via%3Dihub - - [2] Papadias 2021: Bulk Hydrogen as Function of Capacity.docx documentation at - hopp/hydrogen/h2_storage - - [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet -""" - -import numpy as np - -from h2integrate.simulation.technologies.hydrogen.h2_transport.h2_compression import Compressor - - -class SaltCavernStorage: - """ - - Costs are in 2018 USD - """ - - def __init__(self, input_dict): - """ - Initialize SaltCavernStorage. - - Args: - input_dict (dict): - - h2_storage_kg (float): total capacity of hydrogen storage [kg] - - storage_duration_hrs (float): (optional if h2_storage_kg set) [hrs] - - flow_rate_kg_hr (float): (optional if h2_storage_kg set) [kg/hr] - - system_flow_rate (float): [kg/day] - - labor_rate (float): (optional, default: 37.40) [$2018/hr] - - insurance (float): (optional, default: 1%) [decimal percent] - - property_taxes (float): (optional, default: 1%) [decimal percent] - - licensing_permits (float): (optional, default: 0.01%) [decimal percent] - Returns: - - salt_cavern_storage_capex_per_kg (float): the installed capital cost per kg h2 in - 2018 [USD/kg] - - installed_capex (float): the installed capital cost in 2018 [USD] (including - compressor) - - storage_compressor_capex (float): the installed capital cost in 2018 for the - compressor [USD] - - total_opex (float): the OPEX (annual, fixed) in 2018 excluding electricity costs - [USD/kg-yr] - - output_dict (dict): - - salt_cavern_storage_capex (float): installed capital cost in 2018 [USD] - - salt_cavern_storage_opex (float): OPEX (annual, fixed) in 2018 [USD/yr] - """ - self.input_dict = input_dict - self.output_dict = {} - - # inputs - if "h2_storage_kg" in input_dict: - self.h2_storage_kg = input_dict["h2_storage_kg"] # [kg] - elif "storage_duration_hrs" and "flow_rate_kg_hr" in input_dict: - self.h2_storage_kg = input_dict["storage_duration_hrs"] * input_dict["flow_rate_kg_hr"] - else: - raise Exception( - "input_dict must contain h2_storage_kg or storage_duration_hrs and flow_rate_kg_hr" - ) - - if "system_flow_rate" not in input_dict.keys(): - raise ValueError("system_flow_rate required for salt cavern storage model.") - else: - self.system_flow_rate = input_dict["system_flow_rate"] - - self.labor_rate = input_dict.get("labor_rate", 37.39817) # $(2018)/hr - self.insurance = input_dict.get("insurance", 1 / 100) # % of total capital investment - self.property_taxes = input_dict.get( - "property_taxes", 1 / 100 - ) # % of total capital investment - self.licensing_permits = input_dict.get( - "licensing_permits", 0.1 / 100 - ) # % of total capital investment - self.comp_om = input_dict.get( - "compressor_om", 4 / 100 - ) # % of compressor capital investment - self.facility_om = input_dict.get( - "facility_om", 1 / 100 - ) # % of facility capital investment minus compressor capital investment - - def salt_cavern_capex(self): - """ - Calculates the installed capital cost of salt cavern hydrogen storage - Returns: - - salt_cavern_capex_per_kg (float): the installed capital cost per kg h2 in 2018 - [USD/kg] - - installed_capex (float): the installed capital cost in 2018 [USD] (including - compressor) - - storage_compressor_capex (float): the installed capital cost in 2018 for the - compressor [USD] - - output_dict (dict): - - salt_cavern_capex (float): installed capital cost in 2018 [USD] - """ - - # Installed capital cost - a = 0.092548 - b = 1.6432 - c = 10.161 - self.salt_cavern_storage_capex_per_kg = np.exp( - a * (np.log(self.h2_storage_kg / 1000)) ** 2 - b * np.log(self.h2_storage_kg / 1000) + c - ) # 2019 [USD] from Papadias [2] - self.installed_capex = self.salt_cavern_storage_capex_per_kg * self.h2_storage_kg - cepci_overall = 1.29 / 1.30 # Convert from $2019 to $2018 - self.installed_capex = cepci_overall * self.installed_capex - self.output_dict["salt_cavern_storage_capex"] = self.installed_capex - - outlet_pressure = 120 # Max outlet pressure of salt cavern in [1] - n_compressors = 2 - storage_compressor = Compressor( - outlet_pressure, self.system_flow_rate, n_compressors=n_compressors - ) - storage_compressor.compressor_power() - motor_rating, power = storage_compressor.compressor_system_power() - if motor_rating > 1600: - n_compressors += 1 - storage_compressor = Compressor( - outlet_pressure, self.system_flow_rate, n_compressors=n_compressors - ) - storage_compressor.compressor_power() - motor_rating, power = storage_compressor.compressor_system_power() - comp_capex, comp_OM = storage_compressor.compressor_costs() - cepci = 1.36 / 1.29 # convert from $2016 to $2018 - self.comp_capex = comp_capex * cepci - - return ( - self.salt_cavern_storage_capex_per_kg, - self.installed_capex, - self.comp_capex, - ) - - def salt_cavern_opex(self): - """ - Calculates the operation and maintenance costs excluding electricity costs for the salt - cavern hydrogen storage - - Returns: - - total_opex (float): the OPEX (annual, fixed) in 2018 excluding electricity costs - [USD/kg-yr] - - output_dict (dict): - - salt_cavern_storage_opex (float): OPEX (annual, fixed) in 2018 [USD/yr] - """ - # Operations and Maintenace costs [3] - # Labor - # Base case is 1 operator, 24 hours a day, 7 days a week for a 100,000 kg/day average - # capacity facility. Scaling factor of 0.25 is used for other sized facilities - annual_hours = 8760 * (self.system_flow_rate / 100000) ** 0.25 - self.overhead = 0.5 - labor = (annual_hours * self.labor_rate) * (1 + self.overhead) # Burdened labor cost - insurance = self.insurance * self.installed_capex - property_taxes = self.property_taxes * self.installed_capex - licensing_permits = self.licensing_permits * self.installed_capex - comp_op_maint = self.comp_om * self.comp_capex - facility_op_maint = self.facility_om * (self.installed_capex - self.comp_capex) - - # O&M excludes electricity requirements - total_om = ( - labor - + insurance - + licensing_permits - + property_taxes - + comp_op_maint - + facility_op_maint - ) - self.output_dict["salt_cavern_storage_opex"] = total_om - return total_om diff --git a/h2integrate/simulation/technologies/hydrogen/h2_storage/storage_sizing.py b/h2integrate/simulation/technologies/hydrogen/h2_storage/storage_sizing.py deleted file mode 100644 index 4fa94b4ea..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_storage/storage_sizing.py +++ /dev/null @@ -1,84 +0,0 @@ -import numpy as np - - -def hydrogen_storage_capacity(H2_Results, electrolyzer_size_mw, hydrogen_demand_kgphr): - """Calculate storage capacity based on hydrogen demand and production. - - Args: - H2_Results (dict): Dictionary including electrolyzer physics results. - electrolyzer_size_mw (float): Electrolyzer size in MW. - hydrogen_demand_kgphr (float): Hydrogen demand in kg/hr. - - Returns: - hydrogen_demand_kgphr (list): Hydrogen hourly demand in kilograms per hour. - hydrogen_storage_capacity_kg (float): Hydrogen storage capacity in kilograms. - hydrogen_storage_duration_hr (float): Hydrogen storage duration in hours using HHV/LHV. - hydrogen_storage_soc (list): Timeseries of the hydrogen storage state of charge. - """ - - hydrogen_production_kgphr = H2_Results["Hydrogen Hourly Production [kg/hr]"] - - hydrogen_demand_kgphr = max( - hydrogen_demand_kgphr, np.mean(hydrogen_production_kgphr) - ) # TODO: potentially add buffer No buffer needed since we are already oversizing - - # TODO: SOC is just an absolute value and is not a percentage. Ideally would calculate as shortfall in future. - hydrogen_storage_soc = [] - for j in range(len(hydrogen_production_kgphr)): - if j == 0: - hydrogen_storage_soc.append(hydrogen_production_kgphr[j] - hydrogen_demand_kgphr) - else: - hydrogen_storage_soc.append( - hydrogen_storage_soc[j - 1] + hydrogen_production_kgphr[j] - hydrogen_demand_kgphr - ) - - minimum_soc = np.min(hydrogen_storage_soc) - - # adjust soc so it's not negative. - if minimum_soc < 0: - hydrogen_storage_soc = [x + np.abs(minimum_soc) for x in hydrogen_storage_soc] - - hydrogen_storage_capacity_kg = np.max(hydrogen_storage_soc) - np.min(hydrogen_storage_soc) - h2_LHV = 119.96 # MJ/kg - h2_HHV = 141.88 # MJ/kg - hydrogen_storage_capacity_MWh_LHV = hydrogen_storage_capacity_kg * h2_LHV / 3600 - hydrogen_storage_capacity_kg * h2_HHV / 3600 - - # # Get max injection/withdrawal rate - # hydrogen_injection_withdrawal_rate = [] - # for j in range(len(hydrogen_production_kgphr)): - # hydrogen_injection_withdrawal_rate.append( - # hydrogen_production_kgphr[j] - hydrogen_demand_kgphr - # ) - # max_h2_injection_rate_kgphr = max(hydrogen_injection_withdrawal_rate) - - # # Get storage compressor capacity. TODO: sync compressor calculation here with H2Integrate - # compressor model - # compressor_total_capacity_kW = ( - # max_h2_injection_rate_kgphr / 3600 / 2.0158 * 8641.678424 - # ) - - # compressor_max_capacity_kw = 16000 - # n_comps = math.ceil(compressor_total_capacity_kW / compressor_max_capacity_kw) - - # small_positive = 1e-6 - # compressor_avg_capacity_kw = compressor_total_capacity_kW / ( - # n_comps + small_positive - # ) - - # Get average electrolyzer efficiency - electrolyzer_average_efficiency_HHV = H2_Results["Sim: Average Efficiency [%-HHV]"] - - # Calculate storage durationhyd - hydrogen_storage_duration_hr = ( - hydrogen_storage_capacity_MWh_LHV - / electrolyzer_size_mw - / electrolyzer_average_efficiency_HHV - ) - - return ( - np.ones_like(hydrogen_storage_capacity_kg) * hydrogen_demand_kgphr, - hydrogen_storage_capacity_kg, - hydrogen_storage_duration_hr, - hydrogen_storage_soc, - ) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_transport/h2_export_pipe.py b/h2integrate/simulation/technologies/hydrogen/h2_transport/h2_export_pipe.py deleted file mode 100644 index 70d70f6dd..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_transport/h2_export_pipe.py +++ /dev/null @@ -1,436 +0,0 @@ -""" -Author: Jamie Kee -Added to HOPP by: Jared Thomas -Note: ANL costs are in 2018 dollars - -07/15/2024: Jamie removed Z=0.9 assumption with linear approx, -removed f=0.01 assumption with Hofer eqn, added -algebraic solver, and reformatted with black. -08/02/2024: Provide cost overrides -""" - -from __future__ import annotations - -from pathlib import Path - -import numpy as np -import pandas as pd - - -BAR2MPA = 0.1 -BAR2PA = 100_000 -MM2IN = 0.0393701 -M2KM = 1 / 1_000 -M2MM = 1_000 -KM2MI = 0.621371 - - -def run_pipe_analysis( - L: float, - m_dot: float, - p_inlet: float, - p_outlet: float, - depth: float, - risers: int = 1, - data_location: str | Path = Path(__file__).parent / "data_tables", - labor_in_mi: float | None = None, - misc_in_mi: float | None = None, - row_in_mi: float | None = None, - mat_in_mi: float | None = None, - region: str = "SW", -): - """ - This function calculates the cheapest grade, diameter, thickness, subject to ASME B31.12 and .8 - - If $/in/mi values are provided in labor_in_mi, misc_in_mi, row_in_mi, mat_in_mi, those values - will be used in the cost calculations instead of the defaults - """ - if isinstance(data_location, str): - data_location = Path(data_location).resolve() - p_inlet_MPa = p_inlet * BAR2MPA - F = 0.72 # Design option B class 1 - 2011 ASME B31.12 Table PL-3.7.1.2 - E = 1.0 # Long. Weld Factor: Seamless (Table IX-3B) - T_derating = 1 # 2020 ASME B31.8 Table A841.1.8-1 for T<250F, 121C - - # Cost overrides - anl_cost_overrides = {"labor": labor_in_mi, "misc": misc_in_mi, "ROW": row_in_mi} - - # This is a flag for the ASMEB31.8 stress design, if not including risers, then this can be set - # to false - riser = risers > 0 - extra_length = 1 + 0.05 # 5% extra - - # Assuming 5% extra length and 1 riser. Will need two risers for turbine to central platform - total_L = L * extra_length + risers * depth * M2KM # km - - # Import mechanical props and pipe thicknesses (remove A,B ,and A25 since no costing data) - yield_strengths = pd.read_csv( - data_location / "steel_mechanical_props.csv", - index_col=None, - header=0, - ) - yield_strengths = yield_strengths.loc[ - ~yield_strengths["Grade"].isin(["A", "B", "A25"]) - ].reset_index() - schedules_all = pd.read_csv( - data_location / "pipe_dimensions_metric.csv", - index_col=None, - header=0, - ) - steel_costs_kg = pd.read_csv(data_location / "steel_costs_per_kg.csv", index_col=None, header=0) - - # First get the minimum diameter required to achieve the outlet pressure for given length and - # m_dot - min_diam_mm = get_min_diameter_of_pipe(L=L, m_dot=m_dot, p_inlet=p_inlet, p_outlet=p_outlet) - # Filter for diameters larger than min diam required - schedules_spec = schedules_all.loc[schedules_all["DN"] >= (min_diam_mm)] - - # Gather the grades, diameters, and schedules to loop thru - grades = yield_strengths["Grade"].values - diams = schedules_spec["Outer diameter [mm]"].values - schds = schedules_spec.loc[ - :, ~schedules_spec.columns.isin(["DN", "Outer diameter [mm]"]) - ].columns - viable_types = [] - - # Loop thru grades - for grade in grades: - # Get SMYS and SMTS for the specific grade - SMYS = yield_strengths.loc[yield_strengths["Grade"] == grade, "SMYS [Mpa]"].values[0] - SMTS = yield_strengths.loc[yield_strengths["Grade"] == grade, "SMTS [Mpa]"].values[0] - # Loop thru outer diameters - for diam in diams: - diam_row = schedules_spec.loc[schedules_spec["Outer diameter [mm]"] == diam] - dn = diam_row["DN"].values[0] - # Loop thru scheudles (which give the thickness) - for schd in schds: - thickness = diam_row[schd].values[0] - - # Check if thickness satisfies ASME B31.12 - mat_perf_factor = get_mat_factor( - SMYS=SMYS, SMTS=SMTS, design_pressure=p_inlet * BAR2MPA - ) - t_ASME = p_inlet_MPa * dn / (2 * SMYS * F * E * mat_perf_factor) - if thickness < t_ASME: - continue - - # Check if satifies ASME B31.8 - if not checkASMEB318( - SMYS=SMYS, - diam=diam, - thickness=thickness, - riser=riser, - depth=depth, - p_inlet=p_inlet, - T_derating=T_derating, - ): - continue - - # Add qualified pipes to saved answers: - inner_diam = diam - 2 * thickness - viable_types.append([grade, dn, diam, inner_diam, schd, thickness]) - - viable_types_df = pd.DataFrame( - viable_types, - columns=[ - "Grade", - "DN", - "Outer diameter (mm)", - "Inner diameter (mm)", - "Schedule", - "Thickness (mm)", - ], - ).dropna() - - # Calculate material, labor, row, and misc costs - viable_types_df = get_mat_costs( - schedules_spec=viable_types_df, - total_L=total_L, - steel_costs_kg=steel_costs_kg, - mat_cost_override=mat_in_mi, - ) - viable_types_df = get_anl_costs( - costs=viable_types_df, - total_L=total_L, - anl_cost_overrides=anl_cost_overrides, - loc=region, - ) - viable_types_df["total capital cost [$]"] = viable_types_df[ - ["mat cost [$]", "labor cost [$]", "misc cost [$]", "ROW cost [$]"] - ].sum(axis=1) - - # Annual operating cost assumes 1.17% of total capital - # https://doi.org/10.1016/j.esr.2021.100658 - viable_types_df["annual operating cost [$]"] = ( - 0.0117 * viable_types_df["total capital cost [$]"] - ) - - # Take the option with the lowest total capital cost - min_row = viable_types_df.sort_values(by="total capital cost [$]").iloc[:1].reset_index() - return min_row - - -def get_mat_factor(SMYS: float, SMTS: float, design_pressure: float) -> float: - """ - Determine the material performance factor ASMEB31.12. - Dependent on the SMYS and SMTS. - Defaulted to 1 if not within parameters - This may not be a good assumption - """ - dp_array = np.array([6.8948, 13.7895, 15.685, 16.5474, 17.9264, 19.3053, 20.6843]) # MPa - if SMYS <= 358.528 or SMTS <= 455.054: - h_f_array = np.array([1, 1, 0.954, 0.91, 0.88, 0.84, 0.78]) - elif SMYS <= 413.686 and (SMTS > 455.054 and SMTS <= 517.107): - h_f_array = np.array([0.874, 0.874, 0.834, 0.796, 0.77, 0.734, 0.682]) - elif SMYS <= 482.633 and (SMTS > 517.107 and SMTS <= 565.370): - h_f_array = np.array([0.776, 0.776, 0.742, 0.706, 0.684, 0.652, 0.606]) - elif SMYS <= 551.581 and (SMTS > 565.370 and SMTS <= 620.528): - h_f_array = np.array([0.694, 0.694, 0.662, 0.632, 0.61, 0.584, 0.542]) - else: - return 1 - mat_perf_factor = np.interp(design_pressure, dp_array, h_f_array) - return mat_perf_factor - - -def checkASMEB318( - SMYS: float, - diam: float, - thickness: float, - riser: bool, - depth: float, - p_inlet: float, - T_derating: float, -) -> bool: - """ - Determine if pipe parameters satisfy hoop and longitudinal stress requirements - """ - - # Hoop Stress - 2020 ASME B31.8 Table A842.2.2-1 - F1 = 0.50 if riser else 0.72 - - # Hoop stress (MPa) - 2020 ASME B31.8 section A842.2.2.2 eqn (1) - # This is the maximum value for S_h - # Sh <= F1*SMYS*T_derating - S_h_check = F1 * SMYS * T_derating - - # Hoop stress (MPa) - rho_water = 1_000 # kg/m3 - p_hydrostatic = rho_water * 9.81 * depth / BAR2PA # bar - dP = (p_inlet - p_hydrostatic) * BAR2MPA # MPa - S_h = dP * (diam - (thickness if diam / thickness >= 30 else 0)) / (2_000 * thickness) - if S_h >= S_h_check: - return False - - # Longitudinal stress (MPa) - S_L_check = 0.8 * SMYS # 2020 ASME B31.8 Table A842.2.2-1. Same for riser and pipe - S_L = p_inlet * BAR2MPA * (diam - 2 * thickness) / (4 * thickness) - if S_L > S_L_check: - return False - - (0.9 * SMYS) # 2020 ASME B31.8 Table A842.2.2-1. Same for riser and pipe - # Torsional stress?? Under what applied torque? Not sure what to do for this. - - return True - - -def get_anl_costs( - costs: pd.DataFrame, total_L: float, anl_cost_overrides: dict, loc: str = "SW" -) -> pd.DataFrame: - """ - Calculates the labor, right-of-way (ROW), and miscellaneous costs associated with pipe capital - cost - - Users can specify a region (GP,NE,MA,GL,RM,SE,PN,SW,CA) that corresponds to grouping of states - which will apply cost correlations from Brown, D., et al. 2022. “The Development of Natural Gas - and Hydrogen Pipeline Capital Cost Estimating Equations.” International Journal of Hydrogen - Energy https://doi.org/10.1016/j.ijhydene.2022.07.270. - - Alternatively, if a value (not None) is provided in anl_cost_overrides, that value be used as - the $/in/mi cost correlation for the relevant cost type. - """ - - ANL_COEFS = { - "GP": { - "labor": [10406, 0.20953, -0.08419], - "misc": [4944, 0.17351, -0.07621], - "ROW": [2751, -0.28294, 0.00731], - "material": [5813, 0.31599, -0.00376], - }, - "NE": { - "labor": [249131, -0.33162, -0.17892], - "misc": [65990, -0.29673, -0.06856], - "ROW": [83124, -0.66357, -0.07544], - "material": [10409, 0.296847, -0.07257], - }, - "MA": { - "labor": [43692, 0.05683, -0.10108], - "misc": [14616, 0.16354, -0.16186], - "ROW": [1942, 0.17394, -0.01555], - "material": [9113, 0.279875, -0.00840], - }, - "GL": { - "labor": [58154, -0.14821, -0.10596], - "misc": [41238, -0.34751, -0.11104], - "ROW": [14259, -0.65318, 0.06865], - "material": [8971, 0.255012, -0.03138], - }, - "RM": { - "labor": [10406, 0.20953, -0.08419], - "misc": [4944, 0.17351, -0.07621], - "ROW": [2751, -0.28294, 0.00731], - "material": [5813, 0.31599, -0.00376], - }, - "SE": { - "labor": [32094, 0.06110, -0.14828], - "misc": [11270, 0.19077, -0.13669], - "ROW": [9531, -0.37284, 0.02616], - "material": [6207, 0.38224, -0.05211], - }, - "PN": { - "labor": [32094, 0.06110, -0.14828], - "misc": [11270, 0.19077, -0.13669], - "ROW": [9531, -0.37284, 0.02616], - "material": [6207, 0.38224, -0.05211], - }, - "SW": { - "labor": [95295, -0.53848, 0.03070], - "misc": [19211, -0.14178, -0.04697], - "ROW": [72634, -1.07566, 0.05284], - "material": [5605, 0.41642, -0.06441], - }, - "CA": { - "labor": [95295, -0.53848, 0.03070], - "misc": [19211, -0.14178, -0.04697], - "ROW": [72634, -1.07566, 0.05284], - "material": [5605, 0.41642, -0.06441], - }, - } - - if loc not in ANL_COEFS.keys(): - raise ValueError(f"Region {loc} was supplied, but is not a valid region") - - L_mi = total_L * KM2MI - - def cost_per_in_mi(coef: list, DN_in: float, L_mi: float) -> float: - return coef[0] * DN_in ** coef[1] * L_mi ** coef[2] - - diam_col = "DN" - for cost_type in ["labor", "misc", "ROW"]: - cost_per_in_mi_val = anl_cost_overrides[cost_type] - # If no override specified, use defaults - if cost_per_in_mi_val is None: - cost_per_in_mi_val = costs.apply( - lambda x: cost_per_in_mi(ANL_COEFS[loc][cost_type], x[diam_col] * MM2IN, L_mi), - axis=1, - ) - costs[f"{cost_type} cost [$]"] = cost_per_in_mi_val * costs[diam_col] * MM2IN * L_mi - - return costs - - -def get_mat_costs( - schedules_spec: pd.DataFrame, - total_L: float, - steel_costs_kg: pd.DataFrame, - mat_cost_override: float, -): - """ - Calculates the material cost based on $/kg from Savoy for each grade - Inc., S. P. Live Stock List & Current Price. - https://www.savoypipinginc.com/blog/live-stock-and-current-price.html. - Accessed September 22, 2022. - - Users can alternatively provide a $/in/mi override to calculate material cost - """ - rho_steel = 7840 # kg/m3 - L_m = total_L / M2KM - L_mi = total_L * KM2MI - - def get_volume(od_mm: float, id_mm: float, L_m: float) -> float: - return np.pi / 4 * (od_mm**2 - id_mm**2) / M2MM**2 * L_m - - od_col = "Outer diameter (mm)" - id_col = "Inner diameter (mm)" - schedules_spec["volume [m3]"] = schedules_spec.apply( - lambda x: get_volume(x[od_col], x[id_col], L_m), - axis=1, - ) - schedules_spec["weight [kg]"] = schedules_spec["volume [m3]"] * rho_steel - - # If mat cost override is not specified, use $/kg savoy costing - if mat_cost_override is not None: - schedules_spec["mat cost [$]"] = mat_cost_override * L_mi * schedules_spec["DN"] * MM2IN - else: - schedules_spec["mat cost [$]"] = schedules_spec.apply( - lambda x: x["weight [kg]"] - * steel_costs_kg.loc[steel_costs_kg["Grade"] == x["Grade"], "Price [$/kg]"].values[0], - axis=1, - ) - - return schedules_spec - - -def get_min_diameter_of_pipe(L: float, m_dot: float, p_inlet: float, p_outlet: float) -> float: - """ - Overview: - --------- - This function returns the diameter of a pipe for a given length,flow rate, and pressure - boundaries - - Parameters: - ----------- - L : float - Length of pipeline [km] - m_dot : float = Mass flow rate [kg/s] - p_inlet : float = Pressure at inlet of pipe [bar] - p_outlet : float = Pressure at outlet of pipe [bar] - - Returns: - -------- - diameter_mm : float - Diameter of pipe [mm] - - """ - - p_in_Pa = p_inlet * BAR2PA - p_out_Pa = p_outlet * BAR2PA - - p_avg = 2 / 3 * (p_in_Pa + p_out_Pa - p_in_Pa * p_out_Pa / (p_in_Pa + p_out_Pa)) - p_diff = (p_in_Pa**2 - p_out_Pa**2) ** 0.5 - T = 15 + 273.15 # Temperature [K] - R = 8.314 # J/mol-K - z_fit_params = (6.5466916131e-9, 9.9941320278e-1) # Slope fit for 15C - z = z_fit_params[0] * p_avg + z_fit_params[1] - zrt = z * R * T - mw = 2.016 / 1_000 # kg/mol for hydrogen - RO = 0.012 # mm Roughness - mu = 8.764167e-6 # viscosity - L_m = L / M2KM - - f_list = [0.01] - - # Diameter depends on Re and f, but are functions of d. So use initial guess - # of f-0.01, then iteratively solve until f is no longer changing - err = np.inf - max_iter = 50 - while err > 0.001: - d_m = (m_dot / p_diff * 4 / np.pi * (mw / zrt / f_list[-1] / L_m) ** (-0.5)) ** (1 / 2.5) - d_mm = d_m * M2MM - Re = 4 * m_dot / (np.pi * d_m * mu) - f_list.append((-2 * np.log10(4.518 / Re * np.log10(Re / 7) + RO / (3.71 * d_mm))) ** (-2)) - err = abs((f_list[-1] - f_list[-2]) / f_list[-2]) - - # Error out if no solution after max iterations - if len(f_list) > max_iter: - raise ValueError(f"Could not find pipe diameter in {max_iter} iterations") - - return d_mm - - -if __name__ == "__main__": - L = 8 # Length [km] - m_dot = 1.5 # Mass flow rate [kg/s] assuming 300 MW -> 1.5 kg/s - p_inlet = 30 # Inlet pressure [bar] - p_outlet = 10 # Outlet pressure [bar] - depth = 80 # depth of pipe [m] - costs = run_pipe_analysis(L, m_dot, p_inlet, p_outlet, depth) - - for col in costs.columns: - print(col, costs[col][0]) diff --git a/h2integrate/simulation/technologies/hydrogen/h2_transport/h2_pipe_array.py b/h2integrate/simulation/technologies/hydrogen/h2_transport/h2_pipe_array.py deleted file mode 100644 index a0ff76e79..000000000 --- a/h2integrate/simulation/technologies/hydrogen/h2_transport/h2_pipe_array.py +++ /dev/null @@ -1,126 +0,0 @@ -from numpy import flip, isnan, nansum - -from h2integrate.simulation.technologies.hydrogen.h2_transport.h2_export_pipe import ( - run_pipe_analysis, -) - - -""" -Args: - sections_distance (array[array]): array of arrays where each element of each sub-array holds the - horizontal distance in m of a pipe section - depth (float): depth of the site in m - p_inlet (float): pipe inlet pressure in bar - p_outlet (float): pipe outlet pressure in bar - mass_flow_rate_inlet (float): flow rate at each inlet to the system -Returns: - capex (float): total capital costs (USD) including labor, materials, and misc - opex (float): annual operating costs (USD) -""" - - -def run_pipe_array(sections_distance, depth, p_inlet, p_outlet, mass_flow_rate): - capex = 0 - opex = 0 - - # loop over each string - for i, pipe_string in enumerate(sections_distance): - # initialize values for each string - m_dot = 0 - p_drop = (p_inlet - p_outlet) / len(pipe_string) - flow_rates = flip(mass_flow_rate[i]) - - # loop over each section - for j, section_length in enumerate(flip(pipe_string)): - # nan represents an empty section (no pipe there, but array cannot be ragged) - if isnan(section_length): - continue - - # get mass flow rate for current section - m_dot += flow_rates[j] - - # get outlet pressure for current section - p_outlet_section = p_inlet - (j + 1) * p_drop - - # get number of risers for current section - if j == len(pipe_string) - 1: - risers = 2 - else: - risers = 1 - - # get specs and costs for each section - section_outputs = run_pipe_analysis( - section_length, m_dot, p_inlet, p_outlet_section, depth, risers=risers - ) - - capex += section_outputs["total capital cost [$]"][0] - opex += section_outputs["annual operating cost [$]"][0] - - return capex, opex - - -# Assuming one pipe diameter for the pipeline -def run_pipe_array_const_diam(sections_distance, depth, p_inlet, p_outlet, mass_flow_rate): - capex = 0 - opex = 0 - - # loop over each string - for i, pipe_string in enumerate(sections_distance): - # Calculate maximum flow rate per pipe segment (pipe is sized to largest segment) - m_dot = max(mass_flow_rate[i]) - - # Add up the length of the segment - tot_length = nansum(pipe_string) - - # Assume each full run has 2 risers - risers = 2 - risers = ( - len(pipe_string) + 1 - ) # Wasnt sure on this - is it 1 per turbine + 1 for the storage? - Jamie - - # get specs and costs for each section - section_outputs = run_pipe_analysis( - tot_length, m_dot, p_inlet, p_outlet, depth, risers=risers - ) - - capex += section_outputs["total capital cost [$]"][0] - opex += section_outputs["annual operating cost [$]"][0] - - return capex, opex - - -if __name__ == "__main__": - sections_distance = [ - [2.85105454, 2.016, 2.016, 2.016, 2.016, 2.016, 2.016, 2.016], - [2.016, 2.016, 2.016, 2.016, 2.016, 2.016, 2.016, 2.016], - [ - 2.85105454, - 2.016, - 2.016, - 2.016, - float("nan"), - float("nan"), - float("nan"), - float("nan"), - ], - ] - - L = 8 # Length [km] - m_dot = 1.5 # Mass flow rate [kg/s] assuming 300 MW -> 1.5 kg/s - p_inlet = 30 # Inlet pressure [bar] - p_outlet = 10 # Outlet pressure [bar] - depth = 80 # depth of pipe [m] - - # capex, opex = run_pipe_array( - # [[L, L], [L, L]], depth, p_inlet, p_outlet, [[m_dot, m_dot], [m_dot, m_dot]] - # ) - - # print("CAPEX (USD): ", capex) - # print("OPEX (USD): ", opex) - - capex, opex = run_pipe_array_const_diam( - [[L, L], [L, L]], depth, p_inlet, p_outlet, [[m_dot, m_dot], [m_dot, m_dot]] - ) - - print("CAPEX (USD): ", capex) - print("OPEX (USD): ", opex) diff --git a/h2integrate/simulation/technologies/iron/martin_transport/__init__.py b/h2integrate/simulation/technologies/iron/martin_transport/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/h2integrate/simulation/technologies/iron/rosner/__init__.py b/h2integrate/simulation/technologies/iron/rosner/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/h2integrate/simulation/technologies/iron/rosner_ore/__init__.py b/h2integrate/simulation/technologies/iron/rosner_ore/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/h2integrate/simulation/technologies/iron/rosner_override/__init__.py b/h2integrate/simulation/technologies/iron/rosner_override/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/h2integrate/simulation/technologies/offshore/__init__.py b/h2integrate/simulation/technologies/offshore/__init__.py deleted file mode 100644 index 719969766..000000000 --- a/h2integrate/simulation/technologies/offshore/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from h2integrate.simulation.technologies.offshore.fixed_platform import ( - FixedPlatformDesign, - FixedPlatformInstallation, -) -from h2integrate.simulation.technologies.offshore.floating_platform import ( - FloatingPlatformDesign, - FloatingPlatformInstallation, -) diff --git a/h2integrate/simulation/technologies/offshore/all_platforms.py b/h2integrate/simulation/technologies/offshore/all_platforms.py deleted file mode 100644 index 1b4606a9f..000000000 --- a/h2integrate/simulation/technologies/offshore/all_platforms.py +++ /dev/null @@ -1,65 +0,0 @@ -import math - - -def calc_platform_opex(capex, opex_rate=0.011): - """ - Simple opex calculation based on a capex - https://www.acm.nl/sites/default/files/documents/study-on-estimation-method-for-additional-efficient-offshore-grid-opex.pdf - - Output in $USD/year - """ - - opex = capex * opex_rate # USD/year - - return opex - - -def install_platform(mass, area, distance, install_duration=14, vessel=None, foundation="fixed"): - """ - A simplified platform installation costing model. - Total Cost = install_cost * duration - Compares the mass and/or deck space of equipment to the vessel limits to determine - the number of trips. Add an additional "at sea" install duration - - """ - - # If no ORBIT vessel is defined set default values (based on ORBIT's floating_heavy_lift_vessel) - if vessel is None: - if foundation == "fixed": - # If no ORBIT vessel is defined set default values (based on ORBIT's - # example_heavy_lift_vessel) - # Default values are from [3]. - vessel_cargo_mass = 7999 # t - vessel_deck_space = 3999 # m**2 - vessel_day_rate = 500001 # USD/day - vessel_speed = 5 # km/hr - elif foundation == "floating": - # If no ORBIT vessel is defined set default values (based on ORBIT's - # floating_heavy_lift_vessel) - vessel_cargo_mass = 7999 # t - vessel_deck_space = 3999 # m**2 - vessel_day_rate = 500001 # USD/day - vessel_speed = 7 # km/hr - else: - raise ( - ValueError( - "Invalid offshore platform foundation type. Must be one of" - " ['fixed', 'floating']" - ) - ) - else: - vessel_cargo_mass = vessel.storage.max_cargo_mass # t - vessel_deck_space = vessel.storage.max_deck_space # m**2 - vessel_day_rate = vessel.day_rate # USD/day - vessel_speed = vessel.transit_speed # km/hr - - # Get the # of trips based on ships cargo/space limits - num_of_trips = math.ceil(max((mass / vessel_cargo_mass), (area / vessel_deck_space))) - - # Total duration = double the trips + install_duration - duration = (2 * num_of_trips * distance) / (vessel_speed * 24) + install_duration # days\ - - # Final install cost is obtained by using the vessel's daily rate - install_cost = vessel_day_rate * duration # USD - - return install_cost diff --git a/h2integrate/simulation/technologies/offshore/example_fixed_project.yaml b/h2integrate/simulation/technologies/offshore/example_fixed_project.yaml deleted file mode 100644 index 71a51723f..000000000 --- a/h2integrate/simulation/technologies/offshore/example_fixed_project.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Modified orbit configuration file for a single platform to carry "X technology" -design_phases: -- FixedPlatformDesign # Register Design Phase -install_phases: - FixedPlatformInstallation: 0 # Register Install Phase -oss_install_vessel: example_heavy_lift_vessel -site: - depth: 22.5 # site depth [m] - distance: 124 # distance to port [km] -equipment: - tech_required_area: 300. # equipment area [m**2] - tech_combined_mass: 1000 # equipment mass [t] - topside_design_cost: 4500000 # topside design cost [USD] - installation_duration: 14 # time at sea [days] - -# set input values to -1 to use values calculated or input in other files during H2Integrate run (depth, distance, tech_required_area, tech_combined_mass) diff --git a/h2integrate/simulation/technologies/offshore/example_floating_project.yaml b/h2integrate/simulation/technologies/offshore/example_floating_project.yaml deleted file mode 100644 index d1ae52755..000000000 --- a/h2integrate/simulation/technologies/offshore/example_floating_project.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Modified orbit configuration file for a single platform to carry "X technology" -design_phases: -- FloatingPlatformDesign # Resgister Design Phase -install_phases: - FloatingPlatformInstallation: 0 # Register Install Phase -oss_install_vessel: example_heavy_lift_vessel -site: - depth: 500.5 # site depth [m] Site depths for floating projects need to be at depths 500 m to 1500 m because of Orbit SemiTaut branch limitations (7/31) - distance: 124 # distance to port [km] -equipment: - tech_required_area: 300. # equipment area [m**2] - tech_combined_mass: 1000 # equipment mass [t] - topside_design_cost: 4500000 # topside design cost [USD] - installation_duration: 14 # time at sea [days] - -# set input values to -1 to use values calculated or input in other files during H2Integrate run (depth, distance, tech_required_area, tech_combined_mass) diff --git a/h2integrate/simulation/technologies/offshore/fixed_platform.py b/h2integrate/simulation/technologies/offshore/fixed_platform.py deleted file mode 100644 index 842f77c86..000000000 --- a/h2integrate/simulation/technologies/offshore/fixed_platform.py +++ /dev/null @@ -1,311 +0,0 @@ -from __future__ import annotations - - -""" -Author: Nick Riccobono and Charles Kiefer -Date: 1/31/2023 -Institution: National Renewable Energy Lab -Description: This file should handles the cost and sizing of a centralized offshore platform - dedicated to hydrogen production. It has been modeled off of existing BOS cost/sizing - calculations found in ORBIT (Thank you Jake Nunemaker). It can be run as standalone functions - or as appended ORBIT project phases. - - -Sources: - - [1] ORBIT: https://github.com/WISDEM/ORBIT electrical_refactor branch - - [2] J. Nunemaker, M. Shields, R. Hammond, and P. Duffy, - “ORBIT: Offshore Renewables Balance-of-System and Installation Tool,” - NREL/TP-5000-77081, 1660132, MainId:26027, Aug. 2020. doi: 10.2172/1660132. - - [3] M. Maness, B. Maples, and A. Smith, - “NREL Offshore Balance-of-System Model,” - NREL/TP--6A20-66874, 1339522, Jan. 2017. doi: 10.2172/1339522. -Args: - - tech_required_area: (float): area needed for combination of all tech (m^2), not including - buffer or working space - - tech_combined_mass: (float): mass of all tech being placed on the platform (kg or metric tons) - - - depth: (float): bathometry at the platform location (m) - - distance: (float): distance ships must travel from port to site location (km) - - Future arguments: (Not used at this time) - - construction year (int): - - lifetime (int): lifetime of the plant in years (may not be needed) - - Assembly costs and construction on land - -Returns: - - platform_mass (float): Adjusted mass of platform + substructure - - design_capex (float): capital expenditures (platform design + substructure fabrication) - - installation_capex (float): capital expenditures (installation cost) - - platform_opex (float): the OPEX (annual, fixed) in USD for the platform - -""" - -""" -Notes: - - Thank you Jake Nunemaker's oswh2 repository!!! - - pile_cost=0 $US/metric ton for monopile construction. Not a bug, this # is - consistent with the rest of ORBIT [1]. -""" - - -import typing -from pathlib import Path - -import ORBIT as orbit - -from h2integrate.simulation.technologies.offshore.all_platforms import ( - install_platform, - calc_platform_opex, -) - - -class FixedPlatformDesign(orbit.phases.design.DesignPhase): - """ - This is a modified class based on ORBIT's [1] design phase. The implementation - is discussed in [2], Section 2.5: Offshore Substation Design. Default values originate - from [3], Appendix A: Inputs, Key Assumptions and Caveats. - """ - - # phase = "H2 Fixed Platform Design" - - # Expected inputs from config yaml file - expected_config: typing.ClassVar = { - "site": { - "distance": "int | float", - "depth": "int | float", - }, - "equipment": { - "tech_required_area": "float", - "tech_combined_mass": "float", - "topside_design_cost": "USD (optional, default:4.5e6)", - "fabrication_cost_rate": "USD/t (optional, default: 14500.)", - "substructure_steel_rate": "USD/t (optional, default: 3000.)", - }, - } - - # Takes in arguments and initialize library files - def __init__(self, config, **kwargs): - self.phase = "H2 Fixed Platform Design" - - config = self.initialize_library(config, **kwargs) - self.config = self.validate_config(config) - - self._outputs = {} - - # Runs the design cost models - def run(self): - # print("Fixed Platform Design run() is working!!!") - - self.distance = self.config["site"]["distance"] # km - self.depth = self.config["site"]["depth"] # m - - _platform = self.config.get("equipment", {}) - - self.mass = _platform.get("tech_combined_mass", 999) # t - self.area = _platform.get("tech_required_area", 1000) # m**2 - - design_cost = _platform.get("topside_design_cost", 4.5e6) # USD - fab_cost = _platform.get("fabrication_cost_rate", 14500.0) # USD/t - steel_cost = _platform.get("substructure_steel_rate", 3000) # USD/t - - # Add individual calcs/functions in the run() method - total_cost, total_mass = calc_substructure_mass_and_cost( - self.mass, self.area, self.depth, fab_cost, design_cost, steel_cost - ) - - # Create an ouput dict - self._outputs["fixed_platform"] = { - "mass": total_mass, - "area": self.area, - "total_cost": total_cost, - } - - # A design object needs to have attribute design_result and detailed_output - @property - def design_result(self): - return { - "platform_design": { - "mass": self._outputs["fixed_platform"]["mass"], - "area": self._outputs["fixed_platform"]["area"], - "total_cost": self._outputs["fixed_platform"]["total_cost"], - } - } - - @property - def detailed_output(self): - return {} - - -class FixedPlatformInstallation(orbit.phases.install.InstallPhase): - """ - This is a modified class based on ORBIT's [1] install phase. The implementation - is duscussed in [2], Section 3.6: Offshore Substation Installation. Default values - originate from [3], Appendix A: Inputs, Key Assumptions and Caveats. - """ - - # phase = "H2 Fixed Platform Installation" - - # Expected inputs from config yaml file - expected_config: typing.ClassVar = { - "site": { - "distance": "int | float", - "depth": "int | float", - }, - "equipment": { - "tech_required_area": "float", - "tech_combined_mass": "float", - "install_duration": "days (optional, default: 14)", - }, - "oss_install_vessel": "str | dict", - } - - # Need to initialize arguments and weather files - def __init__(self, config, weather=None, **kwargs): - super().__init__(weather, **kwargs) - - config = self.initialize_library(config, **kwargs) - self.config = self.validate_config(config) - - self.initialize_port() - self.setup_simulation(**kwargs) - - # Setup simulation seems to be the install phase's equivalent run() module - def setup_simulation(self, **kwargs): - # print("Fixed Platform Install setup_sim() is working!!!") - - self.distance = self.config["site"]["distance"] - self.depth = self.config["site"]["depth"] - self.mass = self.config["equipment"]["tech_combined_mass"] - self.area = self.config["equipment"]["tech_required_area"] - - _platform = self.config.get("equipment", {}) - design_cost = _platform.get("topside_design_cost", 4.5e6) # USD - fab_cost = _platform.get("fabrication_cost_rate", 14500.0) # USD/t - steel_cost = _platform.get("substructure_steel_rate", 3000) # USD/t - - install_duration = _platform.get("install_duration", 14) # days - - # Initialize vessel - vessel_specs = self.config.get("oss_install_vessel", None) - name = vessel_specs.get("name", "Offshore Substation Install Vessel") - - vessel = orbit.core.Vessel(name, vessel_specs) - self.env.register(vessel) - - vessel.initialize() - self.install_vessel = vessel - - # Add in the mass of the substructure to total mass (may or may not impact the final - # install cost) - _, substructure_mass = calc_substructure_mass_and_cost( - self.mass, self.area, self.depth, fab_cost, design_cost, steel_cost - ) - - self.total_mass = substructure_mass # t - # Call the install_platform function - self.install_capex = install_platform( - self.total_mass, - self.area, - self.distance, - install_duration, - self.install_vessel, - foundation="fixed", - ) - - # An install object needs to have attribute system_capex, installation_capex, and detailed - # output - @property - def system_capex(self): - return {} - - @property - def installation_capex(self): - return self.install_capex - - @property - def detailed_output(self): - return {} - - -# Define individual calculations and functions to use outside or with ORBIT -def calc_substructure_mass_and_cost( - mass, area, depth, fab_cost=14500.0, design_cost=4.5e6, sub_cost=3000, pile_cost=0 -): - """ - calc_substructure_mass_and_cost returns the total mass including substructure, topside and - equipment. Also returns the cost of the substructure and topside - Inputs: mass | Mass of equipment on platform (metric tons) - area | Area needed for equipment (meter^2) (not necessary) - depth | Ocean depth at platform location (meters) (not necessary) - fab_cost_rate | Cost rate to fabricate topside (USD/metric ton) - design_cost | Design cost to design structural components (USD) from ORBIT - sub_cost_rate | Steel cost rate (USD/metric ton) from ORBIT""" - """ - Platform is substructure and topside combined - All functions are based off NREL's ORBIT [1] (oss_design.py) - default values are specified in [3], - """ - # Inputs needed - topside_mass = mass - topside_fab_cost_rate = fab_cost - topside_design_cost = design_cost - - """Topside Cost & Mass (repurposed eq. 2.26 from [2]) - Topside Mass is the required Mass the platform will hold - Topside Cost is a function of topside mass, fab cost and design cost""" - topside_cost = topside_mass * topside_fab_cost_rate + topside_design_cost - - """Substructure (repurposed eq. 2.31-2.33 from [2]) - Substructure Mass is a function of the topside mass - Substructure Cost is a function of of substructure mass pile mass and cost rates for each""" - - # inputs needed - substructure_cost_rate = sub_cost # USD/t - pile_cost_rate = pile_cost # USD/t - - substructure_mass = 0.4 * topside_mass # t - substructure_pile_mass = 8 * substructure_mass**0.5574 # t - substructure_cost = ( - substructure_mass * substructure_cost_rate + substructure_pile_mass * pile_cost_rate - ) # USD - - substructure_total_mass = substructure_mass + substructure_pile_mass # t - - """Total Platform capex = capex Topside + capex substructure""" - - platform_capex = substructure_cost + topside_cost # USD - platform_mass = substructure_total_mass + topside_mass # t - - return platform_capex, platform_mass - - -# Standalone test sections -if __name__ == "__main__": - print("\n*** New FixedPlatform Standalone test section ***\n") - - orbit_libpath = Path.cwd().parents[2] / "ORBIT/library" - print(orbit_libpath) - orbit.core.library.initialize_library(orbit_libpath) - - config_path = Path(__file__).parent - config_fname = orbit.load_config(config_path / "example_fixed_project.yaml") - - orbit.ProjectManager.register_design_phase(FixedPlatformDesign) - - orbit.ProjectManager.register_install_phase(FixedPlatformInstallation) - - platform = orbit.ProjectManager(config_fname) - platform.run() - - design_capex = platform.design_results["platform_design"]["total_cost"] - install_capex = platform.installation_capex - - # print("Project Params", h2platform.project_params.items()) - platform_opex = calc_platform_opex(design_capex + install_capex) - - print("ORBIT Phases: ", platform.phases.keys()) - print(f"\tH2 Platform Design Capex: {design_capex:.0f} USD") - print(f"\tH2 Platform Install Capex: {install_capex:.0f} USD") - print() - print(f"\tTotal H2 Platform Capex: {(design_capex+install_capex)/1e6:.0f} mUSD") - print(f"\tH2 Platform Opex: {platform_opex:.0f} USD/year") diff --git a/h2integrate/simulation/technologies/offshore/floating_platform.py b/h2integrate/simulation/technologies/offshore/floating_platform.py deleted file mode 100644 index 048cd5ea3..000000000 --- a/h2integrate/simulation/technologies/offshore/floating_platform.py +++ /dev/null @@ -1,357 +0,0 @@ -from __future__ import annotations - - -""" -Author:Charles Kiefer -Date: 4/11/2023 -Institution: National Renewable Energy Lab -Description: This file shall handle costing and sizing of offshore floating platforms deicated to - hydrogen production. It uses the same foundation as fixed_platform.py. Both have been modeled - off of existing BOS cost/sizing calculations fond in ORBIT. It can be run as standalone - functions or as appended ORBIT project phases. - - - -Sources: - - [1] ORBIT: https://github.com/WISDEM/ORBIT v1.1 -Args: - - tech_required_area: (float): area needed for combination of all tech (m^2), not including - buffer or working space - - tech_combined_mass: (float): mass of all tech being placed on the platform (kg or metric tons) - - - - depth: (float): bathometry at the platform location (m) ##Site depths for floating projects - need to be at depths 500 m to 1500 m because of Orbit Semitaut limitations (7/31) - - distance_to_port: (float): distance ships must travel from port to site location (km) - - Future arguments: (Not used at this time) - - construction year (int): - - lifetime (int): lifetime of the plant in years (may not be needed) - -Returns: - - platform_mass (float): Adjusted mass of platform + substructure - - design_capex (float): capital expenditures (platform design + substructure fabrication) - - installation_capex (float): capital expenditures (installation cost) - - platform_opex (float): the OPEX (annual, fixed) in USD for the platform - -""" - -""" -Notes: - Thank you Jake Nunemaker's oswh2 repository and Rebecca Fuchs SemiTaut_mooring repository!!! - pile_cost=0 $US/metric ton for monopile construction. Not a bug, this # is consistent with - the rest of ORBIT -""" - - -import typing -from pathlib import Path - -from ORBIT import ProjectManager, load_config -from ORBIT.core import Vessel -from ORBIT.core.library import initialize_library -from ORBIT.phases.design import DesignPhase, MooringSystemDesign -from ORBIT.phases.install import InstallPhase - -from h2integrate.simulation.technologies.offshore.all_platforms import ( - install_platform, - calc_platform_opex, -) - - -class FloatingPlatformDesign(DesignPhase): - """ - This is a modified class based on ORBIT's design phase - """ - - # phase = "H2 Floating Platform Design" - - # Expected inputs from config yaml file - expected_config: typing.ClassVar = { - "site": { - "distance": "int | float", - "depth": "int | float", - }, - "equipment": { - "tech_required_area": "float", - "tech_combined_mass": "float", - "topside_design_cost": "USD (optional, default:4.5e6)", - "fabrication_cost_rate": "USD/t (optional, default: 14500.)", - "substructure_steel_rate": "USD/t (optional, default: 3000.)", - }, - } - - # Takes in arguments and initialize library files - def __init__(self, config, **kwargs): - self.phase = "H2 Floating Platform Design" - - config = self.initialize_library(config, **kwargs) - self.config = self.validate_config(config) - - self._outputs = {} - # Runs the design cost models - - def run(self): - # print("Floating Platform Design run() is working!!!") - - self.distance = self.config["site"]["distance"] # km - self.depth = self.config["site"]["depth"] # m - - _platform = self.config.get("equipment", {}) - - self.mass = _platform.get("tech_combined_mass", 999) # t - self.area = _platform.get("tech_required_area", 1000) # m**2 - - design_cost = _platform.get("topside_design_cost", 4.5e6) # USD - fab_cost_rate = _platform.get("fabrication_cost_rate", 14500.0) # USD/t - steel_cost = _platform.get("substructure_steel_rate", 3000) # USD/t - ##NEED updated version - # Add individual calcs/functions in the run() method - """Calls in SemiTaut Costs and Variables for Substructure mass and cost""" - self.anchor_type = "Drag Embedment" - self.mooring_type = "Semitaut" - self.num_lines = 4 - MooringSystemDesign.MooringSystemDesign.calculate_line_length_mass(self) - MooringSystemDesign.MooringSystemDesign.calculate_anchor_mass_cost(self) - MooringSystemDesign.MooringSystemDesign.determine_mooring_line_cost(self) - total_cost, total_mass = calc_substructure_mass_and_cost( - self.mass, - self.area, - self.depth, - fab_cost_rate, - design_cost, - steel_cost, - self.line_cost, - self.anchor_cost, - self.anchor_mass, - self.line_mass, - self.num_lines, - ) - - # Create an ouput dict - self._outputs["floating_platform"] = { - "mass": total_mass, - "area": self.area, - "total_cost": total_cost, - } - - # A design object needs to have attribute design_result and detailed_output - @property - def design_result(self): - return { - "platform_design": { - "mass": self._outputs["floating_platform"]["mass"], - "area": self._outputs["floating_platform"]["area"], - "total_cost": self._outputs["floating_platform"]["total_cost"], - } - } - - @property - def detailed_output(self): - return {} - - -class FloatingPlatformInstallation(InstallPhase): - """ - This is a modified class based on ORBIT's install phase - """ - - # phase = "H2 Floating Platform Installation" - - # Expected inputs from config yaml file - expected_config: typing.ClassVar = { - "site": { - "distance": "int | float", - "depth": "int | float", - }, - "equipment": { - "tech_required_area": "float", - "tech_combined_mass": "float", - "install_duration": "days (optional, default: 14)", - }, - "oss_install_vessel": "str | dict", - } - - # Need to initialize arguments and weather files - def __init__(self, config, weather=None, **kwargs): - super().__init__(weather, **kwargs) - - config = self.initialize_library(config, **kwargs) - self.config = self.validate_config(config) - - self.initialize_port() - self.setup_simulation(**kwargs) - - # Setup simulation seems to be the install phase's equivalent run() module - def setup_simulation(self, **kwargs): - # print("Floating Platform Install setup_sim() is working!!!") - - self.distance = self.config["site"]["distance"] - self.depth = self.config["site"]["depth"] - self.mass = self.config["equipment"]["tech_combined_mass"] - self.area = self.config["equipment"]["tech_required_area"] - - _platform = self.config.get("equipment", {}) - design_cost = _platform.get("topside_design_cost", 4.5e6) # USD - fab_cost_rate = _platform.get("fabrication_cost_rate", 14500.0) # USD/t - steel_cost = _platform.get("substructure_steel_rate", 3000) # USD/t - - install_duration = _platform.get("install_duration", 14) # days - - # Initialize vessel - vessel_specs = self.config.get("oss_install_vessel", None) - name = vessel_specs.get("name", "Offshore Substation Install Vessel") - - vessel = Vessel(name, vessel_specs) - self.env.register(vessel) - - vessel.initialize() - self.install_vessel = vessel - - # Add in the mass of the substructure to total mass (may or may not impact the final - # install cost) - - """Calls in SemiTaut Costs and Variables""" - self.anchor_type = "Drag Embedment" - self.mooring_type = "Semitaut" - self.num_lines = 4 - MooringSystemDesign.MooringSystemDesign.calculate_line_length_mass(self) - MooringSystemDesign.MooringSystemDesign.calculate_anchor_mass_cost(self) - MooringSystemDesign.MooringSystemDesign.determine_mooring_line_cost(self) - - _, substructure_mass = calc_substructure_mass_and_cost( - self.mass, - self.area, - self.depth, - fab_cost_rate, - design_cost, - steel_cost, - self.line_cost, - self.anchor_cost, - self.anchor_mass, - self.line_mass, - self.num_lines, - ) - - total_mass = substructure_mass # t - - # Call the install_platform function - self.install_capex = install_platform( - total_mass, - self.area, - self.distance, - install_duration, - self.install_vessel, - foundation="floating", - ) - - # An install object needs to have attribute system_capex, installation_capex, and detailed - # output - @property - def system_capex(self): - return {} - - @property - def installation_capex(self): - return self.install_capex - - @property - def detailed_output(self): - return {} - - -# Define individual calculations and functions to use outside or with ORBIT -def calc_substructure_mass_and_cost( - mass, - area, - depth, - fab_cost_rate=14500.0, - design_cost=4.5e6, - sub_cost_rate=3000, - line_cost=0, - anchor_cost=0, - anchor_mass=0, - line_mass=0, - num_lines=4, -): - """ - calc_substructure_mass_and_cost returns the total mass including substructure, topside and - equipment. Also returns the cost of the substructure and topside - Inputs: mass | Mass of equipment on platform (metric tons) - area | Area needed for equipment (meter^2) (not necessary) - depth | Ocean depth at platform location (meters) - fab_cost_rate | Cost rate to fabricate topside (USD/metric ton) - design_cost | Design cost to design structural components (USD) from ORBIT - sub_cost_rate | Steel cost rate (USD/metric ton) from ORBIT - """ - - """ - Platform is substructure and topside combined - All functions are based off NREL's ORBIT (oss_design) - default values are specified in ORBIT - """ - topside_mass = mass - topside_fab_cost_rate = fab_cost_rate - topside_design_cost = design_cost - - """Topside Cost & Mass - Topside Mass is the required Mass the platform will hold - Topside Cost is a function of topside mass, fab cost and design cost""" - topside_cost = topside_mass * topside_fab_cost_rate + topside_design_cost # USD - - """Substructure - Substructure Mass is a function of the topside mass - Substructure Cost is a function of of substructure mass pile mass and cost rates for each""" - - substructure_cost_rate = sub_cost_rate # USD/t - - substructure_mass = 0.4 * topside_mass # t - substructure_cost = substructure_mass * substructure_cost_rate # USD - substructure_total_mass = substructure_mass # t - - """Total Mooring cost and mass for the substructure - Line_cost, anchor_cost, line_mass, anchor_mass are grabbed from MooringSystemDesign in ORBIT - Mooring_mass is returned in kilograms and will need to """ - mooring_cost = (line_cost + anchor_cost) * num_lines # USD - mooring_mass = (line_mass + anchor_mass) * num_lines # kg - - """Total Platform capex = capex Topside + capex substructure""" - total_capex = 2 * (topside_cost + substructure_cost + mooring_cost) - platform_capex = total_capex # USD - platform_mass = substructure_total_mass + topside_mass + mooring_mass / 1000 # t - # mass of equipment and floating substructure for substation - - return platform_capex, platform_mass - - -# Standalone test sections -if __name__ == "__main__": - print("\n*** New FloatingPlatform Standalone test section ***\n") - - orbit_libpath = Path.cwd().parents[2] / "ORBIT/library" - print(orbit_libpath) - initialize_library(orbit_libpath) - - config_path = Path(__file__).parent - config_fname = load_config(config_path / "example_floating_project.yaml") - - # ProjectManager._design_phases.append(FloatingPlatformDesign) - ProjectManager.register_design_phase(FloatingPlatformDesign) - # ProjectManager._install_phases.append(FloatingPlatformInstallation) - ProjectManager.register_install_phase(FloatingPlatformInstallation) - - platform = ProjectManager(config_fname) - platform.run() - - design_capex = platform.design_results["platform_design"]["total_cost"] - install_capex = platform.installation_capex - - # print("Project Params", h2platform.project_params.items()) - platform_opex = calc_platform_opex(design_capex + install_capex) - - print("ORBIT Phases: ", platform.phases.keys()) - print(f"\tH2 Platform Design Capex: {design_capex:.0f} USD") - print(f"\tH2 Platform Install Capex: {install_capex:.0f} USD") - print() - print(f"\tTotal H2 Platform Capex: {(design_capex+install_capex)/1e6:.0f} mUSD") - print(f"\tH2 Platform Opex: {platform_opex:.0f} USD/year") diff --git a/h2integrate/simulation/technologies/steel/__init__.py b/h2integrate/simulation/technologies/steel/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/h2integrate/simulation/technologies/steel/steel.py b/h2integrate/simulation/technologies/steel/steel.py deleted file mode 100644 index 37bdb6eae..000000000 --- a/h2integrate/simulation/technologies/steel/steel.py +++ /dev/null @@ -1,951 +0,0 @@ -from __future__ import annotations - -import copy -from pathlib import Path - -import pandas as pd -import ProFAST -from attrs import Factory, field, define - - -@define -class Feedstocks: - """ - Represents the consumption rates and costs of various feedstocks used in steel - production. - - Attributes: - natural_gas_prices (Dict[str, float]): - Natural gas costs, indexed by year ($/GJ). - unused_oxygen (float): - Unused oxygen produced (kgO2), default = 395. - lime_unitcost (float): - Cost per metric ton of lime ($/metric ton). - lime_transport_cost (float): - Cost to transport lime per metric ton of lime ($/metric ton). - carbon_unitcost (float): - Cost per metric ton of carbon ($/metric ton). - carbon_transport_cost (float): - Cost to transport carbon per metric ton of carbon ($/metric ton). - electricity_cost (float): - Electricity cost per metric ton of steel production ($/metric ton). - iron_ore_pellet_unitcost (float): - Cost per metric ton of iron ore ($/metric ton). - iron_ore_pellet_transport_cost (float): - Cost to transport iron ore per metric ton of iron ore ($/metric ton). - oxygen_market_price (float): - Market price per kg of oxygen ($/kgO2). - raw_water_unitcost (float): - Cost per metric ton of raw water ($/metric ton). - iron_ore_consumption (float): - Iron ore consumption per metric ton of steel production (metric tons). - raw_water_consumption (float): - Raw water consumption per metric ton of steel production (metric tons). - lime_consumption (float): - Lime consumption per metric ton of steel production (metric tons). - carbon_consumption (float): - Carbon consumption per metric ton of steel production (metric tons). - hydrogen_consumption (float): - Hydrogen consumption per metric ton of steel production (metric tons). - natural_gas_consumption (float): - Natural gas consumption per metric ton of steel production (GJ-LHV). - electricity_consumption (float): - Electricity consumption per metric ton of steel production (MWh). - slag_disposal_unitcost (float): - Cost per metric ton of slag disposal ($/metric ton). - slag_production (float): - Slag production per metric ton of steel production (metric tons). - maintenance_materials_unitcost (float): - Cost per metric ton of annual steel slab production at real capacity - factor ($/metric ton). - """ - - natural_gas_prices: dict[str, float] - unused_oxygen: float = 395 - lime_unitcost: float = 122.1 - lime_transport_cost: float = 0.0 # USD/metric ton lime - carbon_unitcost: float = 236.97 - carbon_transport_cost: float = 0.0 # USD/metric ton carbon - electricity_cost: float = 48.92 - iron_ore_pellet_unitcost: float = 207.35 - iron_ore_pellet_transport_cost: float = 0.0 # USD/metric ton iron - oxygen_market_price: float = 0.03 - raw_water_unitcost: float = 0.59289 - iron_ore_consumption: float = 1.62927 - raw_water_consumption: float = 0.80367 - lime_consumption: float = 0.01812 - carbon_consumption: float = 0.0538 - hydrogen_consumption: float = 0.06596 - natural_gas_consumption: float = 0.71657 - electricity_consumption: float = 0.5502 - slag_disposal_unitcost: float = 37.63 - slag_production: float = 0.17433 - maintenance_materials_unitcost: float = 7.72 - - -@define -class SteelCostModelConfig: - """ - Configuration for the steel cost model, including operational parameters and - feedstock costs. - - Attributes: - operational_year (int): The year of operation for cost estimation. - plant_capacity_mtpy (float): Plant capacity in metric tons per year. - lcoh (float): Levelized cost of hydrogen ($/kg). - feedstocks (Feedstocks): - An instance of the Feedstocks class containing feedstock consumption - rates and costs. - o2_heat_integration (bool): - Indicates whether oxygen and heat integration is used, affecting preheating - CapEx, cooling CapEx, and oxygen sales. Default is True. - co2_fuel_emissions (float): - CO2 emissions from fuel per metric ton of steel production. - co2_carbon_emissions (float): - CO2 emissions from carbon per metric ton of steel production. - surface_water_discharge (float): - Surface water discharge per metric ton of steel production. - """ - - operational_year: int - plant_capacity_mtpy: float - lcoh: float - feedstocks: Feedstocks - o2_heat_integration: bool = True - co2_fuel_emissions: float = 0.03929 - co2_carbon_emissions: float = 0.17466 - surface_water_discharge: float = 0.42113 - - -@define -class SteelCosts: - """ - Base dataclass for calculated steel costs. - - Attributes: - capex_eaf_casting (float): - Capital expenditure for electric arc furnace and casting. - capex_shaft_furnace (float): Capital expenditure for shaft furnace. - capex_oxygen_supply (float): Capital expenditure for oxygen supply. - capex_h2_preheating (float): Capital expenditure for hydrogen preheating. - capex_cooling_tower (float): Capital expenditure for cooling tower. - capex_piping (float): Capital expenditure for piping. - capex_elec_instr (float): - Capital expenditure for electrical and instrumentation. - capex_buildings_storage_water (float): - Capital expenditure for buildings, storage, and water service. - capex_misc (float): - Capital expenditure for miscellaneous items. - labor_cost_annual_operation (float): Annual operating labor cost. - labor_cost_maintenance (float): Maintenance labor cost. - labor_cost_admin_support (float): Administrative and support labor cost. - property_tax_insurance (float): Cost for property tax and insurance. - land_cost (float): Cost of land. - installation_cost (float): Cost of installation. - - Note: - These represent the minimum set of required cost data for - `run_steel_finance_model`, as well as base data for `SteelCostModelOutputs`. - """ - - capex_eaf_casting: float - capex_shaft_furnace: float - capex_oxygen_supply: float - capex_h2_preheating: float - capex_cooling_tower: float - capex_piping: float - capex_elec_instr: float - capex_buildings_storage_water: float - capex_misc: float - labor_cost_annual_operation: float - labor_cost_maintenance: float - labor_cost_admin_support: float - property_tax_insurance: float - land_cost: float - installation_cost: float - - -@define -class SteelCostModelOutputs(SteelCosts): - """ - Outputs of the steel cost model, extending the SteelCosts data with total - cost calculations and specific cost components related to the operation and - installation of a steel production plant. - - Attributes: - total_plant_cost (float): - The total capital expenditure (CapEx) for the steel plant. - total_fixed_operating_cost (float): - The total annual operating expenditure (OpEx), including labor, - maintenance, administrative support, and property tax/insurance. - labor_cost_fivemonth (float): - Cost of labor for the first five months of operation, often used in startup - cost calculations. - maintenance_materials_onemonth (float): - Cost of maintenance materials for one month of operation. - non_fuel_consumables_onemonth (float): - Cost of non-fuel consumables for one month of operation. - waste_disposal_onemonth (float): - Cost of waste disposal for one month of operation. - monthly_energy_cost (float): - Cost of energy (electricity, natural gas, etc.) for one month of operation. - spare_parts_cost (float): - Cost of spare parts as part of the initial investment. - misc_owners_costs (float): - Miscellaneous costs incurred by the owner, including but not limited to, - initial supply stock, safety equipment, and initial training programs. - """ - - total_plant_cost: float - total_fixed_operating_cost: float - labor_cost_fivemonth: float - maintenance_materials_onemonth: float - non_fuel_consumables_onemonth: float - waste_disposal_onemonth: float - monthly_energy_cost: float - spare_parts_cost: float - misc_owners_costs: float - - -@define -class SteelCapacityModelConfig: - """ - Configuration inputs for the steel capacity sizing model, including plant capacity and - feedstock details. - - Attributes: - hydrogen_amount_kgpy Optional (float): The amount of hydrogen available in kilograms - per year to make steel. - desired_steel_mtpy Optional (float): The amount of desired steel production in - metric tons per year. - input_capacity_factor_estimate (float): The estimated steel plant capacity factor. - feedstocks (Feedstocks): An instance of the `Feedstocks` class detailing the - costs and consumption rates of resources used in production. - """ - - input_capacity_factor_estimate: float - feedstocks: Feedstocks - hydrogen_amount_kgpy: float | None = field(default=None) - desired_steel_mtpy: float | None = field(default=None) - - def __attrs_post_init__(self): - if self.hydrogen_amount_kgpy is None and self.desired_steel_mtpy is None: - raise ValueError("`hydrogen_amount_kgpy` or `desired_steel_mtpy` is a required input.") - - if self.hydrogen_amount_kgpy and self.desired_steel_mtpy: - raise ValueError( - "can only select one input: `hydrogen_amount_kgpy` or `desired_steel_mtpy`." - ) - - -@define -class SteelCapacityModelOutputs: - """ - Outputs from the steel size model. - - Attributes: - steel_plant_size_mtpy (float): If amount of hydrogen in kilograms per year is input, - the size of the steel plant in metric tons per year is output. - hydrogen_amount_kgpy (float): If amount of steel production in metric tons per year is - input, the amount of necessary hydrogen feedstock in kilograms per year is output. - """ - - steel_plant_capacity_mtpy: float - hydrogen_amount_kgpy: float - - -def run_size_steel_plant_capacity( - config: SteelCapacityModelConfig, -) -> SteelCapacityModelOutputs: - """ - Calculates either the annual steel production in metric tons based on plant capacity and - available hydrogen or the amount of required hydrogen based on a desired steel production. - - Args: - config (SteelCapacityModelConfig): - Configuration object containing all necessary parameters for the capacity sizing, - including capacity factor estimate and feedstock costs. - - Returns: - SteelCapacityModelOutputs: An object containing steel plant capacity in metric tons - per year and amount of hydrogen required in kilograms per year. - - """ - - if config.hydrogen_amount_kgpy: - steel_plant_capacity_mtpy = ( - config.hydrogen_amount_kgpy - / 1000 - / config.feedstocks.hydrogen_consumption - * config.input_capacity_factor_estimate - ) - hydrogen_amount_kgpy = config.hydrogen_amount_kgpy - - if config.desired_steel_mtpy: - hydrogen_amount_kgpy = ( - config.desired_steel_mtpy - * 1000 - * config.feedstocks.hydrogen_consumption - / config.input_capacity_factor_estimate - ) - steel_plant_capacity_mtpy = ( - config.desired_steel_mtpy / config.input_capacity_factor_estimate - ) - - return SteelCapacityModelOutputs( - steel_plant_capacity_mtpy=steel_plant_capacity_mtpy, - hydrogen_amount_kgpy=hydrogen_amount_kgpy, - ) - - -def run_steel_model(plant_capacity_mtpy: float, plant_capacity_factor: float) -> float: - """ - Calculates the annual steel production in metric tons based on plant capacity and - capacity factor. - - Args: - plant_capacity_mtpy (float): - The plant's annual capacity in metric tons per year. - plant_capacity_factor (float): - The capacity factor of the plant. - - Returns: - float: The calculated annual steel production in metric tons per year. - """ - steel_production_mtpy = plant_capacity_mtpy * plant_capacity_factor - - return steel_production_mtpy - - -def run_steel_cost_model(config: SteelCostModelConfig) -> SteelCostModelOutputs: - """ - Calculates the capital expenditure (CapEx) and operating expenditure (OpEx) for - a steel manufacturing plant based on the provided configuration. - - Args: - config (SteelCostModelConfig): - Configuration object containing all necessary parameters for the cost - model, including plant capacity, feedstock costs, and integration options - for oxygen and heat. - - Returns: - SteelCostModelOutputs: An object containing detailed breakdowns of capital and - operating costs, as well as total plant cost and other financial metrics. - - Note: - The calculation includes various cost components such as electric arc furnace - (EAF) casting, shaft furnace, oxygen supply, hydrogen preheating, cooling tower, - and more, adjusted based on the Chemical Engineering Plant Cost Index (CEPCI). - """ - feedstocks = config.feedstocks - - model_year_CEPCI = 816.0 # 2022 - equation_year_CEPCI = 708.8 # 2021 - - capex_eaf_casting = ( - model_year_CEPCI / equation_year_CEPCI * 352191.5237 * config.plant_capacity_mtpy**0.456 - ) - capex_shaft_furnace = ( - model_year_CEPCI / equation_year_CEPCI * 489.68061 * config.plant_capacity_mtpy**0.88741 - ) - capex_oxygen_supply = ( - model_year_CEPCI / equation_year_CEPCI * 1715.21508 * config.plant_capacity_mtpy**0.64574 - ) - if config.o2_heat_integration: - capex_h2_preheating = ( - model_year_CEPCI - / equation_year_CEPCI - * (1 - 0.4) - * (45.69123 * config.plant_capacity_mtpy**0.86564) - ) # Optimistic ballpark estimate of 60% reduction in preheating - capex_cooling_tower = ( - model_year_CEPCI - / equation_year_CEPCI - * (1 - 0.3) - * (2513.08314 * config.plant_capacity_mtpy**0.63325) - ) # Optimistic ballpark estimate of 30% reduction in cooling - else: - capex_h2_preheating = ( - model_year_CEPCI / equation_year_CEPCI * 45.69123 * config.plant_capacity_mtpy**0.86564 - ) - capex_cooling_tower = ( - model_year_CEPCI - / equation_year_CEPCI - * 2513.08314 - * config.plant_capacity_mtpy**0.63325 - ) - capex_piping = ( - model_year_CEPCI / equation_year_CEPCI * 11815.72718 * config.plant_capacity_mtpy**0.59983 - ) - capex_elec_instr = ( - model_year_CEPCI / equation_year_CEPCI * 7877.15146 * config.plant_capacity_mtpy**0.59983 - ) - capex_buildings_storage_water = ( - model_year_CEPCI / equation_year_CEPCI * 1097.81876 * config.plant_capacity_mtpy**0.8 - ) - capex_misc = ( - model_year_CEPCI / equation_year_CEPCI * 7877.1546 * config.plant_capacity_mtpy**0.59983 - ) - - total_plant_cost = ( - capex_eaf_casting - + capex_shaft_furnace - + capex_oxygen_supply - + capex_h2_preheating - + capex_cooling_tower - + capex_piping - + capex_elec_instr - + capex_buildings_storage_water - + capex_misc - ) - - # -------------------------------Fixed O&M Costs------------------------------ - - labor_cost_annual_operation = ( - 69375996.9 - * ((config.plant_capacity_mtpy / 365 * 1000) ** 0.25242) - / ((1162077 / 365 * 1000) ** 0.25242) - ) - labor_cost_maintenance = 0.00863 * total_plant_cost - labor_cost_admin_support = 0.25 * (labor_cost_annual_operation + labor_cost_maintenance) - - property_tax_insurance = 0.02 * total_plant_cost - - total_fixed_operating_cost = ( - labor_cost_annual_operation - + labor_cost_maintenance - + labor_cost_admin_support - + property_tax_insurance - ) - - # ---------------------- Owner's (Installation) Costs -------------------------- - labor_cost_fivemonth = ( - 5 / 12 * (labor_cost_annual_operation + labor_cost_maintenance + labor_cost_admin_support) - ) - - maintenance_materials_onemonth = ( - feedstocks.maintenance_materials_unitcost * config.plant_capacity_mtpy / 12 - ) - non_fuel_consumables_onemonth = ( - config.plant_capacity_mtpy - * ( - feedstocks.raw_water_consumption * feedstocks.raw_water_unitcost - + feedstocks.lime_consumption - * (feedstocks.lime_unitcost + feedstocks.lime_transport_cost) - + feedstocks.carbon_consumption - * (feedstocks.carbon_unitcost + feedstocks.carbon_transport_cost) - + feedstocks.iron_ore_consumption - * (feedstocks.iron_ore_pellet_unitcost + feedstocks.iron_ore_pellet_transport_cost) - ) - / 12 - ) - - waste_disposal_onemonth = ( - config.plant_capacity_mtpy - * feedstocks.slag_disposal_unitcost - * feedstocks.slag_production - / 12 - ) - - monthly_energy_cost = ( - config.plant_capacity_mtpy - * ( - feedstocks.hydrogen_consumption * config.lcoh * 1000 - + feedstocks.natural_gas_consumption - * feedstocks.natural_gas_prices[str(config.operational_year)] - + feedstocks.electricity_consumption * feedstocks.electricity_cost - ) - / 12 - ) - two_percent_tpc = 0.02 * total_plant_cost - - fuel_consumables_60day_supply_cost = ( - config.plant_capacity_mtpy - * ( - feedstocks.raw_water_consumption * feedstocks.raw_water_unitcost - + feedstocks.lime_consumption - * (feedstocks.lime_unitcost + feedstocks.lime_transport_cost) - + feedstocks.carbon_consumption - * (feedstocks.carbon_unitcost + feedstocks.carbon_transport_cost) - + feedstocks.iron_ore_consumption - * (feedstocks.iron_ore_pellet_unitcost + feedstocks.iron_ore_pellet_transport_cost) - ) - / 365 - * 60 - ) - - spare_parts_cost = 0.005 * total_plant_cost - land_cost = 0.775 * config.plant_capacity_mtpy - misc_owners_costs = 0.15 * total_plant_cost - - installation_cost = ( - labor_cost_fivemonth - + two_percent_tpc - + fuel_consumables_60day_supply_cost - + spare_parts_cost - + misc_owners_costs - ) - - return SteelCostModelOutputs( - # CapEx - capex_eaf_casting=capex_eaf_casting, - capex_shaft_furnace=capex_shaft_furnace, - capex_oxygen_supply=capex_oxygen_supply, - capex_h2_preheating=capex_h2_preheating, - capex_cooling_tower=capex_cooling_tower, - capex_piping=capex_piping, - capex_elec_instr=capex_elec_instr, - capex_buildings_storage_water=capex_buildings_storage_water, - capex_misc=capex_misc, - total_plant_cost=total_plant_cost, - # Fixed OpEx - labor_cost_annual_operation=labor_cost_annual_operation, - labor_cost_maintenance=labor_cost_maintenance, - labor_cost_admin_support=labor_cost_admin_support, - property_tax_insurance=property_tax_insurance, - total_fixed_operating_cost=total_fixed_operating_cost, - # Owner's Installation costs - labor_cost_fivemonth=labor_cost_fivemonth, - maintenance_materials_onemonth=maintenance_materials_onemonth, - non_fuel_consumables_onemonth=non_fuel_consumables_onemonth, - waste_disposal_onemonth=waste_disposal_onemonth, - monthly_energy_cost=monthly_energy_cost, - spare_parts_cost=spare_parts_cost, - land_cost=land_cost, - misc_owners_costs=misc_owners_costs, - installation_cost=installation_cost, - ) - - -@define -class SteelFinanceModelConfig: - """ - Configuration for the steel finance model, including plant characteristics, financial - assumptions, and cost inputs. - - Attributes: - plant_life (int): The operational lifetime of the plant in years. - plant_capacity_mtpy (float): Plant capacity in metric tons per year. - plant_capacity_factor (float): - The fraction of the year the plant operates at full capacity. - steel_production_mtpy (float): Annual steel production in metric tons. - lcoh (float): Levelized cost of hydrogen. - grid_prices (Dict[str, float]): Electricity prices per unit. - feedstocks (Feedstocks): - The feedstocks required for steel production, including types and costs. - costs (Union[SteelCosts, SteelCostModelOutputs]): - Calculated CapEx and OpEx costs. - o2_heat_integration (bool): Indicates if oxygen and heat integration is used. - financial_assumptions (Dict[str, float]): - Financial assumptions for model calculations. - install_years (int): The number of years over which the plant is installed. - gen_inflation (float): General inflation rate. - save_plots (bool): select whether or not to save output plots - show_plots (bool): select whether or not to show output plots during run - output_dir (str): where to store any saved plots or data - design_scenario_id (int): what design scenario the plots correspond to - """ - - plant_life: int - plant_capacity_mtpy: float - plant_capacity_factor: float - steel_production_mtpy: float - lcoh: float - grid_prices: dict[str, float] - feedstocks: Feedstocks - costs: SteelCosts | SteelCostModelOutputs - o2_heat_integration: bool = True - financial_assumptions: dict[str, float] = Factory(dict) - install_years: int = 3 - gen_inflation: float = 0.00 - save_plots: bool = False - show_plots: bool = False - output_dir: str = "./output/" - design_scenario_id: int = 0 - - -@define -class SteelFinanceModelOutputs: - """ - Represents the outputs of the steel finance model, encapsulating the results of financial - analysis for steel production. - - Attributes: - sol (dict): - A dictionary containing the solution to the financial model, including key - financial indicators such as NPV (Net Present Value), IRR (Internal Rate of - Return), and breakeven price. - summary (dict): - A summary of key results from the financial analysis, providing a - high-level overview of financial metrics and performance indicators. - price_breakdown (pd.DataFrame): - A Pandas DataFrame detailing the cost breakdown for producing steel, - including both capital and operating expenses, as well as the impact of - various cost factors on the overall price of steel. - """ - - sol: dict - summary: dict - price_breakdown: pd.DataFrame - - -def run_steel_finance_model( - config: SteelFinanceModelConfig, -) -> SteelFinanceModelOutputs: - """ - Executes the financial model for steel production, calculating the breakeven price - of steel and other financial metrics based on the provided configuration and cost - models. - - This function integrates various cost components, including capital expenditures - (CapEx), operating expenses (OpEx), and owner's costs. It leverages the ProFAST - financial analysis software framework. - - Args: - config (SteelFinanceModelConfig): - Configuration object containing all necessary parameters and assumptions - for the financial model, including plant characteristics, cost inputs, - financial assumptions, and grid prices. - - Returns: - SteelFinanceModelOutputs: - Object containing detailed financial analysis results, including solution - metrics, summary values, price breakdown, and steel price breakdown per - metric ton. This output is instrumental in assessing the financial performance - and breakeven price for the steel production facility. - """ - - feedstocks = config.feedstocks - costs = config.costs - - # Set up ProFAST - pf = ProFAST.ProFAST("blank") - - # apply all params passed through from config - for param, val in config.financial_assumptions.items(): - pf.set_params(param, val) - - analysis_start = int([*config.grid_prices][0]) - config.install_years - - # Fill these in - can have most of them as 0 also - pf.set_params( - "commodity", - { - "name": "Steel", - "unit": "metric tons", - "initial price": 1000, - "escalation": config.gen_inflation, - }, - ) - pf.set_params("capacity", config.plant_capacity_mtpy / 365) # units/day - pf.set_params("maintenance", {"value": 0, "escalation": config.gen_inflation}) - pf.set_params("analysis start year", analysis_start) - pf.set_params("operating life", config.plant_life) - pf.set_params("installation months", 12 * config.install_years) - pf.set_params( - "installation cost", - { - "value": costs.installation_cost, - "depr type": "Straight line", - "depr period": 4, - "depreciable": False, - }, - ) - pf.set_params("non depr assets", costs.land_cost) - pf.set_params( - "end of proj sale non depr assets", - costs.land_cost * (1 + config.gen_inflation) ** config.plant_life, - ) - pf.set_params("demand rampup", 5.3) - pf.set_params("long term utilization", config.plant_capacity_factor) - pf.set_params("credit card fees", 0) - pf.set_params("sales tax", 0) - pf.set_params("license and permit", {"value": 00, "escalation": config.gen_inflation}) - pf.set_params("rent", {"value": 0, "escalation": config.gen_inflation}) - pf.set_params("property tax and insurance", 0) - pf.set_params("admin expense", 0) - pf.set_params("sell undepreciated cap", True) - pf.set_params("tax losses monetized", True) - pf.set_params("general inflation rate", config.gen_inflation) - pf.set_params("debt type", "Revolving debt") - pf.set_params("cash onhand", 1) - - # ----------------------------------- Add capital items to ProFAST ---------------- - pf.add_capital_item( - name="EAF & Casting", - cost=costs.capex_eaf_casting, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - pf.add_capital_item( - name="Shaft Furnace", - cost=costs.capex_shaft_furnace, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - pf.add_capital_item( - name="Oxygen Supply", - cost=costs.capex_oxygen_supply, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - pf.add_capital_item( - name="H2 Pre-heating", - cost=costs.capex_h2_preheating, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - pf.add_capital_item( - name="Cooling Tower", - cost=costs.capex_cooling_tower, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - pf.add_capital_item( - name="Piping", - cost=costs.capex_piping, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - pf.add_capital_item( - name="Electrical & Instrumentation", - cost=costs.capex_elec_instr, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - pf.add_capital_item( - name="Buildings, Storage, Water Service", - cost=costs.capex_buildings_storage_water, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - pf.add_capital_item( - name="Other Miscellaneous Costs", - cost=costs.capex_misc, - depr_type="MACRS", - depr_period=7, - refurb=[0], - ) - - # -------------------------------------- Add fixed costs-------------------------------- - pf.add_fixed_cost( - name="Annual Operating Labor Cost", - usage=1, - unit="$/year", - cost=costs.labor_cost_annual_operation, - escalation=config.gen_inflation, - ) - pf.add_fixed_cost( - name="Maintenance Labor Cost", - usage=1, - unit="$/year", - cost=costs.labor_cost_maintenance, - escalation=config.gen_inflation, - ) - pf.add_fixed_cost( - name="Administrative & Support Labor Cost", - usage=1, - unit="$/year", - cost=costs.labor_cost_admin_support, - escalation=config.gen_inflation, - ) - pf.add_fixed_cost( - name="Property tax and insurance", - usage=1, - unit="$/year", - cost=costs.property_tax_insurance, - escalation=0.0, - ) - # Putting property tax and insurance here to zero out depcreciation/escalation. Could instead - # put it in set_params if we think that is more accurate - - # ---------------------- Add feedstocks, note the various cost options------------------- - pf.add_feedstock( - name="Maintenance Materials", - usage=1.0, - unit="Units per metric ton of steel", - cost=feedstocks.maintenance_materials_unitcost, - escalation=config.gen_inflation, - ) - pf.add_feedstock( - name="Raw Water Withdrawal", - usage=feedstocks.raw_water_consumption, - unit="metric tons of water per metric ton of steel", - cost=feedstocks.raw_water_unitcost, - escalation=config.gen_inflation, - ) - pf.add_feedstock( - name="Lime", - usage=feedstocks.lime_consumption, - unit="metric tons of lime per metric ton of steel", - cost=(feedstocks.lime_unitcost + feedstocks.lime_transport_cost), - escalation=config.gen_inflation, - ) - pf.add_feedstock( - name="Carbon", - usage=feedstocks.carbon_consumption, - unit="metric tons of carbon per metric ton of steel", - cost=(feedstocks.carbon_unitcost + feedstocks.carbon_transport_cost), - escalation=config.gen_inflation, - ) - pf.add_feedstock( - name="Iron Ore", - usage=feedstocks.iron_ore_consumption, - unit="metric tons of iron ore per metric ton of steel", - cost=(feedstocks.iron_ore_pellet_unitcost + feedstocks.iron_ore_pellet_transport_cost), - escalation=config.gen_inflation, - ) - pf.add_feedstock( - name="Hydrogen", - usage=feedstocks.hydrogen_consumption, - unit="metric tons of hydrogen per metric ton of steel", - cost=config.lcoh * 1000, - escalation=config.gen_inflation, - ) - pf.add_feedstock( - name="Natural Gas", - usage=feedstocks.natural_gas_consumption, - unit="GJ-LHV per metric ton of steel", - cost=feedstocks.natural_gas_prices, - escalation=config.gen_inflation, - ) - pf.add_feedstock( - name="Electricity", - usage=feedstocks.electricity_consumption, - unit="MWh per metric ton of steel", - cost=config.grid_prices, - escalation=config.gen_inflation, - ) - pf.add_feedstock( - name="Slag Disposal", - usage=feedstocks.slag_production, - unit="metric tons of slag per metric ton of steel", - cost=feedstocks.slag_disposal_unitcost, - escalation=config.gen_inflation, - ) - - pf.add_coproduct( - name="Oxygen sales", - usage=feedstocks.unused_oxygen, - unit="kg O2 per metric ton of steel", - cost=feedstocks.oxygen_market_price, - escalation=config.gen_inflation, - ) - - # ------------------------------ Set up outputs --------------------------- - - sol = pf.solve_price() - summary = pf.get_summary_vals() - price_breakdown = pf.get_cost_breakdown() - - if config.save_plots or config.show_plots: - output_dir = Path(config.output_dir).resolve() - savepaths = [ - output_dir / "figures/capex/", - output_dir / "figures/annual_cash_flow/", - output_dir / "figures/lcos_breakdown/", - output_dir / "data/", - ] - for savepath in savepaths: - if not savepath.exists(): - savepath.mkdir(parents=True) - - pf.plot_capital_expenses( - fileout=savepaths[0] / f"steel_capital_expense_{config.design_scenario_id}.pdf", - show_plot=config.show_plots, - ) - pf.plot_cashflow( - fileout=savepaths[1] / f"steel_cash_flow_{config.design_scenario_id}.png", - show_plot=config.show_plots, - ) - - pd.DataFrame.from_dict(data=pf.cash_flow_out).to_csv( - savepaths[3] / f"steel_cash_flow_{config.design_scenario_id}.csv" - ) - - pf.plot_costs( - savepaths[2] / f"lcos_{config.design_scenario_id}", - show_plot=config.show_plots, - ) - - return SteelFinanceModelOutputs( - sol=sol, - summary=summary, - price_breakdown=price_breakdown, - ) - - -def run_steel_full_model( - h2integrate_config: dict, - save_plots=False, - show_plots=False, - output_dir="./output/", - design_scenario_id=0, -) -> tuple[SteelCapacityModelOutputs, SteelCostModelOutputs, SteelFinanceModelOutputs]: - """ - Runs the full steel model, including capacity, cost, and finance models. - - Args: - h2integrate_config (dict): The configuration for the h2integrate model. - - Returns: - Tuple[SteelCapacityModelOutputs, SteelCostModelOutputs, SteelFinanceModelOutputs]: - A tuple containing the outputs of the steel capacity, cost, and finance models. - """ - # this is likely to change as we refactor to use config dataclasses, but for now - # we'll just copy the config and modify it as needed - config = copy.deepcopy(h2integrate_config) - - if config["steel"]["costs"]["lcoh"] != config["steel"]["finances"]["lcoh"]: - msg = ( - "steel cost LCOH and steel finance LCOH are not equal. You must specify both values" - " or neither. If neither is specified, LCOH will be calculated." - ) - raise ValueError(msg) - - steel_costs = config["steel"]["costs"] - steel_capacity = config["steel"]["capacity"] - feedstocks = Feedstocks(**steel_costs["feedstocks"]) - - # run steel capacity model to get steel plant size - # uses hydrogen amount from electrolyzer physics model - capacity_config = SteelCapacityModelConfig(feedstocks=feedstocks, **steel_capacity) - steel_capacity = run_size_steel_plant_capacity(capacity_config) - - # run steel cost model - steel_costs["feedstocks"] = feedstocks - steel_cost_config = SteelCostModelConfig( - plant_capacity_mtpy=steel_capacity.steel_plant_capacity_mtpy, **steel_costs - ) - steel_cost_config.plant_capacity_mtpy = steel_capacity.steel_plant_capacity_mtpy - steel_costs = run_steel_cost_model(steel_cost_config) - - # run steel finance model - steel_finance = config["steel"]["finances"] - steel_finance["feedstocks"] = feedstocks - - steel_finance_config = SteelFinanceModelConfig( - plant_capacity_mtpy=steel_capacity.steel_plant_capacity_mtpy, - plant_capacity_factor=capacity_config.input_capacity_factor_estimate, - steel_production_mtpy=run_steel_model( - steel_capacity.steel_plant_capacity_mtpy, - capacity_config.input_capacity_factor_estimate, - ), - costs=steel_costs, - show_plots=show_plots, - save_plots=save_plots, - output_dir=output_dir, - design_scenario_id=design_scenario_id, - **steel_finance, - ) - steel_finance = run_steel_finance_model(steel_finance_config) - - return (steel_capacity, steel_costs, steel_finance) diff --git a/h2integrate/storage/hydrogen/eco_storage.py b/h2integrate/storage/hydrogen/eco_storage.py deleted file mode 100644 index 59a47c88c..000000000 --- a/h2integrate/storage/hydrogen/eco_storage.py +++ /dev/null @@ -1,223 +0,0 @@ -import numpy as np -from attrs import field, define - -from h2integrate.core.utilities import BaseConfig, merge_shared_inputs -from h2integrate.core.validators import contains -from h2integrate.core.model_baseclasses import CostModelBaseClass -from h2integrate.simulation.technologies.hydrogen.h2_storage.mch.mch_cost import MCHStorage - - -# TODO: fix import structure in future refactor - - -from h2integrate.simulation.technologies.hydrogen.h2_storage.storage_sizing import hydrogen_storage_capacity # noqa: E501 # fmt: skip # isort:skip -from h2integrate.simulation.technologies.hydrogen.h2_storage.salt_cavern.salt_cavern import SaltCavernStorage # noqa: E501 # fmt: skip # isort:skip -from h2integrate.simulation.technologies.hydrogen.h2_storage.lined_rock_cavern.lined_rock_cavern import LinedRockCavernStorage # noqa: E501 # fmt: skip # isort:skip - - -@define -class H2StorageModelConfig(BaseConfig): - commodity_name: str = field(default="hydrogen") - commodity_units: str = field(default="kg") - electrolyzer_rating_mw_for_h2_storage_sizing: float | None = field(default=None) - size_capacity_from_demand: dict = field(default={"flag": True}) - capacity_from_max_on_turbine_storage: bool = field(default=False) - type: str = field( - default="salt_cavern", - validator=contains(["salt_cavern", "lined_rock_cavern", "none", "mch"]), - ) - days: int = field(default=0) - cost_year: int = field(default=2018, converter=int, validator=contains([2018, 2021])) - - def __attrs_post_init__(self): - if self.type == "mch": - self.cost_year = 2024 - else: - self.cost_year = 2018 - - -class H2Storage(CostModelBaseClass): - def initialize(self): - super().initialize() - self.options.declare("verbose", types=bool, default=False) - - def setup(self): - self.config = H2StorageModelConfig.from_dict( - merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") - ) - - super().setup() - - self.add_input( - "hydrogen_in", - val=0.0, - shape_by_conn=True, - units="kg/h", - ) - - self.add_input( - "rated_h2_production_kg_pr_hr", - val=0.0, - units="kg/h", - desc="Rated hydrogen production of electrolyzer", - ) - self.add_input("efficiency", val=0.0, desc="Average efficiency of the electrolyzer") - - def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - ########### initialize output dictionary ########### - h2_storage_results = {} - - storage_max_fill_rate = np.max(inputs["hydrogen_in"]) - - ########### get hydrogen storage size in kilograms ########### - ##################### no hydrogen storage - if self.config.type == "none": - h2_storage_capacity_kg = 0.0 - storage_max_fill_rate = 0.0 - - ##################### get storage capacity from hydrogen storage demand - elif self.config.size_capacity_from_demand["flag"]: - if self.config.electrolyzer_rating_mw_for_h2_storage_sizing is None: - raise ( - ValueError( - "h2 storage input battery_electricity_discharge must be specified \ - if size_capacity_from_demand is True." - ) - ) - hydrogen_storage_demand = np.mean( - inputs["hydrogen_in"] - ) # TODO: update demand based on end-use needs - results_dict = { - "Hydrogen Hourly Production [kg/hr]": inputs["hydrogen_in"], - "Sim: Average Efficiency [%-HHV]": inputs["efficiency"], - } - ( - hydrogen_demand_kgphr, - hydrogen_storage_capacity_kg, - hydrogen_storage_duration_hr, - hydrogen_storage_soc, - ) = hydrogen_storage_capacity( - results_dict, - self.config.electrolyzer_rating_mw_for_h2_storage_sizing, - hydrogen_storage_demand, - ) - h2_storage_capacity_kg = hydrogen_storage_capacity_kg - h2_storage_results["hydrogen_storage_duration_hr"] = hydrogen_storage_duration_hr - h2_storage_results["hydrogen_storage_soc"] = hydrogen_storage_soc - - ##################### get storage capacity based on storage days in config - else: - storage_hours = self.config.days * 24 - h2_storage_capacity_kg = round(storage_hours * storage_max_fill_rate) - - h2_storage_results["h2_storage_capacity_kg"] = h2_storage_capacity_kg - h2_storage_results["h2_storage_max_fill_rate_kg_hr"] = storage_max_fill_rate - - ########### run specific hydrogen storage models for costs and energy use ########### - if self.config.type == "none": - h2_storage_results["storage_capex"] = 0.0 - h2_storage_results["storage_opex"] = 0.0 - h2_storage_results["storage_energy"] = 0.0 - h2_storage_results["cost_year"] = 2018 - - h2_storage = None - - elif self.config.type == "salt_cavern": - # initialize dictionary for salt cavern storage parameters - storage_input = {} - - # pull parameters from plant_config file - storage_input["h2_storage_kg"] = h2_storage_capacity_kg - storage_input["system_flow_rate"] = storage_max_fill_rate - storage_input["model"] = "papadias" - - # run salt cavern storage model - h2_storage = SaltCavernStorage(storage_input) - - h2_storage.salt_cavern_capex() - h2_storage.salt_cavern_opex() - - h2_storage_results["storage_capex"] = h2_storage.output_dict[ - "salt_cavern_storage_capex" - ] - h2_storage_results["storage_opex"] = h2_storage.output_dict["salt_cavern_storage_opex"] - h2_storage_results["storage_energy"] = 0.0 - h2_storage_results["cost_year"] = 2018 - - elif self.config.type == "lined_rock_cavern": - # initialize dictionary for salt cavern storage parameters - storage_input = {} - - # pull parameters from plat_config file - storage_input["h2_storage_kg"] = h2_storage_capacity_kg - storage_input["system_flow_rate"] = storage_max_fill_rate - storage_input["model"] = "papadias" - - # run salt cavern storage model - h2_storage = LinedRockCavernStorage(storage_input) - - h2_storage.lined_rock_cavern_capex() - h2_storage.lined_rock_cavern_opex() - - h2_storage_results["storage_capex"] = h2_storage.output_dict[ - "lined_rock_cavern_storage_capex" - ] - h2_storage_results["storage_opex"] = h2_storage.output_dict[ - "lined_rock_cavern_storage_opex" - ] - h2_storage_results["storage_energy"] = 0.0 - h2_storage_results["cost_year"] = 2018 # TODO: check - - elif self.config.type == "mch": - if not self.config.size_capacity_from_demand["flag"]: - msg = ( - "To use MCH hydrogen storage, the size_capacity_from_demand " - "flag must be True." - ) - raise ValueError(msg) - - max_rated_h2 = np.max( - [inputs["rated_h2_production_kg_pr_hr"][0], storage_max_fill_rate] - ) - h2_charge_discharge = np.diff(hydrogen_storage_soc, prepend=False) - h2_charged_idx = np.argwhere(h2_charge_discharge > 0).flatten() - annual_h2_stored = sum([h2_charge_discharge[i] for i in h2_charged_idx]) - - h2_storage = MCHStorage( - max_H2_production_kg_pr_hr=max_rated_h2, - hydrogen_storage_capacity_kg=h2_storage_capacity_kg, - hydrogen_demand_kg_pr_hr=hydrogen_demand_kgphr, - annual_hydrogen_stored_kg_pr_yr=annual_h2_stored, - ) - h2_storage_costs = h2_storage.run_costs() - h2_storage_results["storage_capex"] = h2_storage_costs["mch_capex"] - h2_storage_results["storage_opex"] = ( - h2_storage_costs["mch_variable_om"] + h2_storage_costs["mch_opex"] - ) - h2_storage_results["storage_energy"] = 0.0 - h2_storage_results["cost_year"] = 2024 - - else: - msg = ( - "H2 storage type %s was given, but must be one of ['none', 'turbine', 'pipe'," - " 'pressure_vessel', 'salt_cavern', 'lined_rock_cavern', 'mch']" - ) - raise ValueError(msg) - - if self.options["verbose"]: - print("\nH2 Storage Results:") - print("H2 storage capex: ${:,.0f}".format(h2_storage_results["storage_capex"])) - print("H2 storage annual opex: ${:,.0f}/yr".format(h2_storage_results["storage_opex"])) - print( - "H2 storage capacity (metric tons): ", - h2_storage_results["h2_storage_capacity_kg"] / 1000, - ) - if h2_storage_results["h2_storage_capacity_kg"] > 0: - print( - "H2 storage cost $/kg of H2: ", - h2_storage_results["storage_capex"] - / h2_storage_results["h2_storage_capacity_kg"], - ) - - outputs["CapEx"] = h2_storage_results["storage_capex"] - outputs["OpEx"] = h2_storage_results["storage_opex"] diff --git a/h2integrate/storage/hydrogen/h2_storage_cost.py b/h2integrate/storage/hydrogen/h2_storage_cost.py index 20c693cfb..e5e17867c 100644 --- a/h2integrate/storage/hydrogen/h2_storage_cost.py +++ b/h2integrate/storage/hydrogen/h2_storage_cost.py @@ -1,15 +1,11 @@ +import numpy as np from attrs import field, define from openmdao.utils import units from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import contains, gte_zero, range_val from h2integrate.core.model_baseclasses import CostModelBaseClass - - -# TODO: fix import structure in future refactor -from h2integrate.simulation.technologies.hydrogen.h2_storage.lined_rock_cavern.lined_rock_cavern import LinedRockCavernStorage # noqa: E501 # fmt: skip # isort:skip -from h2integrate.simulation.technologies.hydrogen.h2_storage.salt_cavern.salt_cavern import SaltCavernStorage # noqa: E501 # fmt: skip # isort:skip -from h2integrate.simulation.technologies.hydrogen.h2_storage.pipe_storage import UndergroundPipeStorage # noqa: E501 # fmt: skip # isort:skip +from h2integrate.storage.hydrogen.h2_transport.h2_compression import Compressor @define @@ -129,16 +125,17 @@ def make_storage_input_dict(self, inputs): inputs["max_capacity"], f"({self.config.commodity_units})*h", "kg" ) - # convert charge rate to kg/h - # TODO: update to kg/day as a bug-fix + # convert charge rate to kg/day (required for storage models) storage_max_fill_rate = units.convert_units( - inputs["max_charge_rate"], f"{self.config.commodity_units}", "kg/h" + inputs["max_charge_rate"], f"{self.config.commodity_units}", "kg/d" ) storage_input["h2_storage_kg"] = max_capacity_kg[0] - # below should be in kg/day - storage_input["system_flow_rate"] = storage_max_fill_rate[0] + # system_flow_rate must be in kg/day + # NOTE: I believe this conversion is a bug and should not be divided by 24. + # To make the code consistent with previous behavior, I will not change it now. + storage_input["system_flow_rate"] = storage_max_fill_rate[0] / 24 # kg/day to kg/hr return storage_input @@ -149,57 +146,394 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): class LinedRockCavernStorageCostModel(HydrogenStorageBaseCostModel): - def initialize(self): - super().initialize() - - def setup(self): - super().setup() + """ + Author: Kaitlin Brunik + Created: 7/20/2023 + Institution: National Renewable Energy Lab + Description: This file outputs capital and operational costs of lined rock cavern + hydrogen storage. + It needs to be updated to with operational dynamics. + Costs are in 2018 USD + + Sources: + - [1] Papadias 2021: https://www.sciencedirect.com/science/article/pii/S0360319921030834?via%3Dihub + - [2] Papadias 2021: Bulk Hydrogen as Function of Capacity.docx documentation at + hydrogen_storage.md in the docs + - [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet + """ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + """ + Calculates the installed capital cost and operation and maintenance costs for lined rock + cavern hydrogen storage. + + Args: + inputs: OpenMDAO inputs containing: + - max_capacity: total capacity of hydrogen storage [kg] + - max_charge_rate: hydrogen storage charge rate [kg/h] + + Returns via outputs: + - CapEx (float): the installed capital cost in 2018 [USD] (including compressor) + - OpEx (float): the OPEX (annual, fixed) in 2018 excluding electricity costs [USD/yr] + + Additional parameters from storage_input: + - h2_storage_kg (float): total capacity of hydrogen storage [kg] + - system_flow_rate (float): [kg/day] + - labor_rate (float): (default: 37.40) [$2018/hr] + - insurance (float): (default: 1%) [decimal percent] - % of total investment + - property_taxes (float): (default: 1%) [decimal percent] - % of total investment + - licensing_permits (float): (default: 0.1%) [decimal percent] - % of total investment + - compressor_om (float): (default: 4%) [decimal percent] - % of compressor investment + - facility_om (float): (default: 1%) [decimal percent] - % of facility investment + minus compressor investment + """ storage_input = self.make_storage_input_dict(inputs) - h2_storage = LinedRockCavernStorage(storage_input) - h2_storage.lined_rock_cavern_capex() - h2_storage.lined_rock_cavern_opex() + # Extract input parameters + h2_storage_kg = storage_input["h2_storage_kg"] # [kg] + system_flow_rate = storage_input["system_flow_rate"] # [kg/day] + labor_rate = storage_input.get("labor_rate", 37.39817) # $(2018)/hr + insurance = storage_input.get("insurance", 1 / 100) # % of total capital investment + property_taxes = storage_input.get( + "property_taxes", 1 / 100 + ) # % of total capital investment + licensing_permits = storage_input.get( + "licensing_permits", 0.1 / 100 + ) # % of total capital investment + comp_om = storage_input.get("compressor_om", 4 / 100) # % of compressor capital investment + facility_om = storage_input.get( + "facility_om", 1 / 100 + ) # % of facility capital investment minus compressor capital investment + + # ============================================================================ + # Calculate CAPEX + # ============================================================================ + # Installed capital cost per kg from Papadias [2] + # Coefficients for lined rock cavern storage cost equation + a = 0.095803 + b = 1.5868 + c = 10.332 + # Calculate installed capital cost per kg using exponential fit + lined_rock_cavern_storage_capex_per_kg = np.exp( + a * (np.log(h2_storage_kg / 1000)) ** 2 - b * np.log(h2_storage_kg / 1000) + c + ) # 2019 [USD] from Papadias [2] + installed_capex = lined_rock_cavern_storage_capex_per_kg * h2_storage_kg + cepci_overall = 1.29 / 1.30 # Convert from $2019 to $2018 + installed_capex = cepci_overall * installed_capex + + # ============================================================================ + # Calculate compressor costs + # ============================================================================ + outlet_pressure = 200 # Max outlet pressure of lined rock cavern in [1] [bar] + n_compressors = 2 + storage_compressor = Compressor( + outlet_pressure, system_flow_rate, n_compressors=n_compressors + ) + storage_compressor.compressor_power() + motor_rating, power = storage_compressor.compressor_system_power() + # Check if motor rating exceeds maximum, add additional compressor if needed + if motor_rating > 1600: + n_compressors += 1 + storage_compressor = Compressor( + outlet_pressure, system_flow_rate, n_compressors=n_compressors + ) + storage_compressor.compressor_power() + motor_rating, power = storage_compressor.compressor_system_power() + comp_capex, comp_OM = storage_compressor.compressor_costs() + cepci = 1.36 / 1.29 # convert from $2016 to $2018 + comp_capex = comp_capex * cepci + + # ============================================================================ + # Calculate OPEX + # ============================================================================ + # Operations and Maintenance costs [3] + # Labor + # Base case is 1 operator, 24 hours a day, 7 days a week for a 100,000 kg/day + # average capacity facility. Scaling factor of 0.25 is used for other sized facilities + annual_hours = 8760 * (system_flow_rate / 100000) ** 0.25 + overhead = 0.5 + labor = (annual_hours * labor_rate) * (1 + overhead) # Burdened labor cost + insurance_cost = insurance * installed_capex + property_taxes_cost = property_taxes * installed_capex + licensing_permits_cost = licensing_permits * installed_capex + comp_op_maint = comp_om * comp_capex + facility_op_maint = facility_om * (installed_capex - comp_capex) + + # O&M excludes electricity requirements + total_om = ( + labor + + insurance_cost + + licensing_permits_cost + + property_taxes_cost + + comp_op_maint + + facility_op_maint + ) - outputs["CapEx"] = h2_storage.output_dict["lined_rock_cavern_storage_capex"] - outputs["OpEx"] = h2_storage.output_dict["lined_rock_cavern_storage_opex"] + outputs["CapEx"] = installed_capex + outputs["OpEx"] = total_om class SaltCavernStorageCostModel(HydrogenStorageBaseCostModel): - def initialize(self): - super().initialize() - - def setup(self): - super().setup() + """ + Author: Kaitlin Brunik + Created: 7/20/2023 + Institution: National Renewable Energy Lab + Description: This file outputs capital and operational costs of salt cavern hydrogen storage. + It needs to be updated to with operational dynamics. + Costs are in 2018 USD + + Sources: + - [1] Papadias 2021: https://www.sciencedirect.com/science/article/pii/S0360319921030834?via%3Dihub + - [2] Papadias 2021: Bulk Hydrogen as Function of Capacity.docx documentation at + hydrogen_storage.md in the docs + - [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet + """ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + """ + Calculates the installed capital cost and operation and maintenance costs for salt cavern + hydrogen storage. + + Args: + inputs: OpenMDAO inputs containing: + - max_capacity: total capacity of hydrogen storage [kg] + - max_charge_rate: hydrogen storage charge rate [kg/h] + + Returns via outputs: + - CapEx (float): the installed capital cost in 2018 [USD] (including compressor) + - OpEx (float): the OPEX (annual, fixed) in 2018 excluding electricity costs [USD/yr] + + Additional parameters from storage_input: + - h2_storage_kg (float): total capacity of hydrogen storage [kg] + - system_flow_rate (float): [kg/day] + - labor_rate (float): (default: 37.40) [$2018/hr] + - insurance (float): (default: 1%) [decimal percent] - % of total investment + - property_taxes (float): (default: 1%) [decimal percent] - % of total investment + - licensing_permits (float): (default: 0.1%) [decimal percent] - % of total investment + - compressor_om (float): (default: 4%) [decimal percent] - % of compressor investment + - facility_om (float): (default: 1%) [decimal percent] - % of facility investment + minus compressor investment + """ storage_input = self.make_storage_input_dict(inputs) - h2_storage = SaltCavernStorage(storage_input) - h2_storage.salt_cavern_capex() - h2_storage.salt_cavern_opex() + # Extract input parameters + h2_storage_kg = storage_input["h2_storage_kg"] # [kg] + system_flow_rate = storage_input["system_flow_rate"] # [kg/day] + labor_rate = storage_input.get("labor_rate", 37.39817) # $(2018)/hr + insurance = storage_input.get("insurance", 1 / 100) # % of total capital investment + property_taxes = storage_input.get( + "property_taxes", 1 / 100 + ) # % of total capital investment + licensing_permits = storage_input.get( + "licensing_permits", 0.1 / 100 + ) # % of total capital investment + comp_om = storage_input.get("compressor_om", 4 / 100) # % of compressor capital investment + facility_om = storage_input.get( + "facility_om", 1 / 100 + ) # % of facility capital investment minus compressor capital investment + + # ============================================================================ + # Calculate CAPEX + # ============================================================================ + # Installed capital cost per kg from Papadias [2] + # Coefficients for salt cavern storage cost equation + a = 0.092548 + b = 1.6432 + c = 10.161 + # Calculate installed capital cost per kg using exponential fit + salt_cavern_storage_capex_per_kg = np.exp( + a * (np.log(h2_storage_kg / 1000)) ** 2 - b * np.log(h2_storage_kg / 1000) + c + ) # 2019 [USD] from Papadias [2] + installed_capex = salt_cavern_storage_capex_per_kg * h2_storage_kg + cepci_overall = 1.29 / 1.30 # Convert from $2019 to $2018 + installed_capex = cepci_overall * installed_capex + + # ============================================================================ + # Calculate compressor costs + # ============================================================================ + outlet_pressure = 120 # Max outlet pressure of salt cavern in [1] [bar] + n_compressors = 2 + storage_compressor = Compressor( + outlet_pressure, system_flow_rate, n_compressors=n_compressors + ) + storage_compressor.compressor_power() + motor_rating, power = storage_compressor.compressor_system_power() + # Check if motor rating exceeds maximum, add additional compressor if needed + if motor_rating > 1600: + n_compressors += 1 + storage_compressor = Compressor( + outlet_pressure, system_flow_rate, n_compressors=n_compressors + ) + storage_compressor.compressor_power() + motor_rating, power = storage_compressor.compressor_system_power() + comp_capex, comp_OM = storage_compressor.compressor_costs() + cepci = 1.36 / 1.29 # convert from $2016 to $2018 + comp_capex = comp_capex * cepci + + # ============================================================================ + # Calculate OPEX + # ============================================================================ + # Operations and Maintenance costs [3] + # Labor + # Base case is 1 operator, 24 hours a day, 7 days a week for a 100,000 kg/day + # average capacity facility. Scaling factor of 0.25 is used for other sized facilities + annual_hours = 8760 * (system_flow_rate / 100000) ** 0.25 + overhead = 0.5 + labor = (annual_hours * labor_rate) * (1 + overhead) # Burdened labor cost + insurance_cost = insurance * installed_capex + property_taxes_cost = property_taxes * installed_capex + licensing_permits_cost = licensing_permits * installed_capex + comp_op_maint = comp_om * comp_capex + facility_op_maint = facility_om * (installed_capex - comp_capex) + + # O&M excludes electricity requirements + total_om = ( + labor + + insurance_cost + + licensing_permits_cost + + property_taxes_cost + + comp_op_maint + + facility_op_maint + ) - outputs["CapEx"] = h2_storage.output_dict["salt_cavern_storage_capex"] - outputs["OpEx"] = h2_storage.output_dict["salt_cavern_storage_opex"] + outputs["CapEx"] = installed_capex + outputs["OpEx"] = total_om class PipeStorageCostModel(HydrogenStorageBaseCostModel): - def initialize(self): - super().initialize() - - def setup(self): - super().setup() + """ + Author: Kaitlin Brunik + Updated: 7/20/2023 + Institution: National Renewable Energy Lab + Description: This file outputs capital and operational costs of underground pipeline hydrogen + storage. It needs to be updated to with operational dynamics and physical size + (footprint and mass). + Oversize pipe: pipe OD = 24'' schedule 60 [1] + Max pressure: 100 bar + Costs are in 2018 USD + + Sources: + - [1] Papadias 2021: https://www.sciencedirect.com/science/article/pii/S0360319921030834?via%3Dihub + - [2] Papadias 2021: Bulk Hydrogen as Function of Capacity.docx documentation at + hydrogen_storage.md in the docs + - [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet + """ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + """ + Calculates the installed capital cost and operation and maintenance costs for underground + pipe hydrogen storage. + + Args: + inputs: OpenMDAO inputs containing: + - max_capacity: total capacity of hydrogen storage [kg] + - max_charge_rate: hydrogen storage charge rate [kg/h] + + Returns via outputs: + - CapEx (float): the installed capital cost in 2018 [USD] (including compressor) + - OpEx (float): the OPEX (annual, fixed) in 2018 excluding electricity costs [USD/yr] + + Additional parameters from storage_input: + - h2_storage_kg (float): total capacity of hydrogen storage [kg] + - system_flow_rate (float): [kg/day] + - labor_rate (float): (default: 37.40) [$2018/hr] + - insurance (float): (default: 1%) [decimal percent] - % of total investment + - property_taxes (float): (default: 1%) [decimal percent] - % of total investment + - licensing_permits (float): (default: 0.1%) [decimal percent] - % of total investment + - compressor_om (float): (default: 4%) [decimal percent] - % of compressor investment + - facility_om (float): (default: 1%) [decimal percent] - % of facility investment + minus compressor investment + Notes: + - Oversize pipe: pipe OD = 24'' schedule 60 + - Max pressure: 100 bar + - compressor_output_pressure must be 100 bar for underground pipe storage + """ storage_input = self.make_storage_input_dict(inputs) - # compressor_output_pressure must be 100 bar or else an error will be thrown - storage_input.update({"compressor_output_pressure": 100}) - h2_storage = UndergroundPipeStorage(storage_input) - - h2_storage.pipe_storage_capex() - h2_storage.pipe_storage_opex() + # Extract input parameters + h2_storage_kg = storage_input["h2_storage_kg"] # [kg] + system_flow_rate = storage_input["system_flow_rate"] # [kg/day] + labor_rate = storage_input.get("labor_rate", 37.39817) # $(2018)/hr + insurance = storage_input.get("insurance", 1 / 100) # % of total capital investment + property_taxes = storage_input.get( + "property_taxes", 1 / 100 + ) # % of total capital investment + licensing_permits = storage_input.get( + "licensing_permits", 0.1 / 100 + ) # % of total capital investment + comp_om = storage_input.get("compressor_om", 4 / 100) # % of compressor capital investment + facility_om = storage_input.get( + "facility_om", 1 / 100 + ) # % of facility capital investment minus compressor capital investment + + # compressor_output_pressure must be 100 bar for underground pipe storage + compressor_output_pressure = 100 # [bar] + + # ============================================================================ + # Calculate CAPEX + # ============================================================================ + # Installed capital cost per kg from Papadias [2] + # Coefficients for underground pipe storage cost equation + a = 0.0041617 + b = 0.060369 + c = 6.4581 + # Calculate installed capital cost per kg using exponential fit + pipe_storage_capex_per_kg = np.exp( + a * (np.log(h2_storage_kg / 1000)) ** 2 - b * np.log(h2_storage_kg / 1000) + c + ) # 2019 [USD] from Papadias [2] + installed_capex = pipe_storage_capex_per_kg * h2_storage_kg + cepci_overall = 1.29 / 1.30 # Convert from $2019 to $2018 + installed_capex = cepci_overall * installed_capex + + # ============================================================================ + # Calculate compressor costs + # ============================================================================ + outlet_pressure = ( + compressor_output_pressure # Max outlet pressure of underground pipe storage [1] [bar] + ) + n_compressors = 2 + storage_compressor = Compressor( + outlet_pressure, system_flow_rate, n_compressors=n_compressors + ) + storage_compressor.compressor_power() + motor_rating, power = storage_compressor.compressor_system_power() + # Check if motor rating exceeds maximum, add additional compressor if needed + if motor_rating > 1600: + n_compressors += 1 + storage_compressor = Compressor( + outlet_pressure, system_flow_rate, n_compressors=n_compressors + ) + storage_compressor.compressor_power() + motor_rating, power = storage_compressor.compressor_system_power() + comp_capex, comp_OM = storage_compressor.compressor_costs() + cepci = 1.36 / 1.29 # convert from $2016 to $2018 + comp_capex = comp_capex * cepci + + # ============================================================================ + # Calculate OPEX + # ============================================================================ + # Operations and Maintenance costs [3] + # Labor + # Base case is 1 operator, 24 hours a day, 7 days a week for a 100,000 kg/day + # average capacity facility. Scaling factor of 0.25 is used for other sized facilities + annual_hours = 8760 * (system_flow_rate / 100000) ** 0.25 + overhead = 0.5 + labor = (annual_hours * labor_rate) * (1 + overhead) # Burdened labor cost + insurance_cost = insurance * installed_capex + property_taxes_cost = property_taxes * installed_capex + licensing_permits_cost = licensing_permits * installed_capex + comp_op_maint = comp_om * comp_capex + facility_op_maint = facility_om * (installed_capex - comp_capex) + + # O&M excludes electricity requirements + total_om = ( + labor + + insurance_cost + + licensing_permits_cost + + property_taxes_cost + + comp_op_maint + + facility_op_maint + ) - outputs["CapEx"] = h2_storage.output_dict["pipe_storage_capex"] - outputs["OpEx"] = h2_storage.output_dict["pipe_storage_opex"] + outputs["CapEx"] = installed_capex + outputs["OpEx"] = total_om diff --git a/h2integrate/simulation/technologies/iron/martin_ore/__init__.py b/h2integrate/storage/hydrogen/h2_transport/__init__.py similarity index 100% rename from h2integrate/simulation/technologies/iron/martin_ore/__init__.py rename to h2integrate/storage/hydrogen/h2_transport/__init__.py diff --git a/h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/pipe_dimensions_metric.csv b/h2integrate/storage/hydrogen/h2_transport/data_tables/pipe_dimensions_metric.csv similarity index 98% rename from h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/pipe_dimensions_metric.csv rename to h2integrate/storage/hydrogen/h2_transport/data_tables/pipe_dimensions_metric.csv index 409fda019..9a1ded5e1 100644 --- a/h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/pipe_dimensions_metric.csv +++ b/h2integrate/storage/hydrogen/h2_transport/data_tables/pipe_dimensions_metric.csv @@ -1,44 +1,44 @@ -DN,Outer diameter [mm],S 5S,S 10,S 10S,S 20,S 30,S Std,S 40,S 40S,S60,S XS,S 80,S 80S,S 100,S 120,S 140,S 160,S XXS -6,10.29,0.889,,1.245,1.245,1.448,1.727,1.727,1.727,,2.413,2.413,2.413,,,,, -8,13.72,1.245,,1.651,1.651,1.854,2.235,2.235,2.235,,3.023,3.023,3.023,,,,, -10,17.15,1.245,,1.651,1.651,1.854,2.311,2.311,2.311,,3.2,3.2,3.2,,,,, -15,21.34,1.651,,2.108,2.108,2.413,2.769,2.769,2.769,,3.734,3.734,3.734,,,,4.775,7.468 -20,26.67,1.651,,2.108,2.108,2.413,2.87,2.87,2.87,,3.912,3.912,3.912,,,,5.563,7.823 -25,33.4,1.651,,2.769,2.769,2.896,3.378,3.378,3.378,,4.547,4.547,4.547,,,,6.35,9.093 -32,42.16,1.651,,2.769,2.769,2.972,3.556,3.556,3.556,,4.851,4.851,4.851,,,,6.35,9.703 -40,48.26,1.651,,2.769,2.769,3.175,3.683,3.683,3.683,,5.08,5.08,5.08,,,,7.137,10.16 -50,60.33,1.651,,2.769,2.769,3.175,3.912,3.912,3.912,,5.537,5.537,5.537,,,,8.738,11.074 -65,73.03,2.108,,3.048,3.048,4.775,5.156,5.156,5.156,,7.01,7.01,7.01,,,,9.525,14.021 -80,88.9,2.108,,3.048,3.048,4.775,5.486,5.486,5.486,,7.62,7.62,7.62,,,,8.839,15.24 -90,101.6,2.108,,3.048,3.048,4.775,5.74,5.74,5.74,,8.077,8.077,8.077,,,,,16.154 -100,114.3,2.11,3.05,3.05,0,4.78,6.02,6.02,6.02,0,8.56,8.56,8.56,0,0,0,13.03,17.12 -125,141.3,2.77,3.4,3.4,0,0,6.55,6.55,6.55,0,9.53,9.53,9.53,0,0,0,15.88,19.05 -150,168.28,2.77,3.4,3.4,0,0,7.11,7.11,7.11,0,10.97,10.97,10.97,0,0,0,18.26,21.95 -,193.68,,,,,,7.65,7.65,7.65,,12.7,12.7,12.7,,,,,22.23 -200,219.08,2.77,3.76,3.76,6.35,7.04,8.18,8.18,8.18,10.31,12.7,12.7,12.7,15.06,18.26,20.62,23.01,22.23 -,,,,,,,,,,,,,,,,,, -250,273.05,3.404,4.191,4.191,6.35,7.798,9.271,9.271,9.271,12.7,12.7,15.088,12.7,18.237,21.412,25.4,28.575,25.4 -300,323.85,3.962,4.572,4.572,6.35,8.382,9.525,10.312,9.525,14.275,12.7,17.45,12.7,21.412,25.4,28.575,33.325,25.4 -350,355.6,3.962,6.35,4.775,7.925,9.525,9.525,11.1,9.525,15.062,12.7,19.05,12.7,23.8,27.762,31.75,35.712, -400,406.4,4.191,6.35,4.775,7.925,9.525,9.525,12.7,9.525,16.662,12.7,21.412,12.7,26.187,30.937,36.5,40.488, -450,457.2,4.191,6.35,4.775,7.925,11.1,9.525,14.275,9.525,19.05,12.7,23.8,12.7,29.362,34.925,39.675,45.237, -500,508,4.775,6.35,5.537,9.525,12.7,9.525,15.062,9.525,20.625,12.7,26.187,12.7,32.512,38.1,44.45,49.987, -550,558.8,4.775,6.35,5.537,9.525,12.7,9.525,,9.525,22.225,12.7,28.575,12.7,34.925,41.275,47.625,53.975, -600,609.6,5.537,6.35,6.35,9.525,14.275,9.525,17.45,9.525,24.587,12.7,30.937,12.7,38.887,46.025,52.375,59.512, -650,660.4,,7.925,,12.7,,9.525,,9.525,,12.7,,,,,,, -700,711.2,,7.925,,12.7,15.875,9.525,,9.525,,12.7,,,,,,, -750,762,6.35,7.925,7.925,12.7,15.875,9.525,,9.525,,12.7,,,,,,, -800,812.8,,7.925,,12.7,15.875,9.525,17.475,9.525,,12.7,,,,,,, -850,863.6,,7.925,,12.7,15.875,9.525,17.475,9.525,,12.7,,,,,,, -900,914.4,,7.925,,12.7,15.875,9.525,19.05,9.525,,12.7,,,,,,, -1000,1016,,,,,,9.525,,,,12.7,,,,,,,25.4 -1050,1066.8,,,,,,9.525,,,,12.7,,,,,,,25.4 -1100,1117.6,,,,,,9.525,,,,12.7,,,,,,,25.4 -1150,1168.4,,,,,,9.525,,,,12.7,,,,,,,25.4 -1200,1219.2,,,,,,9.525,,,,12.7,,,,,,,25.4 -1300,1320.8,,,,,,9.525,,,,12.7,,,,,,,25.4 -1400,1422.4,,,,,,9.525,,,,12.7,,,,,,,25.4 -1500,1524,,,,,,9.525,,,,12.7,,,,,,,25.4 -1600,1625.6,,,,,,9.525,,,,12.7,,,,,,,25.4 -1700,1727.2,,,,,,9.525,,,,12.7,,,,,,,25.4 -1800,1828.8,,,,,,9.525,,,,12.7,,,,,,,25.4 +DN,Outer diameter [mm],S 5S,S 10,S 10S,S 20,S 30,S Std,S 40,S 40S,S60,S XS,S 80,S 80S,S 100,S 120,S 140,S 160,S XXS +6,10.29,0.889,,1.245,1.245,1.448,1.727,1.727,1.727,,2.413,2.413,2.413,,,,, +8,13.72,1.245,,1.651,1.651,1.854,2.235,2.235,2.235,,3.023,3.023,3.023,,,,, +10,17.15,1.245,,1.651,1.651,1.854,2.311,2.311,2.311,,3.2,3.2,3.2,,,,, +15,21.34,1.651,,2.108,2.108,2.413,2.769,2.769,2.769,,3.734,3.734,3.734,,,,4.775,7.468 +20,26.67,1.651,,2.108,2.108,2.413,2.87,2.87,2.87,,3.912,3.912,3.912,,,,5.563,7.823 +25,33.4,1.651,,2.769,2.769,2.896,3.378,3.378,3.378,,4.547,4.547,4.547,,,,6.35,9.093 +32,42.16,1.651,,2.769,2.769,2.972,3.556,3.556,3.556,,4.851,4.851,4.851,,,,6.35,9.703 +40,48.26,1.651,,2.769,2.769,3.175,3.683,3.683,3.683,,5.08,5.08,5.08,,,,7.137,10.16 +50,60.33,1.651,,2.769,2.769,3.175,3.912,3.912,3.912,,5.537,5.537,5.537,,,,8.738,11.074 +65,73.03,2.108,,3.048,3.048,4.775,5.156,5.156,5.156,,7.01,7.01,7.01,,,,9.525,14.021 +80,88.9,2.108,,3.048,3.048,4.775,5.486,5.486,5.486,,7.62,7.62,7.62,,,,8.839,15.24 +90,101.6,2.108,,3.048,3.048,4.775,5.74,5.74,5.74,,8.077,8.077,8.077,,,,,16.154 +100,114.3,2.11,3.05,3.05,0,4.78,6.02,6.02,6.02,0,8.56,8.56,8.56,0,0,0,13.03,17.12 +125,141.3,2.77,3.4,3.4,0,0,6.55,6.55,6.55,0,9.53,9.53,9.53,0,0,0,15.88,19.05 +150,168.28,2.77,3.4,3.4,0,0,7.11,7.11,7.11,0,10.97,10.97,10.97,0,0,0,18.26,21.95 +,193.68,,,,,,7.65,7.65,7.65,,12.7,12.7,12.7,,,,,22.23 +200,219.08,2.77,3.76,3.76,6.35,7.04,8.18,8.18,8.18,10.31,12.7,12.7,12.7,15.06,18.26,20.62,23.01,22.23 +,,,,,,,,,,,,,,,,,, +250,273.05,3.404,4.191,4.191,6.35,7.798,9.271,9.271,9.271,12.7,12.7,15.088,12.7,18.237,21.412,25.4,28.575,25.4 +300,323.85,3.962,4.572,4.572,6.35,8.382,9.525,10.312,9.525,14.275,12.7,17.45,12.7,21.412,25.4,28.575,33.325,25.4 +350,355.6,3.962,6.35,4.775,7.925,9.525,9.525,11.1,9.525,15.062,12.7,19.05,12.7,23.8,27.762,31.75,35.712, +400,406.4,4.191,6.35,4.775,7.925,9.525,9.525,12.7,9.525,16.662,12.7,21.412,12.7,26.187,30.937,36.5,40.488, +450,457.2,4.191,6.35,4.775,7.925,11.1,9.525,14.275,9.525,19.05,12.7,23.8,12.7,29.362,34.925,39.675,45.237, +500,508,4.775,6.35,5.537,9.525,12.7,9.525,15.062,9.525,20.625,12.7,26.187,12.7,32.512,38.1,44.45,49.987, +550,558.8,4.775,6.35,5.537,9.525,12.7,9.525,,9.525,22.225,12.7,28.575,12.7,34.925,41.275,47.625,53.975, +600,609.6,5.537,6.35,6.35,9.525,14.275,9.525,17.45,9.525,24.587,12.7,30.937,12.7,38.887,46.025,52.375,59.512, +650,660.4,,7.925,,12.7,,9.525,,9.525,,12.7,,,,,,, +700,711.2,,7.925,,12.7,15.875,9.525,,9.525,,12.7,,,,,,, +750,762,6.35,7.925,7.925,12.7,15.875,9.525,,9.525,,12.7,,,,,,, +800,812.8,,7.925,,12.7,15.875,9.525,17.475,9.525,,12.7,,,,,,, +850,863.6,,7.925,,12.7,15.875,9.525,17.475,9.525,,12.7,,,,,,, +900,914.4,,7.925,,12.7,15.875,9.525,19.05,9.525,,12.7,,,,,,, +1000,1016,,,,,,9.525,,,,12.7,,,,,,,25.4 +1050,1066.8,,,,,,9.525,,,,12.7,,,,,,,25.4 +1100,1117.6,,,,,,9.525,,,,12.7,,,,,,,25.4 +1150,1168.4,,,,,,9.525,,,,12.7,,,,,,,25.4 +1200,1219.2,,,,,,9.525,,,,12.7,,,,,,,25.4 +1300,1320.8,,,,,,9.525,,,,12.7,,,,,,,25.4 +1400,1422.4,,,,,,9.525,,,,12.7,,,,,,,25.4 +1500,1524,,,,,,9.525,,,,12.7,,,,,,,25.4 +1600,1625.6,,,,,,9.525,,,,12.7,,,,,,,25.4 +1700,1727.2,,,,,,9.525,,,,12.7,,,,,,,25.4 +1800,1828.8,,,,,,9.525,,,,12.7,,,,,,,25.4 diff --git a/h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/steel_costs_per_kg.csv b/h2integrate/storage/hydrogen/h2_transport/data_tables/steel_costs_per_kg.csv similarity index 94% rename from h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/steel_costs_per_kg.csv rename to h2integrate/storage/hydrogen/h2_transport/data_tables/steel_costs_per_kg.csv index f2da36654..3d5700fef 100644 --- a/h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/steel_costs_per_kg.csv +++ b/h2integrate/storage/hydrogen/h2_transport/data_tables/steel_costs_per_kg.csv @@ -1,10 +1,10 @@ -Specification No.,Grade,Price [$/kg] -API 5L,B,1.8 -API 5L,X42,2.2 -API 5L,X46,2.4 -API 5L,X52,2.8 -API 5L,X56,2.9 -API 5L,X60,3.2 -API 5L,X65,4.3 -API 5L,X70,6.5 -API 5L,X80,10 +Specification No.,Grade,Price [$/kg] +API 5L,B,1.8 +API 5L,X42,2.2 +API 5L,X46,2.4 +API 5L,X52,2.8 +API 5L,X56,2.9 +API 5L,X60,3.2 +API 5L,X65,4.3 +API 5L,X70,6.5 +API 5L,X80,10 diff --git a/h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/steel_mechanical_props.csv b/h2integrate/storage/hydrogen/h2_transport/data_tables/steel_mechanical_props.csv similarity index 96% rename from h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/steel_mechanical_props.csv rename to h2integrate/storage/hydrogen/h2_transport/data_tables/steel_mechanical_props.csv index 9ebe6366a..8ef7f6a9e 100644 --- a/h2integrate/simulation/technologies/hydrogen/h2_transport/data_tables/steel_mechanical_props.csv +++ b/h2integrate/storage/hydrogen/h2_transport/data_tables/steel_mechanical_props.csv @@ -1,11 +1,11 @@ -Specification No.,Grade,SMYS [Mpa],SMYS [ksi],SMTS [Mpa],SMTS [ksi] -API 5L,A25,172,25,310,45 -API 5L,A,207,30,331,48 -API 5L,B,241,35,414,60 -API 5L,X42,290,42,414,60 -API 5L,X52,317,52,455,66 -API 5L,X56,386,56,490,71 -API 5L,X60,414,60,517,75 -API 5L,X65,448,65,531,77 -API 5L,X70,483,70,565,82 -API 5L,X80,552,80,621,90 +Specification No.,Grade,SMYS [Mpa],SMYS [ksi],SMTS [Mpa],SMTS [ksi] +API 5L,A25,172,25,310,45 +API 5L,A,207,30,331,48 +API 5L,B,241,35,414,60 +API 5L,X42,290,42,414,60 +API 5L,X52,317,52,455,66 +API 5L,X56,386,56,490,71 +API 5L,X60,414,60,517,75 +API 5L,X65,448,65,531,77 +API 5L,X70,483,70,565,82 +API 5L,X80,552,80,621,90 diff --git a/h2integrate/simulation/technologies/hydrogen/h2_transport/h2_compression.py b/h2integrate/storage/hydrogen/h2_transport/h2_compression.py similarity index 98% rename from h2integrate/simulation/technologies/hydrogen/h2_transport/h2_compression.py rename to h2integrate/storage/hydrogen/h2_transport/h2_compression.py index 749606e62..fd534555e 100644 --- a/h2integrate/simulation/technologies/hydrogen/h2_transport/h2_compression.py +++ b/h2integrate/storage/hydrogen/h2_transport/h2_compression.py @@ -22,8 +22,8 @@ def __init__( """ Parameters: --------------- - p_outlet: oulet pressure (bar) - flow_Rate_kg_d: mass flow rate in kg/day + p_outlet: outlet pressure (bar) + flow_rate_kg_d: mass flow rate in kg/day """ self.p_inlet = p_inlet # bar self.p_outlet = p_outlet # bar diff --git a/h2integrate/storage/hydrogen/test/test_hydrogen_storage.py b/h2integrate/storage/hydrogen/test/test_hydrogen_storage.py index ffdd91ca1..8fcfefef9 100644 --- a/h2integrate/storage/hydrogen/test/test_hydrogen_storage.py +++ b/h2integrate/storage/hydrogen/test/test_hydrogen_storage.py @@ -167,3 +167,195 @@ def test_buried_pipe_storage(plant_config, subtests): assert pytest.approx(np.sum(prob.get_val("sys.VarOpEx")), rel=1e-6) == 0.0 with subtests.test("Cost year"): assert prob.get_val("sys.cost_year") == 2018 + + +def test_lined_rock_cavern_capex_per_kg(plant_config): + """Test based on original test_lined_rock_storage.py with 1M kg storage capacity.""" + tech_config_dict = { + "model_inputs": { + "shared_parameters": { + "max_capacity": 1000000, + "max_charge_rate": 100000 / 24, # 100000 kg/day converted to kg/h + } + } + } + prob = om.Problem() + comp = LinedRockCavernStorageCostModel( + plant_config=plant_config, + tech_config=tech_config_dict, + driver_config={}, + ) + prob.model.add_subsystem("sys", comp) + prob.setup() + prob.run_model() + + # Calculate expected capex per kg + h2_storage_kg = 1000000 + a = 0.095803 + b = 1.5868 + c = 10.332 + capex_per_kg = np.exp( + a * (np.log(h2_storage_kg / 1000)) ** 2 - b * np.log(h2_storage_kg / 1000) + c + ) + cepci_overall = 1.29 / 1.30 + expected_capex = cepci_overall * capex_per_kg * h2_storage_kg + + assert pytest.approx(prob.get_val("sys.CapEx")[0], rel=1e-6) == expected_capex + + +def test_lined_rock_cavern_1M_kg(plant_config, subtests): + """Test lined rock cavern with 1M kg capacity matching original unit tests.""" + tech_config_dict = { + "model_inputs": { + "shared_parameters": { + "max_capacity": 1000000, + "max_charge_rate": 100000 / 24, # 100000 kg/day converted to kg/h + } + } + } + prob = om.Problem() + comp = LinedRockCavernStorageCostModel( + plant_config=plant_config, + tech_config=tech_config_dict, + driver_config={}, + ) + prob.model.add_subsystem("sys", comp) + prob.setup() + prob.run_model() + + with subtests.test("CapEx"): + assert pytest.approx(prob.get_val("sys.CapEx")[0], rel=1e-5) == 51136144.673 + with subtests.test("OpEx"): + assert pytest.approx(prob.get_val("sys.OpEx")[0], rel=1e-5) == 1833238.1260644312 + with subtests.test("VarOpEx"): + assert pytest.approx(np.sum(prob.get_val("sys.VarOpEx")), rel=1e-6) == 0.0 + with subtests.test("Cost year"): + assert prob.get_val("sys.cost_year") == 2018 + + +def test_salt_cavern_capex_per_kg(plant_config): + """Test based on original test_salt_cavern_storage.py with 1M kg storage capacity.""" + tech_config_dict = { + "model_inputs": { + "shared_parameters": { + "max_capacity": 1000000, + "max_charge_rate": 100000 / 24, # 100000 kg/day converted to kg/h + } + } + } + prob = om.Problem() + comp = SaltCavernStorageCostModel( + plant_config=plant_config, + tech_config=tech_config_dict, + driver_config={}, + ) + prob.model.add_subsystem("sys", comp) + prob.setup() + prob.run_model() + + # Calculate expected capex per kg + h2_storage_kg = 1000000 + a = 0.092548 + b = 1.6432 + c = 10.161 + capex_per_kg = np.exp( + a * (np.log(h2_storage_kg / 1000)) ** 2 - b * np.log(h2_storage_kg / 1000) + c + ) + cepci_overall = 1.29 / 1.30 + expected_capex = cepci_overall * capex_per_kg * h2_storage_kg + + assert pytest.approx(prob.get_val("sys.CapEx")[0], rel=1e-6) == expected_capex + + +def test_salt_cavern_1M_kg(plant_config, subtests): + """Test salt cavern with 1M kg capacity matching original unit tests.""" + tech_config_dict = { + "model_inputs": { + "shared_parameters": { + "max_capacity": 1000000, + "max_charge_rate": 100000 / 24, # 100000 kg/day converted to kg/h + } + } + } + prob = om.Problem() + comp = SaltCavernStorageCostModel( + plant_config=plant_config, + tech_config=tech_config_dict, + driver_config={}, + ) + prob.model.add_subsystem("sys", comp) + prob.setup() + prob.run_model() + + with subtests.test("CapEx"): + assert pytest.approx(prob.get_val("sys.CapEx")[0], rel=1e-5) == 24992482.4198 + with subtests.test("OpEx"): + assert pytest.approx(prob.get_val("sys.OpEx")[0], rel=1e-5) == 1015928.3388478106 + with subtests.test("VarOpEx"): + assert pytest.approx(np.sum(prob.get_val("sys.VarOpEx")), rel=1e-6) == 0.0 + with subtests.test("Cost year"): + assert prob.get_val("sys.cost_year") == 2018 + + +def test_pipe_storage_capex_per_kg(plant_config): + """Test based on original test_underground_pipe_storage.py with 1M kg storage capacity.""" + tech_config_dict = { + "model_inputs": { + "shared_parameters": { + "max_capacity": 1000000, + "max_charge_rate": 100000 / 24, # 100000 kg/day converted to kg/h + } + } + } + prob = om.Problem() + comp = PipeStorageCostModel( + plant_config=plant_config, + tech_config=tech_config_dict, + driver_config={}, + ) + prob.model.add_subsystem("sys", comp) + prob.setup() + prob.run_model() + + # Calculate expected capex per kg + h2_storage_kg = 1000000 + a = 0.0041617 + b = 0.060369 + c = 6.4581 + capex_per_kg = np.exp( + a * (np.log(h2_storage_kg / 1000)) ** 2 - b * np.log(h2_storage_kg / 1000) + c + ) + cepci_overall = 1.29 / 1.30 + expected_capex = cepci_overall * capex_per_kg * h2_storage_kg + + assert pytest.approx(prob.get_val("sys.CapEx")[0], rel=1e-6) == expected_capex + + +def test_pipe_storage_1M_kg(plant_config, subtests): + """Test underground pipe storage with 1M kg capacity matching original unit tests.""" + tech_config_dict = { + "model_inputs": { + "shared_parameters": { + "max_capacity": 1000000, + "max_charge_rate": 100000 / 24, # 100000 kg/day converted to kg/h + } + } + } + prob = om.Problem() + comp = PipeStorageCostModel( + plant_config=plant_config, + tech_config=tech_config_dict, + driver_config={}, + ) + prob.model.add_subsystem("sys", comp) + prob.setup() + prob.run_model() + + with subtests.test("CapEx"): + assert pytest.approx(prob.get_val("sys.CapEx")[0], rel=1e-5) == 508745483.851 + with subtests.test("OpEx"): + assert pytest.approx(prob.get_val("sys.OpEx")[0], rel=1e-5) == 16010851.734082155 + with subtests.test("VarOpEx"): + assert pytest.approx(np.sum(prob.get_val("sys.VarOpEx")), rel=1e-6) == 0.0 + with subtests.test("Cost year"): + assert prob.get_val("sys.cost_year") == 2018 diff --git a/h2integrate/storage/hydrogen/test/test_tol_mch_storage.py b/h2integrate/storage/hydrogen/test/test_tol_mch_storage.py index 7f703ae93..79dd90862 100644 --- a/h2integrate/storage/hydrogen/test/test_tol_mch_storage.py +++ b/h2integrate/storage/hydrogen/test/test_tol_mch_storage.py @@ -4,120 +4,6 @@ from pytest import approx, fixture from h2integrate.storage.hydrogen.mch_storage import MCHTOLStorageCostModel -from h2integrate.simulation.technologies.hydrogen.h2_storage.mch.mch_cost import MCHStorage - - -@fixture -def tol_mch_storage(): - # Test values are based on Supplementary Table 3 of - # https://doi.org/10.1038/s41467-024-53189-2 - Dc_tpd = 304 - Hc_tpd = 304 - As_tpy = 35000 - Ms_tpy = 16200 - in_dict = { - "max_H2_production_kg_pr_hr": (Hc_tpd + Dc_tpd) * 1e3 / 24, - "hydrogen_storage_capacity_kg": Ms_tpy * 1e3, - "hydrogen_demand_kg_pr_hr": Dc_tpd * 1e3 / 24, - "annual_hydrogen_stored_kg_pr_yr": As_tpy * 1e3, - } - - mch_storage = MCHStorage(**in_dict) - - return mch_storage - - -def test_init(): - # Test values are based on Supplementary Table 3 of - # https://doi.org/10.1038/s41467-024-53189-2 - Dc_tpd = 304 - Hc_tpd = 304 - As_tpy = 35000 - Ms_tpy = 16200 - in_dict = { - "max_H2_production_kg_pr_hr": (Hc_tpd + Dc_tpd) * 1e3 / 24, - "hydrogen_storage_capacity_kg": Ms_tpy * 1e3, - "hydrogen_demand_kg_pr_hr": Dc_tpd * 1e3 / 24, - "annual_hydrogen_stored_kg_pr_yr": As_tpy * 1e3, - } - mch_storage = MCHStorage(**in_dict) - - assert mch_storage.cost_year is not None - assert mch_storage.occ_coeff is not None - - -def test_sizing(tol_mch_storage, subtests): - # Test values are based on Supplementary Table 3 of - # https://doi.org/10.1038/s41467-024-53189-2 - Dc_tpd = 304 - Hc_tpd = 304 - As_tpy = 35000 - Ms_tpy = 16200 - with subtests.test("Dehydrogenation capacity"): - assert tol_mch_storage.Dc == approx(Dc_tpd, rel=1e-6) - with subtests.test("Hydrogenation capacity"): - assert tol_mch_storage.Hc == approx(Hc_tpd, rel=1e-6) - with subtests.test("Annual storage capacity"): - assert tol_mch_storage.As == approx(As_tpy, rel=1e-6) - with subtests.test("Maximum storage capacity"): - assert tol_mch_storage.Ms == approx(Ms_tpy, rel=1e-6) - - -def test_cost_calculation_methods(tol_mch_storage, subtests): - # Supplementary Table 3 - toc_actual = 639375591 - foc_actual = 10239180 - voc_actual = 17332229 - - max_cost_error_rel = 0.06 - capex = tol_mch_storage.calc_cost_value(*tol_mch_storage.occ_coeff) - fixed_om = tol_mch_storage.calc_cost_value(*tol_mch_storage.foc_coeff) - var_om = tol_mch_storage.calc_cost_value(*tol_mch_storage.voc_coeff) - with subtests.test("CapEx"): - assert capex == approx(toc_actual, rel=max_cost_error_rel) - with subtests.test("Fixed O&M"): - assert fixed_om == approx(foc_actual, rel=max_cost_error_rel) - with subtests.test("Variable O&M"): - assert var_om == approx(voc_actual, rel=max_cost_error_rel) - - -def test_run_costs(tol_mch_storage, subtests): - # Supplementary Table 3 - toc_actual = 639375591 - foc_actual = 10239180 - voc_actual = 17332229 - - max_cost_error_rel = 0.06 - cost_res = tol_mch_storage.run_costs() - - with subtests.test("CapEx"): - assert cost_res["mch_capex"] == approx(toc_actual, rel=max_cost_error_rel) - with subtests.test("Fixed O&M"): - assert cost_res["mch_opex"] == approx(foc_actual, rel=max_cost_error_rel) - with subtests.test("Variable O&M"): - assert cost_res["mch_variable_om"] == approx(voc_actual, rel=max_cost_error_rel) - - -def test_run_lcos(tol_mch_storage, subtests): - """ - This test is to highlight the difference between the LCOS when computed - using different methods from the same reference. - Specifically, the estimate_lcos and estimate_lcos_from_costs methods which - use Eq. 7 and Eq. 5 respectively from the source. - - Sources: - Breunig, H., Rosner, F., Saqline, S. et al. "Achieving gigawatt-scale green hydrogen - production and seasonal storage at industrial locations across the U.S." *Nat Commun* - **15**, 9049 (2024). https://doi.org/10.1038/s41467-024-53189-2 - """ - max_cost_error_rel = 0.06 - lcos_est = tol_mch_storage.estimate_lcos() - lcos_est_from_costs = tol_mch_storage.estimate_lcos_from_costs() - - with subtests.test("lcos equation"): - assert lcos_est == approx(2.05, rel=max_cost_error_rel) - with subtests.test("lcos equation from costs"): - assert lcos_est_from_costs == approx(2.05, rel=max_cost_error_rel) @fixture @@ -191,24 +77,6 @@ def test_mch_wrapper(plant_config, subtests): assert prob.get_val("sys.cost_year") == 2024 -def test_mch_ex1(subtests): - in_dict = { - "max_H2_production_kg_pr_hr": 14118.146788766157, - "hydrogen_storage_capacity_kg": 2081385.9326778147, - "hydrogen_demand_kg_pr_hr": 10775.824040553021, - "annual_hydrogen_stored_kg_pr_yr": 17878378.49459929, - } - mch_storage = MCHStorage(**in_dict) - cost_res = mch_storage.run_costs() - - with subtests.test("CapEx"): - assert pytest.approx(cost_res["mch_capex"], rel=1e-6) == 2.62304217 * 1e8 - with subtests.test("Fixed O&M"): - assert pytest.approx(cost_res["mch_opex"], rel=1e-6) == 7406935.548022923 - with subtests.test("Variable O&M"): - assert pytest.approx(cost_res["mch_variable_om"], rel=1e-6) == 9420636.00753175 - - def test_mch_wrapper_ex1(plant_config, subtests): # Ran Example 1 with MCH storage # Annual H2 Stored: 17878378.49459929 diff --git a/h2integrate/tools/eco/__init__.py b/h2integrate/tools/eco/__init__.py deleted file mode 100644 index 854517ca9..000000000 --- a/h2integrate/tools/eco/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .electrolysis import * -from .finance import * -from .utilities import * diff --git a/h2integrate/tools/eco/electrolysis.py b/h2integrate/tools/eco/electrolysis.py deleted file mode 100644 index e53b408b2..000000000 --- a/h2integrate/tools/eco/electrolysis.py +++ /dev/null @@ -1,282 +0,0 @@ -import warnings -from pathlib import Path - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -from matplotlib import ticker - -from h2integrate.tools.eco.utilities import ceildiv -from h2integrate.simulation.technologies.hydrogen.electrolysis.run_h2_PEM import run_h2_PEM -from h2integrate.simulation.technologies.hydrogen.electrolysis.pem_mass_and_footprint import ( - mass as run_electrolyzer_mass, - footprint as run_electrolyzer_footprint, -) - - -def run_electrolyzer_physics( - hopp_results, - h2integrate_config, - wind_resource, - design_scenario, - show_plots=False, - save_plots=False, - output_dir="./output/", - verbose=False, -): - if isinstance(output_dir, str): - output_dir = Path(output_dir).resolve() - electrolyzer_size_mw = h2integrate_config["electrolyzer"]["rating"] - electrolyzer_capex_kw = h2integrate_config["electrolyzer"]["electrolyzer_capex"] - - # IF GRID CONNECTED - if h2integrate_config["project_parameters"]["grid_connection"]: - # NOTE: if grid-connected, it assumes that hydrogen demand is input and there is not - # multi-cluster control strategies. This capability exists at the cluster level, not at the - # system level. - if h2integrate_config["electrolyzer"]["sizing"]["hydrogen_dmd"] is not None: - grid_connection_scenario = "grid-only" - hydrogen_production_capacity_required_kgphr = h2integrate_config["electrolyzer"][ - "sizing" - ]["hydrogen_dmd"] - energy_to_electrolyzer_kw = [] - else: - grid_connection_scenario = "off-grid" - hydrogen_production_capacity_required_kgphr = [] - energy_to_electrolyzer_kw = np.ones(8760) * electrolyzer_size_mw * 1e3 - # IF NOT GRID CONNECTED - else: - hydrogen_production_capacity_required_kgphr = [] - grid_connection_scenario = "off-grid" - energy_to_electrolyzer_kw = np.asarray( - hopp_results["combined_hybrid_power_production_hopp"] - ) - - n_pem_clusters = int( - ceildiv( - round(electrolyzer_size_mw, 1), h2integrate_config["electrolyzer"]["cluster_rating_MW"] - ) - ) - - electrolyzer_real_capacity_kW = ( - n_pem_clusters * h2integrate_config["electrolyzer"]["cluster_rating_MW"] * 1e3 - ) - - if np.abs(electrolyzer_real_capacity_kW - (electrolyzer_size_mw * 1e3)) > 1.0: - electrolyzer_real_capacity_mw = electrolyzer_real_capacity_kW / 1e3 - cluster_cap_mw = h2integrate_config["electrolyzer"]["cluster_rating_MW"] - msg = ( - f"setting electrolyzer capacity to {electrolyzer_real_capacity_mw} MW. " - f"Input value of {electrolyzer_size_mw:.2f} MW is not a " - f"multiple of cluster capacity ({cluster_cap_mw} MW)" - ) - warnings.warn(msg, UserWarning) - ## run using greensteel model - pem_param_dict = { - "eol_eff_percent_loss": h2integrate_config["electrolyzer"]["eol_eff_percent_loss"], - "uptime_hours_until_eol": h2integrate_config["electrolyzer"]["uptime_hours_until_eol"], - "include_degradation_penalty": h2integrate_config["electrolyzer"][ - "include_degradation_penalty" - ], - "turndown_ratio": h2integrate_config["electrolyzer"]["turndown_ratio"], - } - - if "water_usage_gal_pr_kg" in h2integrate_config["electrolyzer"]: - pem_param_dict.update( - {"water_usage_gal_pr_kg": h2integrate_config["electrolyzer"]["water_usage_gal_pr_kg"]} - ) - if "curve_coeff" in h2integrate_config["electrolyzer"]: - pem_param_dict.update({"curve_coeff": h2integrate_config["electrolyzer"]["curve_coeff"]}) - - if "time_between_replacement" in h2integrate_config["electrolyzer"]: - msg = ( - "`time_between_replacement` as an input is deprecated. It is now calculated internally" - " and is output in electrolyzer_physics_results['H2_Results']['Time Until Replacement" - " [hrs]']." - ) - warnings.warn(msg) - - H2_Results, h2_ts, h2_tot, power_to_electrolyzer_kw = run_h2_PEM( - electrical_generation_timeseries=energy_to_electrolyzer_kw, - electrolyzer_size=electrolyzer_size_mw, - useful_life=h2integrate_config["project_parameters"][ - "project_lifetime" - ], # EG: should be in years for full plant life - only used in financial model - n_pem_clusters=n_pem_clusters, - pem_control_type=h2integrate_config["electrolyzer"]["pem_control_type"], - electrolyzer_direct_cost_kw=electrolyzer_capex_kw, - user_defined_pem_param_dictionary=pem_param_dict, - grid_connection_scenario=grid_connection_scenario, # if not offgrid, assumes steady h2 demand in kgphr for full year # noqa: E501 - hydrogen_production_capacity_required_kgphr=hydrogen_production_capacity_required_kgphr, - debug_mode=False, - verbose=verbose, - ) - - # calculate mass and foorprint of system - mass_kg = run_electrolyzer_mass(electrolyzer_size_mw) - footprint_m2 = run_electrolyzer_footprint(electrolyzer_size_mw) - - # store results for return - H2_Results.update({"system capacity [kW]": electrolyzer_real_capacity_kW}) - electrolyzer_physics_results = { - "H2_Results": H2_Results, - "capacity_factor": H2_Results["Life: Capacity Factor"], - "equipment_mass_kg": mass_kg, - "equipment_footprint_m2": footprint_m2, - "power_to_electrolyzer_kw": power_to_electrolyzer_kw, - } - - if verbose: - print("\nElectrolyzer Physics:") # 61837444.34555772 145297297.29729727 - print( - "H2 Produced Annually (metric tons): ", - H2_Results["Life: Annual H2 production [kg/year]"] * 1e-3, - ) - print( - "Max H2 hourly (metric tons): ", - max(H2_Results["Hydrogen Hourly Production [kg/hr]"]) * 1e-3, - ) - print( - "Max H2 daily (metric tons): ", - max( - np.convolve( - H2_Results["Hydrogen Hourly Production [kg/hr]"], - np.ones(24), - mode="valid", - ) - ) - * 1e-3, - ) - - prodrate = 1.0 / round(H2_Results["Rated BOL: Efficiency [kWh/kg]"], 2) # kg/kWh - roughest = power_to_electrolyzer_kw * prodrate - print("Energy to electrolyzer (kWh): ", sum(power_to_electrolyzer_kw)) - print( - "Energy per kg (kWh/kg): ", - H2_Results["Sim: Total Input Power [kWh]"] / H2_Results["Sim: Total H2 Produced [kg]"], - ) - print("Max hourly based on est kg/kWh (kg): ", max(roughest)) - print( - "Max daily rough est (metric tons): ", - max(np.convolve(roughest, np.ones(24), mode="valid")) * 1e-3, - ) - print( - "Electrolyzer Life Average Capacity Factor: ", - H2_Results["Life: Capacity Factor"], - ) - - if save_plots or show_plots: - N = 24 * 7 * 4 - fig, ax = plt.subplots(3, 2, sharex=True, sharey="row") - - wind_speed = [W[2] for W in wind_resource._data["data"]] - - # plt.title("4-week running average") - pad = 5 - ax[0, 0].annotate( - "Hourly", - xy=(0.5, 1), - xytext=(0, pad), - xycoords="axes fraction", - textcoords="offset points", - size="large", - ha="center", - va="baseline", - ) - ax[0, 1].annotate( - "4-week running average", - xy=(0.5, 1), - xytext=(0, pad), - xycoords="axes fraction", - textcoords="offset points", - size="large", - ha="center", - va="baseline", - ) - - ax[0, 0].plot(wind_speed) - convolved_wind_speed = np.convolve(wind_speed, np.ones(N) / (N), mode="valid") - ave_x = range(N, len(convolved_wind_speed) + N) - - ax[0, 1].plot(ave_x, convolved_wind_speed) - ax[0, 0].set(ylabel="Wind\n(m/s)", ylim=[0, 30], xlim=[0, len(wind_speed)]) - tick_spacing = 10 - ax[0, 0].yaxis.set_major_locator(ticker.MultipleLocator(tick_spacing)) - - y = h2integrate_config["electrolyzer"]["rating"] - ax[1, 0].plot(energy_to_electrolyzer_kw * 1e-3) - ax[1, 0].axhline(y=y, color="r", linestyle="--", label="Nameplate Capacity") - - convolved_energy_to_electrolyzer = np.convolve( - energy_to_electrolyzer_kw * 1e-3, np.ones(N) / (N), mode="valid" - ) - - ax[1, 1].plot( - ave_x, - convolved_energy_to_electrolyzer, - ) - ax[1, 1].axhline(y=y, color="r", linestyle="--", label="Nameplate Capacity") - ax[1, 0].set(ylabel="Electrolyzer \nPower (MW)", ylim=[0, 500], xlim=[0, len(wind_speed)]) - # ax[1].legend(frameon=False, loc="best") - tick_spacing = 200 - ax[1, 0].yaxis.set_major_locator(ticker.MultipleLocator(tick_spacing)) - ax[1, 0].text(1000, y + 0.1 * tick_spacing, "Electrolyzer Rating", color="r") - - ax[2, 0].plot( - electrolyzer_physics_results["H2_Results"]["Hydrogen Hourly Production [kg/hr]"] * 1e-3 - ) - convolved_hydrogen_production = np.convolve( - electrolyzer_physics_results["H2_Results"]["Hydrogen Hourly Production [kg/hr]"] * 1e-3, - np.ones(N) / (N), - mode="valid", - ) - ax[2, 1].plot( - ave_x, - convolved_hydrogen_production, - ) - tick_spacing = 2 - ax[2, 0].set( - xlabel="Hour", - ylabel="Hydrogen\n(metric tons/hr)", - # ylim=[0, 7000], - xlim=[0, len(H2_Results["Hydrogen Hourly Production [kg/hr]"])], - ) - ax[2, 0].yaxis.set_major_locator(ticker.MultipleLocator(tick_spacing)) - - ax[2, 1].set( - xlabel="Hour", - # ylim=[0, 7000], - xlim=[ - 4 * 7 * 24 - 1, - len(H2_Results["Hydrogen Hourly Production [kg/hr]"] + 4 * 7 * 24 + 2), - ], - ) - ax[2, 1].yaxis.set_major_locator(ticker.MultipleLocator(tick_spacing)) - - plt.tight_layout() - if save_plots: - savepaths = [ - output_dir / "figures/production/", - output_dir / "data/", - ] - for savepath in savepaths: - if not savepath.exists(): - savepath.mkdir(parents=True) - plt.savefig( - savepaths[0] / f"production_overview_{design_scenario['id']}.png", - transparent=True, - ) - pd.DataFrame.from_dict( - data={ - "Hydrogen Hourly Production [kg/hr]": H2_Results[ - "Hydrogen Hourly Production [kg/hr]" - ], - "Hourly Water Consumption [kg/hr]": electrolyzer_physics_results["H2_Results"][ - "Water Hourly Consumption [kg/hr]" - ], - } - ).to_csv(savepaths[1] / f"h2_flow_{design_scenario['id']}.csv") - if show_plots: - plt.show() - - return electrolyzer_physics_results diff --git a/h2integrate/tools/eco/finance.py b/h2integrate/tools/eco/finance.py deleted file mode 100644 index ccd89018e..000000000 --- a/h2integrate/tools/eco/finance.py +++ /dev/null @@ -1,1844 +0,0 @@ -from __future__ import annotations - -from pathlib import Path - -import numpy as np -import pandas as pd -import ProFAST # system financial model -import numpy_financial as npf -from attrs import field, define -from ORBIT import ProjectManager -from hopp.simulation import HoppInterface - - -def adjust_dollar_year(init_cost, init_dollar_year, adj_cost_year, costing_general_inflation): - """Adjust cost based on inflation. - - Args: - init_cost (dict, float, int, list, np.ndarrray): cost of item ($) - init_dollar_year (int): dollar-year of init_cost - adj_cost_year (int): dollar-year to adjust cost to - costing_general_inflation (float): inflation rate (%) - - Returns: - same type as init_cost: cost in dollar-year of adj_cost_year - """ - periods = adj_cost_year - init_dollar_year - if isinstance(init_cost, (float, int)): - adj_cost = -npf.fv(costing_general_inflation, periods, 0.0, init_cost) - elif isinstance(init_cost, dict): - adj_cost = {} - for key, val in init_cost.items(): - new_val = -npf.fv(costing_general_inflation, periods, 0.0, val) - adj_cost.update({key: new_val}) - elif isinstance(init_cost, (list, np.ndarray)): - adj_cost = np.zeros(len(init_cost)) - for i in range(len(init_cost)): - adj_cost[i] = -npf.fv(costing_general_inflation, periods, 0.0, init_cost[i]) - if isinstance(init_cost, list): - adj_cost = list(adj_cost) - - return adj_cost - - -@define -class WindCostConfig: - """ - Represents the inputs to the wind cost models - - Attributes: - design_scenario (Dict[str, str]): - Definition of plant subsystem locations (e.g. onshore platform, offshore, none, etc) - hopp_config (Dict[str, float]): - Configuration parameters for HOPP - h2integrate_config (Dict[str, float]): - Configuration parameters for H2Integrate - orbit_config (Dict[str, float], optional): - Required input structure for ORBIT - turbine_config (Dict[str, float], optional): - Configuration parameters specific to turbine - orbit_hybrid_electrical_export_config (Dict[str, float], optional): - Configuration parameters for hybrid electrical export in ORBIT, required if using a - different substation size for the hybrid plant than for the wind plant alone - weather (Union[list, tuple, numpy.ndarray], optional): - Array-like of wind speeds for ORBIT to use in determining installation time and costs - """ - - design_scenario: dict[str, str] - hopp_config: dict[str, float] - h2integrate_config: dict[str, float] - orbit_config: dict[str, float] | None = field(default={}) - turbine_config: dict[str, float] | None = field(default={}) - orbit_hybrid_electrical_export_config: dict[str, float] | None = field(default={}) - weather: list | tuple | np.ndarray | None = field(default=None) - hopp_interface: HoppInterface | None = field(default=None) - - -@define -class WindCostOutputs: - """ - Represents the outputs to the wind cost models. - - Attributes: - total_wind_cost_no_export (float): - Total wind cost without export system costs - total_used_export_system_costs (float): - Total used export system costs - annual_operating_cost_wind (float): - Annual operating cost for wind - installation_time (float, optional): - Estimated installation time in months (default: 0.0) - orbit_project (dict, optional): - Details of the ORBIT project (default: None) - """ - - total_wind_cost_no_export: float - annual_operating_cost_wind: float - installation_time: float = field(default=0.0) - total_used_export_system_costs: float | None = field(default=0.0) - orbit_project: dict | ProjectManager | None = field(default=None) - - -def run_wind_cost_model(wind_cost_inputs: WindCostConfig, verbose=False) -> WindCostOutputs: - if wind_cost_inputs.design_scenario["wind_location"] == "offshore": - # if per kw - project, orbit_hybrid_electrical_export_project = run_orbit( - wind_cost_inputs.orbit_config, - verbose=verbose, - weather=wind_cost_inputs.weather, - orbit_hybrid_electrical_export_config=wind_cost_inputs.orbit_hybrid_electrical_export_config, - ) - - ( - total_wind_cost_no_export, - total_used_export_system_costs, - ) = breakout_export_costs_from_orbit_results( - project, - wind_cost_inputs.h2integrate_config, - wind_cost_inputs.design_scenario, - ) - - if orbit_hybrid_electrical_export_project is not None: - ( - _, - total_used_export_system_costs, - ) = breakout_export_costs_from_orbit_results( - orbit_hybrid_electrical_export_project, - wind_cost_inputs.h2integrate_config, - wind_cost_inputs.design_scenario, - ) - - # WIND ONLY Total O&M expenses including fixed, variable, and capacity-based, $/year - # use values from hybrid substation if a hybrid plant - if orbit_hybrid_electrical_export_project is None: - annual_operating_cost_wind = ( - max(project.monthly_opex.values()) * 12 - ) # np.average(hopp_results["hybrid_plant"].wind.om_total_expense) - - else: - annual_operating_cost_wind = ( - max(orbit_hybrid_electrical_export_project.monthly_opex.values()) * 12 - ) - - if "installation_time" in wind_cost_inputs.h2integrate_config["project_parameters"]: - installation_time = wind_cost_inputs.h2integrate_config["project_parameters"][ - "installation_time" - ] - else: - installation_time = (project.installation_time / (365 * 24)) * (12.0 / 1.0) - - # if total amount - # TODO - return WindCostOutputs( - total_wind_cost_no_export=total_wind_cost_no_export, - total_used_export_system_costs=total_used_export_system_costs, - annual_operating_cost_wind=annual_operating_cost_wind, - installation_time=installation_time, - orbit_project=project, - ) - elif wind_cost_inputs.design_scenario["wind_location"] == "onshore": - total_wind_cost_no_export = ( - wind_cost_inputs.hopp_config["config"]["cost_info"]["wind_installed_cost_mw"] - * wind_cost_inputs.hopp_config["technologies"]["wind"]["num_turbines"] - * wind_cost_inputs.turbine_config["turbine_rating"] - ) - - annual_operating_cost_wind = wind_cost_inputs.hopp_interface.system.wind.om_total_expense[0] - - if "installation_time" in wind_cost_inputs.h2integrate_config["project_parameters"]: - installation_time = wind_cost_inputs.h2integrate_config["project_parameters"][ - "installation_time" - ] - else: - installation_time = 0 - - return WindCostOutputs( - total_wind_cost_no_export=total_wind_cost_no_export, - annual_operating_cost_wind=annual_operating_cost_wind, - installation_time=installation_time, - ) - else: - raise ValueError( - "Wind design location must either be 'onshore' or 'offshore', but currently " - f"'wind_location' is set to {wind_cost_inputs.design_scenario['wind_location']}." - ) - - -# Function to run orbit from provided inputs - this is just for wind costs -def run_orbit(orbit_config, verbose=False, weather=None, orbit_hybrid_electrical_export_config={}): - # set up ORBIT - project = ProjectManager(orbit_config, weather=weather) - - # run ORBIT - project.run(availability=orbit_config["installation_availability"]) - - # run ORBIT for hybrid substation if applicable - if orbit_hybrid_electrical_export_config == {}: - hybrid_substation_project = None - else: - hybrid_substation_project = ProjectManager( - orbit_hybrid_electrical_export_config, weather=weather - ) - hybrid_substation_project.run(availability=orbit_config["installation_availability"]) - - # print results if desired - if verbose: - print(f"Installation CapEx: {project.installation_capex/1e6:.0f} M") - print(f"System CapEx: {project.system_capex/1e6:.0f} M") - print(f"Turbine CapEx: {project.turbine_capex/1e6:.0f} M") - print(f"Soft CapEx: {project.soft_capex/1e6:.0f} M") - print(f"Total CapEx: {project.total_capex/1e6:.0f} M") - print(f"Annual OpEx Rate: {max(project.monthly_opex.values())*12:.0f} ") - print(f"\nInstallation Time: {project.installation_time:.0f} h") - print("\nN Substations: ", (project.phases["ElectricalDesign"].num_substations)) - print("N cables: ", (project.phases["ElectricalDesign"].num_cables)) - print("\n") - - # cable cost breakdown - print("Cable specific costs") - print( - "Export cable installation CAPEX: %.2f M USD" - % (project.phases["ExportCableInstallation"].installation_capex * 1e-6) - ) - print("\n") - - return project, hybrid_substation_project - - -def adjust_orbit_costs(orbit_project, h2integrate_config): - if ("expected_plant_cost" in h2integrate_config["finance_parameters"]["wind"]) and ( - h2integrate_config["finance_parameters"]["wind"]["expected_plant_cost"] != "none" - ): - wind_capex_multiplier = ( - h2integrate_config["finance_parameters"]["wind"]["expected_plant_cost"] * 1e9 - ) / orbit_project.total_capex - else: - wind_capex_multiplier = 1.0 - - wind_total_capex = orbit_project.total_capex * wind_capex_multiplier - wind_capex_breakdown = orbit_project.capex_breakdown - for key in wind_capex_breakdown.keys(): - wind_capex_breakdown[key] *= wind_capex_multiplier - - return wind_total_capex, wind_capex_breakdown, wind_capex_multiplier - - -def breakout_export_costs_from_orbit_results(orbit_project, h2integrate_config, design_scenario): - # adjust wind capex to meet expectations - wind_total_capex, wind_capex_breakdown, wind_capex_multiplier = adjust_orbit_costs( - orbit_project=orbit_project, h2integrate_config=h2integrate_config - ) - - # onshore substation cost not included in ORBIT costs by default, so add it separately - total_wind_installed_costs_with_export = wind_total_capex - - # breakout export system costs - array_cable_equipment_cost = wind_capex_breakdown["Array System"] - array_cable_installation_cost = wind_capex_breakdown["Array System Installation"] - total_array_cable_system_capex = array_cable_equipment_cost + array_cable_installation_cost - - export_cable_equipment_cost = wind_capex_breakdown[ - "Export System" - ] # this should include the onshore substation - export_cable_installation_cost = wind_capex_breakdown["Export System Installation"] - substation_equipment_cost = wind_capex_breakdown["Offshore Substation"] - substation_installation_cost = wind_capex_breakdown["Offshore Substation Installation"] - total_export_cable_system_capex = export_cable_equipment_cost + export_cable_installation_cost - - total_offshore_substation_capex = substation_equipment_cost + substation_installation_cost - - total_electrical_export_system_cost = ( - total_array_cable_system_capex - + total_offshore_substation_capex - + total_export_cable_system_capex - ) - - ## adjust wind cost to remove export - if design_scenario["transportation"] == "hvdc+pipeline": - unused_export_system_cost = 0.0 - elif ( - design_scenario["electrolyzer_location"] == "turbine" - and design_scenario["h2_storage_location"] == "turbine" - ): - unused_export_system_cost = ( - total_array_cable_system_capex - + total_export_cable_system_capex - + total_offshore_substation_capex - ) - elif ( - design_scenario["electrolyzer_location"] == "turbine" - and design_scenario["h2_storage_location"] == "platform" - ): - unused_export_system_cost = total_export_cable_system_capex # TODO check assumptions here - elif ( - design_scenario["electrolyzer_location"] == "platform" - and design_scenario["h2_storage_location"] == "platform" - ): - unused_export_system_cost = total_export_cable_system_capex # TODO check assumptions here - elif ( - design_scenario["electrolyzer_location"] == "platform" - or design_scenario["electrolyzer_location"] == "turbine" - ) and design_scenario["h2_storage_location"] == "onshore": - unused_export_system_cost = total_export_cable_system_capex # TODO check assumptions here - else: - unused_export_system_cost = 0.0 - - total_used_export_system_costs = total_electrical_export_system_cost - unused_export_system_cost - - total_wind_cost_no_export = ( - total_wind_installed_costs_with_export - total_used_export_system_costs - ) - - return total_wind_cost_no_export, total_used_export_system_costs - - -def run_capex( - hopp_results, - wind_cost_results, - electrolyzer_cost_results, - h2_pipe_array_results, - h2_transport_compressor_results, - h2_transport_pipe_results, - h2_storage_results, - hopp_config, - h2integrate_config, - design_scenario, - desal_results, - platform_results, - verbose=False, -): - # total_wind_cost_no_export, total_used_export_system_costs = breakout_export_costs_from_orbit_results(orbit_project, h2integrate_config, design_scenario) # noqa: E501 - - # if orbit_hybrid_electrical_export_project is not None: - # _, total_used_export_system_costs = breakout_export_costs_from_orbit_results(orbit_hybrid_electrical_export_project, h2integrate_config, design_scenario) # noqa: E501 - - # wave capex - if hopp_config["site"]["wave"]: - cost_dict = hopp_results["hybrid_plant"].wave.mhk_costs.cost_outputs - - wcapex = ( - cost_dict["structural_assembly_cost_modeled"] - + cost_dict["power_takeoff_system_cost_modeled"] - + cost_dict["mooring_found_substruc_cost_modeled"] - ) - wbos = ( - cost_dict["development_cost_modeled"] - + cost_dict["eng_and_mgmt_cost_modeled"] - + cost_dict["plant_commissioning_cost_modeled"] - + cost_dict["site_access_port_staging_cost_modeled"] - + cost_dict["assembly_and_install_cost_modeled"] - + cost_dict["other_infrastructure_cost_modeled"] - ) - welec_infrastruc_costs = ( - cost_dict["array_cable_system_cost_modeled"] - + cost_dict["export_cable_system_cost_modeled"] - + cost_dict["other_elec_infra_cost_modeled"] - ) # +\ - # cost_dict['onshore_substation_cost_modeled']+\ - # cost_dict['offshore_substation_cost_modeled'] - # financial = cost_dict['project_contingency']+\ - # cost_dict['insurance_during_construction']+\ - # cost_dict['reserve_accounts'] - wave_capex = wcapex + wbos + welec_infrastruc_costs - else: - wave_capex = 0.0 - - # solar capex - if "pv" in hopp_config["technologies"].keys(): - solar_capex = hopp_results["hybrid_plant"].pv.total_installed_cost - else: - solar_capex = 0.0 - - # battery capex - if "battery" in hopp_config["technologies"].keys(): - battery_capex = hopp_results["hybrid_plant"].battery.total_installed_cost - else: - battery_capex = 0.0 - - # TODO bos capex - # bos_capex = hopp_results["hybrid_plant"].bos.total_installed_cost - - ## desal capex - if desal_results is not None: - desal_capex = desal_results["desal_capex_usd"] - else: - desal_capex = 0.0 - - ## electrolyzer capex - electrolyzer_total_capital_cost = electrolyzer_cost_results["electrolyzer_total_capital_cost"] - - if ( - design_scenario["electrolyzer_location"] == "platform" - or design_scenario["h2_storage_location"] == "platform" - or hopp_config["site"]["solar"] - ): - platform_costs = platform_results["capex"] - else: - platform_costs = 0.0 - - # h2 transport - h2_transport_compressor_capex = h2_transport_compressor_results["compressor_capex"] - h2_transport_pipe_capex = h2_transport_pipe_results["total capital cost [$]"][0] - - ## h2 storage - if h2integrate_config["h2_storage"]["type"] == "none": - h2_storage_capex = 0.0 - elif ( - h2integrate_config["h2_storage"]["type"] == "pipe" - ): # ug pipe storage model includes compression - h2_storage_capex = h2_storage_results["storage_capex"] - elif ( - h2integrate_config["h2_storage"]["type"] == "turbine" - ): # ug pipe storage model includes compression - h2_storage_capex = h2_storage_results["storage_capex"] - elif ( - h2integrate_config["h2_storage"]["type"] == "pressure_vessel" - ): # pressure vessel storage model includes compression - h2_storage_capex = h2_storage_results["storage_capex"] - elif ( - h2integrate_config["h2_storage"]["type"] == "salt_cavern" - ): # salt cavern storage model includes compression - h2_storage_capex = h2_storage_results["storage_capex"] - elif ( - h2integrate_config["h2_storage"]["type"] == "lined_rock_cavern" - ): # lined rock cavern storage model includes compression - h2_storage_capex = h2_storage_results["storage_capex"] - else: - msg = ( - f'the storage type you have indicated ({h2integrate_config["h2_storage"]["type"]}) ' - 'has not been implemented.' - ) - raise NotImplementedError(msg) - - # store capex component breakdown - capex_breakdown = { - "wind": wind_cost_results.total_wind_cost_no_export, - "wave": wave_capex, - "solar": solar_capex, - "battery": battery_capex, - "platform": platform_costs, - "electrical_export_system": wind_cost_results.total_used_export_system_costs, - "desal": desal_capex, - "electrolyzer": electrolyzer_total_capital_cost, - "h2_pipe_array": h2_pipe_array_results["capex"], - "h2_transport_compressor": h2_transport_compressor_capex, - "h2_transport_pipeline": h2_transport_pipe_capex, - "h2_storage": h2_storage_capex, - } - - # discount capex to appropriate year for unified costing - for key in capex_breakdown.keys(): - if key == "h2_storage": - # if design_scenario["h2_storage_location"] == "turbine" and h2integrate_config["h2_storage"]["type"] == "turbine": # noqa: E501 - # cost_year = h2integrate_config["finance_parameters"]["discount_years"][key][ - # design_scenario["h2_storage_location"] - # ] - # else: - cost_year = h2integrate_config["finance_parameters"]["discount_years"][key][ - h2integrate_config["h2_storage"]["type"] - ] - else: - cost_year = h2integrate_config["finance_parameters"]["discount_years"][key] - - capex_breakdown[key] = adjust_dollar_year( - capex_breakdown[key], - cost_year, - h2integrate_config["project_parameters"]["cost_year"], - h2integrate_config["finance_parameters"]["costing_general_inflation"], - ) - - total_system_installed_cost = sum(capex_breakdown[key] for key in capex_breakdown.keys()) - - if verbose: - print("\nCAPEX Breakdown") - for key in capex_breakdown.keys(): - print(key, "%.2f" % (capex_breakdown[key] * 1e-6), " M") - - print( - "\nTotal system CAPEX: ", - "$%.2f" % (total_system_installed_cost * 1e-9), - " B", - ) - - return total_system_installed_cost, capex_breakdown - - -def run_fixed_opex( - hopp_results, - wind_cost_results, - electrolyzer_cost_results, - h2_pipe_array_results, - h2_transport_compressor_results, - h2_transport_pipe_results, - h2_storage_results, - hopp_config, - h2integrate_config, - desal_results, - platform_results, - verbose=False, - total_export_system_cost=0, -): - # WIND ONLY Total O&M expenses including fixed, variable, and capacity-based, $/year - # use values from hybrid substation if a hybrid plant - # if orbit_hybrid_electrical_export_project is None: - - # wave opex - if hopp_config["site"]["wave"]: - cost_dict = hopp_results["hybrid_plant"].wave.mhk_costs.cost_outputs - wave_opex = cost_dict["maintenance_cost"] + cost_dict["operations_cost"] - else: - wave_opex = 0.0 - - # solar opex - if "pv" in hopp_config["technologies"].keys(): - solar_opex = hopp_results["hybrid_plant"].pv.om_total_expense[0] - if solar_opex < 0.1: - raise (RuntimeWarning(f"Solar OPEX returned as {solar_opex}")) - else: - solar_opex = 0.0 - - # battery opex - if "battery" in hopp_config["technologies"].keys(): - battery_opex = hopp_results["hybrid_plant"].battery.om_total_expense[0] - if battery_opex < 0.1: - raise (RuntimeWarning(f"Battery OPEX returned as {battery_opex}")) - else: - battery_opex = 0.0 - - # H2 OPEX - platform_operating_costs = platform_results["opex"] # TODO update this - - annual_operating_cost_h2 = electrolyzer_cost_results["electrolyzer_OM_cost_annual"] - - h2_transport_compressor_opex = h2_transport_compressor_results["compressor_opex"] # annual - - h2_transport_pipeline_opex = h2_transport_pipe_results["annual operating cost [$]"][0] # annual - - storage_opex = h2_storage_results["storage_opex"] - # desal OPEX - if desal_results is not None: - desal_opex = desal_results["desal_opex_usd_per_year"] - else: - desal_opex = 0.0 - annual_operating_cost_desal = desal_opex - - # store opex component breakdown - opex_breakdown_annual = { - "wind_and_electrical": wind_cost_results.annual_operating_cost_wind, - "platform": platform_operating_costs, - # "electrical_export_system": total_export_om_cost, - "wave": wave_opex, - "solar": solar_opex, - "battery": battery_opex, - "desal": annual_operating_cost_desal, - "electrolyzer": annual_operating_cost_h2, - "h2_pipe_array": h2_pipe_array_results["opex"], - "h2_transport_compressor": h2_transport_compressor_opex, - "h2_transport_pipeline": h2_transport_pipeline_opex, - "h2_storage": storage_opex, - } - - # discount opex to appropriate year for unified costing - for key in opex_breakdown_annual.keys(): - if key == "h2_storage": - cost_year = h2integrate_config["finance_parameters"]["discount_years"][key][ - h2integrate_config["h2_storage"]["type"] - ] - else: - cost_year = h2integrate_config["finance_parameters"]["discount_years"][key] - - opex_breakdown_annual[key] = adjust_dollar_year( - opex_breakdown_annual[key], - cost_year, - h2integrate_config["project_parameters"]["cost_year"], - h2integrate_config["finance_parameters"]["costing_general_inflation"], - ) - - # Calculate the total annual OPEX of the installed system - total_annual_operating_costs = sum(opex_breakdown_annual.values()) - - if verbose: - print("\nAnnual OPEX Breakdown") - for key in opex_breakdown_annual.keys(): - print(key, "%.2f" % (opex_breakdown_annual[key] * 1e-6), " M") - - print( - "\nTotal Annual OPEX: ", - "$%.2f" % (total_annual_operating_costs * 1e-6), - " M", - ) - print(opex_breakdown_annual) - return total_annual_operating_costs, opex_breakdown_annual - - -def run_variable_opex( - electrolyzer_cost_results, - h2integrate_config, -): - """calculate variable O&M in $/kg-H2. - - Args: - electrolyzer_cost_results (dict): output of - h2integrate.tools.eco.electrolysis.run_electrolyzer_cost - h2integrate_config (:obj:`h2integrate_simulation.H2IntegrateSimulationConfig`): H2Integrate - simulation config. - - Returns: - dict: dictionary of components and corresponding variable O&M in $/kg-H2 for - adjusted for inflation so cost is in dollar-year corresponding to - `h2integrate_config["project_parameters"]["cost_year"]` - """ - electrolyzer_vom = electrolyzer_cost_results["electrolyzer_variable_OM_annual"] - - vopex_breakdown_annual = {"electrolyzer": electrolyzer_vom} - - for key in vopex_breakdown_annual.keys(): - cost_year = h2integrate_config["finance_parameters"]["discount_years"][key] - vopex_breakdown_annual[key] = adjust_dollar_year( - vopex_breakdown_annual[key], - cost_year, - h2integrate_config["project_parameters"]["cost_year"], - h2integrate_config["finance_parameters"]["costing_general_inflation"], - ) - return vopex_breakdown_annual - - -def calc_financial_parameter_weighted_average_by_capex( - parameter_name: str, h2integrate_config: dict, capex_breakdown: dict -) -> float: - """Allows the user to provide individual financial parameters for each technology in the system. - The values given will be weighted by their CAPEX values to determine the final - weighted-average parameter value to be supplied to the financial model. If only one - technology has a unique parameter value, a "general" parameter value in the dictionary and - that will be used for all technologies not specified individually. - - Args: - parameter_name (str): The name of the parameter to be weighted by capex. The name should - correspond to the name in the h2integrate config - h2integrate_config (dict): Dictionary form of the h2integrate config - capex_breakdown (dict): Output from `run_capex`, a dictionary of all capital items for - the financial model - - Returns: - parameter_value (float): if the parameter in the h2integrate config is given as a - dictionary, then the weighted average by capex parameter value is returnd. Otherwise no - averaging is done and the value of the parameter in the h2integrate_config is returned. - """ - - if type(h2integrate_config["finance_parameters"][parameter_name]) is not dict: - # if only one value is given for the parameter, use that value - parameter_value = h2integrate_config["finance_parameters"][parameter_name] - - else: - # assign capex amounts as weights - weights = np.array(list(capex_breakdown.values())) - - # initialize value array - values = np.zeros_like(weights) - - # assign values - for i, key in enumerate(capex_breakdown.keys()): - if key in h2integrate_config["finance_parameters"][parameter_name].keys(): - values[i] = h2integrate_config["finance_parameters"][parameter_name][key] - elif capex_breakdown[key] == 0.0: - values[i] = 0.0 - else: - values[i] = h2integrate_config["finance_parameters"][parameter_name]["general"] - - # calcuated weighted average parameter value - parameter_value = np.average(values, weights=weights) - return parameter_value - - -def run_profast_lcoe( - h2integrate_config, - wind_cost_results, - capex_breakdown, - opex_breakdown, - hopp_results, - incentive_option, - design_scenario, - verbose=False, - show_plots=False, - save_plots=False, - output_dir="./output/", -): - if isinstance(output_dir, str): - output_dir = Path(output_dir).resolve() - gen_inflation = h2integrate_config["finance_parameters"]["inflation_rate"] - - # initialize dictionary of weights for averaging financial parameters - finance_param_weights = {} - - if ( - design_scenario["h2_storage_location"] == "onshore" - or design_scenario["electrolyzer_location"] == "onshore" - ): - if "land_cost" in h2integrate_config["finance_parameters"]: - land_cost = h2integrate_config["finance_parameters"]["land_cost"] - else: - land_cost = 1e6 # TODO should model this - else: - land_cost = 0.0 - - pf = ProFAST.ProFAST() - pf.set_params( - "commodity", - { - "name": "electricity", - "unit": "kWh", - "initial price": 100, - "escalation": gen_inflation, - }, - ) - pf.set_params( - "capacity", - np.sum(hopp_results["combined_hybrid_power_production_hopp"]) / 365.0, - ) # kWh/day - pf.set_params("maintenance", {"value": 0, "escalation": gen_inflation}) - pf.set_params( - "analysis start year", - h2integrate_config["project_parameters"]["financial_analysis_start_year"], - ) - pf.set_params("operating life", h2integrate_config["project_parameters"]["project_lifetime"]) - pf.set_params( - "installation months", h2integrate_config["project_parameters"]["installation_time"] - ) - pf.set_params( - "installation cost", - { - "value": 0, - "depr type": "Straight line", - "depr period": 4, - "depreciable": False, - }, - ) - if land_cost > 0: - pf.set_params("non depr assets", land_cost) - pf.set_params( - "end of proj sale non depr assets", - land_cost - * (1 + gen_inflation) ** h2integrate_config["project_parameters"]["project_lifetime"], - ) - pf.set_params("demand rampup", 0) - pf.set_params("long term utilization", 1) - pf.set_params("credit card fees", 0) - pf.set_params("sales tax", h2integrate_config["finance_parameters"]["sales_tax_rate"]) - pf.set_params("license and permit", {"value": 00, "escalation": gen_inflation}) - pf.set_params("rent", {"value": 0, "escalation": gen_inflation}) - pf.set_params( - "property tax and insurance", - h2integrate_config["finance_parameters"]["property_tax"] - + h2integrate_config["finance_parameters"]["property_insurance"], - ) - pf.set_params( - "admin expense", - h2integrate_config["finance_parameters"]["administrative_expense_percent_of_sales"], - ) - pf.set_params( - "total income tax rate", - h2integrate_config["finance_parameters"]["total_income_tax_rate"], - ) - pf.set_params( - "capital gains tax rate", - h2integrate_config["finance_parameters"]["capital_gains_tax_rate"], - ) - pf.set_params("sell undepreciated cap", True) - pf.set_params("tax losses monetized", True) - pf.set_params("general inflation rate", gen_inflation) - - pf.set_params("debt type", h2integrate_config["finance_parameters"]["debt_type"]) - pf.set_params("loan period if used", h2integrate_config["finance_parameters"]["loan_period"]) - - pf.set_params("cash onhand", h2integrate_config["finance_parameters"]["cash_onhand_months"]) - - # ----------------------------------- Add capital items to ProFAST ---------------- - if "wind" in capex_breakdown.keys(): - pf.add_capital_item( - name="Wind system", - cost=capex_breakdown["wind"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["wind"] = capex_breakdown["wind"] - if "wave" in capex_breakdown.keys(): - pf.add_capital_item( - name="Wave system", - cost=capex_breakdown["wave"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["wave"] = capex_breakdown["wave"] - if "solar" in capex_breakdown.keys(): - pf.add_capital_item( - name="Solar PV system", - cost=capex_breakdown["solar"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["solar"] = capex_breakdown["solar"] - if "battery" in capex_breakdown.keys(): - pf.add_capital_item( - name="Battery system", - cost=capex_breakdown["battery"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["battery"] = capex_breakdown["battery"] - if design_scenario["transportation"] == "hvdc+pipeline" or not ( - design_scenario["electrolyzer_location"] == "turbine" - and design_scenario["h2_storage_location"] == "turbine" - ): - pf.add_capital_item( - name="Electrical export system", - cost=capex_breakdown["electrical_export_system"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["electrical_export_system"] = capex_breakdown[ - "electrical_export_system" - ] - # -------------------------------------- Add fixed costs-------------------------------- - pf.add_fixed_cost( - name="Wind and electrical fixed O&M cost", - usage=1.0, - unit="$/year", - cost=opex_breakdown["wind_and_electrical"], - escalation=gen_inflation, - ) - - if "wave" in opex_breakdown.keys(): - pf.add_fixed_cost( - name="Wave O&M cost", - usage=1.0, - unit="$/year", - cost=opex_breakdown["wave"], - escalation=gen_inflation, - ) - - if "solar" in opex_breakdown.keys(): - pf.add_fixed_cost( - name="Solar O&M cost", - usage=1.0, - unit="$/year", - cost=opex_breakdown["solar"], - escalation=gen_inflation, - ) - - if "battery" in opex_breakdown.keys(): - pf.add_fixed_cost( - name="Battery O&M cost", - usage=1.0, - unit="$/year", - cost=opex_breakdown["battery"], - escalation=gen_inflation, - ) - - # ------------------------------------- add incentives ----------------------------------- - """ - Note: ptc units must be given to ProFAST in terms of dollars per unit of the primary commodity - being produced - - Note: full tech-nutral (wind) tax credits are no longer available if constructions starts after - Jan. 1 2034 (Jan 1. 2033 for h2 ptc) - """ - - # catch incentive option and add relevant incentives - incentive_dict = h2integrate_config["policy_parameters"][f"option{incentive_option}"] - # add electricity_ptc ($/kW) - # adjust from 1992 dollars to start year - wind_ptc_in_dollars_per_kw = -npf.fv( - h2integrate_config["finance_parameters"]["costing_general_inflation"], - h2integrate_config["project_parameters"]["financial_analysis_start_year"] - + round(wind_cost_results.installation_time / 12) - - 1992, - 0, - incentive_dict["electricity_ptc"], - ) # given in 1992 dollars but adjust for inflation - - pf.add_incentive( - name="Electricity PTC", - value=wind_ptc_in_dollars_per_kw, - decay=-gen_inflation, - sunset_years=10, - tax_credit=True, - ) # TODO check decay - - # ----------------------- Add weight-averaged parameters ----------------------- - - equity_discount_rate = calc_financial_parameter_weighted_average_by_capex( - parameter_name="discount_rate", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "leverage after tax nominal discount rate", - equity_discount_rate, - ) - - debt_interest_rate = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_interest_rate", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt interest rate", - debt_interest_rate, - ) - - if "debt_equity_split" in h2integrate_config["finance_parameters"].keys(): - debt_equity_split = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_equity_split", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt equity ratio of initial financing", - (debt_equity_split / (100 - debt_equity_split)), - ) - elif "debt_equity_ratio" in h2integrate_config["finance_parameters"].keys(): - debt_equity_ratio = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_equity_ratio", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt equity ratio of initial financing", - debt_equity_ratio, - ) - else: - msg = ( - "missing value in `finance_parameters`. " - "Requires either `debt_equity_ratio` or `debt_equity_split`" - ) - raise ValueError(msg) - - # ---------------------- Run ProFAST ------------------------------------------- - sol = pf.solve_price() - - lcoe = sol["price"] - - if verbose: - print("\nProFAST LCOE: ", "%.2f" % (lcoe * 1e3), "$/MWh") - - # -------------------------- Plots --------------------------------------------- - if show_plots or save_plots: - savepath = output_dir / "figures/wind_only" - if not savepath.exists(): - savepath.mkdir(parents=True) - pf.plot_costs_yearly( - per_kg=False, - scale="M", - remove_zeros=True, - remove_depreciation=False, - fileout=savepath / f'annual_cash_flow_wind_only_{design_scenario["id"]}.png', - show_plot=show_plots, - ) - pf.plot_costs_yearly2( - per_kg=False, - scale="M", - remove_zeros=True, - remove_depreciation=False, - fileout=savepath / f'annual_cash_flow_wind_only_{design_scenario["id"]}.html', - show_plot=show_plots, - ) - pf.plot_capital_expenses( - fileout=savepath / f'capital_expense_only_{design_scenario["id"]}.png', - show_plot=show_plots, - ) - pf.plot_cashflow( - fileout=savepath / f'cash_flow_wind_only_{design_scenario["id"]}.png', - show_plot=show_plots, - ) - pf.plot_costs( - fileout=savepath / f'cost_breakdown_{design_scenario["id"]}.png', - show_plot=show_plots, - ) - - return lcoe, pf, sol - - -def run_profast_grid_only( - h2integrate_config, - wind_cost_results, - electrolyzer_performance_results, - capex_breakdown, - opex_breakdown_total, - hopp_results, - design_scenario, - total_accessory_power_renewable_kw, - total_accessory_power_grid_kw, - verbose=False, - show_plots=False, - save_plots=False, - output_dir="./output/", -): - vopex_breakdown = opex_breakdown_total["variable_om"] - fopex_breakdown = opex_breakdown_total["fixed_om"] - - if isinstance(output_dir, str): - output_dir = Path(output_dir).resolve() - gen_inflation = h2integrate_config["finance_parameters"]["inflation_rate"] - - # initialize dictionary of weights for averaging financial parameters - finance_param_weights = {} - - if ( - design_scenario["h2_storage_location"] == "onshore" - or design_scenario["electrolyzer_location"] == "onshore" - ): - if "land_cost" in h2integrate_config["finance_parameters"]: - land_cost = h2integrate_config["finance_parameters"]["land_cost"] - else: - land_cost = 1e6 # TODO should model this - else: - land_cost = 0.0 - - pf = ProFAST.ProFAST() - pf.set_params( - "commodity", - { - "name": "Hydrogen", - "unit": "kg", - "initial price": 100, - "escalation": gen_inflation, - }, - ) - pf.set_params( - "capacity", - electrolyzer_performance_results.rated_capacity_kg_pr_day, - ) # kg/day - pf.set_params("maintenance", {"value": 0, "escalation": gen_inflation}) - # TODO: update analysis start year below (ESG) - pf.set_params( - "analysis start year", - h2integrate_config["project_parameters"]["financial_analysis_start_year"], - ) - pf.set_params("operating life", h2integrate_config["project_parameters"]["project_lifetime"]) - pf.set_params( - "installation cost", - { - "value": 0, - "depr type": "Straight line", - "depr period": 4, - "depreciable": False, - }, - ) - if land_cost > 0: - pf.set_params("non depr assets", land_cost) - pf.set_params( - "end of proj sale non depr assets", - land_cost - * (1 + gen_inflation) ** h2integrate_config["project_parameters"]["project_lifetime"], - ) - pf.set_params("demand rampup", 0) - pf.set_params("long term utilization", electrolyzer_performance_results.long_term_utilization) - pf.set_params("credit card fees", 0) - pf.set_params("sales tax", h2integrate_config["finance_parameters"]["sales_tax_rate"]) - pf.set_params("license and permit", {"value": 00, "escalation": gen_inflation}) - pf.set_params("rent", {"value": 0, "escalation": gen_inflation}) - pf.set_params( - "property tax and insurance", - h2integrate_config["finance_parameters"]["property_tax"] - + h2integrate_config["finance_parameters"]["property_insurance"], - ) - pf.set_params( - "admin expense", - h2integrate_config["finance_parameters"]["administrative_expense_percent_of_sales"], - ) - pf.set_params( - "total income tax rate", - h2integrate_config["finance_parameters"]["total_income_tax_rate"], - ) - pf.set_params( - "capital gains tax rate", - h2integrate_config["finance_parameters"]["capital_gains_tax_rate"], - ) - pf.set_params("sell undepreciated cap", True) - pf.set_params("tax losses monetized", True) - pf.set_params("general inflation rate", gen_inflation) - - pf.set_params("debt type", h2integrate_config["finance_parameters"]["debt_type"]) - pf.set_params("loan period if used", h2integrate_config["finance_parameters"]["loan_period"]) - - pf.set_params("cash onhand", h2integrate_config["finance_parameters"]["cash_onhand_months"]) - - # ----------------------------------- Add capital items to ProFAST ---------------- - - pf.add_capital_item( - name="Electrolysis system", - cost=capex_breakdown["electrolyzer"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period_electrolyzer"], - refurb=electrolyzer_performance_results.refurb_cost_percent, - ) - finance_param_weights["electrolyzer"] = capex_breakdown["electrolyzer"] - pf.add_capital_item( - name="Hydrogen storage system", - cost=capex_breakdown["h2_storage"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period_electrolyzer"], - refurb=[0], - ) - finance_param_weights["h2_storage"] = capex_breakdown["h2_storage"] - # -------------------------------------- Add fixed costs-------------------------------- - pf.add_fixed_cost( - name="Electrolyzer fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["electrolyzer"], - escalation=gen_inflation, - ) - pf.add_fixed_cost( - name="Hydrogen storage fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["h2_storage"], - escalation=gen_inflation, - ) - - # ---------------------- Add feedstocks, note the various cost options------------------- - pf.add_feedstock( - name="Water", - usage=electrolyzer_performance_results.water_usage_gal_pr_kg, - unit="gal", - cost="US Average", - escalation=gen_inflation, - ) - pf.add_feedstock( - name="Electrolyzer Variable O&M", - usage=1.0, - unit="$/kg", - cost=vopex_breakdown["electrolyzer"], - escalation=gen_inflation, - ) - - # if h2integrate_config["project_parameters"]["grid_connection"]: - - energy_purchase = ( - 365 * 24 * h2integrate_config["electrolyzer"]["rating"] * 1e3 - + sum(total_accessory_power_renewable_kw) - + sum(total_accessory_power_grid_kw) - ) - - pf.add_fixed_cost( - name="Electricity from grid", - usage=1.0, - unit="$/year", - cost=energy_purchase * h2integrate_config["project_parameters"]["ppa_price"], - escalation=gen_inflation, - ) - - # ----------------------- Add weight-averaged parameters ----------------------- - - equity_discount_rate = calc_financial_parameter_weighted_average_by_capex( - parameter_name="discount_rate", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - - pf.set_params( - "leverage after tax nominal discount rate", - equity_discount_rate, - ) - - debt_interest_rate = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_interest_rate", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt interest rate", - debt_interest_rate, - ) - - if "debt_equity_split" in h2integrate_config["finance_parameters"].keys(): - debt_equity_split = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_equity_split", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt equity ratio of initial financing", - (debt_equity_split / (100 - debt_equity_split)), - ) - elif "debt_equity_ratio" in h2integrate_config["finance_parameters"].keys(): - debt_equity_ratio = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_equity_ratio", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt equity ratio of initial financing", - debt_equity_ratio, - ) - else: - msg = ( - "missing value in `finance_parameters`. " - "Requires either `debt_equity_ratio` or `debt_equity_split`" - ) - raise ValueError(msg) - - # ----------------------- Run ProFAST ----------------------------------------- - - sol = pf.solve_price() - - lcoh = sol["price"] - if verbose: - print(f"\nLCOH grid only: {lcoh:.2f} $/kg") - print(f'ProFAST grid only NPV: {sol["NPV"]:.2f}') - print(f'ProFAST grid only IRR: {max(sol["irr"]):.5f}') - print(f'ProFAST grid only LCO: {sol["lco"]:.2f} $/kg') - print(f'ProFAST grid only Profit Index: {sol["profit index"]:.2f}') - print(f'ProFAST grid only payback period: {sol["investor payback period"]}') - - # ----------------------- Plots ----------------------------------------------- - if save_plots or show_plots: - savepaths = [ - output_dir / "figures/capex", - output_dir / "figures/annual_cash_flow", - output_dir / "figures/lcoh_breakdown", - output_dir / "data", - ] - for savepath in savepaths: - if not savepath.exists(): - savepath.mkdir(parents=True) - - pf.plot_capital_expenses( - fileout=savepaths[0] / f"capital_expense_grid_only_{design_scenario['id']}.pdf", - show_plot=show_plots, - ) - pf.plot_cashflow( - fileout=savepaths[1] / f"cash_flow_grid_only_{design_scenario['id']}.png", - show_plot=show_plots, - ) - - pd.DataFrame.from_dict(data=pf.cash_flow_out, orient="index").to_csv( - savepaths[3] / f"cash_flow_grid_only_{design_scenario['id']}.csv" - ) - - pf.plot_costs( - savepaths[2] / f"lcoh_grid_only_{design_scenario['id']}", - show_plot=show_plots, - ) - return lcoh, pf, sol - - -def run_profast_full_plant_model( - h2integrate_config, - wind_cost_results, - electrolyzer_performance_results, - capex_breakdown, - opex_breakdown_total, - hopp_results, - incentive_option, - design_scenario, - total_accessory_power_renewable_kw, - total_accessory_power_grid_kw, - verbose=False, - show_plots=False, - save_plots=False, - output_dir="./output/", -): - vopex_breakdown = opex_breakdown_total["variable_om"] - fopex_breakdown = opex_breakdown_total["fixed_om"] - - if isinstance(output_dir, str): - output_dir = Path(output_dir).resolve() - gen_inflation = h2integrate_config["finance_parameters"]["inflation_rate"] - - if "financial_analysis_start_year" not in h2integrate_config["finance_parameters"]: - financial_analysis_start_year = h2integrate_config["project_parameters"][ - "financial_analysis_start_year" - ] - else: - financial_analysis_start_year = h2integrate_config["finance_parameters"][ - "financial_analysis_start_year" - ] - - if "installation_time" not in h2integrate_config["project_parameters"]: - installation_period_months = wind_cost_results.installation_time - else: - installation_period_months = h2integrate_config["project_parameters"]["installation_time"] - - # initialize dictionary of weights for averaging financial parameters - finance_param_weights = {} - - if ( - design_scenario["h2_storage_location"] == "onshore" - or design_scenario["electrolyzer_location"] == "onshore" - ): - if "land_cost" in h2integrate_config["finance_parameters"]: - land_cost = h2integrate_config["finance_parameters"]["land_cost"] - else: - land_cost = 1e6 # TODO should model this - else: - land_cost = 0.0 - - pf = ProFAST.ProFAST() - pf.set_params( - "commodity", - { - "name": "Hydrogen", - "unit": "kg", - "initial price": 100, - "escalation": gen_inflation, - }, - ) - pf.set_params( - "capacity", - electrolyzer_performance_results.rated_capacity_kg_pr_day, - ) # kg/day - pf.set_params("maintenance", {"value": 0, "escalation": gen_inflation}) - pf.set_params( - "analysis start year", - financial_analysis_start_year, - ) - pf.set_params("operating life", h2integrate_config["project_parameters"]["project_lifetime"]) - pf.set_params( - "installation months", - installation_period_months, - ) - pf.set_params( - "installation cost", - { - "value": 0, - "depr type": "Straight line", - "depr period": 4, - "depreciable": False, - }, - ) - if land_cost > 0: - pf.set_params("non depr assets", land_cost) - pf.set_params( - "end of proj sale non depr assets", - land_cost - * (1 + gen_inflation) ** h2integrate_config["project_parameters"]["project_lifetime"], - ) - pf.set_params("demand rampup", 0) - pf.set_params("long term utilization", electrolyzer_performance_results.long_term_utilization) - pf.set_params("credit card fees", 0) - pf.set_params("sales tax", h2integrate_config["finance_parameters"]["sales_tax_rate"]) - pf.set_params("license and permit", {"value": 00, "escalation": gen_inflation}) - pf.set_params("rent", {"value": 0, "escalation": gen_inflation}) - # TODO how to handle property tax and insurance for fully offshore? - pf.set_params( - "property tax and insurance", - h2integrate_config["finance_parameters"]["property_tax"] - + h2integrate_config["finance_parameters"]["property_insurance"], - ) - pf.set_params( - "admin expense", - h2integrate_config["finance_parameters"]["administrative_expense_percent_of_sales"], - ) - pf.set_params( - "total income tax rate", - h2integrate_config["finance_parameters"]["total_income_tax_rate"], - ) - pf.set_params( - "capital gains tax rate", - h2integrate_config["finance_parameters"]["capital_gains_tax_rate"], - ) - pf.set_params("sell undepreciated cap", True) - pf.set_params("tax losses monetized", True) - pf.set_params("general inflation rate", gen_inflation) - - pf.set_params("debt type", h2integrate_config["finance_parameters"]["debt_type"]) - pf.set_params("loan period if used", h2integrate_config["finance_parameters"]["loan_period"]) - pf.set_params("cash onhand", h2integrate_config["finance_parameters"]["cash_onhand_months"]) - - # ----------------------------------- Add capital and fixed items to ProFAST ---------------- - if "wind" in capex_breakdown.keys(): - pf.add_capital_item( - name="Wind system", - cost=capex_breakdown["wind"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["wind"] = capex_breakdown["wind"] - - if "wave" in capex_breakdown.keys(): - pf.add_capital_item( - name="Wave system", - cost=capex_breakdown["wave"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["wave"] = capex_breakdown["wave"] - - if "solar" in capex_breakdown.keys(): - pf.add_capital_item( - name="Solar PV system", - cost=capex_breakdown["solar"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["solar"] = capex_breakdown["solar"] - - if "battery" in capex_breakdown.keys(): - pf.add_capital_item( - name="Battery system", - cost=capex_breakdown["battery"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["battery"] = capex_breakdown["battery"] - - if "platform" in capex_breakdown.keys() and capex_breakdown["platform"] > 0: - pf.add_capital_item( - name="Equipment platform", - cost=capex_breakdown["platform"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["platform"] = capex_breakdown["platform"] - - pf.add_fixed_cost( - name="Equipment platform O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["platform"], - escalation=gen_inflation, - ) - - pf.add_fixed_cost( - name="Wind and electrical export fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["wind_and_electrical"], - escalation=gen_inflation, - ) - if "wave" in fopex_breakdown.keys(): - pf.add_fixed_cost( - name="Wave O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["wave"], - escalation=gen_inflation, - ) - - if "solar" in fopex_breakdown.keys(): - pf.add_fixed_cost( - name="Solar O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["solar"], - escalation=gen_inflation, - ) - - if "battery" in fopex_breakdown.keys(): - pf.add_fixed_cost( - name="Battery O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["battery"], - escalation=gen_inflation, - ) - - if design_scenario["transportation"] == "hvdc+pipeline" or not ( - design_scenario["electrolyzer_location"] == "turbine" - and design_scenario["h2_storage_location"] == "turbine" - ): - pf.add_capital_item( - name="Electrical export system", - cost=capex_breakdown["electrical_export_system"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period"], - refurb=[0], - ) - finance_param_weights["electrical_export_system"] = capex_breakdown[ - "electrical_export_system" - ] - # TODO assess if this makes sense (electrical export O&M included in wind O&M) - - pf.add_capital_item( - name="Electrolysis system", - cost=capex_breakdown["electrolyzer"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"]["depreciation_period_electrolyzer"], - refurb=electrolyzer_performance_results.refurb_cost_percent, - ) - finance_param_weights["electrolyzer"] = capex_breakdown["electrolyzer"] - pf.add_fixed_cost( - name="Electrolysis system fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["electrolyzer"], - escalation=gen_inflation, - ) - - pf.add_feedstock( - name="Electrolyzer Variable O&M", - usage=1.0, - unit="$/kg", - cost=vopex_breakdown["electrolyzer"], - escalation=gen_inflation, - ) - - if design_scenario["electrolyzer_location"] == "turbine": - pf.add_capital_item( - name="H2 pipe array system", - cost=capex_breakdown["h2_pipe_array"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"][ - "depreciation_period_electrolyzer" - ], - refurb=[0], - ) - finance_param_weights["h2_pipe_array"] = capex_breakdown["h2_pipe_array"] - pf.add_fixed_cost( - name="H2 pipe array fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["h2_pipe_array"], - escalation=gen_inflation, - ) - - if ( - ( - design_scenario["h2_storage_location"] == "onshore" - and design_scenario["electrolyzer_location"] != "onshore" - ) - or ( - design_scenario["h2_storage_location"] != "onshore" - and design_scenario["electrolyzer_location"] == "onshore" - ) - or (design_scenario["transportation"] == "hvdc+pipeline") - ): - pf.add_capital_item( - name="H2 transport compressor system", - cost=capex_breakdown["h2_transport_compressor"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"][ - "depreciation_period_electrolyzer" - ], - refurb=[0], - ) - finance_param_weights["h2_transport_compressor"] = capex_breakdown[ - "h2_transport_compressor" - ] - pf.add_capital_item( - name="H2 transport pipeline system", - cost=capex_breakdown["h2_transport_pipeline"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"][ - "depreciation_period_electrolyzer" - ], - refurb=[0], - ) - finance_param_weights["h2_transport_pipeline"] = capex_breakdown["h2_transport_pipeline"] - - pf.add_fixed_cost( - name="H2 transport compression fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["h2_transport_compressor"], - escalation=gen_inflation, - ) - pf.add_fixed_cost( - name="H2 transport pipeline fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["h2_transport_pipeline"], - escalation=gen_inflation, - ) - - if h2integrate_config["h2_storage"]["type"] != "none": - pf.add_capital_item( - name="Hydrogen storage system", - cost=capex_breakdown["h2_storage"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"][ - "depreciation_period_electrolyzer" - ], - refurb=[0], - ) - finance_param_weights["h2_storage"] = capex_breakdown["h2_storage"] - pf.add_fixed_cost( - name="Hydrogen storage fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["h2_storage"], - escalation=gen_inflation, - ) - - # ---------------------- Add feedstocks, note the various cost options------------------- - if design_scenario["electrolyzer_location"] == "onshore": - pf.add_feedstock( - name="Water", - usage=electrolyzer_performance_results.water_usage_gal_pr_kg, - unit="gal", - cost="US Average", - escalation=gen_inflation, - ) - else: - pf.add_capital_item( - name="Desal system", - cost=capex_breakdown["desal"], - depr_type=h2integrate_config["finance_parameters"]["depreciation_method"], - depr_period=h2integrate_config["finance_parameters"][ - "depreciation_period_electrolyzer" - ], - refurb=[0], - ) - finance_param_weights["desal"] = capex_breakdown["desal"] - pf.add_fixed_cost( - name="Desal fixed O&M cost", - usage=1.0, - unit="$/year", - cost=fopex_breakdown["desal"], - escalation=gen_inflation, - ) - - if ( - h2integrate_config["project_parameters"]["grid_connection"] - or sum(total_accessory_power_grid_kw) > 0 - ): - energy_purchase = sum(total_accessory_power_grid_kw) # * 365 * 24 - - if h2integrate_config["project_parameters"]["grid_connection"]: - annual_energy_shortfall = np.sum(hopp_results["energy_shortfall_hopp"]) - energy_purchase += annual_energy_shortfall - - pf.add_fixed_cost( - name="Electricity from grid", - usage=1.0, - unit="$/year", - cost=energy_purchase * h2integrate_config["project_parameters"]["ppa_price"], - escalation=gen_inflation, - ) - - # ------------------------------------- add incentives ----------------------------------- - """ - Note: units must be given to ProFAST in terms of dollars per unit of the primary commodity being - produced - - Note: full tech-nutral (wind) tax credits are no longer available if constructions starts after - Jan. 1 2034 (Jan 1. 2033 for h2 ptc) - """ - - # catch incentive option and add relevant incentives - incentive_dict = h2integrate_config["policy_parameters"][f"option{incentive_option}"] - - # add wind_itc (% of wind capex) - electricity_itc_value_percent_wind_capex = incentive_dict["electricity_itc"] - electricity_itc_value_dollars = electricity_itc_value_percent_wind_capex * ( - capex_breakdown["wind"] + capex_breakdown["electrical_export_system"] - ) - pf.set_params( - "one time cap inct", - { - "value": electricity_itc_value_dollars, - "depr type": h2integrate_config["finance_parameters"]["depreciation_method"], - "depr period": h2integrate_config["finance_parameters"]["depreciation_period"], - "depreciable": True, - }, - ) - - # add h2_storage_itc (% of h2 storage capex) - itc_value_percent_h2_store_capex = incentive_dict["h2_storage_itc"] - electricity_itc_value_dollars_h2_store = ( - itc_value_percent_h2_store_capex * (capex_breakdown["h2_storage"]) - ) - pf.set_params( - "one time cap inct", - { - "value": electricity_itc_value_dollars_h2_store, - "depr type": h2integrate_config["finance_parameters"]["depreciation_method"], - "depr period": h2integrate_config["finance_parameters"]["depreciation_period"], - "depreciable": True, - }, - ) - - # add electricity_ptc ($/kW) - # adjust from 1992 dollars to start year - electricity_ptc_in_dollars_per_kw = -npf.fv( - h2integrate_config["finance_parameters"]["costing_general_inflation"], - h2integrate_config["project_parameters"]["financial_analysis_start_year"] - + round(wind_cost_results.installation_time / 12) - - 1992, - 0, - incentive_dict["electricity_ptc"], - ) # given in 1992 dollars but adjust for inflation - kw_per_kg_h2 = sum(hopp_results["combined_hybrid_power_production_hopp"]) / np.mean( - electrolyzer_performance_results.electrolyzer_annual_h2_production_kg - ) - electricity_ptc_in_dollars_per_kg_h2 = electricity_ptc_in_dollars_per_kw * kw_per_kg_h2 - pf.add_incentive( - name="Electricity PTC", - value=electricity_ptc_in_dollars_per_kg_h2, - decay=-gen_inflation, - sunset_years=10, - tax_credit=True, - ) # TODO check decay - - # add h2_ptc ($/kg) - h2_ptc_inflation_adjusted = -npf.fv( - h2integrate_config["finance_parameters"][ - "costing_general_inflation" - ], # use ATB year (cost inflation 2.5%) costing_general_inflation - h2integrate_config["project_parameters"]["financial_analysis_start_year"] - + round(wind_cost_results.installation_time / 12) - - 2022, - 0, - incentive_dict["h2_ptc"], - ) - pf.add_incentive( - name="H2 PTC", - value=h2_ptc_inflation_adjusted, - decay=-gen_inflation, # correct inflation - sunset_years=10, - tax_credit=True, - ) # TODO check decay - - # ----------------------- Add weight-averaged parameters ----------------------- - - equity_discount_rate = calc_financial_parameter_weighted_average_by_capex( - parameter_name="discount_rate", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "leverage after tax nominal discount rate", - equity_discount_rate, - ) - - debt_interest_rate = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_interest_rate", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt interest rate", - debt_interest_rate, - ) - - if "debt_equity_split" in h2integrate_config["finance_parameters"].keys(): - debt_equity_split = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_equity_split", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt equity ratio of initial financing", - (debt_equity_split / (100 - debt_equity_split)), - ) - elif "debt_equity_ratio" in h2integrate_config["finance_parameters"].keys(): - debt_equity_ratio = calc_financial_parameter_weighted_average_by_capex( - parameter_name="debt_equity_ratio", - h2integrate_config=h2integrate_config, - capex_breakdown=finance_param_weights, - ) - pf.set_params( - "debt equity ratio of initial financing", - debt_equity_ratio, - ) - else: - msg = ( - "missing value in `finance_parameters`. " - "Requires either `debt_equity_ratio` or `debt_equity_split`" - ) - raise ValueError(msg) - - # ------------------------------------ solve and post-process ----------------------------- - - sol = pf.solve_price() - - df = pf.cash_flow_out - - lcoh = sol["price"] - - if verbose: - print(f"\nProFAST LCOH: {lcoh:.2f} $/kg") - print(f'ProFAST NPV: {sol["NPV"]:.2f}') - print(f'ProFAST IRR: {max(sol["irr"]):.5f}') - print(f'ProFAST LCO: {sol["lco"]:.2f} $/kg') - print(f'ProFAST Profit Index: {sol["profit index"]:.2f}') - print(f'ProFAST payback period: {sol["investor payback period"]}') - - MIRR = npf.mirr( - df["Investor cash flow"], - debt_interest_rate, - equity_discount_rate, - ) # TODO probably ignore MIRR - NPV = npf.npv( - h2integrate_config["finance_parameters"]["inflation_rate"], - df["Investor cash flow"], - ) - ROI = np.sum(df["Investor cash flow"]) / abs( - np.sum(df["Investor cash flow"][df["Investor cash flow"] < 0]) - ) # ROI is not a good way of thinking about the value of the project - - # TODO project level IRR - capex and operating cash flow - - # note: hurdle rate typically 20% IRR before investing in it due to typically optimistic - # assumptions - - # note: negative retained earnings (keeping debt, paying down equity) - to get around it, - # do another line for retained earnings and watch dividends paid by the project - # (net income/equity should stay positive this way) - - print("Investor NPV: ", np.round(NPV * 1e-6, 2), "M USD") - print("Investor MIRR: ", np.round(MIRR, 5), "") - print("Investor ROI: ", np.round(ROI, 5), "") - - if save_plots or show_plots: - savepaths = [ - output_dir / "figures/capex", - output_dir / "figures/annual_cash_flow", - output_dir / "figures/lcoh_breakdown", - output_dir / "data", - ] - for savepath in savepaths: - if not savepath.exists(): - savepath.mkdir(parents=True) - - pf.plot_capital_expenses( - fileout=savepaths[0] / f"capital_expense_{design_scenario['id']}.pdf", - show_plot=show_plots, - ) - pf.plot_cashflow( - fileout=savepaths[1] / f"cash_flow_{design_scenario['id']}.png", - show_plot=show_plots, - ) - - pd.DataFrame.from_dict(data=pf.cash_flow_out).to_csv( - savepaths[3] / f"cash_flow_{design_scenario['id']}.csv" - ) - - pf.plot_costs( - savepaths[2] / f"lcoh_{design_scenario['id']}", - show_plot=show_plots, - ) - - return lcoh, pf, sol diff --git a/h2integrate/tools/eco/utilities.py b/h2integrate/tools/eco/utilities.py deleted file mode 100644 index c2f0b65df..000000000 --- a/h2integrate/tools/eco/utilities.py +++ /dev/null @@ -1,3679 +0,0 @@ -from __future__ import annotations - -import copy -import warnings -from pathlib import Path - -import numpy as np -import ORBIT as orbit -import pandas as pd -import numpy_financial as npf -import matplotlib.pyplot as plt -import matplotlib.ticker as ticker -import matplotlib.patches as patches -from hopp.simulation import HoppInterface -from hopp.tools.dispatch import plot_tools -from hopp.simulation.technologies.resource.greet_data import GREETData -from hopp.simulation.technologies.resource.cambium_data import CambiumData - -from h2integrate.core.utilities import load_yaml -from h2integrate.tools.h2integrate_sim_file_utils import load_dill_pickle - -from .finance import adjust_orbit_costs - - -""" -This function returns the ceiling of a/b (rounded to the nearest greater integer). -The function was copied from https://stackoverflow.com/a/17511341/5128616 -""" - - -def ceildiv(a, b): - return -(a // -b) - - -def convert_relative_to_absolute_path(config_filepath, resource_filepath): - if resource_filepath == "": - return "" - else: - abs_config_filepath = Path(config_filepath).absolute().parent - return abs_config_filepath / resource_filepath - - -# Function to load inputs -def get_inputs( - filename_hopp_config, - filename_h2integrate_config, - filename_orbit_config, - filename_turbine_config, - filename_floris_config=None, - verbose=False, - show_plots=False, - save_plots=False, -): - ############### load turbine inputs from yaml - - # load turbine inputs - turbine_config = load_yaml(filename_turbine_config) - - # load hopp inputs - hopp_config = load_yaml(filename_hopp_config) - - # load eco inputs - h2integrate_config = load_yaml(filename_h2integrate_config) - - # convert relative filepath to absolute for HOPP ingestion - hopp_config["site"]["solar_resource_file"] = convert_relative_to_absolute_path( - filename_hopp_config, hopp_config["site"]["solar_resource_file"] - ) - hopp_config["site"]["wind_resource_file"] = convert_relative_to_absolute_path( - filename_hopp_config, hopp_config["site"]["wind_resource_file"] - ) - hopp_config["site"]["wave_resource_file"] = convert_relative_to_absolute_path( - filename_hopp_config, hopp_config["site"]["wave_resource_file"] - ) - hopp_config["site"]["grid_resource_file"] = convert_relative_to_absolute_path( - filename_hopp_config, hopp_config["site"]["grid_resource_file"] - ) - - ################ load plant inputs from yaml - if filename_orbit_config is not None: - orbit_config = orbit.load_config(filename_orbit_config) - - # print plant inputs if desired - if verbose: - print("\nPlant configuration:") - for key in orbit_config.keys(): - print(key, ": ", orbit_config[key]) - - # check that orbit and hopp inputs are compatible - if ( - orbit_config["plant"]["capacity"] - != hopp_config["technologies"]["wind"]["num_turbines"] - * hopp_config["technologies"]["wind"]["turbine_rating_kw"] - * 1e-3 - ): - raise (ValueError("Provided ORBIT and HOPP wind plant capacities do not match")) - - # update floris_config file with correct input from other files - # load floris inputs - if ( - hopp_config["technologies"]["wind"]["model_name"] == "floris" - ): # TODO replace elements of the file - if filename_floris_config is None: - raise (ValueError("floris input file must be specified.")) - else: - floris_config = load_yaml(filename_floris_config) - floris_config.update({"farm": {"turbine_type": turbine_config}}) - else: - floris_config = None - - # print turbine inputs if desired - if verbose: - print("\nTurbine configuration:") - for key in turbine_config.keys(): - print(key, ": ", turbine_config[key]) - - ############## provide custom layout for ORBIT and FLORIS if desired - if filename_orbit_config is not None: - if orbit_config["plant"]["layout"] == "custom": - # generate ORBIT config from floris layout - for i, x in enumerate(floris_config["farm"]["layout_x"]): - floris_config["farm"]["layout_x"][i] = x + 400 - - layout_config, layout_data_location = convert_layout_from_floris_for_orbit( - floris_config["farm"]["layout_x"], - floris_config["farm"]["layout_y"], - save_config=True, - ) - - # update orbit_config with custom layout - # orbit_config = orbit.core.library.extract_library_data( - # orbit_config, additional_keys=layout_config - # ) - orbit_config["array_system_design"]["location_data"] = layout_data_location - - # if hybrid plant, adjust hybrid plant capacity to include all technologies - total_hybrid_plant_capacity_mw = 0.0 - for tech in hopp_config["technologies"].keys(): - if tech == "grid": - continue - elif tech == "wind": - total_hybrid_plant_capacity_mw += ( - hopp_config["technologies"][tech]["num_turbines"] - * hopp_config["technologies"][tech]["turbine_rating_kw"] - * 1e-3 - ) - elif tech == "pv": - total_hybrid_plant_capacity_mw += ( - hopp_config["technologies"][tech]["system_capacity_kw"] * 1e-3 - ) - elif tech == "wave": - total_hybrid_plant_capacity_mw += ( - hopp_config["technologies"][tech]["num_devices"] - * hopp_config["technologies"][tech]["device_rating_kw"] - * 1e-3 - ) - - # initialize dict for hybrid plant - if filename_orbit_config is not None: - if total_hybrid_plant_capacity_mw != orbit_config["plant"]["capacity"]: - orbit_hybrid_electrical_export_config = copy.deepcopy(orbit_config) - orbit_hybrid_electrical_export_config["plant"]["capacity"] = ( - total_hybrid_plant_capacity_mw - ) - # allow orbit to set num_turbines later based on the new hybrid capacity and - # turbinerating - orbit_hybrid_electrical_export_config["plant"].pop("num_turbines") - else: - orbit_hybrid_electrical_export_config = {} - - if verbose: - print(f"Total hybrid plant rating calculated: {total_hybrid_plant_capacity_mw} MW") - - if filename_orbit_config is None: - orbit_config = None - orbit_hybrid_electrical_export_config = {} - - ############## return all inputs - - return ( - hopp_config, - h2integrate_config, - orbit_config, - turbine_config, - floris_config, - orbit_hybrid_electrical_export_config, - ) - - -def convert_layout_from_floris_for_orbit(turbine_x, turbine_y, save_config=False): - turbine_x_km = (np.array(turbine_x) * 1e-3).tolist() - turbine_y_km = (np.array(turbine_y) * 1e-3).tolist() - - # initialize dict with data for turbines - turbine_dict = { - "id": list(range(0, len(turbine_x))), - "substation_id": ["OSS"] * len(turbine_x), - "name": list(range(0, len(turbine_x))), - "longitude": turbine_x_km, - "latitude": turbine_y_km, - "string": [0] * len(turbine_x), # can be left empty - "order": [0] * len(turbine_x), # can be left empty - "cable_length": [0] * len(turbine_x), - "bury_speed": [0] * len(turbine_x), - } - string_counter = -1 - order_counter = 0 - for i in range(0, len(turbine_x)): - if turbine_x[i] - 400 == 0: - string_counter += 1 - order_counter = 0 - - turbine_dict["order"][i] = order_counter - turbine_dict["string"][i] = string_counter - - order_counter += 1 - - # initialize dict with substation information - substation_dict = { - "id": "OSS", - "substation_id": "OSS", - "name": "OSS", - "longitude": np.min(turbine_x_km) - 200 * 1e-3, - "latitude": np.average(turbine_y_km), - "string": "", # can be left empty - "order": "", # can be left empty - "cable_length": "", - "bury_speed": "", - } - - # combine turbine and substation dicts - for key in turbine_dict.keys(): - # turbine_dict[key].append(substation_dict[key]) - turbine_dict[key].insert(0, substation_dict[key]) - - # add location data - file_name = "osw_cable_layout" - save_location = Path("./input/project/plant/").resolve() - # turbine_dict["array_system_design"]["location_data"] = data_location - if save_config: - if not save_location.exists(): - save_location.mkdir(parents=True) - # create pandas data frame - df = pd.DataFrame.from_dict(turbine_dict) - - # df.drop("index") - df.set_index("id") - - # save to csv - df.to_csv(save_location / f"{file_name}.csv", index=False) - - return turbine_dict, file_name - - -def visualize_plant( - hopp_config, - h2integrate_config, - turbine_config, - wind_cost_outputs, - hopp_results, - platform_results, - desal_results, - h2_storage_results, - electrolyzer_physics_results, - design_scenario, - colors, - plant_design_number, - show_plots=False, - save_plots=False, - output_dir="./output/", -): - if isinstance(output_dir, str): - output_dir = Path(output_dir).resolve() - # save plant sizing to dict - component_areas = {} - - plt.rcParams.update({"font.size": 7}) - - if hopp_config["technologies"]["wind"]["model_name"] != "floris": - msg = ( - f"`visualize_plant()` only works with the 'floris' wind model, `model_name`" - f" {hopp_config['technologies']['wind']['model_name']} has been specified" - ) - raise NotImplementedError(msg) - - # set colors - turbine_rotor_color = colors[0] - turbine_tower_color = colors[1] - pipe_color = colors[2] - cable_color = colors[8] - electrolyzer_color = colors[4] - desal_color = colors[9] - h2_storage_color = colors[6] - substation_color = colors[7] - equipment_platform_color = colors[1] - compressor_color = colors[0] - if hopp_config["site"]["solar"]: - solar_color = colors[2] - if hopp_config["site"]["wave"]: - wave_color = colors[8] - battery_color = colors[8] - - # set hatches - solar_hatch = "//" - wave_hatch = "\\\\" - battery_hatch = "+" - electrolyzer_hatch = "///" - desalinator_hatch = "xxxx" - - # Views - # offshore plant, onshore plant, offshore platform, offshore turbine - - # get plant location - - # get shore location - - # get cable/pipe locations - if design_scenario["wind_location"] == "offshore": - # ORBIT gives coordinates in km, convert to m for (val / 1e3) - - cable_array_points = ( - wind_cost_outputs.orbit_project.phases["ArraySystemDesign"].coordinates * 1e3 - ) - pipe_array_points = ( - wind_cost_outputs.orbit_project.phases["ArraySystemDesign"].coordinates * 1e3 - ) - - # get turbine tower base diameter - tower_base_diameter = wind_cost_outputs.orbit_project.config["turbine"]["tower"][ - "section_diameters" - ][0] # in m - tower_base_radius = tower_base_diameter / 2.0 - - # get turbine locations - turbine_x = ( - wind_cost_outputs.orbit_project.phases["ArraySystemDesign"].turbines_x.flatten() * 1e3 - ) - turbine_x = turbine_x[~np.isnan(turbine_x)] - turbine_y = ( - wind_cost_outputs.orbit_project.phases["ArraySystemDesign"].turbines_y.flatten() * 1e3 - ) - turbine_y = turbine_y[~np.isnan(turbine_y)] - - # get offshore substation location and dimensions (treated as center) - substation_x = wind_cost_outputs.orbit_project.phases["ArraySystemDesign"].oss_x * 1e3 - substation_y = wind_cost_outputs.orbit_project.phases["ArraySystemDesign"].oss_y * 1e3 - - # [m] just based on a large substation - # (https://www.windpowerengineering.com/making-modern-offshore-substation/) - # since the dimensions are not available in ORBIT - substation_side_length = 20 - - # get equipment platform location and dimensions - equipment_platform_area = platform_results["toparea_m2"] - equipment_platform_side_length = np.sqrt(equipment_platform_area) - - # [m] (treated as center) - equipment_platform_x = ( - substation_x - substation_side_length - equipment_platform_side_length / 2 - ) - equipment_platform_y = substation_y - - # get platform equipment dimensions - if design_scenario["electrolyzer_location"] == "turbine": - # equipment_footprint_m2 - desal_equipment_area = desal_results["per_turb_equipment_footprint_m2"] - elif design_scenario["electrolyzer_location"] == "platform": - desal_equipment_area = desal_results["equipment_footprint_m2"] - else: - desal_equipment_area = 0 - - desal_equipment_side = np.sqrt(desal_equipment_area) - - # get pipe points - np.array([substation_x - 1000, substation_x]) - np.array([substation_y, substation_y]) - - # get cable points - - else: - turbine_x = np.array( - hopp_config["technologies"]["wind"]["floris_config"]["farm"]["layout_x"] - ) - turbine_y = np.array( - hopp_config["technologies"]["wind"]["floris_config"]["farm"]["layout_y"] - ) - cable_array_points = [] - - # wind farm area - turbine_length_x = np.max(turbine_x) - np.min(turbine_x) - turbine_length_y = np.max(turbine_y) - np.min(turbine_y) - turbine_area = turbine_length_x * turbine_length_y - - # compressor side # not sized - compressor_area = 25 - compressor_side = np.sqrt(compressor_area) - - # get turbine rotor diameter - rotor_diameter = turbine_config["rotor_diameter"] # in m - rotor_radius = rotor_diameter / 2.0 - - # set onshore substation dimensions - onshore_substation_x_side_length = 127.25 # [m] based on 1 acre area https://www.power-technology.com/features/making-space-for-power-how-much-land-must-renewables-use/ - onshore_substation_y_side_length = 31.8 # [m] based on 1 acre area https://www.power-technology.com/features/making-space-for-power-how-much-land-must-renewables-use/ - onshore_substation_area = onshore_substation_x_side_length * onshore_substation_y_side_length - - if h2integrate_config["h2_storage"]["type"] == "pressure_vessel": - h2_storage_area = h2_storage_results["tank_footprint_m2"] - h2_storage_side = np.sqrt(h2_storage_area) - else: - h2_storage_side = 0 - h2_storage_area = 0 - - electrolyzer_area = electrolyzer_physics_results["equipment_footprint_m2"] - if design_scenario["electrolyzer_location"] == "turbine": - electrolyzer_area /= hopp_config["technologies"]["wind"]["num_turbines"] - - electrolyzer_side = np.sqrt(electrolyzer_area) - - # set onshore origin - onshorex = 50 - onshorey = 50 - - wind_buffer = np.min(turbine_x) - (onshorey + 3 * rotor_diameter + electrolyzer_side) - if "pv" in hopp_config["technologies"].keys(): - wind_buffer -= np.sqrt(hopp_results["hybrid_plant"].pv.footprint_area) - if "battery" in hopp_config["technologies"].keys(): - wind_buffer -= np.sqrt(hopp_results["hybrid_plant"].battery.footprint_area) - if wind_buffer < 50: - onshorey += wind_buffer - 50 - - if design_scenario["wind_location"] == "offshore": - origin_x = substation_x - origin_y = substation_y - else: - origin_x = 0.0 - origin_y = 0.0 - - ## create figure - if design_scenario["wind_location"] == "offshore": - fig, ax = plt.subplots(2, 2, figsize=(10, 6)) - ax_index_plant = (0, 0) - ax_index_detail = (1, 0) - ax_index_wind_plant = (0, 1) - ax_index_turbine_detail = (1, 1) - else: - fig, ax = plt.subplots(1, 2, figsize=(10, 6)) - ax_index_plant = 0 - ax_index_wind_plant = 0 - ax_index_detail = 1 - ax_index_turbine_detail = False - - # plot the stuff - - # onshore plant | offshore plant - # platform/substation | turbine - - ## add turbines - def add_turbines(ax, turbine_x, turbine_y, radius, color): - i = 0 - for x, y in zip(turbine_x, turbine_y): - if i == 0: - rlabel = "Wind turbine rotor" - i += 1 - else: - rlabel = None - turbine_patch = patches.Circle( - (x, y), - radius=radius, - color=color, - fill=False, - label=rlabel, - zorder=10, - ) - ax.add_patch(turbine_patch) - - add_turbines(ax[ax_index_wind_plant], turbine_x, turbine_y, rotor_radius, turbine_rotor_color) - component_areas["turbine_area_m2"] = turbine_area - # turbine_patch01_tower = patches.Circle((x, y), radius=tower_base_radius, color=turbine_tower_color, fill=False, label=tlabel, zorder=10) # noqa: E501 - # ax[0, 1].add_patch(turbine_patch01_tower) - if design_scenario["wind_location"] == "onshore": - add_turbines(ax[ax_index_detail], turbine_x, turbine_y, rotor_radius, turbine_rotor_color) - - if ax_index_turbine_detail: - # turbine_patch11_rotor = patches.Circle((turbine_x[0], turbine_y[0]), radius=rotor_radius, color=turbine_rotor_color, fill=False, label=None, zorder=10) # noqa: E501 - tlabel = "Wind turbine tower" - turbine_patch11_tower = patches.Circle( - (turbine_x[0], turbine_y[0]), - radius=tower_base_radius, - color=turbine_tower_color, - fill=False, - label=tlabel, - zorder=10, - ) - # ax[1, 1].add_patch(turbine_patch11_rotor) - ax[ax_index_turbine_detail].add_patch(turbine_patch11_tower) - - # add pipe array - if design_scenario["transportation"] == "hvdc+pipeline" or ( - design_scenario["h2_storage_location"] != "turbine" - and design_scenario["electrolyzer_location"] == "turbine" - ): - i = 0 - for point_string in pipe_array_points: - if i == 0: - label = "Array pipes" - i += 1 - else: - label = None - ax[0, 1].plot( - point_string[:, 0], - point_string[:, 1] - substation_side_length / 2, - ":", - color=pipe_color, - zorder=0, - linewidth=1, - label=label, - ) - ax[1, 0].plot( - point_string[:, 0], - point_string[:, 1] - substation_side_length / 2, - ":", - color=pipe_color, - zorder=0, - linewidth=1, - label=label, - ) - ax[1, 1].plot( - point_string[:, 0], - point_string[:, 1] - substation_side_length / 2, - ":", - color=pipe_color, - zorder=0, - linewidth=1, - label=label, - ) - - ## add cables - if (len(cable_array_points) > 1) and ( - design_scenario["h2_storage_location"] != "turbine" - or design_scenario["transportation"] == "hvdc+pipeline" - ): - i = 0 - for point_string in cable_array_points: - if i == 0: - label = "Array cables" - i += 1 - else: - label = None - ax[0, 1].plot( - point_string[:, 0], - point_string[:, 1] + substation_side_length / 2, - "-", - color=cable_color, - zorder=0, - linewidth=1, - label=label, - ) - ax[1, 0].plot( - point_string[:, 0], - point_string[:, 1] + substation_side_length / 2, - "-", - color=cable_color, - zorder=0, - linewidth=1, - label=label, - ) - ax[1, 1].plot( - point_string[:, 0], - point_string[:, 1] + substation_side_length / 2, - "-", - color=cable_color, - zorder=0, - linewidth=1, - label=label, - ) - - ## add offshore substation - if design_scenario["wind_location"] == "offshore" and ( - design_scenario["h2_storage_location"] != "turbine" - or design_scenario["transportation"] == "hvdc+pipeline" - ): - substation_patch01 = patches.Rectangle( - ( - substation_x - substation_side_length, - substation_y - substation_side_length / 2, - ), - substation_side_length, - substation_side_length, - fill=True, - color=substation_color, - label="Substation*", - zorder=11, - ) - substation_patch10 = patches.Rectangle( - ( - substation_x - substation_side_length, - substation_y - substation_side_length / 2, - ), - substation_side_length, - substation_side_length, - fill=True, - color=substation_color, - label="Substation*", - zorder=11, - ) - ax[0, 1].add_patch(substation_patch01) - ax[1, 0].add_patch(substation_patch10) - - component_areas["offshore_substation_area_m2"] = substation_side_length**2 - - ## add equipment platform - if design_scenario["wind_location"] == "offshore" and ( - design_scenario["h2_storage_location"] == "platform" - or design_scenario["electrolyzer_location"] == "platform" - ): # or design_scenario["transportation"] == "pipeline": - equipment_platform_patch01 = patches.Rectangle( - ( - equipment_platform_x - equipment_platform_side_length / 2, - equipment_platform_y - equipment_platform_side_length / 2, - ), - equipment_platform_side_length, - equipment_platform_side_length, - color=equipment_platform_color, - fill=True, - label="Equipment platform", - zorder=1, - ) - equipment_platform_patch10 = patches.Rectangle( - ( - equipment_platform_x - equipment_platform_side_length / 2, - equipment_platform_y - equipment_platform_side_length / 2, - ), - equipment_platform_side_length, - equipment_platform_side_length, - color=equipment_platform_color, - fill=True, - label="Equipment platform", - zorder=1, - ) - ax[0, 1].add_patch(equipment_platform_patch01) - ax[1, 0].add_patch(equipment_platform_patch10) - - component_areas["equipment_platform_area_m2"] = equipment_platform_area - - ## add hvdc cable - if ( - design_scenario["transportation"] == "hvdc" - or design_scenario["transportation"] == "hvdc+pipeline" - ): - ax[0, 0].plot( - [onshorex + onshore_substation_x_side_length, 10000], - [ - onshorey - onshore_substation_y_side_length, - onshorey - onshore_substation_y_side_length, - ], - "--", - color=cable_color, - label="HVDC cable", - ) - ax[0, 1].plot( - [-50000, substation_x], - [substation_y - 100, substation_y - 100], - "--", - color=cable_color, - label="HVDC cable", - zorder=0, - ) - ax[1, 0].plot( - [-5000, substation_x], - [substation_y - 2, substation_y - 2], - "--", - color=cable_color, - label="HVDC cable", - zorder=0, - ) - - ## add onshore substation - if ( - design_scenario["transportation"] == "hvdc" - or design_scenario["transportation"] == "hvdc+pipeline" - ): - onshore_substation_patch00 = patches.Rectangle( - ( - onshorex + 0.2 * onshore_substation_y_side_length, - onshorey - onshore_substation_y_side_length * 1.2, - ), - onshore_substation_x_side_length, - onshore_substation_y_side_length, - fill=True, - color=substation_color, - label="Substation*", - zorder=11, - ) - ax[0, 0].add_patch(onshore_substation_patch00) - - component_areas["onshore_substation_area_m2"] = onshore_substation_area - - ## add transport pipeline - if design_scenario["transportation"] == "colocated": - # add hydrogen pipeline to end use - linetype = "-." - label = "Pipeline to storage/end-use" - linewidth = 1.0 - - ax[ax_index_plant].plot( - [onshorex, -10000], - [onshorey, onshorey], - linetype, - color=pipe_color, - label=label, - linewidth=linewidth, - zorder=0, - ) - - ax[ax_index_detail].plot( - [onshorex, -10000], - [onshorey, onshorey], - linetype, - color=pipe_color, - label=label, - linewidth=linewidth, - zorder=0, - ) - if ( - design_scenario["transportation"] == "pipeline" - or design_scenario["transportation"] == "hvdc+pipeline" - or ( - design_scenario["transportation"] == "hvdc" - and design_scenario["h2_storage_location"] == "platform" - ) - ): - linetype = "-." - label = "Transport pipeline" - linewidth = 1.0 - - ax[ax_index_plant].plot( - [onshorex, 1000], - [onshorey + 2, onshorey + 2], - linetype, - color=pipe_color, - label=label, - linewidth=linewidth, - zorder=0, - ) - - if design_scenario["wind_location"] == "offshore": - ax[ax_index_wind_plant].plot( - [-5000, substation_x], - [substation_y + 100, substation_y + 100], - linetype, - linewidth=linewidth, - color=pipe_color, - label=label, - zorder=0, - ) - ax[ax_index_detail].plot( - [-5000, substation_x], - [substation_y + 2, substation_y + 2], - linetype, - linewidth=linewidth, - color=pipe_color, - label=label, - zorder=0, - ) - - if ( - design_scenario["transportation"] == "hvdc" - or design_scenario["transportation"] == "hvdc+pipeline" - ) and design_scenario["h2_storage_location"] == "platform": - h2cx = onshorex - compressor_side - h2cy = onshorey - compressor_side + 2 - h2cax = ax[ax_index_plant] - else: - h2cx = substation_x - substation_side_length - h2cy = substation_y - h2cax = ax[ax_index_detail] - - if design_scenario["wind_location"] == "onshore": - compressor_patch01 = patches.Rectangle( - (origin_x, origin_y), - compressor_side, - compressor_side, - color=compressor_color, - fill=None, - label="Transport compressor*", - hatch="+++", - zorder=20, - ) - ax[ax_index_plant].add_patch(compressor_patch01) - - compressor_patch10 = patches.Rectangle( - (h2cx, h2cy), - compressor_side, - compressor_side, - color=compressor_color, - fill=None, - label="Transport compressor*", - hatch="+++", - zorder=20, - ) - h2cax.add_patch(compressor_patch10) - - component_areas["compressor_area_m2"] = compressor_area - - ## add plant components - if design_scenario["electrolyzer_location"] == "onshore": - electrolyzer_x = onshorex - electrolyzer_y = onshorey - electrolyzer_patch = patches.Rectangle( - (electrolyzer_x, electrolyzer_y), - electrolyzer_side, - electrolyzer_side, - color=electrolyzer_color, - fill=None, - label="H$_2$ Electrolyzer", - zorder=20, - hatch=electrolyzer_hatch, - ) - ax[ax_index_plant].add_patch(electrolyzer_patch) - component_areas["electrolyzer_area_m2"] = electrolyzer_area - - if design_scenario["wind_location"] == "onshore": - electrolyzer_patch = patches.Rectangle( - (onshorex - h2_storage_side, onshorey + 4), - electrolyzer_side, - electrolyzer_side, - color=electrolyzer_color, - fill=None, - label="H$_2$ Electrolyzer", - zorder=20, - hatch=electrolyzer_hatch, - ) - ax[ax_index_detail].add_patch(electrolyzer_patch) - - elif design_scenario["electrolyzer_location"] == "platform": - dx = equipment_platform_x - equipment_platform_side_length / 2 - dy = equipment_platform_y - equipment_platform_side_length / 2 - e_side_y = equipment_platform_side_length - e_side_x = electrolyzer_area / e_side_y - d_side_y = equipment_platform_side_length - d_side_x = desal_equipment_area / d_side_y - electrolyzer_x = dx + d_side_x - electrolyzer_y = dy - - electrolyzer_patch = patches.Rectangle( - (electrolyzer_x, electrolyzer_y), - e_side_x, - e_side_y, - color=electrolyzer_color, - fill=None, - zorder=20, - label="H$_2$ Electrolyzer", - hatch=electrolyzer_hatch, - ) - ax[ax_index_detail].add_patch(electrolyzer_patch) - desal_patch = patches.Rectangle( - (dx, dy), - d_side_x, - d_side_y, - color=desal_color, - zorder=21, - fill=None, - label="Desalinator", - hatch=desalinator_hatch, - ) - ax[ax_index_detail].add_patch(desal_patch) - component_areas["desalination_area_m2"] = desal_equipment_area - - elif design_scenario["electrolyzer_location"] == "turbine": - electrolyzer_patch11 = patches.Rectangle( - (turbine_x[0], turbine_y[0] + tower_base_radius), - electrolyzer_side, - electrolyzer_side, - color=electrolyzer_color, - fill=None, - zorder=20, - label="H$_2$ Electrolyzer", - hatch=electrolyzer_hatch, - ) - ax[ax_index_turbine_detail].add_patch(electrolyzer_patch11) - desal_patch11 = patches.Rectangle( - (turbine_x[0] - desal_equipment_side, turbine_y[0] + tower_base_radius), - desal_equipment_side, - desal_equipment_side, - color=desal_color, - zorder=21, - fill=None, - label="Desalinator", - hatch=desalinator_hatch, - ) - ax[ax_index_turbine_detail].add_patch(desal_patch11) - component_areas["desalination_area_m2"] = desal_equipment_area - i = 0 - for x, y in zip(turbine_x, turbine_y): - if i == 0: - elabel = "H$_2$ Electrolyzer" - dlabel = "Desalinator" - else: - elabel = None - dlabel = None - electrolyzer_patch01 = patches.Rectangle( - (x, y + tower_base_radius), - electrolyzer_side, - electrolyzer_side, - color=electrolyzer_color, - fill=None, - zorder=20, - label=elabel, - hatch=electrolyzer_hatch, - ) - desal_patch01 = patches.Rectangle( - (x - desal_equipment_side, y + tower_base_radius), - desal_equipment_side, - desal_equipment_side, - color=desal_color, - zorder=21, - fill=None, - label=dlabel, - hatch=desalinator_hatch, - ) - ax[ax_index_wind_plant].add_patch(electrolyzer_patch01) - ax[ax_index_wind_plant].add_patch(desal_patch01) - i += 1 - - h2_storage_hatch = "\\\\\\" - if design_scenario["h2_storage_location"] == "onshore" and ( - h2integrate_config["h2_storage"]["type"] != "none" - ): - h2_storage_patch = patches.Rectangle( - (onshorex - h2_storage_side, onshorey - h2_storage_side - 2), - h2_storage_side, - h2_storage_side, - color=h2_storage_color, - fill=None, - label="H$_2$ storage", - hatch=h2_storage_hatch, - ) - ax[ax_index_plant].add_patch(h2_storage_patch) - component_areas["h2_storage_area_m2"] = h2_storage_area - - if design_scenario["wind_location"] == "onshore": - h2_storage_patch = patches.Rectangle( - (onshorex - h2_storage_side, onshorey - h2_storage_side - 2), - h2_storage_side, - h2_storage_side, - color=h2_storage_color, - fill=None, - label="H$_2$ storage", - hatch=h2_storage_hatch, - ) - ax[ax_index_detail].add_patch(h2_storage_patch) - component_areas["h2_storage_area_m2"] = h2_storage_area - elif design_scenario["h2_storage_location"] == "platform" and ( - h2integrate_config["h2_storage"]["type"] != "none" - ): - s_side_y = equipment_platform_side_length - s_side_x = h2_storage_area / s_side_y - sx = equipment_platform_x - equipment_platform_side_length / 2 - sy = equipment_platform_y - equipment_platform_side_length / 2 - if design_scenario["electrolyzer_location"] == "platform": - sx += equipment_platform_side_length - s_side_x - - h2_storage_patch = patches.Rectangle( - (sx, sy), - s_side_x, - s_side_y, - color=h2_storage_color, - fill=None, - label="H$_2$ storage", - hatch=h2_storage_hatch, - ) - ax[ax_index_detail].add_patch(h2_storage_patch) - component_areas["h2_storage_area_m2"] = h2_storage_area - - elif design_scenario["h2_storage_location"] == "turbine": - if h2integrate_config["h2_storage"]["type"] == "turbine": - h2_storage_patch = patches.Circle( - (turbine_x[0], turbine_y[0]), - radius=tower_base_diameter / 2, - color=h2_storage_color, - fill=None, - label="H$_2$ storage", - hatch=h2_storage_hatch, - ) - ax[ax_index_turbine_detail].add_patch(h2_storage_patch) - component_areas["h2_storage_area_m2"] = h2_storage_area - i = 0 - for x, y in zip(turbine_x, turbine_y): - if i == 0: - slabel = "H$_2$ storage" - else: - slabel = None - h2_storage_patch = patches.Circle( - (x, y), - radius=tower_base_diameter / 2, - color=h2_storage_color, - fill=None, - label=None, - hatch=h2_storage_hatch, - ) - ax[ax_index_wind_plant].add_patch(h2_storage_patch) - elif h2integrate_config["h2_storage"]["type"] == "pressure_vessel": - h2_storage_side = np.sqrt(h2_storage_area / h2integrate_config["plant"]["num_turbines"]) - h2_storage_patch = patches.Rectangle( - ( - turbine_x[0] - h2_storage_side - desal_equipment_side, - turbine_y[0] + tower_base_radius, - ), - width=h2_storage_side, - height=h2_storage_side, - color=h2_storage_color, - fill=None, - label="H$_2$ storage", - hatch=h2_storage_hatch, - ) - ax[ax_index_turbine_detail].add_patch(h2_storage_patch) - component_areas["h2_storage_area_m2"] = h2_storage_area - for i in range(zip(turbine_x, turbine_y)): - if i == 0: - slabel = "H$_2$ storage" - else: - slabel = None - h2_storage_patch = patches.Rectangle( - ( - turbine_x[i] - h2_storage_side - desal_equipment_side, - turbine_y[i] + tower_base_radius, - ), - width=h2_storage_side, - height=h2_storage_side, - color=h2_storage_color, - fill=None, - label=slabel, - hatch=h2_storage_hatch, - ) - ax[ax_index_wind_plant].add_patch(h2_storage_patch) - - ## add battery - if "battery" in hopp_config["technologies"].keys(): - component_areas["battery_area_m2"] = hopp_results["hybrid_plant"].battery.footprint_area - if design_scenario["battery_location"] == "onshore": - battery_side_y = np.sqrt(hopp_results["hybrid_plant"].battery.footprint_area) - battery_side_x = battery_side_y - - batteryx = electrolyzer_x - - batteryy = electrolyzer_y + electrolyzer_side + 10 - - battery_patch = patches.Rectangle( - (batteryx, batteryy), - battery_side_x, - battery_side_y, - color=battery_color, - fill=None, - label="Battery array", - hatch=battery_hatch, - ) - ax[ax_index_plant].add_patch(battery_patch) - - if design_scenario["wind_location"] == "onshore": - battery_patch = patches.Rectangle( - (batteryx, batteryy), - battery_side_x, - battery_side_y, - color=battery_color, - fill=None, - label="Battery array", - hatch=battery_hatch, - ) - ax[ax_index_detail].add_patch(battery_patch) - - elif design_scenario["battery_location"] == "platform": - battery_side_y = equipment_platform_side_length - battery_side_x = hopp_results["hybrid_plant"].battery.footprint_area / battery_side_y - - batteryx = equipment_platform_x - equipment_platform_side_length / 2 - batteryy = equipment_platform_y - equipment_platform_side_length / 2 - - battery_patch = patches.Rectangle( - (batteryx, batteryy), - battery_side_x, - battery_side_y, - color=battery_color, - fill=None, - label="Battery array", - hatch=battery_hatch, - ) - ax[ax_index_detail].add_patch(battery_patch) - - else: - battery_side_y = 0.0 - battery_side_x = 0.0 - - ## add solar - if hopp_config["site"]["solar"]: - component_areas["pv_area_m2"] = hopp_results["hybrid_plant"].pv.footprint_area - if design_scenario["pv_location"] == "offshore": - solar_side_y = equipment_platform_side_length - solar_side_x = hopp_results["hybrid_plant"].pv.footprint_area / solar_side_y - - solarx = equipment_platform_x - equipment_platform_side_length / 2 - solary = equipment_platform_y - equipment_platform_side_length / 2 - - solar_patch = patches.Rectangle( - (solarx, solary), - solar_side_x, - solar_side_y, - color=solar_color, - fill=None, - label="Solar array", - hatch=solar_hatch, - ) - ax[ax_index_detail].add_patch(solar_patch) - else: - solar_side_y = np.sqrt(hopp_results["hybrid_plant"].pv.footprint_area) - solar_side_x = hopp_results["hybrid_plant"].pv.footprint_area / solar_side_y - - solarx = electrolyzer_x - - solary = electrolyzer_y + electrolyzer_side + 10 - - if "battery" in hopp_config["technologies"].keys(): - solary += battery_side_y + 10 - - solar_patch = patches.Rectangle( - (solarx, solary), - solar_side_x, - solar_side_y, - color=solar_color, - fill=None, - label="Solar array", - hatch=solar_hatch, - ) - - ax[ax_index_plant].add_patch(solar_patch) - - if design_scenario["wind_location"] != "offshore": - solar_patch = patches.Rectangle( - (solarx, solary), - solar_side_x, - solar_side_y, - color=solar_color, - fill=None, - label="Solar array", - hatch=solar_hatch, - ) - - ax[ax_index_detail].add_patch(solar_patch) - - else: - solar_side_x = 0.0 - solar_side_y = 0.0 - - ## add wave - if hopp_config["site"]["wave"]: - # get wave generation area geometry - num_devices = hopp_config["technologies"]["wave"]["num_devices"] - distance_to_shore = ( - hopp_config["technologies"]["wave"]["cost_inputs"]["distance_to_shore"] * 1e3 - ) - number_rows = hopp_config["technologies"]["wave"]["cost_inputs"]["number_rows"] - device_spacing = hopp_config["technologies"]["wave"]["cost_inputs"]["device_spacing"] - row_spacing = hopp_config["technologies"]["wave"]["cost_inputs"]["row_spacing"] - - # calculate wave generation area dimenstions - wave_side_y = device_spacing * np.ceil(num_devices / number_rows) - wave_side_x = row_spacing * (number_rows) - wave_area = wave_side_x * wave_side_y - component_areas["wave_area_m2"] = wave_area - - # generate wave generation patch - wavex = substation_x - wave_side_x - wavey = substation_y + distance_to_shore - wave_patch = patches.Rectangle( - (wavex, wavey), - wave_side_x, - wave_side_y, - color=wave_color, - fill=None, - label="Wave array", - hatch=wave_hatch, - zorder=1, - ) - ax[ax_index_wind_plant].add_patch(wave_patch) - - # add electrical transmission for wave - wave_export_cable_coords_x = [substation_x, substation_x] - wave_export_cable_coords_y = [substation_y, substation_y + distance_to_shore] - - ax[ax_index_wind_plant].plot( - wave_export_cable_coords_x, - wave_export_cable_coords_y, - cable_color, - zorder=0, - ) - ax[ax_index_detail].plot( - wave_export_cable_coords_x, - wave_export_cable_coords_y, - cable_color, - zorder=0, - ) - - if design_scenario["wind_location"] == "offshore": - allpoints = cable_array_points.flatten() - else: - allpoints = turbine_x - - allpoints = allpoints[~np.isnan(allpoints)] - - if design_scenario["wind_location"] == "offshore": - roundto = -2 - ax[ax_index_plant].set( - xlim=[ - round(np.min(onshorex - 100), ndigits=roundto), - round( - np.max( - [ - onshorex, - onshore_substation_x_side_length, - electrolyzer_side, - solar_side_x, - ] - ) - * 1.8, - ndigits=roundto, - ), - ], - ylim=[ - round(np.min(onshorey - 100), ndigits=roundto), - round( - np.max(onshorey + battery_side_y + electrolyzer_side + solar_side_y + 100) - * 1.9, - ndigits=roundto, - ), - ], - ) - ax[ax_index_plant].set(aspect="equal") - - roundto = -3 - point_range_x = np.max(allpoints) - np.min(allpoints) - point_range_y = np.max(turbine_y) - np.min(turbine_y) - ax[ax_index_wind_plant].set( - xlim=[ - round(np.min(allpoints) - 0.5 * point_range_x, ndigits=roundto), - round(np.max(allpoints) + 0.5 * point_range_x, ndigits=roundto), - ], - ylim=[ - round(np.min(turbine_y) - 0.3 * point_range_y, ndigits=roundto), - round(np.max(turbine_y) + 0.3 * point_range_y, ndigits=roundto), - ], - ) - # ax[ax_index_wind_plant].autoscale() - ax[ax_index_wind_plant].set(aspect="equal") - # ax[ax_index_wind_plant].xaxis.set_major_locator(ticker.\ - # MultipleLocator(np.round(point_range_x*0.5, decimals=-3))) - # ax[ax_index_wind_plant].yaxis.set_major_locator(ticker.\ - # MultipleLocator(np.round(point_range_y*0.5, device_spacing=-3))) - - else: - roundto = -3 - point_range_x = np.max(allpoints) - np.min(allpoints) - point_range_y = np.max(turbine_y) - onshorey - ax[ax_index_plant].set( - xlim=[ - round(np.min(allpoints) - 0.7 * point_range_x, ndigits=roundto), - round(np.max(allpoints + 0.7 * point_range_x), ndigits=roundto), - ], - ylim=[ - round(np.min(onshorey) - 0.2 * point_range_y, ndigits=roundto), - round(np.max(turbine_y) + 1.0 * point_range_y, ndigits=roundto), - ], - ) - # ax[ax_index_plant].autoscale() - ax[ax_index_plant].set(aspect="equal") - # ax[ax_index_plant].xaxis.set_major_locator(ticker.MultipleLocator(2000)) - # ax[ax_index_plant].yaxis.set_major_locator(ticker.MultipleLocator(1000)) - - if design_scenario["wind_location"] == "offshore": - roundto = -2 - ax[ax_index_detail].set( - xlim=[ - round(origin_x - 400, ndigits=roundto), - round(origin_x + 100, ndigits=roundto), - ], - ylim=[ - round(origin_y - 200, ndigits=roundto), - round(origin_y + 200, ndigits=roundto), - ], - ) - ax[ax_index_detail].set(aspect="equal") - else: - roundto = -2 - - if "pv" in hopp_config["technologies"].keys(): - xmax = round( - np.max([onshorex, electrolyzer_side, battery_side_x, solar_side_x]) * 1.1, - ndigits=roundto, - ) - ymax = round( - onshorey + (solar_side_y + electrolyzer_side + battery_side_y) * 1.15, - ndigits=roundto, - ) - else: - xmax = round(np.max([onshorex]) * 1.1, ndigits=roundto) - ymax = round( - onshorey + (electrolyzer_side + battery_side_y + solar_side_y) * 1.1, - ndigits=roundto, - ) - ax[ax_index_detail].set( - xlim=[ - round(onshorex - 10, ndigits=roundto), - xmax, - ], - ylim=[ - round(onshorey - 200, ndigits=roundto), - ymax, - ], - ) - ax[ax_index_detail].set(aspect="equal") - - if design_scenario["wind_location"] == "offshore": - tower_buffer0 = 10 - tower_buffer1 = 10 - roundto = -1 - ax[ax_index_turbine_detail].set( - xlim=[ - round( - turbine_x[0] - tower_base_radius - tower_buffer0 - 50, - ndigits=roundto, - ), - round( - turbine_x[0] + tower_base_radius + 3 * tower_buffer1, - ndigits=roundto, - ), - ], - ylim=[ - round( - turbine_y[0] - tower_base_radius - 2 * tower_buffer0, - ndigits=roundto, - ), - round( - turbine_y[0] + tower_base_radius + 4 * tower_buffer1, - ndigits=roundto, - ), - ], - ) - ax[ax_index_turbine_detail].set(aspect="equal") - ax[ax_index_turbine_detail].xaxis.set_major_locator(ticker.MultipleLocator(10)) - ax[ax_index_turbine_detail].yaxis.set_major_locator(ticker.MultipleLocator(10)) - # ax[0,1].legend(frameon=False) - # ax[0,1].axis('off') - - if design_scenario["wind_location"] == "offshore": - labels = [ - "(a) Onshore plant", - "(b) Offshore plant", - "(c) Equipment platform and substation", - "(d) NW-most wind turbine", - ] - else: - labels = ["(a) Full plant", "(b) Non-wind plant detail"] - for axi, label in zip(ax.flatten(), labels): - axi.legend(frameon=False, ncol=2) # , ncol=2, loc="best") - axi.set(xlabel="Easting (m)", ylabel="Northing (m)") - axi.set_title(label, loc="left") - # axi.spines[['right', 'top']].set_visible(False) - - ## save the plot - plt.tight_layout() - savepaths = [ - output_dir / "figures/layout", - output_dir / "data", - ] - if save_plots: - for savepath in savepaths: - if not savepath.exists(): - savepath.mkdir(parents=True) - plt.savefig(savepaths[0] / f"plant_layout_{plant_design_number}.png", transparent=True) - - df = pd.DataFrame([component_areas]) - df.to_csv( - savepaths[1] / "fcomponent_areas_layout_{plant_design_number}.csv", - index=False, - ) - - if show_plots: - plt.show() - else: - plt.close() - - -def save_energy_flows( - hybrid_plant: HoppInterface.system, - electrolyzer_physics_results, - solver_results, - hours, - h2_storage_results, - simulation_length=8760, - output_dir="./output/", -): - if isinstance(output_dir, str): - output_dir = Path(output_dir).resolve() - - output = {} - if hybrid_plant.pv: - solar_plant_power = np.array(hybrid_plant.pv.generation_profile[0:simulation_length]) - output.update({"pv generation [kW]": solar_plant_power}) - if hybrid_plant.wind: - wind_plant_power = np.array(hybrid_plant.wind.generation_profile[0:simulation_length]) - output.update({"wind generation [kW]": wind_plant_power}) - if hybrid_plant.wave: - wave_plant_power = np.array(hybrid_plant.wave.generation_profile[0:simulation_length]) - output.update({"wave generation [kW]": wave_plant_power}) - if hybrid_plant.battery: - battery_power_out_mw = hybrid_plant.battery.outputs.P - output.update( - {"battery discharge [kW]": [(int(p > 0)) * p for p in battery_power_out_mw]} - ) # convert from MW to kW and extract only discharging - output.update( - {"battery charge [kW]": [-(int(p < 0)) * p for p in battery_power_out_mw]} - ) # convert from MW to kW and extract only charging - output.update({"battery state of charge [%]": hybrid_plant.battery.outputs.dispatch_SOC}) - total_generation_hourly = hybrid_plant.grid._system_model.Outputs.system_pre_interconnect_kwac[ - 0:simulation_length - ] - output.update({"total generation hourly [kW]": total_generation_hourly}) - output.update( - { - "total generation curtailed hourly [kW]": hybrid_plant.grid.generation_curtailed[ - 0:simulation_length - ] - } - ) - output.update({"total accessory power required [kW]": solver_results[0]}) - output.update({"grid energy usage hourly [kW]": solver_results[1]}) - output.update({"desal energy hourly [kW]": [solver_results[2]] * simulation_length}) - output.update( - { - "electrolyzer energy hourly [kW]": electrolyzer_physics_results[ - "power_to_electrolyzer_kw" - ] - } - ) - output.update({"electrolyzer bop energy hourly [kW]": solver_results[5]}) - output.update( - {"transport compressor energy hourly [kW]": [solver_results[3]] * simulation_length} - ) - output.update({"storage energy hourly [kW]": [solver_results[4]] * simulation_length}) - output.update( - { - "h2 production hourly [kg]": electrolyzer_physics_results["H2_Results"][ - "Hydrogen Hourly Production [kg/hr]" - ] - } - ) - if "hydrogen_storage_soc" in h2_storage_results: - output.update({"hydrogen storage SOC [kg]": h2_storage_results["hydrogen_storage_soc"]}) - if "hydrogen_demand_kgphr" in h2_storage_results: - output.update({"hydrogen demand [kg/h]": h2_storage_results["hydrogen_demand_kgphr"]}) - - df = pd.DataFrame.from_dict(output) - - filepath = output_dir / "data/production" - - if not filepath.exists(): - filepath.mkdir(parents=True) - - df.to_csv(filepath / "energy_flows.csv") - - return output - - -def calculate_lca( - wind_annual_energy_kwh, - solar_pv_annual_energy_kwh, - energy_shortfall_hopp, - h2_annual_prod_kg, - energy_to_electrolyzer_kwh, - hopp_config, - h2integrate_config, - total_accessory_power_renewable_kw, - total_accessory_power_grid_kw, - plant_design_scenario_number, - incentive_option_number, -): - """ - Function to perform Life Cycle Assessment (LCA) of the simulated system. - Calculates Scope 1, 2, and 3 average emissions over the lifetime of the plant in kg CO2e per - unit mass of product produced. - CO2e or carbon dioxide equivalent is a metric for the global warming potential of different - greenhouse gases (GHGs) by converting their emissions to the equivalent amount of CO2. - Leverages ANL's GREET model to determine emission intensity (EI), efficiency, feedstock - consumption, and energy consumption values of various processes - Leverages NREL's Cambium API to determine future grid generation mixes and emissions intensities - of grid electricity consumption - - Args: - wind_annual_energy_kwh (float): Annual energy from wind power (kWh) - solar_pv_annual_energy_kwh (float): Annual energy from solar pv power (kWh) - energy_shortfall_hopp: Total electricity to electrolyzer & peripherals from grid power (kWh) - h2_annual_prod_kg: Lifetime average annual H2 production accounting for electrolyzer - degradation (kg H2/year) - energy_to_electrolyzer_kwh: Total electricity to electrolyzer from grid power (kWh) - hopp_config (dict): HOPP configuration inputs based on input files - h2integrate_config (H2IntegrateSimulationConfig): all inputs to the h2integrate simulation - total_accessory_power_renewable_kw (numpy.ndarray): Total electricity to electrolysis - peripherals from renewable power (kWh) with shape = (8760,) - total_accessory_power_grid_kw (numpy.ndarray): Total electricity to electrolysis - peripherals from grid power (kWh) with shape = (8760,) - plant_design_scenario_number (int): plant design scenario number - incentive_option_number (int): incentive option number - - Returns: - lca_df (pandas.DataFrame): Pandas DataFrame containing average emissions intensities over - lifetime of plant and other relevant data - """ - # TODO: - # confirm site lat/long is proper for where electricity use will be - # (set from iron_pre or iron_win?) - - # Load relevant config and results data from HOPP and H2Integrate: - site_latitude = hopp_config["site"]["data"]["lat"] - site_longitude = hopp_config["site"]["data"]["lon"] - project_lifetime = h2integrate_config["project_parameters"][ - "project_lifetime" - ] # system lifetime (years) - plant_design_scenario = h2integrate_config["plant_design"][ - f"scenario{plant_design_scenario_number}" - ] # plant design scenario number - tax_incentive_option = h2integrate_config["policy_parameters"][ - f"option{incentive_option_number}" - ] # tax incentive option number - - # battery_annual_energy_kwh = hopp_results["annual_energies"][ - # "battery" - # ] # annual energy from battery (kWh) - # battery_system_capacity_kwh = hopp_results["hybrid_plant"].battery.system_capacity_kwh - # # battery rated capacity (kWh) - wind_turbine_rating_MW = ( - hopp_config["technologies"]["wind"]["turbine_rating_kw"] / 1000 - ) # wind turbine rating (MW) - wind_model = hopp_config["technologies"]["wind"]["model_name"] # wind model used in analysis - - # Determine renewable technologies in system and define renewables_case string for output file - renewable_technologies_modeled = [ - tech for tech in hopp_config["technologies"] if tech != "grid" - ] - if len(renewable_technologies_modeled) > 1: - renewables_case = "+".join(renewable_technologies_modeled) - elif len(renewable_technologies_modeled) == 1: - renewables_case = str(renewable_technologies_modeled[0]) - else: - renewables_case = "No-ren" - - # Determine grid case and define grid_case string for output file - # NOTE: original LCA project code calculations were created with functionality for a - # hybrid-grid case, however this functionality was removed during prior HOPP refactors - # NOTE: In future, update logic below to include 'hybrid-grid' case. Possibly look at - # input config yamls and technologies present for this logic?(pending modular framework): - # if only grid present -> grid-only? - # if any renewables + grid present -> hybrid-grid? - # if only renewables present -> off-grid? - if h2integrate_config["project_parameters"]["grid_connection"]: - if h2integrate_config["electrolyzer"]["sizing"]["hydrogen_dmd"] is not None: - grid_case = "grid-only" - else: - grid_case = "off-grid" - else: - grid_case = "off-grid" - - # Capture electrolyzer configuration variables / strings for output files - if h2integrate_config["electrolyzer"]["include_degradation_penalty"]: - electrolyzer_degradation = "True" - else: - electrolyzer_degradation = "False" - if plant_design_scenario["transportation"] == "colocated": - electrolyzer_centralization = "Centralized" - else: - electrolyzer_centralization = "Distributed" - electrolyzer_optimized = h2integrate_config["electrolyzer"]["pem_control_type"] - electrolyzer_type = h2integrate_config["lca_config"]["electrolyzer_type"] - number_of_electrolyzer_clusters = int( - ceildiv( - h2integrate_config["electrolyzer"]["rating"], - h2integrate_config["electrolyzer"]["cluster_rating_MW"], - ) - ) - - # Calculate average annual and lifetime h2 production - h2_lifetime_prod_kg = ( - h2_annual_prod_kg * project_lifetime - ) # Lifetime H2 production accounting for electrolyzer degradation (kg H2) - - # Calculate energy to electrolyzer and peripherals when hybrid-grid case - if grid_case == "hybrid-grid": - # Total electricity to electrolyzer and peripherals from grid power (kWh) - energy_shortfall_hopp.shape = ( - project_lifetime, - 8760, - ) # Reshaped to be annual power (project_lifetime, 8760) - annual_energy_to_electrolysis_from_grid = np.mean( - energy_shortfall_hopp, axis=0 - ) # Lifetime Average Annual electricity to electrolyzer and peripherals from grid power - # shape = (8760,) - - # Calculate energy to electrolyzer and peripherals when grid-only case - if grid_case == "grid-only": - energy_to_peripherals = ( - total_accessory_power_renewable_kw + total_accessory_power_grid_kw - ) # Total electricity to peripherals from grid power (kWh) - annual_energy_to_electrolysis_from_grid = ( - energy_to_electrolyzer_kwh + energy_to_peripherals - ) # Average Annual electricity to electrolyzer and peripherals from grid power - # shape = (8760,) - - # Create dataframe for electrolyzer + peripherals grid power profiles if grid connected - if grid_case in ("grid-only", "hybrid-grid"): - electrolyzer_grid_profile_data_dict = { - "Energy to electrolysis from grid (kWh)": annual_energy_to_electrolysis_from_grid - } - electrolyzer_grid_profile_df = pd.DataFrame(data=electrolyzer_grid_profile_data_dict) - electrolyzer_grid_profile_df = electrolyzer_grid_profile_df.reset_index().rename( - columns={"index": "Interval"} - ) - electrolyzer_grid_profile_df["Interval"] = electrolyzer_grid_profile_df["Interval"] + 1 - electrolyzer_grid_profile_df = electrolyzer_grid_profile_df.set_index("Interval") - - # Instantiate lists that define technologies / processes and LCA scopes - # used to dynamically define key value pairs in dictionaries to store data - processes = [ - "electrolysis", - "smr", - "smr_ccs", - "atr", - "atr_ccs", - "NH3_electrolysis", - "NH3_smr", - "NH3_smr_ccs", - "NH3_atr", - "NH3_atr_ccs", - "steel_electrolysis", - "steel_smr", - "steel_smr_ccs", - "steel_atr", - "steel_atr_ccs", - "ng_dri", - "ng_dri_eaf", - "h2_electrolysis_dri", - "h2_electrolysis_dri_eaf", - ] - - scopes = ["Scope3", "Scope2", "Scope1", "Total"] - - # Instantiate dictionary of numpy objects (np.nan -> converts to np.float when assigned value) - # to hold EI values per cambium year - EI_values = { - f"{process}_{scope}_EI": globals().get(f"{process}_{scope}_EI", np.nan) - for process in processes - for scope in scopes - } - - # Instantiate dictionary of lists to hold EI time series (ts) data for all cambium years - # EI_values for each cambium year are appended to corresponding lists - ts_EI_data = {f"{process}_{scope}_EI": [] for process in processes for scope in scopes} - - ## GREET Data - # Define conversions - g_to_kg = 0.001 # 1 g = 0.001 kg - MT_to_kg = 1000 # 1 metric ton = 1000 kg - kWh_to_MWh = 0.001 # 1 kWh = 0.001 MWh - MWh_to_kWh = 1000 # 1 MWh = 1000 kWh - gal_H2O_to_MT = 0.00378541 # 1 US gallon of H2O = 0.00378541 metric tons - - # Instantiate GreetData class object, parse greet if not already parsed - # return class object and load data dictionary - greet_data = GREETData(greet_year=2023) - greet_data_dict = greet_data.data - - # ------------------------------------------------------------------------------ - # Natural Gas - # ------------------------------------------------------------------------------ - NG_combust_EI = greet_data_dict[ - "NG_combust_EI" - ] # GHG Emissions Intensity of Natural Gas combustion in a utility / industrial large boiler - # (g CO2e/MJ Natural Gas combusted) - NG_supply_EI = greet_data_dict[ - "NG_supply_EI" - ] # GHG Emissions Intensity of supplying Natural Gas to processes as a feedstock / process fuel - # (g CO2e/MJ Natural Gas consumed) - - # ------------------------------------------------------------------------------ - # Water - # ------------------------------------------------------------------------------ - if h2integrate_config["lca_config"]["feedstock_water_type"] == "desal": - H2O_supply_EI = greet_data_dict[ - "desal_H2O_supply_EI" - ] # GHG Emissions Intensity of RO desalination and supply of that water to processes - # (kg CO2e/gal H2O). - elif h2integrate_config["lca_config"]["feedstock_water_type"] == "ground": - H2O_supply_EI = greet_data_dict[ - "ground_H2O_supply_EI" - ] # GHG Emissions Intensity of ground water and supply of that water to processes - # (kg CO2e/gal H2O). - elif h2integrate_config["lca_config"]["feedstock_water_type"] == "surface": - H2O_supply_EI = greet_data_dict[ - "surface_H2O_supply_EI" - ] # GHG Emissions Intensity of surface water and supply of that water to processes - # (kg CO2e/gal H2O). - # ------------------------------------------------------------------------------ - # Lime - # ------------------------------------------------------------------------------ - lime_supply_EI = greet_data_dict[ - "lime_supply_EI" - ] # GHG Emissions Intensity of supplying Lime to processes accounting for limestone mining, - # lime production, lime processing, and lime transportation assuming 20 miles via Diesel engines - # (kg CO2e/kg lime) - # ------------------------------------------------------------------------------ - # Carbon Coke - # ------------------------------------------------------------------------------ - coke_supply_EI = greet_data_dict[ - "coke_supply_EI" - ] # GHG Emissions Intensity of supplying Coke to processes accounting for combustion - # and non-combustion emissions of coke production - # (kg CO2e/kg Coke) - # ------------------------------------------------------------------------------ - # Renewable infrastructure embedded emission intensities - # ------------------------------------------------------------------------------ - # NOTE: HOPP/H2Integrate version at time of dev can only model PEM electrolysis - if electrolyzer_type == "pem": - # ely_stack_capex_EI = greet_data_dict[ - # "pem_ely_stack_capex_EI" - # ] # PEM electrolyzer CAPEX emissions (kg CO2e/kg H2) - ely_stack_and_BoP_capex_EI = greet_data_dict[ - "pem_ely_stack_and_BoP_capex_EI" - ] # PEM electrolyzer stack CAPEX + Balance of Plant emissions (kg CO2e/kg H2) - elif electrolyzer_type == "alkaline": - # ely_stack_capex_EI = greet_data_dict[ - # "alk_ely_stack_capex_EI" - # ] # Alkaline electrolyzer CAPEX emissions (kg CO2e/kg H2) - ely_stack_and_BoP_capex_EI = greet_data_dict[ - "alk_ely_stack_and_BoP_capex_EI" - ] # Alkaline electrolyzer stack CAPEX + Balance of Plant emissions (kg CO2e/kg H2) - elif electrolyzer_type == "soec": - # ely_stack_capex_EI = greet_data_dict[ - # "soec_ely_stack_capex_EI" - # ] # SOEC electrolyzer CAPEX emissions (kg CO2e/kg H2) - ely_stack_and_BoP_capex_EI = greet_data_dict[ - "soec_ely_stack_and_BoP_capex_EI" - ] # SOEC electrolyzer stack CAPEX + Balance of Plant emissions (kg CO2e/kg H2) - wind_capex_EI = greet_data_dict["wind_capex_EI"] # Wind CAPEX emissions (g CO2e/kWh) - solar_pv_capex_EI = greet_data_dict[ - "solar_pv_capex_EI" - ] # Solar PV CAPEX emissions (g CO2e/kWh) - battery_EI = greet_data_dict["battery_LFP_EI"] # LFP Battery embodied emissions (g CO2e/kWh) - nuclear_BWR_capex_EI = greet_data_dict[ - "nuclear_BWR_capex_EI" - ] # Nuclear Boiling Water Reactor (BWR) CAPEX emissions (g CO2e/kWh) - nuclear_PWR_capex_EI = greet_data_dict[ - "nuclear_PWR_capex_EI" - ] # Nuclear Pressurized Water Reactor (PWR) CAPEX emissions (g CO2e/kWh) - coal_capex_EI = greet_data_dict["coal_capex_EI"] # Coal CAPEX emissions (g CO2e/kWh) - gas_capex_EI = greet_data_dict[ - "gas_capex_EI" - ] # Natural Gas Combined Cycle (NGCC) CAPEX emissions (g CO2e/kWh) - hydro_capex_EI = greet_data_dict["hydro_capex_EI"] # Hydro CAPEX emissions (g CO2e/kWh) - bio_capex_EI = greet_data_dict["bio_capex_EI"] # Biomass CAPEX emissions (g CO2e/kWh) - # geothermal_egs_capex_EI = greet_data_dict[ - # "geothermal_egs_capex_EI" - # ] # Geothermal EGS CAPEX emissions (g CO2e/kWh) - geothermal_binary_capex_EI = greet_data_dict[ - "geothermal_binary_capex_EI" - ] # Geothermal Binary CAPEX emissions (g CO2e/kWh) - geothermal_flash_capex_EI = greet_data_dict[ - "geothermal_flash_capex_EI" - ] # Geothermal Flash CAPEX emissions (g CO2e/kWh) - - # ------------------------------------------------------------------------------ - # Steam methane reforming (SMR) and Autothermal Reforming (ATR) - # Incumbent H2 production processes - # ------------------------------------------------------------------------------ - smr_HEX_eff = greet_data_dict["smr_HEX_eff"] # SMR Heat exchange efficiency (%) - # SMR without CCS - smr_steam_prod = greet_data_dict[ - "smr_steam_prod" - ] # Steam exported for SMR w/out CCS (MJ/kg H2) - smr_NG_consume = greet_data_dict[ - "smr_NG_consume" - ] # Natural gas consumption for SMR w/out CCS accounting for efficiency, NG as feed and - # process fuel for SMR and steam production (MJ-LHV/kg H2) - smr_electricity_consume = greet_data_dict[ - "smr_electricity_consume" - ] # Electricity consumption for SMR w/out CCS accounting for efficiency, electricity - # as a process fuel (kWh/kg H2) - # SMR with CCS - smr_ccs_steam_prod = greet_data_dict[ - "smr_ccs_steam_prod" - ] # Steam exported for SMR with CCS (MJ/kg H2) - smr_ccs_perc_capture = greet_data_dict["smr_ccs_perc_capture"] # CCS rate for SMR (%) - smr_ccs_NG_consume = greet_data_dict[ - "smr_ccs_NG_consume" - ] # Natural gas consumption for SMR with CCS accounting for efficiency, NG as feed and process - # fuel for SMR and steam production (MJ-LHV/kg H2) - smr_ccs_electricity_consume = greet_data_dict[ - "smr_ccs_electricity_consume" - ] # SMR via NG w/ CCS WTG Total Energy consumption (kWh/kg H2) - # ATR without CCS - atr_NG_consume = greet_data_dict[ - "atr_NG_consume" - ] # Natural gas consumption for ATR w/out CCS accounting for efficiency, NG as feed and - # process fuel for SMR and steam production (MJ-LHV/kg H2) - atr_electricity_consume = greet_data_dict[ - "atr_electricity_consume" - ] # Electricity consumption for ATR w/out CCS accounting for efficiency, electricity as a - # process fuel (kWh/kg H2) - # ATR with CCS - atr_ccs_perc_capture = greet_data_dict["atr_ccs_perc_capture"] # CCS rate for ATR (%) - atr_ccs_NG_consume = greet_data_dict[ - "atr_ccs_NG_consume" - ] # Natural gas consumption for ATR with CCS accounting for efficiency, NG as feed and - # process fuel for SMR and steam production (MJ-LHV/kg H2) - atr_ccs_electricity_consume = greet_data_dict[ - "atr_ccs_electricity_consume" - ] # Electricity consumption for ATR with CCS accounting for efficiency, electricity as a - # process fuel (kWh/kg H2) - - # ------------------------------------------------------------------------------ - # Hydrogen production via water electrolysis - # ------------------------------------------------------------------------------ - if electrolyzer_type == "pem": - ely_H2O_consume = greet_data_dict[ - "pem_ely_H2O_consume" - ] # H2O consumption for H2 production in PEM electrolyzer (gal H20/kg H2) - elif electrolyzer_type == "alkaline": - ely_H2O_consume = greet_data_dict[ - "alk_ely_H2O_consume" - ] # H2O consumption for H2 production in Alkaline electrolyzer (gal H20/kg H2) - elif electrolyzer_type == "soec": - ely_H2O_consume = greet_data_dict[ - "soec_ely_H2O_consume" - ] # H2O consumption for H2 production in High Temp SOEC electrolyzer (gal H20/kg H2) - # ------------------------------------------------------------------------------ - # Ammonia (NH3) - # ------------------------------------------------------------------------------ - NH3_NG_consume = greet_data_dict[ - "NH3_NG_consume" - ] # Natural gas consumption for combustion in the Haber-Bosch process / Boiler for Ammonia - # production (MJ/metric ton NH3) - NH3_H2_consume = greet_data_dict[ - "NH3_H2_consume" - ] # Gaseous Hydrogen consumption for Ammonia production, based on chemical balance and is - # applicable for all NH3 production pathways (kg H2/kg NH3) - NH3_electricity_consume = greet_data_dict[ - "NH3_electricity_consume" - ] # Total Electrical Energy consumption for Ammonia production (kWh/kg NH3) - - # ------------------------------------------------------------------------------ - # Steel - # ------------------------------------------------------------------------------ - # Values agnostic of DRI-EAF config - # NOTE: in future if accounting for different iron ore mining, pelletizing processes, - # and production processes, then add if statement to check h2integrate_config for - # iron production type (DRI, electrowinning, etc) - # iron_ore_mining_EI_per_MT_steel = greet_data_dict[ - # "DRI_iron_ore_mining_EI_per_MT_steel" - # ] # GHG Emissions Intensity of Iron ore mining for use in DRI-EAF Steel production - # # (kg CO2e/metric ton steel produced) - iron_ore_mining_EI_per_MT_ore = greet_data_dict[ - "DRI_iron_ore_mining_EI_per_MT_ore" - ] # GHG Emissions Intensity of Iron ore mining for use in DRI-EAF Steel production - # (kg CO2e/metric ton iron ore) - # iron_ore_pelletizing_EI_per_MT_steel = greet_data_dict[ - # "DRI_iron_ore_pelletizing_EI_per_MT_steel" - # ] # GHG Emissions Intensity of Iron ore pelletizing for use in DRI-EAF Steel production - # # (kg CO2e/metric ton steel produced) - iron_ore_pelletizing_EI_per_MT_ore = greet_data_dict[ - "DRI_iron_ore_pelletizing_EI_per_MT_ore" - ] # GHG Emissions Intensity of Iron ore pelletizing for use in DRI-EAF Steel production - # (kg CO2e/metric ton iron ore) - - # NOTE: in future if accounting for different steel productin processes (DRI-EAF vs XYZ), - # then add if statement to check h2integrate_config for steel production process and - # update HOPP > greet_data.py with specific variables for each process - steel_H2O_consume = greet_data_dict[ - "steel_H2O_consume" - ] # Total H2O consumption for DRI-EAF Steel production w/ 83% H2 and 0% scrap, accounts for - # water used in iron ore mining, pelletizing, DRI, and EAF - # (metric ton H2O/metric ton steel production) - steel_H2_consume = greet_data_dict[ - "steel_H2_consume" - ] # Hydrogen consumption for DRI-EAF Steel production w/ 83% H2 regardless of scrap - # (metric tons H2/metric ton steel production) - steel_NG_consume = greet_data_dict[ - "steel_NG_consume" - ] # Natural gas consumption for DRI-EAF Steel production accounting for DRI with 83% H2, - # and EAF + LRF (GJ/metric ton steel) - steel_electricity_consume = greet_data_dict[ - "steel_electricity_consume" - ] # Total Electrical Energy consumption for DRI-EAF Steel production accounting for - # DRI with 83% H2 and EAF + LRF (MWh/metric ton steel production) - steel_iron_ore_consume = greet_data_dict[ - "steel_iron_ore_consume" - ] # Iron ore consumption for DRI-EAF Steel production - # (metric ton iron ore/metric ton steel production) - steel_lime_consume = greet_data_dict[ - "steel_lime_consume" - ] # Lime consumption for DRI-EAF Steel production - # (metric ton lime/metric ton steel production) - - ## Load in Iron model outputs - # Read iron_performance.performances_df from pkl - iron_performance_fn = "{}/iron_performance/{:.3f}_{:.3f}_{:d}.pkl".format( - h2integrate_config["iron_out_fn"], - site_latitude, - site_longitude, - hopp_config["site"]["data"]["year"], - ) - iron_performance = load_dill_pickle(iron_performance_fn) - iron_performance = iron_performance.performances_df - # Instantiate objects to hold iron performance values - ng_dri_steel_prod = np.nan - ng_dri_pigiron_prod = np.nan - ng_dri_iron_ore_consume = np.nan - ng_dri_NG_consume = np.nan - ng_dri_electricity_consume = np.nan - ng_dri_H2O_consume = np.nan - ng_dri_eaf_steel_prod = np.nan - ng_dri_eaf_pigiron_prod = np.nan - ng_dri_eaf_iron_ore_consume = np.nan - ng_dri_eaf_lime_consume = np.nan - ng_dri_eaf_coke_consume = np.nan - ng_dri_eaf_NG_consume = np.nan - ng_dri_eaf_electricity_consume = np.nan - ng_dri_eaf_H2O_consume = np.nan - h2_dri_steel_prod = np.nan - h2_dri_pigiron_prod = np.nan - h2_dri_H2_consume = np.nan - h2_dri_iron_ore_consume = np.nan - h2_dri_NG_consume = np.nan - h2_dri_electricity_consume = np.nan - h2_dri_H2O_consume = np.nan - h2_dri_eaf_steel_prod = np.nan - h2_dri_eaf_pigiron_prod = np.nan - h2_dri_eaf_H2_consume = np.nan - h2_dri_eaf_iron_ore_consume = np.nan - h2_dri_eaf_lime_consume = np.nan - h2_dri_eaf_coke_consume = np.nan - h2_dri_eaf_NG_consume = np.nan - h2_dri_eaf_electricity_consume = np.nan - h2_dri_eaf_H2O_consume = np.nan - # Pull iron_performance values - if iron_performance["Product"].values[0] == "ng_dri": - # Note to Dakota from Jonathan - the denominator has been corrected, - # we're now getting performance per unit pig iron, not per unit steel - # Leave this code in though, I want to be able to build an option to - # calculate per unit steel instead of per unit iron - ng_dri_steel_prod = iron_performance.loc[ - iron_performance["Name"] == "Steel Production", "Model" - ].item() - # metric tonnes steel per year - ng_dri_pigiron_prod = iron_performance.loc[ - iron_performance["Name"] == "Pig Iron Production", "Model" - ].item() - # metric tonnes pig iron per year - capacity_denominator = h2integrate_config["iron_win"]["performance"]["capacity_denominator"] - if capacity_denominator == "iron": - steel_to_pigiron_ratio = 1 - elif capacity_denominator == "steel": - steel_to_pigiron_ratio = ng_dri_steel_prod / ng_dri_pigiron_prod - # conversion from MT steel to MT pig iron in denominator of units - ng_dri_iron_ore_consume = ( - iron_performance.loc[iron_performance["Name"] == "Iron Ore", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonnes ore / pellet consumed per metric tonne pig iron produced - ng_dri_NG_consume = ( - iron_performance.loc[iron_performance["Name"] == "Natural Gas", "Model"].item() - * steel_to_pigiron_ratio - ) - # GJ-LHV NG consumed per metric tonne pig iron produced - ng_dri_electricity_consume = ( - iron_performance.loc[iron_performance["Name"] == "Electricity", "Model"].item() - * steel_to_pigiron_ratio - ) - # MWh electricity consumed per metric tonne pig iron produced - ng_dri_H2O_consume = ( - iron_performance.loc[iron_performance["Name"] == "Raw Water Withdrawal", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonne H2O consumed per metric tonne pig iron produced - if iron_performance["Product"].values[0] == "ng_eaf": - ng_dri_eaf_steel_prod = iron_performance.loc[ - iron_performance["Name"] == "Steel Production", "Model" - ].item() - # metric tonnes steel per year - ng_dri_eaf_pigiron_prod = iron_performance.loc[ - iron_performance["Name"] == "Pig Iron Production", "Model" - ].item() - # metric tonnes pig iron per year - capacity_denominator = h2integrate_config["iron_win"]["performance"]["capacity_denominator"] - if capacity_denominator == "iron": - steel_to_pigiron_ratio = 1 - elif capacity_denominator == "steel": - steel_to_pigiron_ratio = ng_dri_eaf_steel_prod / ng_dri_eaf_pigiron_prod - # conversion from MT steel to MT pig iron in denominator of units - ng_dri_eaf_iron_ore_consume = ( - iron_performance.loc[iron_performance["Name"] == "Iron Ore", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonnes ore / pellet consumed per metric tonne pig iron produced - ng_dri_eaf_NG_consume = ( - iron_performance.loc[iron_performance["Name"] == "Natural Gas", "Model"].item() - * steel_to_pigiron_ratio - ) - # GJ-LHV NG consumed per metric tonne pig iron produced - ng_dri_eaf_electricity_consume = ( - iron_performance.loc[iron_performance["Name"] == "Electricity", "Model"].item() - * steel_to_pigiron_ratio - ) - # MWh electricity consumed per metric tonne pig iron produced - ng_dri_eaf_coke_consume = ( - iron_performance.loc[iron_performance["Name"] == "Carbon (Coke)", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonnes carbon coke consumed per metric tonne pig iron produced - ng_dri_eaf_lime_consume = ( - iron_performance.loc[iron_performance["Name"] == "Lime", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonnes carbon lime consumed per metric tonne pig iron produced - ng_dri_eaf_H2O_consume = ( - iron_performance.loc[iron_performance["Name"] == "Raw Water Withdrawal", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonne H2O consumed per metric tonne pig iron produced - if iron_performance["Product"].values[0] == "h2_dri": - h2_dri_steel_prod = iron_performance.loc[ - iron_performance["Name"] == "Steel Production", "Model" - ].item() - # metric tonnes steel per year - h2_dri_pigiron_prod = iron_performance.loc[ - iron_performance["Name"] == "Pig Iron Production", "Model" - ].item() - # metric tonnes pig iron per year - capacity_denominator = h2integrate_config["iron_win"]["performance"]["capacity_denominator"] - if capacity_denominator == "iron": - steel_to_pigiron_ratio = 1 - elif capacity_denominator == "steel": - steel_to_pigiron_ratio = h2_dri_steel_prod / h2_dri_pigiron_prod - # conversion from MT steel to MT pig iron in denominator of units - h2_dri_iron_ore_consume = ( - iron_performance.loc[iron_performance["Name"] == "Iron Ore", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonnes ore / pellet consumed per metric tonne pig iron produced - h2_dri_H2_consume = ( - iron_performance.loc[iron_performance["Name"] == "Hydrogen", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonne H2 consumed per metric tonne pig iron produced - h2_dri_NG_consume = ( - iron_performance.loc[iron_performance["Name"] == "Natural Gas", "Model"].item() - * steel_to_pigiron_ratio - ) - # GJ-LHV NG consumed per metric tonne pig iron produced - h2_dri_electricity_consume = ( - iron_performance.loc[iron_performance["Name"] == "Electricity", "Model"].item() - * steel_to_pigiron_ratio - ) - # MWh electricity consumed per metric tonne pig iron produced - h2_dri_H2O_consume = ( - iron_performance.loc[iron_performance["Name"] == "Raw Water Withdrawal", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonne H2O consume per metric tonne pig iron produced - if iron_performance["Product"].values[0] == "h2_eaf": - h2_dri_eaf_steel_prod = iron_performance.loc[ - iron_performance["Name"] == "Steel Production", "Model" - ].item() - # metric tonnes steel per year - h2_dri_eaf_pigiron_prod = iron_performance.loc[ - iron_performance["Name"] == "Pig Iron Production", "Model" - ].item() - # metric tonnes pig iron per year - capacity_denominator = h2integrate_config["iron_win"]["performance"]["capacity_denominator"] - if capacity_denominator == "iron": - steel_to_pigiron_ratio = 1 - elif capacity_denominator == "steel": - steel_to_pigiron_ratio = h2_dri_eaf_steel_prod / h2_dri_eaf_pigiron_prod - # conversion from MT steel to MT pig iron in denominator of units - h2_dri_eaf_iron_ore_consume = ( - iron_performance.loc[iron_performance["Name"] == "Iron Ore", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonnes ore / pellet consumed per metric tonne pig iron produced - h2_dri_eaf_H2_consume = ( - iron_performance.loc[iron_performance["Name"] == "Hydrogen", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonne H2 consumed per metric tonne pig iron produced - h2_dri_eaf_NG_consume = ( - iron_performance.loc[iron_performance["Name"] == "Natural Gas", "Model"].item() - * steel_to_pigiron_ratio - ) - # GJ-LHV NG consumed per metric tonne pig iron produced - h2_dri_eaf_electricity_consume = ( - iron_performance.loc[iron_performance["Name"] == "Electricity", "Model"].item() - * steel_to_pigiron_ratio - ) - # MWh electricity consumed per metric tonne pig iron produced - h2_dri_eaf_coke_consume = ( - iron_performance.loc[iron_performance["Name"] == "Carbon (Coke)", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonnes carbon coke consumed per metric tonne pig iron produced - h2_dri_eaf_lime_consume = ( - iron_performance.loc[iron_performance["Name"] == "Lime", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonnes carbon lime consumed per metric tonne pig iron produced - h2_dri_eaf_H2O_consume = ( - iron_performance.loc[iron_performance["Name"] == "Raw Water Withdrawal", "Model"].item() - * steel_to_pigiron_ratio - ) - # metric tonne H2O consume per metric tonne pig iron produced - - ## Cambium - # Define cambium_year - # NOTE: at time of dev hopp logic for LCOH = atb_year + 2yr + install_period(3yrs) = 5 years - cambium_year = h2integrate_config["project_parameters"]["financial_analysis_start_year"] + 3 - # Pull / download cambium data files - cambium_data = CambiumData( - lat=site_latitude, - lon=site_longitude, - year=cambium_year, - project_uuid=h2integrate_config["lca_config"]["cambium"]["project_uuid"], - scenario=h2integrate_config["lca_config"]["cambium"]["scenario"], - location_type=h2integrate_config["lca_config"]["cambium"]["location_type"], - time_type=h2integrate_config["lca_config"]["cambium"]["time_type"], - ) - - # Read in Cambium data file for each year available - # NOTE: Additional LRMER values for CO2, CH4, and NO2 are available through the cambium call - # that are not used in this analysis - for resource_file in cambium_data.resource_files: - # Read in csv file to a dataframe, update column names and indexes - cambium_data_df = pd.read_csv( - resource_file, - index_col=None, - header=0, - usecols=[ - "lrmer_co2e_c", - "lrmer_co2e_p", - "lrmer_co2e", - "generation", - "battery_MWh", - "biomass_MWh", - "beccs_MWh", - "canada_MWh", - "coal_MWh", - "coal-ccs_MWh", - "csp_MWh", - "distpv_MWh", - "gas-cc_MWh", - "gas-cc-ccs_MWh", - "gas-ct_MWh", - "geothermal_MWh", - "hydro_MWh", - "nuclear_MWh", - "o-g-s_MWh", - "phs_MWh", - "upv_MWh", - "wind-ons_MWh", - "wind-ofs_MWh", - ], - ) - cambium_data_df = cambium_data_df.reset_index().rename( - columns={ - "index": "Interval", - "lrmer_co2e_c": "LRMER CO2 equiv. combustion (kg-CO2e/MWh)", - "lrmer_co2e_p": "LRMER CO2 equiv. precombustion (kg-CO2e/MWh)", - "lrmer_co2e": "LRMER CO2 equiv. total (kg-CO2e/MWh)", - } - ) - cambium_data_df["Interval"] = cambium_data_df["Interval"] + 1 - cambium_data_df = cambium_data_df.set_index("Interval") - - if grid_case in ("grid-only", "hybrid-grid"): - # Calculate consumption and emissions factor for electrolysis powered by the grid - combined_data_df = pd.concat([electrolyzer_grid_profile_df, cambium_data_df], axis=1) - electrolysis_grid_electricity_consume = combined_data_df[ - "Energy to electrolysis from grid (kWh)" - ].sum() # Total energy to the electrolyzer from the grid (kWh) - electrolysis_scope3_grid_emissions = ( - (combined_data_df["Energy to electrolysis from grid (kWh)"] / 1000) - * combined_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"] - ).sum() # Scope 3 Electrolysis Emissions from grid electricity consumption (kg CO2e) - electrolysis_scope2_grid_emissions = ( - (combined_data_df["Energy to electrolysis from grid (kWh)"] / 1000) - * combined_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"] - ).sum() # Scope 2 Electrolysis Emissions from grid electricity consumption (kg CO2e) - - # Calculate annual percentages of nuclear, geothermal, hydropower, wind, solar, battery, - # and fossil fuel power in cambium grid mix (%) - generation_annual_total_MWh = cambium_data_df["generation"].sum() - generation_annual_nuclear_fraction = ( - cambium_data_df["nuclear_MWh"].sum() / generation_annual_total_MWh - ) - generation_annual_coal_oil_fraction = ( - cambium_data_df["coal_MWh"].sum() - + cambium_data_df["coal-ccs_MWh"].sum() - + cambium_data_df["o-g-s_MWh"].sum() - ) / generation_annual_total_MWh - generation_annual_gas_fraction = ( - cambium_data_df["gas-cc_MWh"].sum() - + cambium_data_df["gas-cc-ccs_MWh"].sum() - + cambium_data_df["gas-ct_MWh"].sum() - ) / generation_annual_total_MWh - generation_annual_bio_fraction = ( - cambium_data_df["biomass_MWh"].sum() + cambium_data_df["beccs_MWh"].sum() - ) / generation_annual_total_MWh - generation_annual_geothermal_fraction = ( - cambium_data_df["geothermal_MWh"].sum() / generation_annual_total_MWh - ) - generation_annual_hydro_fraction = ( - cambium_data_df["hydro_MWh"].sum() + cambium_data_df["phs_MWh"].sum() - ) / generation_annual_total_MWh - generation_annual_wind_fraction = ( - cambium_data_df["wind-ons_MWh"].sum() + cambium_data_df["wind-ofs_MWh"].sum() - ) / generation_annual_total_MWh - generation_annual_solar_fraction = ( - cambium_data_df["upv_MWh"].sum() - + cambium_data_df["distpv_MWh"].sum() - + cambium_data_df["csp_MWh"].sum() - ) / generation_annual_total_MWh - generation_annual_battery_fraction = ( - cambium_data_df["battery_MWh"].sum() - ) / generation_annual_total_MWh - nuclear_PWR_fraction = 0.655 # % of grid nuclear power from PWR, calculated from USNRC data - # based on type and rated capacity - nuclear_BWR_fraction = 0.345 # % of grid nuclear power from BWR, calculated from USNRC data - # based on type and rated capacity - # https://www.nrc.gov/reactors/operating/list-power-reactor-units.html - geothermal_binary_fraction = 0.28 # % of grid geothermal power from binary, - # average from EIA data and NREL Geothermal prospector - geothermal_flash_fraction = 0.72 # % of grid geothermal power from flash, - # average from EIA data and NREL Geothermal prospector - # https://www.eia.gov/todayinenergy/detail.php?id=44576# - - # Calculate Grid Imbedded Emissions Intensity for cambium grid mix of power sources - # (kg CO2e/kwh) - grid_capex_EI = ( - (generation_annual_nuclear_fraction * nuclear_PWR_fraction * nuclear_PWR_capex_EI) - + (generation_annual_nuclear_fraction * nuclear_BWR_fraction * nuclear_BWR_capex_EI) - + (generation_annual_coal_oil_fraction * coal_capex_EI) - + (generation_annual_gas_fraction * gas_capex_EI) - + (generation_annual_bio_fraction * bio_capex_EI) - + ( - generation_annual_geothermal_fraction - * geothermal_binary_fraction - * geothermal_binary_capex_EI - ) - + ( - generation_annual_geothermal_fraction - * geothermal_flash_fraction - * geothermal_flash_capex_EI - ) - + (generation_annual_hydro_fraction * hydro_capex_EI) - + (generation_annual_wind_fraction * wind_capex_EI) - + (generation_annual_solar_fraction * solar_pv_capex_EI) - + (generation_annual_battery_fraction * battery_EI) * g_to_kg - ) - - # NOTE: current config assumes SMR, ATR, NH3, and Steel processes are always grid powered - # electricity needed for these processes does not come from renewables - # NOTE: this is reflective of the current state of modeling these systems in the code - # at time of dev and should be updated to allow renewables in the future - if "hybrid-grid" in grid_case: - ## H2 production via electrolysis - # Calculate grid-connected electrolysis emissions (kg CO2e/kg H2) - # future cases should reflect targeted electrolyzer electricity usage - EI_values["electrolysis_Scope3_EI"] = ( - ely_stack_and_BoP_capex_EI - + (ely_H2O_consume * H2O_supply_EI) - + ( - ( - electrolysis_scope3_grid_emissions - + (wind_capex_EI * g_to_kg * wind_annual_energy_kwh) - + (solar_pv_capex_EI * g_to_kg * solar_pv_annual_energy_kwh) - + (grid_capex_EI * electrolysis_grid_electricity_consume) - ) - / h2_annual_prod_kg - ) - ) - EI_values["electrolysis_Scope2_EI"] = ( - electrolysis_scope2_grid_emissions / h2_annual_prod_kg - ) - EI_values["electrolysis_Scope1_EI"] = 0 - EI_values["electrolysis_Total_EI"] = ( - EI_values["electrolysis_Scope1_EI"] - + EI_values["electrolysis_Scope2_EI"] - + EI_values["electrolysis_Scope3_EI"] - ) - - # Calculate ammonia emissions via hybrid grid electrolysis (kg CO2e/kg NH3) - EI_values["NH3_electrolysis_Scope3_EI"] = ( - (NH3_H2_consume * EI_values["electrolysis_Total_EI"]) - + (NH3_NG_consume * NG_supply_EI * g_to_kg / MT_to_kg) - + ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (NH3_electricity_consume * grid_capex_EI) - ) - EI_values["NH3_electrolysis_Scope2_EI"] = ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["NH3_electrolysis_Scope1_EI"] = ( - NH3_NG_consume * NG_combust_EI * g_to_kg / MT_to_kg - ) - EI_values["NH3_electrolysis_Total_EI"] = ( - EI_values["NH3_electrolysis_Scope1_EI"] - + EI_values["NH3_electrolysis_Scope2_EI"] - + EI_values["NH3_electrolysis_Scope3_EI"] - ) - - # Calculate steel emissions via hybrid grid electrolysis (kg CO2e/metric ton steel) - EI_values["steel_electrolysis_Scope3_EI"] = ( - (steel_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (steel_lime_consume * lime_supply_EI * MT_to_kg) - + (steel_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (steel_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (steel_NG_consume * NG_supply_EI) - + (steel_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (steel_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["steel_electrolysis_Scope2_EI"] = ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["steel_electrolysis_Scope1_EI"] = steel_NG_consume * NG_combust_EI - EI_values["steel_electrolysis_Total_EI"] = ( - EI_values["steel_electrolysis_Scope1_EI"] - + EI_values["steel_electrolysis_Scope2_EI"] - + EI_values["steel_electrolysis_Scope3_EI"] - ) - - # Calculate H2 DRI emissions via hybrid grid electrolysis - # (kg CO2e/metric tonne pig iron) - EI_values["h2_electrolysis_dri_Scope3_EI"] = ( - (h2_dri_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (h2_dri_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (h2_dri_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (h2_dri_NG_consume * NG_supply_EI) - + (h2_dri_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - h2_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (h2_dri_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["h2_electrolysis_dri_Scope2_EI"] = ( - h2_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["h2_electrolysis_dri_Scope1_EI"] = h2_dri_NG_consume * NG_combust_EI - EI_values["h2_electrolysis_dri_Total_EI"] = ( - EI_values["h2_electrolysis_dri_Scope1_EI"] - + EI_values["h2_electrolysis_dri_Scope2_EI"] - + EI_values["h2_electrolysis_dri_Scope3_EI"] - ) - - # Calculate H2 DRI EAF emissions via hybrid grid electrolysis - # (kg CO2e/metric tonne pig iron) - EI_values["h2_electrolysis_dri_eaf_Scope3_EI"] = ( - (h2_dri_eaf_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (h2_dri_eaf_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (h2_dri_eaf_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (h2_dri_eaf_lime_consume * MT_to_kg * lime_supply_EI) - + (h2_dri_eaf_coke_consume * MT_to_kg * coke_supply_EI) - + (h2_dri_eaf_NG_consume * NG_supply_EI) - + (h2_dri_eaf_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - h2_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (h2_dri_eaf_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["h2_electrolysis_dri_eaf_Scope2_EI"] = ( - h2_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["h2_electrolysis_dri_eaf_Scope1_EI"] = h2_dri_eaf_NG_consume * NG_combust_EI - EI_values["h2_electrolysis_dri_eaf_Total_EI"] = ( - EI_values["h2_electrolysis_dri_eaf_Scope1_EI"] - + EI_values["h2_electrolysis_dri_eaf_Scope2_EI"] - + EI_values["h2_electrolysis_dri_eaf_Scope3_EI"] - ) - - # Calculate Natural Gas (NG) DRI emissions - # (kg CO2e/metric tonne pig iron) - EI_values["ng_dri_Scope3_EI"] = ( - (ng_dri_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (ng_dri_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (ng_dri_NG_consume * NG_supply_EI) - + (ng_dri_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - ng_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (ng_dri_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["ng_dri_Scope2_EI"] = ( - ng_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["ng_dri_Scope1_EI"] = ng_dri_NG_consume * NG_combust_EI - EI_values["ng_dri_Total_EI"] = ( - EI_values["ng_dri_Scope1_EI"] - + EI_values["ng_dri_Scope2_EI"] - + EI_values["ng_dri_Scope3_EI"] - ) - - # Calculate Natural Gas (NG) DRI EAF emissions - # (kg CO2e/metric tonne pig iron) - EI_values["ng_dri_eaf_Scope3_EI"] = ( - (ng_dri_eaf_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (ng_dri_eaf_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (ng_dri_eaf_lime_consume * MT_to_kg * lime_supply_EI) - + (ng_dri_eaf_coke_consume * MT_to_kg * coke_supply_EI) - + (ng_dri_eaf_NG_consume * NG_supply_EI) - + (ng_dri_eaf_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - ng_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (ng_dri_eaf_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["ng_dri_eaf_Scope2_EI"] = ( - ng_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["ng_dri_eaf_Scope1_EI"] = ng_dri_eaf_NG_consume * NG_combust_EI - EI_values["ng_dri_eaf_Total_EI"] = ( - EI_values["ng_dri_eaf_Scope1_EI"] - + EI_values["ng_dri_eaf_Scope2_EI"] - + EI_values["ng_dri_eaf_Scope3_EI"] - ) - - if "grid-only" in grid_case: - ## H2 production via electrolysis - # Calculate grid-connected electrolysis emissions (kg CO2e/kg H2) - EI_values["electrolysis_Scope3_EI"] = ( - ely_stack_and_BoP_capex_EI - + (ely_H2O_consume * H2O_supply_EI) - + ( - ( - electrolysis_scope3_grid_emissions - + (grid_capex_EI * electrolysis_grid_electricity_consume) - ) - / h2_annual_prod_kg - ) - ) - EI_values["electrolysis_Scope2_EI"] = ( - electrolysis_scope2_grid_emissions / h2_annual_prod_kg - ) - EI_values["electrolysis_Scope1_EI"] = 0 - EI_values["electrolysis_Total_EI"] = ( - EI_values["electrolysis_Scope1_EI"] - + EI_values["electrolysis_Scope2_EI"] - + EI_values["electrolysis_Scope3_EI"] - ) - - # Calculate ammonia emissions via grid only electrolysis (kg CO2e/kg NH3) - EI_values["NH3_electrolysis_Scope3_EI"] = ( - (NH3_H2_consume * EI_values["electrolysis_Total_EI"]) - + (NH3_NG_consume * NG_supply_EI * g_to_kg / MT_to_kg) - + ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (NH3_electricity_consume * grid_capex_EI) - ) - EI_values["NH3_electrolysis_Scope2_EI"] = ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["NH3_electrolysis_Scope1_EI"] = ( - NH3_NG_consume * NG_combust_EI * g_to_kg / MT_to_kg - ) - EI_values["NH3_electrolysis_Total_EI"] = ( - EI_values["NH3_electrolysis_Scope1_EI"] - + EI_values["NH3_electrolysis_Scope2_EI"] - + EI_values["NH3_electrolysis_Scope3_EI"] - ) - - # Calculate steel emissions via grid only electrolysis (kg CO2e/metric ton steel) - EI_values["steel_electrolysis_Scope3_EI"] = ( - (steel_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (steel_lime_consume * lime_supply_EI * MT_to_kg) - + (steel_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (steel_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (steel_NG_consume * NG_supply_EI) - + (steel_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (steel_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["steel_electrolysis_Scope2_EI"] = ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["steel_electrolysis_Scope1_EI"] = steel_NG_consume * NG_combust_EI - EI_values["steel_electrolysis_Total_EI"] = ( - EI_values["steel_electrolysis_Scope1_EI"] - + EI_values["steel_electrolysis_Scope2_EI"] - + EI_values["steel_electrolysis_Scope3_EI"] - ) - - # Calculate H2 DRI emissions via grid only electrolysis - # (kg CO2e/metric tonne pig iron) - EI_values["h2_electrolysis_dri_Scope3_EI"] = ( - (h2_dri_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (h2_dri_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (h2_dri_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (h2_dri_NG_consume * NG_supply_EI) - + (h2_dri_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - h2_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (h2_dri_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["h2_electrolysis_dri_Scope2_EI"] = ( - h2_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["h2_electrolysis_dri_Scope1_EI"] = h2_dri_NG_consume * NG_combust_EI - EI_values["h2_electrolysis_dri_Total_EI"] = ( - EI_values["h2_electrolysis_dri_Scope1_EI"] - + EI_values["h2_electrolysis_dri_Scope2_EI"] - + EI_values["h2_electrolysis_dri_Scope3_EI"] - ) - - # Calculate H2 DRI EAF emissions via grid only electrolysis - # (kg CO2e/metric tonne pig iron) - EI_values["h2_electrolysis_dri_eaf_Scope3_EI"] = ( - (h2_dri_eaf_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (h2_dri_eaf_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (h2_dri_eaf_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (h2_dri_eaf_lime_consume * MT_to_kg * lime_supply_EI) - + (h2_dri_eaf_coke_consume * MT_to_kg * coke_supply_EI) - + (h2_dri_eaf_NG_consume * NG_supply_EI) - + (h2_dri_eaf_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - h2_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (h2_dri_eaf_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["h2_electrolysis_dri_eaf_Scope2_EI"] = ( - h2_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["h2_electrolysis_dri_eaf_Scope1_EI"] = h2_dri_eaf_NG_consume * NG_combust_EI - EI_values["h2_electrolysis_dri_eaf_Total_EI"] = ( - EI_values["h2_electrolysis_dri_eaf_Scope1_EI"] - + EI_values["h2_electrolysis_dri_eaf_Scope2_EI"] - + EI_values["h2_electrolysis_dri_eaf_Scope3_EI"] - ) - - ## H2 production via SMR - # Calculate SMR emissions. SMR and SMR + CCS are always grid-connected (kg CO2e/kg H2) - EI_values["smr_Scope3_EI"] = ( - (NG_supply_EI * g_to_kg * (smr_NG_consume - smr_steam_prod / smr_HEX_eff)) - + ( - smr_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (smr_electricity_consume * grid_capex_EI) - ) - EI_values["smr_Scope2_EI"] = ( - smr_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["smr_Scope1_EI"] = ( - NG_combust_EI * g_to_kg * (smr_NG_consume - smr_steam_prod / smr_HEX_eff) - ) - EI_values["smr_Total_EI"] = ( - EI_values["smr_Scope1_EI"] + EI_values["smr_Scope2_EI"] + EI_values["smr_Scope3_EI"] - ) - - # Calculate ammonia emissions via SMR process (kg CO2e/kg NH3) - EI_values["NH3_smr_Scope3_EI"] = ( - (NH3_H2_consume * EI_values["smr_Total_EI"]) - + (NH3_NG_consume * NG_supply_EI * g_to_kg / MT_to_kg) - + ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (NH3_electricity_consume * grid_capex_EI) - ) - EI_values["NH3_smr_Scope2_EI"] = ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["NH3_smr_Scope1_EI"] = NH3_NG_consume * NG_combust_EI * g_to_kg / MT_to_kg - EI_values["NH3_smr_Total_EI"] = ( - EI_values["NH3_smr_Scope1_EI"] - + EI_values["NH3_smr_Scope2_EI"] - + EI_values["NH3_smr_Scope3_EI"] - ) - - # Calculate steel emissions via SMR process (kg CO2e/metric ton steel) - EI_values["steel_smr_Scope3_EI"] = ( - (steel_H2_consume * MT_to_kg * EI_values["smr_Total_EI"]) - + (steel_lime_consume * lime_supply_EI * MT_to_kg) - + (steel_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (steel_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (steel_NG_consume * NG_supply_EI) - + (steel_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (steel_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["steel_smr_Scope2_EI"] = ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["steel_smr_Scope1_EI"] = steel_NG_consume * NG_combust_EI - EI_values["steel_smr_Total_EI"] = ( - EI_values["steel_smr_Scope1_EI"] - + EI_values["steel_smr_Scope2_EI"] - + EI_values["steel_smr_Scope3_EI"] - ) - - # Calculate SMR + CCS emissions (kg CO2e/kg H2) - EI_values["smr_ccs_Scope3_EI"] = ( - (NG_supply_EI * g_to_kg * (smr_ccs_NG_consume - smr_ccs_steam_prod / smr_HEX_eff)) - + ( - smr_ccs_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (smr_ccs_electricity_consume * grid_capex_EI) - ) - EI_values["smr_ccs_Scope2_EI"] = ( - smr_ccs_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["smr_ccs_Scope1_EI"] = ( - (1 - smr_ccs_perc_capture) - * NG_combust_EI - * g_to_kg - * (smr_ccs_NG_consume - smr_ccs_steam_prod / smr_HEX_eff) - ) - EI_values["smr_ccs_Total_EI"] = ( - EI_values["smr_ccs_Scope1_EI"] - + EI_values["smr_ccs_Scope2_EI"] - + EI_values["smr_ccs_Scope3_EI"] - ) - - # Calculate ammonia emissions via SMR with CCS process (kg CO2e/kg NH3) - EI_values["NH3_smr_ccs_Scope3_EI"] = ( - (NH3_H2_consume * EI_values["smr_ccs_Total_EI"]) - + (NH3_NG_consume * NG_supply_EI * g_to_kg / MT_to_kg) - + ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (NH3_electricity_consume * grid_capex_EI) - ) - EI_values["NH3_smr_ccs_Scope2_EI"] = ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["NH3_smr_ccs_Scope1_EI"] = NH3_NG_consume * NG_combust_EI * g_to_kg / MT_to_kg - EI_values["NH3_smr_ccs_Total_EI"] = ( - EI_values["NH3_smr_ccs_Scope1_EI"] - + EI_values["NH3_smr_ccs_Scope2_EI"] - + EI_values["NH3_smr_ccs_Scope3_EI"] - ) - - # Calculate steel emissions via SMR with CCS process (kg CO2e/metric ton steel) - EI_values["steel_smr_ccs_Scope3_EI"] = ( - (steel_H2_consume * MT_to_kg * EI_values["smr_ccs_Total_EI"]) - + (steel_lime_consume * lime_supply_EI * MT_to_kg) - + (steel_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (steel_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (steel_NG_consume * NG_supply_EI) - + (steel_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (steel_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["steel_smr_ccs_Scope2_EI"] = ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["steel_smr_ccs_Scope1_EI"] = steel_NG_consume * NG_combust_EI - EI_values["steel_smr_ccs_Total_EI"] = ( - EI_values["steel_smr_ccs_Scope1_EI"] - + EI_values["steel_smr_ccs_Scope2_EI"] - + EI_values["steel_smr_ccs_Scope3_EI"] - ) - - ## H2 production via ATR - # Calculate ATR emissions. ATR and ATR + CCS are always grid-connected (kg CO2e/kg H2) - EI_values["atr_Scope3_EI"] = ( - (NG_supply_EI * g_to_kg * atr_NG_consume) - + ( - atr_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (atr_electricity_consume * grid_capex_EI) - ) - EI_values["atr_Scope2_EI"] = ( - atr_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["atr_Scope1_EI"] = NG_combust_EI * g_to_kg * atr_NG_consume - EI_values["atr_Total_EI"] = ( - EI_values["atr_Scope1_EI"] + EI_values["atr_Scope2_EI"] + EI_values["atr_Scope3_EI"] - ) - - # Calculate ammonia emissions via ATR process (kg CO2e/kg NH3) - EI_values["NH3_atr_Scope3_EI"] = ( - (NH3_H2_consume * EI_values["atr_Total_EI"]) - + (NH3_NG_consume * NG_supply_EI * g_to_kg / MT_to_kg) - + ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (NH3_electricity_consume * grid_capex_EI) - ) - EI_values["NH3_atr_Scope2_EI"] = ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["NH3_atr_Scope1_EI"] = NH3_NG_consume * NG_combust_EI * g_to_kg / MT_to_kg - EI_values["NH3_atr_Total_EI"] = ( - EI_values["NH3_atr_Scope1_EI"] - + EI_values["NH3_atr_Scope2_EI"] - + EI_values["NH3_atr_Scope3_EI"] - ) - - # Calculate steel emissions via ATR process (kg CO2e/metric ton steel) - EI_values["steel_atr_Scope3_EI"] = ( - (steel_H2_consume * MT_to_kg * EI_values["atr_Total_EI"]) - + (steel_lime_consume * lime_supply_EI * MT_to_kg) - + (steel_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (steel_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (steel_NG_consume * NG_supply_EI) - + (steel_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (steel_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["steel_atr_Scope2_EI"] = ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["steel_atr_Scope1_EI"] = steel_NG_consume * NG_combust_EI - EI_values["steel_atr_Total_EI"] = ( - EI_values["steel_atr_Scope1_EI"] - + EI_values["steel_atr_Scope2_EI"] - + EI_values["steel_atr_Scope3_EI"] - ) - - # Calculate ATR + CCS emissions (kg CO2e/kg H2) - EI_values["atr_ccs_Scope3_EI"] = ( - (NG_supply_EI * g_to_kg * atr_ccs_NG_consume) - + ( - atr_ccs_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (atr_ccs_electricity_consume * grid_capex_EI) - ) - EI_values["atr_ccs_Scope2_EI"] = ( - atr_ccs_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["atr_ccs_Scope1_EI"] = ( - (1 - atr_ccs_perc_capture) * NG_combust_EI * g_to_kg * atr_ccs_NG_consume - ) - EI_values["atr_ccs_Total_EI"] = ( - EI_values["atr_ccs_Scope1_EI"] - + EI_values["atr_ccs_Scope2_EI"] - + EI_values["atr_ccs_Scope3_EI"] - ) - - # Calculate ammonia emissions via ATR with CCS process (kg CO2e/kg NH3) - EI_values["NH3_atr_ccs_Scope3_EI"] = ( - (NH3_H2_consume * EI_values["atr_ccs_Total_EI"]) - + (NH3_NG_consume * NG_supply_EI * g_to_kg / MT_to_kg) - + ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (NH3_electricity_consume * grid_capex_EI) - ) - EI_values["NH3_atr_ccs_Scope2_EI"] = ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["NH3_atr_ccs_Scope1_EI"] = NH3_NG_consume * NG_combust_EI * g_to_kg / MT_to_kg - EI_values["NH3_atr_ccs_Total_EI"] = ( - EI_values["NH3_atr_ccs_Scope1_EI"] - + EI_values["NH3_atr_ccs_Scope2_EI"] - + EI_values["NH3_atr_ccs_Scope3_EI"] - ) - - # Calculate steel emissions via ATR with CCS process (kg CO2e/metric ton steel) - EI_values["steel_atr_ccs_Scope3_EI"] = ( - (steel_H2_consume * MT_to_kg * EI_values["atr_ccs_Total_EI"]) - + (steel_lime_consume * lime_supply_EI * MT_to_kg) - + (steel_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (steel_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (steel_NG_consume * NG_supply_EI) - + (steel_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (steel_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["steel_atr_ccs_Scope2_EI"] = ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["steel_atr_ccs_Scope1_EI"] = steel_NG_consume * NG_combust_EI - EI_values["steel_atr_ccs_Total_EI"] = ( - EI_values["steel_atr_ccs_Scope1_EI"] - + EI_values["steel_atr_ccs_Scope2_EI"] - + EI_values["steel_atr_ccs_Scope3_EI"] - ) - - # Calculate Natural Gas (NG) DRI emissions (kg CO2e/metric tonne pig iron) - EI_values["ng_dri_Scope3_EI"] = ( - (ng_dri_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (ng_dri_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (ng_dri_NG_consume * NG_supply_EI) - + (ng_dri_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - ng_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (ng_dri_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["ng_dri_Scope2_EI"] = ( - ng_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["ng_dri_Scope1_EI"] = ng_dri_NG_consume * NG_combust_EI - EI_values["ng_dri_Total_EI"] = ( - EI_values["ng_dri_Scope1_EI"] - + EI_values["ng_dri_Scope2_EI"] - + EI_values["ng_dri_Scope3_EI"] - ) - - # Calculate Natural Gas (NG) DRI EAF emissions (kg CO2e/metric tonne pig iron) - EI_values["ng_dri_eaf_Scope3_EI"] = ( - (ng_dri_eaf_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (ng_dri_eaf_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (ng_dri_eaf_lime_consume * MT_to_kg * lime_supply_EI) - + (ng_dri_eaf_coke_consume * MT_to_kg * coke_supply_EI) - + (ng_dri_eaf_NG_consume * NG_supply_EI) - + (ng_dri_eaf_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - ng_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (ng_dri_eaf_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["ng_dri_eaf_Scope2_EI"] = ( - ng_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["ng_dri_eaf_Scope1_EI"] = ng_dri_eaf_NG_consume * NG_combust_EI - EI_values["ng_dri_eaf_Total_EI"] = ( - EI_values["ng_dri_eaf_Scope1_EI"] - + EI_values["ng_dri_eaf_Scope2_EI"] - + EI_values["ng_dri_eaf_Scope3_EI"] - ) - - if "off-grid" in grid_case: - ## H2 production via electrolysis - # Calculate renewable only electrolysis emissions (kg CO2e/kg H2) - EI_values["electrolysis_Scope3_EI"] = ( - ely_stack_and_BoP_capex_EI - + (ely_H2O_consume * H2O_supply_EI) - + ( - ( - (wind_capex_EI * g_to_kg * wind_annual_energy_kwh) - + (solar_pv_capex_EI * g_to_kg * solar_pv_annual_energy_kwh) - ) - / h2_annual_prod_kg - ) - ) - EI_values["electrolysis_Scope2_EI"] = 0 - EI_values["electrolysis_Scope1_EI"] = 0 - EI_values["electrolysis_Total_EI"] = ( - EI_values["electrolysis_Scope1_EI"] - + EI_values["electrolysis_Scope2_EI"] - + EI_values["electrolysis_Scope3_EI"] - ) - - # Calculate ammonia emissions via renewable electrolysis (kg CO2e/kg NH3) - EI_values["NH3_electrolysis_Scope3_EI"] = ( - (NH3_H2_consume * EI_values["electrolysis_Total_EI"]) - + (NH3_NG_consume * NG_supply_EI * g_to_kg / MT_to_kg) - + ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (NH3_electricity_consume * grid_capex_EI) - ) - EI_values["NH3_electrolysis_Scope2_EI"] = ( - NH3_electricity_consume - * kWh_to_MWh - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["NH3_electrolysis_Scope1_EI"] = ( - NH3_NG_consume * NG_combust_EI * g_to_kg / MT_to_kg - ) - EI_values["NH3_electrolysis_Total_EI"] = ( - EI_values["NH3_electrolysis_Scope1_EI"] - + EI_values["NH3_electrolysis_Scope2_EI"] - + EI_values["NH3_electrolysis_Scope3_EI"] - ) - - # Calculate steel emissions via renewable electrolysis (kg CO2e/metric ton steel) - EI_values["steel_electrolysis_Scope3_EI"] = ( - (steel_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (steel_lime_consume * lime_supply_EI * MT_to_kg) - + (steel_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (steel_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (steel_NG_consume * NG_supply_EI) - + (steel_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (steel_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["steel_electrolysis_Scope2_EI"] = ( - steel_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["steel_electrolysis_Scope1_EI"] = steel_NG_consume * NG_combust_EI - EI_values["steel_electrolysis_Total_EI"] = ( - EI_values["steel_electrolysis_Scope1_EI"] - + EI_values["steel_electrolysis_Scope2_EI"] - + EI_values["steel_electrolysis_Scope3_EI"] - ) - - # Calculate H2 DRI emissions via off grid electrolysis (kg CO2e/metric tonne pig iron) - EI_values["h2_electrolysis_dri_Scope3_EI"] = ( - (h2_dri_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (h2_dri_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (h2_dri_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (h2_dri_NG_consume * NG_supply_EI) - + (h2_dri_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - h2_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (h2_dri_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["h2_electrolysis_dri_Scope2_EI"] = ( - h2_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["h2_electrolysis_dri_Scope1_EI"] = h2_dri_NG_consume * NG_combust_EI - EI_values["h2_electrolysis_dri_Total_EI"] = ( - EI_values["h2_electrolysis_dri_Scope1_EI"] - + EI_values["h2_electrolysis_dri_Scope2_EI"] - + EI_values["h2_electrolysis_dri_Scope3_EI"] - ) - - # Calculate H2 DRI EAF emissions via off grid electrolysis (kg CO2e/tonne pig iron) - EI_values["h2_electrolysis_dri_eaf_Scope3_EI"] = ( - (h2_dri_eaf_H2_consume * MT_to_kg * EI_values["electrolysis_Total_EI"]) - + (h2_dri_eaf_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (h2_dri_eaf_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (h2_dri_eaf_lime_consume * MT_to_kg * lime_supply_EI) - + (h2_dri_eaf_coke_consume * MT_to_kg * coke_supply_EI) - + (h2_dri_eaf_NG_consume * NG_supply_EI) - + (h2_dri_eaf_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - h2_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (h2_dri_eaf_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["h2_electrolysis_dri_eaf_Scope2_EI"] = ( - h2_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["h2_electrolysis_dri_eaf_Scope1_EI"] = h2_dri_eaf_NG_consume * NG_combust_EI - EI_values["h2_electrolysis_dri_eaf_Total_EI"] = ( - EI_values["h2_electrolysis_dri_eaf_Scope1_EI"] - + EI_values["h2_electrolysis_dri_eaf_Scope2_EI"] - + EI_values["h2_electrolysis_dri_eaf_Scope3_EI"] - ) - - # Calculate Natural Gas (NG) DRI emissions (kg CO2e/metric tonne pig iron) - EI_values["ng_dri_Scope3_EI"] = ( - (ng_dri_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (ng_dri_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (ng_dri_NG_consume * NG_supply_EI) - + (ng_dri_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - ng_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (ng_dri_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["ng_dri_Scope2_EI"] = ( - ng_dri_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["ng_dri_Scope1_EI"] = ng_dri_NG_consume * NG_combust_EI - EI_values["ng_dri_Total_EI"] = ( - EI_values["ng_dri_Scope1_EI"] - + EI_values["ng_dri_Scope2_EI"] - + EI_values["ng_dri_Scope3_EI"] - ) - - # Calculate Natural Gas (NG) DRI EAF emissions (kg CO2e/metric tonne pig iron) - EI_values["ng_dri_eaf_Scope3_EI"] = ( - (ng_dri_eaf_iron_ore_consume * iron_ore_mining_EI_per_MT_ore) - + (ng_dri_eaf_iron_ore_consume * iron_ore_pelletizing_EI_per_MT_ore) - + (ng_dri_eaf_lime_consume * MT_to_kg * lime_supply_EI) - + (ng_dri_eaf_coke_consume * MT_to_kg * coke_supply_EI) - + (ng_dri_eaf_NG_consume * NG_supply_EI) - + (ng_dri_eaf_H2O_consume * (H2O_supply_EI / gal_H2O_to_MT)) - + ( - ng_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. precombustion (kg-CO2e/MWh)"].mean() - ) - + (ng_dri_eaf_electricity_consume * MWh_to_kWh * grid_capex_EI) - ) - EI_values["ng_dri_eaf_Scope2_EI"] = ( - ng_dri_eaf_electricity_consume - * cambium_data_df["LRMER CO2 equiv. combustion (kg-CO2e/MWh)"].mean() - ) - EI_values["ng_dri_eaf_Scope1_EI"] = ng_dri_eaf_NG_consume * NG_combust_EI - EI_values["ng_dri_eaf_Total_EI"] = ( - EI_values["ng_dri_eaf_Scope1_EI"] - + EI_values["ng_dri_eaf_Scope2_EI"] - + EI_values["ng_dri_eaf_Scope3_EI"] - ) - - # Append emission intensity values for each year to lists in the ts_EI_data dictionary - for key in ts_EI_data: - ts_EI_data[key].append(EI_values[key]) - - ## Interpolation of emission intensities for years not captured by cambium - # (cambium 2023 offers 2025-2050 in 5 year increments) - # Define end of life based on cambium_year and project lifetime - endoflife_year = cambium_year + project_lifetime - - # Instantiate dictionary of lists to hold full EI time series (ts) data - # including interpolated data for years when cambium data is not available - ts_EI_data_interpolated = { - f"{process}_{scope}_EI": [] for process in processes for scope in scopes - } - - # Loop through years between cambium_year and endoflife_year, interpolate values - # Check if the defined cambium_year is less than the earliest data year available - # from the cambium API, flag and warn users - if cambium_year < min(cambium_data.cambium_years): - cambium_year_warning_message = """Warning, the earliest year available for cambium data is - {min_cambium_year}! For all years less than {min_cambium_year}, LCA calculations will use - Cambium data from {min_cambium_year}. Thus, calculated emission intensity values for these - years may be understated.""".format(min_cambium_year=min(cambium_data.cambium_years)) - print("****************** WARNING ******************") - warnings.warn(cambium_year_warning_message) - cambium_warning_flag = True - else: - cambium_warning_flag = False - for year in range(cambium_year, endoflife_year): - # if year < the minimum cambium_year (currently 2025 in Cambium 2023) - # use data from the minimum year - if year < min(cambium_data.cambium_years): - for key in ts_EI_data_interpolated: - ts_EI_data_interpolated[key].append(ts_EI_data[key][0]) - - # else if year <= the maximum cambium_year (currently 2050 in Cambium 2023) - # interpolate the values (copies existing values if year is already present) - elif year <= max(cambium_data.cambium_years): - for key in ts_EI_data_interpolated: - ts_EI_data_interpolated[key].append( - np.interp(year, cambium_data.cambium_years, ts_EI_data[key]) - ) - - # else if year > maximum cambium_year, copy data from maximum year (ie: copy data from 2050) - else: - for key in ts_EI_data_interpolated: - ts_EI_data_interpolated[key].append(ts_EI_data[key][-1]) - - # Put all cumulative metrics and relevant data into a dictionary, then dataframe - # return the dataframe, save results to csv in post_processing() - lca_dict = { - "Cambium Warning": [cambium_year_warning_message if cambium_warning_flag else "None"], - "Total Life Cycle H2 Production (kg-H2)": [h2_lifetime_prod_kg], - "Electrolysis Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["electrolysis_Scope3_EI"])) / project_lifetime - ], - "Electrolysis Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["electrolysis_Scope2_EI"])) / project_lifetime - ], - "Electrolysis Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["electrolysis_Scope1_EI"])) / project_lifetime - ], - "Electrolysis Total Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["electrolysis_Total_EI"])) / project_lifetime - ], - "Ammonia Electrolysis Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_electrolysis_Scope3_EI"])) - / project_lifetime - ], - "Ammonia Electrolysis Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_electrolysis_Scope2_EI"])) - / project_lifetime - ], - "Ammonia Electrolysis Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_electrolysis_Scope1_EI"])) - / project_lifetime - ], - "Ammonia Electrolysis Total Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_electrolysis_Total_EI"])) / project_lifetime - ], - "Steel Electrolysis Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_electrolysis_Scope3_EI"])) - / project_lifetime - ], - "Steel Electrolysis Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_electrolysis_Scope2_EI"])) - / project_lifetime - ], - "Steel Electrolysis Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_electrolysis_Scope1_EI"])) - / project_lifetime - ], - "Steel Electrolysis Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_electrolysis_Total_EI"])) - / project_lifetime - ], - "H2 Electrolysis DRI Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["h2_electrolysis_dri_Scope3_EI"])) - / project_lifetime - ], - "H2 Electrolysis DRI Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["h2_electrolysis_dri_Scope2_EI"])) - / project_lifetime - ], - "H2 Electrolysis DRI Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["h2_electrolysis_dri_Scope1_EI"])) - / project_lifetime - ], - "H2 Electrolysis DRI Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["h2_electrolysis_dri_Total_EI"])) - / project_lifetime - ], - "H2 Electrolysis DRI EAF Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["h2_electrolysis_dri_eaf_Scope3_EI"])) - / project_lifetime - ], - "H2 Electrolysis DRI EAF Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["h2_electrolysis_dri_eaf_Scope2_EI"])) - / project_lifetime - ], - "H2 Electrolysis DRI EAF Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["h2_electrolysis_dri_eaf_Scope1_EI"])) - / project_lifetime - ], - "H2 Electrolysis DRI EAF Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["h2_electrolysis_dri_eaf_Total_EI"])) - / project_lifetime - ], - "SMR Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["smr_Scope3_EI"])) / project_lifetime - ], - "SMR Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["smr_Scope2_EI"])) / project_lifetime - ], - "SMR Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["smr_Scope1_EI"])) / project_lifetime - ], - "SMR Total Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["smr_Total_EI"])) / project_lifetime - ], - "Ammonia SMR Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_smr_Scope3_EI"])) / project_lifetime - ], - "Ammonia SMR Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_smr_Scope2_EI"])) / project_lifetime - ], - "Ammonia SMR Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_smr_Scope1_EI"])) / project_lifetime - ], - "Ammonia SMR Total Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_smr_Total_EI"])) / project_lifetime - ], - "Steel SMR Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_smr_Scope3_EI"])) / project_lifetime - ], - "Steel SMR Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_smr_Scope2_EI"])) / project_lifetime - ], - "Steel SMR Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_smr_Scope1_EI"])) / project_lifetime - ], - "Steel SMR Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_smr_Total_EI"])) / project_lifetime - ], - "SMR with CCS Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["smr_ccs_Scope3_EI"])) / project_lifetime - ], - "SMR with CCS Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["smr_ccs_Scope2_EI"])) / project_lifetime - ], - "SMR with CCS Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["smr_ccs_Scope1_EI"])) / project_lifetime - ], - "SMR with CCS Total Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["smr_ccs_Total_EI"])) / project_lifetime - ], - "Ammonia SMR with CCS Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_smr_ccs_Scope3_EI"])) / project_lifetime - ], - "Ammonia SMR with CCS Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_smr_ccs_Scope2_EI"])) / project_lifetime - ], - "Ammonia SMR with CCS Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_smr_ccs_Scope1_EI"])) / project_lifetime - ], - "Ammonia SMR with CCS Total Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_smr_ccs_Total_EI"])) / project_lifetime - ], - "Steel SMR with CCS Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_smr_ccs_Scope3_EI"])) / project_lifetime - ], - "Steel SMR with CCS Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_smr_ccs_Scope2_EI"])) / project_lifetime - ], - "Steel SMR with CCS Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_smr_ccs_Scope1_EI"])) / project_lifetime - ], - "Steel SMR with CCS Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_smr_ccs_Total_EI"])) / project_lifetime - ], - "ATR Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["atr_Scope3_EI"])) / project_lifetime - ], - "ATR Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["atr_Scope2_EI"])) / project_lifetime - ], - "ATR Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["atr_Scope1_EI"])) / project_lifetime - ], - "ATR Total Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["atr_Total_EI"])) / project_lifetime - ], - "Ammonia ATR Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_atr_Scope3_EI"])) / project_lifetime - ], - "Ammonia ATR Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_atr_Scope2_EI"])) / project_lifetime - ], - "Ammonia ATR Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_atr_Scope1_EI"])) / project_lifetime - ], - "Ammonia ATR Total Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_atr_Total_EI"])) / project_lifetime - ], - "Steel ATR Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_atr_Scope3_EI"])) / project_lifetime - ], - "Steel ATR Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_atr_Scope2_EI"])) / project_lifetime - ], - "Steel ATR Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_atr_Scope1_EI"])) / project_lifetime - ], - "Steel ATR Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_atr_Total_EI"])) / project_lifetime - ], - "ATR with CCS Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["atr_ccs_Scope3_EI"])) / project_lifetime - ], - "ATR with CCS Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["atr_ccs_Scope2_EI"])) / project_lifetime - ], - "ATR with CCS Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["atr_ccs_Scope1_EI"])) / project_lifetime - ], - "ATR with CCS Total Lifetime Average GHG Emissions (kg-CO2e/kg-H2)": [ - sum(np.asarray(ts_EI_data_interpolated["atr_ccs_Total_EI"])) / project_lifetime - ], - "Ammonia ATR with CCS Scope 3 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_atr_ccs_Scope3_EI"])) / project_lifetime - ], - "Ammonia ATR with CCS Scope 2 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_atr_ccs_Scope2_EI"])) / project_lifetime - ], - "Ammonia ATR with CCS Scope 1 Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_atr_ccs_Scope1_EI"])) / project_lifetime - ], - "Ammonia ATR with CCS Total Lifetime Average GHG Emissions (kg-CO2e/kg-NH3)": [ - sum(np.asarray(ts_EI_data_interpolated["NH3_atr_ccs_Total_EI"])) / project_lifetime - ], - "Steel ATR with CCS Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_atr_ccs_Scope3_EI"])) / project_lifetime - ], - "Steel ATR with CCS Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_atr_ccs_Scope2_EI"])) / project_lifetime - ], - "Steel ATR with CCS Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_atr_ccs_Scope1_EI"])) / project_lifetime - ], - "Steel ATR with CCS Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["steel_atr_ccs_Total_EI"])) / project_lifetime - ], - "NG DRI Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["ng_dri_Scope3_EI"])) / project_lifetime - ], - "NG DRI Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["ng_dri_Scope2_EI"])) / project_lifetime - ], - "NG DRI Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["ng_dri_Scope1_EI"])) / project_lifetime - ], - "NG DRI Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["ng_dri_Total_EI"])) / project_lifetime - ], - "NG DRI EAF Scope 3 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["ng_dri_eaf_Scope3_EI"])) / project_lifetime - ], - "NG DRI EAF Scope 2 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["ng_dri_eaf_Scope2_EI"])) / project_lifetime - ], - "NG DRI EAF Scope 1 Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["ng_dri_eaf_Scope1_EI"])) / project_lifetime - ], - "NG DRI EAF Total Lifetime Average GHG Emissions (kg-CO2e/MT steel)": [ - sum(np.asarray(ts_EI_data_interpolated["ng_dri_eaf_Total_EI"])) / project_lifetime - ], - "Site Latitude": [site_latitude], - "Site Longitude": [site_longitude], - "Cambium Year": [cambium_year], - "Electrolysis Case": [electrolyzer_centralization], - "Grid Case": [grid_case], - "Renewables Case": [renewables_case], - "Wind Turbine Rating (MW)": [wind_turbine_rating_MW], - "Wind Model": [wind_model], - "Electrolyzer Degradation Modeled": [electrolyzer_degradation], - "Electrolyzer Stack Optimization": [electrolyzer_optimized], - f"Number of {electrolyzer_type} Electrolyzer Clusters": [number_of_electrolyzer_clusters], - "Electricity ITC (%/100 CapEx)": [tax_incentive_option["electricity_itc"]], - "Electricity PTC ($/kWh 1992 dollars)": [tax_incentive_option["electricity_ptc"]], - "H2 Storage ITC (%/100 CapEx)": [tax_incentive_option["h2_storage_itc"]], - "H2 PTC ($/kWh 2022 dollars)": [tax_incentive_option["h2_ptc"]], - } - - lca_df = pd.DataFrame(data=lca_dict) - - return lca_df - - -# set up function to post-process HOPP results -def post_process_simulation( - lcoe, - lcoh, - pf_lcoh, - pf_lcoe, - hopp_results, - electrolyzer_physics_results, - hopp_config, - h2integrate_config, - orbit_config, - turbine_config, - h2_storage_results, - total_accessory_power_renewable_kw, - total_accessory_power_grid_kw, - capex_breakdown, - opex_breakdown, - wind_cost_results, - platform_results, - desal_results, - design_scenario, - plant_design_number, - incentive_option, - solver_results=[], - show_plots=False, - save_plots=False, - verbose=False, - output_dir="./output/", -): # , lcoe, lcoh, lcoh_with_grid, lcoh_grid_only): - if any(i in h2integrate_config for i in ["iron", "iron_pre", "iron_win", "iron_post"]): - msg = ( - "Post processing not yet implemented for iron model. LCA can still be set up through " - "h2integrate_config.yaml -> lca_config" - ) - raise NotImplementedError(msg) - - if isinstance(output_dir, str): - output_dir = Path(output_dir).resolve() - # colors (official NREL color palette https://brand.nrel.gov/content/index/guid/color_palette?parent=61) - colors = [ - "#0079C2", - "#00A4E4", - "#F7A11A", - "#FFC423", - "#5D9732", - "#8CC63F", - "#5E6A71", - "#D1D5D8", - "#933C06", - "#D9531E", - ] - - # post process results - if verbose: - print("LCOE: ", round(lcoe * 1e3, 2), "$/MWh") - print("LCOH: ", round(lcoh, 2), "$/kg") - print( - "hybrid electricity plant capacity factor: ", - round( - np.sum(hopp_results["combined_hybrid_power_production_hopp"]) - / (hopp_results["hybrid_plant"].system_capacity_kw.hybrid * 365 * 24), - 2, - ), - ) - print( - "electrolyzer capacity factor: ", - round( - np.sum(electrolyzer_physics_results["power_to_electrolyzer_kw"]) - * 1e-3 - / (h2integrate_config["electrolyzer"]["rating"] * 365 * 24), - 2, - ), - ) - print( - "Electrolyzer CAPEX installed $/kW: ", - round( - capex_breakdown["electrolyzer"] - / (h2integrate_config["electrolyzer"]["rating"] * 1e3), - 2, - ), - ) - - # Run LCA analysis if config yaml flag = True - if h2integrate_config["lca_config"]["run_lca"]: - lca_df = calculate_lca( - hopp_results=hopp_results, - electrolyzer_physics_results=electrolyzer_physics_results, - hopp_config=hopp_config, - h2integrate_config=h2integrate_config, - total_accessory_power_renewable_kw=total_accessory_power_renewable_kw, - total_accessory_power_grid_kw=total_accessory_power_grid_kw, - plant_design_scenario_number=plant_design_number, - incentive_option_number=incentive_option, - ) - - if show_plots or save_plots: - visualize_plant( - hopp_config, - h2integrate_config, - turbine_config, - wind_cost_results, - hopp_results, - platform_results, - desal_results, - h2_storage_results, - electrolyzer_physics_results, - design_scenario, - colors, - plant_design_number, - show_plots=show_plots, - save_plots=save_plots, - output_dir=output_dir, - ) - savepaths = [ - output_dir / "data/", - output_dir / "data/lcoe/", - output_dir / "data/lcoh/", - output_dir / "data/lca/", - ] - for sp in savepaths: - if not sp.exists(): - sp.mkdir(parents=True) - - pf_lcoh.get_cost_breakdown().to_csv( - savepaths[2] - / f'cost_breakdown_lcoh_design{plant_design_number}_incentive{incentive_option}_{h2integrate_config["h2_storage"]["type"]}storage.csv' # noqa: E501 - ) - pf_lcoe.get_cost_breakdown().to_csv( - savepaths[1] - / f'cost_breakdown_lcoe_design{plant_design_number}_incentive{incentive_option}_{h2integrate_config["h2_storage"]["type"]}storage.csv' # noqa: E501 - ) - - # Save LCA results if analysis was run - if h2integrate_config["lca_config"]["run_lca"]: - lca_savepath = ( - savepaths[3] - / f'LCA_results_design{plant_design_number}_incentive{incentive_option}_{h2integrate_config["h2_storage"]["type"]}storage.csv' # noqa: E501 - ) - lca_df.to_csv(lca_savepath) - print("LCA Analysis was run as a postprocessing step. Results were saved to:") - print(lca_savepath) - - # create dataframe for saving all the stuff - h2integrate_config["design_scenario"] = design_scenario - h2integrate_config["plant_design_number"] = plant_design_number - h2integrate_config["incentive_options"] = incentive_option - - # save power usage data - if len(solver_results) > 0: - hours = len(hopp_results["combined_hybrid_power_production_hopp"]) - annual_energy_breakdown = { - "electricity_generation_kwh": sum( - hopp_results["combined_hybrid_power_production_hopp"] - ), - "electrolyzer_kwh": sum(electrolyzer_physics_results["power_to_electrolyzer_kw"]), - "renewable_kwh": sum(solver_results[0]), - "grid_power_kwh": sum(solver_results[1]), - "desal_kwh": solver_results[2] * hours, - "h2_transport_compressor_power_kwh": solver_results[3] * hours, - "h2_storage_power_kwh": solver_results[4] * hours, - "electrolyzer_bop_energy_kwh": sum(solver_results[5]), - } - - ######################### save detailed ORBIT cost information - if wind_cost_results.orbit_project: - _, orbit_capex_breakdown, wind_capex_multiplier = adjust_orbit_costs( - orbit_project=wind_cost_results.orbit_project, - h2integrate_config=h2integrate_config, - ) - - # orbit_capex_breakdown["Onshore Substation"] = orbit_project.phases["ElectricalDesign"].onshore_cost # noqa: E501 - # discount ORBIT cost information - for key in orbit_capex_breakdown: - orbit_capex_breakdown[key] = -npf.fv( - h2integrate_config["finance_parameters"]["costing_general_inflation"], - h2integrate_config["project_parameters"]["cost_year"] - - h2integrate_config["finance_parameters"]["discount_years"]["wind"], - 0.0, - orbit_capex_breakdown[key], - ) - - # save ORBIT cost information - ob_df = pd.DataFrame(orbit_capex_breakdown, index=[0]).transpose() - savedir = output_dir / "data/orbit_costs/" - if not savedir.exists(): - savedir.mkdir(parents=True) - ob_df.to_csv( - savedir - / f'orbit_cost_breakdown_lcoh_design{plant_design_number}_incentive{incentive_option}_{h2integrate_config["h2_storage"]["type"]}storage.csv' # noqa: E501 - ) - ############################### - - ###################### Save export system breakdown from ORBIT ################### - - _, orbit_capex_breakdown, wind_capex_multiplier = adjust_orbit_costs( - orbit_project=wind_cost_results.orbit_project, - h2integrate_config=h2integrate_config, - ) - - onshore_substation_costs = ( - wind_cost_results.orbit_project.phases["ElectricalDesign"].onshore_cost - * wind_capex_multiplier - ) - - orbit_capex_breakdown["Export System Installation"] -= onshore_substation_costs - - orbit_capex_breakdown["Onshore Substation and Installation"] = onshore_substation_costs - - # discount ORBIT cost information - for key in orbit_capex_breakdown: - orbit_capex_breakdown[key] = -npf.fv( - h2integrate_config["finance_parameters"]["costing_general_inflation"], - h2integrate_config["project_parameters"]["cost_year"] - - h2integrate_config["finance_parameters"]["discount_years"]["wind"], - 0.0, - orbit_capex_breakdown[key], - ) - - # save ORBIT cost information using directory defined above - ob_df = pd.DataFrame(orbit_capex_breakdown, index=[0]).transpose() - ob_df.to_csv( - savedir - / f'orbit_cost_breakdown_with_onshore_substation_lcoh_design{plant_design_number}_incentive{incentive_option}_{h2integrate_config["h2_storage"]["type"]}storage.csv' # noqa: E501 - ) - - ################################################################################## - if save_plots: - if ( - hasattr(hopp_results["hybrid_plant"], "dispatch_builder") - and hopp_results["hybrid_plant"].battery - ): - savedir = output_dir / "figures/production/" - if not savedir.exists(): - savedir.mkdir(parents=True) - plot_tools.plot_generation_profile( - hopp_results["hybrid_plant"], - start_day=0, - n_days=10, - plot_filename=(savedir / "generation_profile.pdf"), - font_size=14, - power_scale=1 / 1000, - solar_color="r", - wind_color="b", - # wave_color="g", - discharge_color="b", - charge_color="r", - gen_color="g", - price_color="r", - # show_price=False, - ) - else: - print( - "generation profile not plotted because HoppInterface does not have a " - "'dispatch_builder'" - ) - - # save production information - hourly_energy_breakdown = save_energy_flows( - hopp_results["hybrid_plant"], - electrolyzer_physics_results, - solver_results, - hours, - h2_storage_results, - output_dir=output_dir, - ) - - # save hydrogen information - key = "Hydrogen Hourly Production [kg/hr]" - np.savetxt( - output_dir / "h2_usage", - electrolyzer_physics_results["H2_Results"][key], - header="# " + key, - ) - - return annual_energy_breakdown, hourly_energy_breakdown diff --git a/tests/h2integrate/input_files/plant/h2integrate_config.yaml b/tests/h2integrate/input_files/plant/h2integrate_config.yaml deleted file mode 100644 index 544700bb7..000000000 --- a/tests/h2integrate/input_files/plant/h2integrate_config.yaml +++ /dev/null @@ -1,344 +0,0 @@ -site: - mean_windspeed: False - depth: 45 #m - wind_layout: - row_spacing: 7 # Also defined in ORBIT config for offshore layout. H2Integrate config values override the values in ORBIT. - turbine_spacing: 7 # Also defined in ORBIT config for offshore layout. H2Integrate config values override the values in ORBIT. - grid_angle: 0 # wind layout grid angle in degrees where 0 is north, increasing clockwise - row_phase_offset: 0 # wind layout offset of turbines along row from one row to the next -project_parameters: - project_lifetime: 30 - grid_connection: False # option, can be turned on or off - ppa_price: 0.025 # $/kWh based on 2022 land based wind market report (ERCOT area ppa prices) https://www.energy.gov/sites/default/files/2022-08/land_based_wind_market_report_2202.pdf - hybrid_electricity_estimated_cf: 0.492 #should equal 1 if grid_connection = True - financial_analysis_start_year: 2027 - cost_year: 2022 # to match ATB - # installation_time: 0 #36 # months -finance_parameters: - costing_general_inflation: 0.025 # used to adjust modeled costs to cost_year - inflation_rate: 0.025 # based on 2022 ATB - discount_rate: - general: 0.10 # nominal return based on 2022 ATB basline workbook - wind: 0.10 - wave: 0.10 - debt_equity_split: - general: 68.5 # 2022 ATB uses 68.5% debt - wind: 68.5 - wave: 68.5 - property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults - property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults - total_income_tax_rate: 0.257 # 0.257 tax rate in 2022 atb baseline workbook # current federal income tax rate, but proposed 2023 rate is 0.28. No state income tax in Texas - capital_gains_tax_rate: 0.15 # H2FAST default - sales_tax_rate: 0.0 #Verify that a different rate shouldn't be used # minimum total sales tax rate in Corpus Christi https://www.cctexas.com/detail/corpus-christi-type-fund-purpose - does this apply to H2? - debt_interest_rate: - general: 0.06 - wind: 0.06 - wave: 0.06 - debt_type: "Revolving debt" # can be "Revolving debt" or "One time loan". Revolving debt is H2FAST default and leads to much lower LCOH - loan_period: 0 # H2FAST default, not used for revolving debt - cash_onhand_months: 1 # H2FAST default - administrative_expense_percent_of_sales: 0.00 #Check this # percent of sales H2FAST default - depreciation_method: "MACRS" # can be "MACRS" or "Straight line" - MACRS may be better and can reduce LCOH by more than $1/kg and is spec'd in the IRS MACRS schedule https://www.irs.gov/publications/p946#en_US_2020_publink1000107507 - depreciation_period: 5 # years - as specified by the IRS MACRS schedule https://www.irs.gov/publications/p946#en_US_2020_publink1000107507 - depreciation_period_electrolyzer: 7 # based on PEM Electrolysis H2A Production Case Study Documentation estimate of 7 years. also see https://www.irs.gov/publications/p946#en_US_2020_publink1000107507 - discount_years: - wind: 2022 # based on turbine capex value provided to ORBIT from 2022 ATB - wind_and_electrical: 2022 # for ORBIT opex - wave: 2020 # confirmed by Kaitlin Brunik 20240103 - solar: 2022 # TODO check - battery: 2022 # TODO check - platform: 2022 # TODO ask Nick and Charlie - electrical_export_system: 2022 # also from ORBIT, so match wind assumptions. TODO ask Sophie Bradenkamp - desal: 2013 # from code citation: https://www.nrel.gov/docs/fy16osti/66073.pdf - electrolyzer: 2020 # 2020 for singlitico2021, 2016 # for simple h2 cost model in hopp (see https://www.hydrogen.energy.gov/pdfs/19009_h2_production_cost_pem_electrolysis_2019.pdf) ## 2020 # based on IRENA report https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2020/Dec/IRENA_Green_hydrogen_cost_2020.pdf - h2_transport_compressor: 2016 # listed in code header - h2_storage: - pressure_vessel: 2022 # based on readme for Compressed_gas_function - pipe: 2019 # Papadias 2021 - salt_cavern: 2019 # Papadias 2021 - turbine: 2003 # assumed based on Kottenstette 2004 - lined_rock_cavern: 2018 # based on Papadias 2021 and HD SAM - none: 2022 # arbitrary - h2_pipe_array: 2018 # ANL costs - h2_transport_pipeline: 2018 # same model for costs as the h2_pipe_array - wind: - expected_plant_cost: 'none' -electrolyzer: - sizing: - resize_for_enduse: False - size_for: 'BOL' #'BOL' (generous) or 'EOL' (conservative) - hydrogen_dmd: - rating: 180 # MW # 0.9*Plant rating appears near-optimal for 400 MW wind plant with 3 days of underground pipe storage # MW - cluster_rating_MW: 180 - pem_control_type: 'basic' - eol_eff_percent_loss: 10 #eol defined as x% change in efficiency from bol - uptime_hours_until_eol: 77600 #number of 'on' hours until electrolyzer reaches eol - include_degradation_penalty: True #include degradation - turndown_ratio: 0.1 #turndown_ratio = minimum_cluster_power/cluster_rating_MW - electrolyzer_capex: 700 # $/kW conservative 2025 centralized. high 700, low 300 # based on https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2020/Dec/IRENA_Green_hydrogen_cost_2020.pdf - replacement_cost_percent: 0.15 # percent of capex - H2A default case - cost_model: "singlitico2021" # "basic" is a basic cost model based on H2a and HFTO program record for PEM electrolysis. "singlitico2021" uses cost estimates from that paper - -h2_transport_compressor: - outlet_pressure: 68 # bar based on HDSAM -h2_storage_compressor: - output_pressure: 100 # bar (1 bar = 100 kPa) - flow_rate: 89 # kg/hr - energy_rating: 802 # kWe (aka 1 kWh) - mean_days_between_failures: 200 # days - # annual_h2_throughput: 18750 # [kg/yr] -> kg of H2 per year -h2_transport_pipe: - outlet_pressure: 10 # bar - from example in code from Jamie #TODO check this value -h2_storage: - size_capacity_from_demand: - flag: False # If True, then storage is sized to provide steady-state storage - capacity_from_max_on_turbine_storage: False # if True, then days of storage is ignored and storage capacity is based on how much h2 storage fits on the turbines in the plant using Kottenstete 2003. - type: "none" # can be one of ["none", "pipe", "turbine", "pressure_vessel", "salt_cavern", "lined_rock_cavern"] - days: 3 # [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` or `size_capacity_from_demand` is set to True) - -platform: - opex_rate: 0.0111 # % of capex to determine opex (see table 5 in https://www.acm.nl/sites/default/files/documents/study-on-estimation-method-for-additional-efficient-offshore-grid-opex.pdf) - # Modified orbit configuration file for a single platform to carry "X technology" - design_phases: - - FixedPlatformDesign # Register Design Phase - install_phases: - FixedPlatformInstallation: 0 # Register Install Phase - oss_install_vessel: example_heavy_lift_vessel - site: - depth: -1 # site depth [m] (if -1, then will use the full plant depth) - distance: -1 # distance to port [km] (if -1, then will use the full plant distance) - equipment: - tech_required_area: -1. # equipment area [m**2] (-1 will require the input during run) - tech_combined_mass: -1 # equipment mass [t] (-1 will require the input during run) - topside_design_cost: 4500000 # topside design cost [USD] - installation_duration: 14 # time at sea [days] - -policy_parameters: # these should be adjusted for inflation prior to application - order of operations: rate in 1992 $, -#then prevailing wage multiplier if applicable, then inflation - option1: # base # no policy included ---> see files/task1/regulation and policy revue/ page 4 of 13 middle - read this - # and look at assumptions - electricity_itc: 0 - electricity_ptc: 0 - h2_ptc: 0 - h2_storage_itc: 0 - option2: # base credit levels with H2 - electricity_itc: 0 - electricity_ptc: 0.003 # $0.003/kW (this is base, see inflation adjustment in option 3) - h2_ptc: 0.6 # $0.60/kg h2 produced - assumes net zero but not meeting prevailing wage requirements - does this need to be - # adjusted for inflation from 2022 dollars to claim date, probably constant after claim date? - h2_storage_itc: 0.06 - option3: # same as option 5, but assuming prevailing wages are met --> 5x multiplier on both PTCs - electricity_itc: 0 - electricity_ptc: 0.015 # $/kWh 1992 dollars - h2_ptc: 3.00 # $/kg 2022 dollars - do not adjust for inflation - h2_storage_itc: 0.3 - # bonus options, option 5 and 6 but ITC equivalents - option4: # prevailing wages not met - electricity_itc: 0.06 # %/100 capex - electricity_ptc: 0.00 # $/kW 1992 dollars - h2_ptc: 0.6 # $0.60/kg produced 2022 dollars - assumes net zero but not meeting prevailing wage requirements - does this need to be - # do not adjust for inflation, probably constant after claim date? - h2_storage_itc: 0.06 - option5: # prevailing wages met - electricity_itc: 0.30 # %/100 capex - electricity_ptc: 0.0 # $/kWh 1992 dollars - h2_ptc: 3.00 # $/kg of h2 produced 2022 dollars - do adjust for inflation every year applied and until application year - h2_storage_itc: 0.3 - option6: # assumes prevailing wages are met, and includes 10% bonus credit of domestic content (100% of steel and iron - # and mfg. components from the US) - electricity_itc: 0.40 # %/100 capex - electricity_ptc: 0.0 # $/kWh 1992 dollars - h2_ptc: 3.00 # $/kg of h2 produced 2022 dollars - do adjust for inflation every year applied and until application year - h2_storage_itc: 0.4 - option7: # assumes prevailing wages are met, and includes 10% bonus credit of domestic content (100% of steel and iron - # and mfg. components from the US) - electricity_itc: 0.0 # %/100 capex - electricity_ptc: 0.0165 # $/kWh 1992 dollars (0.015*1.1) - h2_ptc: 3.00 # $/kg of h2 produced 2022 dollars - do adjust for inflation every year applied and until application year - # you can elect itc_for_h2 in leu of the h2_ptc - this choice is independent of the other tech credit selections - # 6% or %50 for itc_for_h2 - h2_storage_itc: 0.5 - -plant_design: - scenario0: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "platform" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario1: - electrolyzer_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - transportation: "hvdc" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario2: - electrolyzer_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - transportation: "hvdc" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "platform" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario3: - electrolyzer_location: "turbine" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "turbine" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario4: - electrolyzer_location: "turbine" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "platform" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario5: - electrolyzer_location: "turbine" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario6: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "platform" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario7: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario8: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "hvdc+pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario9: - electrolyzer_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "onshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario10: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "platform" # can be one of ["none", "onshore", "platform"] - battery_location: "platform" # can be one of ["none", "onshore", "platform"] - scenario11: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "platform" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - -lca_config: - run_lca: False #True - electrolyzer_type: pem #alkaline, soec - feedstock_water_type: ground #desal, surface - cambium: #cambium API argument, see cambium_data.py for additional argument options - project_uuid: '0f92fe57-3365-428a-8fe8-0afc326b3b43' - scenario: 'Mid-case with 100% decarbonization by 2035' - location_type: 'GEA Regions 2023' - time_type: 'hourly' - -opt_options: - opt_flag: True - general: - folder_output: "output" - fname_output: "test_run_h2integrate_optimization" - design_variables: - electrolyzer_rating_kw: - flag: False - lower: 1000.0 - upper: 400000.0 - units: "kW" - pv_capacity_kw: - flag: False - lower: 1000.0 - upper: 10000.0 - units: "kW" - wave_capacity_kw: - flag: False - lower: 1000.0 - upper: 1500000.0 - units: "kW*h" - battery_capacity_kw: - flag: False - lower: 1000.0 - upper: 10000.0 - units: "kW" - battery_capacity_kwh: - flag: False - lower: 1000.0 - upper: 10000.0 - units: "kW*h" - turbine_x: - flag: False - lower: 0.0 - upper: 20000.0 - units: "m" - turbine_y: - flag: False - lower: 0.0 - upper: 20000.0 - units: "m" - constraints: - turbine_spacing: - flag: False - lower: 0.0 - boundary_distance: - flag: False - lower: 0.0 - pv_to_platform_area_ratio: - flag: True - upper: 1.0 # relative size of solar pv area to platform area - user: {} - merit_figure: "lcoh" - merit_figure_user: - name: "lcoh" - max_flag: False - ref: 1.0 # value of objective that scales to 1.0 - driver: - optimization: - flag: True - solver: "SNOPT" - tol: 1E-6 - max_major_iter: 50 - max_minor_iter: 500 - # time_limit: 10 # (sec) optional - # "hist_file_name: "snopt_history.txt", # optional - verify_level: 0 # optional - step_calc: None - form: "central" # type of finite differences to use, can be one of ["forward", "backward", "central"] - debug_print: False - gradient_method: "openmdao" - step_size_study: - flag: False - design_of_experiments: - flag: False - run_parallel: False - generator: FullFact # [Uniform, FullFact, PlackettBurman, BoxBehnken, LatinHypercube] - num_samples: 5 # Number of samples to evaluate model at (Uniform and LatinHypercube only) - seed: 2 - levels: 5 # Number of evenly spaced levels between each design variable lower and upper bound (FullFactorial only) - criterion: None # [None, center, c, maximin, m, centermaximin, cm, correelation, corr] - iterations: 1 - debug_print: False - recorder: - flag: True - file_name: "recorder_doe_pv.sql" - includes: ["lcoe", "platform_area", "pv_area", "pv_platform_ratio"] diff --git a/tests/h2integrate/input_files/plant/h2integrate_config_onshore.yaml b/tests/h2integrate/input_files/plant/h2integrate_config_onshore.yaml deleted file mode 100644 index 3413cf6cb..000000000 --- a/tests/h2integrate/input_files/plant/h2integrate_config_onshore.yaml +++ /dev/null @@ -1,368 +0,0 @@ -site: - mean_windspeed: False - depth: 45 #m - wind_layout: - row_spacing: 7 # Also defined in ORBIT config for offshore layout. H2Integrate config values override the values in ORBIT. - turbine_spacing: 7 # Also defined in ORBIT config for offshore layout. H2Integrate config values override the values in ORBIT. - grid_angle: 0 # wind layout grid angle in degrees where 0 is north, increasing clockwise - row_phase_offset: 0 # wind layout offset of turbines along row from one row to the next -project_parameters: - project_lifetime: 30 - grid_connection: False # option, can be turned on or off - ppa_price: 0.025 # $/kWh based on 2022 land based wind market report (ERCOT area ppa prices) https://www.energy.gov/sites/default/files/2022-08/land_based_wind_market_report_2202.pdf - hybrid_electricity_estimated_cf: 0.492 #should equal 1 if grid_connection = True - financial_analysis_start_year: 2027 - cost_year: 2022 # to match ATB - installation_time: 36 # months -finance_parameters: - costing_general_inflation: 0.025 # used to adjust modeled costs to cost_year - inflation_rate: 0.025 # based on 2022 ATB - discount_rate: 0.10 # nominal return based on 2022 ATB basline workbook - debt_equity_split: 68.5 # 2022 ATB uses 68.5% debt - property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults - property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults - total_income_tax_rate: 0.257 # 0.257 tax rate in 2022 atb baseline workbook # current federal income tax rate, but proposed 2023 rate is 0.28. No state income tax in Texas - capital_gains_tax_rate: 0.15 # H2FAST default - sales_tax_rate: 0.0 #Verify that a different rate shouldn't be used # minimum total sales tax rate in Corpus Christi https://www.cctexas.com/detail/corpus-christi-type-fund-purpose - does this apply to H2? - debt_interest_rate: 0.06 - debt_type: "Revolving debt" # can be "Revolving debt" or "One time loan". Revolving debt is H2FAST default and leads to much lower LCOH - loan_period: 0 # H2FAST default, not used for revolving debt - cash_onhand_months: 1 # H2FAST default - administrative_expense_percent_of_sales: 0.00 #Check this # percent of sales H2FAST default - depreciation_method: "MACRS" # can be "MACRS" or "Straight line" - MACRS may be better and can reduce LCOH by more than $1/kg and is spec'd in the IRS MACRS schedule https://www.irs.gov/publications/p946#en_US_2020_publink1000107507 - depreciation_period: 5 # years - as specified by the IRS MACRS schedule https://www.irs.gov/publications/p946#en_US_2020_publink1000107507 - depreciation_period_electrolyzer: 7 # based on PEM Electrolysis H2A Production Case Study Documentation estimate of 7 years. also see https://www.irs.gov/publications/p946#en_US_2020_publink1000107507 - discount_years: - wind: 2022 # based on turbine capex value provided to ORBIT from 2022 ATB - wind_and_electrical: 2022 # for ORBIT opex - wave: 2020 # confirmed by Kaitlin Brunik 20240103 - solar: 2022 # TODO check - battery: 2022 # TODO check - platform: 2022 # TODO ask Nick and Charlie - electrical_export_system: 2022 # also from ORBIT, so match wind assumptions. TODO ask Sophie Bradenkamp - desal: 2013 # from code citation: https://www.nrel.gov/docs/fy16osti/66073.pdf - electrolyzer: 2020 # 2020 for singlitico2021, 2016 # for simple h2 cost model in hopp (see https://www.hydrogen.energy.gov/pdfs/19009_h2_production_cost_pem_electrolysis_2019.pdf) ## 2020 # based on IRENA report https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2020/Dec/IRENA_Green_hydrogen_cost_2020.pdf - h2_transport_compressor: 2016 # listed in code header - h2_storage: - pressure_vessel: 2022 # based on readme for Compressed_gas_function - pipe: 2019 # Papadias 2021 - salt_cavern: 2019 # Papadias 2021 - turbine: 2003 # assumed based on Kottenstette 2004 - lined_rock_cavern: 2018 # based on Papadias 2021 and HD SAM - none: 2022 # arbitrary - h2_pipe_array: 2018 # ANL costs - h2_transport_pipeline: 2018 # same model for costs as the h2_pipe_array - wind: - expected_plant_cost: 'none' -electrolyzer: - sizing: - resize_for_enduse: False - size_for: 'BOL' #'BOL' (generous) or 'EOL' (conservative) - hydrogen_dmd: - rating: 180 # MW # 0.9*Plant rating appears near-optimal for 400 MW wind plant with 3 days of underground pipe storage # MW - cluster_rating_MW: 180 - pem_control_type: 'basic' - eol_eff_percent_loss: 10 #eol defined as x% change in efficiency from bol - uptime_hours_until_eol: 77600 #number of 'on' hours until electrolyzer reaches eol - include_degradation_penalty: True #include degradation - turndown_ratio: 0.1 #turndown_ratio = minimum_cluster_power/cluster_rating_MW - electrolyzer_capex: 700 # $/kW conservative 2025 centralized. high 700, low 300 # based on https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2020/Dec/IRENA_Green_hydrogen_cost_2020.pdf - replacement_cost_percent: 0.15 # percent of capex - H2A default case - cost_model: "singlitico2021" # "basic" is a basic cost model based on H2a and HFTO program record for PEM electrolysis. "singlitico2021" uses cost estimates from that paper - -h2_transport_compressor: - outlet_pressure: 68 # bar based on HDSAM -h2_storage_compressor: - output_pressure: 100 # bar (1 bar = 100 kPa) - flow_rate: 89 # kg/hr - energy_rating: 802 # kWe (aka 1 kWh) - mean_days_between_failures: 200 # days - # annual_h2_throughput: 18750 # [kg/yr] -> kg of H2 per year -h2_transport_pipe: - outlet_pressure: 10 # bar - from example in code from Jamie #TODO check this value -h2_storage: - size_capacity_from_demand: - flag: False # If True, then storage is sized to provide steady-state storage - capacity_from_max_on_turbine_storage: False # if True, then days of storage is ignored and storage capacity is based on how much h2 storage fits on the turbines in the plant using Kottenstete 2003. - type: "none" # can be one of ["none", "pipe", "turbine", "pressure_vessel", "salt_cavern", "lined_rock_cavern"] - days: 3 # [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` is set to True) - -policy_parameters: # these should be adjusted for inflation prior to application - order of operations: rate in 1992 $, - #then prevailing wage multiplier if applicable, then inflation - option1: # base # no policy included ---> see files/task1/regulation and policy revue/ page 4 of 13 middle - read this - # and look at assumptions - electricity_itc: 0 - electricity_ptc: 0 - h2_ptc: 0 - h2_storage_itc: 0 - option2: # base credit levels with H2 - electricity_itc: 0 - electricity_ptc: 0.003 # $0.003/kW (this is base, see inflation adjustment in option 3) - h2_ptc: 0.6 # $0.60/kg h2 produced - assumes net zero but not meeting prevailing wage requirements - does this need to be - # adjusted for inflation from 2022 dollars to claim date, probably constant after claim date? - h2_storage_itc: 0.06 - option3: # same as option 5, but assuming prevailing wages are met --> 5x multiplier on both PTCs - electricity_itc: 0 - electricity_ptc: 0.015 # $/kWh 1992 dollars - h2_ptc: 3.00 # $/kg 2022 dollars - do not adjust for inflation - h2_storage_itc: 0.3 - # bonus options, option 5 and 6 but ITC equivalents - option4: # prevailing wages not met - electricity_itc: 0.06 # %/100 capex - electricity_ptc: 0.00 # $/kW 1992 dollars - h2_ptc: 0.6 # $0.60/kg produced 2022 dollars - assumes net zero but not meeting prevailing wage requirements - does this need to be - # do not adjust for inflation, probably constant after claim date? - h2_storage_itc: 0.06 - option5: # prevailing wages met - electricity_itc: 0.30 # %/100 capex - electricity_ptc: 0.0 # $/kWh 1992 dollars - h2_ptc: 3.00 # $/kg of h2 produced 2022 dollars - do adjust for inflation every year applied and until application year - h2_storage_itc: 0.3 - option6: # assumes prevailing wages are met, and includes 10% bonus credit of domestic content (100% of steel and iron - # and mfg. components from the US) - electricity_itc: 0.40 # %/100 capex - electricity_ptc: 0.0 # $/kWh 1992 dollars - h2_ptc: 3.00 # $/kg of h2 produced 2022 dollars - do adjust for inflation every year applied and until application year - h2_storage_itc: 0.4 - option7: # assumes prevailing wages are met, and includes 10% bonus credit of domestic content (100% of steel and iron - # and mfg. components from the US) - electricity_itc: 0.0 # %/100 capex - electricity_ptc: 0.0165 # $/kWh 1992 dollars (0.015*1.1) - h2_ptc: 3.00 # $/kg of h2 produced 2022 dollars - do adjust for inflation every year applied and until application year - # you can elect itc_for_h2 in leu of the h2_ptc - this choice is independent of the other tech credit selections - # 6% or %50 for itc_for_h2 - h2_storage_itc: 0.5 - -plant_design: - scenario0: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "platform" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario1: - electrolyzer_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - transportation: "hvdc" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario2: - electrolyzer_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - transportation: "hvdc" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "platform" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario3: - electrolyzer_location: "turbine" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "turbine" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario4: - electrolyzer_location: "turbine" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "platform" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario5: - electrolyzer_location: "turbine" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario6: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "platform" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario7: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario8: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "hvdc+pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - scenario9: - electrolyzer_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "onshore" # can be one of ["onshore", "offshore"] - pv_location: "none" # can be one of ["none", "onshore", "platform"] - battery_location: "none" # can be one of ["none", "onshore", "platform"] - scenario10: - electrolyzer_location: "platform" # can be one of ["onshore", "turbine", "platform"] - transportation: "pipeline" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - pv_location: "platform" # can be one of ["none", "onshore", "platform"] - battery_location: "platform" # can be one of ["none", "onshore", "platform"] - wind_location: "offshore" # can be one of ["onshore", "offshore"] - scenario11: - electrolyzer_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "onshore" # can be one of ["onshore", "offshore"] - pv_location: "onshore" # can be one of ["none", "onshore", "platform"] - battery_location: "onshore" # can be one of ["none", "onshore", "platform"] - scenario12: - electrolyzer_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - transportation: "none" # can be one of ["hvdc", "pipeline", "none", hvdc+pipeline, "colocated"] - h2_storage_location: "onshore" # can be one of ["onshore", "turbine", "platform"] - wind_location: "onshore" # can be one of ["onshore", "offshore"] - pv_location: "onshore" # can be one of ["none", "onshore", "platform"] - battery_location: "onshore" # can be one of ["none", "onshore", "platform"] - -steel: - capacity: - input_capacity_factor_estimate: 0.9 - costs: - operational_year: 2035 - o2_heat_integration: false - feedstocks: - oxygen_market_price: 0.03 - natural_gas_prices: - "2035": 3.76232 - "2036": 3.776032 - "2037": 3.812906 - "2038": 3.9107960000000004 - "2039": 3.865776 - "2040": 3.9617400000000003 - "2041": 4.027136 - "2042": 4.017166 - "2043": 3.9715339999999997 - "2044": 3.924314 - "2045": 3.903287 - "2046": 3.878192 - "2047": 3.845413 - "2048": 3.813366 - "2049": 3.77735 - "2050": 3.766164 - "2051": 3.766164 - "2052": 3.766164 - "2053": 3.766164 - "2054": 3.766164 - "2055": 3.766164 - "2056": 3.766164 - "2057": 3.766164 - "2058": 3.766164 - "2059": 3.766164 - "2060": 3.766164 - "2061": 3.766164 - "2062": 3.766164 - "2063": 3.766164 - "2064": 3.766164 - finances: - plant_life: 30 - grid_prices: - "2035": 89.42320514456621 - "2036": 89.97947569251141 - "2037": 90.53574624045662 - "2038": 91.09201678840184 - "2039": 91.64828733634704 - "2040": 92.20455788429224 - "2041": 89.87291235917809 - "2042": 87.54126683406393 - "2043": 85.20962130894978 - "2044": 82.87797578383562 - "2045": 80.54633025872147 - "2046": 81.38632144593608 - "2047": 82.22631263315068 - "2048": 83.0663038203653 - "2049": 83.90629500757991 - "2050": 84.74628619479452 - "2051": 84.74628619479452 - "2052": 84.74628619479452 - "2053": 84.74628619479452 - "2054": 84.74628619479452 - "2055": 84.74628619479452 - "2056": 84.74628619479452 - "2057": 84.74628619479452 - "2058": 84.74628619479452 - "2059": 84.74628619479452 - "2060": 84.74628619479452 - "2061": 84.74628619479452 - "2062": 84.74628619479452 - "2063": 84.74628619479452 - "2064": 84.74628619479452 - - # Additional parameters passed to ProFAST - financial_assumptions: - "total income tax rate": 0.2574 - "capital gains tax rate": 0.15 - "leverage after tax nominal discount rate": 0.10893 - "debt equity ratio of initial financing": 0.624788 - "debt interest rate": 0.050049 - -ammonia: - capacity: - input_capacity_factor_estimate: 0.9 - costs: - feedstocks: - electricity_cost: 89.42320514456621 - hydrogen_cost: 4.2986685034417045 - cooling_water_cost: 0.00291 - iron_based_catalyst_cost: 23.19977341 - oxygen_cost: 0 - finances: - plant_life: 30 - grid_prices: - "2035": 89.42320514456621 - "2036": 89.97947569251141 - "2037": 90.53574624045662 - "2038": 91.09201678840184 - "2039": 91.64828733634704 - "2040": 92.20455788429224 - "2041": 89.87291235917809 - "2042": 87.54126683406393 - "2043": 85.20962130894978 - "2044": 82.87797578383562 - "2045": 80.54633025872147 - "2046": 81.38632144593608 - "2047": 82.22631263315068 - "2048": 83.0663038203653 - "2049": 83.90629500757991 - "2050": 84.74628619479452 - "2051": 84.74628619479452 - "2052": 84.74628619479452 - "2053": 84.74628619479452 - "2054": 84.74628619479452 - "2055": 84.74628619479452 - "2056": 84.74628619479452 - "2057": 84.74628619479452 - "2058": 84.74628619479452 - "2059": 84.74628619479452 - "2060": 84.74628619479452 - "2061": 84.74628619479452 - "2062": 84.74628619479452 - "2063": 84.74628619479452 - "2064": 84.74628619479452 - - # Additional parameters passed to ProFAST - financial_assumptions: - "total income tax rate": 0.2574 - "capital gains tax rate": 0.15 - "leverage after tax nominal discount rate": 0.10893 - "debt equity ratio of initial financing": 0.624788 - "debt interest rate": 0.050049 - -lca_config: - run_lca: False #True - electrolyzer_type: pem #alkaline, soec - feedstock_water_type: ground #desal, surface - cambium: #cambium API argument, see cambium_data.py for additional argument options - project_uuid: '0f92fe57-3365-428a-8fe8-0afc326b3b43' - scenario: 'Mid-case with 100% decarbonization by 2035' - location_type: 'GEA Regions 2023' - time_type: 'hourly' diff --git a/tests/h2integrate/test_finance.py b/tests/h2integrate/test_finance.py deleted file mode 100644 index ed938d0c7..000000000 --- a/tests/h2integrate/test_finance.py +++ /dev/null @@ -1,248 +0,0 @@ -import os -from pathlib import Path - -import pytest -from pytest import approx - -from h2integrate import EXAMPLE_DIR -from h2integrate.core.dict_utils import update_defaults -from h2integrate.tools.profast_tools import ( - run_profast, - create_years_of_operation, - create_and_populate_profast, -) -from h2integrate.core.h2integrate_model import H2IntegrateModel -from h2integrate.core.inputs.validation import load_yaml - - -def test_calc_financial_parameter_weighted_average_by_capex(subtests): - from h2integrate.tools.eco.finance import calc_financial_parameter_weighted_average_by_capex - - with subtests.test("single value"): - h2integrate_config = {"finance_parameters": {"discount_rate": 0.1}} - - assert ( - calc_financial_parameter_weighted_average_by_capex( - "discount_rate", h2integrate_config=h2integrate_config, capex_breakdown={} - ) - == 0.1 - ) - - with subtests.test("weighted average value - all values specified"): - h2integrate_config = {"finance_parameters": {"discount_rate": {"wind": 0.05, "solar": 0.1}}} - - capex_breakdown = {"wind": 1e9, "solar": 1e8} - - return_value = calc_financial_parameter_weighted_average_by_capex( - "discount_rate", h2integrate_config=h2integrate_config, capex_breakdown=capex_breakdown - ) - - assert return_value == approx(0.05454545454545454) - - with subtests.test("weighted average value - not all values specified"): - h2integrate_config = { - "finance_parameters": {"discount_rate": {"wind": 0.05, "solar": 0.1, "general": 0.15}} - } - - capex_breakdown = {"wind": 1e9, "solar": 1e8, "electrolyzer": 3e8, "battery": 2e8} - - return_value = calc_financial_parameter_weighted_average_by_capex( - "discount_rate", h2integrate_config=h2integrate_config, capex_breakdown=capex_breakdown - ) - - assert return_value == approx(0.084375) - - -def test_variable_om_no_escalation(subtests): - os.chdir(EXAMPLE_DIR / "02_texas_ammonia") - - inflation_rate = 0.0 - # Create a H2Integrate model - model = H2IntegrateModel(Path.cwd() / "02_texas_ammonia.yaml") - - # Run the model - model.run() - - model.post_process() - - with subtests.test("Check original LCOH with zero escalation"): - assert ( - pytest.approx(model.prob.get_val("finance_subgroup_hydrogen.LCOH")[0], rel=1e-3) - == 3.9705799099 - ) - - outputs_dir = Path.cwd() / "outputs" - - yaml_fpath = outputs_dir / "profast_output_hydrogen_config.yaml" - - pf_dict = load_yaml(yaml_fpath) - - plant_life = int(pf_dict["params"]["operating life"]) - - years_of_operation = create_years_of_operation( - plant_life, - pf_dict["params"]["analysis start year"], - pf_dict["params"]["installation months"], - ) - - pf_dict = update_defaults(pf_dict, "escalation", inflation_rate) - pf_dict["params"].update({"general inflation rate": inflation_rate}) - - water_cost_per_gal = 0.003 # [$/gal] - gal_water_pr_kg_H2 = 3.8 # [gal H2O / kg-H2] - - # calculate annual water cost - annual_h2_kg = pf_dict["params"]["capacity"] * pf_dict["params"]["long term utilization"] * 365 - annual_water_gal = annual_h2_kg * gal_water_pr_kg_H2 - annual_water_cost_USD_per_kg = annual_water_gal * water_cost_per_gal / annual_h2_kg - water_feedstock_entry = { - "Water": { - "escalation": inflation_rate, - "unit": "$/kg", - "usage": 1.0, - "cost": annual_water_cost_USD_per_kg, - } - } - - # update feedstock entry - pf_dict["feedstocks"].update(water_feedstock_entry) - - # run profast for feedstock cost as a scalar - pf = create_and_populate_profast(pf_dict) - sol_scalar, summary_scalar, price_breakdown_scalar = run_profast(pf) - - with subtests.test( - "Check variable o&m as scalar LCOH against original LCOH with zero escalation" - ): - assert sol_scalar["price"] > model.prob.get_val("finance_subgroup_hydrogen.LCOH")[0] - - with subtests.test("Check variable o&m as scalar LCOH with zero escalation value"): - assert pytest.approx(sol_scalar["price"], rel=1e-3) == 3.98205152215 - - # create water cost entry as array - annual_water_cost_USD_per_year = [annual_water_cost_USD_per_kg] * plant_life - annual_water_cost_USD_per_year_dict = dict( - zip(years_of_operation, annual_water_cost_USD_per_year) - ) - water_feedstock_entry = { - "Water": { - "escalation": inflation_rate, - "unit": "$/kg", - "usage": 1.0, - "cost": annual_water_cost_USD_per_year_dict, - } - } - - # update feedstock entry - pf_dict["feedstocks"].update(water_feedstock_entry) - - pf = create_and_populate_profast(pf_dict) - sol_list, summary_list, price_breakdown_list = run_profast(pf) - with subtests.test( - "Check variable o&m as array LCOH against original LCOH with zero escalation" - ): - assert sol_list["price"] > model.prob.get_val("finance_subgroup_hydrogen.LCOH")[0] - - with subtests.test("Check variable o&m as array LCOH with zero escalation value"): - assert pytest.approx(sol_list["price"], rel=1e-3) == 3.98205152215 - - with subtests.test( - "Check variable o&m as scalar and as array have same LCOH with zero escalation" - ): - assert pytest.approx(sol_list["price"], rel=1e-6) == sol_scalar["price"] - - -def test_variable_om_with_escalation(subtests): - os.chdir(EXAMPLE_DIR / "02_texas_ammonia") - - inflation_rate = 0.025 - # Create a H2Integrate model - model = H2IntegrateModel(Path.cwd() / "02_texas_ammonia.yaml") - - # Run the model - model.run() - - outputs_dir = Path.cwd() / "outputs" - - yaml_fpath = outputs_dir / "profast_output_hydrogen_config.yaml" - - # load the profast dictionary - pf_dict = load_yaml(yaml_fpath) - - plant_life = int(pf_dict["params"]["operating life"]) - - years_of_operation = create_years_of_operation( - plant_life, - pf_dict["params"]["analysis start year"], - pf_dict["params"]["installation months"], - ) - - # update the inflation rate - pf_dict = update_defaults(pf_dict, "escalation", inflation_rate) - pf_dict["params"].update({"general inflation rate": inflation_rate}) - - # rerun profast without variable o&m costs - pf = create_and_populate_profast(pf_dict) - sol_init, summary_init, price_breakdown_init = run_profast(pf) - - with subtests.test("Check original LCOH with escalation"): - assert pytest.approx(sol_init["price"], rel=1e-3) == 2.9981730 - - # calculate annual water cost - water_cost_per_gal = 0.003 # [$/gal] - gal_water_pr_kg_H2 = 3.8 # [gal H2O / kg-H2] - annual_h2_kg = pf_dict["params"]["capacity"] * pf_dict["params"]["long term utilization"] * 365 - annual_water_gal = annual_h2_kg * gal_water_pr_kg_H2 - - # calculate water cost per kg H2 - annual_water_cost_USD_per_kg = annual_water_gal * water_cost_per_gal / annual_h2_kg - water_feedstock_entry = { - "Water": { - "escalation": inflation_rate, - "unit": "$/kg", - "usage": 1.0, - "cost": annual_water_cost_USD_per_kg, - } - } - - # update feedstock entry - pf_dict["feedstocks"].update(water_feedstock_entry) - - # run profast for feedstock cost as a scalar - pf = create_and_populate_profast(pf_dict) - sol_scalar, summary_scalar, price_breakdown_scalar = run_profast(pf) - - with subtests.test("Check variable o&m as scalar LCOH against original LCOH with escalation"): - assert sol_scalar["price"] > sol_init["price"] - - with subtests.test("Check variable o&m as scalar LCOH with escalation value"): - assert pytest.approx(sol_scalar["price"], rel=1e-3) == 3.00964412171 - - # calculate water cost per kg-H2 and format for costs per year - annual_water_cost_USD_per_year = [annual_water_cost_USD_per_kg] * plant_life - annual_water_cost_USD_per_year_dict = dict( - zip(years_of_operation, annual_water_cost_USD_per_year) - ) - water_feedstock_entry = { - "Water": { - "escalation": inflation_rate, - "unit": "$/kg", - "usage": 1.0, - "cost": annual_water_cost_USD_per_year_dict, - } - } - - # update feedstock entry - pf_dict["feedstocks"].update(water_feedstock_entry) - - # run profast for feedstock cost as an array - pf = create_and_populate_profast(pf_dict) - sol_list, summary_list, price_breakdown_list = run_profast(pf) - with subtests.test("Check variable o&m as array LCOH against original LCOH with escalation"): - assert sol_list["price"] > sol_init["price"] - - with subtests.test("Check variable o&m as array LCOH with escalation value"): - assert pytest.approx(sol_list["price"], rel=1e-3) == 3.0062575558 - - with subtests.test("Check variable o&m as array LCOH is less than variable o&m as scalar LCOH"): - assert sol_scalar["price"] > sol_list["price"] diff --git a/tests/h2integrate/test_h2integrate_utilities.py b/tests/h2integrate/test_h2integrate_utilities.py deleted file mode 100644 index 8ce345ea1..000000000 --- a/tests/h2integrate/test_h2integrate_utilities.py +++ /dev/null @@ -1,43 +0,0 @@ -from pytest import raises - -from h2integrate.tools.eco.utilities import ceildiv, visualize_plant - - -def test_visualize_plant(subtests): - with subtests.test("'visualize_plant()' only works with the 'floris' wind model"): - hopp_config = {"technologies": {"wind": {"model_name": "pysam"}}} - with raises(NotImplementedError, match="only works with the 'floris' wind model"): - visualize_plant( - hopp_config, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - None, - ) - - -def test_ceildiv(subtests): - with subtests.test("ceildiv"): - a = 8 - b = 3 - - assert ceildiv(a, b) == 3 - - with subtests.test("ceildiv with one negative value"): - a = 8 - b = -3 - - assert ceildiv(a, b) == -2 - - with subtests.test("ceildiv with two negative values"): - a = -8 - b = -3 - - assert ceildiv(a, b) == 3 diff --git a/tests/h2integrate/test_hydrogen/test_PEM_costs_Singlitico_model.py b/tests/h2integrate/test_hydrogen/test_PEM_costs_Singlitico_model.py deleted file mode 100644 index 6919c9943..000000000 --- a/tests/h2integrate/test_hydrogen/test_PEM_costs_Singlitico_model.py +++ /dev/null @@ -1,79 +0,0 @@ -import numpy as np -from pytest import approx - -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_costs_Singlitico_model import ( - PEMCostsSingliticoModel, -) - - -TOL = 1e-3 - -BASELINE = np.array( - [ - # onshore, [capex, opex] - [ - [50.7105172052493, 1.2418205567631722], - ], - # offshore, [capex, opex] - [ - [67.44498788298158, 2.16690312809502], - ], - ] -) - - -class TestPEMCostsSingliticoModel: - def test_calc_capex(self): - P_elec = 0.1 # [GW] - RC_elec = 700 # [MUSD/GW] - - # test onshore capex - pem_onshore = PEMCostsSingliticoModel(elec_location=0) - capex_onshore = pem_onshore.calc_capex(P_elec, RC_elec) - - assert capex_onshore == approx(BASELINE[0][0][0], TOL) - - # test offshore capex - pem_offshore = PEMCostsSingliticoModel(elec_location=1) - capex_offshore = pem_offshore.calc_capex(P_elec, RC_elec) - - assert capex_offshore == approx(BASELINE[1][0][0], TOL) - - def test_calc_opex(self): - P_elec = 0.1 # [GW] - capex_onshore = BASELINE[0][0][0] - capex_offshore = BASELINE[1][0][0] - - # test onshore opex - pem_onshore = PEMCostsSingliticoModel(elec_location=0) - opex_onshore = pem_onshore.calc_opex(P_elec, capex_onshore) - - assert opex_onshore == approx(BASELINE[0][0][1], TOL) - - # test offshore opex - pem_offshore = PEMCostsSingliticoModel(elec_location=1) - opex_offshore = pem_offshore.calc_opex(P_elec, capex_offshore) - - assert opex_offshore == approx(BASELINE[1][0][1], TOL) - - def test_run(self): - P_elec = 0.1 # [GW] - RC_elec = 700 # [MUSD/GW] - - # test onshore opex - pem_onshore = PEMCostsSingliticoModel(elec_location=0) - capex_onshore, opex_onshore = pem_onshore.run(P_elec, RC_elec) - - assert capex_onshore == approx(BASELINE[0][0][0], TOL) - assert opex_onshore == approx(BASELINE[0][0][1], TOL) - - # test offshore opex - pem_offshore = PEMCostsSingliticoModel(elec_location=1) - capex_offshore, opex_offshore = pem_offshore.run(P_elec, RC_elec) - - assert capex_offshore == approx(BASELINE[1][0][0], TOL) - assert opex_offshore == approx(BASELINE[1][0][1], TOL) - - -if __name__ == "__main__": - test_set = TestPEMCostsSingliticoModel() diff --git a/tests/h2integrate/test_hydrogen/test_PEM_costs_custom.py b/tests/h2integrate/test_hydrogen/test_PEM_costs_custom.py deleted file mode 100644 index 675fe5248..000000000 --- a/tests/h2integrate/test_hydrogen/test_PEM_costs_custom.py +++ /dev/null @@ -1,24 +0,0 @@ -from pytest import approx - -from h2integrate.simulation.technologies.hydrogen.electrolysis.PEM_costs_custom import ( - calc_custom_electrolysis_capex_fom, -) - - -TOL = 1e-6 - -electrolyzer_size_MW = 1 -electrolyzer_size_kW = electrolyzer_size_MW * 1e3 -fom_usd_pr_kW = 10.0 -capex_usd_pr_kW = 15.0 -elec_config = {"electrolyzer_capex": capex_usd_pr_kW, "fixed_om_per_kw": fom_usd_pr_kW} - - -def test_custom_capex(): - capex, fom = calc_custom_electrolysis_capex_fom(electrolyzer_size_kW, elec_config) - assert capex == approx(capex_usd_pr_kW * electrolyzer_size_kW, TOL) - - -def test_custom_fixed_om(): - capex, fom = calc_custom_electrolysis_capex_fom(electrolyzer_size_kW, elec_config) - assert fom == approx(fom_usd_pr_kW * electrolyzer_size_kW, TOL) diff --git a/tests/h2integrate/test_hydrogen/test_RO_desal.py b/tests/h2integrate/test_hydrogen/test_RO_desal.py deleted file mode 100644 index 218284732..000000000 --- a/tests/h2integrate/test_hydrogen/test_RO_desal.py +++ /dev/null @@ -1,72 +0,0 @@ -from pytest import approx - -from h2integrate.simulation.technologies.hydrogen.desal.desal_model_eco import RO_desal_eco - - -# Test values are based on hand calculations - - -class TestRODesal: - rel_tol = 1e-2 - - freshwater_needed = 10000 # [kg/hr] - - saltwater = RO_desal_eco(freshwater_needed, "Seawater") - brackish = RO_desal_eco(freshwater_needed, "Brackish") - - def test_capacity_m3_per_hr(self): - assert self.saltwater[0] == approx(10.03, rel=1e-5) - assert self.brackish[0] == approx(10.03, rel=1e-5) - - def test_feedwater(self): - assert self.saltwater[1] == approx(20.06, rel=1e-5) - assert self.brackish[1] == approx(13.37, rel=1e-3) - - def test_power(self): - assert self.saltwater[2] == approx(40.12, rel=1e-5) - assert self.brackish[2] == approx(15.04, rel=1e-3) - - def test_capex(self): - assert self.saltwater[3] == approx(91372, rel=1e-2) - assert self.brackish[3] == approx(91372, rel=1e-2) - - def test_opex(self): - assert self.saltwater[4] == approx(13447, rel=1e-2) - assert self.brackish[4] == approx(13447, rel=1e-2) - - def test_RO_Desal_Seawater(self): - """Test Seawater RO Model""" - outputs = RO_desal_eco(freshwater_kg_per_hr=997, salinity="Seawater") - RO_desal_mass = outputs[5] - RO_desal_footprint = outputs[6] - assert approx(RO_desal_mass) == 346.7 - assert approx(RO_desal_footprint) == 0.467 - - def test_RO_Desal_distributed(self): - """Test Seawater RO Model""" - n_systems = 2 - total_freshwater_kg_per_hr_required = 997 - per_system_freshwater_kg_per_hr_required = total_freshwater_kg_per_hr_required / n_systems - - total_outputs = RO_desal_eco( - freshwater_kg_per_hr=total_freshwater_kg_per_hr_required, - salinity="Seawater", - ) - per_system_outputs = RO_desal_eco( - per_system_freshwater_kg_per_hr_required, salinity="Seawater" - ) - - for t, s in zip(total_outputs, per_system_outputs): - assert t == approx(s * n_systems) - - def test_RO_Desal_Brackish(self): - """Test Brackish Model""" - outputs = RO_desal_eco(freshwater_kg_per_hr=997, salinity="Brackish") - RO_desal_mass = outputs[5] - RO_desal_footprint = outputs[6] - assert approx(RO_desal_mass) == 346.7 - assert approx(RO_desal_footprint) == 0.467 - - -if __name__ == "__main__": - test_set = TestRODesal() diff --git a/tests/h2integrate/test_hydrogen/test_basic_h2_cost.py b/tests/h2integrate/test_hydrogen/test_basic_h2_cost.py deleted file mode 100644 index 7397e2fb5..000000000 --- a/tests/h2integrate/test_hydrogen/test_basic_h2_cost.py +++ /dev/null @@ -1,177 +0,0 @@ -import numpy as np -from pytest import approx - -from h2integrate.simulation.technologies.hydrogen.electrolysis.H2_cost_model import ( - basic_H2_cost_model, -) - - -class TestBasicH2Costs: - electrolyzer_size_mw = 100 - h2_annual_output = 500 - nturbines = 10 - electrical_generation_timeseries = ( - electrolyzer_size_mw * (np.sin(range(0, 500))) * 0.5 + electrolyzer_size_mw * 0.5 - ) - - per_turb_electrolyzer_size_mw = electrolyzer_size_mw / nturbines - per_turb_h2_annual_output = h2_annual_output / nturbines - per_turb_electrical_generation_timeseries = electrical_generation_timeseries / nturbines - - elec_capex = 600 # $/kW - time_between_replacement = 80000 # hours - useful_life = 30 # years - - def test_on_turbine_capex(self): - ( - per_turb_electrolyzer_total_capital_cost, - per_turb_electrolyzer_OM_cost, - per_turb_electrolyzer_capex_kw, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) = basic_H2_cost_model( - self.elec_capex, - self.time_between_replacement, - self.per_turb_electrolyzer_size_mw, - self.useful_life, - self.per_turb_electrical_generation_timeseries, - self.per_turb_h2_annual_output, - 0.0, - 0.0, - include_refurb_in_opex=False, - offshore=1, - ) - - electrolyzer_total_capital_cost = per_turb_electrolyzer_total_capital_cost * self.nturbines - per_turb_electrolyzer_OM_cost * self.nturbines - - assert electrolyzer_total_capital_cost == approx(127698560.0) - - def test_on_platform_capex(self): - ( - electrolyzer_total_capital_cost, - electrolyzer_OM_cost, - electrolyzer_capex_kw, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) = basic_H2_cost_model( - self.elec_capex, - self.time_between_replacement, - self.electrolyzer_size_mw, - self.useful_life, - self.electrical_generation_timeseries, - self.h2_annual_output, - 0.0, - 0.0, - include_refurb_in_opex=False, - offshore=1, - ) - - assert electrolyzer_total_capital_cost == approx(125448560.0) - - def test_on_land_capex(self): - ( - per_turb_electrolyzer_total_capital_cost, - per_turb_electrolyzer_OM_cost, - per_turb_electrolyzer_capex_kw, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) = basic_H2_cost_model( - self.elec_capex, - self.time_between_replacement, - self.per_turb_electrolyzer_size_mw, - self.useful_life, - self.per_turb_electrical_generation_timeseries, - self.per_turb_h2_annual_output, - 0.0, - 0.0, - include_refurb_in_opex=False, - offshore=0, - ) - - electrolyzer_total_capital_cost = per_turb_electrolyzer_total_capital_cost * self.nturbines - per_turb_electrolyzer_OM_cost * self.nturbines - - assert electrolyzer_total_capital_cost == approx(116077280.00000003) - - def test_on_turbine_opex(self): - ( - per_turb_electrolyzer_total_capital_cost, - per_turb_electrolyzer_OM_cost, - per_turb_electrolyzer_capex_kw, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) = basic_H2_cost_model( - self.elec_capex, - self.time_between_replacement, - self.per_turb_electrolyzer_size_mw, - self.useful_life, - self.per_turb_electrical_generation_timeseries, - self.per_turb_h2_annual_output, - 0.0, - 0.0, - include_refurb_in_opex=False, - offshore=1, - ) - - (per_turb_electrolyzer_total_capital_cost * self.nturbines) - electrolyzer_OM_cost = per_turb_electrolyzer_OM_cost * self.nturbines - - assert electrolyzer_OM_cost == approx(1377207.4599629682) - - def test_on_platform_opex(self): - ( - electrolyzer_total_capital_cost, - electrolyzer_OM_cost, - electrolyzer_capex_kw, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) = basic_H2_cost_model( - self.elec_capex, - self.time_between_replacement, - self.electrolyzer_size_mw, - self.useful_life, - self.electrical_generation_timeseries, - self.h2_annual_output, - 0.0, - 0.0, - include_refurb_in_opex=False, - offshore=1, - ) - - assert electrolyzer_OM_cost == approx(1864249.9310054395) - - def test_on_land_opex(self): - ( - per_turb_electrolyzer_total_capital_cost, - per_turb_electrolyzer_OM_cost, - per_turb_electrolyzer_capex_kw, - time_between_replacement, - h2_tax_credit, - h2_itc, - ) = basic_H2_cost_model( - self.elec_capex, - self.time_between_replacement, - self.per_turb_electrolyzer_size_mw, - self.useful_life, - self.per_turb_electrical_generation_timeseries, - self.per_turb_h2_annual_output, - 0.0, - 0.0, - include_refurb_in_opex=False, - offshore=0, - ) - - (per_turb_electrolyzer_total_capital_cost * self.nturbines) - electrolyzer_OM_cost = per_turb_electrolyzer_OM_cost * self.nturbines - - assert electrolyzer_OM_cost == approx(1254447.4599629682) - - -if __name__ == "__main__": - test_set = TestBasicH2Costs() diff --git a/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_compressor.py b/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_compressor.py index 7ce0cb9ab..d05b7b927 100644 --- a/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_compressor.py +++ b/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_compressor.py @@ -1,6 +1,6 @@ from pytest import approx, raises -from h2integrate.simulation.technologies.hydrogen.h2_transport.h2_compression import Compressor +from h2integrate.storage.hydrogen.h2_transport.h2_compression import Compressor # test that we get the results we got when the code was received diff --git a/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_export_pipeline.py b/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_export_pipeline.py deleted file mode 100644 index 6bd8bb0f4..000000000 --- a/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_export_pipeline.py +++ /dev/null @@ -1,132 +0,0 @@ -from h2integrate.simulation.technologies.hydrogen.h2_transport.h2_export_pipe import ( - run_pipe_analysis, -) - - -# test that we the results we got when the code was recieved -class TestExportPipeline: - L = 8 # Length [km] - m_dot = 1.5 # Mass flow rate [kg/s] assuming 300 MW -> 1.5 kg/s - p_inlet = 30 # Inlet pressure [bar] - p_outlet = 10 # Outlet pressure [bar] - depth = 80 # depth of pipe [m] - costs = run_pipe_analysis(L, m_dot, p_inlet, p_outlet, depth) - - def test_grade(self): - assert self.costs["Grade"][0] == "X42" - - def test_od(self): - assert self.costs["Outer diameter (mm)"][0] == 168.28 - - def test_id(self): - assert self.costs["Inner diameter (mm)"][0] == 162.74 - - def test_schedule(self): - assert self.costs["Schedule"][0] == "S 5S" - - def test_thickness(self): - assert self.costs["Thickness (mm)"][0] == 2.77 - - def test_volume(self): - assert self.costs["volume [m3]"][0] == 12.213769866246679 - - def test_weight(self): - assert self.costs["weight [kg]"][0] == 95755.95575137396 - - def test_material_cost(self): - assert self.costs["mat cost [$]"][0] == 210663.1026530227 - - def test_labor_cost(self): - assert self.costs["labor cost [$]"][0] == 1199293.4563603932 - - def test_misc_cost(self): - assert self.costs["misc cost [$]"][0] == 429838.3943856504 - - def test_row_cost(self): # ROW = right of way - assert self.costs["ROW cost [$]"][0] == 365317.5476681454 - - def test_total_cost_output(self): - assert self.costs["total capital cost [$]"][0] == 2205112.501067212 - - def test_total_capital_cost_sum(self): - total_capital_cost = ( - self.costs["mat cost [$]"][0] - + self.costs["labor cost [$]"][0] - + self.costs["misc cost [$]"][0] - + self.costs["ROW cost [$]"][0] - ) - - assert self.costs["total capital cost [$]"][0] == total_capital_cost - - def test_annual_opex(self): - assert ( - self.costs["annual operating cost [$]"][0] - == 0.0117 * self.costs["total capital cost [$]"][0] - ) - - -class TestExportPipelineRegion: - L = 8 # Length [km] - m_dot = 1.5 # Mass flow rate [kg/s] assuming 300 MW -> 1.5 kg/s - p_inlet = 30 # Inlet pressure [bar] - p_outlet = 10 # Outlet pressure [bar] - depth = 80 # depth of pipe [m] - region = "GP" # great plains region - costs = run_pipe_analysis(L, m_dot, p_inlet, p_outlet, depth, region=region) - - def test_material_cost(self): - assert self.costs["mat cost [$]"][0] == 210663.1026530227 - - def test_labor_cost(self): - assert self.costs["labor cost [$]"][0] == 408438.062500015 - - def test_misc_cost(self): - assert self.costs["misc cost [$]"][0] == 184458.92890187018 - - def test_row_cost(self): # ROW = right of way - assert self.costs["ROW cost [$]"][0] == 52426.57591258784 - - def test_total_cost_output(self): - assert self.costs["total capital cost [$]"][0] == 855986.6699674957 - - -class TestExportPipelineOverrides: - L = 8 # Length [km] - m_dot = 1.5 # Mass flow rate [kg/s] assuming 300 MW -> 1.5 kg/s - p_inlet = 30 # Inlet pressure [bar] - p_outlet = 10 # Outlet pressure [bar] - depth = 80 # depth of pipe [m] - labor_in_mi = 1000 - misc_in_mi = 2000 - row_in_mi = 3000 - mat_in_mi = 4000 - costs = run_pipe_analysis( - L, - m_dot, - p_inlet, - p_outlet, - depth, - labor_in_mi=labor_in_mi, - misc_in_mi=misc_in_mi, - row_in_mi=row_in_mi, - mat_in_mi=mat_in_mi, - ) - - def test_material_cost(self): - assert self.costs["mat cost [$]"][0] == 124469.9746153248 - - def test_labor_cost(self): - assert self.costs["labor cost [$]"][0] == 31117.4936538312 - - def test_misc_cost(self): - assert self.costs["misc cost [$]"][0] == 62234.9873076624 - - def test_row_cost(self): # ROW = right of way - assert self.costs["ROW cost [$]"][0] == 93352.4809614936 - - def test_total_cost_output(self): - assert self.costs["total capital cost [$]"][0] == 311174.936538312 - - -if __name__ == "__main__": - test_set = TestExportPipeline() diff --git a/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_pipe_array.py b/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_pipe_array.py deleted file mode 100644 index 47d18c782..000000000 --- a/tests/h2integrate/test_hydrogen/test_h2_transport/test_h2_pipe_array.py +++ /dev/null @@ -1,63 +0,0 @@ -from h2integrate.simulation.technologies.hydrogen.h2_transport.h2_pipe_array import ( - run_pipe_array, - run_pipe_array_const_diam, -) - - -# test that we the results we got when the code was recieved -class TestPipeArraySingleSection: - L = 8 # Length [km] - m_dot = 1.5 # Mass flow rate [kg/s] assuming 300 MW -> 1.5 kg/s - p_inlet = 30 # Inlet pressure [bar] - p_outlet = 10 # Outlet pressure [bar] - depth = 80 # depth of pipe [m] - capex, opex = run_pipe_array([[L]], depth, p_inlet, p_outlet, [[m_dot]]) - - def test_capex(self): - assert self.capex == 2226256.16387454 - - def test_opex(self): - assert self.opex == 26047.197117332118 - - -# TODO check the values in these test, they are gut checked for being slightly above what would be expected for a single distance, but not well determined - - -class TestPipeArrayMultiSection: - L = 8 # Length [km] - m_dot = 1.5 # Mass flow rate [kg/s] assuming 300 MW -> 1.5 kg/s - p_inlet = 30 # Inlet pressure [bar] - p_outlet = 10 # Outlet pressure [bar] - depth = 80 # depth of pipe [m] - - def test_capex(self): - assert self.capex == 5129544.170342567 - - def test_opex(self): - assert self.opex == 60015.666793008044 - - capex, opex = run_pipe_array([[L, L]], depth, p_inlet, p_outlet, [[m_dot, m_dot]]) - - -class TestPipeArrayMultiSectionConstDiameter: - L = 8 # Length [km] - m_dot = 1.5 # Mass flow rate [kg/s] assuming 300 MW -> 1.5 kg/s - p_inlet = 30 # Inlet pressure [bar] - p_outlet = 10 # Outlet pressure [bar] - depth = 80 # depth of pipe [m] - - capex, opex = run_pipe_array_const_diam( - [[L, L], [L, L]], depth, p_inlet, p_outlet, [[m_dot, m_dot], [m_dot, m_dot]] - ) - - def test_capex(self): - assert self.capex == 10360272.637584394 - - def test_opex(self): - assert self.opex == 121215.18985973741 - - -if __name__ == "__main__": - test_set = TestPipeArraySingleSection() - test_set = TestPipeArrayMultiSection() - test_set = TestPipeArrayMultiSectionConstDiameter() diff --git a/tests/h2integrate/test_hydrogen/test_lined_rock_storage.py b/tests/h2integrate/test_hydrogen/test_lined_rock_storage.py deleted file mode 100644 index 503ce914e..000000000 --- a/tests/h2integrate/test_hydrogen/test_lined_rock_storage.py +++ /dev/null @@ -1,64 +0,0 @@ -import pytest -from pytest import fixture - -from h2integrate.simulation.technologies.hydrogen.h2_storage.lined_rock_cavern.lined_rock_cavern import ( # noqa: E501 - LinedRockCavernStorage, -) - - -# Test values are based on conclusions of Papadias 2021 and are in 2019 USD -in_dict = {"h2_storage_kg": 1000000, "system_flow_rate": 100000} - - -@fixture -def lined_rock_cavern_storage(): - lined_rock_cavern_storage = LinedRockCavernStorage(in_dict) - - return lined_rock_cavern_storage - - -def test_init(): - lined_rock_cavern_storage = LinedRockCavernStorage(in_dict) - - assert lined_rock_cavern_storage.input_dict is not None - assert lined_rock_cavern_storage.output_dict is not None - - -def test_capex_per_kg(lined_rock_cavern_storage): - lined_rock_cavern_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - lined_rock_cavern_storage.lined_rock_cavern_capex() - ) - assert lined_rock_cavern_storage_capex_per_kg == pytest.approx(51.532548895265045) - - -def test_capex(lined_rock_cavern_storage): - _lined_rock_cavern_storage_capex_per_kg, installed_capex, _compressor_capex = ( - lined_rock_cavern_storage.lined_rock_cavern_capex() - ) - assert installed_capex == pytest.approx(51136144.673) - - -def test_compressor_capex(lined_rock_cavern_storage): - _lined_rock_cavern_storage_capex_per_kg, _installed_capex, compressor_capex = ( - lined_rock_cavern_storage.lined_rock_cavern_capex() - ) - assert compressor_capex == pytest.approx(9435600.2555) - - -def test_capex_output_dict(lined_rock_cavern_storage): - _lined_rock_cavern_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - lined_rock_cavern_storage.lined_rock_cavern_capex() - ) - assert lined_rock_cavern_storage.output_dict[ - "lined_rock_cavern_storage_capex" - ] == pytest.approx(51136144.673) - - -def test_opex(lined_rock_cavern_storage): - _lined_rock_cavern_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - lined_rock_cavern_storage.lined_rock_cavern_capex() - ) - lined_rock_cavern_storage.lined_rock_cavern_opex() - assert lined_rock_cavern_storage.output_dict["lined_rock_cavern_storage_opex"] == pytest.approx( - 2359700 - ) diff --git a/tests/h2integrate/test_hydrogen/test_pem_mass_and_footprint.py b/tests/h2integrate/test_hydrogen/test_pem_mass_and_footprint.py deleted file mode 100644 index 6a6173300..000000000 --- a/tests/h2integrate/test_hydrogen/test_pem_mass_and_footprint.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -from h2integrate.simulation.technologies.hydrogen.electrolysis.pem_mass_and_footprint import ( - mass, - footprint, -) - - -def test_footprint_0mw(): - assert footprint(0.0) == 0.0 - - -def test_footprint_1mw(): - assert footprint(1) == 48 - - -def test_mass(): - assert mass(0.045) == pytest.approx(900.0, rel=1e-4) diff --git a/tests/h2integrate/test_hydrogen/test_pressure_vessel.py b/tests/h2integrate/test_hydrogen/test_pressure_vessel.py deleted file mode 100644 index 25de53872..000000000 --- a/tests/h2integrate/test_hydrogen/test_pressure_vessel.py +++ /dev/null @@ -1,317 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from pytest import approx - -from h2integrate.simulation.technologies.hydrogen.h2_storage.pressure_vessel.compressed_gas_storage_model_20221021.Compressed_all import ( # noqa: E501 - PressureVessel, -) - - -# test that we the results we got when the code was recieved -class TestPressureVessel: - # pressure_vessel_instance_no_cost = PressureVessel(Energy_cost=0.0) - # pressure_vessel_instance_no_cost.run() - pressure_vessel_instance = PressureVessel(Energy_cost=0.07) - pressure_vessel_instance.run() - - pressure_vessel_instance_no_cost = PressureVessel(Energy_cost=0.0) - pressure_vessel_instance_no_cost.run() - - def test_capacity_max(self): - assert self.pressure_vessel_instance.capacity_max == 5585222.222222222 - - def test_t_discharge_hr_max(self): - assert self.pressure_vessel_instance.t_discharge_hr_max == 25.133499999999998 - - def test_a_fit_capex(self): - # assert self.pressure_vessel_instance.a_fit_capex == 9084.035219940572 - assert self.pressure_vessel_instance.a_fit_capex == approx(0.053925726563169414) - - def test_b_fit_capex(self): - # assert self.pressure_vessel_instance.b_fit_capex == -0.127478041731842 - assert self.pressure_vessel_instance.b_fit_capex == approx(1.6826965840450498) - - def test_c_fit_capex(self): - assert self.pressure_vessel_instance.c_fit_capex == approx(20.297862568544417) - - def test_a_fit_opex(self): - assert self.pressure_vessel_instance.a_fit_opex == approx(0.05900068374896024) - - def test_b_fit_opex(self): - assert self.pressure_vessel_instance.b_fit_opex == approx(1.8431485607717895) - - def test_c_fit_opex(self): - assert self.pressure_vessel_instance.c_fit_opex == approx(17.538017086792006) - - def test_energy_fit(self): - capacity = 1e6 # 1000 metric tons h2 - _, _, energy_per_kg = self.pressure_vessel_instance.calculate_from_fit(capacity_kg=capacity) - assert energy_per_kg == approx(2.688696, 1e-5) # kWh/kg - - def test_compare_price_change_capex(self): - capacity = 1e6 # 1000 metric tons h2 - capex_07, _, _ = self.pressure_vessel_instance.calculate_from_fit(capacity_kg=capacity) - capex_00, _, _ = self.pressure_vessel_instance_no_cost.calculate_from_fit( - capacity_kg=capacity - ) - - assert capex_00 == capex_07 - - def test_compare_price_change_opex(self): - capacity = 1e6 # 1000 metric tons h2 - _, opex_07, _ = self.pressure_vessel_instance.calculate_from_fit(capacity_kg=capacity) - _, opex_00, _ = self.pressure_vessel_instance_no_cost.calculate_from_fit( - capacity_kg=capacity - ) - - assert opex_00 < opex_07 - - def test_compare_price_change_energy(self): - capacity = 1e6 # 1000 metric tons h2 - _, _, energy_per_kg_07 = self.pressure_vessel_instance.calculate_from_fit( - capacity_kg=capacity - ) - _, _, energy_per_kg_00 = self.pressure_vessel_instance_no_cost.calculate_from_fit( - capacity_kg=capacity - ) - - assert energy_per_kg_00 == energy_per_kg_07 - - def test_mass_footprint(self): - """ - extension of gold standard test to new tank footprint outputs - """ - cap_H2_tank_ref = 179.6322517351785 - capacity_req = 1.2e3 - Ntank_ref = np.ceil(capacity_req / cap_H2_tank_ref) - Atank_ref = (2 * 53.7e-2) ** 2 - footprint_ref = Ntank_ref * Atank_ref - mass_ref = 1865.0 - - assert self.pressure_vessel_instance.get_tanks(capacity_req) == approx(Ntank_ref) - assert self.pressure_vessel_instance.get_tank_footprint(capacity_req)[0] == approx( - Atank_ref, rel=0.01 - ) - assert self.pressure_vessel_instance.get_tank_footprint(capacity_req)[1] == approx( - footprint_ref, rel=0.01 - ) - - assert self.pressure_vessel_instance.get_tank_mass(capacity_req)[0] == approx( - mass_ref, rel=0.01 - ) - - def test_output_function(self): - capacity = self.pressure_vessel_instance.compressed_gas_function.capacity_1[5] - capex, opex, energy = self.pressure_vessel_instance.calculate_from_fit(capacity) - tol = 1.0 - - assert capex == approx( - self.pressure_vessel_instance.compressed_gas_function.cost_kg[5] * capacity, - tol, - ) - assert opex == approx( - self.pressure_vessel_instance.compressed_gas_function.Op_c_Costs_kg[5] * capacity, - tol, - ) - assert energy == approx( - self.pressure_vessel_instance.compressed_gas_function.total_energy_used_kwh[5] - * capacity, - tol, - ) - - def test_distributed(self): - capacity = self.pressure_vessel_instance.compressed_gas_function.capacity_1[5] - capex, opex, energy_kg = self.pressure_vessel_instance.calculate_from_fit(capacity) - print(capex) - self.pressure_vessel_instance.get_tank_mass(capacity) - self.pressure_vessel_instance.get_tank_footprint(capacity) - - ( - capex_dist_05, - opex_dist_05, - energy_kg_dist_05, - area_footprint_site_05, - mass_tank_empty_site_05, - capacity_site_05, - ) = self.pressure_vessel_instance.distributed_storage_vessels(capacity, 5) - assert capex_dist_05 == approx(6205232868.4722595) - assert opex_dist_05 == approx(113433768.86938927) - assert energy_kg_dist_05 == approx(2.6886965443907727) - assert area_footprint_site_05 == approx(4866.189496204457) - assert mass_tank_empty_site_05 == approx(7870274.025926539) - assert capacity_site_05 == approx(capacity / 5) - - ( - capex_dist_10, - opex_dist_10, - energy_kg_dist_10, - area_footprint_site_10, - mass_tank_empty_site_10, - capacity_site_10, - ) = self.pressure_vessel_instance.distributed_storage_vessels(capacity, 10) - assert capex_dist_10 == approx(7430302244.729572) - assert opex_dist_10 == approx(138351814.3102437) - assert energy_kg_dist_10 == approx(2.6886965443907727) - assert area_footprint_site_10 == approx(2433.0947481022286) - assert mass_tank_empty_site_10 == approx(3935137.0129632694) - assert capacity_site_10 == approx(capacity / 10) - - ( - capex_dist_20, - opex_dist_20, - energy_kg_dist_20, - area_footprint_site_20, - mass_tank_empty_site_20, - capacity_site_20, - ) = self.pressure_vessel_instance.distributed_storage_vessels(capacity, 20) - assert capex_dist_20 == approx(9370417735.496975) - assert opex_dist_20 == approx(178586780.2083488) - assert energy_kg_dist_20 == approx(2.6886965443907727) - assert area_footprint_site_20 == approx(1216.5473740511143) - assert mass_tank_empty_site_20 == approx(1967568.5064816347) - assert capacity_site_20 == approx(capacity / 20) - - assert ( - (capex < capex_dist_05) - and (capex_dist_05 < capex_dist_10) - and (capex_dist_10 < capex_dist_20) - ), "capex should increase w/ number of sites" - assert ( - (opex < opex_dist_05) - and (opex_dist_05 < opex_dist_10) - and (opex_dist_10 < opex_dist_20) - ), "opex should increase w/ number of sites" - assert ( - (energy_kg == approx(energy_kg_dist_05)) - and (energy_kg_dist_05 == approx(energy_kg_dist_10)) - and (energy_kg_dist_10 == approx(energy_kg_dist_20)) - ), "energy_kg be approx. equal across number of sites" - - # assert False - - # def test_plots(self): - # self.pressure_vessel_instance.plot() - - -class PlotTestPressureVessel: - def __init__(self) -> None: - self.pressure_vessel_instance = PressureVessel(Energy_cost=0.07) - self.pressure_vessel_instance.run() - - self.pressure_vessel_instance_no_cost = PressureVessel(Energy_cost=0.0) - self.pressure_vessel_instance_no_cost.run() - - def plot_size_and_divisions(self): - # set sweep ranges - capacity_range = np.arange(1e6, 5e6, step=1e3) # kg - divisions = np.array([1, 5, 10, 15, 20]) - - # initialize outputs - capex_results = np.zeros((len(divisions), len(capacity_range))) - opex_results = np.zeros((len(divisions), len(capacity_range))) - - # run capex and opex for range - for ( - j, - div, - ) in enumerate(divisions): - for i, capacity in enumerate(capacity_range): - if div == 1: - capex_results[j, i], opex_results[j, i], energy_kg = ( - self.pressure_vessel_instance.calculate_from_fit(capacity) - ) - else: - ( - capex_results[j, i], - opex_results[j, i], - energy_kg_dist_05, - area_footprint_site_05, - mass_tank_empty_site_05, - capacity_site_05, - ) = self.pressure_vessel_instance.distributed_storage_vessels(capacity, div) - - # plot results - fig, ax = plt.subplots(2, 2, sharex=True) - for j in np.arange(0, len(divisions)): - ax[0, 0].plot( - capacity_range / 1e3, - capex_results[j] / capacity_range, - label=f"{divisions[j]} Divisions", - ) - ax[0, 1].plot( - capacity_range / 1e3, - opex_results[j] / capacity_range, - label=f"{divisions[j]} Divisions", - ) - - # ax[0,0].set(ylabel="CAPEX (USD/kg)", ylim=[0, 5000], yticks=[0,1000,2000,3000,4000,5000]) - ax[0, 1].set(ylabel="OPEX (USD/kg)", ylim=[0, 100]) - - for j in np.arange(0, len(divisions)): - ax[1, 0].plot( - capacity_range / 1e3, - capex_results[j] / capex_results[0], - label=f"{divisions[j]} Divisions", - ) - ax[1, 1].plot( - capacity_range / 1e3, - opex_results[j] / opex_results[0], - label=f"{divisions[j]} Divisions", - ) - - ax[1, 0].set(ylabel="CAPEX_div/CAPEX_cent", ylim=[0, 4]) - ax[1, 1].set(ylabel="OPEX_div/OPEX_cent", ylim=[0, 4]) - - for axi in ax[1]: - axi.set(xlabel="H$_2$ Capacity (metric tons)") - ax[1, 1].legend() - - plt.tight_layout() - - plt.show() - - -if __name__ == "__main__": - # test_set = TestPressureVessel() - plot_tests = PlotTestPressureVessel() - plot_tests.plot_size_and_divisions() - -# 0.0 -# 6322420.744236805 -# 1331189.5844818645 -# 7363353.502353448 - -# 0.07 -# 6322420.744236805 -# 1331189.5844818645 -# 7363353.502353448 - -# energy cost for both cases match as per above - - -# op costs - 0.07 -# 442569.45209657634 -# 345243.94167843653 -# 0 -# 93183.27091373052 - -# op costs - 0.0 -# 0.0 -# 0.0 -# 0 -# 0.0 - -# op c costs -# op_c_costs 0.07 -# 880996.6646887433 -# 799322.4503233839 -# 0.03 -# 4262490675.039804 -# 25920 - -# op_c_costs 0.00 -# 0.0 -# 0.0 -# 0.03 -# 4262490675.039804 -# 25920 diff --git a/tests/h2integrate/test_hydrogen/test_pressurized_turbine.py b/tests/h2integrate/test_hydrogen/test_pressurized_turbine.py deleted file mode 100644 index be6eb165d..000000000 --- a/tests/h2integrate/test_hydrogen/test_pressurized_turbine.py +++ /dev/null @@ -1,485 +0,0 @@ -import numpy as np -from pytest import approx - -from h2integrate.simulation.technologies.hydrogen.h2_storage.on_turbine.on_turbine_hydrogen_storage import ( # noqa: E501 - PressurizedTower, -) - - -class TestPressurizedTower: - def test_frustum(self): - """ - test static methods for geometry of a frustum - """ - - # try a cone - D_ref = 15.0 - h_ref = 2.1 - V_cone_ref = np.pi / 3.0 * (D_ref / 2) ** 2 * h_ref - - V_cone = PressurizedTower.compute_frustum_volume(h_ref, D_ref, 0.0) - - assert V_cone == approx(V_cone_ref) - - # try a cylinder - D_ref = 45.0 - h_ref = 612.3 - V_cyl_ref = np.pi / 4.0 * D_ref**2 * h_ref - - V_cyl = PressurizedTower.compute_frustum_volume(h_ref, D_ref, D_ref) - - assert V_cyl == approx(V_cyl_ref) - - # try a frustum by delta of two cones - D0_ref = 5.7 - h0_ref = 0.0 + 1.0 - D1_ref = 1.9 - h1_ref = 2.4 + 1.0 - D2_ref = 0.0 - h2_ref = 2.4 + 1.2 + 1.0 - V_2_ref = np.pi / 3.0 * (D1_ref / 2) ** 2 * (h2_ref - h1_ref) - V_12_ref = np.pi / 3.0 * (D0_ref / 2) ** 2 * (h2_ref - h0_ref) - V_1_ref = V_12_ref - V_2_ref - - V_1 = PressurizedTower.compute_frustum_volume(h1_ref - h0_ref, D0_ref, D1_ref) - V_12 = PressurizedTower.compute_frustum_volume(h2_ref - h0_ref, D0_ref, D2_ref) - V_2 = PressurizedTower.compute_frustum_volume(h2_ref - h1_ref, D1_ref, D2_ref) - - assert V_1 == approx(V_1_ref) - assert V_12 == approx(V_12_ref) - assert V_2 == approx(V_2_ref) - - def test_crossover_pressure(self): - # random plausible values - E = 0.8 - Sut = 1200e3 - d_t_ratio = 321.0 - - p_ref = 4 * E * Sut / (7 * d_t_ratio * (1 - E / 7.0)) - - p = PressurizedTower.get_crossover_pressure(E, Sut, d_t_ratio) - - assert p == approx(p_ref) - - if False: # paper values are untrustworthy - - def test_thickness_increment_const(self): - # plot values - p = 600.0e3 - Sut = 636.0e6 - - alpha_dtp = PressurizedTower.get_thickness_increment_const(p, Sut) - - # values from graph seem to be off by a factor of ten... jacked up!!! - # these are the values as I read them off the graph assuming that factor - # is in fact erroneous - assert 2 * 1.41 * alpha_dtp == approx(0.675e-3, rel=0.05) - assert 2 * 2.12 * alpha_dtp == approx(0.900e-3, abs=0.05) - assert 2 * 2.82 * alpha_dtp == approx(1.250e-3, abs=0.05) - - def test_cylinder(self): - """ - a hypothetical (nonsensical) cylindical tower -> easy to compute - """ - - ### SETUP REFERENCE VALUES - - # input reference values - h_ref = 100.0 - D_ref = 10.0 - d_t_ratio_ref = 320.0 - density_steel_ref = 7817.0 # kg/m^3 - strength_ultimate_steel_ref = 636e6 # Pa - strength_yield_steel_ref = 350e6 # Pa - Eweld_ref = 0.80 - costrate_steel_ref = 1.50 - costrate_cap_ref = 2.66 - costrate_ladder_ref = 32.80 - cost_door_ref = 2000.0 - cost_mainframe_ref = 6300 - cost_nozzlesmanway_ref = 16000 - costrate_conduit_ref = 35 - temp_ref = 25.0 # degC - R_H2_ref = 4126.0 # J/(kg K) - maintenance_rate_ref = 0.03 - staff_hours_ref = 60 - wage_ref = 36 - - # geometric reference values - thickness_wall_trad_ref = D_ref / d_t_ratio_ref - surfacearea_wall_ref = np.pi * D_ref * h_ref - surfacearea_cap_ref = np.pi / 4.0 * D_ref**2 - - # non-pressurized/traditional geometry values - volume_wall_trad_ref = surfacearea_wall_ref * thickness_wall_trad_ref - volume_inner_ref = h_ref * surfacearea_cap_ref - volume_cap_top_trad_ref = 0.0 # surfacearea_cap_ref*thickness_top_ref - volume_cap_bot_trad_ref = 0.0 # surfacearea_cap_ref*thickness_bot_ref - - # non-pressurized/traditional mass/cost values - mass_wall_trad_ref = density_steel_ref * volume_wall_trad_ref - mass_cap_top_trad_ref = density_steel_ref * volume_cap_top_trad_ref - mass_cap_bot_trad_ref = density_steel_ref * volume_cap_bot_trad_ref - cost_tower_trad_ref = costrate_steel_ref * ( - mass_wall_trad_ref + mass_cap_top_trad_ref + mass_cap_bot_trad_ref - ) - cost_nontower_trad_ref = h_ref * costrate_ladder_ref + cost_door_ref - - # pressurization info - p_crossover_ref = ( - 4 * Eweld_ref * strength_ultimate_steel_ref / (7 * d_t_ratio_ref * (1 - Eweld_ref / 7)) - ) - delta_t_ref = p_crossover_ref * (D_ref / 2) / (2 * strength_ultimate_steel_ref) - thickness_wall_ref = D_ref / d_t_ratio_ref + delta_t_ref - thickness_cap_top_ref = D_ref * np.sqrt( - 0.10 * p_crossover_ref / (Eweld_ref * strength_yield_steel_ref / 1.5) - ) - thickness_cap_bot_ref = D_ref * np.sqrt( - 0.10 * p_crossover_ref / (Eweld_ref * strength_yield_steel_ref / 1.5) - ) - - # pressurized geometry values - volume_wall_ref = surfacearea_wall_ref * thickness_wall_ref - volume_cap_top_ref = surfacearea_cap_ref * (thickness_cap_top_ref) - volume_cap_bot_ref = surfacearea_cap_ref * (thickness_cap_bot_ref) - - # pressurized mass/cost values - mass_wall_ref = density_steel_ref * volume_wall_ref - mass_cap_top_ref = density_steel_ref * volume_cap_top_ref - mass_cap_bot_ref = density_steel_ref * volume_cap_bot_ref - cost_tower_ref = costrate_steel_ref * mass_wall_ref + costrate_cap_ref * ( - mass_cap_top_ref + mass_cap_bot_ref - ) - cost_nontower_ref = ( - 2 * h_ref * costrate_ladder_ref - + 2 * cost_door_ref - + cost_mainframe_ref - + cost_nozzlesmanway_ref - + costrate_conduit_ref * h_ref - ) - - # gas - rho_H2_ref = p_crossover_ref / (R_H2_ref * (temp_ref + 273.15)) - m_H2_ref = volume_inner_ref * rho_H2_ref - - # capex - capex_ref = ( - cost_tower_ref + cost_nontower_ref - cost_tower_trad_ref - cost_nontower_trad_ref - ) - - # opex - opex_ref = maintenance_rate_ref * capex_ref + wage_ref * staff_hours_ref - - turbine = { - "tower_length": h_ref, - "section_diameters": [D_ref, D_ref, D_ref], - "section_heights": [0.0, 0.0 + 0.5 * h_ref, 0.0 + h_ref], - # 'section_diameters': [D_ref, D_ref], - # 'section_heights': [0., 0. + h_ref], - } - - ## traditional estimates (non-pressurized) - - pressurized_cylinder = PressurizedTower(1992, turbine) - - assert pressurized_cylinder.get_volume_tower_inner() == approx(volume_inner_ref) - assert pressurized_cylinder.get_volume_tower_material(pressure=0)[0] == approx( - volume_wall_trad_ref - ) - assert pressurized_cylinder.get_volume_tower_material(pressure=0)[1] == approx( - volume_cap_bot_trad_ref - ) - assert pressurized_cylinder.get_volume_tower_material(pressure=0)[2] == approx( - volume_cap_top_trad_ref - ) - assert pressurized_cylinder.get_mass_tower_material(pressure=0)[0] == approx( - mass_wall_trad_ref - ) - assert pressurized_cylinder.get_mass_tower_material(pressure=0)[1] == approx( - mass_cap_bot_trad_ref - ) - assert pressurized_cylinder.get_mass_tower_material(pressure=0)[2] == approx( - mass_cap_top_trad_ref - ) - - assert np.sum(pressurized_cylinder.get_cost_tower_material(pressure=0)) == approx( - cost_tower_trad_ref - ) - assert pressurized_cylinder.get_cost_nontower(traditional=True) == approx( - cost_nontower_trad_ref - ) - - ## pressurized estimates - - assert pressurized_cylinder.operating_pressure == p_crossover_ref - - assert pressurized_cylinder.get_volume_tower_material()[0] == approx(volume_wall_ref) - assert pressurized_cylinder.get_volume_tower_material()[1] == approx(volume_cap_bot_ref) - assert pressurized_cylinder.get_volume_tower_material()[2] == approx(volume_cap_top_ref) - assert pressurized_cylinder.get_mass_tower_material()[0] == approx(mass_wall_ref) - assert pressurized_cylinder.get_mass_tower_material()[1] == approx(mass_cap_bot_ref) - assert pressurized_cylinder.get_mass_tower_material()[2] == approx(mass_cap_top_ref) - - assert np.sum(pressurized_cylinder.get_cost_tower_material()) == approx(cost_tower_ref) - assert pressurized_cylinder.get_cost_nontower() == approx(cost_nontower_ref) - - ## output interface - - # make sure the final values match expectation - assert pressurized_cylinder.get_capex() == approx(capex_ref) - assert pressurized_cylinder.get_opex() == approx(opex_ref) - assert pressurized_cylinder.get_mass_empty() == approx( - mass_wall_ref - + mass_cap_bot_ref - + mass_cap_top_ref - - mass_wall_trad_ref - - mass_cap_bot_trad_ref - - mass_cap_top_trad_ref - ) - assert pressurized_cylinder.get_capacity_H2() == approx(m_H2_ref) - assert pressurized_cylinder.get_pressure_H2() == approx(p_crossover_ref) - - if True: - - def test_cone(self): - """ - a hypothetical (nonsensical) conical tower -> easy to compute - """ - - ### SETUP REFERENCE VALUES - - # input reference values - h_ref = 81.0 - D_base_ref = 10.0 - D_top_ref = 0.0 - - # non-input parameters - d_t_ratio_ref = 320.0 - density_steel_ref = 7817.0 # kg/m^3 - strength_ultimate_steel_ref = 636e6 # Pa - strength_yield_steel_ref = 350e6 # Pa - Eweld_ref = 0.8 - costrate_steel_ref = 1.50 - costrate_cap_ref = 2.66 - costrate_ladder_ref = 32.80 - cost_door_ref = 2000.0 - cost_mainframe_ref = 6300 - cost_nozzlesmanway_ref = 16000 - costrate_conduit_ref = 35 - temp_ref = 25.0 # degC - R_H2_ref = 4126.0 # J/(kg K) - maintenance_rate_ref = 0.03 - staff_hours_ref = 60 - wage_ref = 36 - - # geometric reference values - surfacearea_cap_top_ref = np.pi / 4.0 * D_top_ref**2 - surfacearea_cap_bot_ref = np.pi / 4.0 * D_base_ref**2 - D_top_ref / d_t_ratio_ref - thickness_wall_bot_ref = D_base_ref / d_t_ratio_ref - - def cone_volume(h, d): - return np.pi / 3.0 * (d / 2) ** 2 * h - - # non-pressurized/traditional geometry values - volume_inner_ref = cone_volume(h_ref, D_base_ref) - print(volume_inner_ref) - volume_wall_trad_ref = cone_volume( - h_ref, D_base_ref + thickness_wall_bot_ref - ) - cone_volume(h_ref, D_base_ref - thickness_wall_bot_ref) - volume_cap_top_trad_ref = 0.0 # surfacearea_cap_top_ref*thickness_top_ref - volume_cap_bot_trad_ref = 0.0 # surfacearea_cap_bot_ref*thickness_bot_ref - - # non-pressurized/traditional mass/cost values - mass_wall_trad_ref = density_steel_ref * volume_wall_trad_ref - mass_cap_top_trad_ref = density_steel_ref * volume_cap_top_trad_ref - mass_cap_bot_trad_ref = density_steel_ref * volume_cap_bot_trad_ref - cost_tower_trad_ref = costrate_steel_ref * ( - mass_wall_trad_ref + mass_cap_top_trad_ref + mass_cap_bot_trad_ref - ) - cost_nontower_trad_ref = h_ref * costrate_ladder_ref + cost_door_ref - - # pressurization info - p_crossover_ref = ( - 4 - * Eweld_ref - * strength_ultimate_steel_ref - / (7 * d_t_ratio_ref * (1 - Eweld_ref / 7)) - ) - dt_bot_ref = p_crossover_ref * (D_base_ref / 2) / (2 * strength_ultimate_steel_ref) - thickness_wall_bot_ref = D_base_ref / d_t_ratio_ref + dt_bot_ref - thickness_cap_top_ref = D_top_ref * np.sqrt( - 0.10 * p_crossover_ref / (Eweld_ref * strength_yield_steel_ref / 1.5) - ) - thickness_cap_bot_ref = D_base_ref * np.sqrt( - 0.10 * p_crossover_ref / (Eweld_ref * strength_yield_steel_ref / 1.5) - ) - - # pressurized geometry values - volume_wall_ref = cone_volume(h_ref, D_base_ref + thickness_wall_bot_ref) - cone_volume( - h_ref, D_base_ref - thickness_wall_bot_ref - ) - volume_cap_top_ref = surfacearea_cap_top_ref * (thickness_cap_top_ref) - volume_cap_bot_ref = surfacearea_cap_bot_ref * (thickness_cap_bot_ref) - - # pressurized mass/cost values - mass_wall_ref = density_steel_ref * volume_wall_ref - mass_cap_top_ref = density_steel_ref * volume_cap_top_ref - mass_cap_bot_ref = density_steel_ref * volume_cap_bot_ref - cost_tower_ref = costrate_steel_ref * mass_wall_ref + costrate_cap_ref * ( - mass_cap_top_ref + mass_cap_bot_ref - ) - cost_nontower_ref = ( - 2 * h_ref * costrate_ladder_ref - + 2 * cost_door_ref - + cost_mainframe_ref - + cost_nozzlesmanway_ref - + costrate_conduit_ref * h_ref - ) - - # gas - rho_H2_ref = p_crossover_ref / (R_H2_ref * (temp_ref + 273.15)) - m_H2_ref = volume_inner_ref * rho_H2_ref - - # capex - capex_ref = ( - cost_tower_ref + cost_nontower_ref - cost_tower_trad_ref - cost_nontower_trad_ref - ) - - # opex - opex_ref = maintenance_rate_ref * capex_ref + wage_ref * staff_hours_ref - - turbine = { - "tower_length": h_ref, - # 'section_diameters': [D_base_ref, D_top_ref], - # 'section_heights': [0., 0. + h_ref], - "section_diameters": [ - D_base_ref, - 0.5 * (D_top_ref + D_base_ref), - D_top_ref, - ], - "section_heights": [0.0, 0.0 + h_ref / 2.0, 0.0 + h_ref], - } - - ## traditional estimates (non-pressurized) - - pressurized_cone = PressurizedTower(1992, turbine) - - assert pressurized_cone.get_volume_tower_inner() == approx(volume_inner_ref) - assert pressurized_cone.get_volume_tower_material(pressure=0)[0] == approx( - volume_wall_trad_ref - ) - assert pressurized_cone.get_volume_tower_material(pressure=0)[1] == approx( - volume_cap_bot_trad_ref - ) - assert pressurized_cone.get_volume_tower_material(pressure=0)[2] == approx( - volume_cap_top_trad_ref - ) - assert pressurized_cone.get_mass_tower_material(pressure=0)[0] == approx( - mass_wall_trad_ref - ) - assert pressurized_cone.get_mass_tower_material(pressure=0)[1] == approx( - mass_cap_bot_trad_ref - ) - assert pressurized_cone.get_mass_tower_material(pressure=0)[2] == approx( - mass_cap_top_trad_ref - ) - - assert np.sum(pressurized_cone.get_cost_tower_material(pressure=0)) == approx( - cost_tower_trad_ref - ) - assert pressurized_cone.get_cost_nontower(traditional=True) == approx( - cost_nontower_trad_ref - ) - - ## pressurized estimates - - assert pressurized_cone.operating_pressure == p_crossover_ref - - assert pressurized_cone.get_volume_tower_material()[0] == approx(volume_wall_ref) - assert pressurized_cone.get_volume_tower_material()[1] == approx(volume_cap_bot_ref) - assert pressurized_cone.get_volume_tower_material()[2] == approx(volume_cap_top_ref) - assert pressurized_cone.get_mass_tower_material()[0] == approx(mass_wall_ref) - assert pressurized_cone.get_mass_tower_material()[1] == approx(mass_cap_bot_ref) - assert pressurized_cone.get_mass_tower_material()[2] == approx(mass_cap_top_ref) - - assert np.sum(pressurized_cone.get_cost_tower_material()) == approx(cost_tower_ref) - assert pressurized_cone.get_cost_nontower() == approx(cost_nontower_ref) - - ## output interface - - # make sure the final values match expectation - assert pressurized_cone.get_capex() == approx(capex_ref) - assert pressurized_cone.get_opex() == approx(opex_ref) - assert pressurized_cone.get_mass_empty() == approx( - mass_wall_ref - + mass_cap_bot_ref - + mass_cap_top_ref - - mass_wall_trad_ref - - mass_cap_bot_trad_ref - - mass_cap_top_trad_ref - ) - assert pressurized_cone.get_capacity_H2() == approx(m_H2_ref) - assert pressurized_cone.get_pressure_H2() == approx(p_crossover_ref) - - if True: - - def test_paper(self): - h_ref = 84.0 - D_bot_ref = 5.66 - D_top_ref = 2.83 - # d_t_ratio_ref= 320. - # rho_density_ref= 7817 - # costrate_steel= 1.50 - cost_tower_ref = 183828 - - cost_tower_trad_ref = 183828 - cost_nontower_trad_ref = 188584 - cost_tower_trad_ref - m_H2_stored_ref = 951 # kg - cost_tower_ref = cost_tower_trad_ref + 21182 - cost_cap_bot_ref = 29668 - cost_cap_top_ref = 5464 - cost_nontower_ref = 2756 + 2000 + 2297 + 2450 + 6300 + 15918 - - turbine = { - "tower_length": 84.0, - "section_diameters": [5.66, 4.9525, 4.245, 3.5375, 2.83], - "section_heights": [0.0, 21.0, 42.0, 63.0, 84.0], - } - - pressurized_tower_instance = PressurizedTower(2004, turbine) - pressurized_tower_instance.run() - - PressurizedTower.compute_frustum_volume(h_ref, D_bot_ref, D_top_ref) - - # traditional sizing should get cost within 5% - assert pressurized_tower_instance.get_cost_tower_material(pressure=0)[0] == approx( - cost_tower_trad_ref, rel=0.05 - ) - assert pressurized_tower_instance.get_cost_tower_material(pressure=0)[1] == 0.0 - assert pressurized_tower_instance.get_cost_tower_material(pressure=0)[2] == 0.0 - assert pressurized_tower_instance.get_cost_nontower(traditional=True) == approx( - cost_nontower_trad_ref, rel=0.05 - ) - - # pressurized sizing should get wall cost within 10% - assert pressurized_tower_instance.get_cost_tower_material()[0] == approx( - cost_tower_ref, rel=0.10 - ) - # not sure why but the cap sizing is way off: 200% error allowed for bottom cap - assert pressurized_tower_instance.get_cost_tower_material()[1] == approx( - cost_cap_bot_ref, rel=2.0 - ) - # not sure why but the cap sizing is way off: 100% error allowed for top cap - assert pressurized_tower_instance.get_cost_tower_material()[2] == approx( - cost_cap_top_ref, rel=1.0 - ) - - # non-tower pressurized sizing evidently has some weird assumptions but should - # get within 10% - assert pressurized_tower_instance.get_cost_nontower() == approx( - cost_nontower_ref, rel=0.1 - ) - - # capacity within 10% - assert pressurized_tower_instance.get_capacity_H2() == approx(m_H2_stored_ref, rel=0.1) diff --git a/tests/h2integrate/test_hydrogen/test_salt_cavern_storage.py b/tests/h2integrate/test_hydrogen/test_salt_cavern_storage.py deleted file mode 100644 index 821ee33e2..000000000 --- a/tests/h2integrate/test_hydrogen/test_salt_cavern_storage.py +++ /dev/null @@ -1,62 +0,0 @@ -import pytest -from pytest import fixture - -from h2integrate.simulation.technologies.hydrogen.h2_storage.salt_cavern.salt_cavern import ( - SaltCavernStorage, -) - - -# Test values are based on conclusions of Papadias 2021 and are in 2019 USD -in_dict = {"h2_storage_kg": 1000000, "system_flow_rate": 100000} - - -@fixture -def salt_cavern_storage(): - salt_cavern_storage = SaltCavernStorage(in_dict) - - return salt_cavern_storage - - -def test_init(): - salt_cavern_storage = SaltCavernStorage(in_dict) - - assert salt_cavern_storage.input_dict is not None - assert salt_cavern_storage.output_dict is not None - - -def test_capex_per_kg(salt_cavern_storage): - salt_cavern_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - salt_cavern_storage.salt_cavern_capex() - ) - assert salt_cavern_storage_capex_per_kg == pytest.approx(25.18622259358959) - - -def test_capex(salt_cavern_storage): - _salt_cavern_storage_capex_per_kg, installed_capex, _compressor_capex = ( - salt_cavern_storage.salt_cavern_capex() - ) - assert installed_capex == pytest.approx(24992482.4198) - - -def test_compressor_capex(salt_cavern_storage): - _salt_cavern_storage_capex_per_kg, _installed_capex, compressor_capex = ( - salt_cavern_storage.salt_cavern_capex() - ) - assert compressor_capex == pytest.approx(6516166.67163) - - -def test_capex_output_dict(salt_cavern_storage): - _salt_caven_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - salt_cavern_storage.salt_cavern_capex() - ) - assert salt_cavern_storage.output_dict["salt_cavern_storage_capex"] == pytest.approx( - 24992482.4198 - ) - - -def test_opex(salt_cavern_storage): - _salt_cavern_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - salt_cavern_storage.salt_cavern_capex() - ) - salt_cavern_storage.salt_cavern_opex() - assert salt_cavern_storage.output_dict["salt_cavern_storage_opex"] == pytest.approx(1461664) diff --git a/tests/h2integrate/test_hydrogen/test_tankinator.py b/tests/h2integrate/test_hydrogen/test_tankinator.py deleted file mode 100644 index a8913a960..000000000 --- a/tests/h2integrate/test_hydrogen/test_tankinator.py +++ /dev/null @@ -1,348 +0,0 @@ -import numpy as np -import pytest - -from h2integrate.simulation.technologies.hydrogen.h2_storage.pressure_vessel import von_mises -from h2integrate.simulation.technologies.hydrogen.h2_storage.pressure_vessel.tankinator import ( - Tank, - TypeITank, - TypeIVTank, - TypeIIITank, -) - - -# test that we the results we got when the code was recieved -class TestTankinator: - def test_tank_type(self): - tank1 = TypeITank("6061_T6_Aluminum") - tank3 = TypeIIITank() - tank4 = TypeIVTank() - - assert tank1.tank_type == 1 - assert tank3.tank_type == 3 - assert tank4.tank_type == 4 - - # def test_tank_type_exc(self): - # with pytest.raises(TypeError): - # TypeITank(2) - - def test_hemicylindrical_static(self): - """a random sphere, and a random cylinder""" - - # L = 2R; should reduce to a sphere! - radius = 1.5 - length = 2 * radius - thickness = 0.19 - volume_exact = 4.0 / 3.0 * np.pi * radius**3 - - assert Tank.compute_hemicylinder_outer_length(length, thickness) == pytest.approx( - length + 2.0 * thickness - ) - assert Tank.compute_hemicylinder_outer_radius(radius, thickness) == pytest.approx( - radius + thickness - ) - assert Tank.compute_hemicylinder_volume(radius, length) == pytest.approx(volume_exact) - - # a lil bit between sphere halves - radius = 2.1 - length = 6.4 - thickness = 0.02 - volume_exact = 4.0 / 3.0 * np.pi * radius**3 + (length - 2 * radius) * (np.pi * radius**2) - - assert Tank.compute_hemicylinder_outer_length(length, thickness) == pytest.approx( - length + 2.0 * thickness - ) - assert Tank.compute_hemicylinder_outer_radius(radius, thickness) == pytest.approx( - radius + thickness - ) - assert Tank.compute_hemicylinder_volume(radius, length) == pytest.approx(volume_exact) - - def test_tankI_geometric(self): - """make sure geometric calls work correctly""" - - # L = 2R; should reduce to a sphere! - radius = 1.5 - length = 2 * radius - thickness = 0.19 - volume_exact = 4.0 / 3.0 * np.pi * radius**3 - - # create tank, set dimensions, check dimensioning - tank = TypeITank("6061_T6_Aluminum") - tank.set_length_radius(length, radius) - tank.thickness = thickness # manual override - tank.volume_inner = Tank.compute_hemicylinder_volume( - tank.get_radius_inner(), tank.get_length_inner() - ) - - assert tank.get_length_outer() == pytest.approx(length + 2.0 * thickness) - assert tank.get_radius_outer() == pytest.approx(radius + thickness) - assert tank.get_volume_inner() == pytest.approx(volume_exact) - - volume_outer_exact = 4.0 / 3.0 * np.pi * (radius + thickness) ** 3 - dvolume_exact = volume_outer_exact - volume_exact - rho_ref = 0.002663 - cost_ref = 4.45 - mass_exact = dvolume_exact * rho_ref # mass of spherical vessel - cost_exact = mass_exact * cost_ref - - assert tank.get_volume_outer() == pytest.approx(volume_outer_exact) - assert tank.get_volume_metal() == pytest.approx(dvolume_exact) - assert tank.get_mass_metal() == pytest.approx(mass_exact) - assert tank.get_cost_metal() == pytest.approx(cost_exact) - - assert tank.get_gravimetric_tank_efficiency() == pytest.approx( - (volume_exact / 1e3) / mass_exact - ) - - def test_tankI_set_functions(self): - """make sure that the inverse geometry spec works""" - - radius = 4.3 - length = 14.9 - volume_exact = 4.0 / 3.0 * np.pi * radius**3 + (length - 2 * radius) * (np.pi * radius**2) - - tank = Tank(1, "316SS") - tank.set_length_radius(length, radius) - assert tank.get_volume_inner() == pytest.approx(volume_exact) - - tank.length_inner = tank.radius_inner = None # reset - tank.set_length_volume(length, volume_exact) - assert tank.get_radius_inner() == pytest.approx(radius) - - tank.length_inner = tank.radius_inner = None # reset - tank.set_radius_volume(radius, volume_exact) - assert tank.get_length_inner() == pytest.approx(length) - - def test_tankinator_typeI_comp(self): - """compare to the tankinator case""" - - T_op = -50 # degC - p_op = 170 # bar - Ltank = 1000 # cm - Vtank = 2994542 # ccm - - # reference values from the excel sheet default values - R_ref = 31.2 - Sy_ref = 2953.475284 - Su_ref = 3327.58062 - density_ref = 0.002663 - costrate_ref = 4.45 - yield_thickness_ref = 2.69 - ultimate_thickness_ref = 3.586389441300914 - disp_vol_tw_ref = 7.462e5 - mass_tw_ref = 1987.08 - cost_tw_ref = 8842.51 - grav_eff_tw_ref = 1.51 - vmS1_0_ref = 1568.544508 - vmS2_0_ref = 699.2722538 - vmS3_0_ref = -170.0 - vmSproof_0_ref = 2258.435564 - vmSburst_0_ref = 3387.653346 - WTAF_0_ref = 1.018052974 - thickness_1_ref = ultimate_thickness_ref * WTAF_0_ref - WTAF_1_ref = 1.002742019 - thickness_2_ref = thickness_1_ref * WTAF_1_ref - thickness_f_ref = 3.6626944898294997 - mass_f_ref = 2031.9 - cost_f_ref = 9041.80 - - # set up w/ lookup shear approximation - tank = TypeITank("6061_T6_Aluminum", shear_approx="lookup") - tank.set_operating_temperature(T_op) - tank.set_operating_pressure(p_op) - tank.set_length_volume(Ltank, Vtank) - - # check agains reference values - assert tank.get_radius_inner() == pytest.approx(R_ref) - assert tank.material.ultimate_shear_fun(T_op) == pytest.approx(Su_ref) - assert tank.material.yield_shear_fun(T_op) == pytest.approx(Sy_ref) - assert tank.material.density == pytest.approx(density_ref) - assert tank.material.cost_rate == pytest.approx(costrate_ref) - - # check the thinwall calculations - assert tank.get_yield_thickness() == pytest.approx(yield_thickness_ref, abs=0.01) - assert tank.get_ultimate_thickness() == pytest.approx(ultimate_thickness_ref, abs=0.001) - assert tank.get_thickness_thinwall() == pytest.approx(ultimate_thickness_ref, abs=0.001) - - # check the implied geometry if we set the thickness to the thinwall - tank.set_thickness_thinwall() - assert tank.get_volume_metal() == pytest.approx(disp_vol_tw_ref, rel=0.001) - assert tank.get_mass_metal() == pytest.approx(mass_tw_ref, rel=0.001) - assert tank.get_cost_metal() == pytest.approx(cost_tw_ref) - assert tank.get_gravimetric_tank_efficiency() == pytest.approx(grav_eff_tw_ref, abs=0.01) - - # check von Mises analysis variables - assert von_mises.S1(p_op, R_ref + ultimate_thickness_ref, R_ref) == pytest.approx( - vmS1_0_ref - ) - assert von_mises.S2(p_op, R_ref + ultimate_thickness_ref, R_ref) == pytest.approx( - vmS2_0_ref - ) - assert von_mises.S3(p_op, R_ref + ultimate_thickness_ref, R_ref) == pytest.approx( - vmS3_0_ref - ) - vmSproof, vmSburst = von_mises.getPeakStresses(p_op, R_ref + ultimate_thickness_ref, R_ref) - assert vmSproof == pytest.approx(vmSproof_0_ref) - assert vmSburst == pytest.approx(vmSburst_0_ref) - assert not Tank.check_thinwall(R_ref, ultimate_thickness_ref) - assert von_mises.wallThicknessAdjustmentFactor( - p_op, R_ref + ultimate_thickness_ref, R_ref, Sy_ref, Su_ref - ) == pytest.approx(WTAF_0_ref) - - # check cycle iterations, through two - WTAF_0, thickness_1 = von_mises.iterate_thickness( - p_op, R_ref, ultimate_thickness_ref, Sy_ref, Su_ref - ) - assert WTAF_0 == pytest.approx(WTAF_0_ref) - assert thickness_1 == pytest.approx(thickness_1_ref) - - WTAF_1, thickness_2 = von_mises.iterate_thickness(p_op, R_ref, thickness_1, Sy_ref, Su_ref) - assert WTAF_1 == pytest.approx(WTAF_1_ref) - assert thickness_2 == pytest.approx(thickness_2_ref) - - # check final value: cycle three times (no tol) to match tankinator - (thickness_cycle, WTAF_cycle, n_iter) = von_mises.cycle( - p_op, R_ref, ultimate_thickness_ref, Sy_ref, Su_ref, max_iter=3, WTAF_tol=0 - ) - - print(thickness_cycle, WTAF_cycle, n_iter) # DEBUG - assert thickness_cycle == pytest.approx(thickness_f_ref) - - # make sure final calculations are correct - tank.set_thickness_vonmises(p_op, T_op, max_cycle_iter=3, adj_fac_tol=0.0) - assert tank.get_thickness() == pytest.approx(thickness_f_ref) - assert tank.get_mass_metal() == pytest.approx(mass_f_ref, abs=0.1) - assert tank.get_cost_metal() == pytest.approx(cost_f_ref, abs=0.01) - - def test_tankinator_typeIII_comp(self): - """compare to the tankinator case""" - - T_op = 20.0 # degC - p_op = 250.0 # bar - Rtank = 16.0 - Ltank = 1219.0 # cm - Vtank = 971799.0 # ccm - - # reference values from the excel sheet default values, best estimate - R_ref = 16.0 - thickness_liner_ref = 0.61 - thickness_ideal_jacket_ref = 0.602759006 - Nlayer_jacket_ref = 7 - thickness_jacket_ref = 0.64008 - length_liner_ref = 1220.218176 - radius_liner_ref = 16.60908798 - V_outer_liner_ref = 1047.900361 * 1000 - V_liner_ref = 76101.03372 - m_liner_ref = 202.66 - cost_liner_ref = 901.82 - length_outer_ref = 1221.498336 - radius_outer_ref = 17.24916798 - V_outer_ref = 1131.022248 * 1000 - V_jacket_ref = 83121.88687 - m_jacket_ref = 133.91 - cost_jacket_ref = 4104.32 - m_tank_ref = 336.57 - cost_tank_ref = 5006.15 - gravimetric_tank_efficiency_ref = 2.89 - - # set up w/ lookup shear approximation - tank = TypeIIITank() - tank.set_operating_temperature(T_op) - tank.set_operating_pressure(p_op) - tank.set_length_radius(Ltank, Rtank) - - tank.set_thicknesses_thinwall() - - # check against reference values - assert tank.get_radius_inner() == pytest.approx(R_ref) - assert tank.get_volume_inner() == pytest.approx(Vtank) - assert tank.thickness_liner == pytest.approx(thickness_liner_ref, abs=0.01) - assert tank.thickness_ideal_jacket == pytest.approx(thickness_ideal_jacket_ref) - assert tank.Nlayer_jacket == pytest.approx(Nlayer_jacket_ref) - assert tank.thickness_jacket == pytest.approx(thickness_jacket_ref) - - assert tank.get_length_liner() == pytest.approx(length_liner_ref) - assert tank.get_radius_liner() == pytest.approx(radius_liner_ref) - assert tank.get_volume_outer_liner() == pytest.approx(V_outer_liner_ref) - assert tank.get_volume_liner() == pytest.approx(V_liner_ref) - - assert tank.get_length_outer() == pytest.approx(length_outer_ref) - assert tank.get_radius_outer() == pytest.approx(radius_outer_ref) - assert tank.get_volume_outer() == pytest.approx(V_outer_ref) - assert tank.get_volume_jacket() == pytest.approx(V_jacket_ref) - - assert tank.get_mass_liner() == pytest.approx(m_liner_ref, abs=0.01) - assert tank.get_mass_jacket() == pytest.approx(m_jacket_ref, abs=0.01) - assert tank.get_cost_liner() == pytest.approx(cost_liner_ref, abs=0.01) - assert tank.get_cost_jacket() == pytest.approx(cost_jacket_ref, abs=0.01) - assert tank.get_mass_tank() == pytest.approx(m_tank_ref, abs=0.01) - assert tank.get_cost_tank() == pytest.approx(cost_tank_ref, abs=0.01) - assert tank.get_gravimetric_tank_efficiency() == pytest.approx( - gravimetric_tank_efficiency_ref, abs=0.01 - ) - - def test_tankinator_typeIV_comp(self): - """compare to the tankinator case""" - - T_op = 20.0 # degC - p_op = 350.0 # bar - Rtank = 50.0 - Ltank = 1000.0 # cm - Vtank = 7592182.0 # ccm - - # reference values from the excel sheet default values, best estimate - R_ref = 50.0 - thickness_liner_ref = 0.4 - thickness_ideal_jacket_ref = 3.241375931 - Nlayer_jacket_ref = 36 - thickness_jacket_ref = 3.29184 - # length_liner_ref= 1220.218176 - radius_liner_ref = 50.4 - # V_outer_liner_ref= 1047.900361*1000 - # V_liner_ref= 76101.03372 - # m_liner_ref= 202.66 - # cost_liner_ref= 901.82 - # length_outer_ref= 1221.498336 - # radius_outer_ref= 17.24916798 - # V_outer_ref= 1131.022248*1000 - # V_jacket_ref= 83121.88687 - # m_jacket_ref= 133.91 - # cost_jacket_ref= 4104.32 - # m_tank_ref= 336.57 - # cost_tank_ref= 5006.15 - # gravimetric_tank_efficiency_ref= 2.89 - - # set up w/ lookup shear approximation - tank = TypeIVTank() - tank.set_operating_temperature(T_op) - tank.set_operating_pressure(p_op) - tank.set_length_radius(Ltank, Rtank) - - tank.set_thicknesses_thinwall() - - # check against reference values - assert tank.get_radius_inner() == pytest.approx(R_ref) - assert tank.get_volume_inner() == pytest.approx(Vtank) - assert tank.thickness_liner == pytest.approx(thickness_liner_ref, abs=0.01) - assert tank.thickness_ideal_jacket == pytest.approx(thickness_ideal_jacket_ref) - assert tank.Nlayer_jacket == pytest.approx(Nlayer_jacket_ref) - assert tank.thickness_jacket == pytest.approx(thickness_jacket_ref) - - # assert tank.get_length_liner() == pytest.approx(length_liner_ref) - assert tank.get_radius_liner() == pytest.approx(radius_liner_ref) - # assert tank.get_volume_outer_liner() == pytest.approx(V_outer_liner_ref) - # assert tank.get_volume_liner() == pytest.approx(V_liner_ref) - - # assert tank.get_length_outer() == pytest.approx(length_outer_ref) - # assert tank.get_radius_outer() == pytest.approx(radius_outer_ref) - # assert tank.get_volume_outer() == pytest.approx(V_outer_ref) - # assert tank.get_volume_jacket() == pytest.approx(V_jacket_ref) - - # assert tank.get_mass_liner() == pytest.approx(m_liner_ref, abs= 0.01) - # assert tank.get_mass_jacket() == pytest.approx(m_jacket_ref, abs= 0.01) - # assert tank.get_cost_liner() == pytest.approx(cost_liner_ref, abs= 0.01) - # assert tank.get_cost_jacket() == pytest.approx(cost_jacket_ref, abs= 0.01) - # assert tank.get_mass_tank() == pytest.approx(m_tank_ref, abs= 0.01) - # assert tank.get_cost_tank() == pytest.approx(cost_tank_ref, abs= 0.01) - # assert tank.get_gravimetric_tank_efficiency() == pytest.approx( - # gravimetric_tank_efficiency_ref, abs= 0.01 - # ) diff --git a/tests/h2integrate/test_hydrogen/test_underground_pipe_storage.py b/tests/h2integrate/test_hydrogen/test_underground_pipe_storage.py deleted file mode 100644 index 68ded360f..000000000 --- a/tests/h2integrate/test_hydrogen/test_underground_pipe_storage.py +++ /dev/null @@ -1,65 +0,0 @@ -import pytest -from pytest import fixture - -from h2integrate.simulation.technologies.hydrogen.h2_storage.pipe_storage.underground_pipe_storage import ( # noqa: E501 - UndergroundPipeStorage, -) - - -# Test values are based on conclusions of Papadias 2021 and are in 2019 USD - -in_dict = { - "h2_storage_kg": 1000000, - "system_flow_rate": 100000, - "compressor_output_pressure": 100, -} - - -@fixture -def pipe_storage(): - pipe_storage = UndergroundPipeStorage(in_dict) - - return pipe_storage - - -def test_init(): - pipe_storage = UndergroundPipeStorage(in_dict) - - assert pipe_storage.input_dict is not None - assert pipe_storage.output_dict is not None - - -def test_capex_per_kg(pipe_storage): - pipe_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - pipe_storage.pipe_storage_capex() - ) - assert pipe_storage_capex_per_kg == pytest.approx(512.689247292) - - -def test_capex(pipe_storage): - _pipe_storage_capex_per_kg, installed_capex, _compressor_capex = ( - pipe_storage.pipe_storage_capex() - ) - assert installed_capex == pytest.approx(508745483.851) - - -def test_compressor_capex(pipe_storage): - _pipe_storage_capex_per_kg, _installed_capex, compressor_capex = ( - pipe_storage.pipe_storage_capex() - ) - assert compressor_capex == pytest.approx(5907549.297) - - -def test_capex_output_dict(pipe_storage): - _pipe_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - pipe_storage.pipe_storage_capex() - ) - assert pipe_storage.output_dict["pipe_storage_capex"] == pytest.approx(508745483.851) - - -def test_opex(pipe_storage): - _pipe_storage_capex_per_kg, _installed_capex, _compressor_capex = ( - pipe_storage.pipe_storage_capex() - ) - pipe_storage.pipe_storage_opex() - assert pipe_storage.output_dict["pipe_storage_opex"] == pytest.approx(16439748) diff --git a/tests/h2integrate/test_iron/input/h2integrate_config_modular.yaml b/tests/h2integrate/test_iron/input/h2integrate_config_modular.yaml index 32311e176..e080e847b 100644 --- a/tests/h2integrate/test_iron/input/h2integrate_config_modular.yaml +++ b/tests/h2integrate/test_iron/input/h2integrate_config_modular.yaml @@ -61,7 +61,6 @@ electrolyzer: hydrogen_dmd: rating: 1160 # MW cluster_rating_MW: 40 - pem_control_type: 'basic' eol_eff_percent_loss: 13 #eol defined as x% change in efficiency from bol uptime_hours_until_eol: 77600 #number of 'on' hours until electrolyzer reaches eol include_degradation_penalty: True #include degradation diff --git a/tests/h2integrate/test_iron/test_iron_ore.py b/tests/h2integrate/test_iron/test_iron_ore.py index a783125fa..b6a124290 100644 --- a/tests/h2integrate/test_iron/test_iron_ore.py +++ b/tests/h2integrate/test_iron/test_iron_ore.py @@ -4,7 +4,7 @@ from pytest import approx, fixture -from h2integrate.simulation.technologies.iron import iron +from h2integrate.converters.iron import iron @fixture @@ -122,11 +122,7 @@ def test_run_martin_iron_ore(iron_ore, subtests): def test_refit_coefficients(iron_ore, subtests): # Determine the model directory based on the model name iron_tech_dir = ( - Path(__file__).parent.parent.parent.parent - / "h2integrate" - / "simulation" - / "technologies" - / "iron" + Path(__file__).parent.parent.parent.parent / "h2integrate" / "converters" / "iron" ) model_name = iron_ore["iron"]["cost_model"]["name"] model_dir = iron_tech_dir / model_name diff --git a/tests/h2integrate/test_iron/test_iron_post.py b/tests/h2integrate/test_iron/test_iron_post.py index cde180cc8..1ee4f6dc5 100644 --- a/tests/h2integrate/test_iron/test_iron_post.py +++ b/tests/h2integrate/test_iron/test_iron_post.py @@ -4,7 +4,7 @@ from pytest import approx, fixture -from h2integrate.simulation.technologies.iron import iron +from h2integrate.converters.iron import iron @fixture @@ -165,11 +165,7 @@ def test_rosner_override(iron_post, subtests): def test_refit_coefficients(iron_post, subtests): # Determine the model directory based on the model name iron_tech_dir = ( - Path(__file__).parent.parent.parent.parent - / "h2integrate" - / "simulation" - / "technologies" - / "iron" + Path(__file__).parent.parent.parent.parent / "h2integrate" / "converters" / "iron" ) model_name = iron_post["iron"]["cost_model"]["name"] model_dir = iron_tech_dir / model_name diff --git a/tests/h2integrate/test_iron/test_iron_win.py b/tests/h2integrate/test_iron/test_iron_win.py index 5104e75cf..b8dd58c8f 100644 --- a/tests/h2integrate/test_iron/test_iron_win.py +++ b/tests/h2integrate/test_iron/test_iron_win.py @@ -4,7 +4,7 @@ from pytest import approx, fixture -from h2integrate.simulation.technologies.iron import iron +from h2integrate.converters.iron import iron @fixture @@ -167,11 +167,7 @@ def test_rosner_override(iron_win, subtests): def test_refit_coefficients(iron_win, subtests): # Determine the model directory based on the model name iron_tech_dir = ( - Path(__file__).parent.parent.parent.parent - / "h2integrate" - / "simulation" - / "technologies" - / "iron" + Path(__file__).parent.parent.parent.parent / "h2integrate" / "converters" / "iron" ) model_name = iron_win["iron"]["cost_model"]["name"] model_dir = iron_tech_dir / model_name diff --git a/tests/h2integrate/test_offshore/__init__.py b/tests/h2integrate/test_offshore/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/h2integrate/test_offshore/test_fixed_platform.py b/tests/h2integrate/test_offshore/test_fixed_platform.py deleted file mode 100644 index f9a52b4eb..000000000 --- a/tests/h2integrate/test_offshore/test_fixed_platform.py +++ /dev/null @@ -1,80 +0,0 @@ -from pathlib import Path - -import ORBIT as orbit -import pytest - -from h2integrate.simulation.technologies.offshore.fixed_platform import ( - install_platform, - calc_platform_opex, - calc_substructure_mass_and_cost, -) - - -"""Sources: - - [1] M. Maness, B. Maples and A. Smith, "NREL Offshore Balance-of-System Model," National - Renewable Energy Laboratory, 2017. https://www.nrel.gov/docs/fy17osti/66874.pdf -""" - - -@pytest.mark.skip(reason="no way of currently testing this") -@pytest.fixture -def config(): - offshore_path = ( - Path(__file__).parents[3] / "h2integrate" / "simulation" / "technologies" / "offshore" - ) - - return orbit.load_config(offshore_path / "example_fixed_project.yaml") - - -@pytest.mark.skip(reason="no way of currently testing this") -def test_install_platform(config): - """ - Test the code that calculates the platform installation cost - [1]: equations (91),(113),(98) - """ - distance = 24 - mass = 2100 - area = 500 - - cost = install_platform(mass, area, distance, install_duration=14) - - assert pytest.approx(cost) == 7200014 - - -def test_calc_substructure_cost(config): - """ - Test the code that calculates the CapEx from fixed_platform.py - [1]: equations (81),(83),(84) - """ - topmass = 200 - toparea = 1000 - depth = 45 - - cost, _ = calc_substructure_mass_and_cost(topmass, toparea, depth) - - assert pytest.approx(cost) == 7640000 - - -def test_calc_substructure_mass(config): - """ - Test the code that calculates the CapEx from fixed_platform.py - [1]: equations (81),(83),(84) - """ - topmass = 200 - toparea = 1000 - depth = 45 - - _, mass = calc_substructure_mass_and_cost(topmass, toparea, depth) - - assert pytest.approx(mass, 0.1) == 372.02 - - -def test_calc_platform_opex(): - """ - Test the code that calculates the OpEx from fixed_platform.py - """ - capex = 28e6 - opex_rate = 0.01 - cost = calc_platform_opex(capex, opex_rate) - - assert pytest.approx(cost) == 28e4 diff --git a/tests/h2integrate/test_offshore/test_floating_platform.py b/tests/h2integrate/test_offshore/test_floating_platform.py deleted file mode 100644 index 5cc996549..000000000 --- a/tests/h2integrate/test_offshore/test_floating_platform.py +++ /dev/null @@ -1,102 +0,0 @@ -from pathlib import Path - -import ORBIT as orbit -import pytest - -from h2integrate.simulation.technologies.offshore.floating_platform import ( - install_platform, - calc_platform_opex, - calc_substructure_mass_and_cost, -) - - -"""Sources: - - [1] M. Maness, B. Maples and A. Smith, "NREL Offshore Balance-of-System Model," National - Renewable Energy Laboratory, 2017. https://www.nrel.gov/docs/fy17osti/66874.pdf -""" - - -@pytest.fixture -def config(): - offshore_path = ( - Path(__file__).parents[3] / "h2integrate" / "simulation" / "technologies" / "offshore" - ) - - return orbit.load_config(offshore_path / "example_floating_project.yaml") - - -def test_install_platform(config): - """ - Test the code that calculates the platform installation cost - [1]: equations (91),(113),(98) - """ - distance = 24 - mass = 2100 - area = 500 - - cost = install_platform(mass, area, distance, install_duration=14, foundation="floating") - - assert pytest.approx(cost) == 7142871 - - -def test_calc_substructure_cost(config): - """ - Test the code that calculates the CapEx from floating_platform.py - [1]: equations (81),(83),(84) - """ - topmass = 200 - toparea = 1000 - depth = 500 - - cost, _ = calc_substructure_mass_and_cost( - topmass, - toparea, - depth, - fab_cost_rate=14500, - design_cost=4500000, - sub_cost_rate=3000, - line_cost=850000, - anchor_cost=120000, - anchor_mass=20, - line_mass=100000, - num_lines=4, - ) - - assert pytest.approx(cost) == 23040000 - - -def test_calc_substructure_mass(config): - """ - Test the code that calculates the CapEx from floating_platform.py - [1]: equations (81),(83),(84) - """ - topmass = 200 - toparea = 1000 - depth = 500 - - _, mass = calc_substructure_mass_and_cost( - topmass, - toparea, - depth, - fab_cost_rate=14500, - design_cost=4500000, - sub_cost_rate=3000, - line_cost=850000, - anchor_cost=120000, - anchor_mass=20, - line_mass=100000, - num_lines=4, - ) - - assert pytest.approx(mass, 0.1) == 680.0 - - -def test_calc_platform_opex(): - """ - Test the code that calculates the OpEx from floating_platform.py - """ - capex = 28e6 - opex_rate = 0.01 - cost = calc_platform_opex(capex, opex_rate) - - assert pytest.approx(cost) == 28e4 diff --git a/tests/h2integrate/test_steel.py b/tests/h2integrate/test_steel.py deleted file mode 100644 index b63956eb4..000000000 --- a/tests/h2integrate/test_steel.py +++ /dev/null @@ -1,298 +0,0 @@ -import copy - -from pytest import approx, raises, fixture - -from h2integrate.simulation.technologies.steel import steel - - -ng_prices_dict = { - "2035": 3.76232, - "2036": 3.776032, - "2037": 3.812906, - "2038": 3.9107960000000004, - "2039": 3.865776, - "2040": 3.9617400000000003, - "2041": 4.027136, - "2042": 4.017166, - "2043": 3.9715339999999997, - "2044": 3.924314, - "2045": 3.903287, - "2046": 3.878192, - "2047": 3.845413, - "2048": 3.813366, - "2049": 3.77735, - "2050": 3.766164, - "2051": 3.766164, - "2052": 3.766164, - "2053": 3.766164, - "2054": 3.766164, - "2055": 3.766164, - "2056": 3.766164, - "2057": 3.766164, - "2058": 3.766164, - "2059": 3.766164, - "2060": 3.766164, - "2061": 3.766164, - "2062": 3.766164, - "2063": 3.766164, - "2064": 3.766164, -} -grid_prices_dict = { - "2035": 89.42320514456621, - "2036": 89.97947569251141, - "2037": 90.53574624045662, - "2038": 91.09201678840184, - "2039": 91.64828733634704, - "2040": 92.20455788429224, - "2041": 89.87291235917809, - "2042": 87.54126683406393, - "2043": 85.20962130894978, - "2044": 82.87797578383562, - "2045": 80.54633025872147, - "2046": 81.38632144593608, - "2047": 82.22631263315068, - "2048": 83.0663038203653, - "2049": 83.90629500757991, - "2050": 84.74628619479452, - "2051": 84.74628619479452, - "2052": 84.74628619479452, - "2053": 84.74628619479452, - "2054": 84.74628619479452, - "2055": 84.74628619479452, - "2056": 84.74628619479452, - "2057": 84.74628619479452, - "2058": 84.74628619479452, - "2059": 84.74628619479452, - "2060": 84.74628619479452, - "2061": 84.74628619479452, - "2062": 84.74628619479452, - "2063": 84.74628619479452, - "2064": 84.74628619479452, -} - -financial_assumptions = { - "total income tax rate": 0.2574, - "capital gains tax rate": 0.15, - "leverage after tax nominal discount rate": 0.10893, - "debt equity ratio of initial financing": 0.624788, - "debt interest rate": 0.050049, -} - - -@fixture -def cost_config(): - config = steel.SteelCostModelConfig( - operational_year=2035, - plant_capacity_mtpy=1084408.2137715619, - lcoh=4.2986685034417045, - feedstocks=steel.Feedstocks(natural_gas_prices=ng_prices_dict, oxygen_market_price=0), - o2_heat_integration=False, - ) - return config - - -def test_run_steel_model(): - capacity = 100.0 - capacity_factor = 0.9 - - steel_production_mtpy = steel.run_steel_model(capacity, capacity_factor) - - assert steel_production_mtpy == 90.0 - - -def test_steel_cost_model(subtests, cost_config): - res: steel.SteelCostModelOutputs = steel.run_steel_cost_model(cost_config) - - with subtests.test("CapEx"): - assert res.total_plant_cost == approx(617972269.2565368) - with subtests.test("Fixed OpEx"): - assert res.total_fixed_operating_cost == approx(104244740.28004119) - with subtests.test("Installation"): - assert res.installation_cost == approx(209403678.7623758) - - -def test_steel_finance_model(cost_config): - # Parameter -> Hydrogen/Steel/Ammonia - costs: steel.SteelCostModelOutputs = steel.run_steel_cost_model(cost_config) - - plant_capacity_factor = 0.9 - steel_production_mtpy = steel.run_steel_model( - cost_config.plant_capacity_mtpy, plant_capacity_factor - ) - - config = steel.SteelFinanceModelConfig( - plant_life=30, - plant_capacity_mtpy=cost_config.plant_capacity_mtpy, - plant_capacity_factor=plant_capacity_factor, - steel_production_mtpy=steel_production_mtpy, - lcoh=cost_config.lcoh, - feedstocks=cost_config.feedstocks, - grid_prices=grid_prices_dict, - financial_assumptions=financial_assumptions, - costs=costs, - ) - - lcos_expected = 1003.6498479621724 - - res: steel.SteelFinanceModelOutputs = steel.run_steel_finance_model(config) - - assert res.sol.get("price") == lcos_expected - - -def test_steel_size_h2_input(subtests): - config = steel.SteelCapacityModelConfig( - hydrogen_amount_kgpy=73288888.8888889, - input_capacity_factor_estimate=0.9, - feedstocks=steel.Feedstocks(natural_gas_prices=ng_prices_dict, oxygen_market_price=0), - ) - - res: steel.SteelCapacityModelOutputs = steel.run_size_steel_plant_capacity(config) - - with subtests.test("steel plant size"): - assert res.steel_plant_capacity_mtpy == approx(1000000) - with subtests.test("hydrogen input"): - assert res.hydrogen_amount_kgpy == approx(73288888.8888889) - - -def test_steel_size_steel_input(subtests): - config = steel.SteelCapacityModelConfig( - desired_steel_mtpy=1000000, - input_capacity_factor_estimate=0.9, - feedstocks=steel.Feedstocks(natural_gas_prices=ng_prices_dict, oxygen_market_price=0), - ) - - res: steel.SteelCapacityModelOutputs = steel.run_size_steel_plant_capacity(config) - - with subtests.test("steel plant size"): - assert res.steel_plant_capacity_mtpy == approx(1111111.111111111) - with subtests.test("hydrogen input"): - assert res.hydrogen_amount_kgpy == approx(73288888.8888889) - - -def test_run_steel_full_model(subtests): - config = { - "steel": { - "capacity": { - "input_capacity_factor_estimate": 0.9, - "desired_steel_mtpy": 1000000, - }, - "costs": { - "operational_year": 2035, - "o2_heat_integration": False, - "feedstocks": { - "natural_gas_prices": ng_prices_dict, - "oxygen_market_price": 0, - }, - "lcoh": 4.2986685034417045, - }, - "finances": { - "plant_life": 30, - "lcoh": 4.2986685034417045, - "grid_prices": grid_prices_dict, - "financial_assumptions": financial_assumptions, - }, - } - } - - res = steel.run_steel_full_model(config) - - with subtests.test("output length"): - assert len(res) == 3 - - with subtests.test("h2 mass per year"): - assert res[0].hydrogen_amount_kgpy == approx(73288888.8888889) - - with subtests.test("plant cost"): - assert res[1].total_plant_cost == approx(627667493.7760644) - with subtests.test("Installation"): - assert res[1].installation_cost == approx(212913296.16069925) - with subtests.test("steel price"): - assert res[2].sol.get("price") == approx(1000.0534906485253) - - -def test_run_steel_full_model_changing_lcoh(subtests): - config_0 = { - "steel": { - "capacity": { - "input_capacity_factor_estimate": 0.9, - "desired_steel_mtpy": 1000000, - }, - "costs": { - "operational_year": 2035, - "o2_heat_integration": False, - "feedstocks": { - "natural_gas_prices": ng_prices_dict, - "oxygen_market_price": 0, - }, - "lcoh": 4.2986685034417045, - }, - "finances": { - "plant_life": 30, - "lcoh": 4.2986685034417045, - "grid_prices": grid_prices_dict, - "financial_assumptions": financial_assumptions, - }, - } - } - - config_1 = copy.deepcopy(config_0) - config_1["steel"]["costs"]["lcoh"] = 20.0 - config_1["steel"]["finances"]["lcoh"] = 20.0 - - res0 = steel.run_steel_full_model(config_0) - res1 = steel.run_steel_full_model(config_1) - - with subtests.test("output length 0"): - assert len(res0) == 3 - with subtests.test("output length 1"): - assert len(res1) == 3 - with subtests.test("res0 res1 equal h2 mass per year"): - assert res0[0].hydrogen_amount_kgpy == res1[0].hydrogen_amount_kgpy - with subtests.test("res0 res1 equal plant cost"): - assert res0[1].total_plant_cost == res1[1].total_plant_cost - with subtests.test("res0 price lt res1 price"): - assert res0[2].sol.get("price") < res1[2].sol.get("price") - with subtests.test("raise value error when LCOH values do not match"): - config_1["steel"]["finances"]["lcoh"] = 40.0 - with raises(ValueError, match="steel cost LCOH and steel finance LCOH are not equal"): - res1 = steel.run_steel_full_model(config_1) - - -def test_run_steel_full_model_changing_feedstock_transport_costs(subtests): - config = { - "steel": { - "capacity": { - "input_capacity_factor_estimate": 0.9, - "desired_steel_mtpy": 1000000, - }, - "costs": { - "operational_year": 2035, - "o2_heat_integration": False, - "feedstocks": { - "natural_gas_prices": ng_prices_dict, - "oxygen_market_price": 0, - "lime_transport_cost": 47.72, - "carbon_transport_cost": 64.91, - "iron_ore_pellet_transport_cost": 0.63, - }, - "lcoh": 4.2986685034417045, - }, - "finances": { - "plant_life": 30, - "lcoh": 4.2986685034417045, - "grid_prices": grid_prices_dict, - "financial_assumptions": financial_assumptions, - }, - } - } - - res = steel.run_steel_full_model(config) - - with subtests.test("plant cost"): - assert res[1].total_plant_cost == approx(627667493.7760644) - - with subtests.test("Installation"): - assert res[1].installation_cost == approx(213896544.47120154) - - with subtests.test("steel price"): - assert res[2].sol.get("price") == approx(1005.7008348727317)