Source code for pvrpm.core.case

import json
import yaml
import os
from glob import glob
from typing import Optional, Union, Any

import PySAM.PySSC as pssc
import numpy as np

from pvrpm.core.logger import logger
from pvrpm.core.exceptions import CaseError
from pvrpm.core.utils import filename_to_module, summarize_dc_energy, component_degradation
from pvrpm.core.enums import ConfigKeys as ck


[docs]class SamCase: """ SAM Case loader, verifier, and simulation """ def __init__(self, sam_json_dir: str, config: str, num_realizations: int = 0, results_folder: str = None): """""" self.ssc = pssc.PySSC() self.config = self.__load_config(config, type="yaml") self.sam_json_dir = sam_json_dir self.daily_tracker_coeffs = None self.modules = self.__load_modules() # will be calculated after base case simulation self.daylight_hours = None self.annual_daylight_hours = None self.base_lcoe = None self.base_npv = None self.base_ac_energy = None self.base_annual_energy = None self.base_dc_energy = None self.base_load = None self.base_tax_cash_flow = None self.base_losses = {} if not self.config: raise CaseError("There are errors in the configuration files, see logs.") if not self.modules: raise CaseError( "Could not load PySAM modules, please make sure you did not modify the case JSON file names after generation and that the case filepath is correct." ) # override results folder and number of realizations if num_realizations >= 2: self.config[ck.NUM_REALIZATION] = num_realizations if results_folder is not None: self.config[ck.RESULTS_FOLDER] = results_folder # lookup table for module order: self.module_orders = [ ["Pvsamv1", "Grid", "Utilityrate5", "Cashloan"], ["Pvsamv1", "Grid", "Utilityrate5", "Merchantplant"], ["Pvsamv1", "Grid", "Utilityrate5", "Levpartflip"], ["Pvsamv1", "Grid", "Utilityrate5", "Equpartflip"], ["Belpe", "Pvsamv1", "Grid", "Utilityrate5", "Cashloan"], ["Pvsamv1", "Grid", "Utilityrate5", "Saleleaseback"], ["Pvsamv1", "Grid", "Utilityrate5", "Singleowner"], ["Pvsamv1", "Grid", "Utilityrate5", "HostDeveloper"], ] # lookup table for models that pvrpm cannot use self.bad_module_orders = [ ["Pvsamv1", "Grid"], ["Pvsamv1", "Grid", "Lcoefcr"], ["Belpe", "Pvsamv1", "Grid", "Utilityrate5", "Thirdpartyownership"], ] self.__verify_case() self.__verify_config() @staticmethod def __load_config(path: str, type: str = "yaml") -> dict: """ Loads a configuration from a YAML or JSON file and returns the dictionary. Args: path (str): String path to the file type (str): One of `yaml` or `json`, specifies the file to load Returns: :obj:`dict`: the data loaded from the file """ try: with open(path, "r") as f: if type.lower().strip() == "yaml" or type.lower().strip() == "yml": config = yaml.full_load(f) elif type.lower().strip() == "json": config = json.load(f) except json.decoder.JSONDecodeError as e: logger.error(f"Theres an error reading the JSON configuration file: {e}") return None except yaml.scanner.ScannerError as e: logger.error(f"Theres an error reading the YAML configuration file: {e}") return None except yaml.parser.ParserError as e: logger.error(f"Theres an error reading the YAML configuration file: {e}") return None except FileNotFoundError: logger.error(f"The configuration file at {path} couldn't be found!") return None return config def __load_modules(self) -> dict: """ Loads the case modules and initalizes them with parameters define in the json Returns: :obj:`dict`: A dictionary containing the loaded and configured modules """ first_module = None modules = {} for path in glob(os.path.join(self.sam_json_dir, "*.json")): module_name = os.path.basename(path) try: module = filename_to_module(module_name) except AttributeError: raise CaseError(f"Couldn't find module for file {module_name}!") if not module: raise CaseError(f"Couldn't find module for file {module_name}!") module_name = module.__name__.replace("PySAM.", "") if not first_module: first_module = module.new() module = first_module else: module = module.from_existing(first_module) case_config = self.__load_config(path, type="json") for k, v in case_config.items(): if k != "number_inputs": try: module.value(k, v) except AttributeError: logger.warning( f"Unknown key in module {module_name}: {k}\n Most likely you used the wrong SAM version to generate the JSONs for the case!" ) modules[module_name] = module return modules def __verify_config(self) -> None: """ Verifies loaded YAML configuration file. """ # helper function to check distribution parameters def check_params(component: str, name: str, config: dict): if ck.MEAN not in config[ck.PARAM]: raise CaseError(f"Mean parameter for {name} is missing!") if config[ck.DIST] == ck.WEIBULL: if ck.STD not in config[ck.PARAM] and ck.SHAPE not in config[ck.PARAM]: raise CaseError(f"STD or SHAPE parameter for {name} is missing!") elif config[ck.DIST] != ck.EXPON and ck.STD not in config[ck.PARAM]: raise CaseError(f"STD parameter for {name} is missing!") # tracking check if self.config[ck.TRACKING] and not self.config.get(ck.TRACKER, False): raise CaseError( "Tracking modules loaded in SAM config but no trackers defined in the PVRPM YML configuration, please make sure it is setup!" ) if not self.config[ck.TRACKING] and self.config.get(ck.TRACKER, False): logger.warning( "No tracking modules loaded for the SAM case, however there is a tracking configuration in the PVRPM configuration. This configuration will be ignored and no simulation with trackers will be performed." ) top_level_keys = set(ck.needed_keys) included_keys = set(self.config.keys()) & top_level_keys if included_keys != top_level_keys: raise CaseError( f"Missing required configuration options in the PVRPM configuration: {top_level_keys - included_keys}" ) if self.config[ck.NUM_REALIZATION] < 2: raise CaseError("Number of realizations must be greater than or equal to 2!") if self.config[ck.TRACKING] and self.config[ck.NUM_TRACKERS] <= 0: raise CaseError("If tracking is defined the number of trackers must be greater than 0!") if self.config[ck.NUM_COMBINERS] <= 0: raise CaseError("Number of combiners must be greater than 0!") # static monitoring if self.config.get(ck.INDEP_MONITOR, None): needed_keys = set(ck.indep_monitor_keys) for name, monitor_config in self.config[ck.INDEP_MONITOR].items(): included_keys = set(monitor_config.keys()) & needed_keys unknown_keys = ( set(monitor_config.keys()) - needed_keys - {ck.INTERVAL, ck.FAIL_THRESH, ck.DIST, ck.PARAM, ck.FAIL_PER_THRESH} ) if included_keys != needed_keys: raise CaseError(f"Independent monitoring for {name} is missing keys {needed_keys - included_keys}") if ck.INTERVAL not in monitor_config and ( ck.FAIL_THRESH not in monitor_config and ck.FAIL_PER_THRESH not in monitor_config ): raise CaseError( f"Independent monitoring for {name} is missing the interval and/or global_threshold/failure_per_threshold" ) if ck.DIST in monitor_config: if not ck.PARAM in monitor_config: raise CaseError( f"Independent monitoring for {name} is missing the parameters for distribution." ) if monitor_config[ck.DIST] in ck.dists: check_params("", name, monitor_config) if unknown_keys: logger.warning(f"Unknown keys in independent monitoring configuration: {unknown_keys}") for level in monitor_config[ck.LEVELS]: if ck.INDEP_MONITOR not in self.config[level]: self.config[level][ck.INDEP_MONITOR] = {} if ck.INTERVAL in monitor_config: self.config[level][ck.INDEP_MONITOR][name] = monitor_config[ck.INTERVAL] else: self.config[level][ck.INDEP_MONITOR][name] = None # cross level monitoring and compounding if self.config.get(ck.COMP_MONITOR, None): # parse levels in order from lowest -> highest to maintain priority on monitoring for component in ck.compound_levels: if not self.config[ck.COMP_MONITOR].get(component, None): continue monitor_component_data = self.config[ck.COMP_MONITOR][component] for monitor_component, monitor_config in monitor_component_data.items(): needed_keys = set(ck.compound_keys) included_keys = set(monitor_config.keys()) & needed_keys unknown_keys = set(monitor_config.keys()) - needed_keys - {ck.FAIL_PER_THRESH} - {ck.FAIL_THRESH} if included_keys != needed_keys: raise CaseError( f"Cross component monitoring under component {component}:{monitor_component} is missing keys {needed_keys - included_keys}" ) # if monitor_config[ck.COMP_FUNC] not in ck.compound_funcs: # raise CaseError( # f"Compound function for {component}:{monitor_component} is not a valid function!" # ) if unknown_keys: logger.warning(f"Unknown keys in cross level monitoring configuration: {unknown_keys}") if ( not monitor_config.get(ck.FAIL_THRESH, None) is not None and not monitor_config.get(ck.FAIL_PER_THRESH, None) is not None ): raise CaseError( f"You must specify at least {ck.FAIL_THRESH} or {ck.FAIL_PER_THRESH} for {component}:{monitor_component}" ) if monitor_config.get(ck.FAIL_THRESH, None) is not None and ( monitor_config[ck.FAIL_THRESH] < 0 or monitor_config[ck.FAIL_THRESH] > 1 ): raise CaseError( f"Global failure threshold for {component}:{monitor_component} must be between 0 and 1." ) if monitor_config.get(ck.FAIL_PER_THRESH, None) is not None and ( monitor_config[ck.FAIL_PER_THRESH] < 0 or monitor_config[ck.FAIL_PER_THRESH] > 1 ): raise CaseError( f"{ck.FAIL_PER_THRESH} for {component}:{monitor_component} must be between 0 and 1." ) if monitor_config[ck.DIST] in ck.dists: check_params(component, monitor_component, monitor_config) if not self.config[monitor_component].get(ck.COMP_MONITOR, None): # add what level is monitoring this component level for compounding later monitor_config[ck.LEVELS] = component self.config[monitor_component][ck.COMP_MONITOR] = monitor_config if self.config[ck.STR_PER_COMBINER] * self.config[ck.NUM_COMBINERS] != self.num_strings: logger.warning( "There is not an integer number of strings per combiner, this may cause slight inaccuracies if you are using failure per thresholding for component level monitoring. This shouldn't effect the output of the simulation noticeably, but it is better practice to have an integer number of strings per combiner." ) if self.config[ck.INVERTER_PER_TRANS] * self.config[ck.NUM_TRANSFORMERS] != self.num_inverters: logger.warning( "There is not an integer number of inverters per transformer, this may cause slight inaccuracies if you are using failure per thresholding for component level monitoring. This shouldn't effect the output of the simulation noticeably, but it is better practice to have an integer number of inverters per transformer." ) for component in ck.component_keys: if not self.config.get(component, None): # for non-needed components, needed ones checked already above continue missing = [] if not self.config[component].get(ck.NAME, None): missing.append(ck.NAME) if self.config[component].get(ck.CAN_FAIL, None) is None: missing.append(ck.CAN_FAIL) if self.config[component].get(ck.CAN_REPAIR, None) is None: missing.append(ck.CAN_REPAIR) if self.config[component].get(ck.CAN_MONITOR, None) is None: missing.append(ck.CAN_MONITOR) if missing: raise CaseError(f"Missing configurations for component '{component}': {missing}") if self.config[component][ck.CAN_FAIL] and not self.config[component].get(ck.FAILURE, None): missing.append(ck.FAILURE) if self.config[component][ck.CAN_REPAIR] and not self.config[component].get(ck.REPAIR, None): missing.append(ck.REPAIR) if self.config[component][ck.CAN_MONITOR] and not self.config[component].get(ck.MONITORING, None): missing.append(ck.MONITORING) if missing: raise CaseError(f"Missing configurations for component '{component}': {missing}") if self.config[component].get(ck.WARRANTY, None) and not self.config[component][ck.WARRANTY].get( ck.DAYS, None ): missing.append(ck.DAYS) # check the number of repairs / monitoring is either 1 or equal to number of failures if self.config[component][ck.CAN_FAIL]: # in case there are no failures for components that cant fail num_failure_modes = len(self.config[component].get(ck.FAILURE, {})) if self.config[component][ck.CAN_REPAIR]: num_repair_modes = len(self.config[component].get(ck.REPAIR, {})) if num_repair_modes != 1 and num_repair_modes != num_failure_modes: raise CaseError( f"Number of repairs for component '{component}' must be 1 or equal to the number of failures" ) if self.config[component][ck.CAN_MONITOR]: num_monitor_modes = len(self.config[component].get(ck.MONITORING, {})) if num_monitor_modes != 1 and num_monitor_modes != num_failure_modes: raise CaseError( f"Number of monitoring modes for component '{component}' must be 1 or equal to the number of failures" ) # check concurrent failures and repairs num_failure_modes = len(self.config[component].get(ck.PARTIAL_FAIL, {})) if self.config[component][ck.CAN_REPAIR]: num_repair_modes = len(self.config[component].get(ck.PARTIAL_REPAIR, {})) if num_repair_modes != 1 and num_repair_modes != num_failure_modes: raise CaseError( f"Number of concurrent repairs for component '{component}' must be 1 or equal to the number of concurrent failures" ) for failure, fail_config in self.config[component].get(ck.FAILURE, {}).items(): fails = set(ck.failure_keys) if component == ck.INVERTER: # inverters may have cost_per_watt specified instead of cost fails.discard(ck.COST) included = fails & set(fail_config.keys()) if included != fails: missing += list(fails - included) unknown_keys = set(fail_config.keys()) - fails - {ck.FRAC, ck.COST, ck.COST_PER_WATT, ck.DECAY_FRAC} if unknown_keys: logger.warning(f"Unknown keys in failure configuration {failure}: {unknown_keys}") keys = set(fail_config.keys()) if ck.FRAC in keys and ck.DECAY_FRAC in keys: raise CaseError(f"Must specify either `fraction` or `decay_fraction`, not both for '{component}'") # update cost for inverters if component == ck.INVERTER: if fail_config.get(ck.COST, None) is None and fail_config.get(ck.COST_PER_WATT, None) is None: missing.append(ck.COST) if fail_config.get(ck.COST_PER_WATT, None) is not None: # calculate costs based on cents/watt self.config[component][ck.FAILURE][failure][ck.COST] = ( fail_config[ck.COST_PER_WATT] * self.config[ck.INVERTER_SIZE] ) if fail_config.get(ck.DIST, None) in ck.dists: check_params(component, failure, fail_config) # partial failure check for failure, fail_config in self.config[component].get(ck.PARTIAL_FAIL, {}).items(): fails = set(ck.partial_failure_keys) if component == ck.INVERTER: # inverters may have cost_per_watt specified instead of cost fails.discard(ck.COST) included = fails & set(fail_config.keys()) if included != fails: missing += list(fails - included) unknown_keys = set(fail_config.keys()) - fails - {ck.FRAC, ck.COST, ck.COST_PER_WATT, ck.DECAY_FRAC} if unknown_keys: logger.warning(f"Unknown keys in concurrent failure configuration {failure}: {unknown_keys}") keys = set(fail_config.keys()) if ck.FRAC in keys and ck.DECAY_FRAC in keys: raise CaseError(f"Must specify either `fraction` or `decay_fraction`, not both for '{component}'") # update cost for inverters if component == ck.INVERTER: if fail_config.get(ck.COST, None) is None and fail_config.get(ck.COST_PER_WATT, None) is None: missing.append(ck.COST) if fail_config.get(ck.COST_PER_WATT, None) is not None: # calculate costs based on cents/watt self.config[component][ck.PARTIAL_FAIL][failure][ck.COST] = ( fail_config[ck.COST_PER_WATT] * self.config[ck.INVERTER_SIZE] ) if fail_config.get(ck.DIST, None) in ck.dists: check_params(component, failure, fail_config) for monitor, monitor_config in self.config[component].get(ck.MONITORING, {}).items(): monitor_ = set(ck.monitoring_keys) included = monitor_ & set(monitor_config.keys()) if included != monitor_: missing += list(monitor_ - included) unknown_keys = set(monitor_config.keys()) - monitor_ if unknown_keys: logger.warning(f"Unknown keys in monitoring configuration {monitor}: {unknown_keys}") if monitor_config.get(ck.DIST, None) in ck.dists: check_params(component, monitor, monitor_config) for repair, repair_config in self.config[component].get(ck.REPAIR, {}).items(): repairs_ = set(ck.repair_keys) included = repairs_ & set(repair_config.keys()) if included != repairs_: missing += list(repairs_ - included) unknown_keys = set(repair_config.keys()) - repairs_ if unknown_keys: logger.warning(f"Unknown keys in repair configuration {repair}: {unknown_keys}") if repair_config.get(ck.DIST, None) in ck.dists: check_params(component, repair, repair_config) # partial repairs for repair, repair_config in self.config[component].get(ck.PARTIAL_REPAIR, {}).items(): repairs_ = set(ck.partial_repair_keys) included = repairs_ & set(repair_config.keys()) if included != repairs_: missing += list(repairs_ - included) unknown_keys = set(repair_config.keys()) - repairs_ if unknown_keys: logger.warning(f"Unknown keys in concurrent repair configuration {repair}: {unknown_keys}") if repair_config.get(ck.DIST, None) in ck.dists: check_params(component, repair, repair_config) if missing: raise CaseError(f"Missing configurations for component '{component}': {missing}") # add the number of each component to its configuration information if component == ck.MODULE: self.config[component][ck.NUM_COMPONENT] = int(self.num_modules) elif component == ck.STRING: self.config[component][ck.NUM_COMPONENT] = int(self.num_strings) elif component == ck.COMBINER: self.config[component][ck.NUM_COMPONENT] = self.config[ck.NUM_COMBINERS] elif component == ck.INVERTER: self.config[component][ck.NUM_COMPONENT] = int(self.num_inverters) elif component == ck.DISCONNECT: self.config[component][ck.NUM_COMPONENT] = int(self.num_disconnects) elif component == ck.TRANSFORMER: self.config[component][ck.NUM_COMPONENT] = self.config[ck.NUM_TRANSFORMERS] elif component == ck.GRID: self.config[component][ck.NUM_COMPONENT] = 1 elif component == ck.TRACKER: self.config[component][ck.NUM_COMPONENT] = self.config[ck.NUM_TRACKERS] # make directory for results if it doesnt exist os.makedirs(self.config[ck.RESULTS_FOLDER], exist_ok=True) if self.config[ck.TRACKING] and self.config[ck.TRACKER][ck.CAN_FAIL]: self.precalculate_tracker_losses() def __verify_case(self) -> None: """ Verifies loaded module configuration from SAM and also sets class variables for some information about the case. """ # since we are finding order simulation now, remove set order in config for old pvrpm config files self.config[ck.MODULE_ORDER] = None # setup module order for simulation, also # need to check that an LCOE calculator that supports lifetime is used # in this case its only 1 unusable calculator and if no calculator is present my_modules = list(self.modules.keys()) for module_loadout in self.module_orders: if len(module_loadout) != len(self.modules): continue found = True for module in module_loadout: if module not in my_modules: found = False break if found: self.config[ck.MODULE_ORDER] = module_loadout break if self.config[ck.MODULE_ORDER] is None: for module_loadout in self.bad_module_orders: if len(module_loadout) != len(self.modules): continue found = True for module in module_loadout: if module not in my_modules: found = False break if found: raise CaseError( "You have either selected the `LCOE Calculator (FCR Method)`, `Third Party Owner - Host` or `No Financial Model` for your financial model, which PVRPM does not support. Please select a supported financial model." ) raise CaseError( "You have selected an unknown financial model or are not using the `Detailed Photovoltaic Model`. Please update your model to a supported model." ) if self.value("en_dc_lifetime_losses") or self.value("en_ac_lifetime_losses"): logger.warning("Lifetime daily DC and AC losses will be overridden for this run.") if self.value("om_fixed") != [0]: logger.warning( "There is a non-zero value in the fixed annual O&M costs input. These will be overwritten with the new values." ) if self.value("dc_degradation") != [0]: logger.warning( "Degradation is set by the PVRPM script, you have entered a non-zero degradation to the degradation input. This script will set the degradation input to zero." ) self.value("dc_degradation", [0]) if ck.NUM_TRANSFORMERS not in self.config or self.config[ck.NUM_TRANSFORMERS] < 1: raise CaseError("Number of transformers must be greater than 0!") self.num_modules = 0 self.num_strings = 0 # assume the number of modules per string is the same for each subarray self.config[ck.MODULES_PER_STR] = int(self.value("subarray1_modules_per_string")) self.config[ck.TRACKING] = False self.config[ck.MULTI_SUBARRAY] = 0 for sub in range(1, 5): if sub == 1 or self.value(f"subarray{sub}_enable"): # subarry 1 is always enabled self.num_modules += self.value(f"subarray{sub}_modules_per_string") * self.value( f"subarray{sub}_nstrings" ) self.num_strings += self.value(f"subarray{sub}_nstrings") if self.value(f"subarray{sub}_track_mode"): self.config[ck.TRACKING] = True self.config[ck.MULTI_SUBARRAY] += 1 inverter = self.value("inverter_model") if inverter == 0: self.config[ck.INVERTER_SIZE] = self.value("inv_snl_paco") elif inverter == 1: self.config[ck.INVERTER_SIZE] = self.value("inv_ds_paco") elif inverter == 2: self.config[ck.INVERTER_SIZE] = self.value("inv_pd_paco") else: raise CaseError("Unknown inverter model! Should be 0, 1, or 2") # assume 1 AC disconnect per inverter self.num_inverters = self.value("inverter_count") self.num_disconnects = self.num_inverters self.config[ck.STR_PER_COMBINER] = int(np.floor(self.num_strings / self.config[ck.NUM_COMBINERS])) self.config[ck.COMBINER_PER_INVERTER] = int(np.floor(self.config[ck.NUM_COMBINERS] / self.num_inverters)) self.config[ck.INVERTER_PER_TRANS] = int(np.floor(self.num_inverters / self.config[ck.NUM_TRANSFORMERS])) self.config[ck.LIFETIME_YRS] = int(self.value("analysis_period")) # for pickling
[docs] def __getstate__(self) -> dict: """ Converts the case into a dictionary for pickling """ state = self.__dict__.copy() del state["modules"] del state["ssc"] return state
[docs] def __setstate__(self, state: dict) -> None: """ Creates the object from a dictionary """ self.__dict__ = state self.ssc = pssc.PySSC() self.modules = self.__load_modules()
[docs] def get_npv(self): """ Returns the NPV for the case after a simulation has been ran, regardless of financial model used. """ try: return self.output("npv") except AttributeError: pass try: return self.output("project_return_aftertax_npv") except AttributeError: pass try: return self.output("tax_investor_aftertax_npv") except AttributeError: return None
[docs] def precalculate_tracker_losses(self): """ Precalculate_tracker_losses calculates an array of coefficients (one for every day of the year) that account for the "benefit" of trackers on that particular day. This is used to determine how much power is lost if a tracker fails. """ user_analysis_period = self.value("analysis_period") self.value("analysis_period", 1) self.value("en_ac_lifetime_losses", 0) self.value("en_dc_lifetime_losses", 0) self.simulate() timeseries_with_tracker = self.output("dc_net") # calculate timeseries performance without trackers for one year user_tracking_mode = {} user_azimuth = {} user_tilt = {} for i in range(1, 5): if i == 1 or self.value(f"subarray{i}_enable"): user_tracking_mode[i] = self.value(f"subarray{i}_track_mode") user_azimuth[i] = self.value(f"subarray{i}_azimuth") user_tilt[i] = self.value(f"subarray{i}_tilt") self.value(f"subarray{i}_track_mode", 0) # fixed tilt for i, uaz in user_azimuth.items(): if uaz > 360 or uaz < 0: raise CaseError( f"Azimuth must be between 0 and 360. Please adjust the azimuth for subarray {i} and try again." ) for i, uaz in user_azimuth.items(): if self.config[ck.WORST_TRACKER]: # assume worst case tracker gets stuck to north. If axis is north-south, assume gets stuck to west. worst_case_az = uaz if uaz < 180: worst_case_az -= 90 else: worst_case_az += 90 if worst_case_az < 0: worst_case_az += 360 if worst_case_az >= 360: worst_case_az -= 360 self.value(f"subarray{i}_azimuth", worst_case_az) self.value(f"subarray{i}_tilt", self.value(f"subarray{i}_rotlim")) else: # assume average case is that tracker gets stuck flat self.value(f"subarray{i}_tilt", 0) self.simulate() timeseries_without_tracker = self.output("dc_net") # summarize it to daily energy sum_with_tracker = summarize_dc_energy(timeseries_with_tracker, 365) sum_without_tracker = summarize_dc_energy(timeseries_without_tracker, 365) # calculate daily loss statistics self.daily_tracker_coeffs = sum_without_tracker / sum_with_tracker self.value("analysis_period", user_analysis_period) for i in user_azimuth.keys(): self.value(f"subarray{i}_track_mode", user_tracking_mode[i]) self.value(f"subarray{i}_azimuth", user_azimuth[i]) self.value(f"subarray{i}_tilt", user_tilt[i])
[docs] def base_case_sim(self) -> None: """ Runs the base case simulation for this case, with no failures and optimal lifetime losses This also sets base case output parameters of this object """ lifetime = self.config[ck.LIFETIME_YRS] # run the dummy base case self.value("en_dc_lifetime_losses", 0) self.value("en_ac_lifetime_losses", 0) self.value("om_fixed", [0]) module_degradation_rate = self.config[ck.MODULE].get(ck.DEGRADE, 0) / 365 degrade = [(1 - component_degradation(module_degradation_rate, i)) * 100 for i in range(lifetime * 365)] self.value("en_dc_lifetime_losses", 1) self.value("dc_lifetime_losses", degrade) self.simulate() self.base_lcoe = (self.output("lcoe_real"), self.output("lcoe_nom")) self.base_npv = self.get_npv() # ac energy # remove the first element from cf_energy_net because it is always 0, representing year 0 self.base_annual_energy = self.output("cf_energy_net")[1:] self.base_ac_energy = self.value("gen") self.base_dc_energy = summarize_dc_energy(self.output("dc_net"), lifetime) # other outputs from base case (for graphing) try: self.base_load = self.value("load") except AttributeError: self.base_load = None try: self.base_tax_cash_flow = self.output("cf_after_tax_cash_flow") except AttributeError: self.base_tax_cash_flow = self.output("cf_pretax_cashflow") for loss in ck.losses: try: self.base_losses[loss] = self.output(loss) except: self.base_losses[loss] = 0 # calculate availability using sun hours # contains every hour in the year and whether is sun up, down, sunrise, sunset # for every year of simulation sunup = self.value("sunup") # 0 sun is down, 1 sun is up, 2 surnise, 3 sunset, we only considered sun up (1) sunup = np.reshape(np.array(sunup), (25, -1))[0] # determine the frequency of the data, same as frequncy of supplied weather file total = len(sunup) if total == 8760: freq = 1 else: freq = 0 while total > 8760: freq += 1 total /= freq # sometimes it gives half hourly or quater hourly data, so just pull out the hourly sunup = np.reshape(sunup[0::freq], (365, 24)) # zero out every value except where the value is 1 (for sunup) sunup = np.where(sunup == 1, sunup, 0) # sum up daylight hours for each day self.daylight_hours = np.sum(sunup, axis=1) self.annual_daylight_hours = np.sum(self.daylight_hours)
[docs] def simulate(self, verbose: int = 0) -> None: """ Executes simulations for all modules in this case. Args: verbose (int): 0 for no log messages, 1 for simulation log messages """ for m_name in self.config[ck.MODULE_ORDER]: self.modules[m_name].execute(verbose)
[docs] def value(self, name: str, value: Optional[Any] = None) -> Union[None, float, dict, list, str]: """ Get or set by string name a module value, without specifying the module the variable resides in. If there is no value provided, the value is returned for the variable name. This will search the module's data and update the variable if found. If the value is not found in all of the modules, an AttributeError is raised. Args: name (str): Name of the value value (Any, optional): Value to set variable to Returns: Value or the variable if value is None Note: Some modules have the same keys, this function will return the first key found in the module order specified in the configuration. Because of the way modules share data in PySAM, setting the value in the first module will propagate it to the other modules. """ for m_name in self.modules.keys(): try: if value: return self.modules[m_name].value(name, value) else: return self.modules[m_name].value(name) except: pass raise AttributeError(f"Variable {name} not found or incorrect value datatype in {list(self.modules.keys())}")
[docs] def output(self, name: str) -> Union[None, float, dict, list, str]: """ Get an output variable by string name, without specifying the module the variable resides in. This will search all of the module's outputs. If the value is not found in all of the modules, an AttributeError is raised. Args: name (str): Name of the output Returns: The value of the output variable """ for m_name in self.modules.keys(): try: return getattr(self.modules[m_name].Outputs, name) except AttributeError: pass # in case something else should be done except: # if this happens, value was found but was not set, which in PvSAM raises an exception # so, return None return None raise AttributeError(f"Output variable {name} not found in {list(self.modules.keys())}")