Multi-Day Timeseries and Summary Grids¶
This tutorial runs a 4-day SOLWEIG timeseries over the Athens demo area and explores the summary products: aggregated grids, timeseries plots, and the text report.
calculate() carries thermal state (ground and wall temperatures) between
timesteps, so results are more accurate than looping over single-timestep calls manually.
from pathlib import Path
import geopandas as gpd
import matplotlib.pyplot as plt
import solweig
from pyproj import CRS
# Resolve the repo root so this notebook runs from the repo root or docs/tutorials/
ROOT = next(p for p in [Path.cwd(), *Path.cwd().parents] if (p / "demos").exists())
DATA_DIR = ROOT / "demos/data/athens"
WORK_DIR = ROOT / "temp/tutorial_cache"
WORK_DIR.mkdir(parents=True, exist_ok=True)
OUTPUT_DIR = WORK_DIR / "output_timeseries"
assert (DATA_DIR / "DSM.tif").exists(), f"Demo data not found at {DATA_DIR.resolve()}"
EXTENTS_BBOX = [476800, 4205850, 477200, 4206250]
TARGET_CRS = 2100
1. Prepare the surface¶
If you ran the Quick Start tutorial, the cached SVF and wall
data in working_dir will be reused automatically.
# Rasterise tree vectors (or reuse saved CDSM)
cdsm_path = WORK_DIR / "CDSM.tif"
if not cdsm_path.exists():
trees_gdf = gpd.read_file(str(DATA_DIR / "trees.gpkg")).to_crs(TARGET_CRS)
cdsm_rast, cdsm_transf = solweig.io.rasterise_gdf(
trees_gdf,
"geometry",
"height",
bbox=EXTENTS_BBOX,
pixel_size=1.0,
)
solweig.io.save_raster(
str(cdsm_path),
cdsm_rast,
cdsm_transf.to_gdal(),
CRS.from_epsg(TARGET_CRS).to_wkt(),
)
surface = solweig.SurfaceData.prepare(
dsm=str(DATA_DIR / "DSM.tif"),
working_dir=str(WORK_DIR / "working"),
cdsm=str(cdsm_path),
bbox=EXTENTS_BBOX,
pixel_size=1.0,
)
print(f"Surface ready: {surface.dsm.shape}")
solweig.models.surface: Fast-path cache hit — loading prepared surface from /Users/dev/repos/solweig/temp/tutorial_cache/working
solweig.models.surface: Loading prepared surface from /Users/dev/repos/solweig/temp/tutorial_cache/working/cleaned
solweig.io: No-data value is -9999.0, replacing with NaN
solweig.models.surface: DSM: 400×400 pixels
solweig.io: No-data value is -9999.0, replacing with NaN
solweig.io: No-data value is -9999.0, replacing with NaN
solweig.io: No-data value is -9999.0, replacing with NaN
solweig.io: No-data value is -9999.0, replacing with NaN
solweig.models.precomputed: Loaded SVF memmap cache from /Users/dev/repos/solweig/temp/tutorial_cache/working/svf/px1.000/memmap
solweig.models.precomputed: Loaded shadow matrices from /Users/dev/repos/solweig/temp/tutorial_cache/working/svf/px1.000/shadowmats.npz
solweig.models.precomputed: Loaded SVF data: (400, 400)
solweig.models.precomputed: Loaded shadow matrices for anisotropic sky
solweig.models.surface: Loaded: DSM, CDSM, TDSM, walls, SVF, shadows
Surface ready: (400, 400)
2. Load 4 days of weather¶
epw_path = str(DATA_DIR / "athens_2023.epw")
weather_list = solweig.Weather.from_epw(epw_path, start="2023-07-01", end="2023-07-04")
location = solweig.Location.from_epw(epw_path)
print(f"{len(weather_list)} hourly timesteps")
print(f"Date range: {weather_list[0].datetime} → {weather_list[-1].datetime}")
solweig.io_epw: Loaded EPW file: NA, 8760 timesteps (pure Python parser)
solweig.models.weather: Loaded 96 timesteps from EPW: 2023-07-01 00:00 → 2023-07-04 23:00
solweig.models.location: Location from EPW: NA — 38.0000°N, 23.7500°E (UTC+2, 175m)
96 hourly timesteps
Date range: 2023-07-01 00:00:00 → 2023-07-04 23:00:00
3. Run the timeseries¶
We save Tmrt and shadow GeoTIFFs to disk and also use the anisotropic sky model for better accuracy.
summary = solweig.calculate(
surface=surface,
weather=weather_list,
location=location,
use_anisotropic_sky=True,
output_dir=str(OUTPUT_DIR),
outputs=["tmrt", "shadow"],
)
solweig.tiling: Resource-aware tile sizing (context=solweig): GPU budget=30,150,672,384 bytes, RAM=11,631,132,672 available of 51,539,607,552 total, max_tile_side=3812 px
solweig.timeseries: ============================================================
solweig.timeseries: Starting SOLWEIG timeseries calculation
solweig.timeseries: Grid size: 400x400 pixels
solweig.timeseries: Timesteps: 96
solweig.timeseries: Period: 2023-07-01 00:00 -> 2023-07-04 23:00
solweig.timeseries: Location: 38.00N, 23.75E
solweig.timeseries: ============================================================
solweig.timeseries: Pre-computing sun positions and radiation splits...
solweig.timeseries: Pre-computed 96 timesteps in 0.1s
[GPU] Shadow GPU context initialized successfully
SOLWEIG timeseries: 0%| | 0/96 [00:00<?, ?it/s]
[GPU] GVF GPU context initialized
[GPU] Anisotropic sky GPU context initialized
SOLWEIG timeseries: 3%|▎ | 3/96 [00:00<00:03, 26.00it/s]
SOLWEIG timeseries: 8%|▊ | 8/96 [00:00<00:02, 35.19it/s]
SOLWEIG timeseries: 14%|█▎ | 13/96 [00:00<00:02, 38.10it/s]
SOLWEIG timeseries: 18%|█▊ | 17/96 [00:00<00:02, 38.69it/s]
SOLWEIG timeseries: 22%|██▏ | 21/96 [00:00<00:01, 38.51it/s]
SOLWEIG timeseries: 27%|██▋ | 26/96 [00:00<00:01, 40.19it/s]
SOLWEIG timeseries: 32%|███▏ | 31/96 [00:00<00:01, 40.74it/s]
SOLWEIG timeseries: 38%|███▊ | 36/96 [00:00<00:01, 40.16it/s]
SOLWEIG timeseries: 43%|████▎ | 41/96 [00:01<00:01, 39.75it/s]
SOLWEIG timeseries: 47%|████▋ | 45/96 [00:01<00:01, 39.00it/s]
SOLWEIG timeseries: 52%|█████▏ | 50/96 [00:01<00:01, 40.61it/s]
SOLWEIG timeseries: 57%|█████▋ | 55/96 [00:01<00:00, 41.17it/s]
SOLWEIG timeseries: 62%|██████▎ | 60/96 [00:01<00:00, 40.73it/s]
SOLWEIG timeseries: 68%|██████▊ | 65/96 [00:01<00:00, 40.42it/s]
SOLWEIG timeseries: 73%|███████▎ | 70/96 [00:01<00:00, 39.71it/s]
SOLWEIG timeseries: 78%|███████▊ | 75/96 [00:01<00:00, 40.70it/s]
SOLWEIG timeseries: 83%|████████▎ | 80/96 [00:02<00:00, 40.77it/s]
SOLWEIG timeseries: 89%|████████▊ | 85/96 [00:02<00:00, 39.15it/s]
SOLWEIG timeseries: 93%|█████████▎| 89/96 [00:02<00:00, 38.73it/s]
SOLWEIG timeseries: 97%|█████████▋| 93/96 [00:02<00:00, 38.15it/s]
SOLWEIG timeseries: 100%|██████████| 96/96 [00:02<00:00, 39.41it/s]
solweig.timeseries: ============================================================
solweig.timeseries: Calculation complete: 96 timesteps processed
solweig.timeseries: Total time: 2.4s (39.29 steps/s)
solweig.timeseries: ============================================================
4. Summary report¶
The TimeseriesSummary object has a built-in report with statistics for Tmrt, UTCI, and sun hours.
SOLWEIG Summary: 96 timesteps (60 day, 36 night)
Period: 2023-07-01 00:00 — 2023-07-04 23:00
Tmrt — mean: 35.3°C, range: 13.9 – 69.7°C
UTCI — mean: 28.8°C, range: 18.9 – 41.3°C
Sun — 0.0 – 60.0 hours
UTCI > 26°C (night) — max 3.0h
UTCI > 32°C (day) — max 47.0h
UTCI > 38°C (day) — max 15.0h
Ta — range: 20.4 – 33.2°C
Summary GeoTIFFs: /Users/dev/repos/solweig/temp/tutorial_cache/output_timeseries/summary/
shade_hours.tif
sun_hours.tif
tmrt_day_mean.tif
tmrt_max.tif
tmrt_mean.tif
tmrt_min.tif
tmrt_night_mean.tif
utci_day_mean.tif
utci_hours_above_26_night.tif
utci_hours_above_32_day.tif
utci_hours_above_38_day.tif
utci_max.tif
utci_mean.tif
utci_min.tif
utci_night_mean.tif
Tip: per-timestep arrays are in summary.timeseries (e.g. .ta, .tmrt_mean, .utci_mean).
Spatial grids are on the summary itself (e.g. .tmrt_mean, .utci_max).
Summary grids are saved as GeoTIFFs above; timeseries arrays are in memory only.
5. Timeseries plot¶
summary.plot() produces a multi-panel figure showing temperature, radiation,
and sun exposure over the simulation period.

6. Summary grids¶
The summary also contains spatial grids aggregated over all timesteps: mean/max/min Tmrt, mean UTCI, sun hours, and hours above UTCI thresholds.
fig, axes = plt.subplots(2, 3, figsize=(15, 10))
im0 = axes[0, 0].imshow(summary.tmrt_mean, cmap="hot")
axes[0, 0].set_title("Mean Tmrt (°C)")
plt.colorbar(im0, ax=axes[0, 0])
im1 = axes[0, 1].imshow(summary.utci_mean, cmap="hot")
axes[0, 1].set_title("Mean UTCI (°C)")
plt.colorbar(im1, ax=axes[0, 1])
im2 = axes[0, 2].imshow(summary.sun_hours, cmap="YlOrRd")
axes[0, 2].set_title("Sun hours")
plt.colorbar(im2, ax=axes[0, 2])
im3 = axes[1, 0].imshow(summary.tmrt_day_mean, cmap="hot")
axes[1, 0].set_title("Mean daytime Tmrt (°C)")
plt.colorbar(im3, ax=axes[1, 0])
im4 = axes[1, 1].imshow(summary.tmrt_night_mean, cmap="cool")
axes[1, 1].set_title("Mean nighttime Tmrt (°C)")
plt.colorbar(im4, ax=axes[1, 1])
# Show hours above the first UTCI threshold
threshold = sorted(summary.utci_hours_above.keys())[0]
im5 = axes[1, 2].imshow(summary.utci_hours_above[threshold], cmap="Reds")
axes[1, 2].set_title(f"UTCI hours > {threshold}°C")
plt.colorbar(im5, ax=axes[1, 2])
for ax in axes.flat:
ax.set_xticks([])
ax.set_yticks([])
plt.suptitle(
f"SOLWEIG Summary — {len(summary)} timesteps ({summary.n_daytime} day, {summary.n_nighttime} night)",
fontsize=14,
)
plt.tight_layout()
plt.show()

7. Custom timeseries charts¶
The summary.timeseries object contains 1-D arrays of spatial means at each timestep.
You can use these to build any custom chart.
ts = summary.timeseries
fig, ax = plt.subplots(figsize=(12, 4))
ax.plot(ts.datetime, ts.ta, label="Air temperature (Ta)", color="steelblue")
ax.plot(ts.datetime, ts.tmrt_mean, label="Mean Tmrt", color="firebrick")
ax.plot(ts.datetime, ts.utci_mean, label="Mean UTCI", color="darkorange", linestyle="--")
ax.set_ylabel("Temperature (°C)")
ax.set_xlabel("Time")
ax.legend()
ax.set_title("Spatial-mean temperatures over 4 days")
fig.autofmt_xdate()
plt.tight_layout()
plt.show()
