All notable changes to Urban Solar Carver are documented in this file.
Format follows Keep a Changelog, versioning follows Semantic Versioning.
- Solar-usefulness generator (simulated benefit weights) —
usc usefulnessand theurbansolarcarver.usefulnessmodule: an ISO 13790 Annex C simple hourly ("5R1C") thermal model plus batched central-difference perturbation, producing hourly marginal solar usefulness —benefit[t](fraction of a marginal joule of solar gain that offsets heating over the year) andharm[t](fraction that becomes cooling load) — written to asolar_usefulness.jsonartifact with full provenance metadata. Inputs: one EPW plus ~10 archetype parameters (configs/archetype_example.yaml; neutral values, no national defaults). The engine is validated against ETH's RC_BuildingSimulator on three archetypes under the bundled Golden TMY3 weather (annual demands and hourly node states; driving arrays stored verbatim intests/data/), plus steady-state closure against an independent network reduction. Design, tier ladder, and declared limitations:design/solar-usefulness.md. usefulness_path(benefit mode): consume a solar-usefulness artifact in place of the balance-point hour filter — each hour's DNI/DHI is scaled bybenefit[t]before the cumulative sky matrix (the balance filter is the binary special case of this mechanism), andinclude_harm: truesubtracts theharm[t]-scaled matrix clipped at zero. Analysis-period hours outside the period are zero-weighted.balance_temperature/balance_offsetare unused (warns if customized); artifact provenance is recorded in preprocessing diagnostics and the patch-weight cache keys on artifact content. Occupancy: archetypes accept a flat internal-gains value or a 24-value daily profile.- Shoebox archetype shorthand: instead of explicit areas, an archetype
may give
width/length/heightplus per-facadewwr(window-to-wall ratio),g_value, and optionalorientation— floor area, volume, per-facade window areas,area_window, andarea_opaque(opaque walls + roof) are derived.configs/archetype_example.yamlshows both forms. - Weights preview:
usc usefulness/generate_usefulnesswrite a.pngcompanion next to the artifact — benefit and harm as day x hour heatmaps — so the schedule can be inspected without writing code. - Sky-weight comparison in diagnostics: a benefit run that consumes a
usefulness artifact (with
diagnostic_plots: true) also records a difference dome versus the simple balance-point rule for the same config (sky_patch_comparison_image+ redistribution stats indiagnostic.json). - Tutorial 5 (
examples/5_benefit_5r1c.ipynb): simulated benefit weights end-to-end — archetype, weights generation and inspection, simple-vs-simulated carve comparison, harm channel, provenance. - The sample Golden CO TMY3 EPW is now actually committed
(
.gitignoreexception) — the tutorials and EPW-dependent tests run out of the box as documented.
- Benefit mode now implements its documented formula exactly (by
default): patch weights are a plain cumulative irradiance matrix over
the hours below
balance_temperature − balance_offset(Heaviside filter on EPW dry-bulb). Previously the weights were always Ladybug's signed benefit−harm net clipped at zero, which silently let hot-hour radiation erode winter weights and made results depend on whether the analysis period included summer. A full-year analysis period is now safe and natural — the balance filter selects the beneficial hours by itself. The composite remains available as a declared, opt-in experiment:include_harm: truesubtracts hot-hour radiation per patch (clipped at zero, dead-band hours count for neither side), with its period-dependence documented. Shading value is deliberately never a carving force — with signed retention, every voxel along a hot-hour ray earns shade credit, so the "optimal" envelope keeps whole sun-path prisms (cave-like volumes). balance_temperaturedefault changed from 20 to 15 °C (Ladybug's default; typical range ~12 commercial to ~18 residential, higher for older poorly insulated stock). Derive project-specific values via the Honeybee/E+ balance-point workflow.balance_offsetnow validates>= 0(a negative offset silently inverted the dead-band into inverted hour classification).- User-facing terminology for the two benefit weightings: "simple weights"
(balance-point rule) vs "simulated weights" (5R1C hourly simulation);
the internal "Tier 1.5" label no longer appears in docs, CLI, or configs.
generate_tier15is renamedgenerate_usefulness(old name kept as a compatible alias). - Benefit mode warns loudly and returns all-zero weights when the analysis period contains no beneficial hours (previously this fell through toward a degenerate carve with only a generic downstream warning; an empty hour list must also never reach Ladybug, which treats it as "whole year").
ground_reflectanceconfig field — it never affected results: Ladybug SkyMatrix only stores it as metadata for RadiationStudy, which USC does not run, and USC's ray casting targets sky patches only. Configs carrying the key now fail validation with a clear message; remove the line. The USC_RaySettings Grasshopper component no longer emits it (old canvases keep the input; it is ignored).
-
edge_taperdesign constraint (all weighted sky-patch modes): sample points withinedge_tapermeters of their test-surface component's boundary count proportionally less (linear ramp), so perimeter/corner points no longer force carving of directly adjacent volume — replacing the manual practice of offsetting test-surface edges. Distances are exact 2D distances to the welded component outline (exterior and holes), so exploded-but-coincident panels (e.g. street networks) have no phantom seams. Zero-weight rays are dropped before tracing. Components narrower than ~2× the taper warn loudly and per-component stats are recorded in the preprocessing diagnostics — the taper never silently excludes geometry. Ignored (with a warning) by the binary violation-count modes. -
min_sky_elevation_degprotection cone (radiative_cooling): sky patches below the declared elevation are excluded from the cooling weights (renormalized) — a declared design constraint analogous to obstruction-angle daylight rules, controlling how steeply the envelope may rise around protected surfaces. Default 0 keeps the pure physical model. -
The radiative-cooling template now documents both constraints and ships a mode-appropriate
carve_fraction: 0.35default (cooling scores are diffuse; the 0.7 used by solar modes over-carves drastically). -
Canonical
ThresholdSpec:thresholdnow normalizes every accepted spelling (bare number,headtail,carve_fraction, or the canonical mapping{method: ..., value: ...}) into one validated object at config load. Methods:carve_fraction,headtail,cutoff. Mode defaults are resolved at load time (weighted modes →carve_fraction, count modes →cutoff 0), and the method is cross-checked against the mode's score kind — misuse fails at load with a precise message. All previous config spellings keep working. -
analysis_periodmapping: the analysis period can be given as one mapping using Ladybug'sAnalysisPeriod.to_dict()keys (st_month…end_hour;start_*spellings also accepted; LB'stype/timestep/is_leap_yearextras are ignored). The six flat fields remain valid. -
Bundled example weather file: a public-domain NREL TMY3 EPW (Golden, Colorado) ships under
examples/weather/, so the tutorial notebooks and the EPW-dependent tests run out of the box.USC_EPW_PATHstill overrides it for the test suite. -
setup_env.pylong-path preflight: on Windows, the installer now predicts the 260-character path failure that torch's wheel triggers under deep repo paths, and falls back to a short per-user venv (~/.usc-venv) with clear instructions — instead of dying mid-install with WinError 206. A--venv-diroption overrides the location.
- The monolithic
configs/user_config.yamlstarter (superseded by the per-mode templates). - Dead nested/dotted-key override machinery in
load_config— the schema is flat and the advertiseda.b=cform never validated. Overrides are plainkey=value(JSON lists/objects are parsed as values).
- The thresholding stage hash now derives from
{threshold_method, threshold_value, score_smoothing}— previously it could embed a score-dependent resolved cutoff (making identical configs hash differently) and an irrelevantcarve_fractionfor non-fraction methods. Stage hashes change once across this version boundary. threshold_methodin diagnostics reportscutoffwhere it previously reportednumeric.
setup_env.pyon current NVIDIA drivers: the CUDA-to-wheel map ended at 12.x, so drivers reporting CUDA 13.x silently fell back to CPU-only PyTorch; CUDA >= 13 now maps to cu128 wheels. The script also installs the package editable (pip install -e .[dev]) as its docs always claimed, instead of a static site-packages copy that ignored source edits.- Session cache on CUDA machines with CPU sessions:
get_active_session()with no device preferred cuda-when-available, so adevice: cpurun on a GPU machine silently lost all session caching; a single active session now wins regardless of device. - Matplotlib backend hijack:
api_core._diagnosticsforced the Agg backend at import time, breaking inline plotting in any notebook that imports USC; Agg is now only the fallback when no backend was chosen yet. - The Warp voxelizer parity test crashed on CUDA machines (
.numpy()on a cuda tensor) before asserting anything; it is now device-aware and parametrized over both cpu and cuda, so the parity guarantee is actually exercised on GPUs. - CPU ray tracing accuracy and speed: the CPU path now runs the same exact
Warp DDA kernel as CUDA. The previous fixed-step marcher skipped up to ~37 %
of the voxels traversed by oblique rays, systematically under-counting
obstruction on CPU-only machines. Measured on the
full_blocksexample (daylight mode): preprocessing 101 s → 5.8 s (~17×), obstruction score mass +61 %. The fixed-step marcher remains as a fallback when Warp is missing and now warns that it is approximate. north_degconvention: USC documentsnorth_degas degrees clockwise from +Y, but Ladybug'sSkyMatrix/Sunpathuse counterclockwise. The angle is now negated at the Ladybug boundary, andnorth_degis honored by the time-based mode (sun vectors) and the tilted-plane per-octant lookup, which previously ignored it. Runs with the defaultnorth_deg: 0are unaffected.apply_smoothing=Truemeshing: marching cubes now contours the continuous smoothed SDF (trimesh'sthresholdargument binarized the field, silently discarding the SDF smoothing), with correct voxel-center alignment (surfaces were previously shifted half a voxel) and outward face winding.- Violation counts on the fallback tracer:
trace_multi_hit_gridnow guarantees each (ray, voxel) pair is reported once; the fixed-step marcher could report duplicates, inflating time-based / tilted-plane violation counts. --dry-rungrid estimate: now mirrors the actual cubic grid built byvoxelize_mesh(the estimate used per-axis dimensions and could badly under-report memory for elongated sites).threshold: numeric(a Grasshopper-side placeholder) is now rejected with a clear message at config load instead of crashing mid-pipeline.- Missing input files (meshes, EPW) are validated at the start of preprocessing for fast, clear errors.
Measured on a 306³ grid / 375k sample points (daylight mode, CPU): full pipeline 112 s → ~34 s. Except for the CPU trace dispatch, the gains apply equally to CUDA machines.
-
Voxelization: new Warp parity voxelizer — one BVH ray per grid column along each axis, majority vote across the three parities plus surface-hit marking (the same robustness idea as trimesh's orthographic fill). Runs on CUDA or the Warp CPU device: 10–50× faster than the trimesh rasterize-and-fill path (6.4 s → 0.6 s at 306³), which remains as the no-Warp fallback. The occupancy is center-exact; differences vs trimesh are confined to the 1-voxel surface shell (regression-tested).
-
Fixed a half-voxel embed bug in the trimesh voxelization path:
vox.transform[:3, 3]is the CENTER of trimesh's voxel (0,0,0), not a grid corner, so the occupancy grid was systematically misplaced by half a voxel (rounding to a full voxel at ambiguous alignments) in all previous releases. -
Smoothed exporting: marching-cubes output is welded (
merge_vertices) and then given the light cleanup instead of the full trimesh repair whenever it is watertight (it normally is); the full repair remains the fallback. Welding first matters: MC emits zero-area faces where the iso crosses lattice nodes, and dropping them before welding would open pinholes. -
carve_fractionthresholding: the fullargsortover the score volume (O(N log N); ~5 s at 306³ and minutes at 500³) is replaced by an exact two-pass weighted-histogram method (O(N)). Mass accounting now accumulates in float64 — the old float32 cumulative sum drifted measurably on large grids, so thresholds may shift very slightly (the new values are the more accurate ones). -
Perona–Malik SDF smoothing (
apply_smoothing=Trueexports): the serial in-place stencil is now a parallel Jacobi stencil (numbaprange, ~10× on a 306³ grid). The Jacobi update scheme differs from the previous in-place sweep by a fraction of a voxel in SDF units; the volume-matched iso compensates globally. The two distance transforms feeding the SDF also run concurrently (~1.9×). -
CPU ray tracing: Warp executes CPU launches single-threaded, so batches of the fused trace+score kernel are now dispatched from a small thread pool when running on CPU (kernels release the GIL; the shared score buffer stays correct via atomic adds). Like on CUDA, atomic float addition makes scores reproducible only up to rounding noise. CUDA dispatch is unchanged.
- The carvers no longer transfer the full ray set to host memory: at
typical ray counts (~34M rays at 0.5 m sampling) this was ~0.8 GB of
device→host copying per preprocessing run whose result was immediately
discarded.
carve_with_sky_patch_raysandcarve_with_sun_raysnow takereturn_rays=False(opt-in) and return None ray fields otherwise. - New fused DDA count kernel (
trace_and_count_dda) for the binary carving modes (time-based, tilted_plane): counts hits atomically in-place, replacing the buffered trace path that guessed 20 hits/ray and re-traced every batch on overflow (2-3x wasted GPU work on scenes with long rays). The buffered path remains for the no-Warp fallback. Equivalence with the buffered counts is regression-tested. generate_sky_patch_raysno longer materializes per-ray normals (an (R, 3) tensor nobody consumes — hundreds of MB of VRAM); passinclude_normals=Trueto get them.
- Kernel warmup: Warp JIT-compiles kernels on first use (~3-10 s once
per machine, per device, per code version; cached on disk afterwards).
That one-time cost is now paid where waiting is expected instead of on
the first carving run:
setup_env.pyprecompiles at the end of installation, the daemon precompiles during startup (before READY), and a newusc warmup [-d cpu|cuda]command covers manual updates. Public API:urbansolarcarver.raytracer.warmup_kernels(device).
- Defensive validation at stage boundaries: thresholding and exporting now verify that scores/mask files exist and match the grid shape recorded in the manifest, failing with a clear message instead of a cryptic numpy/torch error when artifacts from different runs are mixed.
load_meshrejects files with no triangle geometry at load time (was a cryptic empty-grid failure later).- Preprocessing warns loudly when scores contain NaN/Inf (non-finite voxels are always carved, previously without any signal); the test suite now exercises the real check instead of a copy of its logic.
usc schema(and any CLI output) no longer crashes with UnicodeEncodeError on legacy Windows code pages when piped.- CLI overrides accept scientific notation (
-o score_smoothing=1e-1); 'inf'/'nan' spellings stay strings so validation rejects them clearly. session_cachewarns once when a key template does not match the call signature (a silently dead cache was invisible before).- Direct attribute access on the validated config everywhere (the
getattr(conf, "field", default)pattern silently masked typos). - Daemon RPC handlers consolidated into one dispatcher (was 4 copies of the same try/except/close block).
- Monolith decomposition: threshold resolution and score smoothing
extracted from
thresholding(); mode dispatch and the radiative-cooling guard extracted frompreprocessing(); boundary-loop chaining extracted fromsample_planar_surface(). - Removed dead/speculative API:
carve_directional,voxelize_and_clean,mesh_from_voxels_select,CarverSession.get_kernel(Warp caches its own compiled modules), plus unused parameters and unreachable branches.
- The
diagnosticsconfig flag (previously unused) now gates the detailed score statistics (median, std, percentiles) in per-stage diagnostics JSON. Basic statistics (count, min, max, mean, nonzero counts) and timings are always written. - Grasshopper components: the PLY preview loader no longer hangs Rhino on
non-PLY export formats (obj/stl/glb are saved to disk with a canvas
Remark instead); USC_Threshold warns instead of emitting the invalid
bare
threshold=numericplaceholder; USC_Session finds the backend Python in.venv/venv(Windows and POSIX layouts) or via an optionalpython_path.txtoverride, and no longer passes Windows-only process flags on macOS; USC_RunPipeline errors turn the component red like the stage components. carve_above_columnscompiled with numba (was a pure-Python triple loop; large grids dropped from minutes to milliseconds).- Diagnostic plots render synchronously; the previous "background thread" was started and immediately joined, adding overhead without concurrency.
- Documentation/docstring corrections (nonexistent config fields, stale return signatures, wrong output filename in CLI help).
First public beta release.
- 3-stage pipeline: preprocessing (voxelise, ray-cast) → thresholding (score ranking, carve fraction) → exporting (mesh reconstruction)
- 6 analysis modes: time-based, irradiance, benefit (heating/cooling), daylight (CIE overcast), tilted-plane, radiative-cooling (experimental)
- GPU acceleration: NVIDIA Warp ray tracer with automatic CPU fallback
- Thresholding strategies:
carve_fraction(direct),headtail(automatic heavy-tail detection), numeric threshold - Score smoothing: optional Gaussian smoothing with auto-default sigma (1.1× voxel size)
- Carve-above column post-processing: remove structurally implausible floating mass above carved zones, with configurable
min_consecutivesensitivity threshold - Connected-component filtering:
min_voxelsparameter removes small isolated fragments - Multi-format mesh export: PLY, OBJ, STL, GLB output via trimesh
- Run report: automatic
run_report.mdsummarising every pipeline run - Diagnostic outputs: score histograms, sky-patch hemisphere plots, config snapshots, step timings
- CLI with two entry points (
usc,urbansolarcarver):run,validate,info,list-modescommands - Python API:
preprocess(),threshold(),export()for decomposed workflows - Grasshopper integration: 17 GHPython components for Rhino 8
- Configuration: single YAML file with Pydantic v2 validation (
extra='forbid') - Memory guard: rejects grids exceeding 500 million voxels
- Mode registry: single source of truth for mode definitions and parameter requirements
- 5 tutorial notebooks: quick start, mode comparison, threshold tuning, Grasshopper bridge, advanced post-processing
- Reference YAML: fully commented
REFERENCE_all_options.yamlwith all configuration fields
radiative_coolingmode is marked experimental (clear-sky only, horizontal surfaces)- GPU (
[cuda]extra) is recommended for grids above ~100³ but not required - Requires Python >= 3.9