Source code for reV.config.sam_config

# -*- coding: utf-8 -*-
"""
reV configuration framework for SAM config inputs.
"""
import logging
import os
from warnings import warn

from gaps.config import load_config

from reV.utilities.exceptions import SAMInputError, SAMInputWarning
from reV.config.base_config import BaseConfig

logger = logging.getLogger(__name__)


[docs]class SAMConfig(BaseConfig): """Class to handle the SAM section of config input.""" def __init__(self, SAM_configs): """ Parameters ---------- SAM_configs : dict Keys are config ID's, values are filepaths to the SAM configs. """ super().__init__(SAM_configs, check_keys=False) self._clearsky = None self._bifacial = None self._icing = None self._inputs = None self._downscale = None self._time_index_step = None @property def clearsky(self): """Get a boolean for whether solar resource requires clearsky irrad. Returns ------- clearsky : bool Flag set in the SAM config input with key "clearsky" for solar analysis to process generation for clearsky irradiance. Defaults to False (normal all-sky irradiance). """ if self._clearsky is None: self._clearsky = False for v in self.inputs.values(): self._clearsky = any((self._clearsky, bool(v.get('clearsky', False)))) if self._clearsky: logger.debug('Solar analysis being performed on clearsky ' 'irradiance.') return self._clearsky @property def bifacial(self): """Get a boolean for whether bifacial solar analysis is being run. Returns ------- bifacial : bool Flag set in the SAM config input with key "bifaciality" for solar analysis to analyze bifacial PV panels. Will require albedo input. Defaults to False (no bifacial panels is default). """ if self._bifacial is None: self._bifacial = False for v in self.inputs.values(): bi_flags = ('bifaciality', 'spe_is_bifacial', 'cec_is_bifacial', '6par_is_bifacial') bi_bools = [bool(v.get(flag, 0)) for flag in bi_flags] self._bifacial = any(bi_bools + [self._bifacial]) return self._bifacial @property def icing(self): """Get a boolean for whether wind generation is considering icing. Returns ------- _icing : bool Flag for whether wind generation is considering icing effects. Based on whether SAM input json has "en_icing_cutoff" == 1. """ if self._icing is None: self._icing = False for v in self.inputs.values(): self._icing = any((self._icing, bool(v.get('en_icing_cutoff', False)))) if self._icing: logger.debug('Icing analysis active for wind gen.') return self._icing @property def time_index_step(self): """ Step size for time_index for SAM profile output resolution Returns ------- int | None Step size for time_index, used to reduce temporal resolution """ if self._time_index_step is None: time_index_step = [] for v in self.inputs.values(): time_index_step.append(v.get('time_index_step', None)) self._time_index_step = list(set(time_index_step)) if len(self._time_index_step) > 1: msg = ('Expecting a single unique value for "time_index_step" but ' 'received: {}'.format(self._time_index_step)) logger.error(msg) raise SAMInputError(msg) return self._time_index_step[0] @property def downscale(self): """ Resolution to downscale NSRDB resource to. Returns ------- dict | None Option for NSRDB resource downscaling to higher temporal resolution. The config expects a str entry in the Pandas frequency format, e.g. '5min' or a dict of downscaling kwargs such as {'frequency': '5min', 'variability_kwargs': {'var_frac': 0.05, 'distribution': 'uniform'}}. A str entry will be converted to a kwarg dict for the output of this property e.g. '5min' -> {'frequency': '5min'} """ if self._downscale is None: ds_list = [] for v in self.inputs.values(): ds_list.append(v.get('downscale', None)) self._downscale = ds_list[0] ds_list = list({str(x) for x in ds_list}) if len(ds_list) > 1: msg = ('Expecting a single unique value for "downscale" but ' 'received: {}'.format(ds_list)) logger.error(msg) raise SAMInputError(msg) if isinstance(self._downscale, str): self._downscale = {'frequency': self._downscale} return self._downscale @property def inputs(self): """Get the SAM input file(s) (JSON/JSON5/YAML/TOML) and return as a dictionary. Parameters ---------- _inputs : dict The keys of this dictionary are the "configuration ID's". The values are the imported json SAM input dictionaries. """ if self._inputs is None: self._inputs = {} for key, config in self.items(): # key is ID (i.e. sam_param_0) that matches project points json # fname is the actual SAM config file name (with path) if isinstance(config, str): if not os.path.exists(config): raise IOError('SAM config file does not exist: "{}"' .format(config)) else: config = load_config(config) if not isinstance(config, dict): raise RuntimeError('SAM config must be a file or a ' 'pre-extracted dictionary, but got: {}' .format(config)) SAMInputsChecker.check(config) self._inputs[key] = config return self._inputs
[docs]class SAMInputsChecker: """Class to check SAM input jsons and warn against bad inputs.""" # Keys that are used to identify a technology config KEYS_PV = ('tilt', 'azimuth', 'module_type', 'array_type') def __init__(self, config): """ Parameters ---------- config : dict Extracted SAM technology input config in dict form. """ if isinstance(config, dict): self._config = config else: raise TypeError('Bad SAM tech config type: {}' .format(type(config)))
[docs] def check_pv(self): """Run input checks for a pv input config.""" if self._config['array_type'] >= 2 and self._config['tilt'] != 0: w = ('SAM input for PV has array type {} (tracking) and tilt ' 'of {}. This is uncommon!' .format(self._config['array_type'], self._config['tilt'])) logger.warning(w) warn(w, SAMInputWarning)
def _run_checks(self): """Infer config type and run applicable checks.""" if all(c in self._config for c in self.KEYS_PV): self.check_pv()
[docs] @classmethod def check(cls, config): """Run checks on a SAM input json config. Parameters ---------- config : dict Extracted SAM technology input config in dict form. """ c = cls(config) c._run_checks()