diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 3ea381e1a..8cb6233df 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -1575,6 +1575,7 @@ def __init__(self, model: Model): self.LCOC = self.OutputParameterDict[self.LCOC.Name] = OutputParameter( Name="LCOC", + display_name='Direct-Use Cooling Breakeven Price (LCOC)', UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.DOLLARSPERMMBTU, CurrentUnits=EnergyCostUnit.DOLLARSPERMMBTU @@ -1582,24 +1583,27 @@ def __init__(self, model: Model): self.LCOE = self.OutputParameterDict[self.LCOE.Name] = OutputParameter( Name="LCOE", + display_name='Electricity breakeven price', UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.CENTSSPERKWH, CurrentUnits=EnergyCostUnit.CENTSSPERKWH ) self.LCOH = self.OutputParameterDict[self.LCOH.Name] = OutputParameter( Name="LCOH", + display_name='Direct-Use heat breakeven price (LCOH)', UnitType=Units.ENERGYCOST, - PreferredUnits=EnergyCostUnit.DOLLARSPERMMBTU, + PreferredUnits=EnergyCostUnit.DOLLARSPERMMBTU, # $/MMBTU CurrentUnits=EnergyCostUnit.DOLLARSPERMMBTU - ) # $/MMBTU + ) self.Cstim = self.OutputParameterDict[self.Cstim.Name] = OutputParameter( - Name="O&M Surface Plant costs", # FIXME wrong name - should be Stimulation Costs + Name="O&M Surface Plant costs", # FIXME wrong name - should be Stimulation Costs UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS ) self.Cexpl = self.OutputParameterDict[self.Cexpl.Name] = OutputParameter( Name="Exploration cost", + display_name='Exploration costs', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS @@ -1607,6 +1611,7 @@ def __init__(self, model: Model): self.Cwell = self.OutputParameterDict[self.Cwell.Name] = OutputParameter( Name="Wellfield cost", + display_name='Drilling and completion costs', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, @@ -1617,6 +1622,7 @@ def __init__(self, model: Model): ) self.Coamwell = self.OutputParameterDict[self.Coamwell.Name] = OutputParameter( Name="O&M Wellfield cost", + display_name='Wellfield maintenance costs', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR @@ -1629,30 +1635,35 @@ def __init__(self, model: Model): ) self.Coamplant = self.OutputParameterDict[self.Coamplant.Name] = OutputParameter( Name="O&M Surface Plant costs", + display_name='Power plant maintenance costs', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) self.Cgath = self.OutputParameterDict[self.Cgath.Name] = OutputParameter( Name="Field gathering system cost", + display_name='Field gathering system costs', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS ) self.Cpiping = self.OutputParameterDict[self.Cpiping.Name] = OutputParameter( Name="Transmission pipeline costs", + display_name='Transmission pipeline cost', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS ) self.Coamwater = self.OutputParameterDict[self.Coamwater.Name] = OutputParameter( Name="O&M Make-up Water costs", + display_name='Water costs', UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) self.CCap = self.OutputParameterDict[self.CCap.Name] = OutputParameter( Name="Total Capital Cost", + display_name='Total capital costs', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS @@ -1663,8 +1674,6 @@ def __init__(self, model: Model): PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR ) -# self.averageannualpumpingcosts = self.OutputParameterDict[ -# self.averageannualpumpingcosts.Name] = OutputParameter( #typo here!??! self.averageannualpumpingcosts = OutputParameter( Name="Average Annual Pumping Costs", UnitType=Units.CURRENCYFREQUENCY, @@ -1775,6 +1784,7 @@ def __init__(self, model: Model): self.CarbonThatWouldHaveBeenProducedTotal = self.OutputParameterDict[ self.CarbonThatWouldHaveBeenProducedTotal.Name] = OutputParameter( "Total Saved Carbon Production", + display_name='Total Avoided Carbon Emissions', UnitType=Units.MASS, PreferredUnits=MassUnit.LB, CurrentUnits=MassUnit.LB @@ -1806,6 +1816,7 @@ def __init__(self, model: Model): self.ProjectNPV = self.OutputParameterDict[self.ProjectNPV.Name] = OutputParameter( "Project Net Present Value", + display_name='Project NPV', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS, @@ -1817,12 +1828,14 @@ def __init__(self, model: Model): ) self.ProjectIRR = self.OutputParameterDict[self.ProjectIRR.Name] = OutputParameter( "Project Internal Rate of Return", + display_name='Project IRR', UnitType=Units.PERCENT, CurrentUnits=PercentUnit.PERCENT, PreferredUnits=PercentUnit.PERCENT, ) self.ProjectVIR = self.OutputParameterDict[self.ProjectVIR.Name] = OutputParameter( "Project Value Investment Ratio", + display_name='Project VIR=PI=PIR', UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH @@ -1842,6 +1855,7 @@ def __init__(self, model: Model): ) self.RITCValue = self.OutputParameterDict[self.RITCValue.Name] = OutputParameter( Name="Investment Tax Credit Value", + display_name='Investment Tax Credit', UnitType=Units.CURRENCY, PreferredUnits=CurrencyUnit.MDOLLARS, CurrentUnits=CurrencyUnit.MDOLLARS diff --git a/src/geophires_x/EconomicsS_DAC_GT.py b/src/geophires_x/EconomicsS_DAC_GT.py index 26e6f1275..78cf9a5c1 100644 --- a/src/geophires_x/EconomicsS_DAC_GT.py +++ b/src/geophires_x/EconomicsS_DAC_GT.py @@ -267,6 +267,7 @@ def __init__(self, model: Model): ) self.LCOH = self.OutputParameterDict[self.LCOH.Name] = OutputParameter( Name="LCOH", + display_name='Direct-Use heat breakeven price (LCOH)', UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.DOLLARSPERKWH, CurrentUnits=EnergyCostUnit.DOLLARSPERKWH diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index c816c4a17..934c5b69c 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -187,12 +187,13 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(' ***SUMMARY OF RESULTS***\n') f.write(NL) - f.write(' End-Use Option: ' + str(model.surfaceplant.enduse_option.value.value) + NL) + f.write(f' End-Use Option: {str(model.surfaceplant.enduse_option.value.value)}\n') if model.surfaceplant.plant_type.value in [PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]: f.write(' Surface Application: ' + str(model.surfaceplant.plant_type.value.value) + NL) if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY, EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: # there is an electricity component f.write(f' Average Net Electricity Production: {np.average(model.surfaceplant.NetElectricityProduced.value):10.2f} ' + model.surfaceplant.NetElectricityProduced.CurrentUnits.value + NL) - if model.surfaceplant.enduse_option.value is not EndUseOptions.ELECTRICITY: # there is a direct-use component + if model.surfaceplant.enduse_option.value is not EndUseOptions.ELECTRICITY: + # there is a direct-use component f.write(f' Average Direct-Use Heat Production: {np.average(model.surfaceplant.HeatProduced.value):10.2f} '+ model.surfaceplant.HeatProduced.CurrentUnits.value + NL) if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: f.write(f' Annual District Heating Demand: {np.average(model.surfaceplant.annual_heating_demand.value):10.2f} ' + model.surfaceplant.annual_heating_demand.CurrentUnits.value + NL) @@ -202,20 +203,20 @@ def PrintOutputs(self, model: Model): f.write(f' Average Cooling Production: {np.average(model.surfaceplant.cooling_produced.value):10.2f} ' + model.surfaceplant.cooling_produced.CurrentUnits.value + NL) if model.surfaceplant.enduse_option.value in [EndUseOptions.ELECTRICITY]: - f.write(f' Electricity breakeven price: {model.economics.LCOE.value:10.2f} ' + model.economics.LCOE.CurrentUnits.value + NL) + f.write(f' {model.economics.LCOE.display_name}: {model.economics.LCOE.value:10.2f} {model.economics.LCOE.CurrentUnits.value}\n') elif model.surfaceplant.enduse_option.value in [EndUseOptions.HEAT] and \ - model.surfaceplant.plant_type.value not in [PlantType.ABSORPTION_CHILLER]: - f.write(f' Direct-Use heat breakeven price (LCOH): {model.economics.LCOH.value:10.2f} ' + model.economics.LCOH.CurrentUnits.value + NL) + model.surfaceplant.plant_type.value not in [PlantType.ABSORPTION_CHILLER]: + f.write(f' {model.economics.LCOH.display_name}: {model.economics.LCOH.value:10.2f} {model.economics.LCOH.CurrentUnits.value}\n') elif model.surfaceplant.enduse_option.value in [EndUseOptions.HEAT] and model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: - f.write(f' Direct-Use Cooling Breakeven Price (LCOC): {model.economics.LCOC.value:10.2f} ' + model.economics.LCOC.CurrentUnits.value + NL) + f.write(f' {model.economics.LCOC.display_name}: {model.economics.LCOC.value:10.2f} {model.economics.LCOC.CurrentUnits.value}\n') elif model.surfaceplant.enduse_option.value in [EndUseOptions.COGENERATION_TOPPING_EXTRA_HEAT, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_HEAT, EndUseOptions.COGENERATION_PARALLEL_EXTRA_HEAT, EndUseOptions.COGENERATION_TOPPING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_BOTTOMING_EXTRA_ELECTRICITY, EndUseOptions.COGENERATION_PARALLEL_EXTRA_ELECTRICITY]: - f.write(f' Electricity breakeven price: {model.economics.LCOE.value:10.2f} ' + model.economics.LCOE.CurrentUnits.value + NL) - f.write(f' Direct-Use heat breakeven price (LCOH): {model.economics.LCOH.value:10.2f} ' + model.economics.LCOH.CurrentUnits.value + NL) + f.write(f' {model.economics.LCOE.display_name}: {model.economics.LCOE.value:10.2f} {model.economics.LCOE.CurrentUnits.value}\n') + f.write(f' {model.economics.LCOH.display_name}: {model.economics.LCOH.value:10.2f} {model.economics.LCOH.CurrentUnits.value}\n') f.write(f' Number of production wells: {model.wellbores.nprod.value:10.0f}'+NL) f.write(f' Number of injection wells: {model.wellbores.ninj.value:10.0f}'+NL) @@ -230,34 +231,35 @@ def PrintOutputs(self, model: Model): f.write(f' Segment {str(i):s} Thickness: {round(model.reserv.layerthickness.value[i-1], 10)} {model.reserv.layerthickness.CurrentUnits.value}\n') f.write(f' Segment {str(i+1):s} Geothermal gradient: {model.reserv.gradient.value[i]:10.4g} ' + model.reserv.gradient.CurrentUnits.value + NL) if model.economics.DoCarbonCalculations.value: - f.write(f' Total Avoided Carbon Emissions: {model.economics.CarbonThatWouldHaveBeenProducedTotal.value:10.2f} ' - f'{model.economics.CarbonThatWouldHaveBeenProducedTotal.CurrentUnits.value}\n') + f.write(f' {model.economics.CarbonThatWouldHaveBeenProducedTotal.display_name}:' + f' {model.economics.CarbonThatWouldHaveBeenProducedTotal.value:10.2f}' + f' {model.economics.CarbonThatWouldHaveBeenProducedTotal.CurrentUnits.value}\n') f.write(NL) f.write(NL) f.write(' ***ECONOMIC PARAMETERS***\n') f.write(NL) if model.economics.econmodel.value == EconomicModel.FCR: - f.write(' Economic Model = ' + model.economics.econmodel.value.value + NL) - f.write(f' Fixed Charge Rate (FCR): {model.economics.FCR.value*100.0:10.2f} ' + model.economics.FCR.CurrentUnits.value + NL) + f.write(f' Economic Model = {model.economics.econmodel.value.value}\n') + f.write(f' Fixed Charge Rate (FCR): {model.economics.FCR.value*100.0:10.2f} {model.economics.FCR.CurrentUnits.value}\n') elif model.economics.econmodel.value == EconomicModel.STANDARDIZED_LEVELIZED_COST: - f.write(' Economic Model = ' + model.economics.econmodel.value.value + NL) + f.write(f' Economic Model = {model.economics.econmodel.value.value}\n') f.write(f' {model.economics.interest_rate.Name}: {model.economics.interest_rate.value:10.2f} {model.economics.interest_rate.CurrentUnits.value}\n') elif model.economics.econmodel.value == EconomicModel.BICYCLE: - f.write(' Economic Model = ' + model.economics.econmodel.value.value + NL) - f.write(f' Accrued financing during construction: {model.economics.inflrateconstruction.value*100:10.2f} ' + model.economics.inflrateconstruction.CurrentUnits.value + NL) - f.write(f' Project lifetime: {model.surfaceplant.plant_lifetime.value:10.0f} ' + model.surfaceplant.plant_lifetime.CurrentUnits.value + NL) - f.write(f' Capacity factor: {model.surfaceplant.utilization_factor.value * 100:10.1f} %' + NL) + f.write(f' Economic Model = {model.economics.econmodel.value.value}\n') + f.write(f' Accrued financing during construction: {model.economics.inflrateconstruction.value*100:10.2f} {model.economics.inflrateconstruction.CurrentUnits.value}\n') + f.write(f' Project lifetime: {model.surfaceplant.plant_lifetime.value:10.0f} {model.surfaceplant.plant_lifetime.CurrentUnits.value}\n') + f.write(f' Capacity factor: {model.surfaceplant.utilization_factor.value * 100:10.1f} %\n') - e_npv = model.economics.ProjectNPV - npv_field_label = Outputs._field_label('Project NPV', 49) + e_npv: OutputParameter = model.economics.ProjectNPV + npv_field_label = Outputs._field_label(e_npv.display_name, 49) # TODO should use CurrentUnits instead of PreferredUnits f.write(f' {npv_field_label}{e_npv.value:10.2f} {e_npv.PreferredUnits.value}\n') - f.write(f' Project IRR: {model.economics.ProjectIRR.value:10.2f} ' + model.economics.ProjectIRR.PreferredUnits.value + NL) - f.write(f' Project VIR=PI=PIR: {model.economics.ProjectVIR.value:10.2f}' + NL) - f.write(f' {model.economics.ProjectMOIC.Name}: {model.economics.ProjectMOIC.value:10.2f}' + NL) + f.write(f' {model.economics.ProjectIRR.display_name}: {model.economics.ProjectIRR.value:10.2f} {model.economics.ProjectIRR.PreferredUnits.value}\n') + f.write(f' {model.economics.ProjectVIR.display_name}: {model.economics.ProjectVIR.value:10.2f}\n') + f.write(f' {model.economics.ProjectMOIC.Name}: {model.economics.ProjectMOIC.value:10.2f}\n') payback_period_val = model.economics.ProjectPaybackPeriod.value project_payback_period_display = f'{payback_period_val:10.2f} {model.economics.ProjectPaybackPeriod.PreferredUnits.value}' \ @@ -316,21 +318,21 @@ def PrintOutputs(self, model: Model): f.write(' ***RESERVOIR PARAMETERS***\n') f.write(NL) if model.wellbores.IsAGS.value: - f.write('The AGS models contain an intrinsic reservoir model that doesn\'t expose values that can be used in extensive reporting.' + NL) + f.write('The AGS models contain an intrinsic reservoir model that doesn\'t expose values that can be used in extensive reporting.\n') else: - f.write(' Reservoir Model = ' + str(model.reserv.resoption.value.value) + ' Model\n') + f.write(f' Reservoir Model = {str(model.reserv.resoption.value.value)} Model\n') if model.reserv.resoption.value is ReservoirModel.SINGLE_FRACTURE: f.write(f' m/A Drawdown Parameter: {model.reserv.drawdp.value:.5f} ' + model.reserv.drawdp.CurrentUnits.value + NL) elif model.reserv.resoption.value is ReservoirModel.ANNUAL_PERCENTAGE: f.write(f' Annual Thermal Drawdown: {model.reserv.drawdp.value*100:.3f} ' + model.reserv.drawdp.CurrentUnits.value + NL) - f.write(f' Bottom-hole temperature: {model.reserv.Trock.value:10.2f} ' + model.reserv.Trock.CurrentUnits.value + NL) + f.write(f' Bottom-hole temperature: {model.reserv.Trock.value:10.2f} {model.reserv.Trock.CurrentUnits.value}\n') if model.reserv.resoption.value in [ReservoirModel.ANNUAL_PERCENTAGE, ReservoirModel.USER_PROVIDED_PROFILE, ReservoirModel.TOUGH2_SIMULATOR]: f.write(' Warning: the reservoir dimensions and thermo-physical properties \n') f.write(' listed below are default values if not provided by the user. \n') f.write(' They are only used for calculating remaining heat content. \n') if model.reserv.resoption.value in [ReservoirModel.MULTIPLE_PARALLEL_FRACTURES, ReservoirModel.LINEAR_HEAT_SWEEP]: - f.write(' Fracture model = ' + model.reserv.fracshape.value.value + NL) + f.write(f' Fracture model = {model.reserv.fracshape.value.value}\n') if model.reserv.fracshape.value == FractureShape.CIRCULAR_AREA: f.write(f' Well separation: fracture diameter: {model.reserv.fracheightcalc.value:10.2f} ' + model.reserv.fracheight.CurrentUnits.value + NL) elif model.reserv.fracshape.value == FractureShape.CIRCULAR_DIAMETER: @@ -339,8 +341,8 @@ def PrintOutputs(self, model: Model): f.write(f' Well separation: fracture height: {model.reserv.fracheightcalc.value:10.2f} ' + model.reserv.fracheight.CurrentUnits.value + NL) elif model.reserv.fracshape.value == FractureShape.RECTANGULAR: f.write(f' Well separation: fracture height: {model.reserv.fracheightcalc.value:10.2f} ' + model.reserv.fracheight.CurrentUnits.value + NL) - f.write(f' Fracture width: {model.reserv.fracwidthcalc.value:10.2f} ' + model.reserv.fracwidth.CurrentUnits.value + NL) - f.write(f' Fracture area: {model.reserv.fracareacalc.value:10.2f} ' + model.reserv.fracarea.CurrentUnits.value + NL) + f.write(f' {model.reserv.fracwidthcalc.display_name}: {model.reserv.fracwidthcalc.value:10.2f} {model.reserv.fracwidth.CurrentUnits.value }\n') + f.write(f' {model.reserv.fracareacalc.display_name}: {model.reserv.fracareacalc.value:10.2f} {model.reserv.fracarea.CurrentUnits.value}\n') if model.reserv.resvoloption.value == ReservoirVolume.FRAC_NUM_SEP: f.write(' Reservoir volume calculated with fracture separation and number of fractures as input\n') elif model.reserv.resvoloption.value == ReservoirVolume.RES_VOL_FRAC_SEP: @@ -349,10 +351,10 @@ def PrintOutputs(self, model: Model): f.write(' Fracture separation calculated with reservoir volume and number of fractures as input\n') elif model.reserv.resvoloption.value == ReservoirVolume.RES_VOL_ONLY: f.write(' Reservoir volume provided as input\n') - if model.reserv.resvoloption.value in [ReservoirVolume.FRAC_NUM_SEP, ReservoirVolume.RES_VOL_FRAC_SEP,ReservoirVolume.FRAC_NUM_SEP]: - f.write(f' Number of fractures: {model.reserv.fracnumbcalc.value:10.2f}' + NL) - f.write(f' Fracture separation: {model.reserv.fracsepcalc.value:10.2f} ' + model.reserv.fracsep.CurrentUnits.value + NL) - f.write(f' Reservoir volume: {model.reserv.resvolcalc.value:10.0f} ' + model.reserv.resvol.CurrentUnits.value + NL) + if model.reserv.resvoloption.value in [ReservoirVolume.FRAC_NUM_SEP, ReservoirVolume.RES_VOL_FRAC_SEP, ReservoirVolume.FRAC_NUM_SEP]: + f.write(f' {model.reserv.fracnumbcalc.display_name}: {model.reserv.fracnumbcalc.value:10.2f}\n') + f.write(f' {model.reserv.fracsepcalc.display_name}: {model.reserv.fracsepcalc.value:10.2f} {model.reserv.fracsep.CurrentUnits.value}\n') + f.write(f' Reservoir volume: {model.reserv.resvolcalc.value:10.0f} {model.reserv.resvol.CurrentUnits.value}\n') if model.wellbores.impedancemodelused.value: # See note re: unit conversion: @@ -361,7 +363,7 @@ def PrintOutputs(self, model: Model): else: if model.wellbores.overpressure_percentage.Provided: # write the reservoir pressure as an average in the overpressure case - f.write(f' Average reservoir pressure: {model.wellbores.average_production_reservoir_pressure.value:10.2f} ' + model.wellbores.average_production_reservoir_pressure.CurrentUnits.value + NL) + f.write(f' {model.wellbores.average_production_reservoir_pressure.display_name}: {model.wellbores.average_production_reservoir_pressure.value:10.2f} {model.wellbores.average_production_reservoir_pressure.CurrentUnits.value}\n') else: # write the reservoir pressure as a single value f.write(f' Reservoir hydrostatic pressure: {model.wellbores.production_reservoir_pressure.value[0]:10.2f} ' + model.wellbores.production_reservoir_pressure.CurrentUnits.value + NL) @@ -385,18 +387,18 @@ def PrintOutputs(self, model: Model): f.write(NL) f.write(NL) - f.write(' ***RESERVOIR SIMULATION RESULTS***' + NL) + f.write(' ***RESERVOIR SIMULATION RESULTS***\n') f.write(NL) f.write(f' Maximum Production Temperature: {np.max(model.wellbores.ProducedTemperature.value):10.1f} ' + model.wellbores.ProducedTemperature.PreferredUnits.value + NL) f.write(f' Average Production Temperature: {np.average(model.wellbores.ProducedTemperature.value):10.1f} ' + model.wellbores.ProducedTemperature.PreferredUnits.value + NL) f.write(f' Minimum Production Temperature: {np.min(model.wellbores.ProducedTemperature.value):10.1f} ' + model.wellbores.ProducedTemperature.PreferredUnits.value + NL) f.write(f' Initial Production Temperature: {model.wellbores.ProducedTemperature.value[0]:10.1f} ' + model.wellbores.ProducedTemperature.PreferredUnits.value + NL) if model.wellbores.IsAGS.value: - f.write('The AGS models contain an intrinsic reservoir model that doesn\'t expose values that can be used in extensive reporting.' + NL) + f.write('The AGS models contain an intrinsic reservoir model that doesn\'t expose values that can be used in extensive reporting.\n') else: f.write(f' Average Reservoir Heat Extraction: {np.average(model.surfaceplant.HeatExtracted.value):10.2f} ' + model.surfaceplant.HeatExtracted.PreferredUnits.value + NL) if model.wellbores.rameyoptionprod.value: - f.write(' Production Wellbore Heat Transmission Model = Ramey Model' + NL) + f.write(' Production Wellbore Heat Transmission Model = Ramey Model\n') f.write(f' Average Production Well Temperature Drop: {np.average(model.wellbores.ProdTempDrop.value):10.1f} ' + model.wellbores.ProdTempDrop.PreferredUnits.value + NL) else: f.write(f' Wellbore Heat Transmission Model = Constant Temperature Drop:{model.wellbores.tempdropprod.value:10.1f} ' + model.wellbores.tempdropprod.PreferredUnits.value + NL) @@ -416,13 +418,13 @@ def PrintOutputs(self, model: Model): f.write(' ***CAPITAL COSTS (M$)***\n') f.write(NL) if not model.economics.totalcapcost.Valid: - f.write(f' Drilling and completion costs: {model.economics.Cwell.value:10.2f} ' + model.economics.Cwell.CurrentUnits.value + NL) + f.write(f' {model.economics.Cwell.display_name}: {model.economics.Cwell.value:10.2f} {model.economics.Cwell.CurrentUnits.value}\n') if econ.cost_lateral_section.value > 0.0: f.write(f' Drilling and completion costs per vertical production well: {econ.cost_one_production_well.value:10.2f} ' + econ.cost_one_production_well.CurrentUnits.value + NL) f.write(f' Drilling and completion costs per vertical injection well: {econ.cost_one_injection_well.value:10.2f} ' + econ.cost_one_injection_well.CurrentUnits.value + NL) f.write(f' {econ.cost_per_lateral_section.Name}: {econ.cost_per_lateral_section.value:10.2f} {econ.cost_lateral_section.CurrentUnits.value}\n') - elif round(econ.cost_one_production_well.value, 4) != round(econ.cost_one_injection_well.value, 4) and \ - model.economics.cost_one_injection_well.value != -1: + elif round(econ.cost_one_production_well.value, 4) != round(econ.cost_one_injection_well.value, 4) \ + and model.economics.cost_one_injection_well.value != -1: f.write(f' Drilling and completion costs per production well: {econ.cost_one_production_well.value:10.2f} ' + econ.cost_one_production_well.CurrentUnits.value + NL) f.write(f' Drilling and completion costs per injection well: {econ.cost_one_injection_well.value:10.2f} ' + econ.cost_one_injection_well.CurrentUnits.value + NL) else: @@ -435,20 +437,20 @@ def PrintOutputs(self, model: Model): f.write(f' of which Heat Pump Cost: {model.economics.heatpumpcapex.value:10.2f} ' + model.economics.Cplant.CurrentUnits.value + NL) if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: f.write(f' of which Peaking Boiler Cost: {model.economics.peakingboilercost.value:10.2f} ' + model.economics.peakingboilercost.CurrentUnits.value + NL) - f.write(f' Field gathering system costs: {model.economics.Cgath.value:10.2f} ' + model.economics.Cgath.CurrentUnits.value + NL) + f.write(f' {model.economics.Cgath.display_name}: {model.economics.Cgath.value:10.2f} {model.economics.Cgath.CurrentUnits.value}\n') if model.surfaceplant.piping_length.value > 0: - f.write(f' Transmission pipeline cost: {model.economics.Cpiping.value:10.2f} ' + model.economics.Cpiping.CurrentUnits.value + NL) + f.write(f' {model.economics.Cpiping.display_name}: {model.economics.Cpiping.value:10.2f} {model.economics.Cpiping.CurrentUnits.value}\n') if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: - f.write(f' District Heating System Cost: {model.economics.dhdistrictcost.value:10.2f} ' + model.economics.dhdistrictcost.CurrentUnits.value + NL) + f.write(f' District Heating System Cost: {model.economics.dhdistrictcost.value:10.2f} {model.economics.dhdistrictcost.CurrentUnits.value}\n') f.write(f' Total surface equipment costs: {(model.economics.Cplant.value+model.economics.Cgath.value):10.2f} ' + model.economics.Cplant.CurrentUnits.value + NL) - f.write(f' Exploration costs: {model.economics.Cexpl.value:10.2f} ' + model.economics.Cexpl.CurrentUnits.value + NL) + f.write(f' {model.economics.Cexpl.display_name}: {model.economics.Cexpl.value:10.2f} {model.economics.Cexpl.CurrentUnits.value}\n') if model.economics.totalcapcost.Valid and model.wellbores.redrill.value > 0: f.write(f' Drilling and completion costs (for redrilling):{model.economics.Cwell.value:10.2f} ' + model.economics.Cwell.CurrentUnits.value + NL) f.write(f' Drilling and completion costs per redrilled well: {(model.economics.Cwell.value/(model.wellbores.nprod.value+model.wellbores.ninj.value)):10.2f} ' + model.economics.Cwell.CurrentUnits.value + NL) f.write(f' Stimulation costs (for redrilling): {model.economics.Cstim.value:10.2f} ' + model.economics.Cstim.CurrentUnits.value + NL) if model.economics.RITCValue.value: - f.write(f' Investment Tax Credit: {-1*model.economics.RITCValue.value:10.2f} ' + model.economics.RITCValue.CurrentUnits.value + NL) - f.write(f' Total capital costs: {model.economics.CCap.value:10.2f} ' + model.economics.CCap.CurrentUnits.value + NL) + f.write(f' {model.economics.RITCValue.display_name}: {-1*model.economics.RITCValue.value:10.2f} {model.economics.RITCValue.CurrentUnits.value}\n') + f.write(f' {model.economics.CCap.display_name}: {model.economics.CCap.value:10.2f} {model.economics.CCap.CurrentUnits.value}\n') if model.economics.econmodel.value == EconomicModel.FCR: f.write(f' Annualized capital costs: {(model.economics.CCap.value*(1+model.economics.inflrateconstruction.value)*model.economics.FCR.value):10.2f} ' + model.economics.CCap.CurrentUnits.value + NL) @@ -457,18 +459,18 @@ def PrintOutputs(self, model: Model): f.write(' ***OPERATING AND MAINTENANCE COSTS (M$/yr)***\n') f.write(NL) if not model.economics.oamtotalfixed.Valid: - f.write(f' Wellfield maintenance costs: {model.economics.Coamwell.value:10.2f} ' + model.economics.Coamwell.CurrentUnits.value + NL) - f.write(f' Power plant maintenance costs: {model.economics.Coamplant.value:10.2f} ' + model.economics.Coamplant.CurrentUnits.value + NL) - f.write(f' Water costs: {model.economics.Coamwater.value:10.2f} ' + model.economics.Coamwater.CurrentUnits.value + NL) + f.write(f' {model.economics.Coamwell.display_name}: {model.economics.Coamwell.value:10.2f} {model.economics.Coamwell.CurrentUnits.value}\n') + f.write(f' {model.economics.Coamplant.display_name}: {model.economics.Coamplant.value:10.2f} {model.economics.Coamplant.CurrentUnits.value}\n') + f.write(f' {model.economics.Coamwater.display_name}: {model.economics.Coamwater.value:10.2f} {model.economics.Coamwater.CurrentUnits.value}\n') if model.surfaceplant.plant_type.value in [PlantType.INDUSTRIAL, PlantType.ABSORPTION_CHILLER, PlantType.HEAT_PUMP, PlantType.DISTRICT_HEATING]: - f.write(f' Average Reservoir Pumping Cost: {model.economics.averageannualpumpingcosts.value:10.2f} ' + model.economics.averageannualpumpingcosts.CurrentUnits.value + NL) - if model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: - f.write(f' Absorption Chiller O&M Cost: {model.economics.chilleropex.value:10.2f} ' + model.economics.chilleropex.CurrentUnits.value + NL) - if model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: - f.write(f' Average Heat Pump Electricity Cost: {model.economics.averageannualheatpumpelectricitycost.value:10.2f} ' + model.economics.averageannualheatpumpelectricitycost.CurrentUnits.value + NL) + f.write(f' Average Reservoir Pumping Cost: {model.economics.averageannualpumpingcosts.value:10.2f} {model.economics.averageannualpumpingcosts.CurrentUnits.value}\n') + if model.surfaceplant.plant_type.value == PlantType.ABSORPTION_CHILLER: + f.write(f' Absorption Chiller O&M Cost: {model.economics.chilleropex.value:10.2f} {model.economics.chilleropex.CurrentUnits.value}\n') + if model.surfaceplant.plant_type.value == PlantType.HEAT_PUMP: + f.write(f' Average Heat Pump Electricity Cost: {model.economics.averageannualheatpumpelectricitycost.value:10.2f} {model.economics.averageannualheatpumpelectricitycost.CurrentUnits.value}\n') if model.surfaceplant.plant_type.value == PlantType.DISTRICT_HEATING: - f.write(f' Annual District Heating O&M Cost: {model.economics.dhdistrictoandmcost.value:10.2f} ' + model.economics.dhdistrictoandmcost.CurrentUnits.value + NL) - f.write(f' Average Annual Peaking Fuel Cost: {model.economics.averageannualngcost.value:10.2f} ' + model.economics.averageannualngcost.CurrentUnits.value + NL) + f.write(f' Annual District Heating O&M Cost: {model.economics.dhdistrictoandmcost.value:10.2f} {model.economics.dhdistrictoandmcost.CurrentUnits.value}\n') + f.write(f' Average Annual Peaking Fuel Cost: {model.economics.averageannualngcost.value:10.2f} {model.economics.averageannualngcost.CurrentUnits.value}\n') f.write(f' Total operating and maintenance costs: {(model.economics.Coam.value + model.economics.averageannualpumpingcosts.value+model.economics.averageannualheatpumpelectricitycost.value):10.2f} ' + model.economics.Coam.CurrentUnits.value + NL) else: @@ -751,7 +753,7 @@ def o(output_param: OutputParameter): @staticmethod - def _field_label(field_name:str, print_width_before_value: int) -> str: + def _field_label(field_name: str, print_width_before_value: int) -> str: return f'{field_name}:{" " * (print_width_before_value - len(field_name) - 1)}' diff --git a/src/geophires_x/Parameter.py b/src/geophires_x/Parameter.py index 3e43dbba2..e3e51a219 100644 --- a/src/geophires_x/Parameter.py +++ b/src/geophires_x/Parameter.py @@ -6,6 +6,7 @@ import sys from array import array +from collections.abc import Iterable from typing import List, Optional, Any from dataclasses import dataclass, field from enum import IntEnum @@ -21,6 +22,13 @@ _ureg = get_unit_registry() _DISABLE_FOREX_API = True # See https://github.com/NREL/GEOPHIRES-X/issues/236#issuecomment-2414681434 +_JSON_PARAMETER_TYPE_STRING = 'string' +_JSON_PARAMETER_TYPE_INTEGER = 'integer' +_JSON_PARAMETER_TYPE_NUMBER = 'number' +_JSON_PARAMETER_TYPE_ARRAY = 'array' +_JSON_PARAMETER_TYPE_BOOLEAN = 'boolean' +_JSON_PARAMETER_TYPE_OBJECT = 'object' + class HasQuantity(ABC): def quantity(self) -> PlainQuantity: @@ -63,6 +71,7 @@ class OutputParameter(HasQuantity): """ Name: str = "" + display_name: str = None value: Any = 0 ToolTipText: str = "" UnitType: IntEnum = Units.NONE @@ -70,6 +79,7 @@ class OutputParameter(HasQuantity): # set to PreferredUnits by default assuming that the current units are the preferred units - # they will only change if the read function reads a different unit associated with a parameter CurrentUnits: Enum = PreferredUnits + json_parameter_type: str = None @property def UnitsMatch(self) -> str: @@ -81,7 +91,27 @@ def with_preferred_units(self) -> Any: # Any is a proxy for Self ret.CurrentUnits = ret.PreferredUnits return ret - + def __post_init__(self): + if self.display_name is None: + self.display_name: str = self.Name + + if self.json_parameter_type is None: + # Note that this is sensitive to order of comparison; unit test ensures correct behavior: + # test_parameter.ParameterTestCase.test_output_parameter_json_types + if isinstance(self.value, str): + self.json_parameter_type = _JSON_PARAMETER_TYPE_STRING + elif isinstance(self.value, bool): + self.json_parameter_type = _JSON_PARAMETER_TYPE_BOOLEAN + elif isinstance(self.value, float) or isinstance(self.value, int): + # Default number values may not be representative of whether calculated values are integer-only, + # so we specify number type even if value is int. + self.json_parameter_type = _JSON_PARAMETER_TYPE_NUMBER + elif isinstance(self.value, dict): + self.json_parameter_type = _JSON_PARAMETER_TYPE_OBJECT + elif isinstance(self.value, Iterable): + self.json_parameter_type = _JSON_PARAMETER_TYPE_ARRAY + else: + self.json_parameter_type = _JSON_PARAMETER_TYPE_OBJECT @dataclass class Parameter(HasQuantity): @@ -148,7 +178,7 @@ def __post_init__(self): value: bool = None DefaultValue: bool = value - json_parameter_type: str = 'boolean' + json_parameter_type: str = _JSON_PARAMETER_TYPE_BOOLEAN @dataclass @@ -170,7 +200,7 @@ def __post_init__(self): value: int = None DefaultValue: int = value AllowableRange: List[int] = field(default_factory=list) - json_parameter_type: str = 'integer' + json_parameter_type: str = _JSON_PARAMETER_TYPE_INTEGER def coerce_value_to_enum(self): if self.ValuesEnum is not None: @@ -203,7 +233,7 @@ def __post_init__(self): DefaultValue: float = 0.0 Min: float = -1.8e30 Max: float = 1.8e30 - json_parameter_type: str = 'number' + json_parameter_type: str = _JSON_PARAMETER_TYPE_NUMBER @dataclass @@ -218,11 +248,11 @@ class strParameter(Parameter): """ def __post_init__(self): if self.value is None: - self.value:str = self.DefaultValue + self.value: str = self.DefaultValue value: str = None DefaultValue: str = value - json_parameter_type: str = 'string' + json_parameter_type: str = _JSON_PARAMETER_TYPE_STRING @dataclass @@ -242,13 +272,13 @@ class listParameter(Parameter): def __post_init__(self): if self.value is None: - self.value:str = self.DefaultValue + self.value: str = self.DefaultValue value: List[float] = None DefaultValue: List[float] = field(default_factory=list) Min: float = -1.8e308 Max: float = 1.8e308 - json_parameter_type: str = 'array' + json_parameter_type: str = _JSON_PARAMETER_TYPE_ARRAY def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model): diff --git a/src/geophires_x/Reservoir.py b/src/geophires_x/Reservoir.py index c52fbea72..f6b67cff4 100644 --- a/src/geophires_x/Reservoir.py +++ b/src/geophires_x/Reservoir.py @@ -104,7 +104,7 @@ def __init__(self, model: Model): CurrentUnits=TemperatureGradientUnit.DEGREESCPERM, Required=True, ErrMessage="assume default geothermal gradients 1 (50, 0, 0, 0 deg.C/km)", - ToolTipText="Geothermal gradients" + ToolTipText="Geothermal gradient(s)" ) self.gradient1 = self.ParameterDict[self.gradient1.Name] = floatParameter( @@ -420,6 +420,7 @@ def __init__(self, model: Model): # starts as a copy of the input value and only changes if needed. self.fracsepcalc = self.OutputParameterDict[self.fracsepcalc.Name] = OutputParameter( "Calculated Fracture Separation", + display_name='Fracture separation', value=self.fracsep.value, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, @@ -428,12 +429,14 @@ def __init__(self, model: Model): self.fracnumbcalc = self.OutputParameterDict[self.fracnumbcalc.Name] = OutputParameter( "Calculated Number of Fractures", + display_name='Number of fractures', value=self.fracnumb.value, UnitType=Units.NONE ) self.fracwidthcalc = self.OutputParameterDict[self.fracwidthcalc.Name] = OutputParameter( "Calculated Fracture Width", + display_name='Fracture width', value=self.fracwidth.value, UnitType=Units.LENGTH, PreferredUnits=LengthUnit.METERS, @@ -450,6 +453,7 @@ def __init__(self, model: Model): self.fracareacalc = self.OutputParameterDict[self.fracareacalc.Name] = OutputParameter( "Calculated Fracture Area", + display_name='Fracture area', value=self.fracarea.value, UnitType=Units.AREA, PreferredUnits=AreaUnit.METERS2, @@ -458,6 +462,7 @@ def __init__(self, model: Model): self.resvolcalc = self.OutputParameterDict[self.resvolcalc.Name] = floatParameter( "Calculated Reservoir Volume", + # display_name='Reservoir volume', value=self.resvol.value, UnitType=Units.VOLUME, PreferredUnits=VolumeUnit.METERS3, @@ -467,13 +472,15 @@ def __init__(self, model: Model): self.cpwater = self.OutputParameterDict[self.cpwater.Name] = floatParameter( "cpwater", value=0.0, - UnitType=Units.NONE + UnitType=Units.NONE, + ToolTipText='water heat capacity' ) self.rhowater = self.OutputParameterDict[self.rhowater.Name] = floatParameter( "rhowater", value=0.0, - UnitType=Units.NONE + UnitType=Units.NONE, + ToolTipText='water density' ) self.averagegradient = self.OutputParameterDict[self.averagegradient.Name] = floatParameter( diff --git a/src/geophires_x/SUTRAEconomics.py b/src/geophires_x/SUTRAEconomics.py index b866f8fc3..dbe8f9f53 100644 --- a/src/geophires_x/SUTRAEconomics.py +++ b/src/geophires_x/SUTRAEconomics.py @@ -204,6 +204,7 @@ def __init__(self, model: Model): self.LCOH = self.OutputParameterDict[self.LCOH.Name] = OutputParameter( "Heat Sale Price Model", + display_name='Direct-Use heat breakeven price (LCOH)', value=[0.025], UnitType=Units.ENERGYCOST, PreferredUnits=EnergyCostUnit.CENTSSPERKWH, diff --git a/src/geophires_x/WellBores.py b/src/geophires_x/WellBores.py index 7317ecf4f..9f250d49b 100644 --- a/src/geophires_x/WellBores.py +++ b/src/geophires_x/WellBores.py @@ -1072,6 +1072,7 @@ def __init__(self, model: Model): ) self.average_production_reservoir_pressure = self.OutputParameterDict[self.average_production_reservoir_pressure.Name] = OutputParameter( Name="Average Reservoir Pressure", + display_name='Average reservoir pressure', UnitType=Units.PRESSURE, PreferredUnits=PressureUnit.KPASCAL, CurrentUnits=PressureUnit.KPASCAL diff --git a/src/geophires_x_schema_generator/.gitignore b/src/geophires_x_schema_generator/.gitignore index a1de5eca0..e739df7bb 100644 --- a/src/geophires_x_schema_generator/.gitignore +++ b/src/geophires_x_schema_generator/.gitignore @@ -1,4 +1,5 @@ *parameters.rst all_messages_conf.log !geophires-request.json +!geophires-result.json !hip-ra-x-request.json diff --git a/src/geophires_x_schema_generator/__init__.py b/src/geophires_x_schema_generator/__init__.py index 9c90e48ea..fa775f8fd 100644 --- a/src/geophires_x_schema_generator/__init__.py +++ b/src/geophires_x_schema_generator/__init__.py @@ -1,4 +1,5 @@ import json +import logging import os import sys from pathlib import Path @@ -27,6 +28,7 @@ from geophires_x.SUTRAWellBores import SUTRAWellBores from geophires_x.TDPReservoir import TDPReservoir from geophires_x.TOUGH2Reservoir import TOUGH2Reservoir +from geophires_x_client import GeophiresXResult from hip_ra_x.hip_ra_x import HIP_RA_X @@ -96,7 +98,11 @@ def _with_cat(p: Parameter, cat: str): return json_dumpse(input_params), json_dumpse(output_params) - def generate_json_schema(self) -> dict: + def generate_json_schema(self) -> Tuple[dict, dict]: + """ + :return: request schema, result schema + :rtype: Tuple[dict, dict] + """ input_params_json, output_params_json = self.get_parameters_json() input_params = json.loads(input_params_json) @@ -108,6 +114,7 @@ def generate_json_schema(self) -> dict: units_val = param['CurrentUnits'] if isinstance(param['CurrentUnits'], str) else None min_val, max_val = _get_min_and_max(param, default_val=None) + properties[param_name] = { 'description': param['ToolTipText'], 'type': param['json_parameter_type'], @@ -124,16 +131,73 @@ def generate_json_schema(self) -> dict: if param['ValuesEnum']: properties[param_name]['enum_values'] = param['ValuesEnum'] - schema = { + request_schema = { + 'definitions': {}, + '$schema': 'http://json-schema.org/draft-04/schema#', + 'type': 'object', + 'title': f'{self.get_schema_title()} Request Schema', + 'required': required, + 'properties': properties, + } + + return request_schema, self.get_result_json_schema(output_params_json) + + def get_result_json_schema(self, output_params_json) -> dict: + properties = {} + required = [] + + output_params = json.loads(output_params_json) + display_name_aliases = {} + for param_name in output_params: + if 'display_name' in output_params[param_name]: + display_name = output_params[param_name]['display_name'] + if display_name not in [None, ''] and display_name != param_name: + # output_params[display_name] = output_params[param_name] + display_name_aliases[display_name] = output_params[param_name] + display_name_aliases[display_name]['output_parameter_name'] = param_name + + output_params = {**output_params, **display_name_aliases} + + # noinspection PyProtectedMember + for category in GeophiresXResult._RESULT_FIELDS_BY_CATEGORY: + cat_properties = {} + # noinspection PyProtectedMember + for field in GeophiresXResult._RESULT_FIELDS_BY_CATEGORY[category]: + param_name = field if isinstance(field, str) else field.field_name + + if param_name in properties: + _log.warning(f'Param {param_name} is already in properties: {properties[param_name]}') + + param = {} if param_name not in properties else properties[param_name] + + if param_name in output_params: + output_param = output_params[param_name] + param['type'] = output_param['json_parameter_type'] + description = output_param['ToolTipText'] + if 'output_parameter_name' in output_param: + if description is not None and description != '': + description = f'{output_param["output_parameter_name"]}. {description}' + else: + description = output_param['output_parameter_name'] + param['description'] = description + param['units'] = ( + output_param['CurrentUnits'] if isinstance(output_param['CurrentUnits'], str) else None + ) + + cat_properties[param_name] = param.copy() + + properties[category] = {'type': 'object', 'properties': cat_properties} + + result_schema = { 'definitions': {}, '$schema': 'http://json-schema.org/draft-04/schema#', 'type': 'object', - 'title': f'{self.get_schema_title()} Schema', + 'title': f'{self.get_schema_title()} Result Schema', 'required': required, 'properties': properties, } - return schema + return result_schema def generate_parameters_reference_rst(self) -> str: input_params_json, output_params_json = self.get_parameters_json() @@ -189,6 +253,25 @@ def get_input_params_table(_category_params, category_name) -> str: output_rst = self.get_output_params_table_rst(output_params_json) + schema_ref_base_url = ( + 'https://github.com/softwareengineerprogrammer/GEOPHIRES/blob/main/src/geophires_x_schema_generator/' + ) + input_schema_ref_rst = '' + if self.get_input_schema_reference() is not None: + input_schema_ref_rst = ( + f'Schema: ' + f'`{self.get_input_schema_reference()} ' + f'<{schema_ref_base_url}{self.get_input_schema_reference()}>`__' + ) + + output_schema_ref_rst = '' + if self.get_output_schema_reference() is not None: + output_schema_ref_rst = ( + f'Schema: ' + f'`{self.get_output_schema_reference()} ' + f'<{schema_ref_base_url}{self.get_output_schema_reference()}>`__' + ) + rst = f"""{self.get_schema_title()} Parameters ========== @@ -196,21 +279,36 @@ def get_input_params_table(_category_params, category_name) -> str: Input Parameters ################ +{input_schema_ref_rst} {input_rst} -Output Parameters +Outputs ################# +{output_schema_ref_rst} {output_rst} """ return rst - @staticmethod - def get_output_params_table_rst(output_params_json) -> str: - output_params = json.loads(output_params_json) + def get_output_params_table_rst(self, output_params_json) -> str: + output_schema = self.get_result_json_schema(output_params_json) - output_rst = """ - .. list-table:: Output Parameters + output_params_by_category: dict = {} + + for category, category_params in output_schema['properties'].items(): + if category not in output_params_by_category: + output_params_by_category[category] = {} # [] + + for param_name, param in category_params['properties'].items(): + output_params_by_category[category][param_name] = param + + def get_output_params_table(_category_params, category_name) -> str: + category_display = category_name if category_name is not None else '' + category_display = category_display.replace(' (M$)', '').replace(' (M$/yr)', '') + _output_rst = f""" +{category_display} +{'-' * len(category_display)} + .. list-table:: {category_display}{' ' if len(category_display) > 0 else ''}Outputs :header-rows: 1 * - Name @@ -218,22 +316,26 @@ def get_output_params_table_rst(output_params_json) -> str: - Preferred Units - Default Value Type""" - for param_name in output_params: - param = output_params[param_name] + for _param_name, _param in _category_params.items(): + _output_rst += f"""\n * - {_param_name} + - {_get_key(_param, 'description')} + - {_get_key(_param, 'units')} + - {_get_key(_param, 'type')}""" - def get_key(k): - if k in param and str(param[k]) != '': # noqa - return param[k] # noqa - else: - return '' + return _output_rst - output_rst += f"""\n * - {param['Name']} - - {get_key('ToolTipText')} - - {get_key('PreferredUnits')} - - {get_key('json_parameter_type')}""" + output_rst = '' + for category, category_params in output_params_by_category.items(): + output_rst += get_output_params_table(category_params, category) return output_rst + def get_input_schema_reference(self) -> str: + return 'geophires-request.json' + + def get_output_schema_reference(self) -> str: + return 'geophires-result.json' + def _get_key(param: dict, k: str, default_val='') -> Any: if k in param and str(param[k]) != '': @@ -271,3 +373,61 @@ def get_parameter_sources(self) -> list: def get_schema_title(self) -> str: return 'HIP-RA-X' + + def get_result_json_schema(self, output_params_json) -> dict: + return None # FIXME TODO + + def get_output_params_table_rst(self, output_params_json) -> str: + """ + FIXME TODO consolidate with generated result schema + """ + + output_params = json.loads(output_params_json) + + output_rst = """ + .. list-table:: Outputs + :header-rows: 1 + + * - Name + - Description + - Preferred Units + - Default Value Type""" + + for param_name in output_params: + param = output_params[param_name] + + def get_key(k): + if k in param and str(param[k]) != '': # noqa + return param[k] # noqa + else: + return '' + + output_rst += f"""\n * - {param['Name']} + - {get_key('ToolTipText')} + - {get_key('PreferredUnits')} + - {get_key('json_parameter_type')}""" + + return output_rst + + def get_input_schema_reference(self) -> str: + return 'hip-ra-x-request.json' + + def get_output_schema_reference(self) -> str: + return None + + +def _get_logger(logger_name=None): + sh = logging.StreamHandler(sys.stdout) + sh.setLevel(logging.INFO) + sh.setFormatter(logging.Formatter(fmt='[%(asctime)s][%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')) + + if logger_name is None: + logger_name = __name__ + + _l = logging.getLogger(logger_name) + _l.addHandler(sh) + + return _l + + +_log = _get_logger() diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 04338a7ef..9d91f4e9a 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -2,7 +2,7 @@ "definitions": {}, "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "title": "GEOPHIRES-X Schema", + "title": "GEOPHIRES-X Request Schema", "required": [ "Reservoir Model", "Reservoir Depth", @@ -125,7 +125,7 @@ "maximum": 4 }, "Gradients": { - "description": "Geothermal gradients", + "description": "Geothermal gradient(s)", "type": "array", "units": "degC/m", "category": "Reservoir", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json new file mode 100644 index 000000000..04fdcbdab --- /dev/null +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -0,0 +1,470 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "title": "GEOPHIRES-X Result Schema", + "required": [], + "properties": { + "SUMMARY OF RESULTS": { + "type": "object", + "properties": { + "End-Use Option": {}, + "End-Use": {}, + "Surface Application": {}, + "Average Net Electricity Production": {}, + "Electricity breakeven price": { + "type": "number", + "description": "LCOE", + "units": "cents/kWh" + }, + "Average Direct-Use Heat Production": {}, + "Direct-Use heat breakeven price": {}, + "Direct-Use heat breakeven price (LCOH)": { + "type": "number", + "description": "LCOH", + "units": "USD/MMBTU" + }, + "Direct-Use Cooling Breakeven Price (LCOC)": { + "type": "number", + "description": "LCOC", + "units": "USD/MMBTU" + }, + "Annual District Heating Demand": {}, + "Average Cooling Production": {}, + "Average Annual Geothermal Heat Production": {}, + "Average Annual Peaking Fuel Heat Production": {}, + "Direct-Use Cooling Breakeven Price": {}, + "Number of production wells": {}, + "Number of injection wells": {}, + "Flowrate per production well": {}, + "Well depth": {}, + "Well depth (or total length, if not vertical)": {}, + "Geothermal gradient": {}, + "Segment 1 Geothermal gradient": {}, + "Segment 1 Thickness": {}, + "Segment 2 Geothermal gradient": {}, + "Segment 2 Thickness": {}, + "Segment 3 Geothermal gradient": {}, + "Segment 3 Thickness": {}, + "Segment 4 Geothermal gradient": {}, + "LCOE": { + "type": "number", + "description": "LCOE", + "units": "cents/kWh" + }, + "LCOH": { + "type": "number", + "description": "LCOH", + "units": "USD/MMBTU" + }, + "Lifetime Average Well Flow Rate": {}, + "Total Avoided Carbon Emissions": { + "type": "number", + "description": "Total Saved Carbon Production", + "units": "pound" + } + } + }, + "ECONOMIC PARAMETERS": { + "type": "object", + "properties": { + "Economic Model": {}, + "Interest Rate": { + "type": "number", + "description": "", + "units": "%" + }, + "Accrued financing during construction": {}, + "Project lifetime": {}, + "Capacity factor": {}, + "Project NPV": { + "type": "number", + "description": "Project Net Present Value. NPV is calculated with cashflows lumped at the end of periods. See: Short W et al, 1995. \"A Manual for the Economic Evaluation of Energy Efficiency and Renewable Energy Technologies.\", p. 41. https://www.nrel.gov/docs/legosti/old/5173.pdf", + "units": "MUSD" + }, + "Project IRR": { + "type": "number", + "description": "Project Internal Rate of Return", + "units": "%" + }, + "Project VIR=PI=PIR": { + "type": "number", + "description": "Project Value Investment Ratio", + "units": "" + }, + "Project MOIC": { + "type": "number", + "description": "Project Multiple of Invested Capital", + "units": "" + }, + "Fixed Charge Rate (FCR)": {}, + "Project Payback Period": { + "type": "number", + "description": "", + "units": "yr" + }, + "CHP: Percent cost allocation for electrical plant": {}, + "Estimated Jobs Created": { + "type": "number", + "description": "", + "units": null + } + } + }, + "EXTENDED ECONOMICS": { + "type": "object", + "properties": { + "Adjusted Project LCOE (after incentives, grants, AddOns,etc)": {}, + "Adjusted Project LCOH (after incentives, grants, AddOns,etc)": {}, + "Adjusted Project CAPEX (after incentives, grants, AddOns, etc)": {}, + "Adjusted Project OPEX (after incentives, grants, AddOns, etc)": {}, + "Project NPV (including AddOns)": {}, + "Project IRR (including AddOns)": {}, + "Project VIR=PI=PIR (including AddOns)": {}, + "Project MOIC (including AddOns)": {}, + "Project Payback Period (including AddOns)": {}, + "Total Add-on CAPEX": {}, + "Total Add-on OPEX": {}, + "Total Add-on Net Elec": {}, + "Total Add-on Net Heat": {}, + "Total Add-on Profit": {}, + "AddOns Payback Period": {} + } + }, + "CCUS ECONOMICS": { + "type": "object", + "properties": { + "Total Avoided Carbon Production": {}, + "Project NPV (including carbon credit)": {}, + "Project IRR (including carbon credit)": {}, + "Project VIR=IR=PIR (including carbon credit)": {}, + "Project MOIC (including carbon credit)": {}, + "Project Payback Period (including carbon credit)": {} + } + }, + "S-DAC-GT ECONOMICS": { + "type": "object", + "properties": { + "LCOD using grid-based electricity only": {}, + "LCOD using natural gas only": {}, + "LCOD using geothermal energy only": {}, + "CO2 Intensity using grid-based electricity only": {}, + "CO2 Intensity using natural gas only": {}, + "CO2 Intensity using geothermal energy only": {}, + "Geothermal LCOH": {}, + "Geothermal Ratio (electricity vs heat)": {}, + "Percent Energy Devoted To Process": {}, + "Total Cost of Capture": {} + } + }, + "ENGINEERING PARAMETERS": { + "type": "object", + "properties": { + "Number of Production Wells": {}, + "Number of Injection Wells": {}, + "Well depth": {}, + "Well depth (or total length, if not vertical)": {}, + "Water loss rate": {}, + "Pump efficiency": {}, + "Injection temperature": {}, + "Injection Temperature": { + "type": "array", + "description": "", + "units": "degC" + }, + "Average production well temperature drop": {}, + "Flowrate per production well": {}, + "Injection well casing ID": {}, + "Production well casing ID": {}, + "Number of times redrilling": {}, + "Power plant type": {}, + "Fluid": {}, + "Design": {}, + "Flow rate": {}, + "Lateral Length": {}, + "Vertical Depth": {}, + "Wellbore Diameter": {}, + "Lifetime Average Well Flow Rate": {} + } + }, + "RESOURCE CHARACTERISTICS": { + "type": "object", + "properties": { + "Maximum reservoir temperature": {}, + "Number of segments": {}, + "Geothermal gradient": {}, + "Segment 1 Geothermal gradient": {}, + "Segment 1 Thickness": {}, + "Segment 2 Geothermal gradient": {}, + "Segment 2 Thickness": {}, + "Segment 3 Geothermal gradient": {}, + "Segment 3 Thickness": {}, + "Segment 4 Geothermal gradient": {} + } + }, + "RESERVOIR PARAMETERS": { + "type": "object", + "properties": { + "Reservoir Model": {}, + "Fracture model": {}, + "Bottom-hole temperature": { + "type": "number", + "description": "", + "units": "degC" + }, + "Well separation: fracture diameter": {}, + "Well separation: fracture height": {}, + "Fracture width": { + "type": "number", + "description": "Calculated Fracture Width", + "units": "meter" + }, + "Fracture area": { + "type": "number", + "description": "Calculated Fracture Area", + "units": "m**2" + }, + "Number of fractures": { + "type": "number", + "description": "Calculated Number of Fractures", + "units": null + }, + "Fracture separation": { + "type": "number", + "description": "Calculated Fracture Separation", + "units": "meter" + }, + "Reservoir volume": {}, + "Reservoir impedance": {}, + "Reservoir hydrostatic pressure": {}, + "Average reservoir pressure": { + "type": "number", + "description": "Average Reservoir Pressure", + "units": "kPa" + }, + "Plant outlet pressure": {}, + "Production wellhead pressure": { + "type": "number", + "description": "", + "units": "kPa" + }, + "Productivity Index": {}, + "Injectivity Index": {}, + "Reservoir density": {}, + "Reservoir thermal conductivity": {}, + "Reservoir heat capacity": {}, + "Reservoir porosity": {}, + "Thermal Conductivity": {} + } + }, + "RESERVOIR SIMULATION RESULTS": { + "type": "object", + "properties": { + "Maximum Production Temperature": {}, + "Average Production Temperature": { + "type": "number", + "description": "", + "units": "degC" + }, + "Minimum Production Temperature": {}, + "Initial Production Temperature": {}, + "Average Reservoir Heat Extraction": {}, + "Production Wellbore Heat Transmission Model": {}, + "Wellbore Heat Transmission Model": {}, + "Average Production Well Temperature Drop": {}, + "Total Average Pressure Drop": {}, + "Average Injection Well Pressure Drop": {}, + "Average Production Pressure": { + "type": "number", + "description": "", + "units": "bar" + }, + "Average Reservoir Pressure Drop": {}, + "Average Production Well Pressure Drop": {}, + "Average Buoyancy Pressure Drop": {}, + "Average Injection Well Pump Pressure Drop": {}, + "Average Production Well Pump Pressure Drop": {}, + "Average Heat Production": {}, + "First Year Heat Production": {}, + "Average Net Electricity Production": {}, + "First Year Electricity Production": {}, + "Maximum Storage Well Temperature": {}, + "Average Storage Well Temperature": {}, + "Minimum Storage Well Temperature": {}, + "Maximum Balance Well Temperature": {}, + "Average Balance Well Temperature": {}, + "Minimum Balance Well Temperature": {}, + "Maximum Annual Heat Stored": {}, + "Average Annual Heat Stored": {}, + "Minimum Annual Heat Stored": {}, + "Maximum Annual Heat Supplied": {}, + "Average Annual Heat Supplied": {}, + "Minimum Annual Heat Supplied": {}, + "Average Round-Trip Efficiency": {} + } + }, + "CAPITAL COSTS (M$)": { + "type": "object", + "properties": { + "Drilling and completion costs": { + "type": "number", + "description": "Wellfield cost. Includes total drilling and completion cost of all injection and production wells and laterals, plus 5% indirect costs.", + "units": "MUSD" + }, + "Drilling and completion costs per well": {}, + "Drilling and completion costs per production well": {}, + "Drilling and completion costs per injection well": {}, + "Drilling and completion costs per vertical production well": {}, + "Drilling and completion costs per vertical injection well": {}, + "Drilling and completion costs per non-vertical section": { + "type": "number", + "description": "", + "units": "MUSD" + }, + "Drilling and completion costs (for redrilling)": {}, + "Drilling and completion costs per redrilled well": {}, + "Stimulation costs": {}, + "Stimulation costs (for redrilling)": {}, + "Surface power plant costs": {}, + "of which Absorption Chiller Cost": {}, + "of which Heat Pump Cost": {}, + "of which Peaking Boiler Cost": {}, + "Transmission pipeline cost": { + "type": "number", + "description": "Transmission pipeline costs", + "units": "MUSD" + }, + "District Heating System Cost": { + "type": "number", + "description": "", + "units": "MUSD" + }, + "Field gathering system costs": { + "type": "number", + "description": "Field gathering system cost", + "units": "MUSD" + }, + "Total surface equipment costs": {}, + "Exploration costs": { + "type": "number", + "description": "Exploration cost", + "units": "MUSD" + }, + "Investment Tax Credit": { + "type": "number", + "description": "Investment Tax Credit Value", + "units": "MUSD" + }, + "Total capital costs": { + "type": "number", + "description": "Total Capital Cost", + "units": "MUSD" + }, + "Annualized capital costs": {}, + "Total CAPEX": {}, + "Drilling Cost": {}, + "Drilling and Completion Costs": {}, + "Drilling and Completion Costs per Well": {}, + "Auxiliary Heater Cost": {}, + "Pump Cost": {}, + "Total Capital Costs": {} + } + }, + "OPERATING AND MAINTENANCE COSTS (M$/yr)": { + "type": "object", + "properties": { + "Wellfield maintenance costs": { + "type": "number", + "description": "O&M Wellfield cost", + "units": "MUSD/yr" + }, + "Power plant maintenance costs": { + "type": "number", + "description": "O&M Surface Plant costs", + "units": "MUSD/yr" + }, + "Water costs": { + "type": "number", + "description": "O&M Make-up Water costs", + "units": "MUSD/yr" + }, + "Average Reservoir Pumping Cost": {}, + "Absorption Chiller O&M Cost": {}, + "Average Heat Pump Electricity Cost": {}, + "Annual District Heating O&M Cost": { + "type": "number", + "description": "", + "units": "MUSD/yr" + }, + "Average Annual Peaking Fuel Cost": { + "type": "number", + "description": "", + "units": "MUSD/yr" + }, + "Average annual pumping costs": {}, + "Total operating and maintenance costs": {}, + "OPEX": {}, + "Average annual auxiliary fuel cost": {}, + "Average annual pumping cost": {}, + "Total average annual O&M costs": {} + } + }, + "SURFACE EQUIPMENT SIMULATION RESULTS": { + "type": "object", + "properties": { + "Initial geofluid availability": {}, + "Maximum Total Electricity Generation": {}, + "Average Total Electricity Generation": {}, + "Minimum Total Electricity Generation": {}, + "Initial Total Electricity Generation": {}, + "Maximum Net Electricity Generation": {}, + "Average Net Electricity Generation": {}, + "Minimum Net Electricity Generation": {}, + "Initial Net Electricity Generation": {}, + "Average Annual Total Electricity Generation": {}, + "Average Annual Net Electricity Generation": {}, + "Maximum Net Heat Production": {}, + "Average Net Heat Production": {}, + "Minimum Net Heat Production": {}, + "Initial Net Heat Production": {}, + "Average Annual Heat Production": {}, + "Average Pumping Power": {}, + "Average Annual Heat Pump Electricity Use": {}, + "Maximum Cooling Production": {}, + "Average Cooling Production": {}, + "Minimum Cooling Production": {}, + "Initial Cooling Production": {}, + "Average Annual Cooling Production": {}, + "Annual District Heating Demand": {}, + "Maximum Daily District Heating Demand": {}, + "Average Daily District Heating Demand": {}, + "Minimum Daily District Heating Demand": {}, + "Maximum Geothermal Heating Production": {}, + "Average Geothermal Heating Production": {}, + "Minimum Geothermal Heating Production": {}, + "Maximum Peaking Boiler Heat Production": {}, + "Average Peaking Boiler Heat Production": {}, + "Minimum Peaking Boiler Heat Production": {}, + "Initial pumping power/net installed power": {}, + "Heat to Power Conversion Efficiency": { + "type": "object", + "description": "First law efficiency average over project lifetime", + "units": "%" + }, + "Surface Plant Cost": {}, + "Average RTES Heating Production": {}, + "Average Auxiliary Heating Production": {}, + "Average Annual RTES Heating Production": {}, + "Average Annual Auxiliary Heating Production": {}, + "Average Annual Total Heating Production": {}, + "Average Annual Electricity Use for Pumping": {} + } + }, + "Simulation Metadata": { + "type": "object", + "properties": { + "GEOPHIRES Version": {} + } + } + } +} diff --git a/src/geophires_x_schema_generator/hip-ra-x-request.json b/src/geophires_x_schema_generator/hip-ra-x-request.json index 8420f0be1..054a49dd3 100644 --- a/src/geophires_x_schema_generator/hip-ra-x-request.json +++ b/src/geophires_x_schema_generator/hip-ra-x-request.json @@ -2,7 +2,7 @@ "definitions": {}, "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", - "title": "HIP-RA-X Schema", + "title": "HIP-RA-X Request Schema", "required": [ "Reservoir Temperature", "Rejection Temperature", diff --git a/src/geophires_x_schema_generator/main.py b/src/geophires_x_schema_generator/main.py index 7dfeea9a8..4db172f7c 100644 --- a/src/geophires_x_schema_generator/main.py +++ b/src/geophires_x_schema_generator/main.py @@ -21,13 +21,19 @@ build_dir.mkdir(exist_ok=True) - def build(json_file_name: str, generator: GeophiresXSchemaGenerator, rst_file_name: str): - build_path = Path(build_dir, json_file_name) - schema_json = generator.generate_json_schema() + def build(json_file_name_prefix: str, generator: GeophiresXSchemaGenerator, rst_file_name: str): + request_schema_json, result_schema_json = generator.generate_json_schema() - with open(build_path, 'w') as f: - f.write(json.dumps(schema_json, indent=2)) - print(f'Wrote JSON schema file to {build_path}.') + request_build_path = Path(build_dir, f'{json_file_name_prefix}request.json') + with open(request_build_path, 'w') as f: + f.write(json.dumps(request_schema_json, indent=2)) + print(f'Wrote request JSON schema file to {request_build_path}.') + + if result_schema_json is not None: + result_build_path = Path(build_dir, f'{json_file_name_prefix}result.json') + with open(result_build_path, 'w') as f: + f.write(json.dumps(result_schema_json, indent=2)) + print(f'Wrote result JSON schema file to {result_build_path}.') rst = generator.generate_parameters_reference_rst() @@ -36,5 +42,5 @@ def build(json_file_name: str, generator: GeophiresXSchemaGenerator, rst_file_na f.write(rst) print(f'Wrote RST file to {build_path_rst}.') - build('geophires-request.json', GeophiresXSchemaGenerator(), 'parameters.rst') - build('hip-ra-x-request.json', HipRaXSchemaGenerator(), 'hip_ra_x_parameters.rst') + build('geophires-', GeophiresXSchemaGenerator(), 'parameters.rst') + build('hip-ra-x-', HipRaXSchemaGenerator(), 'hip_ra_x_parameters.rst') diff --git a/tests/geophires_x_schema_generator_tests/test_geophires_x_schema_generator.py b/tests/geophires_x_schema_generator_tests/test_geophires_x_schema_generator.py index e86cd4d9b..40ecab098 100644 --- a/tests/geophires_x_schema_generator_tests/test_geophires_x_schema_generator.py +++ b/tests/geophires_x_schema_generator_tests/test_geophires_x_schema_generator.py @@ -1,3 +1,4 @@ +import json import unittest from geophires_x_schema_generator import GeophiresXSchemaGenerator @@ -10,6 +11,55 @@ def test_parameters_rst(self): rst = g.generate_parameters_reference_rst() self.assertIsNotNone(rst) # TODO sanity checks on content + def test_outputs_rst(self): + g = GeophiresXSchemaGenerator() + _, output_params_json = g.get_parameters_json() + rst = g.get_output_params_table_rst(output_params_json) + + self.assertIn( + """ECONOMIC PARAMETERS +------------------- + .. list-table:: ECONOMIC PARAMETERS Outputs + :header-rows: 1 + + * - Name + - Description + - Preferred Units + - Default Value Type + * - Economic Model +""", + rst, + ) + + self.assertIn( + """ * - Project IRR + - Project Internal Rate of Return + - % + - number +""", + rst, + ) + + def test_get_json_schema(self): + g = GeophiresXSchemaGenerator() + req_schema, result_schema = g.generate_json_schema() + self.assertIsNotNone(req_schema) # TODO sanity checks on content + self.assertIsNotNone(result_schema) # TODO sanity checks on content + + print(f'Generated result schema: {json.dumps(result_schema, indent=2)}') + + def get_result_prop(cat: str, name: str) -> dict: + return result_schema['properties'][cat]['properties'][name] + + self.assertIn( + 'multiple of invested capital', + get_result_prop('ECONOMIC PARAMETERS', 'Project MOIC')['description'].lower(), + ) + + self.assertIn( + 'Wellfield cost. ', get_result_prop('CAPITAL COSTS (M$)', 'Drilling and completion costs')['description'] + ) + if __name__ == '__main__': unittest.main() diff --git a/tests/test_parameter.py b/tests/test_parameter.py index 592622265..4972a11f9 100644 --- a/tests/test_parameter.py +++ b/tests/test_parameter.py @@ -151,6 +151,23 @@ def test_output_parameter_with_preferred_units(self): self.assertEqual(5.5, result.value[0]) self.assertEqual(5.5, result.value[-1]) + def test_output_parameter_json_types(self): + cases = [ + ('foo', 'string'), + (1, 'number'), + (44.4, 'number'), + (True, 'boolean'), + ([1, 2, 3], 'array'), + ({4, 5, 6}, 'array'), + (None, 'object'), + ({'foo': 'bar'}, 'object'), + ] + + for case in cases: + with self.subTest(case=case): + jpt = OutputParameter(value=case[0]).json_parameter_type + self.assertEqual(case[1], jpt) + def test_convert_units_back_currency(self): model = self._new_model()