From 6c87ef681370dd37f8d873a551e745006cb22a57 Mon Sep 17 00:00:00 2001 From: Matthew Boyd Date: Wed, 3 May 2023 15:23:10 -0600 Subject: [PATCH 1/9] Improve capacity verification during init --- hybrid/detailed_pv_plant.py | 82 ++++++++++++++++++++-- hybrid/layout/pv_design_utils.py | 115 ++++++++++++++++--------------- 2 files changed, 136 insertions(+), 61 deletions(-) diff --git a/hybrid/detailed_pv_plant.py b/hybrid/detailed_pv_plant.py index c7de34101..1ca6428db 100644 --- a/hybrid/detailed_pv_plant.py +++ b/hybrid/detailed_pv_plant.py @@ -56,15 +56,83 @@ def processed_assign(self, params): to enforce coherence between attributes """ if 'tech_config' in params.keys(): - self.assign(params['tech_config']) + config = params['tech_config'] + + if 'subarray2_enable' in config.keys() and config['subarray2_enable'] == 1 \ + or 'subarray3_enable' in config.keys() and config['subarray3_enable'] == 1 \ + or 'subarray4_enable' in config.keys() and config['subarray4_enable'] == 1: + raise Exception('Detailed PV plant currently only supports one subarray.') + + # Get PV module attributes + system_params = flatten_dict(self._system_model.export()) + system_params.update(config) + module_attribs = get_module_attribs(system_params) + + # Verify system capacity is cohesive with interdependent parameters if all are specified + nstrings_keys = [f'subarray{i}_nstrings' for i in range(1, 5)] + if 'system_capacity' in config.keys() and any(nstrings in config.keys() for nstrings in nstrings_keys): + # Build subarray electrical configuration input lists + n_strings = [] + modules_per_string = [] + for i in range(1, 5): + if i == 1: + subarray_enabled = True + else: + subarray_enabled = config[f'subarray{i}_enable'] \ + if f'subarray{i}_enable' in config.keys() \ + else self.value(f'subarray{i}_enable') + + if not subarray_enabled: + n_strings.append(0) + elif f'subarray{i}_nstrings' in config.keys(): + n_strings.append(config[f'subarray{i}_nstrings']) + else: + try: + n_strings.append(self.value(f'subarray{i}_nstrings')) + except: + n_strings.append(0) + + if f'subarray{i}_modules_per_string' in config.keys(): + modules_per_string.append(config[f'subarray{i}_modules_per_string']) + else: + try: + modules_per_string.append(self.value(f'subarray{i}_modules_per_string')) + except: + modules_per_string.append(0) + + config['system_capacity'] = verify_capacity_from_electrical_parameters( + system_capacity_target=config['system_capacity'], + n_strings=n_strings, + modules_per_string=modules_per_string, + module_power=module_attribs['P_mp_ref'] + ) + + # Set all interdependent parameters directly and at once to avoid interdependent changes with existing values via properties + if 'system_capacity' in config.keys(): + self._system_model.value('system_capacity', config['system_capacity']) + for i in range(1, 5): + if f'subarray{i}_nstrings' in config.keys(): + self._system_model.value(f'subarray{i}_nstrings', config[f'subarray{i}_nstrings']) + if f'subarray{i}_modules_per_string' in config.keys(): + self._system_model.value(f'subarray{i}_modules_per_string', config[f'subarray{i}_modules_per_string']) + if 'module_model' in config.keys(): + self._system_model.value('module_model', config['module_model']) + if 'module_aspect_ratio' in config.keys(): + self._system_model.value('module_aspect_ratio', config['module_aspect_ratio']) + for key in config.keys(): + # set module parameters: + if key.startswith('spe_') \ + or key.startswith('cec_') \ + or key.startswith('sixpar_') \ + or key.startswith('snl_') \ + or key.startswith('sd11par_') \ + or key.startswith('mlm_'): + self._system_model.value(key, config[key]) + + # Set all parameters + self.assign(config) self._layout.set_layout_params(self.system_capacity, self._layout.parameters) - self.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 - ) def simulate_financials(self, interconnect_kw: float, project_life: int): """ diff --git a/hybrid/layout/pv_design_utils.py b/hybrid/layout/pv_design_utils.py index 5dbd115c2..e7c18fb74 100644 --- a/hybrid/layout/pv_design_utils.py +++ b/hybrid/layout/pv_design_utils.py @@ -1,4 +1,7 @@ import math +from typing import Union, List +import numpy as np +from tools.utils import flatten_dict import PySAM.Pvsamv1 as pv_detailed import PySAM.Pvwattsv8 as pv_simple import hybrid.layout.pv_module as pvwatts_defaults @@ -114,8 +117,8 @@ def size_electrical_parameters( # Verify sizing was close to the target size, otherwise error out calculated_system_capacity = verify_capacity_from_electrical_parameters( system_capacity_target=target_system_capacity, - n_strings=n_strings, - modules_per_string=modules_per_string, + n_strings=[n_strings], + modules_per_string=[modules_per_string], module_power=module_power ) @@ -124,8 +127,8 @@ def size_electrical_parameters( def verify_capacity_from_electrical_parameters( system_capacity_target: float, - n_strings: float, - modules_per_string: float, + n_strings: List[int], + modules_per_string: List[int], module_power: float, ) -> float: """ @@ -133,14 +136,15 @@ def verify_capacity_from_electrical_parameters( If computed capacity is significantly different than the specified capacity an exception will be thrown. :param system_capacity_target: target system capacity, kW - :param n_strings: number of strings in array, - - :param modules_per_string: modules per string, - + :param n_strings: number of strings in each subarray, - + :param modules_per_string: modules per string in each subarray, - :param module_power: module power at maximum point point at reference conditions, kW :returns: calculated system capacity, kW """ PERCENT_MAX_DEVIATION = 5 # [%] - calculated_system_capacity = n_strings * modules_per_string * module_power + assert len(n_strings) == len(modules_per_string) + calculated_system_capacity = sum(np.array(n_strings) * np.array(modules_per_string)) * module_power if abs((calculated_system_capacity / system_capacity_target - 1)) * 100 > PERCENT_MAX_DEVIATION: raise Exception(f"The specified system capacity of {system_capacity_target} kW is more than " \ f"{PERCENT_MAX_DEVIATION}% from the value calculated from the specified number " \ @@ -215,12 +219,12 @@ def spe_power(spe_eff_level, spe_rad_level, spe_area) -> float: return spe_eff_level / 100 * spe_rad_level * spe_area -def get_module_attribs(model) -> dict: +def get_module_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1, dict]) -> dict: """ Returns the module attributes for either the PVsamv1 or PVWattsv8 models, see: https://nrel-pysam.readthedocs.io/en/main/modules/Pvsamv1.html#module-group - :param model: PVsamv1 or PVWattsv8 model + :param model: PVsamv1 or PVWattsv8 model or parameter dictionary :return: dict, with keys: area [m2] aspect_ratio [-] @@ -232,7 +236,10 @@ def get_module_attribs(model) -> dict: V_oc_ref [V] width [m] """ - if isinstance(model, pv_simple.Pvwattsv8): + if not isinstance(model, dict): + model = flatten_dict(model.export()) + + if 'module_model' not in model: # Pvwattsv8 P_mp = pvwatts_defaults.module_power I_mp = None I_sc = None @@ -242,61 +249,61 @@ def get_module_attribs(model) -> dict: width = pvwatts_defaults.module_width area = length * width aspect_ratio = length / width - elif isinstance(model, pv_detailed.Pvsamv1): + else: # Pvsamv1 # module_model: 0=spe, 1=cec, 2=sixpar_user, #3=snl, 4=sd11-iec61853, 5=PVYield - module_model = int(model.value('module_model')) + module_model = int(model['module_model']) if module_model == 0: # spe SPE_FILL_FACTOR_ASSUMED = 0.79 P_mp = spe_power( - model.value('spe_eff4'), - model.value('spe_rad4'), - model.value('spe_area')) # 4 = reference conditions - I_mp = P_mp / model.value('spe_vmp') - I_sc = model.value('spe_vmp') * model.value('spe_imp') \ - / (model.value('spe_voc') * SPE_FILL_FACTOR_ASSUMED) - V_oc = model.value('spe_voc') - V_mp = model.value('spe_vmp') - area = model.value('spe_area') - aspect_ratio = model.value('module_aspect_ratio') + model['spe_eff4'], + model['spe_rad4'], + model['spe_area']) # 4 = reference conditions + I_mp = P_mp / model['spe_vmp'] + I_sc = model['spe_vmp'] * model['spe_imp'] \ + / (model['spe_voc'] * SPE_FILL_FACTOR_ASSUMED) + V_oc = model['spe_voc'] + V_mp = model['spe_vmp'] + area = model['spe_area'] + aspect_ratio = model['module_aspect_ratio'] elif module_model == 1: # cec - I_mp = model.value('cec_i_mp_ref') - I_sc = model.value('cec_i_sc_ref') - V_oc = model.value('cec_v_oc_ref') - V_mp = model.value('cec_v_mp_ref') - area = model.value('cec_area') + I_mp = model['cec_i_mp_ref'] + I_sc = model['cec_i_sc_ref'] + V_oc = model['cec_v_oc_ref'] + V_mp = model['cec_v_mp_ref'] + area = model['cec_area'] try: - aspect_ratio = model.value('cec_module_length') \ - / model.value('cec_module_width') + aspect_ratio = model['cec_module_length'] \ + / model['cec_module_width'] except: - aspect_ratio = model.value('module_aspect_ratio') + aspect_ratio = model['module_aspect_ratio'] elif module_model == 2: # sixpar_user - I_mp = model.value('sixpar_imp') - I_sc = model.value('sixpar_isc') - V_oc = model.value('sixpar_voc') - V_mp = model.value('sixpar_vmp') - area = model.value('sixpar_area') - aspect_ratio = model.value('module_aspect_ratio') + I_mp = model['sixpar_imp'] + I_sc = model['sixpar_isc'] + V_oc = model['sixpar_voc'] + V_mp = model['sixpar_vmp'] + area = model['sixpar_area'] + aspect_ratio = model['module_aspect_ratio'] elif module_model == 3: # snl - I_mp = model.value('snl_impo') - I_sc = model.value('snl_isco') - V_oc = model.value('snl_voco') - V_mp = model.value('snl_vmpo') - area = model.value('snl_area') - aspect_ratio = model.value('module_aspect_ratio') + I_mp = model['snl_impo'] + I_sc = model['snl_isco'] + V_oc = model['snl_voco'] + V_mp = model['snl_vmpo'] + area = model['snl_area'] + aspect_ratio = model['module_aspect_ratio'] elif module_model == 4: # sd11-iec61853 - I_mp = model.value('sd11par_Imp0') - I_sc = model.value('sd11par_Isc0') - V_oc = model.value('sd11par_Voc0') - V_mp = model.value('sd11par_Vmp0') - area = model.value('sd11par_area') - aspect_ratio = model.value('module_aspect_ratio') + I_mp = model['sd11par_Imp0'] + I_sc = model['sd11par_Isc0'] + V_oc = model['sd11par_Voc0'] + V_mp = model['sd11par_Vmp0'] + area = model['sd11par_area'] + aspect_ratio = model['module_aspect_ratio'] elif module_model == 5: # PVYield - I_mp = model.value('mlm_I_mp_ref') - I_sc = model.value('mlm_I_sc_ref') - V_oc = model.value('mlm_V_oc_ref') - V_mp = model.value('mlm_V_mp_ref') - area = model.value('mlm_Length') * model.value('mlm_Width') - aspect_ratio = model.value('mlm_Length') / model.value('mlm_Width') + I_mp = model['mlm_I_mp_ref'] + I_sc = model['mlm_I_sc_ref'] + V_oc = model['mlm_V_oc_ref'] + V_mp = model['mlm_V_mp_ref'] + area = model['mlm_Length'] * model['mlm_Width'] + aspect_ratio = model['mlm_Length'] / model['mlm_Width'] else: raise Exception("Module model number not recognized.") From 90fea534120e45c9494091b85039bc0042cd5deb Mon Sep 17 00:00:00 2001 From: Matthew Boyd Date: Thu, 4 May 2023 14:29:39 -0600 Subject: [PATCH 2/9] Get inverter attributes for pvwattsv8 --- hybrid/layout/pv_design_utils.py | 103 ++++++++++++++++++------------- 1 file changed, 59 insertions(+), 44 deletions(-) diff --git a/hybrid/layout/pv_design_utils.py b/hybrid/layout/pv_design_utils.py index e7c18fb74..a1b1e0539 100644 --- a/hybrid/layout/pv_design_utils.py +++ b/hybrid/layout/pv_design_utils.py @@ -324,12 +324,13 @@ def get_module_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1, di } -def get_inverter_attribs(pvsam_model: pv_detailed.Pvsamv1) -> dict: +def get_inverter_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1, dict]) -> dict: """ - Returns the inverter attributes for the PVsamv1 model, see: + Returns the inverter attributes for the PVwattsv8 or PVsamv1 model, see: + https://nrel-pysam.readthedocs.io/en/main/modules/Pvwattsv8.html#systemdesign-group https://nrel-pysam.readthedocs.io/en/main/modules/Pvsamv1.html#inverter-group - :param pvsam_model: PVsamv1 model + :param model: PVsamv1 or PVWattsv8 model or parameter dictionary :return: dict, with keys: V_mpp_nom [V] V_dc_max [V] @@ -340,55 +341,69 @@ def get_inverter_attribs(pvsam_model: pv_detailed.Pvsamv1) -> dict: V_mppt_min [V] V_mppt_max [V] """ - inverter_model = int(pvsam_model.value('inverter_model')) # 0=cec, 1=datasheet, 2=partload, 3=coefficientgenerator, 4=PVYield - if inverter_model == 0: # cec - V_mpp_nom = pvsam_model.value('inv_snl_vdco') - V_dc_max = pvsam_model.value('inv_snl_vdcmax') - P_ac = pvsam_model.value('inv_snl_paco') - P_dc = pvsam_model.value('inv_snl_pdco') - P_ac_night_loss = pvsam_model.value('inv_snl_pnt') - elif inverter_model == 1: # datasheet - V_mpp_nom = pvsam_model.value('inv_ds_vdco') - V_dc_max = pvsam_model.value('inv_ds_vdcmax') - P_ac = pvsam_model.value('inv_ds_paco') - P_dc = pvsam_model.value('inv_ds_pdco') - P_ac_night_loss = pvsam_model.value('inv_ds_pnt') - elif inverter_model == 2: # partload - V_mpp_nom = pvsam_model.value('inv_pd_vdco') - V_dc_max = pvsam_model.value('inv_pd_vdcmax') - P_ac = pvsam_model.value('inv_pd_paco') - P_dc = pvsam_model.value('inv_pd_pdco') - P_ac_night_loss = pvsam_model.value('inv_pd_pnt') - elif inverter_model == 3: # coefficientgenerator - V_mpp_nom = pvsam_model.value('inv_cec_cg_vdco') - V_dc_max = pvsam_model.value('inv_cec_cg_vdcmax') - P_ac = pvsam_model.value('inv_cec_cg_paco') - P_dc = pvsam_model.value('inv_cec_cg_pdco') - P_ac_night_loss = pvsam_model.value('inv_cec_cg_pnt') - elif inverter_model == 4: # PVYield TODO: these should be verified - V_mpp_nom = pvsam_model.value('ond_VNomEff') - V_dc_max = pvsam_model.value('ond_VAbsMax') - P_ac = pvsam_model.value('ond_PMaxOUT') - P_dc = pvsam_model.value('ond_PNomDC') - P_ac_night_loss = pvsam_model.value('ond_Night_Loss') - else: - raise Exception("Inverter model number not recognized.") + if not isinstance(model, dict): + model = flatten_dict(model.export()) - n_mppt_inputs = pvsam_model.value('inv_num_mppt') + if 'inverter_model' not in model: # Pvwattsv8 + V_mpp_nom = None + V_dc_max = None + P_ac = model['system_capacity'] / model['dc_ac_ratio'] * 1e3 # [W] + P_dc = P_ac / model['inv_eff'] # [W] + P_ac_night_loss = None + n_mppt_inputs = None + V_mppt_min = None + V_mppt_max = None + else: # Pvsamv1 + # 0=cec, 1=datasheet, 2=partload, 3=coefficientgenerator, 4=PVYield + inverter_model = int(model['inverter_model']) + if inverter_model == 0: # cec + V_mpp_nom = model['inv_snl_vdco'] + V_dc_max = model['inv_snl_vdcmax'] + P_ac = model['inv_snl_paco'] + P_dc = model['inv_snl_pdco'] + P_ac_night_loss = model['inv_snl_pnt'] + elif inverter_model == 1: # datasheet + V_mpp_nom = model['inv_ds_vdco'] + V_dc_max = model['inv_ds_vdcmax'] + P_ac = model['inv_ds_paco'] + P_dc = model['inv_ds_pdco'] + P_ac_night_loss = model['inv_ds_pnt'] + elif inverter_model == 2: # partload + V_mpp_nom = model['inv_pd_vdco'] + V_dc_max = model['inv_pd_vdcmax'] + P_ac = model['inv_pd_paco'] + P_dc = model['inv_pd_pdco'] + P_ac_night_loss = model['inv_pd_pnt'] + elif inverter_model == 3: # coefficientgenerator + V_mpp_nom = model['inv_cec_cg_vdco'] + V_dc_max = model['inv_cec_cg_vdcmax'] + P_ac = model['inv_cec_cg_paco'] + P_dc = model['inv_cec_cg_pdco'] + P_ac_night_loss = model['inv_cec_cg_pnt'] + elif inverter_model == 4: # PVYield TODO: these should be verified + V_mpp_nom = model['ond_VNomEff'] + V_dc_max = model['ond_VAbsMax'] + P_ac = model['ond_PMaxOUT'] + P_dc = model['ond_PNomDC'] + P_ac_night_loss = model['ond_Night_Loss'] + else: + raise Exception("Inverter model number not recognized.") - if inverter_model == 4: - V_mppt_min = pvsam_model.InverterMermoudLejeuneModel.ond_VMppMin - V_mppt_max = pvsam_model.InverterMermoudLejeuneModel.ond_VMPPMax - else: - V_mppt_min = pvsam_model.Inverter.mppt_low_inverter - V_mppt_max = pvsam_model.Inverter.mppt_hi_inverter + n_mppt_inputs = model['inv_num_mppt'] + + if inverter_model == 4: + V_mppt_min = model['ond_VMppMin'] + V_mppt_max = model['ond_VMPPMax'] + else: + V_mppt_min = model['mppt_low_inverter'] + V_mppt_max = model['mppt_hi_inverter'] return { 'V_mpp_nom': V_mpp_nom, # [V] 'V_dc_max': V_dc_max, # [V] 'P_ac': P_ac * 1e-3, # [kW] 'P_dc': P_dc * 1e-3, # [kW] - 'P_ac_night_loss': P_ac_night_loss * 1e-3, # [kW] + 'P_ac_night_loss': P_ac_night_loss * 1e-3 if P_ac_night_loss is not None else None, # [kW] 'n_mppt_inputs': n_mppt_inputs, # [-] 'V_mppt_min': V_mppt_min, # [V] 'V_mppt_max': V_mppt_max, # [V] From 3f54a881138b9b57e491bbea6c4b4e487210c63e Mon Sep 17 00:00:00 2001 From: Matthew Boyd Date: Thu, 4 May 2023 14:32:38 -0600 Subject: [PATCH 3/9] Fix align_from_capacity --- hybrid/layout/pv_design_utils.py | 9 ++++----- hybrid/layout/pv_layout.py | 9 ++++++++- tests/hybrid/test_hybrid.py | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/hybrid/layout/pv_design_utils.py b/hybrid/layout/pv_design_utils.py index a1b1e0539..e4280bc47 100644 --- a/hybrid/layout/pv_design_utils.py +++ b/hybrid/layout/pv_design_utils.py @@ -155,16 +155,17 @@ def verify_capacity_from_electrical_parameters( def align_from_capacity( system_capacity_target: float, + dc_ac_ratio: float, modules_per_string: float, module_power: float, inverter_power: float, - n_inverters_orig: float, ) -> list: """ Ensure coherence between parameters for detailed PV model (pvsamv1), keeping the DC-to-AC ratio approximately the same :param system_capacity_target: target system capacity, kW + :param dc_ac_ratio: DC-to-AC ratio :param modules_per_string: modules per string, - :param module_power: module power at maximum point point at reference conditions, kW :param inverter_power: inverter maximum AC power, kW @@ -176,11 +177,9 @@ def align_from_capacity( n_strings = max(1, round(n_strings_frac)) system_capacity = module_power * n_strings * modules_per_string - # Calculate inverter count, keeping the dc/ac ratio the same as before - dc_ac_ratio_orig = system_capacity / (n_inverters_orig * inverter_power) - if dc_ac_ratio_orig > 0: + if dc_ac_ratio > 0: n_inverters_frac = modules_per_string * n_strings * module_power \ - / (dc_ac_ratio_orig * inverter_power) + / (dc_ac_ratio * inverter_power) else: n_inverters_frac = modules_per_string * n_strings * module_power / inverter_power n_inverters = max(1, round(n_inverters_frac)) diff --git a/hybrid/layout/pv_layout.py b/hybrid/layout/pv_layout.py index 753a27d26..a1fb6ca07 100644 --- a/hybrid/layout/pv_layout.py +++ b/hybrid/layout/pv_layout.py @@ -58,6 +58,9 @@ def __init__(self, self.module_height: float = module_attribs['length'] self.modules_per_string: int = get_modules_per_string(self._system_model) + inverter_attribs = get_inverter_attribs(self._system_model) + self.inverter_power: float = inverter_attribs['P_ac'] + # solar array layout variables self.parameters = parameters @@ -82,10 +85,10 @@ def _set_system_layout(self): elif isinstance(self._system_model, pv_detailed.Pvsamv1): n_strings, system_capacity, n_inverters = align_from_capacity( system_capacity_target=target_solar_kw, + dc_ac_ratio=self.get_dc_ac_ratio(), modules_per_string=self.modules_per_string, module_power=self.module_power, inverter_power=get_inverter_attribs(self._system_model)['P_ac'], - n_inverters_orig=self._system_model.SystemDesign.inverter_count ) self._system_model.SystemDesign.subarray1_nstrings = n_strings self._system_model.SystemDesign.system_capacity = system_capacity @@ -224,6 +227,10 @@ def set_flicker_loss(self, self.flicker_loss = flicker_loss_multipler self._set_system_layout() + def get_dc_ac_ratio(self): + return self._system_model.value('system_capacity') / \ + (self._system_model.value('inverter_count') * self.inverter_power) + def plot(self, figure=None, axes=None, diff --git a/tests/hybrid/test_hybrid.py b/tests/hybrid/test_hybrid.py index 407fb7ca6..84782c105 100644 --- a/tests/hybrid/test_hybrid.py +++ b/tests/hybrid/test_hybrid.py @@ -185,8 +185,8 @@ def test_hybrid_detailed_pv_only(site): assert npvs.hybrid == approx(npv_expected, 1e-3) # Run detailed PV model using parameters from file and autosizing electrical parameters - annual_energy_expected = 102065385 - npv_expected = -26537322 + annual_energy_expected = 102439127 + npv_expected = -26503369 pvsamv1_defaults_file = Path(__file__).absolute().parent / "pvsamv1_basic_params.json" with open(pvsamv1_defaults_file, 'r') as f: tech_config = json.load(f) From cd15eb794bc114998b2c4d8241228d4698b3d4f9 Mon Sep 17 00:00:00 2001 From: Matthew Boyd Date: Fri, 5 May 2023 15:30:13 -0600 Subject: [PATCH 4/9] Add properties for system capacity, modules per string and number of strings --- hybrid/detailed_pv_plant.py | 47 +++++++++- tests/analysis/test_custom_financial.py | 117 ++++++++++++++++++++++++ 2 files changed, 159 insertions(+), 5 deletions(-) diff --git a/hybrid/detailed_pv_plant.py b/hybrid/detailed_pv_plant.py index 1ca6428db..6a7182ec6 100644 --- a/hybrid/detailed_pv_plant.py +++ b/hybrid/detailed_pv_plant.py @@ -170,13 +170,28 @@ 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): + def system_capacity_kw(self, system_capacity_kw_: float): """ Sets the system capacity - :param size_kw: DC system size in kW + :param system_capacity_kw_: DC system size in kW :return: """ - self._system_model.value('system_capacity', size_kw) + n_strings, system_capacity, n_inverters = align_from_capacity( + system_capacity_target=system_capacity_kw_, + dc_ac_ratio=self.dc_ac_ratio, + modules_per_string=self.modules_per_string, + module_power=self.module_power, + inverter_power=self.inverter_power, + ) + self._system_model.value('system_capacity', system_capacity) + self._system_model.value('subarray1_nstrings', n_strings) + self._system_model.value('subarray2_nstrings', 0) + self._system_model.value('subarray3_nstrings', 0) + self._system_model.value('subarray4_nstrings', 0) + self._system_model.value('subarray2_enable', 0) + self._system_model.value('subarray3_enable', 0) + self._system_model.value('subarray4_enable', 0) + self._system_model.value('inverter_count', n_inverters) @property def dc_degradation(self) -> float: @@ -231,7 +246,18 @@ def modules_per_string(self, _modules_per_string: float): 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 + # update system capacity directly to not recalculate the number of inverters, consistent with the SAM UI + self._system_model.value('system_capacity', self.module_power * _modules_per_string * self.n_strings) + + @property + def subarray1_modules_per_string(self) -> float: + """Number of modules per string in subarray 1""" + return self._system_model.value('subarray1_modules_per_string') + + @subarray1_modules_per_string.setter + def subarray1_modules_per_string(self, subarray1_modules_per_string_: float): + """Sets the number of modules per string in subarray 1, which is for now the same in all subarrays""" + self.modules_per_string = subarray1_modules_per_string_ @property def n_strings(self) -> float: @@ -248,7 +274,18 @@ def n_strings(self, _n_strings: float): 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 + # update system capacity directly to not recalculate the number of inverters, consistent with the SAM UI + self._system_model.value('system_capacity', self.module_power * self.modules_per_string * _n_strings) + + @property + def subarray1_nstrings(self) -> float: + """Number of strings in subarray 1""" + return self._system_model.value('subarray1_nstrings') + + @subarray1_nstrings.setter + def subarray1_nstrings(self, subarray1_nstrings_: float): + """Sets the number of strings in subarray 1, which is for now the total number of strings""" + self.n_strings = subarray1_nstrings_ @property def n_inverters(self) -> float: diff --git a/tests/analysis/test_custom_financial.py b/tests/analysis/test_custom_financial.py index 7fd416e21..786e577e1 100644 --- a/tests/analysis/test_custom_financial.py +++ b/tests/analysis/test_custom_financial.py @@ -57,6 +57,123 @@ def test_custom_financial(): assert npv == approx(7412807, 1e-3) +def test_detailed_pv_properties(site): + SYSTEM_CAPACITY_DEFAULT = 50002.22178 + SUBARRAY1_NSTRINGS_DEFAULT = 13435 + SUBARRAY1_MODULES_PER_STRING_DEFAULT = 12 + INVERTER_COUNT_DEFAULT = 99 + CEC_V_MP_REF_DEFAULT = 54.7 + CEC_I_MP_REF_DEFAULT = 5.67 + INV_SNL_PACO_DEFAULT = 753200 + DC_AC_RATIO_DEFAULT = 0.67057 + + pvsamv1_defaults_file = Path(__file__).absolute().parent.parent / "hybrid/pvsamv1_basic_params.json" + with open(pvsamv1_defaults_file, 'r') as f: + tech_config = json.load(f) + + # Verify the values in the pvsamv1_basic_params.json config file are as expected + assert tech_config['system_capacity'] == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) + assert tech_config['subarray1_nstrings'] == SUBARRAY1_NSTRINGS_DEFAULT + assert tech_config['subarray1_modules_per_string'] == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert tech_config['inverter_count'] == INVERTER_COUNT_DEFAULT + assert tech_config['cec_v_mp_ref'] == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert tech_config['cec_i_mp_ref'] == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert tech_config['inv_snl_paco'] == approx(INV_SNL_PACO_DEFAULT, 1e-3) + + # Create a detailed PV plant with the pvsamv1_basic_params.json config file + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + # Verify that the detailed PV plant has the same values as in the config file + def verify_defaults(): + assert detailed_pvplant.value('system_capacity') == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(DC_AC_RATIO_DEFAULT, 1e-3) + verify_defaults() + + # Modify system capacity and check that values update correctly + detailed_pvplant.value('system_capacity', 20000) + assert detailed_pvplant.value('system_capacity') == approx(20000.889, 1e-6) + assert detailed_pvplant.value('subarray1_nstrings') == 5374 + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert detailed_pvplant.value('inverter_count') == 40 + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + # The dc_ac_ratio changes because the inverter_count is a function of the system capacity, and it is rounded to an integer. + # Changes to the inverter count do not influence the system capacity, therefore the dc_ac_ratio does not adjust back to the original value + assert detailed_pvplant.dc_ac_ratio == approx(0.6639, 1e-3) + # Reset system capacity back to the default value to verify values update correctly + detailed_pvplant.value('system_capacity', SYSTEM_CAPACITY_DEFAULT) + # The dc_ac_ratio is not noticeably affected because the inverter_count, calculated from the prior dc_ac_ratio, barely changed when rounded + assert detailed_pvplant.dc_ac_ratio == approx(0.6639, 1e-3) + assert detailed_pvplant.value('system_capacity') == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + # The inverter count did not change back to the default value because the dc_ac_ratio did not change back to the default value, + # and unlike the UI, there is no 'desired' dc_ac_ratio that is used to calculate the inverter count, only the prior dc_ac_ratio + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + 1 + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(0.664, 1e-3) + + # Reinstantiate (reset) the detailed PV plant + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + # Modify the number of strings and verify that values update correctly + detailed_pvplant.value('subarray1_nstrings', 10000) + assert detailed_pvplant.value('system_capacity') == approx(37217.88, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == 10000 + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(0.499, 1e-3) + # Reset the number of strings back to the default value to verify other values reset back to their defaults + detailed_pvplant.value('subarray1_nstrings', SUBARRAY1_NSTRINGS_DEFAULT) + verify_defaults() + + # Reinstantiate (reset) the detailed PV plant + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + # Modify the modules per string and verify that values update correctly + detailed_pvplant.value('subarray1_modules_per_string', 10) + assert detailed_pvplant.value('system_capacity') == approx(41668.52, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT + assert detailed_pvplant.value('subarray1_modules_per_string') == 10 + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(0.559, 1e-3) + # Reset the modules per string back to the default value to verify other values reset back to their defaults + detailed_pvplant.value('subarray1_modules_per_string', SUBARRAY1_MODULES_PER_STRING_DEFAULT) + verify_defaults() + + # TODO: change the module and inverter + pass + def test_detailed_pv(site): # Run detailed PV model (pvsamv1) using a custom financial model annual_energy_expected = 108239401 From 2ebec1e1784bf68a5bca7b4358ec6405894a57f0 Mon Sep 17 00:00:00 2001 From: Matthew Boyd Date: Wed, 10 May 2023 20:44:20 -0600 Subject: [PATCH 5/9] Add properties for pv modules and inverters --- hybrid/detailed_pv_plant.py | 32 +++ hybrid/layout/pv_design_utils.py | 207 +------------- hybrid/layout/pv_inverter.py | 232 ++++++++++++++++ hybrid/layout/pv_layout.py | 2 +- hybrid/layout/pv_module.py | 341 ++++++++++++++++++++++++ tests/analysis/test_custom_financial.py | 117 -------- tests/hybrid/test_layout.py | 211 +++++++++++++++ 7 files changed, 821 insertions(+), 321 deletions(-) create mode 100644 hybrid/layout/pv_inverter.py diff --git a/hybrid/detailed_pv_plant.py b/hybrid/detailed_pv_plant.py index 6a7182ec6..a2e5e9c81 100644 --- a/hybrid/detailed_pv_plant.py +++ b/hybrid/detailed_pv_plant.py @@ -6,6 +6,8 @@ from hybrid.layout.pv_design_utils import * from hybrid.layout.pv_layout import PVLayout, PVGridParameters 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 class DetailedPVPlant(PowerSource): @@ -155,6 +157,36 @@ def simulate_financials(self, interconnect_kw: float, project_life: int): self._financial_model.value('om_replacement_cost_escal', self._system_model.SystemCosts.om_replacement_cost_escal) super().simulate_financials(interconnect_kw, project_life) + def get_pv_module(self, only_ref_vals=True) -> dict: + """ + Returns the PV module attributes for either the PVsamv1 or PVWattsv8 models + :param only_ref_vals: ``bool``, optional, returns only the reference values (e.g., I_sc_ref) if True or model params if False + """ + return get_module_attribs(self._system_model, only_ref_vals) + + def set_pv_module(self, params: dict): + """ + Sets the PV module model parameters for either the PVsamv1 or PVWattsv8 models. + :param params: dictionary of parameters + """ + set_module_attribs(self._system_model, params) + # update system capacity directly to not recalculate the number of inverters, consistent with the SAM UI + self._system_model.value('system_capacity', self.module_power * self.modules_per_string * self.n_strings) + + def get_inverter(self, only_ref_vals=True) -> dict: + """ + Returns the inverter attributes for either the PVsamv1 or PVWattsv8 models + :param only_ref_vals: ``bool``, optional, returns only the reference values (e.g., V_dc_max) if True or model params if False + """ + return get_inverter_attribs(self._system_model, only_ref_vals) + + def set_inverter(self, params: dict): + """ + Sets the inverter model parameters for either the PVsamv1 or PVWattsv8 models. + :param params: dictionary of parameters + """ + set_inverter_attribs(self._system_model, params) + @property def system_capacity(self) -> float: """pass through to established name property""" diff --git a/hybrid/layout/pv_design_utils.py b/hybrid/layout/pv_design_utils.py index e4280bc47..59109ff29 100644 --- a/hybrid/layout/pv_design_utils.py +++ b/hybrid/layout/pv_design_utils.py @@ -1,10 +1,9 @@ import math -from typing import Union, List +from typing import List import numpy as np -from tools.utils import flatten_dict import PySAM.Pvsamv1 as pv_detailed -import PySAM.Pvwattsv8 as pv_simple -import hybrid.layout.pv_module as pvwatts_defaults +import hybrid.layout.pv_module as pv_module +from hybrid.layout.pv_inverter import get_inverter_attribs """ @@ -203,207 +202,9 @@ def get_modules_per_string(system_model) -> float: if isinstance(system_model, pv_detailed.Pvsamv1): return system_model.value('subarray1_modules_per_string') else: - return pvwatts_defaults.modules_per_string + return pv_module.modules_per_string def get_inverter_power(pvsam_model: pv_detailed.Pvsamv1) -> float: inverter_attribs = get_inverter_attribs(pvsam_model) return inverter_attribs['P_ac'] - - -def spe_power(spe_eff_level, spe_rad_level, spe_area) -> float: - """ - Computes the module power per the SPE model - """ - return spe_eff_level / 100 * spe_rad_level * spe_area - - -def get_module_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1, dict]) -> dict: - """ - Returns the module attributes for either the PVsamv1 or PVWattsv8 models, see: - https://nrel-pysam.readthedocs.io/en/main/modules/Pvsamv1.html#module-group - - :param model: PVsamv1 or PVWattsv8 model or parameter dictionary - :return: dict, with keys: - area [m2] - aspect_ratio [-] - length [m] - I_mp_ref [A] - I_sc_ref [A] - P_mp_ref [kW] - V_mp_ref [V] - V_oc_ref [V] - width [m] - """ - if not isinstance(model, dict): - model = flatten_dict(model.export()) - - if 'module_model' not in model: # Pvwattsv8 - P_mp = pvwatts_defaults.module_power - I_mp = None - I_sc = None - V_oc = None - V_mp = None - length = pvwatts_defaults.module_height - width = pvwatts_defaults.module_width - area = length * width - aspect_ratio = length / width - else: # Pvsamv1 - # module_model: 0=spe, 1=cec, 2=sixpar_user, #3=snl, 4=sd11-iec61853, 5=PVYield - module_model = int(model['module_model']) - if module_model == 0: # spe - SPE_FILL_FACTOR_ASSUMED = 0.79 - P_mp = spe_power( - model['spe_eff4'], - model['spe_rad4'], - model['spe_area']) # 4 = reference conditions - I_mp = P_mp / model['spe_vmp'] - I_sc = model['spe_vmp'] * model['spe_imp'] \ - / (model['spe_voc'] * SPE_FILL_FACTOR_ASSUMED) - V_oc = model['spe_voc'] - V_mp = model['spe_vmp'] - area = model['spe_area'] - aspect_ratio = model['module_aspect_ratio'] - elif module_model == 1: # cec - I_mp = model['cec_i_mp_ref'] - I_sc = model['cec_i_sc_ref'] - V_oc = model['cec_v_oc_ref'] - V_mp = model['cec_v_mp_ref'] - area = model['cec_area'] - try: - aspect_ratio = model['cec_module_length'] \ - / model['cec_module_width'] - except: - aspect_ratio = model['module_aspect_ratio'] - elif module_model == 2: # sixpar_user - I_mp = model['sixpar_imp'] - I_sc = model['sixpar_isc'] - V_oc = model['sixpar_voc'] - V_mp = model['sixpar_vmp'] - area = model['sixpar_area'] - aspect_ratio = model['module_aspect_ratio'] - elif module_model == 3: # snl - I_mp = model['snl_impo'] - I_sc = model['snl_isco'] - V_oc = model['snl_voco'] - V_mp = model['snl_vmpo'] - area = model['snl_area'] - aspect_ratio = model['module_aspect_ratio'] - elif module_model == 4: # sd11-iec61853 - I_mp = model['sd11par_Imp0'] - I_sc = model['sd11par_Isc0'] - V_oc = model['sd11par_Voc0'] - V_mp = model['sd11par_Vmp0'] - area = model['sd11par_area'] - aspect_ratio = model['module_aspect_ratio'] - elif module_model == 5: # PVYield - I_mp = model['mlm_I_mp_ref'] - I_sc = model['mlm_I_sc_ref'] - V_oc = model['mlm_V_oc_ref'] - V_mp = model['mlm_V_mp_ref'] - area = model['mlm_Length'] * model['mlm_Width'] - aspect_ratio = model['mlm_Length'] / model['mlm_Width'] - else: - raise Exception("Module model number not recognized.") - - P_mp = I_mp * V_mp * 1e-3 # [kW] - width = math.sqrt(area / aspect_ratio) - length = math.sqrt(area * aspect_ratio) - - return { - 'area': area, # [m2] - 'aspect_ratio': aspect_ratio, # [-] - 'length': length, # [m] - 'I_mp_ref': I_mp, # [A] - 'I_sc_ref': I_sc, # [A] - 'P_mp_ref': P_mp, # [kW] - 'V_mp_ref': V_mp, # [V] - 'V_oc_ref': V_oc, # [V] - 'width': width # [m] - } - - -def get_inverter_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1, dict]) -> dict: - """ - Returns the inverter attributes for the PVwattsv8 or PVsamv1 model, see: - https://nrel-pysam.readthedocs.io/en/main/modules/Pvwattsv8.html#systemdesign-group - https://nrel-pysam.readthedocs.io/en/main/modules/Pvsamv1.html#inverter-group - - :param model: PVsamv1 or PVWattsv8 model or parameter dictionary - :return: dict, with keys: - V_mpp_nom [V] - V_dc_max [V] - P_ac [kW] - P_dc [kW] - P_ac_night_loss [kW] - n_mppt_inputs [-] - V_mppt_min [V] - V_mppt_max [V] - """ - if not isinstance(model, dict): - model = flatten_dict(model.export()) - - if 'inverter_model' not in model: # Pvwattsv8 - V_mpp_nom = None - V_dc_max = None - P_ac = model['system_capacity'] / model['dc_ac_ratio'] * 1e3 # [W] - P_dc = P_ac / model['inv_eff'] # [W] - P_ac_night_loss = None - n_mppt_inputs = None - V_mppt_min = None - V_mppt_max = None - else: # Pvsamv1 - # 0=cec, 1=datasheet, 2=partload, 3=coefficientgenerator, 4=PVYield - inverter_model = int(model['inverter_model']) - if inverter_model == 0: # cec - V_mpp_nom = model['inv_snl_vdco'] - V_dc_max = model['inv_snl_vdcmax'] - P_ac = model['inv_snl_paco'] - P_dc = model['inv_snl_pdco'] - P_ac_night_loss = model['inv_snl_pnt'] - elif inverter_model == 1: # datasheet - V_mpp_nom = model['inv_ds_vdco'] - V_dc_max = model['inv_ds_vdcmax'] - P_ac = model['inv_ds_paco'] - P_dc = model['inv_ds_pdco'] - P_ac_night_loss = model['inv_ds_pnt'] - elif inverter_model == 2: # partload - V_mpp_nom = model['inv_pd_vdco'] - V_dc_max = model['inv_pd_vdcmax'] - P_ac = model['inv_pd_paco'] - P_dc = model['inv_pd_pdco'] - P_ac_night_loss = model['inv_pd_pnt'] - elif inverter_model == 3: # coefficientgenerator - V_mpp_nom = model['inv_cec_cg_vdco'] - V_dc_max = model['inv_cec_cg_vdcmax'] - P_ac = model['inv_cec_cg_paco'] - P_dc = model['inv_cec_cg_pdco'] - P_ac_night_loss = model['inv_cec_cg_pnt'] - elif inverter_model == 4: # PVYield TODO: these should be verified - V_mpp_nom = model['ond_VNomEff'] - V_dc_max = model['ond_VAbsMax'] - P_ac = model['ond_PMaxOUT'] - P_dc = model['ond_PNomDC'] - P_ac_night_loss = model['ond_Night_Loss'] - else: - raise Exception("Inverter model number not recognized.") - - n_mppt_inputs = model['inv_num_mppt'] - - if inverter_model == 4: - V_mppt_min = model['ond_VMppMin'] - V_mppt_max = model['ond_VMPPMax'] - else: - V_mppt_min = model['mppt_low_inverter'] - V_mppt_max = model['mppt_hi_inverter'] - - return { - 'V_mpp_nom': V_mpp_nom, # [V] - 'V_dc_max': V_dc_max, # [V] - 'P_ac': P_ac * 1e-3, # [kW] - 'P_dc': P_dc * 1e-3, # [kW] - 'P_ac_night_loss': P_ac_night_loss * 1e-3 if P_ac_night_loss is not None else None, # [kW] - 'n_mppt_inputs': n_mppt_inputs, # [-] - 'V_mppt_min': V_mppt_min, # [V] - 'V_mppt_max': V_mppt_max, # [V] - } diff --git a/hybrid/layout/pv_inverter.py b/hybrid/layout/pv_inverter.py new file mode 100644 index 000000000..8544f7fe0 --- /dev/null +++ b/hybrid/layout/pv_inverter.py @@ -0,0 +1,232 @@ +from typing import Union +import PySAM.Pvsamv1 as pv_detailed +import PySAM.Pvwattsv8 as pv_simple + +from tools.utils import flatten_dict + +def get_inverter_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1, dict], only_ref_values=True) -> dict: + """ + Returns the inverter attributes for the PVwattsv8 or PVsamv1 model, see: + https://nrel-pysam.readthedocs.io/en/main/modules/Pvwattsv8.html#systemdesign-group + https://nrel-pysam.readthedocs.io/en/main/modules/Pvsamv1.html#inverter-group + + :param model: PVsamv1 or PVWattsv8 model or parameter dictionary + :param only_ref_vals: if True, only return the reference values (e.g., P_ac) + :return: dict, with keys (if only_ref_values is True, otherwise will include all model-specific parameters): + V_mpp_nom [V] + V_dc_max [V] + P_ac [kW] + P_dc [kW] + P_ac_night_loss [kW] + n_mppt_inputs [-] + V_mppt_min [V] + V_mppt_max [V] + """ + MODEL_PREFIX = ['inv_snl', 'inv_ds', 'inv_pd', 'inv_cec', 'ond'] + + if not isinstance(model, dict): + model = flatten_dict(model.export()) + + params = {} + if 'inverter_model' not in model: # Pvwattsv8 + params['V_mpp_nom'] = None + params['V_dc_max'] = None + params['P_ac'] = model['system_capacity'] / model['dc_ac_ratio'] # [kW] + params['P_dc'] = params['P_ac'] / model['inv_eff'] * 1e-3 # [kW] + params['P_ac_night_loss'] = None + params['n_mppt_inputs'] = None + params['V_mppt_min'] = None + params['V_mppt_max'] = None + else: # Pvsamv1 + inverter_model = int(model['inverter_model']) # 0=cec, 1=datasheet, 2=partload, 3=coefficientgenerator, 4=PVYield + if not only_ref_values: + params['inverter_model'] = model['inverter_model'] + params['mppt_low_inverter'] = model['mppt_low_inverter'] + params['mppt_hi_inverter'] = model['mppt_hi_inverter'] + params['inv_num_mppt'] = model['inv_num_mppt'] + if inverter_model < 4: + temp_derate_curve = ['inv_tdc_cec_db', 'inv_tdc_ds', 'inv_tdc_plc', 'inv_tdc_cec_cg'][inverter_model] + params[temp_derate_curve] = model[temp_derate_curve] + for key in model.keys(): + if key.startswith(MODEL_PREFIX[inverter_model] + '_'): + params[key] = model[key] + elif inverter_model == 0: # cec (snl) + param_map = { + 'V_mpp_nom': 'inv_snl_vdco', + 'V_dc_max': 'inv_snl_vdcmax', + 'P_ac': 'inv_snl_paco', + 'P_dc': 'inv_snl_pdco', + 'P_ac_night_loss': 'inv_snl_pnt', + } + elif inverter_model == 1: # datasheet + param_map = { + 'V_mpp_nom': 'inv_ds_vdco', + 'V_dc_max': 'inv_ds_vdcmax', + 'P_ac': 'inv_ds_paco', + 'P_dc': 'inv_ds_pdco', + 'P_ac_night_loss': 'inv_ds_pnt', + } + elif inverter_model == 2: # partload + param_map = { + 'V_mpp_nom': 'inv_pd_vdco', + 'V_dc_max': 'inv_pd_vdcmax', + 'P_ac': 'inv_pd_paco', + 'P_dc': 'inv_pd_pdco', + 'P_ac_night_loss': 'inv_pd_pnt', + } + elif inverter_model == 3: # coefficientgenerator (cec) + param_map = { + 'V_mpp_nom': 'inv_cec_cg_vdco', + 'V_dc_max': 'inv_cec_cg_vdcmax', + 'P_ac': 'inv_cec_cg_paco', + 'P_dc': 'inv_cec_cg_pdco', + 'P_ac_night_loss': 'inv_cec_cg_pnt', + } + elif inverter_model == 4: # PVYield TODO: these should be verified + param_map = { + 'V_mpp_nom': 'ond_VNomEff', + 'V_dc_max': 'ond_VAbsMax', + 'P_ac': 'ond_PMaxOUT', + 'P_dc': 'ond_PNomDC', + 'P_ac_night_loss': 'ond_Night_Loss', + } + else: + raise Exception("Inverter model number not recognized.") + + if only_ref_values: + for key, value in param_map.items(): + params[key] = model[value] + + params['P_ac'] = params['P_ac'] * 1e-3 # [kW] + params['P_dc'] = params['P_dc'] * 1e-3 # [kW] + params['P_ac_night_loss'] = params['P_ac_night_loss'] * 1e-3 # [kW] + + if inverter_model == 4: + params['V_mppt_min'] = model['ond_VMppMin'] + params['V_mppt_max'] = model['ond_VMPPMax'] + else: + params['V_mppt_min'] = model['mppt_low_inverter'] + params['V_mppt_max'] = model['mppt_hi_inverter'] + + return params + +def set_inverter_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1], params: dict): + """ + Sets the inverter model parameters for either the PVsamv1 or PVWattsv8 models. + Will raise exception if not all required parameters are provided. + + :param model: PVWattsv8 or PVsamv1 model + :param params: dictionary of parameters + """ + if isinstance(model, pv_simple.Pvwattsv8): + inverter_model = 'PVWatts' + req_vals = ['inv_eff'] + elif isinstance(model, pv_detailed.Pvsamv1): + if 'inverter_model' not in params.keys(): + params['inverter_model'] = model.value('inverter_model') + req_vals = ['inverter_model'] + + inverter_model = params['inverter_model'] + if inverter_model == 0: # cec (snl) + req_vals += [ + 'inv_snl_c0', 'inv_snl_c1', 'inv_snl_c2', 'inv_snl_c3', + 'inv_snl_paco', + 'inv_snl_pdco', + 'inv_snl_pnt', + 'inv_snl_pso', + 'inv_snl_vdco', + 'inv_snl_vdcmax', + 'inv_tdc_cec_db', + ] + elif inverter_model == 1: # datasheet + req_vals += [ + 'inv_ds_paco', + 'inv_ds_eff', + 'inv_ds_pnt', + 'inv_ds_pso', + 'inv_ds_vdco', + 'inv_ds_vdcmax', + 'inv_tdc_ds', + ] + elif inverter_model == 2: # partload + req_vals += [ + 'inv_pd_paco', + 'inv_pd_pdco', + 'inv_pd_partload', + 'inv_pd_efficiency', + 'inv_pd_pnt', + 'inv_pd_vdco', + 'inv_pd_vdcmax', + 'inv_tdc_plc', + ] + elif inverter_model == 3: # coefficientgenerator (cec) + req_vals += [ + 'inv_cec_cg_c0', 'inv_cec_cg_c1', 'inv_cec_cg_c2', 'inv_cec_cg_c3', + 'inv_cec_cg_paco', + 'inv_cec_cg_pdco', + 'inv_cec_cg_pnt', + 'inv_cec_cg_psco', + 'inv_cec_cg_vdco', + 'inv_cec_cg_vdcmax', + 'inv_tdc_cec_cg', + ] + elif inverter_model == 4: # PVYield + req_vals += [ + 'ond_PNomConv', + 'ond_PMaxOUT', + 'ond_VOutConv', + 'ond_VMppMin', + 'ond_VMPPMax', + 'ond_VAbsMax', + 'ond_PSeuil', + 'ond_ModeOper', + 'ond_CompPMax', + 'ond_CompVMax', + 'ond_ModeAffEnum', + 'ond_PNomDC', + 'ond_PMaxDC', + 'ond_IMaxDC', + 'ond_INomDC', + 'ond_INomAC', + 'ond_IMaxAC', + 'ond_TPNom', + 'ond_TPMax', + 'ond_TPLim1', + 'ond_TPLimAbs', + 'ond_PLim1', + 'ond_PLimAbs', + 'ond_VNomEff', + 'ond_NbInputs', + 'ond_NbMPPT', + 'ond_Aux_Loss', + 'ond_Night_Loss', + 'ond_lossRDc', + 'ond_lossRAc', + 'ond_effCurve_elements', + 'ond_effCurve_Pdc', + 'ond_effCurve_Pac', + 'ond_effCurve_eta', + 'ond_Aux_Loss', + 'ond_Aux_Loss', + 'ond_doAllowOverpower', + 'ond_doUseTemperatureLimit', + ] + else: + raise Exception("Inverter model number not recognized.") + + if 'inv_num_mppt' in params.keys(): + req_vals.append('inv_num_mppt') + if inverter_model == 4 and 'ond_VMppMin' in params.keys(): + req_vals.append('ond_VMppMin') + if inverter_model == 4 and 'ond_VMPPMax' in params.keys(): + req_vals.append('ond_VMPPMax') + if inverter_model != 4 and 'mppt_low_inverter' in params.keys(): + req_vals.append('mppt_low_inverter') + if inverter_model != 4 and 'mppt_hi_inverter' in params.keys(): + req_vals.append('mppt_hi_inverter') + + if not set(req_vals).issubset(params.keys()): + raise Exception("Not all parameters specified for inverter model {}.".format(inverter_model)) + + for value in req_vals: + model.value(value, params[value]) diff --git a/hybrid/layout/pv_layout.py b/hybrid/layout/pv_layout.py index a1fb6ca07..1cf84f06e 100644 --- a/hybrid/layout/pv_layout.py +++ b/hybrid/layout/pv_layout.py @@ -6,7 +6,7 @@ from hybrid.log import hybrid_logger as logger from hybrid.sites import SiteInfo -from hybrid.layout.pv_module import module_width, module_height, modules_per_string, module_power +from hybrid.layout.pv_module import get_module_attribs from hybrid.layout.plot_tools import plot_shape from hybrid.layout.layout_tools import make_polygon_from_bounds from hybrid.layout.pv_layout_tools import find_best_solar_size diff --git a/hybrid/layout/pv_module.py b/hybrid/layout/pv_module.py index eb47f9a74..2367a24f9 100644 --- a/hybrid/layout/pv_module.py +++ b/hybrid/layout/pv_module.py @@ -1,5 +1,12 @@ +import math import numpy as np +from typing import Union +import PySAM.Pvsamv1 as pv_detailed +import PySAM.Pvwattsv8 as pv_simple +from tools.utils import flatten_dict + +# PVWatts default module # pvmismatch standard module description cell_len = 0.124 cell_rows = 12 @@ -17,3 +24,337 @@ module_height = cell_len * cell_cols modules_per_string = 10 module_power = .321 # kW + + +def spe_power(spe_eff_level, spe_rad_level, spe_area) -> float: + """ + Computes the module power per the SPE model + """ + return spe_eff_level / 100 * spe_rad_level * spe_area + + +def get_module_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1, dict], only_ref_vals=True) -> dict: + """ + Returns the module attributes for either the PVsamv1 or PVWattsv8 models, see: + https://nrel-pysam.readthedocs.io/en/main/modules/Pvsamv1.html#module-group + + :param model: PVsamv1 or PVWattsv8 model or parameter dictionary + :param only_ref_vals: if True, only return the reference values (e.g., I_sc_ref) + :return: dict, with keys (if only_ref_values is True, otherwise will include all model-specific parameters): + area [m2] + aspect_ratio [-] + length [m] + I_mp_ref [A] + I_sc_ref [A] + P_mp_ref [kW] + V_mp_ref [V] + V_oc_ref [V] + width [m] + """ + MODEL_PREFIX = ['spe', 'cec', '6par', 'snl', 'sd11par', 'mlm'] + + if not isinstance(model, dict): + model = flatten_dict(model.export()) + + params = {} + if 'module_model' not in model: # Pvwattsv8 + params['P_mp_ref'] = module_power + params['I_mp_ref'] = None + params['I_sc_ref'] = None + params['V_oc_ref'] = None + params['V_mp_ref'] = None + params['length'] = module_height + params['width'] = module_width + params['area'] = params['length'] * params['width'] + params['aspect_ratio'] = params['length'] / params['width'] + else: # Pvsamv1 + module_model = int(model['module_model']) # 0=spe, 1=cec, 2=sixpar_user, #3=snl, 4=sd11-iec61853, 5=PVYield + if not only_ref_vals: + params['module_model'] = model['module_model'] + params['module_aspect_ratio'] = model['module_aspect_ratio'] + for key in model.keys(): + if key.startswith(MODEL_PREFIX[module_model] + '_'): + params[key] = model[key] + elif module_model == 0: # spe + param_map = { + 'V_oc_ref': 'spe_voc', + 'V_mp_ref': 'spe_vmp', + 'area': 'spe_area', + 'aspect_ratio': 'module_aspect_ratio', + } + SPE_FILL_FACTOR_ASSUMED = 0.79 + params['P_mp_ref'] = spe_power( + model['spe_eff4'], + model['spe_rad4'], + model['spe_area'] + ) # 4 = reference conditions + params['I_mp_ref'] = params['P_mp_ref'] / model['spe_vmp'] + params['I_sc_ref'] = model['spe_vmp'] * params['I_mp_ref'] \ + / (model['spe_voc'] * SPE_FILL_FACTOR_ASSUMED) + elif module_model == 1: # cec + param_map = { + 'I_mp_ref': 'cec_i_mp_ref', + 'I_sc_ref': 'cec_i_sc_ref', + 'V_oc_ref': 'cec_v_oc_ref', + 'V_mp_ref': 'cec_v_mp_ref', + 'area': 'cec_area', + 'aspect_ratio': 'module_aspect_ratio', + } + elif module_model == 2: # sixpar_user + param_map = { + 'I_mp_ref': 'sixpar_imp', + 'I_sc_ref': 'sixpar_isc', + 'V_oc_ref': 'sixpar_voc', + 'V_mp_ref': 'sixpar_vmp', + 'area': 'sixpar_area', + 'aspect_ratio': 'module_aspect_ratio', + } + elif module_model == 3: # snl + param_map = { + 'I_mp_ref': 'snl_impo', + 'I_sc_ref': 'snl_isco', + 'V_oc_ref': 'snl_voco', + 'V_mp_ref': 'snl_vmpo', + 'area': 'snl_area', + 'aspect_ratio': 'module_aspect_ratio', + } + elif module_model == 4: # sd11-iec61853 + param_map = { + 'I_mp_ref': 'sd11par_Imp0', + 'I_sc_ref': 'sd11par_Isc0', + 'V_oc_ref': 'sd11par_Voc0', + 'V_mp_ref': 'sd11par_Vmp0', + 'area': 'sd11par_area', + 'aspect_ratio': 'module_aspect_ratio', + } + elif module_model == 5: # PVYield + param_map = { + 'I_mp_ref': 'mlm_I_mp_ref', + 'I_sc_ref': 'mlm_I_sc_ref', + 'V_oc_ref': 'mlm_V_oc_ref', + 'V_mp_ref': 'mlm_V_mp_ref', + } + params['area'] = model['mlm_Length'] * model['mlm_Width'] + params['aspect_ratio'] = model['mlm_Length'] / model['mlm_Width'] + else: + raise Exception("Module model number not recognized.") + + if only_ref_vals: + for key, value in param_map.items(): + params[key] = model[value] + + params['P_mp_ref'] = params['I_mp_ref'] * params['V_mp_ref'] * 1e-3 # [kW] + params['width'] = math.sqrt(params['area'] / params['aspect_ratio']) + params['length'] = math.sqrt(params['area'] * params['aspect_ratio']) + + return params + + +def set_module_attribs(model: Union[pv_simple.Pvwattsv8, pv_detailed.Pvsamv1], params: dict): + """ + Sets the module model parameters for either the PVsamv1 or PVWattsv8 models. + Will raise exception if not all required parameters are provided. + + :param model: PVWattsv8 or PVsamv1 model + :param params: dictionary of parameters + """ + + if isinstance(model, pv_simple.Pvwattsv8): + module_model = 'PVWatts' + req_vals = ['module_type'] + elif isinstance(model, pv_detailed.Pvsamv1): + if 'module_model' not in params.keys(): + params['module_model'] = model.value('module_model') + req_vals = ['module_model'] + + module_model = params['module_model'] + if module_model == 0: # spe + req_vals += [ + 'spe_area', + 'spe_rad0', 'spe_rad1', 'spe_rad2', 'spe_rad3', 'spe_rad4', + 'spe_eff0', 'spe_eff1', 'spe_eff2', 'spe_eff3', 'spe_eff4', + 'spe_reference', + 'spe_module_structure', + 'spe_a', 'spe_b', + 'spe_dT', + 'spe_temp_coeff', + 'spe_fd', + 'spe_vmp', + 'spe_voc', + 'spe_is_bifacial', + 'spe_bifacial_transmission_factor', + 'spe_bifaciality', + 'spe_bifacial_ground_clearance_height', + ] + elif module_model == 1: # cec + req_vals += [ + 'cec_area', + 'cec_a_ref', + 'cec_adjust', + 'cec_alpha_sc', + 'cec_beta_oc', + 'cec_gamma_r', + 'cec_i_l_ref', + 'cec_i_mp_ref', + 'cec_i_o_ref', + 'cec_i_sc_ref', + 'cec_n_s', + 'cec_r_s', + 'cec_r_sh_ref', + 'cec_t_noct', + 'cec_v_mp_ref', + 'cec_v_oc_ref', + 'cec_temp_corr_mode', + 'cec_is_bifacial', + 'cec_bifacial_transmission_factor', + 'cec_bifaciality', + 'cec_bifacial_ground_clearance_height', + 'cec_standoff', + 'cec_height', + 'cec_transient_thermal_model_unit_mass', + ] + if 'cec_temp_corr_mode' in params.keys() and params['cec_temp_corr_mode'] == 1: + req_vals += [ + 'cec_mounting_config', + 'cec_heat_transfer', + 'cec_mounting_orientation', + 'cec_gap_spacing', + 'cec_module_width', + 'cec_module_length', + 'cec_array_rows', + 'cec_array_cols', + 'cec_backside_temp', + ] + if 'cec_lacunarity_enable' in params.keys() and params['cec_lacunarity_enable'] == 1: + req_vals += ['cec_lacunarity_enable'] + if 'cec_temp_corr_mode' in params.keys() and params['cec_temp_corr_mode'] == 1: + req_vals += [ + 'cec_lacunarity_length', + 'cec_ground_clearance_height', + ] + elif module_model == 2: # sixpar_user + req_vals += [ + '6par_celltech', + '6par_vmp', + '6par_imp', + '6par_voc', + '6par_isc', + '6par_bvoc', + '6par_aisc', + '6par_gpmp', + '6par_nser', + '6par_area', + '6par_tnoct', + '6par_standoff', + '6par_mounting', + '6par_is_bifacial', + '6par_bifacial_transmission_factor', + '6par_bifaciality', + '6par_bifacial_ground_clearance_height', + '6par_transient_thermal_model_unit_mass', + ] + elif module_model == 3: # snl + req_vals += [ + 'snl_module_structure', + 'snl_a', + 'snl_b', + 'snl_dtc', + 'snl_ref_a', + 'snl_ref_b', + 'snl_ref_dT', + 'snl_fd', + 'snl_a0', 'snl_a1', 'snl_a2', 'snl_a3', 'snl_a4', + 'snl_aimp', + 'snl_aisc', + 'snl_area', + 'snl_b0', 'snl_b1', 'snl_b2', 'snl_b3', 'snl_b4', 'snl_b5', + 'snl_bvmpo', + 'snl_bvoco', + 'snl_c0', 'snl_c1', 'snl_c2', 'snl_c3', 'snl_c4', 'snl_c5', 'snl_c6', 'snl_c7', + 'snl_impo', + 'snl_isco', + 'snl_ixo', + 'snl_ixxo', + 'snl_mbvmp', + 'snl_mbvoc', + 'snl_n', + 'snl_series_cells', + 'snl_vmpo', + 'snl_voco', + 'snl_transient_thermal_model_unit_mass', + ] + elif module_model == 4: # sd11-iec61853 + req_vals += [ + 'sd11par_nser', + 'sd11par_area', + 'sd11par_AMa0', 'sd11par_AMa1', 'sd11par_AMa2', 'sd11par_AMa3', 'sd11par_AMa4', + 'sd11par_glass', + 'sd11par_tnoct', + 'sd11par_standoff', + 'sd11par_mounting', + 'sd11par_Vmp0', + 'sd11par_Imp0', + 'sd11par_Voc0', + 'sd11par_Isc0', + 'sd11par_alphaIsc', + 'sd11par_n', + 'sd11par_Il', + 'sd11par_Io', + 'sd11par_Egref', + 'sd11par_d1', 'sd11par_d2', 'sd11par_d3', + 'sd11par_c1', 'sd11par_c2', 'sd11par_c3', + ] + elif module_model == 5: # PVYield + req_vals += [ + 'mlm_N_series', + 'mlm_N_parallel', + 'mlm_N_diodes', + 'mlm_Width', + 'mlm_Length', + 'mlm_V_mp_ref', + 'mlm_I_mp_ref', + 'mlm_V_oc_ref', + 'mlm_I_sc_ref', + 'mlm_S_ref', + 'mlm_T_ref', + 'mlm_R_shref', + 'mlm_R_sh0', + 'mlm_R_shexp', + 'mlm_R_s', + 'mlm_alpha_isc', + 'mlm_beta_voc_spec', + 'mlm_E_g', + 'mlm_n_0', + 'mlm_mu_n', + 'mlm_D2MuTau', + 'mlm_T_mode', + 'mlm_T_c_no_tnoct', + 'mlm_T_c_no_mounting', + 'mlm_T_c_no_standoff', + 'mlm_T_c_fa_alpha', + 'mlm_T_c_fa_U0', 'mlm_T_c_fa_U1', + 'mlm_AM_mode', + 'mlm_AM_c_sa0', 'mlm_AM_c_sa1', 'mlm_AM_c_sa2', 'mlm_AM_c_sa3', 'mlm_AM_c_sa4', + 'mlm_AM_c_lp0', 'mlm_AM_c_lp1', 'mlm_AM_c_lp2', 'mlm_AM_c_lp3', 'mlm_AM_c_lp4', 'mlm_AM_c_lp5', + 'mlm_IAM_mode', + 'mlm_IAM_c_as', + 'mlm_IAM_c_sa0', 'mlm_IAM_c_sa1', 'mlm_IAM_c_sa2', 'mlm_IAM_c_sa3', 'mlm_IAM_c_sa4', 'mlm_IAM_c_sa5', + 'mlm_IAM_c_cs_incAngle', + 'mlm_IAM_c_cs_iamValue', + 'mlm_groundRelfectionFraction', + 'mlm_is_bifacial', + 'mlm_bifacial_transmission_factor', + 'mlm_bifaciality', + 'mlm_bifacial_ground_clearance_height', + ] + else: + raise Exception("Module model number not recognized.") + + if 'module_aspect_ratio' in params.keys(): + req_vals.append('module_aspect_ratio') + + if not set(req_vals).issubset(params.keys()): + raise Exception("Not all parameters specified for module model {}.".format(module_model)) + + for value in req_vals: + model.value(value, params[value]) diff --git a/tests/analysis/test_custom_financial.py b/tests/analysis/test_custom_financial.py index 786e577e1..7fd416e21 100644 --- a/tests/analysis/test_custom_financial.py +++ b/tests/analysis/test_custom_financial.py @@ -57,123 +57,6 @@ def test_custom_financial(): assert npv == approx(7412807, 1e-3) -def test_detailed_pv_properties(site): - SYSTEM_CAPACITY_DEFAULT = 50002.22178 - SUBARRAY1_NSTRINGS_DEFAULT = 13435 - SUBARRAY1_MODULES_PER_STRING_DEFAULT = 12 - INVERTER_COUNT_DEFAULT = 99 - CEC_V_MP_REF_DEFAULT = 54.7 - CEC_I_MP_REF_DEFAULT = 5.67 - INV_SNL_PACO_DEFAULT = 753200 - DC_AC_RATIO_DEFAULT = 0.67057 - - pvsamv1_defaults_file = Path(__file__).absolute().parent.parent / "hybrid/pvsamv1_basic_params.json" - with open(pvsamv1_defaults_file, 'r') as f: - tech_config = json.load(f) - - # Verify the values in the pvsamv1_basic_params.json config file are as expected - assert tech_config['system_capacity'] == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) - assert tech_config['subarray1_nstrings'] == SUBARRAY1_NSTRINGS_DEFAULT - assert tech_config['subarray1_modules_per_string'] == SUBARRAY1_MODULES_PER_STRING_DEFAULT - assert tech_config['inverter_count'] == INVERTER_COUNT_DEFAULT - assert tech_config['cec_v_mp_ref'] == approx(CEC_V_MP_REF_DEFAULT, 1e-3) - assert tech_config['cec_i_mp_ref'] == approx(CEC_I_MP_REF_DEFAULT, 1e-3) - assert tech_config['inv_snl_paco'] == approx(INV_SNL_PACO_DEFAULT, 1e-3) - - # Create a detailed PV plant with the pvsamv1_basic_params.json config file - detailed_pvplant = DetailedPVPlant( - site=site, - pv_config={ - 'tech_config': tech_config, - } - ) - - # Verify that the detailed PV plant has the same values as in the config file - def verify_defaults(): - assert detailed_pvplant.value('system_capacity') == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) - assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT - assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT - assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT - assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) - assert detailed_pvplant.dc_ac_ratio == approx(DC_AC_RATIO_DEFAULT, 1e-3) - verify_defaults() - - # Modify system capacity and check that values update correctly - detailed_pvplant.value('system_capacity', 20000) - assert detailed_pvplant.value('system_capacity') == approx(20000.889, 1e-6) - assert detailed_pvplant.value('subarray1_nstrings') == 5374 - assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT - assert detailed_pvplant.value('inverter_count') == 40 - assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) - # The dc_ac_ratio changes because the inverter_count is a function of the system capacity, and it is rounded to an integer. - # Changes to the inverter count do not influence the system capacity, therefore the dc_ac_ratio does not adjust back to the original value - assert detailed_pvplant.dc_ac_ratio == approx(0.6639, 1e-3) - # Reset system capacity back to the default value to verify values update correctly - detailed_pvplant.value('system_capacity', SYSTEM_CAPACITY_DEFAULT) - # The dc_ac_ratio is not noticeably affected because the inverter_count, calculated from the prior dc_ac_ratio, barely changed when rounded - assert detailed_pvplant.dc_ac_ratio == approx(0.6639, 1e-3) - assert detailed_pvplant.value('system_capacity') == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) - assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT - assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT - # The inverter count did not change back to the default value because the dc_ac_ratio did not change back to the default value, - # and unlike the UI, there is no 'desired' dc_ac_ratio that is used to calculate the inverter count, only the prior dc_ac_ratio - assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + 1 - assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) - assert detailed_pvplant.dc_ac_ratio == approx(0.664, 1e-3) - - # Reinstantiate (reset) the detailed PV plant - detailed_pvplant = DetailedPVPlant( - site=site, - pv_config={ - 'tech_config': tech_config, - } - ) - - # Modify the number of strings and verify that values update correctly - detailed_pvplant.value('subarray1_nstrings', 10000) - assert detailed_pvplant.value('system_capacity') == approx(37217.88, 1e-3) - assert detailed_pvplant.value('subarray1_nstrings') == 10000 - assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT - assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT - assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) - assert detailed_pvplant.dc_ac_ratio == approx(0.499, 1e-3) - # Reset the number of strings back to the default value to verify other values reset back to their defaults - detailed_pvplant.value('subarray1_nstrings', SUBARRAY1_NSTRINGS_DEFAULT) - verify_defaults() - - # Reinstantiate (reset) the detailed PV plant - detailed_pvplant = DetailedPVPlant( - site=site, - pv_config={ - 'tech_config': tech_config, - } - ) - - # Modify the modules per string and verify that values update correctly - detailed_pvplant.value('subarray1_modules_per_string', 10) - assert detailed_pvplant.value('system_capacity') == approx(41668.52, 1e-3) - assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT - assert detailed_pvplant.value('subarray1_modules_per_string') == 10 - assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT - assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) - assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) - assert detailed_pvplant.dc_ac_ratio == approx(0.559, 1e-3) - # Reset the modules per string back to the default value to verify other values reset back to their defaults - detailed_pvplant.value('subarray1_modules_per_string', SUBARRAY1_MODULES_PER_STRING_DEFAULT) - verify_defaults() - - # TODO: change the module and inverter - pass - def test_detailed_pv(site): # Run detailed PV model (pvsamv1) using a custom financial model annual_energy_expected = 108239401 diff --git a/tests/hybrid/test_layout.py b/tests/hybrid/test_layout.py index ba184e07a..4f50dda08 100644 --- a/tests/hybrid/test_layout.py +++ b/tests/hybrid/test_layout.py @@ -1,8 +1,10 @@ import pytest +from pytest import approx from pathlib import Path from timeit import default_timer import numpy as np import os +import json import matplotlib.pyplot as plt from shapely import affinity from shapely.ops import unary_union @@ -14,6 +16,7 @@ from hybrid.layout.hybrid_layout import HybridLayout, WindBoundaryGridParameters, PVGridParameters, get_flicker_loss_multiplier from hybrid.layout.wind_layout_tools import create_grid from hybrid.layout.pv_design_utils import size_electrical_parameters, find_modules_per_string +from hybrid.detailed_pv_plant import DetailedPVPlant @pytest.fixture @@ -278,3 +281,211 @@ def test_system_electrical_sizing(site): target_relative_string_voltage=0.5, ) assert modules_per_string == 12 + + +def test_detailed_pv_properties(site): + SYSTEM_CAPACITY_DEFAULT = 50002.22178 + SUBARRAY1_NSTRINGS_DEFAULT = 13435 + SUBARRAY1_MODULES_PER_STRING_DEFAULT = 12 + INVERTER_COUNT_DEFAULT = 99 + CEC_V_MP_REF_DEFAULT = 54.7 + CEC_I_MP_REF_DEFAULT = 5.67 + INV_SNL_PACO_DEFAULT = 753200 + DC_AC_RATIO_DEFAULT = 0.67057 + + pvsamv1_defaults_file = Path(__file__).absolute().parent.parent / "hybrid/pvsamv1_basic_params.json" + with open(pvsamv1_defaults_file, 'r') as f: + tech_config = json.load(f) + + # Verify the values in the pvsamv1_basic_params.json config file are as expected + assert tech_config['system_capacity'] == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) + assert tech_config['subarray1_nstrings'] == SUBARRAY1_NSTRINGS_DEFAULT + assert tech_config['subarray1_modules_per_string'] == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert tech_config['inverter_count'] == INVERTER_COUNT_DEFAULT + assert tech_config['cec_v_mp_ref'] == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert tech_config['cec_i_mp_ref'] == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert tech_config['inv_snl_paco'] == approx(INV_SNL_PACO_DEFAULT, 1e-3) + + # Create a detailed PV plant with the pvsamv1_basic_params.json config file + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + # Verify that the detailed PV plant has the same values as in the config file + def verify_defaults(): + assert detailed_pvplant.value('system_capacity') == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(DC_AC_RATIO_DEFAULT, 1e-3) + verify_defaults() + + # Modify system capacity and check that values update correctly + detailed_pvplant.value('system_capacity', 20000) + assert detailed_pvplant.value('system_capacity') == approx(20000.889, 1e-6) + assert detailed_pvplant.value('subarray1_nstrings') == 5374 + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert detailed_pvplant.value('inverter_count') == 40 + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + # The dc_ac_ratio changes because the inverter_count is a function of the system capacity, and it is rounded to an integer. + # Changes to the inverter count do not influence the system capacity, therefore the dc_ac_ratio does not adjust back to the original value + assert detailed_pvplant.dc_ac_ratio == approx(0.6639, 1e-3) + # Reset system capacity back to the default value to verify values update correctly + detailed_pvplant.value('system_capacity', SYSTEM_CAPACITY_DEFAULT) + # The dc_ac_ratio is not noticeably affected because the inverter_count, calculated from the prior dc_ac_ratio, barely changed when rounded + assert detailed_pvplant.dc_ac_ratio == approx(0.6639, 1e-3) + assert detailed_pvplant.value('system_capacity') == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + # The inverter count did not change back to the default value because the dc_ac_ratio did not change back to the default value, + # and unlike the UI, there is no 'desired' dc_ac_ratio that is used to calculate the inverter count, only the prior dc_ac_ratio + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + 1 + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(0.664, 1e-3) + + # Reinstantiate (reset) the detailed PV plant + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + # Modify the number of strings and verify that values update correctly + detailed_pvplant.value('subarray1_nstrings', 10000) + assert detailed_pvplant.value('system_capacity') == approx(37217.88, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == 10000 + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(0.499, 1e-3) + # Reset the number of strings back to the default value to verify other values reset back to their defaults + detailed_pvplant.value('subarray1_nstrings', SUBARRAY1_NSTRINGS_DEFAULT) + verify_defaults() + + # Reinstantiate (reset) the detailed PV plant + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + # Modify the modules per string and verify that values update correctly + detailed_pvplant.value('subarray1_modules_per_string', 10) + assert detailed_pvplant.value('system_capacity') == approx(41668.52, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT + assert detailed_pvplant.value('subarray1_modules_per_string') == 10 + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(0.559, 1e-3) + # Reset the modules per string back to the default value to verify other values reset back to their defaults + detailed_pvplant.value('subarray1_modules_per_string', SUBARRAY1_MODULES_PER_STRING_DEFAULT) + verify_defaults() + + # Reinstantiate (reset) the detailed PV plant + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + # Change the PV module and verify that values update correctly + # (SunPower PL-SUNP-SPR-215) + default_pv_module = detailed_pvplant.get_pv_module(only_ref_vals=False) + module_params = { + 'module_model': 1, # cec + 'module_aspect_ratio': 1.95363, + 'cec_area': 1.244, + 'cec_a_ref': 1.87559, + 'cec_adjust': 13.0949, + 'cec_alpha_sc': 0.0020822, + 'cec_beta_oc': -0.134854, + 'cec_gamma_r': -0.3904, + 'cec_i_l_ref': 5.81, + 'cec_i_mp_ref': 5.4, + 'cec_i_o_ref': 3.698e-11, + 'cec_i_sc_ref': 5.8, + 'cec_n_s': 72, + 'cec_r_s': 0.514452, + 'cec_r_sh_ref': 298.34, + 'cec_t_noct': 44.7, + 'cec_v_mp_ref': 39.8, + 'cec_v_oc_ref': 48.3, + 'cec_temp_corr_mode': 0, + 'cec_is_bifacial': 0, + 'cec_bifacial_transmission_factor': 0, + 'cec_bifaciality': 0, + 'cec_bifacial_ground_clearance_height': 0, + 'cec_standoff': 6, + 'cec_height': 0, + 'cec_transient_thermal_model_unit_mass': 0, + } + detailed_pvplant.set_pv_module(module_params) + assert detailed_pvplant.value('system_capacity') == approx(34649.402, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + assert detailed_pvplant.value('cec_v_mp_ref') == approx(module_params['cec_v_mp_ref'], 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(module_params['cec_i_mp_ref'], 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(INV_SNL_PACO_DEFAULT, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(0.465, 1e-3) + # Reset the PV module back to the default module to verify other values reset back to their defaults + detailed_pvplant.set_pv_module(default_pv_module) + verify_defaults() + + # Reinstantiate (reset) the detailed PV plant + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + # Change the inverter and verify that values update correctly + # (Yaskawa Solectria Solar: SGI 500XTM) + default_inverter = detailed_pvplant.get_inverter(only_ref_vals=False) + inverter_params = { + 'inverter_model': 0, # cec + 'mppt_low_inverter': 545, + 'mppt_hi_inverter': 820, + 'inv_num_mppt': 1, + 'inv_tdc_cec_db': ((1300, 50, -0.02, 53, -0.47),), + 'inv_snl_c0': -1.81149e-8, + 'inv_snl_c1': 1.11794e-5, + 'inv_snl_c2': 0.000884631, + 'inv_snl_c3': -0.000339117, + 'inv_snl_paco': 507000, + 'inv_snl_pdco': 522637, + 'inv_snl_pnt': 88.78, + 'inv_snl_pso': 2725.47, + 'inv_snl_vdco': 615, + 'inv_snl_vdcmax': 820, + } + detailed_pvplant.set_inverter(inverter_params) + assert detailed_pvplant.value('system_capacity') == approx(SYSTEM_CAPACITY_DEFAULT, 1e-3) + assert detailed_pvplant.value('subarray1_nstrings') == SUBARRAY1_NSTRINGS_DEFAULT + assert detailed_pvplant.value('subarray1_modules_per_string') == SUBARRAY1_MODULES_PER_STRING_DEFAULT + assert detailed_pvplant.value('inverter_count') == INVERTER_COUNT_DEFAULT + assert detailed_pvplant.value('cec_v_mp_ref') == approx(CEC_V_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('cec_i_mp_ref') == approx(CEC_I_MP_REF_DEFAULT, 1e-3) + assert detailed_pvplant.value('inv_snl_paco') == approx(507000, 1e-3) + assert detailed_pvplant.dc_ac_ratio == approx(0.996, 1e-3) + # Reset the inverter back to the default inverter to verify other values reset back to their defaults + detailed_pvplant.set_inverter(default_inverter) + verify_defaults() From a6275ccdf2b333ca84bf607a0bddfeb543e2b60f Mon Sep 17 00:00:00 2001 From: dguittet Date: Wed, 17 May 2023 14:14:39 -0600 Subject: [PATCH 6/9] add system capacity check --- tests/hybrid/test_hybrid.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/hybrid/test_hybrid.py b/tests/hybrid/test_hybrid.py index 84782c105..394e09221 100644 --- a/tests/hybrid/test_hybrid.py +++ b/tests/hybrid/test_hybrid.py @@ -120,6 +120,7 @@ def test_hybrid_detailed_pv_only(site): annual_energy_expected = 112401677 solar_only = deepcopy(technologies['pv']) pv_plant = DetailedPVPlant(site=site, pv_config=solar_only) + assert pv_plant.system_capacity_kw == solar_only['system_capacity_kw'] pv_plant.simulate_power(1, False) assert pv_plant._system_model.Outputs.annual_energy == approx(annual_energy_expected, 1e-2) assert pv_plant._system_model.Outputs.capacity_factor == approx(25.66, 1e-2) From 53fa22f7152a6a3a8e07aa828781818549943ed3 Mon Sep 17 00:00:00 2001 From: Matthew Boyd Date: Wed, 17 May 2023 21:16:17 -0600 Subject: [PATCH 7/9] Utilize system_capacity_kw parameter in detailed PV model --- hybrid/detailed_pv_plant.py | 4 ++++ tests/hybrid/test_hybrid.py | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/hybrid/detailed_pv_plant.py b/hybrid/detailed_pv_plant.py index a2e5e9c81..56c3409e4 100644 --- a/hybrid/detailed_pv_plant.py +++ b/hybrid/detailed_pv_plant.py @@ -57,6 +57,10 @@ def processed_assign(self, params): Assign attributes from dictionaries with additional processing to enforce coherence between attributes """ + if 'system_capacity_kw' in params.keys(): # aggregate into tech_config + if 'tech_config' not in params.keys(): + params['tech_config'] = {} + params['tech_config']['system_capacity'] = params['system_capacity_kw'] if 'tech_config' in params.keys(): config = params['tech_config'] diff --git a/tests/hybrid/test_hybrid.py b/tests/hybrid/test_hybrid.py index 394e09221..99b6de8cd 100644 --- a/tests/hybrid/test_hybrid.py +++ b/tests/hybrid/test_hybrid.py @@ -119,8 +119,9 @@ def test_hybrid_detailed_pv_only(site): # Run standalone detailed PV model (pvsamv1) using defaults annual_energy_expected = 112401677 solar_only = deepcopy(technologies['pv']) + solar_only.pop('system_capacity_kw') # use default system capacity instead pv_plant = DetailedPVPlant(site=site, pv_config=solar_only) - assert pv_plant.system_capacity_kw == solar_only['system_capacity_kw'] + assert pv_plant.system_capacity_kw == approx(50002.2, 1e-2) pv_plant.simulate_power(1, False) assert pv_plant._system_model.Outputs.annual_energy == approx(annual_energy_expected, 1e-2) assert pv_plant._system_model.Outputs.capacity_factor == approx(25.66, 1e-2) @@ -130,6 +131,7 @@ def test_hybrid_detailed_pv_only(site): solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) solar_only['pv']['use_pvwatts'] = False # specify detailed PV model but don't change any defaults solar_only['grid']['interconnect_kw'] = 150e3 + solar_only['pv'].pop('system_capacity_kw') # use default system capacity instead hybrid_plant = HybridSimulation(solar_only, site) hybrid_plant.layout.plot() hybrid_plant.ppa_price = (0.01, ) @@ -152,6 +154,7 @@ def test_hybrid_detailed_pv_only(site): solar_only['pv']['use_pvwatts'] = False # specify detailed PV model solar_only['pv']['tech_config'] = tech_config # specify parameters solar_only['grid']['interconnect_kw'] = 150e3 + solar_only['pv'].pop('system_capacity_kw') # use default system capacity instead hybrid_plant = HybridSimulation(solar_only, site) hybrid_plant.layout.plot() hybrid_plant.ppa_price = (0.01, ) @@ -195,6 +198,7 @@ def test_hybrid_detailed_pv_only(site): solar_only['pv']['use_pvwatts'] = False # specify detailed PV model solar_only['pv']['tech_config'] = tech_config # specify parameters solar_only['grid']['interconnect_kw'] = 150e3 + solar_only['pv'].pop('system_capacity_kw') # use default system capacity instead # autosize number of strings, number of inverters and adjust system capacity n_strings, n_combiners, n_inverters, calculated_system_capacity = size_electrical_parameters( @@ -245,7 +249,6 @@ def test_hybrid_user_instantiated(site): solar_only = { 'pv': { 'use_pvwatts': False, - 'system_capacity_kw': system_capacity_kw, 'layout_params': layout_params, }, 'grid': { @@ -269,7 +272,6 @@ def test_hybrid_user_instantiated(site): detailed_pvplant = DetailedPVPlant( site=site, pv_config={ - 'system_capacity_kw': system_capacity_kw, 'layout_params': layout_params, 'fin_model': Singleowner.default('FlatPlatePVSingleOwner'), } From b3d2b0e9b6b91eafef6473dad4cd7e6dfde418a2 Mon Sep 17 00:00:00 2001 From: Matthew Boyd Date: Thu, 18 May 2023 14:43:55 -0600 Subject: [PATCH 8/9] Add test for top level system_capacity_kw parameter use in detailed PV model --- tests/hybrid/test_hybrid.py | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/hybrid/test_hybrid.py b/tests/hybrid/test_hybrid.py index 99b6de8cd..bb9412a2c 100644 --- a/tests/hybrid/test_hybrid.py +++ b/tests/hybrid/test_hybrid.py @@ -115,6 +115,63 @@ def test_hybrid_pv_only(site): assert npvs.hybrid == approx(-5121293, 1e3) +def test_detailed_pv_system_capacity(site): + # Run detailed PV model (pvsamv1) using defaults except the top level system_capacity_kw parameter + annual_energy_expected = 11236853 + npv_expected = -2566581 + solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) # includes system_capacity_kw parameter + solar_only['pv']['use_pvwatts'] = False # specify detailed PV model but don't change any defaults + solar_only['grid']['interconnect_kw'] = 150e3 + hybrid_plant = HybridSimulation(solar_only, site) + assert hybrid_plant.pv.value('subarray1_nstrings') == 1343 + hybrid_plant.layout.plot() + hybrid_plant.ppa_price = (0.01, ) + hybrid_plant.pv.dc_degradation = [0] * 25 + hybrid_plant.simulate() + aeps = hybrid_plant.annual_energies + npvs = hybrid_plant.net_present_values + assert aeps.pv == approx(annual_energy_expected, 1e-3) + assert aeps.hybrid == approx(annual_energy_expected, 1e-3) + assert npvs.pv == approx(npv_expected, 1e-3) + assert npvs.hybrid == approx(npv_expected, 1e-3) + + # Run detailed PV model (pvsamv1) using parameters from file except the top level system_capacity_kw parameter + pvsamv1_defaults_file = Path(__file__).absolute().parent / "pvsamv1_basic_params.json" + with open(pvsamv1_defaults_file, 'r') as f: + tech_config = json.load(f) + solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) # includes system_capacity_kw parameter + solar_only['pv']['use_pvwatts'] = False # specify detailed PV model + solar_only['pv']['tech_config'] = tech_config # specify parameters + solar_only['grid']['interconnect_kw'] = 150e3 + with raises(Exception) as context: + hybrid_plant = HybridSimulation(solar_only, site) + assert "The specified system capacity of 5000 kW is more than 5% from the value calculated" in str(context.value) + + # Run detailed PV model (pvsamv1) using file parameters, minus the number of strings, and the top level system_capacity_kw parameter + annual_energy_expected = 8893309 + npv_expected = -2768562 + pvsamv1_defaults_file = Path(__file__).absolute().parent / "pvsamv1_basic_params.json" + with open(pvsamv1_defaults_file, 'r') as f: + tech_config = json.load(f) + tech_config.pop('subarray1_nstrings') + solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) # includes system_capacity_kw parameter + solar_only['pv']['use_pvwatts'] = False # specify detailed PV model + solar_only['pv']['tech_config'] = tech_config # specify parameters + solar_only['grid']['interconnect_kw'] = 150e3 + hybrid_plant = HybridSimulation(solar_only, site) + assert hybrid_plant.pv.value('subarray1_nstrings') == 1343 + hybrid_plant.layout.plot() + hybrid_plant.ppa_price = (0.01, ) + hybrid_plant.pv.dc_degradation = [0] * 25 + hybrid_plant.simulate() + aeps = hybrid_plant.annual_energies + npvs = hybrid_plant.net_present_values + assert aeps.pv == approx(annual_energy_expected, 1e-3) + assert aeps.hybrid == approx(annual_energy_expected, 1e-3) + assert npvs.pv == approx(npv_expected, 1e-3) + assert npvs.hybrid == approx(npv_expected, 1e-3) + + def test_hybrid_detailed_pv_only(site): # Run standalone detailed PV model (pvsamv1) using defaults annual_energy_expected = 112401677 From 0ad58a057313485ec27c6048a637d09b48408137 Mon Sep 17 00:00:00 2001 From: dguittet Date: Thu, 29 Jun 2023 14:52:55 -0600 Subject: [PATCH 9/9] add additional tests --- hybrid/detailed_pv_plant.py | 22 ++++++++ hybrid/layout/pv_design_utils.py | 16 +++--- tests/hybrid/test_layout.py | 87 ++++++++++++++++++++++++++++++++ 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/hybrid/detailed_pv_plant.py b/hybrid/detailed_pv_plant.py index 56c3409e4..ef43cd98f 100644 --- a/hybrid/detailed_pv_plant.py +++ b/hybrid/detailed_pv_plant.py @@ -241,6 +241,28 @@ def dc_degradation(self, dc_deg_per_year: Sequence): @property def dc_ac_ratio(self) -> float: return self.system_capacity / (self.n_inverters * self.inverter_power) + + @dc_ac_ratio.setter + def dc_ac_ratio(self, target_dc_ac_ratio: float): + """ + Sets the dc to ac ratio while keeping the existing system capacity, by adjusting the modules per string and number of inverters + """ + n_strings, system_capacity, n_inverters = align_from_capacity( + system_capacity_target=self.system_capacity_kw, + dc_ac_ratio=target_dc_ac_ratio, + modules_per_string=self.modules_per_string, + module_power=self.module_power, + inverter_power=self.inverter_power, + ) + self._system_model.value('system_capacity', system_capacity) + self._system_model.value('subarray1_nstrings', n_strings) + self._system_model.value('subarray2_nstrings', 0) + self._system_model.value('subarray3_nstrings', 0) + self._system_model.value('subarray4_nstrings', 0) + self._system_model.value('subarray2_enable', 0) + self._system_model.value('subarray3_enable', 0) + self._system_model.value('subarray4_enable', 0) + self._system_model.value('inverter_count', n_inverters) @property def module_power(self) -> float: diff --git a/hybrid/layout/pv_design_utils.py b/hybrid/layout/pv_design_utils.py index d345bfe07..c11a1dd35 100644 --- a/hybrid/layout/pv_design_utils.py +++ b/hybrid/layout/pv_design_utils.py @@ -1,5 +1,5 @@ import math -from typing import List +from typing import List, Optional import numpy as np import PySAM.Pvsamv1 as pv_detailed import hybrid.layout.pv_module as pv_module @@ -79,8 +79,8 @@ def size_electrical_parameters( modules_per_string: float, module_power: float, inverter_power: float, - n_inputs_inverter: float=50, - n_inputs_combiner: float=32, + n_inputs_inverter: Optional[float]=None, + n_inputs_combiner: Optional[float]=None, ): """ Calculates the number of strings, combiner boxes and inverters to best match target capacity and DC/AC ratio @@ -108,10 +108,12 @@ def size_electrical_parameters( inverter_power=inverter_power, ) - n_combiners = math.ceil(n_strings / n_inputs_combiner) - - # Ensure there are enough inverters for the number of combiner boxes - n_inverters = max(n_inverters, math.ceil(n_combiners / n_inputs_inverter)) + if n_inputs_combiner: + n_combiners = math.ceil(n_strings / n_inputs_combiner) + # Ensure there are enough inverters for the number of combiner boxes + n_inverters = max(n_inverters, math.ceil(n_combiners / n_inputs_inverter)) + else: + n_combiners = None # Verify sizing was close to the target size, otherwise error out calculated_system_capacity = verify_capacity_from_electrical_parameters( diff --git a/tests/hybrid/test_layout.py b/tests/hybrid/test_layout.py index 4f50dda08..a1184d3ca 100644 --- a/tests/hybrid/test_layout.py +++ b/tests/hybrid/test_layout.py @@ -489,3 +489,90 @@ def verify_defaults(): # Reset the inverter back to the default inverter to verify other values reset back to their defaults detailed_pvplant.set_inverter(default_inverter) verify_defaults() + + +def test_detailed_pv_plant_custom_design(site): + pvsamv1_defaults_file = Path(__file__).absolute().parent.parent / "hybrid/pvsamv1_basic_params.json" + with open(pvsamv1_defaults_file, 'r') as f: + tech_config = json.load(f) + + # Modify the inputs for a custom design + target_solar_kw = 3e5 + target_dc_ac_ratio = 1.34 + modules_per_string = 12 + module_power = tech_config['cec_v_mp_ref'] * tech_config['cec_i_mp_ref'] * 1e-3 # [kW] + inverter_power = tech_config['inv_snl_paco'] * 1e-3 # [kW] + n_inputs_inverter = None + n_inputs_combiner = None + + n_strings, n_combiners, n_inverters, calculated_system_capacity = size_electrical_parameters( + target_system_capacity=target_solar_kw, + target_dc_ac_ratio=target_dc_ac_ratio, + modules_per_string=modules_per_string, + module_power=module_power, + inverter_power=inverter_power, + n_inputs_inverter=n_inputs_inverter, + n_inputs_combiner=n_inputs_combiner, + ) + + tech_config['system_capacity'] = calculated_system_capacity + tech_config['subarray1_nstrings'] = n_strings + tech_config['subarray1_modules_per_string'] = modules_per_string + tech_config['n_inverters'] = n_inverters + + # Create a detailed PV plant with the pvsamv1_basic_params.json config file + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + assert detailed_pvplant.system_capacity == pytest.approx(calculated_system_capacity, 1e-3) + assert detailed_pvplant.dc_ac_ratio == pytest.approx(1.341, 1e-3) + + detailed_pvplant.simulate(target_solar_kw) + + assert detailed_pvplant._system_model.Outputs.annual_ac_inv_clip_loss_percent < 1.2 + assert detailed_pvplant._system_model.Outputs.annual_ac_inv_eff_loss_percent < 3 + assert detailed_pvplant._system_model.Outputs.annual_ac_gross / detailed_pvplant._system_model.Outputs.annual_dc_gross > 0.91 + + +def test_detailed_pv_plant_modify_after_init(site): + pvsamv1_defaults_file = Path(__file__).absolute().parent.parent / "hybrid/pvsamv1_basic_params.json" + with open(pvsamv1_defaults_file, 'r') as f: + tech_config = json.load(f) + + # Create a detailed PV plant with the pvsamv1_basic_params.json config file + detailed_pvplant = DetailedPVPlant( + site=site, + pv_config={ + 'tech_config': tech_config, + } + ) + + assert detailed_pvplant.system_capacity == pytest.approx(tech_config['system_capacity'], 1e-3) + assert detailed_pvplant.dc_ac_ratio == pytest.approx(0.671, 1e-3) + + detailed_pvplant.simulate(5e5) + + assert detailed_pvplant._system_model.Outputs.annual_ac_inv_clip_loss_percent < 1.2 + assert detailed_pvplant._system_model.Outputs.annual_ac_inv_eff_loss_percent < 3 + assert detailed_pvplant._system_model.Outputs.annual_ac_gross / detailed_pvplant._system_model.Outputs.annual_dc_gross > 0.91 + assert detailed_pvplant.annual_energy_kwh * 1e-6 == pytest.approx(108.239, abs=10) + + # modify dc ac ratio + detailed_pvplant.dc_ac_ratio = 1.341 + detailed_pvplant.simulate(5e5) + assert detailed_pvplant._system_model.Outputs.annual_ac_inv_clip_loss_percent < 1.2 + assert detailed_pvplant._system_model.Outputs.annual_ac_inv_eff_loss_percent < 3 + assert detailed_pvplant._system_model.Outputs.annual_ac_gross / detailed_pvplant._system_model.Outputs.annual_dc_gross > 0.91 + assert detailed_pvplant.annual_energy_kwh * 1e-6 == pytest.approx(107.502, abs=10) + + # modify system capacity + detailed_pvplant.system_capacity_kw *= 2 + detailed_pvplant.simulate(5e5) + assert detailed_pvplant._system_model.Outputs.annual_ac_inv_clip_loss_percent < 1.2 + assert detailed_pvplant._system_model.Outputs.annual_ac_inv_eff_loss_percent < 3 + assert detailed_pvplant._system_model.Outputs.annual_ac_gross / detailed_pvplant._system_model.Outputs.annual_dc_gross > 0.91 + assert detailed_pvplant.annual_energy_kwh * 1e-6 == pytest.approx(215.0, abs=10)