Skip to content

Commit

Permalink
Update Custom Financial Model (#195)
Browse files Browse the repository at this point in the history
* pull in changes from pysam_update_capacity

* fix import

* update tests

* undo a commenting
  • Loading branch information
dguittet authored Aug 21, 2023
1 parent 9506a41 commit 1051727
Show file tree
Hide file tree
Showing 21 changed files with 397 additions and 131 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ jobs:
env:
SKLEARN_ALLOW_DEPRECATED_SKLEARN_PACKAGE_INSTALL: True
run: |
pip install scikit-learn
sudo apt-get update && sudo apt-get install -y libglpk-dev glpk-utils coinor-cbc
python -m pip install --upgrade pip
pip install -r requirements.txt
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ HOPP_examples/
HOPP-demos/
# needed for h2 test inputs: tests/analysis/results/
examples/H2 Analysis/results/reopt_precomputes/
*_disp.txt

tests/hybrid/REoptResultsNoExportAboveLoad.json

Expand Down
30 changes: 30 additions & 0 deletions .readthedocs.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.8"
# You can also specify other tool versions:
# nodejs: "19"
# rust: "1.64"
# golang: "1.19"

# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
fail_on_warning: true

# If using Sphinx, optionally build your docs in additional formats such as PDF
# formats:
# - pdf

# Optionally declare the Python requirements required to build your docs
python:
install:
- requirements: requirements-dev.txt
9 changes: 0 additions & 9 deletions .readthedocs.yml

This file was deleted.

8 changes: 6 additions & 2 deletions hybrid/battery.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ def __init__(self,
if key not in battery_config.keys():
raise ValueError

self.config_name = "StandaloneBatterySingleOwner"
system_model = BatteryModel.default(chemistry)

if 'fin_model' in battery_config.keys():
financial_model = battery_config['fin_model']
financial_model = self.import_financial_model(battery_config['fin_model'], system_model, self.config_name)
else:
financial_model = Singleowner.from_existing(system_model, "StandaloneBatterySingleOwner")
financial_model = Singleowner.from_existing(system_model, self.config_name)

super().__init__("Battery", site, system_model, financial_model)

Expand Down Expand Up @@ -255,6 +256,9 @@ def simulate_financials(self, interconnect_kw: float, project_life: int, cap_cre
:param cap_cred_avail_storage: Base capacity credit on available storage (True),
otherwise use only dispatched generation (False)
"""
if not isinstance(self._financial_model, Singleowner.Singleowner):
self._financial_model.assign(self._system_model.export(), ignore_missing_vals=True) # copy system parameter values having same name

self._financial_model.value('batt_computed_bank_capacity', self.system_capacity_kwh)

self.validate_replacement_inputs(project_life)
Expand Down
3 changes: 3 additions & 0 deletions hybrid/csp_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,9 @@ def simulate_financials(self, interconnect_kw: float, project_life: int = 25, ca
:param cap_cred_avail_storage: Base capacity credit on available storage (True),
otherwise use only dispatched generation (False)
"""
if not isinstance(self._financial_model, Singleowner.Singleowner):
self._financial_model.assign(self._system_model.export(), ignore_missing_vals=True) # copy system parameter values having same name

if project_life > 1:
self._financial_model.value('system_use_lifetime_output', 1)
else:
Expand Down
12 changes: 7 additions & 5 deletions hybrid/detailed_pv_plant.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from hybrid.dispatch.power_sources.pv_dispatch import PvDispatch
from hybrid.layout.pv_module import get_module_attribs, set_module_attribs
from hybrid.layout.pv_inverter import set_inverter_attribs
from tools.utils import flatten_dict


class DetailedPVPlant(PowerSource):
Expand All @@ -27,12 +28,13 @@ def __init__(self,
'layout_model': optional layout model object to use instead of the PVLayout model
'layout_params': optional DetailedPVParameters, the design vector w/ values. Required for layout modeling
"""
system_model = Pvsam.default("FlatPlatePVSingleOwner")
self.config_name = "FlatPlatePVSingleOwner"
system_model = Pvsam.default(self.config_name)

if 'fin_model' in pv_config.keys():
financial_model = pv_config['fin_model']
financial_model = self.import_financial_model(pv_config['fin_model'], system_model, self.config_name)
else:
financial_model = Singleowner.from_existing(system_model, "FlatPlatePVSingleOwner")
financial_model = Singleowner.from_existing(system_model, self.config_name)

super().__init__("SolarPlant", site, system_model, financial_model)

Expand Down Expand Up @@ -142,14 +144,14 @@ def processed_assign(self, params):

def simulate_financials(self, interconnect_kw: float, project_life: int):
"""
Runs the finanical model
Runs the financial model
:param interconnect_kw: ``float``,
Hybrid interconnect limit [kW]
:param project_life: ``int``,
Number of year in the analysis period (execepted project lifetime) [years]
:return:
"""
"""
if not self._financial_model:
return
if self.system_capacity_kw <= 0:
Expand Down
18 changes: 10 additions & 8 deletions hybrid/dispatch/hybrid_dispatch_builder_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from hybrid.sites import SiteInfo
from hybrid.dispatch import HybridDispatch, HybridDispatchOptions, DispatchProblemState
from hybrid.clustering import Clustering
from hybrid.log import hybrid_logger as logger


class HybridDispatchBuilderSolver:
"""Helper class for building hybrid system dispatch problem, solving dispatch problem, and simulating system
Expand Down Expand Up @@ -211,7 +213,7 @@ def cbc_solve_call(pyomo_model: pyomo.ConcreteModel,
if sys.platform == 'win32' or sys.platform == 'cygwin':
cbc_path = Path(__file__).parent / "cbc_solver" / "cbc-win64" / "cbc"
if log_name != "":
print("Warning: CBC solver logging is active... This will significantly increase simulation time.")
logger.warning("Warning: CBC solver logging is active... This will significantly increase simulation time.")
solver_options.constructed['log'] = 2
solver = pyomo.SolverFactory('asl:cbc', executable=cbc_path)
results = solver.solve(pyomo_model, logfile=solver_options.instance_log, options=solver_options.constructed)
Expand Down Expand Up @@ -304,7 +306,7 @@ def check_solve_condition(solver_termination_condition, pyomo_model):
if solver_termination_condition == TerminationCondition.infeasible:
HybridDispatchBuilderSolver.print_infeasible_problem(pyomo_model)
elif not solver_termination_condition == TerminationCondition.optimal:
print("Warning: Dispatch problem termination condition was '"
logger.warning("Warning: Dispatch problem termination condition was '"
+ str(solver_termination_condition) + "'")

@staticmethod
Expand Down Expand Up @@ -358,9 +360,9 @@ def display_all_blocks(model: pyomo.ConcreteModel):
def simulate_power(self):
if self.needs_dispatch:
# Dispatch Optimization Simulation with Rolling Horizon
print("Simulating system with dispatch optimization...")
logger.info("Simulating system with dispatch optimization...")
else:
print("Dispatch optimization not required...")
logger.info("Dispatch optimization not required...")
return
ti = list(range(0, self.site.n_timesteps, self.options.n_roll_periods))
self.dispatch.initialize_parameters()
Expand All @@ -373,15 +375,15 @@ def simulate_power(self):
start_time = time.time()
self.simulate_with_dispatch(t)
sim_w_dispath_time = time.time()
print('Day {} dispatch optimized.'.format(i))
print(" %6.2f seconds required to simulate with dispatch" % (sim_w_dispath_time - start_time))
logger.info('Day {} dispatch optimized.'.format(i))
logger.info(" %6.2f seconds required to simulate with dispatch" % (sim_w_dispath_time - start_time))
else:
continue
# TODO: can we make the csp and battery model run with heuristic dispatch here?
# Maybe calling a simulate_with_heuristic() method
else:
if (i % 73) == 0:
print("\t {:.0f} % complete".format(i*20/73))
logger.info("\t {:.0f} % complete".format(i*20/73))
self.simulate_with_dispatch(t)
else:

Expand Down Expand Up @@ -459,7 +461,7 @@ def simulate_with_dispatch(self,
transmission_limit = self.power_sources['grid'].value('grid_interconnection_limit_kwac') / 1e3
for count, value in enumerate(system_limit):
if value > transmission_limit:
print('Warning: Desired schedule is greater than transmission limit. '
logger.warning('Warning: Desired schedule is greater than transmission limit. '
'Overwriting schedule to transmission limit')
system_limit[count] = transmission_limit

Expand Down
2 changes: 1 addition & 1 deletion hybrid/dispatch/hybrid_dispatch_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def __init__(self, dispatch_options: dict = None):
dict: {
'solver': str (default='glpk'), MILP solver used for dispatch optimization problem
options: ('glpk', 'cbc')
options: ('glpk', 'cbc', 'xpress', 'xpress_persistent', 'gurobi_ampl', 'gurobi')
'solver_options': dict, Dispatch solver options
'battery_dispatch': str (default='simple'), sets the battery dispatch model to use for dispatch
options: ('simple', 'one_cycle_heuristic', 'heuristic', 'non_convex_LV', 'convex_LV'),
Expand Down
48 changes: 40 additions & 8 deletions hybrid/financial/custom_financial_model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from dataclasses import dataclass, _is_classvar, asdict
from typing import Sequence, List
import numpy as np
from tools.utils import flatten_dict, equal


@dataclass
Expand Down Expand Up @@ -79,7 +80,7 @@ class SystemCosts(FinancialData):
class Revenue(FinancialData):
ppa_price_input: float=None
ppa_soln_mode: float=1
ppa_escalation: float=0
ppa_escalation: float=1
ppa_multiplier_model: float=None
dispatch_factors_ts: Sequence=(0,)

Expand Down Expand Up @@ -158,6 +159,7 @@ def __init__(self,
fin_config: dict) -> None:

# Input parameters
self._system_model = None
self.batt_annual_discharge_energy = None
self.batt_annual_charge_energy = None
self.batt_annual_charge_from_system = None
Expand All @@ -178,17 +180,28 @@ def __init__(self,
self.assign(fin_config)


def set_financial_inputs(self, power_source_dict: dict):
def set_financial_inputs(self, system_model=None):
"""
Set financial inputs from PowerSource (e.g., PVPlant) parameters and outputs
Set financial inputs from PowerSource (e.g., PVPlant)
This custom financial model needs to be able to update its inputs from the system model, as
parameters are not linked like they are when a PySAM.Singleowner model is created using from_existing().
The inputs that need to be updated will depend on the financial model implementation, and these
are specified here.
The system model reference is also update here, as the system model is not always available during __init__.
"""
if system_model is not None:
self._system_model = system_model
elif self._system_model is None:
raise ValueError('System model not set in custom financial model')

power_source_dict = flatten_dict(self._system_model.export())
if 'system_capacity' in power_source_dict:
self.value('system_capacity', power_source_dict['system_capacity'])
if 'dc_degradation' in power_source_dict:
self.value('degradation', power_source_dict['dc_degradation'])


def execute(self, n=0):
self.set_financial_inputs() # update inputs from system model
npv = self.npv(
rate=self.nominal_discount_rate(
inflation_rate=self.value('inflation_rate'),
Expand Down Expand Up @@ -294,16 +307,35 @@ def value(self, var_name, var_value=None):
else:
try:
setattr(attr_obj, var_name, var_value)
try:
# update system model if it has the same named attribute
# avoid infinite loops if same functionality is implemented in system model
if not equal(self._system_model.value(var_name), var_value) and var_name != 'gen':
self._system_model.value(var_name, var_value)
except:
pass
except Exception as e:
raise IOError(f"{self.__class__}'s attribute {var_name} could not be set to {var_value}: {e}")


def assign(self, input_dict):
def assign(self, input_dict, ignore_missing_vals=False):
"""
Assign attribues from nested dictionary, except for Outputs
:param input_dict: nested dictionary of values
:param ignore_missing_vals: if True, do not throw exception if value not in self
"""
for k, v in input_dict.items():
if not isinstance(v, dict):
self.value(k, v)
try:
self.value(k, v)
except:
if not ignore_missing_vals:
raise IOError(f"{self.__class__}'s attribute {k} could not be set to {v}")
elif k == 'Outputs':
continue # do not assign from Outputs category
else:
getattr(self, k).assign(v)
self.assign(input_dict[k], ignore_missing_vals)


def unassign(self, var_name):
Expand Down
6 changes: 5 additions & 1 deletion hybrid/grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ def __init__(self,
system_model = GridModel.default("GenericSystemSingleOwner")

if 'fin_model' in grid_config.keys():
financial_model = grid_config['fin_model']
if isinstance(grid_config['fin_model'], Singleowner.Singleowner):
financial_model = Singleowner.from_existing(system_model, "GenericSystemSingleOwner")
financial_model.assign(grid_config['fin_model'].export())
else:
financial_model = grid_config['fin_model']
else:
financial_model = Singleowner.from_existing(system_model, "GenericSystemSingleOwner")
financial_model.value("add_om_num_types", 1)
Expand Down
Loading

0 comments on commit 1051727

Please sign in to comment.