Skip to content

Commit d5f5075

Browse files
authored
Add complexipy (#326)
* Add dep * Update lockfile * Update workflow * Fix incorrect directory * Refactor * Split out warnings * Refactor * Refactor * Refactor * Refactor * Add directive * Refactor * Refactor * Minor refactor * Add ignore directive
1 parent 93b747d commit d5f5075

13 files changed

Lines changed: 9279 additions & 8913 deletions

File tree

.github/workflows/ci-python.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,30 @@ on:
1616

1717
jobs:
1818
lint:
19-
name: Lint Python Code Base with Ruff
19+
name: Lint Python Code Base
2020
runs-on: ubuntu-latest
2121
steps:
2222
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23-
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v3.6.1
23+
- name: Lint Python Code (Ruff)
24+
uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v3.6.1
2425
with:
2526
version: "latest"
2627
args: "check"
2728
src: "./revrt"
28-
- uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v3.6.1
29+
- name: Check Python Code Format (Ruff)
30+
uses: astral-sh/ruff-action@278981a28ce3188b1e39527901f38254bf3aac89 # v3.6.1
2931
with:
3032
version: "latest"
3133
args: "format --check"
3234
src: "./revrt"
35+
- name: Check Python Code Complexity (Complexipy)
36+
uses: rohaquinlop/complexipy-action@e2b05bcc06d899a24e2b6bb8b1354ac42800a95e # v7.0.1
37+
with:
38+
paths: "./revrt"
39+
max_complexity_allowed: 10
40+
failed: false # true
41+
sort: desc
42+
ignore_complexity: false # Set to true to ignore complexity checks
3343

3444
locked-tests:
3545
needs: lint

pixi.lock

Lines changed: 8958 additions & 8748 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ dependencies = [
5353

5454
[project.optional-dependencies]
5555
dev = [
56+
"complexipy>=7.0.1,<8",
5657
"contextily>=1.7.0,<2",
5758
"datashader>=0.19.0,<0.20",
5859
"geoviews>=1.15.1,<2",
@@ -294,6 +295,9 @@ rust-src = ">=1.96.1,<1.97"
294295
maturin = ">=1.13.1,<2"
295296
rattler-build = ">=0.69.1,<0.70"
296297

298+
[tool.pixi.feature.dev.pypi-dependencies]
299+
complexipy = ">=7.0.1,<8"
300+
297301
[tool.pixi.feature.test.dependencies]
298302
hypothesis = ">=6.152.1,<7"
299303
pytest = ">=9.0.0,<9.1"
@@ -382,3 +386,12 @@ omit = [
382386
[tool.pytest.ini_options]
383387
addopts = "--disable-warnings"
384388
testpaths = ["tests/python/unit", "tests/python/integration"]
389+
390+
391+
[tool.complexipy]
392+
paths = ["revrt"]
393+
max-complexity-allowed = 10
394+
exclude = ["tests/**"]
395+
failed = true
396+
sort = "desc"
397+
check-script = true

revrt/costs/dry_costs_creator.py

Lines changed: 66 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,40 @@ def _compute_multipliers(
319319
default_multipliers=None,
320320
):
321321
"""Create costs multiplier raster"""
322+
multipliers, regions_mask = self._process_iso_multipliers(
323+
iso_multipliers,
324+
iso_lookup,
325+
iso_layer,
326+
land_use_layer,
327+
slope_layer,
328+
land_use_classes,
329+
)
330+
if default_multipliers is not None:
331+
multipliers = self._process_default_region(
332+
multipliers,
333+
regions_mask,
334+
default_multipliers,
335+
land_use_layer,
336+
slope_layer,
337+
land_use_classes,
338+
)
339+
340+
# Set water multiplier last so we don't get super high
341+
# multipliers at water body boundaries next to steep slopes
342+
return da.where(
343+
land_use_layer == WATER_NLCD_CODE, WATER_MULTIPLIER, multipliers
344+
)
345+
346+
def _process_iso_multipliers(
347+
self,
348+
iso_multipliers,
349+
iso_lookup,
350+
iso_layer,
351+
land_use_layer,
352+
slope_layer,
353+
land_use_classes,
354+
):
355+
"""Create ISO multipliers"""
322356
multipliers = da.ones(
323357
self.shape, dtype=self._dtype, chunks=self.chunks
324358
)
@@ -357,43 +391,42 @@ def _compute_multipliers(
357391
multipliers = da.where(
358392
mask, multipliers * slope_multipliers, multipliers
359393
)
394+
return multipliers, regions_mask
360395

361-
# Calculate multipliers for regions not defined in `config`
396+
def _process_default_region(
397+
self,
398+
multipliers,
399+
regions_mask,
400+
default_multipliers,
401+
land_use_layer,
402+
slope_layer,
403+
land_use_classes,
404+
):
405+
"""Calculate multipliers for regions not defined in `config`"""
362406
logger.debug("Processing default region")
363-
if default_multipliers is not None:
364-
default_mask = ~regions_mask
407+
default_mask = ~regions_mask
365408

366-
if "land_use" in default_multipliers:
367-
region_land_use = da.where(
368-
default_mask, land_use_layer, da.nan
369-
)
370-
lum_dict = default_multipliers["land_use"]
371-
lum = compute_land_use_multipliers(
372-
region_land_use,
373-
lum_dict,
374-
land_use_classes,
375-
chunks=self.chunks,
376-
)
377-
multipliers = da.where(
378-
default_mask, multipliers * lum, multipliers
379-
)
380-
381-
if "slope" in default_multipliers:
382-
region_slope = da.where(default_mask, slope_layer, da.nan)
383-
slope_multipliers = compute_slope_multipliers(
384-
region_slope,
385-
chunks=self.chunks,
386-
config=default_multipliers["slope"],
387-
)
388-
multipliers = da.where(
389-
default_mask, multipliers * slope_multipliers, multipliers
390-
)
409+
if "land_use" in default_multipliers:
410+
region_land_use = da.where(default_mask, land_use_layer, da.nan)
411+
lum_dict = default_multipliers["land_use"]
412+
lum = compute_land_use_multipliers(
413+
region_land_use, lum_dict, land_use_classes, chunks=self.chunks
414+
)
415+
multipliers = da.where(
416+
default_mask, multipliers * lum, multipliers
417+
)
391418

392-
# Set water multiplier last so we don't get super high
393-
# multipliers at water body boundaries next to steep slopes
394-
return da.where(
395-
land_use_layer == WATER_NLCD_CODE, WATER_MULTIPLIER, multipliers
396-
)
419+
if "slope" in default_multipliers:
420+
region_slope = da.where(default_mask, slope_layer, da.nan)
421+
slope_multipliers = compute_slope_multipliers(
422+
region_slope,
423+
chunks=self.chunks,
424+
config=default_multipliers["slope"],
425+
)
426+
multipliers = da.where(
427+
default_mask, multipliers * slope_multipliers, multipliers
428+
)
429+
return multipliers
397430

398431
def _compute_base_line_costs(
399432
self, capacity, base_line_costs, iso_layer, iso_lookup

revrt/costs/layer_creator.py

Lines changed: 58 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -452,31 +452,8 @@ def _process_forced_inclusions(self, data, fi_layers, tiff_chunks="file"):
452452
fi = da.zeros(self.shape, dtype=self._dtype, chunks=self.chunks)
453453

454454
for fname, config in fi_layers.items():
455-
if Path(fname).suffix.lower() not in TIFF_EXTENSIONS:
456-
msg = (
457-
f"Forced inclusion file {fname!r} does not end with .tif."
458-
" GeoTIFFs are the only format allowed for forced "
459-
"inclusions."
460-
)
461-
raise revrtValueError(msg)
462-
463-
global_value_given = config.global_value is not None
464-
map_given = config.map is not None
465-
range_given = config.bins is not None
466-
rasterize_given = config.rasterize is not None
467-
bad_input_given = (
468-
global_value_given
469-
or map_given
470-
or range_given
471-
or rasterize_given
472-
)
473-
if bad_input_given:
474-
msg = (
475-
"`global_value`, `map`, `bins`, and `rasterize` are "
476-
"not allowed if `forced_inclusion` is True, but one "
477-
f"was found in config: {fname!r}: {config}"
478-
)
479-
raise revrtValueError(msg)
455+
_validate_fi_file_extension(fname)
456+
_validate_fi_config_input(config, fname)
480457

481458
# Past guard clauses, process FI
482459
if config.extent != ALL:
@@ -550,6 +527,35 @@ def _check_tiff_layer_config(config, fname):
550527
raise revrtValueError(msg)
551528

552529

530+
def _validate_fi_file_extension(fname):
531+
"""Validate that the forced inclusion file has correct extension"""
532+
if Path(fname).suffix.lower() not in TIFF_EXTENSIONS:
533+
msg = (
534+
f"Forced inclusion file {fname!r} does not end with .tif."
535+
" GeoTIFFs are the only format allowed for forced "
536+
"inclusions."
537+
)
538+
raise revrtValueError(msg)
539+
540+
541+
def _validate_fi_config_input(config, fname):
542+
"""Validate that the forced inclusion config is correct"""
543+
global_value_given = config.global_value is not None
544+
map_given = config.map is not None
545+
range_given = config.bins is not None
546+
rasterize_given = config.rasterize is not None
547+
bad_input_given = (
548+
global_value_given or map_given or range_given or rasterize_given
549+
)
550+
if bad_input_given:
551+
msg = (
552+
"`global_value`, `map`, `bins`, and `rasterize` are "
553+
"not allowed if `forced_inclusion` is True, but one "
554+
f"was found in config: {fname!r}: {config}"
555+
)
556+
raise revrtValueError(msg)
557+
558+
553559
def _validate_bin_range(bins):
554560
"""Check for correctness in bin range"""
555561
for input_bin in bins:
@@ -570,26 +576,38 @@ def _validate_bin_continuity(bins):
570576
sorted_bins = sorted(bins, key=lambda x: x.min)
571577
last_max = float("-inf")
572578
for i, input_bin in enumerate(sorted_bins):
573-
if input_bin.min < last_max:
574-
last_bin = sorted_bins[i - 1] if i > 0 else "-infinity"
575-
msg = (
576-
"Overlapping bins detected between "
577-
f"{last_bin!r} and {input_bin!r}"
578-
)
579-
warn(msg, revrtWarning)
579+
last_bin = sorted_bins[i - 1] if i > 0 else "-infinity"
580580

581-
if input_bin.min > last_max:
582-
last_bin = sorted_bins[i - 1] if i > 0 else "-infinity"
583-
msg = f"Gap detected between {last_bin!r} and {input_bin!r}"
584-
warn(msg, revrtWarning)
585-
586-
if i + 1 == len(sorted_bins) and input_bin.max < float("inf"):
587-
msg = f"Gap detected between {input_bin!r} and 'infinity'"
588-
warn(msg, revrtWarning)
581+
_warn_about_overlapping_bins(input_bin, last_bin, last_max)
582+
_warn_about_gap_in_bins(input_bin, last_bin, last_max)
583+
_warn_about_unbounded_bins(i, sorted_bins, input_bin)
589584

590585
last_max = input_bin.max
591586

592587

588+
def _warn_about_overlapping_bins(input_bin, last_bin, last_max):
589+
"""Warn about overlapping bins"""
590+
if input_bin.min < last_max:
591+
msg = (
592+
f"Overlapping bins detected between {last_bin!r} and {input_bin!r}"
593+
)
594+
warn(msg, revrtWarning)
595+
596+
597+
def _warn_about_gap_in_bins(input_bin, last_bin, last_max):
598+
"""Warn about gaps in bin continuity"""
599+
if input_bin.min > last_max:
600+
msg = f"Gap detected between {last_bin!r} and {input_bin!r}"
601+
warn(msg, revrtWarning)
602+
603+
604+
def _warn_about_unbounded_bins(i, sorted_bins, input_bin):
605+
"""Warn if the last bin is not unbounded"""
606+
if i + 1 == len(sorted_bins) and input_bin.max < float("inf"):
607+
msg = f"Gap detected between {input_bin!r} and 'infinity'"
608+
warn(msg, revrtWarning)
609+
610+
593611
def _vector_raster_dtype(burn_value, default_dtype):
594612
"""Choose a compact dtype for vector rasterization"""
595613
try:

revrt/routing/base.py

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,22 @@ def _empty_cost_layer_data_array(self):
235235

236236
def _build_cost_layer_from_option(self, option, config):
237237
"""Build a single routing option's cost layer"""
238+
cost_layers = self._build_cost_layers_single_option(config, option)
239+
scaled_layers = self._scale_single_option_cost_layers(
240+
config, *cost_layers
241+
)
242+
option_cost, option_li_cost, option_untracked_cost = scaled_layers
243+
244+
self.costs[option] = option_cost
245+
self.li_costs[option] = option_li_cost
246+
self._build_final_routing_layer_from_option(
247+
option,
248+
config,
249+
option_cost + option_li_cost + option_untracked_cost,
250+
)
251+
252+
def _build_cost_layers_single_option(self, config, option):
253+
"""Build cost layers for a single routing option"""
238254
option_cost = self._empty_cost_layer_data_array()
239255
option_li_cost = self._empty_cost_layer_data_array()
240256
option_untracked_cost = self._empty_cost_layer_data_array()
@@ -243,14 +259,10 @@ def _build_cost_layer_from_option(self, option, config):
243259
cost = self._extract_and_scale_layer(layer_info)
244260
cost.values = da.where(cost > 0, cost, 0)
245261
is_li = layer_info.get("is_invariant", False)
246-
if layer_info.get("include_in_final_cost", True):
247-
if is_li:
248-
option_li_cost += cost
249-
else:
250-
option_cost += cost
251-
else:
252-
option_untracked_cost += cost
253-
262+
base_cost_layer = _select_base_cost_layer(
263+
layer_info, option_li_cost, option_cost, option_untracked_cost
264+
)
265+
base_cost_layer += cost
254266
if layer_info.get("include_in_report", True):
255267
report_key = (layer_info["layer_name"], is_li)
256268
reported_costs[report_key] = (
@@ -267,6 +279,12 @@ def _build_cost_layer_from_option(self, option, config):
267279
)
268280
)
269281

282+
return option_cost, option_li_cost, option_untracked_cost
283+
284+
def _scale_single_option_cost_layers(
285+
self, config, option_cost, option_li_cost, option_untracked_cost
286+
):
287+
"""Scale the cost layers for a single routing option"""
270288
mult = config.get("cost_multiplier_scalar", 1) or 1
271289
option_cost *= mult
272290
option_li_cost *= mult
@@ -281,13 +299,7 @@ def _build_cost_layer_from_option(self, option, config):
281299
option_li_cost *= multiplier
282300
option_untracked_cost *= multiplier
283301

284-
self.costs[option] = option_cost
285-
self.li_costs[option] = option_li_cost
286-
self._build_final_routing_layer_from_option(
287-
option,
288-
config,
289-
option_cost + option_li_cost + option_untracked_cost,
290-
)
302+
return option_cost, option_li_cost, option_untracked_cost
291303

292304
def _build_final_routing_layer_from_option(
293305
self, option, config, option_layer
@@ -828,3 +840,14 @@ def _driver_zones_for_rust(zones):
828840
out_zone["mask_operator"] = mask_operator
829841
out_zone["mask_threshold"] = mask_threshold
830842
yield out_zone
843+
844+
845+
def _select_base_cost_layer(
846+
layer_info, option_li_cost, option_cost, option_untracked_cost
847+
):
848+
"""Select the appropriate base cost layer based on config"""
849+
if layer_info.get("include_in_final_cost", True):
850+
if layer_info.get("is_invariant", False):
851+
return option_li_cost
852+
return option_cost
853+
return option_untracked_cost

0 commit comments

Comments
 (0)