Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3f37fef
add seasonal adjustment of vegetation albedo alb_id
dayantur Feb 19, 2026
bde81c6
squeezed/optimised code for new seasonal adjustment
dayantur Feb 19, 2026
e0da107
remove test yaml
dayantur Feb 19, 2026
9092226
add minimal tests fro new seasonal adjustments
dayantur Feb 19, 2026
1090b11
Update PHASE_B_DETAILED.md
dayantur Feb 19, 2026
25782f1
Update CHANGELOG.md
dayantur Feb 19, 2026
93b9c68
Merge branch 'master' into dayantur/validator/seasonal-adjustment-of-…
dayantur Mar 11, 2026
a3c7661
Update CHANGELOG.md
dayantur Mar 11, 2026
7cb5acc
Merge branch 'master' into dayantur/validator/seasonal-adjustment-of-…
dayantur Mar 12, 2026
c252bd5
remove duplicate
dayantur Mar 12, 2026
1afbbfa
Update CHANGELOG.md
dayantur Mar 12, 2026
8a7e9fb
typos in PHASE_B_DETAILED.md
dayantur Mar 12, 2026
ccffffc
add guard for surf_key not in initial_states
dayantur Mar 12, 2026
a5a8bd7
split logic into lines
dayantur Mar 12, 2026
c746e67
Update CHANGELOG.md
dayantur Mar 12, 2026
bf123bc
Merge branch 'master' into dayantur/validator/seasonal-adjustment-of-…
dayantur Mar 18, 2026
65112e8
replace strict == with math.isclose
dayantur Mar 18, 2026
fa7a258
refactor _get_range_and_id and _set_alb_id (outside adjust_seasonal_p…
dayantur Mar 18, 2026
5f740e1
refactor _set_alb_id
dayantur Mar 18, 2026
5785f33
dropping the grass assertion for the midseason case.
dayantur Mar 18, 2026
69c4489
Skip albedo adjustment if surface fraction is zero
dayantur Mar 19, 2026
2512f38
Update test_validation.py
dayantur Mar 19, 2026
db4c2c9
Style: add blank lines between top-level helpers, simplify sfr guard
sunt05 Mar 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ EXAMPLES:

| Year | Features | Bugfixes | Changes | Maintenance | Docs | Total |
|------|----------|----------|---------|-------------|------|-------|
| 2026 | 65 | 78 | 26 | 79 | 39 | 287 |
| 2026 | 66 | 78 | 26 | 79 | 40 | 289 |
| 2025 | 60 | 68 | 22 | 71 | 36 | 256 |
| 2024 | 12 | 17 | 1 | 12 | 1 | 43 |
| 2023 | 11 | 14 | 3 | 9 | 1 | 38 |
Expand All @@ -62,6 +62,11 @@ EXAMPLES:
- [bugfix] Guard SPARTACUS LW solver NaN from matrix singularity in certain urban canopy geometries (#1212)
- [changes] Removed internal-only parameters (diagnose, dqndt, dqnsdt, dt_since_start, lenday_id, qn_av, qn_s_av, tair_av, tmax_id, tmin_id, tstep_prev, snowfallcum) from sample_config.yml. (PR #1216)

### 19 Feb 2026

- [feature] Add update logic under seasonal adjustments in phase_b.py to handle different alb_id behaviour across vegetated surface types (PR #1211).
Comment thread
dayantur marked this conversation as resolved.
Outdated
- [doc] Updated PHASE_B_DETAILED.md with new seasonal adjustment logic (PR #1211).

### 18 Feb 2026

- [feature][experimental] Add attribution module for diagnosing T2, q2, and U10 changes by decomposing model output into physical process contributions, with diurnal cycle and heatmap visualisation helpers (#918)
Expand Down
8 changes: 8 additions & 0 deletions src/supy/data_model/validation/pipeline/PHASE_B_DETAILED.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,14 @@ Phase B makes scientific adjustments that improve model realism without changing
- **Fraction Normalisation**: Adjusts surface fractions to sum to 1.0 by rounding the surface with maximum fraction value
- **Seasonal LAI Adjustments**: Calculates LAI for deciduous trees based on seasonal parameters (laimin, laimax) when surface fraction > 0. When surface fraction is 0, existing lai_id values are preserved and validation is skipped with a warning

### Seasonal Vegetation Albedo Adjustments

- **Season‑Aware `alb_id`**: Updates `initial_states.*.alb_id` for grass, dectr, evetr
Comment thread
dayantur marked this conversation as resolved.
Outdated
- **Summer Regime**: `alb_id(grass) = alb_min(grass)`; `alb_id(dectr/evetr) = alb_max(dectr/evetr)`
- **Winter Regime**: `alb_id(grass) = alb_max(grass)`; `alb_id(dectr/evetr) = alb_min(dectr/evetr)`
- **Transition Seasons**: `alb_id` set to midpoint `(alb_min + alb_max)/2` for grass, dectr, evetr


### STEBBS Method Integration

- **Conditional Logic**: When `stebbsmethod == 0`, nullifies STEBBS parameters
Expand Down
52 changes: 52 additions & 0 deletions src/supy/data_model/validation/pipeline/phase_b.py
Original file line number Diff line number Diff line change
Expand Up @@ -1662,6 +1662,58 @@ def adjust_seasonal_parameters(
# Note: When sfr=0, we don't nullify lai_id - we simply skip validation
# The warning "Parameters not checked because surface fraction is 0" covers this

# Seasonal adjustment of vegetation albedo ranges (alb_id only)
land_cover = props.get("land_cover", {})
Comment thread
dayantur marked this conversation as resolved.
Outdated
vegetated_surfaces = (
("grass", "grass"),
("dectr", "deciduous trees"),
("evetr", "evergreen trees"),
)

def _get_range_and_id(surf_key: str):
surf_props = land_cover.get(surf_key, {})
surf_state = initial_states.get(surf_key, {})
alb_min = surf_props.get("alb_min", {}).get("value") if isinstance(surf_props.get("alb_min", {}), dict) else surf_props.get("alb_min")
Comment thread
dayantur marked this conversation as resolved.
Outdated
alb_max = surf_props.get("alb_max", {}).get("value") if isinstance(surf_props.get("alb_max", {}), dict) else surf_props.get("alb_max")
alb_id = surf_state.get("alb_id", {}).get("value") if isinstance(surf_state.get("alb_id", {}), dict) else surf_state.get("alb_id")
return alb_min, alb_max, alb_id

def _set_alb_id(surf_key: str, new_alb_id: Optional[float], label: str):
if new_alb_id is None:
return
surf_state = initial_states.get(surf_key, {})
Comment thread
dayantur marked this conversation as resolved.
Outdated
if not isinstance(surf_state, dict):
return
old_val = surf_state.get("alb_id", {}).get("value") if isinstance(surf_state.get("alb_id", {}), dict) else surf_state.get("alb_id")
Comment thread
dayantur marked this conversation as resolved.
Outdated
if old_val == new_alb_id:
Comment thread
dayantur marked this conversation as resolved.
Outdated
return
surf_state["alb_id"] = {"value": new_alb_id}
adjustments.append(
ScientificAdjustment(
parameter=f"{surf_key}.alb_id",
site_index=site_idx,
site_gridid=site_gridid,
old_value=str(old_val),
new_value=str(new_alb_id),
reason=f"Set seasonal albedo for {season} on {label} based on (alb_min, alb_max)",
)
)

for surf_key, label in vegetated_surfaces:
Comment thread
dayantur marked this conversation as resolved.
Outdated
alb_min, alb_max, alb_id_val = _get_range_and_id(surf_key)
if alb_min is None or alb_max is None:
continue
if season in ("summer", "tropical", "equatorial"):
target = alb_min if surf_key == "grass" else alb_max
elif season == "winter":
target = alb_max if surf_key == "grass" else alb_min
else:
if alb_id_val is None:
continue
target = 0.5 * (alb_min + alb_max)
_set_alb_id(surf_key, target, label)


if lat is not None and lng is not None:
try:
dls = DLSCheck(lat=lat, lng=lng, year=model_year)
Expand Down
123 changes: 122 additions & 1 deletion test/data_model/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@
)
from supy.data_model.core.type import RefValue
from supy.data_model.validation.core.utils import check_missing_params
from supy.data_model.validation.pipeline.phase_b import validate_model_option_samealbedo
from supy.data_model.validation.pipeline.phase_b import validate_model_option_samealbedo, adjust_seasonal_parameters
import copy




Expand Down Expand Up @@ -1295,6 +1297,125 @@ def test_phase_b_deep_soil_temperature_missing_stebbs():
assert len(annual_temp_adjustments) == 0


def _make_minimal_phase_b_yaml_for_albedo(lat: float) -> dict:
"""Build a minimal Phase-B-style YAML dict with vegetation albedo ranges and alb_id."""
return {
"sites": [
{
"name": "TestSite",
"gridiv": 1,
"properties": {
"lat": {"value": lat},
"lng": {"value": 0.0},
"land_cover": {
"grass": {
"sfr": {"value": 0.1},
"alb_min": {"value": 0.10},
"alb_max": {"value": 0.20},
},
"dectr": {
"sfr": {"value": 0.1},
"alb_min": {"value": 0.12},
"alb_max": {"value": 0.30},
},
"evetr": {
"sfr": {"value": 0.1},
"alb_min": {"value": 0.14},
"alb_max": {"value": 0.40},
},
},
},
"initial_states": {
"grass": {"alb_id": {"value": 0.15}},
"dectr": {"alb_id": {"value": 0.18}},
"evetr": {"alb_id": {"value": 0.25}},
},
}
]
}


def _get_phase_b_alb_ids(yaml_data: dict):
site = yaml_data["sites"][0]
ist = site["initial_states"]
return (
ist["grass"]["alb_id"]["value"],
ist["dectr"]["alb_id"]["value"],
ist["evetr"]["alb_id"]["value"],
)


def test_phase_b_seasonal_albedo_summer_updates_alb_id_from_ranges():
"""Summer: grass.alb_id -> alb_min, dectr/evetr.alb_id -> alb_max."""
# Northern Hemisphere, DOY in summer window: 2017-07-15
yaml_data = _make_minimal_phase_b_yaml_for_albedo(lat=51.5)
yaml_before = copy.deepcopy(yaml_data)

updated, adjustments = adjust_seasonal_parameters(
yaml_data, start_date="2017-07-15", model_year=2017
)

grass_id, dectr_id, evetr_id = _get_phase_b_alb_ids(updated)

# Expected from ranges defined above
assert grass_id == pytest.approx(0.10) # alb_min(grass)
assert dectr_id == pytest.approx(0.30) # alb_max(dectr)
assert evetr_id == pytest.approx(0.40) # alb_max(evetr)

# We should have ScientificAdjustment entries for each
params = {a.parameter for a in adjustments}
assert "grass.alb_id" in params
assert "dectr.alb_id" in params
assert "evetr.alb_id" in params

# Land-cover ranges themselves must be unchanged
lc_before = yaml_before["sites"][0]["properties"]["land_cover"]
lc_after = updated["sites"][0]["properties"]["land_cover"]
assert lc_after == lc_before


def test_phase_b_seasonal_albedo_winter_updates_alb_id_from_ranges():
"""Winter: grass.alb_id -> alb_max, dectr/evetr.alb_id -> alb_min."""
# Northern Hemisphere, DOY in winter window: 2017-01-15
yaml_data = _make_minimal_phase_b_yaml_for_albedo(lat=51.5)

updated, adjustments = adjust_seasonal_parameters(
yaml_data, start_date="2017-01-15", model_year=2017
)

grass_id, dectr_id, evetr_id = _get_phase_b_alb_ids(updated)

assert grass_id == pytest.approx(0.20) # alb_max(grass)
assert dectr_id == pytest.approx(0.12) # alb_min(dectr)
assert evetr_id == pytest.approx(0.14) # alb_min(evetr)

params = {a.parameter for a in adjustments}
assert "grass.alb_id" in params
assert "dectr.alb_id" in params
assert "evetr.alb_id" in params


def test_phase_b_seasonal_albedo_midseason_sets_alb_id_midpoint():
"""Spring/fall: alb_id -> (alb_min + alb_max)/2 for all veg surfaces."""
# Northern Hemisphere, DOY in spring window: 2017-04-01
yaml_data = _make_minimal_phase_b_yaml_for_albedo(lat=51.5)

updated, adjustments = adjust_seasonal_parameters(
yaml_data, start_date="2017-04-01", model_year=2017
)

grass_id, dectr_id, evetr_id = _get_phase_b_alb_ids(updated)

# Expected midpoints
assert grass_id == pytest.approx((0.10 + 0.20) / 2) # 0.15
assert dectr_id == pytest.approx((0.12 + 0.30) / 2) # 0.21
assert evetr_id == pytest.approx((0.14 + 0.40) / 2) # 0.27

params = {a.parameter for a in adjustments}
Comment thread
dayantur marked this conversation as resolved.
assert "grass.alb_id" in params
assert "dectr.alb_id" in params
assert "evetr.alb_id" in params

# =====================================================================
# Phase A: nlayer dimension validation tests
# =====================================================================
Expand Down
Loading