Settings: how calculate() resolves configuration¶
solweig.calculate() accepts configuration from three sources:
- Per-call keyword arguments —
use_anisotropic_sky=True,wall_material="brick",conifer=False, etc. - A
ModelConfigobject passed as theconfig=keyword. - Defaults baked into the library (e.g. anisotropic sky on,
max_shadow_distance_m=1000.0).
When the same setting is specified at more than one level, the
precedence is: per-call kwarg > ModelConfig > default. A
None at any level means "inherit from the next level down."
This is implemented by
solweig.models.settings.Settings.resolve().
You usually don't construct Settings directly — calculate() does
it internally — but knowing the rules helps reason about which value
"wins" when sources disagree.
Example¶
import solweig
from solweig.models.config import HumanParams, ModelConfig
# A reusable config: anisotropic off, conservative shadow distance.
cfg = ModelConfig(
use_anisotropic_sky=False,
max_shadow_distance_m=500.0,
human=HumanParams(abs_k=0.6), # custom shortwave absorption
)
# Call A — pure config:
solweig.calculate(surface, weather, location, config=cfg, output_dir="a/")
# Effective: aniso=False, max_shadow=500, abs_k=0.6.
# Call B — kwargs override one field:
solweig.calculate(
surface, weather, location,
config=cfg,
use_anisotropic_sky=True, # overrides cfg.use_anisotropic_sky
output_dir="b/",
)
# Effective: aniso=True (kwarg), max_shadow=500 (config), abs_k=0.6 (config).
# Call C — kwarg explicitly None falls through to config:
solweig.calculate(
surface, weather, location,
config=cfg,
use_anisotropic_sky=None, # no override, defer to config
output_dir="c/",
)
# Effective: same as Call A — aniso=False.
The fields Settings carries¶
| Field | Type | Default | Notes |
|---|---|---|---|
use_anisotropic_sky |
bool | True | Perez sky model |
conifer |
bool | False | Treat all vegetation as evergreen (leaf-on year-round) |
wall_material |
str | None | None | "brick", "concrete", "wood", "cobblestone", or None to use bundled wall defaults |
max_shadow_distance_m |
float | 1000.0 | Caps horizontal shadow ray reach + tile overlap |
human |
HumanParams | HumanParams() | Body geometry + absorption coefficients |
physics |
SimpleNamespace | None | None (lazy-loaded from bundled JSON) | Tree settings, posture |
materials |
SimpleNamespace | None | None (lazy-loaded from bundled JSON) | Land-cover albedo / emissivity / thermal coefficients |
physics and materials are intentionally kept as None after
Settings.resolve() — the underlying JSON files are only loaded
when downstream code actually needs them. Call
.with_loaded_defaults() to force the load explicitly.
Why a separate Settings exists¶
Before 0.1.0b85, the configuration merge happened inline at the top of
api._calculate_single — a 50-line block with one if effective_X is
None chain per field. It was correct but hard to test in isolation and
hostile to extension.
Settings.resolve() packages the merge into a single typed function
with explicit semantics, comprehensive tests
(tests/test_settings_merge.py),
and a frozen dataclass for the resolved result. Adding a new setting
is now a one-line dataclass field + one line in resolve().
If you need to inspect the effective settings for a call, you can build them yourself: