NREL-reV 0.13.1__py3-none-any.whl → 0.14.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,31 +34,54 @@ class EconomiesOfScale:
34
34
  lcoe : $/MWh
35
35
  """
36
36
 
37
- def __init__(self, eqn, data):
37
+ def __init__(self, data, cap_eqn=None, fixed_eqn=None, var_eqn=None):
38
38
  """
39
+
39
40
  Parameters
40
41
  ----------
41
- eqn : str
42
- LCOE scaling equation to implement "economies of scale".
43
- Equation must be in python string format and return a scalar
44
- value to multiply the capital cost by. Independent variables in
45
- the equation should match the keys in the data input arg. This
46
- equation may use numpy functions with the package prefix "np".
47
42
  data : dict | pd.DataFrame
48
43
  Namespace of econ data to use to calculate economies of scale. Keys
49
44
  in dict or column labels in dataframe should match the Independent
50
45
  variables in the eqn input. Should also include variables required
51
46
  to calculate LCOE.
47
+ cap_eqn : str, optional
48
+ LCOE scaling equation to implement "economies of scale".
49
+ Equation must be in python string format and return a scalar
50
+ value to multiply the capital cost by. Independent variables in
51
+ the equation should match the keys in the data input arg. This
52
+ equation may use numpy functions with the package prefix "np". If
53
+ ``None``, no economies of scale are applied to the capital cost.
54
+ By default, ``None``.
55
+ fixed_eqn : str, optional
56
+ LCOE scaling equation to implement "economies of scale".
57
+ Equation must be in python string format and return a scalar
58
+ value to multiply the fixed operating cost by. Independent
59
+ variables in the equation should match the keys in the data input
60
+ arg. This equation may use numpy functions with the package prefix
61
+ "np". If ``None``, no economies of scale are applied to the
62
+ fixed operating cost. By default, ``None``.
63
+ var_eqn : str, optional
64
+ LCOE scaling equation to implement "economies of scale".
65
+ Equation must be in python string format and return a scalar
66
+ value to multiply the variable operating cost by. Independent
67
+ variables in the equation should match the keys in the data input
68
+ arg. This equation may use numpy functions with the package prefix
69
+ "np". If ``None``, no economies of scale are applied to the
70
+ variable operating cost. By default, ``None``.
52
71
  """
53
- self._eqn = eqn
54
72
  self._data = data
73
+ self._cap_eqn = cap_eqn
74
+ self._fixed_eqn = fixed_eqn
75
+ self._var_eqn = var_eqn
76
+ self._vars = None
55
77
  self._preflight()
56
78
 
57
79
  def _preflight(self):
58
80
  """Run checks to validate EconomiesOfScale equation and input data."""
59
81
 
60
- if self._eqn is not None:
61
- check_eval_str(str(self._eqn))
82
+ for eq in self._all_equations:
83
+ if eq is not None:
84
+ check_eval_str(str(eq))
62
85
 
63
86
  if isinstance(self._data, pd.DataFrame):
64
87
  self._data = {
@@ -79,11 +102,16 @@ class EconomiesOfScale:
79
102
  if any(missing):
80
103
  e = (
81
104
  "Cannot evaluate EconomiesOfScale, missing data for variables"
82
- ": {} for equation: {}".format(missing, self._eqn)
105
+ ": {} for equation: {}".format(missing, self._cap_eqn)
83
106
  )
84
107
  logger.error(e)
85
108
  raise KeyError(e)
86
109
 
110
+ @property
111
+ def _all_equations(self):
112
+ """gen: All EOS equations"""
113
+ yield from (self._cap_eqn, self._fixed_eqn, self._var_eqn)
114
+
87
115
  @staticmethod
88
116
  def is_num(s):
89
117
  """Check if a string is a number"""
@@ -111,36 +139,51 @@ class EconomiesOfScale:
111
139
  the equation string. This will return an empty list if the equation
112
140
  has no variables.
113
141
  """
114
- var_names = []
115
- if self._eqn is not None:
116
- delimiters = (">", "<", ">=", "<=", "==", ",", "*", "/", "+", "-",
117
- " ", "(", ")", "[", "]")
142
+ if self._vars is not None:
143
+ return self._vars
144
+
145
+ self._vars = []
146
+ for eq in self._all_equations:
147
+ if eq is None:
148
+ continue
149
+
150
+ delimiters = (">", "<", ">=", "<=", "==", ",", "*", "/", "+",
151
+ "-", " ", "(", ")", "[", "]")
118
152
  regex_pattern = "|".join(map(re.escape, delimiters))
119
- var_names = []
120
- for sub in re.split(regex_pattern, str(self._eqn)):
121
- if sub and not self.is_num(sub) and not self.is_method(sub):
122
- var_names.append(sub)
123
- var_names = sorted(set(var_names))
153
+ for sub_str in re.split(regex_pattern, str(eq)):
154
+ is_valid_var_name = (sub_str and not self.is_num(sub_str)
155
+ and not self.is_method(sub_str))
156
+ if is_valid_var_name:
157
+ self._vars.append(sub_str)
124
158
 
125
- return var_names
159
+ self._vars = sorted(set(self._vars))
160
+ return self._vars
126
161
 
127
- def _evaluate(self):
162
+ def _evaluate(self, eqn):
128
163
  """Evaluate the EconomiesOfScale equation with Independent variables
129
164
  parsed into a kwargs dictionary input.
130
165
 
166
+ Parameters
167
+ ----------
168
+ eqn : str
169
+ LCOE scaling equation to implement "economies of scale".
170
+ Equation must be in python string format and return a scalar
171
+ multiplier. Independent variables in the equation should match the
172
+ keys in the data input arg. This equation may use numpy functions
173
+ with the package prefix "np". If ``None``, this function returns
174
+ ``1``.
175
+
131
176
  Returns
132
177
  -------
133
178
  out : float | np.ndarray
134
- Evaluated output of the EconomiesOfScale equation. Should be
135
- numeric scalars to apply directly to the capital cost.
179
+ Evaluated output of the EconomiesOfScale equation.
136
180
  """
137
- out = 1
138
- if self._eqn is not None:
139
- kwargs = {k: self._data[k] for k in self.vars}
140
- # pylint: disable=eval-used
141
- out = eval(str(self._eqn), globals(), kwargs)
181
+ if eqn is None:
182
+ return 1
142
183
 
143
- return out
184
+ kwargs = {k: self._data[k] for k in self.vars}
185
+ # pylint: disable=eval-used
186
+ return eval(str(eqn), globals(), kwargs)
144
187
 
145
188
  @staticmethod
146
189
  def _get_prioritized_keys(input_dict, key_list):
@@ -180,8 +223,8 @@ class EconomiesOfScale:
180
223
 
181
224
  @property
182
225
  def capital_cost_scalar(self):
183
- """Evaluated output of the EconomiesOfScale equation. Should be
184
- numeric scalars to apply directly to the capital cost.
226
+ """Evaluated output of the EconomiesOfScale capital cost equation.
227
+ Should be numeric scalars to apply directly to the capital cost.
185
228
 
186
229
  Returns
187
230
  -------
@@ -189,7 +232,35 @@ class EconomiesOfScale:
189
232
  Evaluated output of the EconomiesOfScale equation. Should be
190
233
  numeric scalars to apply directly to the capital cost.
191
234
  """
192
- return self._evaluate()
235
+ return self._evaluate(self._cap_eqn)
236
+
237
+ @property
238
+ def fixed_operating_cost_scalar(self):
239
+ """Evaluated output of the EconomiesOfScale fixed operating cost
240
+ equation. Should be numeric scalars to apply directly to the fixed
241
+ operating cost.
242
+
243
+ Returns
244
+ -------
245
+ out : float | np.ndarray
246
+ Evaluated output of the EconomiesOfScale equation. Should be
247
+ numeric scalars to apply directly to the fixed operating cost.
248
+ """
249
+ return self._evaluate(self._fixed_eqn)
250
+
251
+ @property
252
+ def variable_operating_cost_scalar(self):
253
+ """Evaluated output of the EconomiesOfScale equation variable
254
+ operating cost. Should be numeric scalars to apply directly to the
255
+ variable operating cost.
256
+
257
+ Returns
258
+ -------
259
+ out : float | np.ndarray
260
+ Evaluated output of the EconomiesOfScale equation. Should be
261
+ numeric scalars to apply directly to the variable operating cost.
262
+ """
263
+ return self._evaluate(self._var_eqn)
193
264
 
194
265
  def _cost_from_cap(self, col_name):
195
266
  """Get full cost value from cost per mw in data.
@@ -221,10 +292,10 @@ class EconomiesOfScale:
221
292
  Returns
222
293
  -------
223
294
  out : float | np.ndarray
224
- Unscaled (raw) capital_cost found in the data input arg.
295
+ Unscaled (raw) capital_cost ($) found in the data input arg.
225
296
  """
226
297
  raw_capital_cost_from_cap = self._cost_from_cap(
227
- SupplyCurveField.COST_SITE_OCC_USD_PER_AC_MW
298
+ SupplyCurveField.COST_SITE_CC_USD_PER_AC_MW
228
299
  )
229
300
  if raw_capital_cost_from_cap is not None:
230
301
  return raw_capital_cost_from_cap
@@ -240,8 +311,8 @@ class EconomiesOfScale:
240
311
  Returns
241
312
  -------
242
313
  out : float | np.ndarray
243
- Capital cost found in the data input arg scaled by the evaluated
244
- EconomiesOfScale equation.
314
+ Capital cost ($) found in the data input arg scaled by the
315
+ evaluated EconomiesOfScale equation.
245
316
  """
246
317
  cc = copy.deepcopy(self.raw_capital_cost)
247
318
  cc *= self.capital_cost_scalar
@@ -265,13 +336,13 @@ class EconomiesOfScale:
265
336
  return self._get_prioritized_keys(self._data, key_list)
266
337
 
267
338
  @property
268
- def foc(self):
269
- """Fixed operating cost from input data arg
339
+ def raw_fixed_operating_cost(self):
340
+ """Unscaled (raw) fixed operating cost from input data arg
270
341
 
271
342
  Returns
272
343
  -------
273
344
  out : float | np.ndarray
274
- Fixed operating cost from input data arg
345
+ Unscaled (raw) fixed operating cost ($/year) from input data arg
275
346
  """
276
347
  foc_from_cap = self._cost_from_cap(
277
348
  SupplyCurveField.COST_SITE_FOC_USD_PER_AC_MW
@@ -284,42 +355,70 @@ class EconomiesOfScale:
284
355
  return self._get_prioritized_keys(self._data, key_list)
285
356
 
286
357
  @property
287
- def voc(self):
288
- """Variable operating cost from input data arg
358
+ def scaled_fixed_operating_cost(self):
359
+ """Fixed operating cost found in the data input arg scaled by the
360
+ evaluated EconomiesOfScale input equation.
289
361
 
290
362
  Returns
291
363
  -------
292
364
  out : float | np.ndarray
293
- Variable operating cost from input data arg
365
+ Fixed operating cost ($/year) found in the data input arg scaled
366
+ by the evaluated EconomiesOfScale equation.
294
367
  """
295
- voc_from_cap = self._cost_from_cap(
296
- SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MW
297
- )
298
- if voc_from_cap is not None:
299
- return voc_from_cap
368
+ foc = copy.deepcopy(self.raw_fixed_operating_cost)
369
+ foc *= self.fixed_operating_cost_scalar
370
+ return foc
371
+
372
+ @property
373
+ def raw_variable_operating_cost(self):
374
+ """Unscaled (raw) variable operating cost from input data arg
375
+
376
+ Returns
377
+ -------
378
+ out : float | np.ndarray
379
+ Unscaled (raw) variable operating cost ($/kWh) from input
380
+ data arg
381
+ """
382
+ voc_mwh = self._data.get(SupplyCurveField.COST_SITE_VOC_USD_PER_AC_MWH)
383
+ if voc_mwh is not None:
384
+ return voc_mwh / 1000 # convert to $/kWh
300
385
 
301
386
  key_list = ["variable_operating_cost", "mean_variable_operating_cost",
302
387
  "voc", "mean_voc"]
303
388
  return self._get_prioritized_keys(self._data, key_list)
304
389
 
390
+ @property
391
+ def scaled_variable_operating_cost(self):
392
+ """Variable operating cost found in the data input arg scaled by the
393
+ evaluated EconomiesOfScale input equation.
394
+
395
+ Returns
396
+ -------
397
+ out : float | np.ndarray
398
+ Variable operating cost ($/kWh) found in the data input arg
399
+ scaled by the evaluated EconomiesOfScale equation.
400
+ """
401
+ voc = copy.deepcopy(self.raw_variable_operating_cost)
402
+ voc *= self.variable_operating_cost_scalar
403
+ return voc
404
+
305
405
  @property
306
406
  def aep(self):
307
- """Annual energy production back-calculated from the raw LCOE:
407
+ """Annual energy production (kWh) back-calculated from the raw LCOE:
308
408
 
309
- AEP = (fcr * raw_cap_cost + foc) / raw_lcoe
409
+ AEP = (fcr * raw_cap_cost + raw_foc) / (raw_lcoe - raw_voc)
310
410
 
311
411
  Returns
312
412
  -------
313
413
  out : float | np.ndarray
314
414
  """
315
-
316
- aep = (self.fcr * self.raw_capital_cost + self.foc) / self.raw_lcoe
317
- aep *= 1000 # convert MWh to KWh
318
- return aep
415
+ num = self.fcr * self.raw_capital_cost + self.raw_fixed_operating_cost
416
+ denom = self.raw_lcoe - (self.raw_variable_operating_cost * 1000)
417
+ return num / denom * 1000 # convert MWh to KWh
319
418
 
320
419
  @property
321
420
  def raw_lcoe(self):
322
- """Raw LCOE taken from the input data
421
+ """Raw LCOE ($/MWh) taken from the input data
323
422
 
324
423
  Returns
325
424
  -------
@@ -330,17 +429,17 @@ class EconomiesOfScale:
330
429
 
331
430
  @property
332
431
  def scaled_lcoe(self):
333
- """LCOE calculated with the scaled capital cost based on the
432
+ """LCOE ($/MWh) calculated with the scaled costs based on the
334
433
  EconomiesOfScale input equation.
335
434
 
336
- LCOE = (FCR * scaled_capital_cost + FOC) / AEP + VOC
435
+ LCOE = (FCR * scaled_capital_cost + scaled_FOC) / AEP + scaled_VOC
337
436
 
338
437
  Returns
339
438
  -------
340
439
  lcoe : float | np.ndarray
341
- LCOE calculated with the scaled capital cost based on the
440
+ LCOE calculated with the scaled costs based on the
342
441
  EconomiesOfScale input equation.
343
442
  """
344
- return lcoe_fcr(
345
- self.fcr, self.scaled_capital_cost, self.foc, self.aep, self.voc
346
- )
443
+ return lcoe_fcr(self.fcr, self.scaled_capital_cost,
444
+ self.scaled_fixed_operating_cost, self.aep,
445
+ self.scaled_variable_operating_cost)
reV/generation/cli_gen.py CHANGED
@@ -67,8 +67,10 @@ def _parse_res_files(res_fps, analysis_years):
67
67
 
68
68
  # get base filename, may have {} for year format
69
69
  if isinstance(res_fps, str) and '{}' in res_fps:
70
- # need to make list of res files for each year
71
- res_fps = [res_fps.format(year) for year in analysis_years]
70
+ # make list of res files for each year
71
+ # .replace("{}", "{0}") used for multiple "{}" in path
72
+ res_fps = [res_fps.replace("{}", "{0}").format(year)
73
+ for year in analysis_years]
72
74
  elif isinstance(res_fps, str):
73
75
  # only one resource file request, still put in list
74
76
  res_fps = [res_fps]
reV/losses/scheduled.py CHANGED
@@ -7,7 +7,6 @@ import warnings
7
7
  import json
8
8
 
9
9
  import numpy as np
10
- import PySAM
11
10
 
12
11
  from reV.losses.utils import (convert_to_full_month_names,
13
12
  filter_unknown_month_names,
@@ -524,8 +523,8 @@ class ScheduledLossesMixin:
524
523
  information is expected to be a list of dictionaries containing
525
524
  outage specifications. See :class:`Outage` for a description of
526
525
  the specifications allowed for each outage. The scheduled losses
527
- are passed to SAM via the ``adjust_hourly`` key to signify which
528
- hourly capacity factors should be adjusted with outage losses.
526
+ are passed to SAM via the ``adjust_timeindex`` key to signify
527
+ which capacity factors should be adjusted with outage losses.
529
528
  If no outage info is specified in ``sam_sys_inputs``, no
530
529
  scheduled losses are added.
531
530
 
@@ -543,13 +542,13 @@ class ScheduledLossesMixin:
543
542
 
544
543
  Notes
545
544
  -----
546
- The scheduled losses are passed to SAM via the ``adjust_hourly``
547
- key to signify which hourly capacity factors should be adjusted
548
- with outage losses. If the user specifies other hourly
549
- adjustment factors via the ``adjust_hourly`` key, the effect is
550
- combined. For example, if the user inputs a 33% hourly
545
+ The scheduled losses are passed to SAM via the
546
+ ``adjust_timeindex`` key to signify which capacity factors
547
+ should be adjusted with outage losses. If the user specifies
548
+ other adjustment factors via the ``adjust_timeindex`` key, the
549
+ effect is combined. For example, if the user inputs a 33%
551
550
  adjustment factor and reV schedules an outage for 70% of the
552
- farm down for the same hour, then the resulting adjustment
551
+ farm down for the same time step, then the resulting adjustment
553
552
  factor is
554
553
 
555
554
  .. math: 1 - [(1 - 70/100) * (1 - 33/100)] = 0.799
@@ -575,7 +574,7 @@ class ScheduledLossesMixin:
575
574
  self._add_outages_to_sam_inputs(hourly_outages)
576
575
 
577
576
  logger.debug("Hourly adjustment factors after scheduled outages: {}"
578
- .format(list(self.sam_sys_inputs['adjust_hourly'])))
577
+ .format(list(self.sam_sys_inputs['adjust_timeindex'])))
579
578
 
580
579
  def _user_outage_input(self):
581
580
  """Get outage and seed info from config. """
@@ -601,16 +600,20 @@ class ScheduledLossesMixin:
601
600
  def _add_outages_to_sam_inputs(self, outages):
602
601
  """Add the hourly adjustment factors to config, checking user input."""
603
602
 
603
+ if self.time_interval > 1:
604
+ outages = np.array([outage
605
+ for _ in range(self.time_interval)
606
+ for outage in outages])
604
607
  hourly_mult = 1 - outages / 100
605
- hourly_mult = self._fix_pysam_bug(hourly_mult)
606
608
 
607
- user_hourly_input = self.sam_sys_inputs.pop('adjust_hourly',
608
- [0] * 8760)
609
+ default_user_input = [0] * 8760 * self.time_interval
610
+ user_hourly_input = self.sam_sys_inputs.pop('adjust_timeindex',
611
+ default_user_input)
609
612
  user_hourly_mult = 1 - np.array(user_hourly_input) / 100
610
613
 
611
614
  final_hourly_mult = hourly_mult * user_hourly_mult
612
- self.sam_sys_inputs['adjust_hourly'] = (1 - final_hourly_mult) * 100
613
- self.sam_sys_inputs['adjust_en_hourly'] = 1
615
+ self.sam_sys_inputs['adjust_timeindex'] = (1 - final_hourly_mult) * 100
616
+ self.sam_sys_inputs['adjust_en_timeindex'] = 1
614
617
 
615
618
  @property
616
619
  def outage_seed(self):
@@ -631,19 +634,3 @@ class ScheduledLossesMixin:
631
634
  pass
632
635
 
633
636
  return self.__base_seed
634
-
635
- def _fix_pysam_bug(self, hourly_mult):
636
- """Fix PySAM bug that squares HAF user input"""
637
- if getattr(self, "MODULE", "").casefold() != "windpower":
638
- return hourly_mult
639
-
640
- bugged_pysam_version = (PySAM.__version__.startswith("5")
641
- or PySAM.__version__.startswith("6"))
642
- if not bugged_pysam_version:
643
- return hourly_mult
644
-
645
- # Bug in PySAM windpower module that applies HAF twice (i.e.
646
- # squares the input values), so we sqrt the desired loss value
647
- return np.sqrt(hourly_mult)
648
-
649
-
@@ -9,6 +9,7 @@ import h5py
9
9
  import numpy as np
10
10
  import pandas as pd
11
11
  from rex.resource import Resource
12
+ from rex.utilities import check_res_file
12
13
  from rex.utilities.execution import SpawnProcessPool
13
14
  from rex.utilities.loggers import log_mem
14
15
 
@@ -113,6 +114,7 @@ class AggFileHandler(AbstractAggFileHandler):
113
114
  area_filter_kernel="queen",
114
115
  min_area=None,
115
116
  h5_handler=None,
117
+ **h5_handler_kwargs,
116
118
  ):
117
119
  """
118
120
  Parameters
@@ -136,6 +138,8 @@ class AggFileHandler(AbstractAggFileHandler):
136
138
  h5_handler : rex.Resource | None
137
139
  Optional special handler similar to the rex.Resource handler which
138
140
  is default.
141
+ **h5_handler_kwargs
142
+ Optional keyword-value pairs to pass to the h5 handler.
139
143
  """
140
144
  super().__init__(
141
145
  excl_fpath,
@@ -145,9 +149,9 @@ class AggFileHandler(AbstractAggFileHandler):
145
149
  )
146
150
 
147
151
  if h5_handler is None:
148
- self._h5 = Resource(h5_fpath)
152
+ self._h5 = Resource(h5_fpath, **h5_handler_kwargs)
149
153
  else:
150
- self._h5 = h5_handler(h5_fpath)
154
+ self._h5 = h5_handler(h5_fpath, **h5_handler_kwargs)
151
155
 
152
156
  @property
153
157
  def h5(self):
@@ -504,8 +508,9 @@ class BaseAggregation(ABC):
504
508
  generation run.
505
509
  """
506
510
 
511
+ __, hsds = check_res_file(gen_fpath)
507
512
  if gen_fpath.endswith(".h5"):
508
- with Resource(gen_fpath) as f:
513
+ with Resource(gen_fpath, hsds=hsds) as f:
509
514
  gen_index = f.meta
510
515
  elif gen_fpath.endswith(".csv"):
511
516
  gen_index = pd.read_csv(gen_fpath)
@@ -631,7 +636,8 @@ class Aggregation(BaseAggregation):
631
636
  )
632
637
  )
633
638
 
634
- if not os.path.exists(h5_fpath):
639
+ __, hsds = check_res_file(h5_fpath)
640
+ if not hsds and not os.path.exists(h5_fpath):
635
641
  raise FileNotFoundError(
636
642
  "Could not find required h5 file: " "{}".format(h5_fpath)
637
643
  )
@@ -645,7 +651,7 @@ class Aggregation(BaseAggregation):
645
651
  )
646
652
  )
647
653
 
648
- with Resource(h5_fpath) as f:
654
+ with Resource(h5_fpath, hsds=hsds) as f:
649
655
  for dset in self._agg_dsets:
650
656
  if dset not in f:
651
657
  raise FileInputError(
@@ -744,6 +750,7 @@ class Aggregation(BaseAggregation):
744
750
  "excl_dict": excl_dict,
745
751
  "area_filter_kernel": area_filter_kernel,
746
752
  "min_area": min_area,
753
+ "hsds": check_res_file(h5_fpath)[1],
747
754
  }
748
755
  dsets = (
749
756
  *agg_dset,
@@ -1011,7 +1018,9 @@ class Aggregation(BaseAggregation):
1011
1018
  chunks = {}
1012
1019
  dtypes = {}
1013
1020
  time_index = None
1014
- with Resource(h5_fpath) as f:
1021
+
1022
+ __, hsds = check_res_file(h5_fpath)
1023
+ with Resource(h5_fpath, hsds=hsds) as f:
1015
1024
  for dset, data in agg_out.items():
1016
1025
  dsets.append(dset)
1017
1026
  shape = data.shape
@@ -0,0 +1,66 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ reV Tech Mapping CLI utility functions.
4
+ """
5
+ import logging
6
+
7
+ from gaps.cli import as_click_command, CLICommandFromClass
8
+
9
+ from reV.supply_curve.tech_mapping import TechMapping
10
+ from reV.utilities import ModuleName
11
+ from reV.utilities.exceptions import ConfigError
12
+ from reV.supply_curve.cli_sc_aggregation import _format_res_fpath
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _preprocessor(config):
18
+ """Preprocess tech mapping config user input.
19
+
20
+ Parameters
21
+ ----------
22
+ config : dict
23
+ User configuration file input as (nested) dict.
24
+
25
+ Returns
26
+ -------
27
+ dict
28
+ Updated config file.
29
+ """
30
+ _validate_excl_fpath(config)
31
+ config = _format_res_fpath(config)
32
+ _validate_dset(config)
33
+
34
+ return config
35
+
36
+
37
+ def _validate_excl_fpath(config):
38
+ paths = config["excl_fpath"]
39
+ if isinstance(paths, list):
40
+ raise ConfigError(
41
+ "Multiple exclusion file paths passed via excl_fpath. "
42
+ "Cannot run tech mapping with arbitrary multiple exclusion. "
43
+ "Specify a single exclusion file path to write to."
44
+ )
45
+
46
+
47
+ def _validate_dset(config):
48
+ if config.get("dset") is None:
49
+ raise ConfigError(
50
+ "dset must be specified to run tech mapping."
51
+ )
52
+
53
+
54
+ tm_command = CLICommandFromClass(TechMapping, method="run",
55
+ name=str(ModuleName.TECH_MAPPING),
56
+ add_collect=False, split_keys=None,
57
+ config_preprocessor=_preprocessor)
58
+ main = as_click_command(tm_command)
59
+
60
+
61
+ if __name__ == '__main__':
62
+ try:
63
+ main(obj={})
64
+ except Exception:
65
+ logger.exception('Error running reV Tech Mapping CLI.')
66
+ raise