import math
import sys
import os
import numpy as np
import numpy_financial as npf
import geophires_x.Economics as Economics
import geophires_x.Model as Model
from geophires_x.OptionList import EndUseOptions
from geophires_x.Parameter import listParameter, OutputParameter
from geophires_x.Units import *
[docs]
class EconomicsAddOns(Economics.Economics):
def __init__(self, model: Model):
"""
The __init__ function is called automatically when a class is instantiated.
It initializes the attributes of an object, and sets default values for certain arguments that can be
overridden by user input.
The __init__ function is used to set up all the parameters in Economics AddOns.
Set up all the Parameters that will be predefined by this class using the different types of parameter classes.
Setting up includes giving it a name, a default value, The Unit Type (length, volume, temperature, etc.)
and Unit Name of that value, sets it as required (or not), sets allowable range, the error message if
that range is exceeded, the ToolTip Text, and the name of the class that created it.
This includes setting up temporary variables that will be available to all the class but noy read in by user,
or used for Output
This also includes all Parameters that are calculated and then published using the Printouts function.
If you choose to subclass this master class, you can do so before or after you create your own parameters.
If you do, you can also choose to call this method from you class, which will effectively add and
set all these parameters to your class.
set up the parameters using the Parameter Constructors (intParameter, floatParameter, strParameter, etc.);
initialize with their name, default value, and valid range (if int or float). Optionally, you can specify:
Required (is it required to run? default value = False), ErrMessage (what GEOPHIRES will report if the value
provided is invalid, "assume default value (see manual)"), ToolTipText (when there is a GIU, this is the
text that the user will see, "This is ToolTip Text"), UnitType (the type of units associated with this
parameter (length, temperature, density, etc), Units.NONE), CurrentUnits (what the units are for this
parameter (meters, Celsius, gm/cc, etc., Units:NONE), and PreferredUnits (usually equal to CurrentUnits,
but these are the units that the calculations assume when running, Units.NONE
:param model: The container class of the application, giving access to everything else, including the logger
:type model: :class:`~geophires_x.Model.Model`
:return: None
"""
model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}')
super().__init__(model) # initialize the parent parameters and variables
sclass = str(__class__).replace("<class \'", "")
self.MyClass = sclass.replace("\'>", "")
self.MyPath = os.path.abspath(__file__)
self.AddOnNickname = self.ParameterDict[self.AddOnNickname.Name] = listParameter(
"AddOn Nickname",
UnitType=Units.NONE,
Min=0.0,
Max=1000.0
)
self.AddOnCAPEX = self.ParameterDict[self.AddOnCAPEX.Name] = listParameter(
"AddOn CAPEX",
Min=0.0,
Max=1000.0,
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS
)
self.AddOnOPEXPerYear = self.ParameterDict[self.AddOnOPEXPerYear.Name] = listParameter(
"AddOn OPEX",
Min=0.0,
Max=1000.0,
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
self.AddOnElecGainedPerYear = self.ParameterDict[self.AddOnElecGainedPerYear.Name] = listParameter(
"AddOn Electricity Gained",
Min=0.0,
Max=1000.0,
UnitType=Units.ENERGYFREQUENCY,
PreferredUnits=EnergyFrequencyUnit.KWPERYEAR,
CurrentUnits=EnergyFrequencyUnit.KWPERYEAR
)
self.AddOnHeatGainedPerYear = self.ParameterDict[self.AddOnHeatGainedPerYear.Name] = listParameter(
"AddOn Heat Gained",
Min=0.0,
Max=1000.0,
UnitType=Units.ENERGYFREQUENCY,
PreferredUnits=EnergyFrequencyUnit.KWPERYEAR,
CurrentUnits=EnergyFrequencyUnit.KWPERYEAR
)
self.AddOnProfitGainedPerYear = self.ParameterDict[self.AddOnProfitGainedPerYear.Name] = listParameter(
"AddOn Profit Gained",
Min=0.0,
Max=1000.0,
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
# local variables that need initialization
# results
self.AddOnCAPEXTotal = self.OutputParameterDict[self.AddOnCAPEXTotal.Name] = OutputParameter(
"AddOn CAPEX Total",
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS
)
self.AddOnOPEXTotalPerYear = self.OutputParameterDict[self.AddOnOPEXTotalPerYear.Name] = OutputParameter(
"AddOn OPEX Total Per Year",
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
self.AddOnElecGainedTotalPerYear = self.OutputParameterDict[
self.AddOnElecGainedTotalPerYear.Name] = OutputParameter(
"AddOn Electricity Gained Total Per Year",
UnitType=Units.ENERGYFREQUENCY,
PreferredUnits=EnergyFrequencyUnit.KWPERYEAR,
CurrentUnits=EnergyFrequencyUnit.KWPERYEAR
)
self.AddOnHeatGainedTotalPerYear = self.OutputParameterDict[
self.AddOnHeatGainedTotalPerYear.Name] = OutputParameter(
"AddOn Heat Gained Total Per Year",
UnitType=Units.ENERGYFREQUENCY,
PreferredUnits=EnergyFrequencyUnit.KWPERYEAR,
CurrentUnits=EnergyFrequencyUnit.KWPERYEAR
)
self.AddOnProfitGainedTotalPerYear = self.OutputParameterDict[
self.AddOnProfitGainedTotalPerYear.Name] = OutputParameter(
"AddOn Profit Gained Total Per Year",
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
self.AddOnPaybackPeriod = self.OutputParameterDict[self.AddOnPaybackPeriod.Name] = OutputParameter(
"AddOn Payback Period",
UnitType=Units.TIME,
PreferredUnits=TimeUnit.YEAR,
CurrentUnits=TimeUnit.YEAR
)
self.AdjustedProjectCAPEX = self.OutputParameterDict[self.AdjustedProjectCAPEX.Name] = OutputParameter(
"Adjusted CAPEX",
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS
)
self.AdjustedProjectOPEX = self.OutputParameterDict[self.AdjustedProjectOPEX.Name] = OutputParameter(
"Adjusted OPEX",
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS
)
self.AddOnCashFlow = self.OutputParameterDict[self.AddOnCashFlow.Name] = OutputParameter(
"Annual AddOn Cash Flow",
value=[0.0],
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
self.AddOnCummCashFlow = self.OutputParameterDict[self.AddOnCummCashFlow.Name] = OutputParameter(
"Cumulative AddOn Cash Flow",
value=[0.0],
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS
)
self.ProjectCashFlow = self.OutputParameterDict[self.ProjectCashFlow.Name] = OutputParameter(
"Annual Project Cash Flow",
value=[0.0],
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
self.ProjectCummCashFlow = self.OutputParameterDict[self.ProjectCummCashFlow.Name] = OutputParameter(
"Cumulative Project Cash Flow",
value=[0.0],
UnitType=Units.CURRENCY,
PreferredUnits=CurrencyUnit.MDOLLARS,
CurrentUnits=CurrencyUnit.MDOLLARS
)
self.AddOnElecRevenue = self.OutputParameterDict[self.AddOnElecRevenue.Name] = OutputParameter(
"Annual Revenue Generated from Electricity Sales",
value=[0.0],
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
self.AddOnHeatRevenue = self.OutputParameterDict[self.AddOnHeatRevenue.Name] = OutputParameter(
"Annual Revenue Generated from Heat Sales",
value=[0.0],
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
self.AddOnRevenue = self.OutputParameterDict[self.AddOnRevenue.Name] = OutputParameter(
"Annual Revenue Generated from AddOns",
value=[0.0],
UnitType=Units.CURRENCYFREQUENCY,
PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR,
CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR
)
model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name)
[docs]
def read_parameters(self, model: Model) -> None:
"""
The read_parameters function is called by the model to read in all the parameters that are used for this
extension. The user can create as many or as few parameters
as needed. Each parameter is created by a call to the InputParameter class, which is defined below, and then
stored in a dictionary with a name assigned to
:param model: The container class of the application, giving access to everything else, including the logger
:type model: :class:`~geophires_x.Model.Model`
:return: None
"""
model.logger.info(f'Init {str(__class__)}: {sys._getframe().f_code.co_name}')
super().read_parameters(model) # read the parameters for the parent.
# Deal with all the parameter values that the user has provided that relate to this extension.
# super.read_parameter will have already dealt with all the regular values, but anything unusual
# may not be dealt with, so check.
# In this case, all the values are array values, and weren't correctly dealt with, so below is where
# we process them. The problem is that they have a position number i.e., "AddOnCAPEX 1, AddOnCAPEX 2"
# appended to them, while the
# Parameter name is just "AddOnCAPEX" and the position indicates where in the array the user wants it stored.
# So we need to look for the 5 arrays and position values and insert them into the arrays.
# this does not deal with units if the user wants to do any conversions...
# In this case, the read_parameters function didn't deal with the arrays of values we wanted,
# so we will craft that here.
for key in model.InputParameters.keys():
if key.startswith("AddOn Nickname"):
val = str(model.InputParameters[key].sValue)
self.AddOnNickname.value.append(val) # this assumes they put the values in the file in consecutive fashion
if key.startswith("AddOn CAPEX"):
val = float(model.InputParameters[key].sValue)
self.AddOnCAPEX.value.append(val) # this assumes they put the values in the file in consecutive fashion
if key.startswith("AddOn OPEX"):
val = float(model.InputParameters[key].sValue)
self.AddOnOPEXPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion
if key.startswith("AddOn Electricity Gained"):
val = float(model.InputParameters[key].sValue)
self.AddOnElecGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion
if key.startswith("AddOn Heat Gained"):
val = float(model.InputParameters[key].sValue)
self.AddOnHeatGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion
if key.startswith("AddOn Profit Gained"):
val = float(model.InputParameters[key].sValue)
self.AddOnProfitGainedPerYear.value.append(val) # this assumes they put the values in the file in consecutive fashion
model.logger.info("complete " + str(__class__) + ": " + sys._getframe().f_code.co_name)
[docs]
def Calculate(self, model: Model) -> None:
"""
The Calculate function is where all the calculations are done.
This function can be called multiple times, and will only recalculate what has changed each time it is called.
This is where all the calculations are made using all the values that have been set.
If you subclass this class, you can choose to run these calculations before (or after) your calculations,
but that assumes you have set all the values that are required for these calculations
If you choose to subclass this master class, you can also choose to override this method (or not),
and if you do, do it before or after you call you own version of this method.
If you do, you can also choose to call this method from you class, which can effectively run the
calculations of the superclass, making all thr values available to your methods.
but you had better have set all the parameters!
:param model: The container class of the application, giving access to everything else, including the logger
:type model: :class:`~geophires_x.Model.Model`
:return: Nothing, but it does make calculations and set values in the model
"""
model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name)
# sum all the AddOn values together, so we can treat all AddOns together. If an AddOn slot is not used,
# it has zeros for the values, so this won't create problems
if len(self.AddOnCAPEX.value) > 0:
self.AddOnCAPEXTotal.value = np.sum(self.AddOnCAPEX.value)
if len(self.AddOnOPEXPerYear.value) > 0:
self.AddOnOPEXTotalPerYear.value = np.sum(self.AddOnOPEXPerYear.value)
if len(self.AddOnElecGainedPerYear.value) > 0:
self.AddOnElecGainedTotalPerYear.value = np.sum(self.AddOnElecGainedPerYear.value)
if len(self.AddOnHeatGainedPerYear.value) > 0:
self.AddOnHeatGainedTotalPerYear.value = np.sum(self.AddOnHeatGainedPerYear.value)
if len(self.AddOnProfitGainedPerYear.value) > 0:
self.AddOnProfitGainedTotalPerYear.value = np.sum(self.AddOnProfitGainedPerYear.value)
# The amount of electricity and/or heat have for the project already been calculated in SurfacePlant,
# so we need to update them here so when they get used in the final economic calculation (below),
# the new values reflect the addition of the AddOns
for i in range(0, model.surfaceplant.plant_lifetime.value):
if model.surfaceplant.enduse_option.value is not EndUseOptions.HEAT: # all these end-use options have an electricity generation component
model.surfaceplant.TotalkWhProduced.value[i] = model.surfaceplant.TotalkWhProduced.value[i] + self.AddOnElecGainedTotalPerYear.value
model.surfaceplant.NetkWhProduced.value[i] = model.surfaceplant.NetkWhProduced.value[i] + self.AddOnElecGainedTotalPerYear.value
if model.surfaceplant.enduse_option.value is not EndUseOptions.ELECTRICITY:
model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] + self.AddOnHeatGainedTotalPerYear.value
else:
# all the end-use option of direct-use only components have a heat generation component
model.surfaceplant.HeatkWhProduced.value[i] = model.surfaceplant.HeatkWhProduced.value[i] + self.AddOnHeatGainedTotalPerYear.value
# Calculate the adjusted OPEX and CAPEX
self.AdjustedProjectCAPEX.value = model.economics.CCap.value + self.AddOnCAPEXTotal.value
self.AdjustedProjectOPEX.value = model.economics.Coam.value + self.AddOnOPEXTotalPerYear.value
AddOnCapCostPerYear = self.AddOnCAPEXTotal.value / model.surfaceplant.construction_years.value
ProjectCapCostPerYear = self.AdjustedProjectCAPEX.value / model.surfaceplant.construction_years.value
# (re)Calculate the revenues
self.AddOnElecRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value
self.AddOnHeatRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value
self.AddOnRevenue.value = [0.0] * model.surfaceplant.plant_lifetime.value
self.AddOnCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value
self.ProjectCashFlow.value = [0.0] * model.surfaceplant.plant_lifetime.value
for i in range(0, model.surfaceplant.plant_lifetime.value, 1):
ProjectElectricalEnergy = 0.0
ProjectHeatEnergy = 0.0
AddOnElectricalEnergy = 0.0
AddOnHeatEnergy = 0.0
if model.surfaceplant.enduse_option.value == EndUseOptions.ELECTRICITY: # This option has no heat component
ProjectElectricalEnergy = model.surfaceplant.NetkWhProduced.value[i]
AddOnElectricalEnergy = self.AddOnElecGainedTotalPerYear.value
elif model.surfaceplant.enduse_option.value == EndUseOptions.HEAT: # has heat component but no electricity
ProjectHeatEnergy = model.surfaceplant.HeatkWhProduced.value[i]
AddOnHeatEnergy = self.AddOnHeatGainedTotalPerYear.value
else: # everything else has a component of both
ProjectElectricalEnergy = model.surfaceplant.NetkWhProduced.value[i]
ProjectHeatEnergy = model.surfaceplant.HeatkWhProduced.value[i]
AddOnElectricalEnergy = self.AddOnElecGainedTotalPerYear.value
AddOnHeatEnergy = self.AddOnHeatGainedTotalPerYear.value
self.AddOnElecRevenue.value[i] = (AddOnElectricalEnergy * model.economics.ElecPrice.value[
i]) / 1_000_000.0 # Electricity revenue in MUSD
self.AddOnHeatRevenue.value[i] = (AddOnHeatEnergy * model.economics.HeatPrice.value[
i]) / 1_000_000.0 # Heat revenue in MUSD
self.AddOnRevenue.value[i] = self.AddOnElecRevenue.value[i] + self.AddOnHeatRevenue.value[
i] + self.AddOnProfitGainedTotalPerYear.value - self.AddOnOPEXTotalPerYear.value
self.AddOnCashFlow.value[i] = self.AddOnRevenue.value[i]
self.ProjectCashFlow.value[i] = self.AddOnRevenue.value[i] + (((ProjectElectricalEnergy *
model.economics.ElecPrice.value[i]) + (ProjectHeatEnergy *
model.economics.HeatPrice.value[i])) / 1_000_000.0) - model.economics.Coam.value # MUSD
# now insert the cost of construction into the front of the array that will be used to calculate
# NPV = the convention is that the upfront CAPEX is negative
for i in range(0, model.surfaceplant.construction_years.value, 1):
self.AddOnCashFlow.value.insert(0, -1.0 * AddOnCapCostPerYear)
self.ProjectCashFlow.value.insert(0, -1.0 * ProjectCapCostPerYear)
# Now calculate a new "NPV", "IRR", "VIR", "Payback Period", and "MOIC"
# Calculate more financial values using numpy financials
self.ProjectNPV.value = npf.npv(self.FixedInternalRate.value / 100, self.ProjectCashFlow.value)
self.ProjectIRR.value = npf.irr(self.ProjectCashFlow.value)
if math.isnan(self.ProjectIRR.value):
self.ProjectIRR.value = 0.0
self.ProjectVIR.value = 1.0 + (self.ProjectNPV.value / self.AdjustedProjectCAPEX.value)
# calculate Cummcashflows and payback period
self.ProjectCummCashFlow.value = [0.0] * len(self.ProjectCashFlow.value)
i = 0
for val in self.ProjectCashFlow.value:
if i == 0:
self.ProjectCummCashFlow.value[i] = val
else:
self.ProjectCummCashFlow.value[i] = self.ProjectCummCashFlow.value[i - 1] + val
i = i + 1
i = 0
self.AddOnCummCashFlow.value = [0.0] * len(self.AddOnCashFlow.value)
for val in self.AddOnCashFlow.value:
if i == 0:
self.AddOnCummCashFlow.value[0] = val
else:
self.AddOnCummCashFlow.value[i] = self.AddOnCummCashFlow.value[i - 1] + val
if self.AddOnCummCashFlow.value[i] > 0 >= self.AddOnCummCashFlow.value[
i - 1]: # we just crossed the threshold into positive project cummcashflow, so we can calculate payback period
dFullDiff = self.AddOnCummCashFlow.value[i] + math.fabs(self.AddOnCummCashFlow.value[(i - 1)])
dPerc = math.fabs(self.AddOnCummCashFlow.value[(i - 1)]) / dFullDiff
self.AddOnPaybackPeriod.value = i + dPerc
i = i + 1
# Calculate MOIC which depends on CumCashFlow
self.ProjectMOIC.value = self.ProjectCummCashFlow.value[len(self.ProjectCummCashFlow.value) - 1] / (
self.AdjustedProjectCAPEX.value + (
self.AdjustedProjectOPEX.value * model.surfaceplant.plant_lifetime.value))
# recalculate LCOE/LCOH
self.LCOE.value, self.LCOH.value, LCOC = Economics.CalculateLCOELCOHLCOC(self, model)
model.logger.info(f'complete {str(__class__)}: {sys._getframe().f_code.co_name}')
def __str__(self):
return "EconomicsAddOns"