NREL-reV 0.8.7__py3-none-any.whl → 0.9.0__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.
Files changed (43) hide show
  1. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/METADATA +13 -10
  2. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/RECORD +43 -43
  3. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/WHEEL +1 -1
  4. reV/SAM/SAM.py +217 -133
  5. reV/SAM/econ.py +18 -14
  6. reV/SAM/generation.py +611 -422
  7. reV/SAM/windbos.py +93 -79
  8. reV/bespoke/bespoke.py +681 -377
  9. reV/bespoke/cli_bespoke.py +2 -0
  10. reV/bespoke/place_turbines.py +187 -43
  11. reV/config/output_request.py +2 -1
  12. reV/config/project_points.py +218 -140
  13. reV/econ/econ.py +166 -114
  14. reV/econ/economies_of_scale.py +91 -45
  15. reV/generation/base.py +331 -184
  16. reV/generation/generation.py +326 -200
  17. reV/generation/output_attributes/lcoe_fcr_inputs.json +38 -3
  18. reV/handlers/__init__.py +0 -1
  19. reV/handlers/exclusions.py +16 -15
  20. reV/handlers/multi_year.py +57 -26
  21. reV/handlers/outputs.py +6 -5
  22. reV/handlers/transmission.py +44 -27
  23. reV/hybrids/hybrid_methods.py +30 -30
  24. reV/hybrids/hybrids.py +305 -189
  25. reV/nrwal/nrwal.py +262 -168
  26. reV/qa_qc/cli_qa_qc.py +14 -10
  27. reV/qa_qc/qa_qc.py +217 -119
  28. reV/qa_qc/summary.py +228 -146
  29. reV/rep_profiles/rep_profiles.py +349 -230
  30. reV/supply_curve/aggregation.py +349 -188
  31. reV/supply_curve/competitive_wind_farms.py +90 -48
  32. reV/supply_curve/exclusions.py +138 -85
  33. reV/supply_curve/extent.py +75 -50
  34. reV/supply_curve/points.py +735 -390
  35. reV/supply_curve/sc_aggregation.py +357 -248
  36. reV/supply_curve/supply_curve.py +604 -347
  37. reV/supply_curve/tech_mapping.py +144 -82
  38. reV/utilities/__init__.py +274 -16
  39. reV/utilities/pytest_utils.py +8 -4
  40. reV/version.py +1 -1
  41. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/LICENSE +0 -0
  42. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/entry_points.txt +0 -0
  43. {NREL_reV-0.8.7.dist-info → NREL_reV-0.9.0.dist-info}/top_level.txt +0 -0
@@ -3,10 +3,12 @@
3
3
  Competitive Wind Farms exclusion handler
4
4
  """
5
5
  import logging
6
- import numpy as np
7
6
 
7
+ import numpy as np
8
8
  from rex.utilities.utilities import parse_table
9
9
 
10
+ from reV.utilities import SupplyCurveField
11
+
10
12
  logger = logging.getLogger(__name__)
11
13
 
12
14
 
@@ -33,30 +35,36 @@ class CompetitiveWindFarms:
33
35
  """
34
36
  self._wind_dirs = self._parse_wind_dirs(wind_dirs)
35
37
 
36
- self._sc_gids, self._sc_point_gids, self._mask = \
37
- self._parse_sc_points(sc_points, offshore=offshore)
38
+ self._sc_gids, self._sc_point_gids, self._mask = self._parse_sc_points(
39
+ sc_points, offshore=offshore
40
+ )
38
41
 
39
42
  self._offshore = offshore
40
43
 
41
44
  valid = np.isin(self.sc_point_gids, self._wind_dirs.index)
42
45
  if not np.all(valid):
43
- msg = ("'sc_points contains sc_point_gid values that do not "
44
- "correspond to valid 'wind_dirs' sc_point_gids:\n{}"
45
- .format(self.sc_point_gids[~valid]))
46
+ msg = (
47
+ "'sc_points contains sc_point_gid values that do not "
48
+ "correspond to valid 'wind_dirs' sc_point_gids:\n{}".format(
49
+ self.sc_point_gids[~valid]
50
+ )
51
+ )
46
52
  logger.error(msg)
47
53
  raise RuntimeError(msg)
48
54
 
49
55
  mask = self._wind_dirs.index.isin(self._sc_point_gids.keys())
50
56
  self._wind_dirs = self._wind_dirs.loc[mask]
51
- self._upwind, self._downwind = self._get_neighbors(self._wind_dirs,
52
- n_dirs=n_dirs)
57
+ self._upwind, self._downwind = self._get_neighbors(
58
+ self._wind_dirs, n_dirs=n_dirs
59
+ )
53
60
 
54
61
  def __repr__(self):
55
62
  gids = len(self._upwind)
56
63
  # pylint: disable=unsubscriptable-object
57
64
  neighbors = len(self._upwind.values[0])
58
- msg = ("{} with {} sc_point_gids and {} prominent directions"
59
- .format(self.__class__.__name__, gids, neighbors))
65
+ msg = "{} with {} sc_point_gids and {} prominent directions".format(
66
+ self.__class__.__name__, gids, neighbors
67
+ )
60
68
 
61
69
  return msg
62
70
 
@@ -76,23 +84,25 @@ class CompetitiveWindFarms:
76
84
  """
77
85
  if not isinstance(keys, tuple):
78
86
  msg = ("{} must be a tuple of form (source, gid) where source is: "
79
- "'sc_gid', 'sc_point_gid', or 'upwind', 'downwind'"
80
- .format(keys))
87
+ "{}, '{}', or 'upwind', 'downwind'"
88
+ .format(keys, SupplyCurveField.SC_GID,
89
+ SupplyCurveField.SC_POINT_GID))
81
90
  logger.error(msg)
82
91
  raise ValueError(msg)
83
92
 
84
93
  source, gid = keys
85
- if source == 'sc_point_gid':
94
+ if source == SupplyCurveField.SC_POINT_GID:
86
95
  out = self.map_sc_gid_to_sc_point_gid(gid)
87
- elif source == 'sc_gid':
96
+ elif source == SupplyCurveField.SC_GID:
88
97
  out = self.map_sc_point_gid_to_sc_gid(gid)
89
- elif source == 'upwind':
98
+ elif source == "upwind":
90
99
  out = self.map_upwind(gid)
91
- elif source == 'downwind':
100
+ elif source == "downwind":
92
101
  out = self.map_downwind(gid)
93
102
  else:
94
- msg = ("{} must be: 'sc_gid', 'sc_point_gid', or 'upwind', "
95
- "'downwind'".format(source))
103
+ msg = ("{} must be: {}, {}, or 'upwind', "
104
+ "'downwind'".format(source, SupplyCurveField.SC_GID,
105
+ SupplyCurveField.SC_POINT_GID))
96
106
  logger.error(msg)
97
107
  raise ValueError(msg)
98
108
 
@@ -133,9 +143,9 @@ class CompetitiveWindFarms:
133
143
  -------
134
144
  ndarray
135
145
  """
136
- sc_gids = \
137
- np.concatenate([self._sc_point_gids[gid]
138
- for gid in self.sc_point_gids])
146
+ sc_gids = np.concatenate(
147
+ [self._sc_point_gids[gid] for gid in self.sc_point_gids]
148
+ )
139
149
 
140
150
  return sc_gids
141
151
 
@@ -181,8 +191,10 @@ class CompetitiveWindFarms:
181
191
  cardinal direction for each sc point gid
182
192
  """
183
193
  wind_dirs = cls._parse_table(wind_dirs)
194
+ wind_dirs = wind_dirs.rename(
195
+ columns=SupplyCurveField.map_from_legacy())
184
196
 
185
- wind_dirs = wind_dirs.set_index('sc_point_gid')
197
+ wind_dirs = wind_dirs.set_index(SupplyCurveField.SC_POINT_GID)
186
198
  columns = [c for c in wind_dirs if c.endswith(('_gid', '_pr'))]
187
199
  wind_dirs = wind_dirs[columns]
188
200
 
@@ -212,21 +224,25 @@ class CompetitiveWindFarms:
212
224
  Mask array to mask excluded sc_point_gids
213
225
  """
214
226
  sc_points = cls._parse_table(sc_points)
215
- if 'offshore' in sc_points and not offshore:
227
+ sc_points = sc_points.rename(
228
+ columns=SupplyCurveField.map_from_legacy())
229
+ if SupplyCurveField.OFFSHORE in sc_points and not offshore:
216
230
  logger.debug('Not including offshore supply curve points in '
217
231
  'CompetitiveWindFarm')
218
- mask = sc_points['offshore'] == 0
232
+ mask = sc_points[SupplyCurveField.OFFSHORE] == 0
219
233
  sc_points = sc_points.loc[mask]
220
234
 
221
- mask = np.ones(int(1 + sc_points['sc_point_gid'].max()), dtype=bool)
235
+ mask = np.ones(int(1 + sc_points[SupplyCurveField.SC_POINT_GID].max()),
236
+ dtype=bool)
222
237
 
223
- sc_points = sc_points[['sc_gid', 'sc_point_gid']]
224
- sc_gids = sc_points.set_index('sc_gid')
238
+ sc_points = sc_points[[SupplyCurveField.SC_GID,
239
+ SupplyCurveField.SC_POINT_GID]]
240
+ sc_gids = sc_points.set_index(SupplyCurveField.SC_GID)
225
241
  sc_gids = {k: int(v[0]) for k, v in sc_gids.iterrows()}
226
242
 
227
- sc_point_gids = \
228
- sc_points.groupby('sc_point_gid')['sc_gid'].unique().to_frame()
229
- sc_point_gids = {int(k): v['sc_gid']
243
+ groups = sc_points.groupby(SupplyCurveField.SC_POINT_GID)
244
+ sc_point_gids = groups[SupplyCurveField.SC_GID].unique().to_frame()
245
+ sc_point_gids = {int(k): v[SupplyCurveField.SC_GID]
230
246
  for k, v in sc_point_gids.iterrows()}
231
247
 
232
248
  return sc_gids, sc_point_gids, mask
@@ -251,19 +267,30 @@ class CompetitiveWindFarms:
251
267
  downwind : pandas.DataFrame
252
268
  Downwind neighbor gids for n prominent wind directions
253
269
  """
254
- cols = [c for c in wind_dirs
255
- if (c.endswith('_gid') and not c.startswith('sc'))]
256
- directions = [c.split('_')[0] for c in cols]
270
+ cols = [
271
+ c
272
+ for c in wind_dirs
273
+ if (c.endswith("_gid") and not c.startswith("sc"))
274
+ ]
275
+ directions = [c.split("_")[0] for c in cols]
257
276
  upwind_gids = wind_dirs[cols].values
258
277
 
259
- cols = ['{}_pr'.format(d) for d in directions]
278
+ cols = ["{}_pr".format(d) for d in directions]
260
279
  neighbor_pr = wind_dirs[cols].values
261
280
 
262
281
  neighbors = np.argsort(neighbor_pr)[:, :n_dirs]
263
282
  upwind_gids = np.take_along_axis(upwind_gids, neighbors, axis=1)
264
283
 
265
- downwind_map = {'N': 'S', 'NE': 'SW', 'E': 'W', 'SE': 'NW', 'S': 'N',
266
- 'SW': 'NE', 'W': 'E', 'NW': 'SE'}
284
+ downwind_map = {
285
+ "N": "S",
286
+ "NE": "SW",
287
+ "E": "W",
288
+ "SE": "NW",
289
+ "S": "N",
290
+ "SW": "NE",
291
+ "W": "E",
292
+ "NW": "SE",
293
+ }
267
294
  cols = ["{}_gid".format(downwind_map[d]) for d in directions]
268
295
  downwind_gids = wind_dirs[cols].values
269
296
  downwind_gids = np.take_along_axis(downwind_gids, neighbors, axis=1)
@@ -338,6 +365,7 @@ class CompetitiveWindFarms:
338
365
  ----------
339
366
  sc_point_gid : int
340
367
  Supply point curve gid to get upwind neighbors
368
+
341
369
  Returns
342
370
  -------
343
371
  int | list
@@ -353,6 +381,7 @@ class CompetitiveWindFarms:
353
381
  ----------
354
382
  sc_point_gid : int
355
383
  Supply point curve gid to get downwind neighbors
384
+
356
385
  Returns
357
386
  -------
358
387
  int | list
@@ -383,8 +412,9 @@ class CompetitiveWindFarms:
383
412
 
384
413
  return out
385
414
 
386
- def remove_noncompetitive_farm(self, sc_points, sort_on='total_lcoe',
387
- downwind=False):
415
+ def remove_noncompetitive_farm(
416
+ self, sc_points, sort_on="total_lcoe", downwind=False
417
+ ):
388
418
  """
389
419
  Remove neighboring sc points for given number of prominent wind
390
420
  directions
@@ -407,34 +437,45 @@ class CompetitiveWindFarms:
407
437
  wind farms
408
438
  """
409
439
  sc_points = self._parse_table(sc_points)
410
- if 'offshore' in sc_points and not self._offshore:
411
- mask = sc_points['offshore'] == 0
440
+ sc_points = sc_points.rename(
441
+ columns=SupplyCurveField.map_from_legacy())
442
+ if SupplyCurveField.OFFSHORE in sc_points and not self._offshore:
443
+ mask = sc_points[SupplyCurveField.OFFSHORE] == 0
412
444
  sc_points = sc_points.loc[mask]
413
445
 
414
446
  sc_points = sc_points.sort_values(sort_on)
415
447
 
416
- sc_point_gids = sc_points['sc_point_gid'].values.astype(int)
448
+ sc_point_gids = sc_points[SupplyCurveField.SC_POINT_GID].values
449
+ sc_point_gids = sc_point_gids.astype(int)
417
450
 
418
451
  for i in range(len(sc_points)):
419
452
  gid = sc_point_gids[i]
420
453
  if self.mask[gid]:
421
- upwind_gids = self['upwind', gid]
454
+ upwind_gids = self["upwind", gid]
422
455
  for n in upwind_gids:
423
456
  self.exclude_sc_point_gid(n)
424
457
 
425
458
  if downwind:
426
- downwind_gids = self['downwind', gid]
459
+ downwind_gids = self["downwind", gid]
427
460
  for n in downwind_gids:
428
461
  self.exclude_sc_point_gid(n)
429
462
 
430
463
  sc_gids = self.sc_gids
431
- mask = sc_points['sc_gid'].isin(sc_gids)
464
+ mask = sc_points[SupplyCurveField.SC_GID].isin(sc_gids)
432
465
 
433
466
  return sc_points.loc[mask].reset_index(drop=True)
434
467
 
435
468
  @classmethod
436
- def run(cls, wind_dirs, sc_points, n_dirs=2, offshore=False,
437
- sort_on='total_lcoe', downwind=False, out_fpath=None):
469
+ def run(
470
+ cls,
471
+ wind_dirs,
472
+ sc_points,
473
+ n_dirs=2,
474
+ offshore=False,
475
+ sort_on="total_lcoe",
476
+ downwind=False,
477
+ out_fpath=None,
478
+ ):
438
479
  """
439
480
  Exclude given number of neighboring Supply Point gids based on most
440
481
  prominent wind directions
@@ -469,8 +510,9 @@ class CompetitiveWindFarms:
469
510
  wind farms
470
511
  """
471
512
  cwf = cls(wind_dirs, sc_points, n_dirs=n_dirs, offshore=offshore)
472
- sc_points = cwf.remove_noncompetitive_farm(sc_points, sort_on=sort_on,
473
- downwind=downwind)
513
+ sc_points = cwf.remove_noncompetitive_farm(
514
+ sc_points, sort_on=sort_on, downwind=downwind
515
+ )
474
516
 
475
517
  if out_fpath is not None:
476
518
  sc_points.to_csv(out_fpath, index=False)
@@ -3,14 +3,14 @@
3
3
  Generate reV inclusion mask from exclusion layers
4
4
  """
5
5
  import logging
6
- import numpy as np
7
- from scipy import ndimage
8
6
  from warnings import warn
9
7
 
8
+ import numpy as np
10
9
  from rex.utilities.loggers import log_mem
11
- from reV.handlers.exclusions import ExclusionLayers
12
- from reV.utilities.exceptions import ExclusionLayerError
13
- from reV.utilities.exceptions import SupplyCurveInputError
10
+ from scipy import ndimage
11
+
12
+ from reV.handlers.exclusions import ExclusionLayers, LATITUDE, LONGITUDE
13
+ from reV.utilities.exceptions import ExclusionLayerError, SupplyCurveInputError
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -32,6 +32,7 @@ class LayerMask:
32
32
  weight=1.0,
33
33
  exclude_nodata=False,
34
34
  nodata_value=None,
35
+ extent=None,
35
36
  **kwargs):
36
37
  """
37
38
  Parameters
@@ -49,39 +50,44 @@ class LayerMask:
49
50
 
50
51
  By default, ``None``.
51
52
  exclude_range : list | tuple, optional
52
- Two-item list of (min threshold, max threshold) for values
53
- to exclude. Mutually exclusive with other inputs - see info
54
- in the description of `exclude_values`.
55
- By default, ``None``.
53
+ Two-item list of [min threshold, max threshold] (ends are
54
+ inclusive) for values to exclude. Mutually exclusive
55
+ with other inputs (see info in the description of
56
+ `exclude_values`). By default, ``None``.
56
57
  include_values : int | float | list, optional
57
58
  Single value or list of values to include. Mutually
58
- exclusive with other inputs - see info in the description of
59
- `exclude_values`. By default, ``None``.
59
+ exclusive with other inputs (see info in the description of
60
+ `exclude_values`). By default, ``None``.
60
61
  include_range : list | tuple, optional
61
- Two-item list of (min threshold, max threshold) for values
62
- to include. Mutually exclusive with other inputs - see info
63
- in the description of `exclude_values`.
64
- By default, ``None``.
62
+ Two-item list of [min threshold, max threshold] (ends are
63
+ inclusive) for values to include. Mutually exclusive with
64
+ other inputs (see info in the description of
65
+ `exclude_values`). By default, ``None``.
65
66
  include_weights : dict, optional
66
67
  A dictionary of ``{value: weight}`` pairs, where the
67
68
  ``value`` in the layer that should be included with the
68
- given ``weight``. Mutually exclusive with other inputs - see
69
- info in the description of `exclude_values`.
69
+ given ``weight``. Mutually exclusive with other inputs (see
70
+ info in the description of `exclude_values`).
70
71
  By default, ``None``.
71
72
  force_include_values : int | float | list, optional
72
- Force the inclusion of the given value(s). Mutually
73
- exclusive with other inputs - see info in the description of
74
- `exclude_values`. By default, ``None``.
73
+ Force the inclusion of the given value(s). This input
74
+ completely replaces anything provided as `include_values`
75
+ and is mutually exclusive with other inputs (eee info in
76
+ the description of `exclude_values`). By default, ``None``.
75
77
  force_include_range : list | tuple, optional
76
78
  Force the inclusion of given values in the range
77
- (min threshold, max threshold). Mutually exclusive with
78
- other inputs - see info in the description of
79
- `exclude_values`. By default, ``None``.
79
+ [min threshold, max threshold] (ends are inclusive). This
80
+ input completely replaces anything provided as
81
+ `include_range` and is mutually exclusive with other inputs
82
+ (see info in the description of `exclude_values`).
83
+ By default, ``None``.
80
84
  use_as_weights : bool, optional
81
- Option to use layer as final inclusion weights. If ``True``,
82
- all inclusion/exclusions specifications for the layer are
83
- ignored and the raw values (scaled by the `weight` input)
84
- are used as weights. By default, ``False``.
85
+ Option to use layer as final inclusion weights (i.e.
86
+ 1 = fully included, 0.75 = 75% included, 0.5 = 50% included,
87
+ etc.). If ``True``, all inclusion/exclusions specifications
88
+ for the layer are ignored and the raw values (scaled by the
89
+ `weight` input) are used as inclusion weights.
90
+ By default, ``False``.
85
91
  weight : float, optional
86
92
  Weight applied to exclusion layer after it is calculated.
87
93
  Can be used, for example, to turn a binary exclusion layer
@@ -98,6 +104,38 @@ class LayerMask:
98
104
  inferred when LayerMask is added to
99
105
  :class:`reV.supply_curve.exclusions.ExclusionMask`.
100
106
  By default, ``None``.
107
+ extent : dict, optional
108
+ Optional dictionary with values that can be used to
109
+ initialize this class (i.e. `layer`, `exclude_values`,
110
+ `include_range`, etc.). This dictionary should contain the
111
+ specifications to create a boolean mask that defines the
112
+ extent to which the original mask should be applied.
113
+ For example, suppose you specify the input the following
114
+ way::
115
+
116
+ input_dict = {
117
+ "viewsheds": {
118
+ "exclude_values": 1,
119
+ "extent": {
120
+ "layer": "federal_parks",
121
+ "include_range": [1, 5]
122
+ }
123
+ }
124
+ }
125
+
126
+ for layer_name, kwargs in input_dict.items():
127
+ layer = LayerMask(layer_name, **kwargs)
128
+ ...
129
+
130
+ This would mean that you are masking out all viewshed layer
131
+ values equal to 1, **but only where the "federal_parks"
132
+ layer is equal to 1, 2, 3, 4, or 5**. Outside of these
133
+ regions (i.e. outside of federal park regions), the viewshed
134
+ exclusion is **NOT** applied. If the extent mask created by
135
+ these options is not boolean, an error is thrown (i.e. do
136
+ not specify `weight` or `use_as_weights`).
137
+ By default ``None``, which applies the original layer mask
138
+ to the full extent.
101
139
  **kwargs
102
140
  Optional inputs to maintain legacy kwargs of ``inclusion_*``
103
141
  instead of ``include_*``.
@@ -125,13 +163,14 @@ class LayerMask:
125
163
  self.nodata_value = nodata_value
126
164
 
127
165
  if weight > 1 or weight < 0:
128
- msg = ('Invalide weight ({}) provided for layer {}:'
166
+ msg = ('Invalid weight ({}) provided for layer {}:'
129
167
  '\nWeight must fall between 0 and 1!'.format(weight, layer))
130
168
  logger.error(msg)
131
169
  raise ValueError(msg)
132
170
 
133
171
  self._weight = weight
134
172
  self._mask_type = self._check_mask_type()
173
+ self.extent = LayerMask(**extent) if extent is not None else None
135
174
 
136
175
  def __repr__(self):
137
176
  msg = ('{} for "{}" exclusion, of type "{}"'
@@ -207,8 +246,7 @@ class LayerMask:
207
246
 
208
247
  if all(isinstance(x, (int, float)) for x in range_var):
209
248
  return min(range_var)
210
- else:
211
- return range_var[0]
249
+ return range_var[0]
212
250
 
213
251
  @property
214
252
  def max_value(self):
@@ -226,8 +264,7 @@ class LayerMask:
226
264
 
227
265
  if all(isinstance(x, (int, float)) for x in range_var):
228
266
  return max(range_var)
229
- else:
230
- return range_var[1]
267
+ return range_var[1]
231
268
 
232
269
  @property
233
270
  def exclude_values(self):
@@ -331,7 +368,7 @@ class LayerMask:
331
368
  contradictory
332
369
 
333
370
  Returns
334
- ------
371
+ -------
335
372
  mask : str
336
373
  Mask type
337
374
  """
@@ -355,6 +392,16 @@ class LayerMask:
355
392
  logger.error(msg)
356
393
  raise ExclusionLayerError(msg)
357
394
 
395
+ if mask is None:
396
+ msg = ('Exactly one approach must be specified to create the '
397
+ 'inclusion mask for layer {!r}! Please specify one of: '
398
+ '`exclude_values`, `exclude_range`, `include_values`, '
399
+ '`include_range`, `include_weights`, '
400
+ '`force_include_values`, or `force_include_range`.'
401
+ .format(self.name))
402
+ logger.error(msg)
403
+ raise ExclusionLayerError(msg)
404
+
358
405
  if mask == 'include_weights' and self._weight < 1:
359
406
  msg = ("Values are individually weighted when using "
360
407
  "'include_weights', the supplied weight of {} will be "
@@ -640,7 +687,7 @@ class ExclusionMask:
640
687
  Returns
641
688
  -------
642
689
  _excl_h5 : ExclusionLayers
643
- """
690
+ """
644
691
  return self._excl_h5
645
692
 
646
693
  @property
@@ -665,7 +712,7 @@ class ExclusionMask:
665
712
  Returns
666
713
  -------
667
714
  list
668
- """
715
+ """
669
716
  return self._layers.keys()
670
717
 
671
718
  @property
@@ -676,7 +723,7 @@ class ExclusionMask:
676
723
  Returns
677
724
  -------
678
725
  list
679
- """
726
+ """
680
727
  return self._layers.values()
681
728
 
682
729
  @property
@@ -700,7 +747,7 @@ class ExclusionMask:
700
747
  -------
701
748
  ndarray
702
749
  """
703
- return self.excl_h5['latitude']
750
+ return self.excl_h5[LATITUDE]
704
751
 
705
752
  @property
706
753
  def longitude(self):
@@ -711,7 +758,7 @@ class ExclusionMask:
711
758
  -------
712
759
  ndarray
713
760
  """
714
- return self.excl_h5['longitude']
761
+ return self.excl_h5[LONGITUDE]
715
762
 
716
763
  def add_layer(self, layer, replace=False):
717
764
  """
@@ -888,34 +935,54 @@ class ExclusionMask:
888
935
 
889
936
  return mask
890
937
 
891
- def _force_include(self, mask, layers, ds_slice):
892
- """
893
- Apply force inclusion layers
938
+ def _add_layer_to_mask(self, mask, layer, ds_slice, check_layers,
939
+ combine_func):
940
+ """Add layer mask to full mask."""
941
+ layer_mask = self._compute_layer_mask(layer, ds_slice, check_layers)
942
+ if mask is None:
943
+ return layer_mask
894
944
 
895
- Parameters
896
- ----------
897
- mask : ndarray | None
898
- Mask to apply force inclusion layers to
899
- layers : list
900
- List of force inclusion layers
901
- ds_slice : int | slice | list | ndarray
902
- What to extract from ds, each arg is for a sequential axis.
903
- For example, (slice(0, 64), slice(0, 64)) will extract a 64x64
904
- exclusions mask.
905
- """
906
- for layer in layers:
907
- layer_slice = (layer.name, ) + ds_slice
908
- layer_mask = layer[self.excl_h5[layer_slice]]
909
- logger.debug('Computing forced inclusions for {}. Layer has '
910
- 'average value of {:.2f}'
911
- .format(layer, layer_mask.mean()))
912
- log_mem(logger, log_level='DEBUG')
913
- if mask is None:
914
- mask = layer_mask
915
- else:
916
- mask = np.maximum(mask, layer_mask, dtype='float32')
945
+ return combine_func(mask, layer_mask, dtype='float32')
917
946
 
918
- return mask
947
+ def _compute_layer_mask(self, layer, ds_slice, check_layers=False):
948
+ """Compute mask for single layer, including extent."""
949
+ layer_mask = self._masked_layer_data(layer, ds_slice)
950
+ layer_mask = self._apply_layer_mask_extent(layer, layer_mask, ds_slice)
951
+
952
+ logger.debug('Computed exclusions {} for {}. Layer has average value '
953
+ 'of {:.2f}.'
954
+ .format(layer, ds_slice, layer_mask.mean()))
955
+ log_mem(logger, log_level='DEBUG')
956
+
957
+ if check_layers and not layer_mask.any():
958
+ msg = "Layer {} is fully excluded!".format(layer.name)
959
+ logger.error(msg)
960
+ raise ExclusionLayerError(msg)
961
+
962
+ return layer_mask
963
+
964
+ def _apply_layer_mask_extent(self, layer, layer_mask, ds_slice):
965
+ """Apply extent to layer mask, if any."""
966
+ if layer.extent is None:
967
+ return layer_mask
968
+
969
+ layer_extent = self._masked_layer_data(layer.extent, ds_slice)
970
+ if not np.array_equal(layer_extent, layer_extent.astype(bool)):
971
+ msg = ("Extent layer must be boolean (i.e. 0 and 1 values "
972
+ "only)! Please check your extent definition for layer "
973
+ "{} to ensure you are producing a boolean layer!"
974
+ .format(layer.name))
975
+ logger.error(msg)
976
+ raise ExclusionLayerError(msg)
977
+
978
+ logger.debug("Filtering mask for layer %s down to specified extent",
979
+ layer.name)
980
+ layer_mask = np.where(layer_extent, layer_mask, 1)
981
+ return layer_mask
982
+
983
+ def _masked_layer_data(self, layer, ds_slice):
984
+ """Extract masked data for layer."""
985
+ return layer[self.excl_h5[(layer.name, ) + ds_slice]]
919
986
 
920
987
  def _generate_mask(self, *ds_slice, check_layers=False):
921
988
  """
@@ -950,27 +1017,13 @@ class ExclusionMask:
950
1017
  if layer.force_include:
951
1018
  force_include.append(layer)
952
1019
  else:
953
- layer_slice = (layer.name, ) + ds_slice
954
- layer_mask = layer[self.excl_h5[layer_slice]]
955
-
956
- logger.debug('Computed exclusions {} for {}. '
957
- 'Layer has average value of {:.2f}.'
958
- .format(layer, ds_slice, layer_mask.mean()))
959
- log_mem(logger, log_level='DEBUG')
960
-
961
- if check_layers and not layer_mask.any():
962
- msg = ("Layer {} is fully excluded!"
963
- .format(layer.name))
964
- logger.error(msg)
965
- raise ExclusionLayerError(msg)
966
-
967
- if mask is None:
968
- mask = layer_mask
969
- else:
970
- mask = np.minimum(mask, layer_mask, dtype='float32')
971
-
972
- if force_include:
973
- mask = self._force_include(mask, force_include, ds_slice)
1020
+ mask = self._add_layer_to_mask(mask, layer, ds_slice,
1021
+ check_layers,
1022
+ combine_func=np.minimum)
1023
+ for layer in force_include:
1024
+ mask = self._add_layer_to_mask(mask, layer, ds_slice,
1025
+ check_layers,
1026
+ combine_func=np.maximum)
974
1027
 
975
1028
  if self._min_area is not None:
976
1029
  mask = self._area_filter(mask, self._min_area,