1616 project_payback_period_parameter , inflation_cost_during_construction_output_parameter , \
1717 interest_during_construction_output_parameter , total_capex_parameter_output_parameter , \
1818 overnight_capital_cost_output_parameter , CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME , \
19- _YEAR_INDEX_VALUE_EXPLANATION_SNIPPET , investment_tax_credit_output_parameter , lcoh_output_parameter , lcoc_output_parameter
20- from geophires_x .ParameterUtils import expand_schedule_dsl
19+ _YEAR_INDEX_VALUE_EXPLANATION_SNIPPET , investment_tax_credit_output_parameter , lcoh_output_parameter , \
20+ lcoc_output_parameter
21+ from geophires_x .ParameterUtils import expand_schedule_dsl , CALCULATED_PARAMETER_PLACEHOLDER_VALUE
2122from geophires_x .GeoPHIRESUtils import quantity
2223from geophires_x .OptionList import Configuration , WellDrillingCostCorrelation , EconomicModel , EndUseOptions , PlantType , \
2324 _WellDrillingCostCorrelationCitation
@@ -652,6 +653,8 @@ def __init__(self, model: Model):
652653 f'For traditional hydrothermal reservoirs, this parameter should be set to $0.'
653654 )
654655
656+ before_stim_modifiers_note = f'before adjustment factor, indirect costs, and contingency'
657+
655658 max_stimulation_cost_per_well_MUSD = 100
656659 self .stimulation_cost_per_injection_well = \
657660 self .ParameterDict [self .stimulation_cost_per_injection_well .Name ] = floatParameter (
@@ -663,7 +666,7 @@ def __init__(self, model: Model):
663666 PreferredUnits = CurrencyUnit .MDOLLARS ,
664667 CurrentUnits = CurrencyUnit .MDOLLARS ,
665668 Provided = False ,
666- ToolTipText = 'Reservoir stimulation capital cost per injection well before indirect costs and contingency '
669+ ToolTipText = f 'Reservoir stimulation capital cost per injection well { before_stim_modifiers_note } '
667670 )
668671
669672 stimulation_cost_per_production_well_default_value_MUSD = 0
@@ -674,15 +677,30 @@ def __init__(self, model: Model):
674677 self .ParameterDict [self .stimulation_cost_per_production_well .Name ] = floatParameter (
675678 'Reservoir Stimulation Capital Cost per Production Well' ,
676679 DefaultValue = stimulation_cost_per_production_well_default_value_MUSD ,
677- Min = 0 ,
680+ Min = - 1 ,
678681 Max = max_stimulation_cost_per_well_MUSD ,
679682 UnitType = Units .CURRENCY ,
680683 PreferredUnits = CurrencyUnit .MDOLLARS ,
681684 CurrentUnits = CurrencyUnit .MDOLLARS ,
682- ToolTipText = f'Reservoir stimulation capital cost per production well before indirect costs and contingency '
685+ ToolTipText = f'Reservoir stimulation capital cost per production well { before_stim_modifiers_note } '
683686 f'{ stimulation_cost_per_production_well_default_value_note } '
684687 )
685688
689+ self .stimulation_cost_per_fracture_surface_area = \
690+ self .ParameterDict [self .stimulation_cost_per_fracture_surface_area .Name ] = floatParameter (
691+ 'Reservoir Stimulation Capital Cost per Fracture Surface Area' ,
692+ Min = 0 ,
693+ Max = 1000 ,
694+ DefaultValue = 0.9 ,
695+ UnitType = Units .COSTPERAREA ,
696+ PreferredUnits = CostPerAreaUnit .DOLLARSPERMETERS2 ,
697+ CurrentUnits = CostPerAreaUnit .DOLLARSPERMETERS2 ,
698+ ToolTipText = f'Direct reservoir stimulation cost per fracture surface area { before_stim_modifiers_note } . '
699+ f'By default, this applies only to injection wells. To include production wells, set '
700+ f'{ self .stimulation_cost_per_production_well .Name } = { CALCULATED_PARAMETER_PLACEHOLDER_VALUE } .'
701+ )
702+
703+ # noinspection SpellCheckingInspection
686704 self .ccstimadjfactor = self .ParameterDict [self .ccstimadjfactor .Name ] = floatParameter (
687705 "Reservoir Stimulation Capital Cost Adjustment Factor" ,
688706 DefaultValue = 1.0 ,
@@ -2822,6 +2840,8 @@ def _warn(_msg: str) -> None:
28222840 if sam_em_only_param .Provided :
28232841 raise NotImplementedError (f'{ sam_em_only_param .Name } is only supported for SAM Economic Models' )
28242842
2843+ self ._validate_read_stimulation_cost_parameters (model )
2844+
28252845 else :
28262846 model .logger .info ("No parameters read because no content provided" )
28272847
@@ -2865,6 +2885,34 @@ def _set_ratio(frac: float) -> None:
28652885
28662886 model .logger .info (f'complete { __class__ !s} : { sys ._getframe ().f_code .co_name } ' )
28672887
2888+ def _validate_read_stimulation_cost_parameters (self , _model : Model ) -> None :
2889+ def _raise_mutually_exclusive_error (param1 : Parameter , param2 : Parameter ) -> None :
2890+ raise ValueError (f'Cannot provide both { param1 .Name } and { param2 .Name } parameters. ' )
2891+
2892+ if self .stimulation_cost_per_fracture_surface_area .Provided :
2893+ if self .ccstimfixed .Provided :
2894+ _raise_mutually_exclusive_error (
2895+ self .ccstimfixed ,
2896+ self .stimulation_cost_per_fracture_surface_area
2897+ )
2898+ if self .stimulation_cost_per_injection_well .Provided :
2899+ _raise_mutually_exclusive_error (
2900+ self .stimulation_cost_per_injection_well ,
2901+ self .stimulation_cost_per_fracture_surface_area
2902+ )
2903+ if self .stimulation_cost_per_production_well .Provided and \
2904+ self .stimulation_cost_per_production_well .value != \
2905+ CALCULATED_PARAMETER_PLACEHOLDER_VALUE : # Placeholder indicates production wells are stimulated
2906+ _raise_mutually_exclusive_error (
2907+ self .stimulation_cost_per_production_well ,
2908+ self .stimulation_cost_per_fracture_surface_area
2909+ )
2910+ elif self .stimulation_cost_per_production_well .Provided and self .stimulation_cost_per_production_well .value < 0 :
2911+ raise ValueError (f'{ self .stimulation_cost_per_production_well .Name } must be positive' )
2912+
2913+ # TODO validate fixed stimulation cost param mutual exclusivity with stim cost per well params (warn instead of
2914+ # raising error for backwards compatibility where applicable)
2915+
28682916 def sync_interest_rate (self , model ):
28692917 def discount_rate_display () -> str :
28702918 return str (self .discountrate .quantity ()).replace (' dimensionless' , '' )
@@ -3099,6 +3147,7 @@ def calculate_wellfield_costs(self, model: Model) -> None:
30993147
31003148 def calculate_stimulation_costs (self , model : Model ) -> PlainQuantity :
31013149 production_wells_stimulated : bool = self .stimulation_cost_per_production_well .Provided
3150+
31023151 if self .ccstimfixed .Valid :
31033152 stimulation_costs_cstim_u = self .ccstimfixed .quantity ().to (self .Cstim .CurrentUnits ).magnitude
31043153
@@ -3122,6 +3171,39 @@ def calculate_stimulation_costs(self, model: Model) -> PlainQuantity:
31223171
31233172 ret = quantity (stimulation_costs_cstim_u , self .Cstim .CurrentUnits )
31243173 else :
3174+ if self .stimulation_cost_per_fracture_surface_area .Provided :
3175+ total_fracture_surface_area_q = (model .reserv .fracareacalc .quantity () * model .reserv .fracnumbcalc .value ).to (
3176+ self .stimulation_cost_per_fracture_surface_area .CurrentUnits .get_area_unit_str ())
3177+ direct_stim_cost_q = (total_fracture_surface_area_q *
3178+ self .stimulation_cost_per_fracture_surface_area .quantity ())
3179+ inj_to_prod_cost_ratio = 1 if not production_wells_stimulated else \
3180+ model .wellbores .ninj .value / (model .wellbores .nprod .value + model .wellbores .ninj .value )
3181+
3182+ # Coerces equal injection and production well costs required for stimulation cost per well output
3183+ # display heuristic
3184+ per_well_cost_precision = 3
3185+
3186+ self .stimulation_cost_per_injection_well .value = round (
3187+ quantity (
3188+ inj_to_prod_cost_ratio * direct_stim_cost_q / model .wellbores .ninj .value ,
3189+ self .stimulation_cost_per_fracture_surface_area .CurrentUnits .get_currency_unit_str ()
3190+ ).to (self .stimulation_cost_per_injection_well .CurrentUnits ).magnitude ,
3191+ per_well_cost_precision
3192+ )
3193+
3194+ self .stimulation_cost_per_production_well .value = round (
3195+ quantity (
3196+ (1 - inj_to_prod_cost_ratio ) * direct_stim_cost_q / model .wellbores .nprod .value ,
3197+ self .stimulation_cost_per_fracture_surface_area .CurrentUnits .get_currency_unit_str ()
3198+ ).to (self .stimulation_cost_per_production_well .CurrentUnits ).magnitude ,
3199+ per_well_cost_precision
3200+ )
3201+
3202+ # In an ideal world we might use separate calculated values instead of mutating
3203+ # stimulation_cost_per_injection_well and stimulation_cost_per_production_well above. But the current
3204+ # pattern is OK for now and nominally captures the fact that the value is mutated internally by
3205+ # virtue of the Provided attribute remaining False.
3206+
31253207 direct_stim_cost_per_injection_well_cstim_u = self .stimulation_cost_per_injection_well .quantity ().to (
31263208 self .Cstim .CurrentUnits ).magnitude
31273209 direct_stim_cost_per_production_well_cstim_u = self .stimulation_cost_per_production_well .quantity ().to (
0 commit comments