Skip to content

Commit

Permalink
Update ADORB Cost
Browse files Browse the repository at this point in the history
- Update the ADORB cost calc to fix the off-by-1 year error
- Update all tests with verification case data from Phius-GUI
- Add new Present-Value calculation and supportint type
  • Loading branch information
ed-p-may committed Nov 16, 2024
1 parent 30687b7 commit 0636069
Show file tree
Hide file tree
Showing 6 changed files with 502 additions and 144 deletions.
175 changes: 128 additions & 47 deletions ph_adorb/adorb_cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,88 +10,165 @@
electrical service capacity.
"""


import logging
import pandas as pd

from ph_adorb.yearly_values import YearlyCost
from ph_adorb.yearly_values import YearlyCost, YearlyPresentValueFactor

logger = logging.getLogger(__name__)

# -- Constants
# TODO: Support non-USA countries.
# TODO: Move these to be variables someplace...
USA_NUM_YEARS_TO_TRANSITION = 30
USA_NATIONAL_TRANSITION_COST = 4_500_000_000_000.00
NAMEPLATE_CAPACITY_INCREASE_GW = 1_600.00
USA_TRANSITION_COST_FACTOR = USA_NATIONAL_TRANSITION_COST / (NAMEPLATE_CAPACITY_INCREASE_GW * 1_000_000_000.00)
USA_NATIONAL_TRANSITION_COST = 4.5e12
NAMEPLATE_CAPACITY_INCREASE_GW = 1_600
USA_TRANSITION_COST_FACTOR = USA_NATIONAL_TRANSITION_COST / (NAMEPLATE_CAPACITY_INCREASE_GW * 1e9)


# ---------------------------------------------------------------------------------------


def pv_direct_energy_cost(
_year: int, _annual_cost_electric: float, _annual_cost_gas: float, _discount_rate: float = 0.02
def present_value_factor(_year: int, _discount_rate: float) -> YearlyPresentValueFactor:
"""Calculate the present value factor for a given year."""
rate = (1 + _discount_rate) ** (_year + 1)
return YearlyPresentValueFactor(rate, _year + 1)


def energy_purchase_cost_PV(
_pv_factor: YearlyPresentValueFactor, _annual_cost_electric: float, _annual_cost_gas: float
) -> float:
"""Calculate the total direct energy cost for a given year."""
try:
return (_annual_cost_electric + _annual_cost_gas) / ((1 + _discount_rate) ** _year)
except ZeroDivisionError:
logger.info(f"energy_purchase_cost_PV(year={_pv_factor.year}, factor={_pv_factor.factor :.3f})")

if _pv_factor.factor == 0:
return 0.0

annual_energy_cost = _annual_cost_electric + _annual_cost_gas
annual_energy_cost_PV = annual_energy_cost / _pv_factor.factor

def pv_operation_carbon_cost(
_year: int,
logger.debug(
f"Energy Actual Cost: ${_annual_cost_electric :,.0f}[Elec.] + ${_annual_cost_gas :,.0f}[Gas] = ${annual_energy_cost :,.0f}"
)
logger.debug(
f"Energy PV Cost: ${annual_energy_cost :,.0f} / {_pv_factor.factor :.3f} = PV${annual_energy_cost_PV :,.0f}"
)

return annual_energy_cost_PV


def energy_CO2_cost_PV(
_pv_factor: YearlyPresentValueFactor,
_future_annual_CO2_electric: list[float],
_annual_CO2_gas: float,
_price_of_carbon: float,
_discount_rate: float = 0.075,
) -> float:
"""Calculate the total operational carbon cost for a given year."""
try:
return ((_future_annual_CO2_electric[_year] + _annual_CO2_gas) * _price_of_carbon) / (
(1 + _discount_rate) ** _year
)
except ZeroDivisionError:
logger.info(f"energy_CO2_cost_PV(year={_pv_factor.year}, factor={_pv_factor.factor :.3f})")

if _pv_factor.factor == 0:
return 0.0

annual_elec_CO2 = _future_annual_CO2_electric[_pv_factor.year - 1]
annual_CO2 = annual_elec_CO2 + _annual_CO2_gas
annual_CO2_cost = annual_CO2 * _price_of_carbon
annual_CO2_cost_PV = annual_CO2_cost / _pv_factor.factor

def pv_install_cost(_year: int, _carbon_measure_yearly_costs: list[YearlyCost], _discount_rate: float = 0.02) -> float:
"""Calculate the total direct maintenance cost for a given year."""
try:
return sum(
row.cost / ((1 + _discount_rate) ** _year) for row in _carbon_measure_yearly_costs if row.year == _year
)
except ZeroDivisionError:
logger.debug(
f"Energy CO2 Emissions: {annual_elec_CO2 :,.0f}[Elec.] + {_annual_CO2_gas :,.0f}[Gas] = {annual_CO2 :,.0f}"
)
logger.debug(
f"Energy CO2 Actual Cost: {annual_CO2 :,.0f} * ${_price_of_carbon :,.2f}/unit = ${annual_CO2_cost :,.0f}"
)
logger.debug(
f"Energy CO2 PV Cost [{_pv_factor.year}]: ${annual_CO2_cost :,.0f} / {_pv_factor.factor :.3f} = PV${annual_CO2_cost_PV :,.0f}"
)

return annual_CO2_cost_PV


def measure_purchase_cost_PV(
_pv_factor: YearlyPresentValueFactor, _carbon_measure_yearly_purchase_costs: list[YearlyCost]
) -> float:
"""Calculate the total Measure purchase, install and maintenance cost for a single year."""
logger.info(f"measure_purchase_cost_PV(year={_pv_factor.year}, factor={_pv_factor.factor :.3f})")

if _pv_factor == 0:
return 0.0

measure_costs = [
measure.cost for measure in _carbon_measure_yearly_purchase_costs if measure.year == _pv_factor.year - 1
]
if not measure_costs:
return 0.0

total_measure_cost = sum(measure_costs)
total_measure_PV_cost = total_measure_cost / _pv_factor.factor

def pv_embodied_CO2_cost(
_year: int, _carbon_measure_embodied_CO2_yearly_costs: list[YearlyCost], _discount_rate: float = 0.00
logging.debug(f"Measure Actual Costs: {[f'${_ :,.0f}' for _ in measure_costs]} = {total_measure_cost :,.0f}")
logging.debug(
f"Measure PV Cost: ${total_measure_cost :,.0f} / {_pv_factor.factor :.3f} = PV${total_measure_PV_cost :,.0f}"
)

return total_measure_PV_cost


def measure_CO2_cost_PV(
_pv_factor: YearlyPresentValueFactor, _carbon_measure_embodied_CO2_yearly_costs: list[YearlyCost]
) -> float:
"""Calculate the total embodied CO2 cost for a given year."""
"""Calculate the total Measure embodied CO2 cost for a given year."""
logger.info(f"measure_CO2_cost_PV(year={_pv_factor.year}, factor={_pv_factor.factor :.3f})")

# TODO: What is this factor for? Why do we multiply by it?
FACTOR = 0.75
try:
return sum(
FACTOR * (yearly_cost.cost / ((1 + _discount_rate) ** _year))
for yearly_cost in _carbon_measure_embodied_CO2_yearly_costs
if yearly_cost.year == _year
)
except ZeroDivisionError:

if _pv_factor.factor == 0:
return 0.0

measure_costs = [
yearly_cost.cost
for yearly_cost in _carbon_measure_embodied_CO2_yearly_costs
if yearly_cost.year == _pv_factor.year
]
if not measure_costs:
return 0.0

total_measure_cost = sum(measure_costs)
total_measure_PV_cost = sum(FACTOR * (cost / _pv_factor.factor) for cost in measure_costs)

def pv_grid_transition_cost(_year: int, _grid_transition_cost: float, _discount_rate: float = 0.02) -> float:
logging.debug(f"Measure CO2 Actual Cost: {[f'${_ :,.0f}' for _ in measure_costs]} = {total_measure_cost :,.0f}")
logging.debug(
f"Measure CO2 PV Cost: ${total_measure_cost :,.0f} / {_pv_factor.factor :.3f} = PV${total_measure_PV_cost :,.0f}"
)

return total_measure_PV_cost


def grid_transition_cost_PV(_pv_factor: YearlyPresentValueFactor, _grid_transition_cost: float) -> float:
"""Calculate the total grid transition cost for a given year."""
if _year > USA_NUM_YEARS_TO_TRANSITION:
logger.info(f"grid_transition_PV_cost(year={_pv_factor.year}, factor={_pv_factor.factor :.3f})")

if _pv_factor.year > USA_NUM_YEARS_TO_TRANSITION:
year_transition_cost_factor = 0 # $/Watt-yr
else:
# TODO: Support non-USA countries.
year_transition_cost_factor = USA_TRANSITION_COST_FACTOR / USA_NUM_YEARS_TO_TRANSITION # linear transition <- ?

try:
return (year_transition_cost_factor * _grid_transition_cost) / ((1 + _discount_rate) ** _year)
except ZeroDivisionError:
if _pv_factor.factor == 0:
return 0.0

transition_cost = year_transition_cost_factor * _grid_transition_cost
transition_PV_cost = transition_cost / _pv_factor.factor

logger.debug(
f"Transition Cost [{_pv_factor.year}]: {year_transition_cost_factor: .0f} * ${_grid_transition_cost :,.0f} = ${transition_cost :,.0f}"
)
logger.debug(
f"Transition PV Cost [{_pv_factor.year}]: ${transition_cost :,.0f} / {_pv_factor.factor :.3f} = PV${transition_PV_cost :,.0f}"
)

return transition_PV_cost


def calculate_annual_ADORB_costs(
_analysis_duration_years: int,
Expand All @@ -117,16 +194,20 @@ def calculate_annual_ADORB_costs(

# -- Create the row data
rows: list[pd.Series] = []
for n in range(1, _analysis_duration_years + 1):
for n in range(0, _analysis_duration_years):
logger.info(f"Calculating year {n} costs:")

new_row: pd.Series[float] = pd.Series(
{
columns[0]: pv_direct_energy_cost(n, _annual_total_cost_electric, _annual_total_cost_gas),
columns[1]: pv_operation_carbon_cost(
n, _annual_hourly_CO2_electric, _annual_total_CO2_gas, _price_of_carbon
columns[0]: energy_purchase_cost_PV(
present_value_factor(n, 0.02), _annual_total_cost_electric, _annual_total_cost_gas
),
columns[1]: energy_CO2_cost_PV(
present_value_factor(n, 0.075), _annual_hourly_CO2_electric, _annual_total_CO2_gas, _price_of_carbon
),
columns[2]: pv_install_cost(n, _all_yearly_install_costs),
columns[3]: pv_embodied_CO2_cost(n, _all_yearly_embodied_kgCO2),
columns[4]: pv_grid_transition_cost(n, _grid_transition_cost),
columns[2]: measure_purchase_cost_PV(present_value_factor(n, 0.02), _all_yearly_install_costs),
columns[3]: measure_CO2_cost_PV(present_value_factor(n, 0.0), _all_yearly_embodied_kgCO2),
columns[4]: grid_transition_cost_PV(present_value_factor(n, 0.02), _grid_transition_cost),
}
)
rows.append(new_row)
Expand Down
15 changes: 2 additions & 13 deletions ph_adorb/from_HBJSON/create_variant.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
from ph_adorb.equipment import PhAdorbEquipment, PhAdorbEquipmentCollection, PhAdorbEquipmentType
from ph_adorb.fuel import PhAdorbFuel, PhAdorbFuelType
from ph_adorb.grid_region import PhAdorbGridRegion, load_CO2_factors_from_json_file
from ph_adorb.measures import PhAdorbCO2MeasureCollection, PhAdorbCO2ReductionMeasure
from ph_adorb.measures import PhAdorbCO2MeasureCollection, PhAdorbCO2ReductionMeasure, CO2MeasureType
from ph_adorb.national_emissions import PhAdorbNationalEmissions
from ph_adorb.variant import PhAdorbVariant

Expand Down Expand Up @@ -83,7 +83,7 @@ def get_PhAdorbCO2Measures_from_hb_model(_hb_model_prop: ModelReviveProperties)
for co2_measure in _hb_model_prop.co2_measures:
measure_collection_.add_measure(
PhAdorbCO2ReductionMeasure(
measure_type=co2_measure.measure_type,
measure_type=CO2MeasureType(co2_measure.measure_type),
name=co2_measure.name,
year=co2_measure.year,
cost=co2_measure.cost,
Expand Down Expand Up @@ -199,17 +199,6 @@ def get_PhAdorbEquipment_from_hb_model(_hb_model: Model) -> PhAdorbEquipmentColl
continue
equipment_collection_.add_equipment(convert_hb_shade_pv(shade_prop_e.pv_properties))

# TODO: Batteries....
equipment_collection_.add_equipment(
PhAdorbEquipment(
name="Battery",
equipment_type=PhAdorbEquipmentType.BATTERY,
cost=3_894.54,
lifetime_years=10,
labor_fraction=0.5,
)
)

return equipment_collection_


Expand Down
Loading

0 comments on commit 0636069

Please sign in to comment.