Skip to content

Feat: check SUEWS dectreeh/evetreeh consistency with SPARTACUS veg_scale/veg_frac#1222

Open
dayantur wants to merge 6 commits intomasterfrom
dayantur/validator/feat/dectreeh-evetreeh-veg-dimensions
Open

Feat: check SUEWS dectreeh/evetreeh consistency with SPARTACUS veg_scale/veg_frac#1222
dayantur wants to merge 6 commits intomasterfrom
dayantur/validator/feat/dectreeh-evetreeh-veg-dimensions

Conversation

@dayantur
Copy link

@dayantur dayantur commented Feb 23, 2026

This PR addresses #1204 and introduces a new conditional validation rule to ensure vertical consistency between urban vegetation layers and tree heights in SUEWS when SPARTACUS is enabled.

It adds logic to check that veg_frac and veg_scale arrays are zero or null above the layer where trees end.

Main Changes

  • Add new conditional rule _validate_spartacus_veg_dimensions in config.py
  • Add description of new conditional rule into PHASE_C_DETAILED.md
  • Add tests for the new conditional rule
  • Update CHANGELOG accordingly

@github-actions
Copy link

github-actions bot commented Feb 23, 2026

CI Build Plan

Changed Files

Python source (2 files)

  • src/supy/data_model/core/config.py
  • src/supy/data_model/validation/pipeline/PHASE_C_DETAILED.md

Tests (4 files)

  • test/data_model/test_validation.py
  • test/fixtures/data_test/AVL_6_310/Input/GridLayouttest.nml
  • test/fixtures/data_test/stebbs_test/sample_config.yml
  • test/fixtures/data_test/stebbs_test/sample_output_stebbs.csv

Documentation (3 files)

  • CHANGELOG.md
  • src/supy/data_model/core/config.py
  • src/supy/data_model/validation/pipeline/PHASE_C_DETAILED.md

Build Configuration

Configuration
Platforms Linux x86_64
Python 3.9, 3.14
Test tier standard (all except slow)
UMEP build Skipped (no ABI changes)
PR status Ready (standard matrix)

Rationale

  • Python source changed -> single-platform build
  • Test files changed -> validation build
  • No compiled extension changes -> UMEP build skipped (nightly provides coverage)

Updated by CI on each push. See path-filters.yml for category definitions.

@github-actions
Copy link

Preview Deployed

Content Preview URL
Site https://suews.io/preview/pr-1222/
Docs https://suews.io/preview/pr-1222/docs/

Note

This preview is ephemeral. It will be lost when:

  • Another PR with site/ or docs/ changes is pushed
  • Changes are merged to master
  • A manual workflow dispatch runs

To restore, push any commit to this PR.

@dayantur dayantur changed the title Feat: check dectreeh/evetreeh consistency with veg_scale/veg_frac dimensions Feat: check dectreeh/evetreeh consistency with veg_scale/veg_frac Feb 24, 2026
@dayantur dayantur marked this pull request as ready for review February 24, 2026 00:29
@dayantur dayantur changed the title Feat: check dectreeh/evetreeh consistency with veg_scale/veg_frac Feat: check SUEWS dectreeh/evetreeh consistency with SPARTACUS veg_scale/veg_frac Feb 24, 2026
@dayantur
Copy link
Author

hi @yiqing1021 :) the only test failing is the stebbs one (test_stebbs_building_energy_outputs). dectreeh and evetreeh cannot be taller (13.1 m at the moment) than SPARTACUS top layer (11 m at the moment). Can you please have a look at you steebs sample config, change the parameters and try to make this test pass? Here to help with anything if needed :) many thanks!

@dayantur
Copy link
Author

hi @sunt05 - this PR is ready for review :) please have a look once you have time - especially the tests, since I had to change one of the .nml to make them green
many thanks!

Copy link

@sunt05 sunt05 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding this validation, @dayantur — ensuring vegetation arrays are physically consistent with tree heights is a valuable check for SPARTACUS users. The orchestration wiring and guard clauses follow the established patterns well, and the fixture updates (GridLayouttest.nml, stebbs_test) are sensible corrections.

Two things need addressing before merge:

The boundary comparison on line 1720 uses strict <, which means that when a tree height exactly equals a layer boundary (e.g. max_tree=15, height=[0, 5, 10, 15, 20]), the layers above the tree go unchecked. Changing to <= fixes this.

The five new tests all pass by hitting the early guard return (no land_cover with tree heights in the stubs), so the core layer-checking loop is never exercised. We need at least one test where trees are present and vegetation is correctly zero above the tree layer, and one where it isn't.

Two minor points are noted in the inline comments below.

# Find the last layer where max_tree < height[layer] (layer index 1..nlayer)
layer_index = None
for i in range(1, len(height_arr)):
if max_tree < height_arr[i]:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be <= rather than <. With strict <, when max_tree exactly equals a layer boundary height, the code picks the wrong layer_index and skips checking layers above the tree.

Example: max_tree=15, height=[0, 5, 10, 15, 20]

  • With <: finds layer_index=4 (at height[4]=20), range(4, 4)=[] — layer 4 (15–20 m) goes unchecked
  • With <=: finds layer_index=3 (at height[3]=15), range(3, 4)=[3] — correctly checks layer 4
Suggested change
if max_tree < height_arr[i]:
if max_tree <= height_arr[i]:

if isinstance(arr, (list, tuple)):
for i in range(layer_index, nlayer):
val = arr[i]
if val != 0:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: val != 0 uses exact float equality. The adjacent _validate_spartacus_sfr uses np.isclose(..., atol=tol) for the same kind of check (lines 1671, 1680). Worth aligning for floating-point robustness:

Suggested change
if val != 0:
if not np.isclose(val, 0, atol=1e-6):

"""
Check that veg_scale and veg_frac are zero above the layer where max_tree falls.
max_tree = max(dectreeh, evetreeh)
The last layer where max_tree < height[layer] (layer index 1..nlayer) is the tree layer.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the docstring says "The last layer where max_tree < height[layer]" but the loop finds the first such layer (it uses break). Suggest changing to "The first layer where…".

Comment on lines +751 to +809
def test_validate_spartacus_veg_dimensions_valid():
"""Test validate_spartacus_veg_dimensions passes with matching veg_frac and nlayer."""
cfg = SUEWSConfig.model_construct()
vertical_layers = SimpleNamespace(
nlayer=2,
veg_frac=[0.3, 0.7],
)
props = SimpleNamespace(vertical_layers=vertical_layers)
site = SimpleNamespace(properties=props, name="TestSite")
msgs = cfg._validate_spartacus_veg_dimensions(site, 0)
assert msgs == []

def test_validate_spartacus_veg_dimensions_too_few_elements():
"""Test validate_spartacus_veg_dimensions detects too few veg_frac elements."""
cfg = SUEWSConfig.model_construct()
vertical_layers = SimpleNamespace(
nlayer=3,
veg_frac=[0.2, 0.8],
)
props = SimpleNamespace(vertical_layers=vertical_layers)
site = SimpleNamespace(properties=props, name="TestSite")
msgs = cfg._validate_spartacus_veg_dimensions(site, 0)
assert msgs == []

def test_validate_spartacus_veg_dimensions_too_many_elements():
"""Test validate_spartacus_veg_dimensions detects too many veg_frac elements."""
cfg = SUEWSConfig.model_construct()
vertical_layers = SimpleNamespace(
nlayer=2,
veg_frac=[0.1, 0.2, 0.7],
)
props = SimpleNamespace(vertical_layers=vertical_layers)
site = SimpleNamespace(properties=props, name="TestSite")
msgs = cfg._validate_spartacus_veg_dimensions(site, 0)
assert msgs == []

def test_validate_spartacus_veg_dimensions_missing_veg_frac():
"""Test validate_spartacus_veg_dimensions handles missing veg_frac gracefully."""
cfg = SUEWSConfig.model_construct()
vertical_layers = SimpleNamespace(
nlayer=2,
# veg_frac missing
)
props = SimpleNamespace(vertical_layers=vertical_layers)
site = SimpleNamespace(properties=props, name="TestSite")
msgs = cfg._validate_spartacus_veg_dimensions(site, 0)
assert msgs == []

def test_validate_spartacus_veg_dimensions_missing_nlayer():
"""Test validate_spartacus_veg_dimensions handles missing nlayer gracefully."""
cfg = SUEWSConfig.model_construct()
vertical_layers = SimpleNamespace(
veg_frac=[0.5, 0.5],
# nlayer missing
)
props = SimpleNamespace(vertical_layers=vertical_layers)
site = SimpleNamespace(properties=props, name="TestSite")
msgs = cfg._validate_spartacus_veg_dimensions(site, 0)
assert msgs == []
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All five tests pass via the early if not tree_heights: return guard because none of the stubs include land_cover with dectreeh/evetreeh. The core loop (config.py L1718–1743) is never reached.

Suggested additions:

  • Passing case: stub with dectreeh=12, height=[0, 5, 10, 15, 20], veg_frac=[0.3, 0.3, 0.2, 0, 0] — should return []
  • Failing case: same but veg_frac=[0.3, 0.3, 0.2, 0.1, 0] — should flag veg_frac[3]
  • Boundary case: max_tree exactly on a layer boundary
  • Exceeds-all case: max_tree=100 with height=[0, 5, 10] — should produce the "exceeds" message

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants