Skip to content

Commit

Permalink
Add detailed PV model (pvsamv1) (#99)
Browse files Browse the repository at this point in the history
* Improve value assignment in power_source

* Add detailed PV model (pvsamv1)

* Add custom PV model capability

* Fix test interaction

* Properly enforce coherence between detailed plant attributes

* Verify capacity from electrical parameters

* Explicitly specify use of detailed pv model (pvsamv1)

* Refactor find_strings_per_inverter into size_electrical_parameters

* Refactor find_modules_per_string and add test

* Demonstrate use of autosizing electrical parameters

* Move electrical sizing test to test_layout file

* Add documentation of new PV model options and their default behavior

* Add detailed pv model getters and setters

* Utilize system capacity property setter

* Fix docstrings in pv_design_utils.py

* Fix subarray designation
  • Loading branch information
Matthew-Boyd authored Feb 15, 2023
1 parent c4e2a55 commit 84f2934
Show file tree
Hide file tree
Showing 8 changed files with 761 additions and 14 deletions.
147 changes: 147 additions & 0 deletions hybrid/detailed_pv_plant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
from typing import Union, Optional, Sequence, Any

import PySAM.Pvsamv1 as Pvsam
import PySAM.Singleowner as Singleowner

from hybrid.power_source import *
from hybrid.layout.pv_design_utils import *
from hybrid.layout.pv_layout import PVLayout, PVGridParameters
from hybrid.dispatch.power_sources.pv_dispatch import PvDispatch


class DetailedPVPlant(PowerSource):
_system_model: Pvsam.Pvsamv1
_financial_model: Union[Any, Singleowner.Singleowner]
_layout: Union[Any, PVLayout]
_dispatch: PvDispatch

def __init__(self,
site: SiteInfo,
pv_config: dict):
"""
:param pv_config: dict, with following keys:
'tech_config': dict, contains parameters for pvsamv1 technology model
'fin_config': dict, contains `model_type` and any inputs for chosen financial model type
'layout_params': optional DetailedPVParameters, the design vector w/ values. Required for layout modeling
'layout_config': optional dict, contains all keys for PVLayoutConfig dataclass. Required for layout modeling
"""
system_model = Pvsam.default("FlatPlatePVSingleOwner")
financial_model = Singleowner.from_existing(system_model, "FlatPlatePVSingleOwner")

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

self._system_model.SolarResource.solar_resource_data = self.site.solar_resource.data

self.dc_degradation = [0]

params: Optional[PVGridParameters] = None
if 'layout_params' in pv_config.keys():
params: PVGridParameters = pv_config['layout_params']
self._layout = PVLayout(site, system_model, params)

self._dispatch: PvDispatch = None

if 'tech_config' in pv_config.keys():
self.processed_assign(pv_config['tech_config'])

def processed_assign(self, params):
"""
Assign attributes from dictionary with additional processing
to enforce coherence between attributes
"""
self.assign(params)
calculated_system_capacity = verify_capacity_from_electrical_parameters(
system_capacity_target=self.system_capacity,
n_strings=self.n_strings,
modules_per_string=self.modules_per_string,
module_power=self.module_power
)
self.system_capacity = calculated_system_capacity

@property
def system_capacity(self) -> float:
"""pass through to established name property"""
return self.system_capacity_kw

@system_capacity.setter
def system_capacity(self, size_kw: float):
"""pass through to established name setter"""
self.system_capacity_kw = size_kw

@property
def system_capacity_kw(self) -> float:
return self._system_model.value('system_capacity') # [kW] DC

@system_capacity_kw.setter
def system_capacity_kw(self, size_kw: float):
"""
Sets the system capacity
:param size_kw: DC system size in kW
:return:
"""
self._system_model.value('system_capacity', size_kw)

@property
def dc_degradation(self) -> float:
"""Annual DC degradation for lifetime simulations [%/year]"""
return self._system_model.Lifetime.dc_degradation

@dc_degradation.setter
def dc_degradation(self, dc_deg_per_year: Sequence):
self._system_model.Lifetime.dc_degradation = dc_deg_per_year

@property
def dc_ac_ratio(self) -> float:
return self.system_capacity / (self.n_inverters * self.inverter_power)

@property
def module_power(self) -> float:
"""Module power in kW"""
return get_module_power(self._system_model) * 1e-3

@property
def inverter_power(self) -> float:
"""Inverter power in kW"""
return get_inverter_power(self._system_model) * 1e-3

@property
def modules_per_string(self) -> float:
"""Modules per string"""
return self._system_model.SystemDesign.subarray1_modules_per_string

@modules_per_string.setter
def modules_per_string(self, _modules_per_string: float):
"""Sets the modules per string and updates the system capacity"""
self._system_model.SystemDesign.subarray1_modules_per_string = _modules_per_string
self._system_model.SystemDesign.subarray2_modules_per_string = 0
self._system_model.SystemDesign.subarray3_modules_per_string = 0
self._system_model.SystemDesign.subarray4_modules_per_string = 0
self.system_capacity = self.module_power * _modules_per_string * self.n_strings

@property
def n_strings(self) -> float:
"""Total number of strings"""
return self._system_model.SystemDesign.subarray1_nstrings \
+ self._system_model.SystemDesign.subarray2_nstrings \
+ self._system_model.SystemDesign.subarray3_nstrings \
+ self._system_model.SystemDesign.subarray4_nstrings

@n_strings.setter
def n_strings(self, _n_strings: float):
"""Sets the total number of strings and updates the system capacity"""
self._system_model.SystemDesign.subarray1_nstrings = _n_strings
self._system_model.SystemDesign.subarray2_nstrings = 0
self._system_model.SystemDesign.subarray3_nstrings = 0
self._system_model.SystemDesign.subarray4_nstrings = 0
self.system_capacity = self.module_power * self.modules_per_string * _n_strings

@property
def n_inverters(self) -> float:
"""Total number of inverters"""
return self._system_model.SystemDesign.inverter_count

@n_inverters.setter
def n_inverters(self, _n_inverters: float):
"""Sets the total number of inverters"""
self._system_model.SystemDesign.inverter_count = _n_inverters
13 changes: 12 additions & 1 deletion hybrid/hybrid_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from tools.analysis import create_cost_calculator
from hybrid.sites import SiteInfo
from hybrid.pv_source import PVPlant
from hybrid.detailed_pv_plant import DetailedPVPlant
from hybrid.wind_source import WindPlant
from hybrid.tower_source import TowerPlant
from hybrid.trough_source import TroughPlant
Expand Down Expand Up @@ -103,6 +104,11 @@ def __init__(self,
``battery`` :class:`hybrid.battery.Battery`
=============== =============================================
The default PV technology model is PVWatts (Pvwattsv8). The detailed PV model
can be used by setting: ``{'pv': {'use_pvwatts': False}}``
A user-instantiated PV plant can be used by passing the plant object via:
``{'pv': {'pv_plant': plant_object}}``
:param site: :class:`hybrid.sites.site_info.SiteInfo`,
Hybrid plant site information which includes layout, location and resource data
Expand Down Expand Up @@ -148,7 +154,12 @@ def __init__(self,
power_sources[k.lower()] = power_sources.pop(k)

if 'pv' in power_sources.keys():
self.pv = PVPlant(self.site, power_sources['pv'])
if 'pv_plant' in power_sources['pv']:
self.pv = power_sources['pv']['pv_plant'] # User instantiated plant
elif 'use_pvwatts' in power_sources['pv'].keys() and not power_sources['pv']['use_pvwatts']:
self.pv = DetailedPVPlant(self.site, power_sources['pv']) # PVSAMv1 plant
else:
self.pv = PVPlant(self.site, power_sources['pv']) # PVWatts plant
self.power_sources['pv'] = self.pv
logger.info("Created HybridSystem.pv with system size {} mW".format(power_sources['pv']))
if 'wind' in power_sources.keys():
Expand Down
Loading

0 comments on commit 84f2934

Please sign in to comment.