Skip to content
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

| Year | Features | Bugfixes | Changes | Maintenance | Docs | Total |
|------|----------|----------|---------|-------------|------|-------|
| 2025 | 39 | 28 | 18 | 36 | 19 | 136 |
| 2025 | 39 | 29 | 18 | 36 | 19 | 137 |
| 2024 | 12 | 17 | 1 | 12 | 1 | 43 |
| 2023 | 11 | 14 | 3 | 9 | 1 | 38 |
| 2022 | 15 | 18 | 0 | 7 | 0 | 40 |
Expand All @@ -35,6 +35,7 @@

### 30 Nov 2025
- [bugfix] Add missing `rcmethod` to CRITICAL_PHYSICS_PARAMS in _check_critical_null_physics_params (config.py).
- [bugfix] Add check on ARCHETYPE_REQUIRED_PARAMS in _validate_stebbs (config.py).

### 29 Nov 2025
- [maintenance] Reorganise `.claude/` directory structure from 7 to 3 directories (#945)
Expand Down
96 changes: 75 additions & 21 deletions src/supy/data_model/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
from datetime import datetime
import pytz

from types import SimpleNamespace

# Optional import of logger - use standalone if supy not available
try:
from ..._env import logger_supy
Expand Down Expand Up @@ -189,6 +191,49 @@ class SUEWSConfig(BaseModel):
"MinimumVolumeOfDHWinUse",
]

ARCHETYPE_REQUIRED_PARAMS: ClassVar[List[str]] = [
"BuildingType",
"BuildingName",
"BuildingCount",
"Occupants",
"stebbs_Height",
"FootprintArea",
"WallExternalArea",
"RatioInternalVolume",
"WWR",
"WallThickness",
"WallEffectiveConductivity",
"WallDensity",
"WallCp",
"WallOuterCapFrac",
"WallExternalEmissivity",
"WallInternalEmissivity",
"WallTransmissivity",
"WallAbsorbtivity",
"WallReflectivity",
"FloorThickness",
"GroundFloorEffectiveConductivity",
"GroundFloorDensity",
"GroundFloorCp",
"WindowThickness",
"WindowEffectiveConductivity",
"WindowDensity",
"WindowCp",
"WindowExternalEmissivity",
"WindowInternalEmissivity",
"WindowTransmissivity",
"WindowAbsorbtivity",
"WindowReflectivity",
"InternalMassDensity",
"InternalMassCp",
"InternalMassEmissivity",
"MaxHeatingPower",
"WaterTankWaterVolume",
"MaximumHotWaterHeatingPower",
"HeatingSetpointTemperature",
"CoolingSetpointTemperature",
]

# Sort the filtered columns numerically
@staticmethod
def sort_key(col):
Expand Down Expand Up @@ -1110,7 +1155,8 @@ def _needs_stebbs_validation(self) -> bool:
def _validate_stebbs(self, site: Site, site_index: int) -> List[str]:
"""
If stebbsmethod==1, enforce that site.properties.stebbs
has all required parameters with non-null values.
and site.properties.building_archetype have all
required parameters with non-null values.
Returns a list of issue messages.
"""
issues: List[str] = []
Expand All @@ -1129,27 +1175,35 @@ def _validate_stebbs(self, site: Site, site_index: int) -> List[str]:
issues.append("Missing 'stebbs' section (required when stebbsmethod=1)")
return issues

stebbs = props.stebbs
## Must have a building_archetype block
if not hasattr(props, "building_archetype") or props.building_archetype is None:
# Do not return early — create an empty container so we can list all
# missing ARCHETYPE_REQUIRED_PARAMS alongside missing stebbs params.
building_archetype = SimpleNamespace()
else:
building_archetype = props.building_archetype

## Check each parameter
missing_params = []
for param in self.STEBBS_REQUIRED_PARAMS:
## Check if parameter exists
if not hasattr(stebbs, param):
missing_params.append(param)
continue
stebbs = props.stebbs

## Get parameter value
param_obj = getattr(stebbs, param)
missing_params: List[str] = []

## Check if the parameter has a value attribute that is None
if hasattr(param_obj, "value") and param_obj.value is None:
missing_params.append(param)
continue
# helper to check and append missing params
def _check_required(container, param_list):
for param in param_list:
# existence
if not hasattr(container, param):
missing_params.append(param)
continue
param_obj = getattr(container, param)
# unwrap any RefValue/Enum wrappers
val = _unwrap_value(param_obj) if param_obj is not None else None
if val is None:
missing_params.append(param)

## If the parameter itself is None
if param_obj is None:
missing_params.append(param)
# Validate stebbs required params
_check_required(stebbs, self.STEBBS_REQUIRED_PARAMS)
# Validate building_archetype required params
_check_required(building_archetype, self.ARCHETYPE_REQUIRED_PARAMS)

## Always list all missing parameters, regardless of count
if missing_params:
Expand Down Expand Up @@ -1748,20 +1802,20 @@ def _collect_land_cover_issues(
# missing_data = any(cut_forcing.isna().any())
# if missing_data:
# raise ValueError("Forcing data contains missing values.")

#
# # Check initial meteorology (for initial_states)
# first_day_forcing = cut_forcing.loc[self.model.control.start_time]
# first_day_min_temp = first_day_forcing.iloc[0]["Tair"]
# first_day_precip = first_day_forcing.iloc[0]["rain"] # Could check previous day if available

#
# # Use min temp for surface temperature states
# for site in self.site:
# for surf_type in SurfaceType:
# surface = getattr(site.initial_states, surf_type)
# surface.temperature.value = [first_day_min_temp]*5
# surface.tsfc.value = first_day_min_temp
# surface.tin.value = first_day_min_temp

#
# # Use precip to determine wetness state
# for site in self.site:
# for surf_type in SurfaceType:
Expand Down
Loading