Insolation

Overview

Insolation analysis estimates how long a place receives direct sunlight among buildings of known height and, optionally, terrain. objectnat.get_insolation() computes the daily sun hours on a regular grid of ground receivers; objectnat.get_facade_insolation() computes them for facade points on every floor of the selected buildings. Both functions accept a single day or a period. Typical uses are checks of insolation norms for playgrounds and dwellings, comparison of development scenarios, and the placement of planting that needs direct sun.

Ground shadows of a residential district on 21 June, 15-minute frames

Sunlit and shaded ground on 21 June 2026 in the example district (Vasilyevsky Island, Saint Petersburg, 59.96° N), one frame every 15 minutes from sunrise to sunset. The title gives Moscow time (UTC+3) and the sun elevation and azimuth.

Method

Sun position. For every simulated day the sun position is sampled every time_step_min minutes (the midpoints of the intervals of a local solar day) with the NOAA solar position algorithm, including atmospheric refraction; the stated accuracy of the algorithm is about 0.01°. The date and the centre of the area determine the sun path, so latitude and season are taken into account. Positions below min_sun_elevation (degrees) count as no sun.

Scene. Buildings are extruded footprints with flat roofs. The height of a building is taken from height_column; where it is missing, from storeys_column × storey_height; where both are missing, from default_height. Buildings with no height at all are skipped with a warning. Terrain is optional and can be given as

  • a GeoDataFrame of polygons with heights in terrain_height_column (for example, elevation zones), rasterized with terrain_cell cells; overlapping polygons take the highest value;

  • a path to a GeoTIFF (requires rasterio);

  • a tuple (heights, transform, crs) with a 2-D array, a north-up affine transform in rasterio order and a projected CRS.

Buildings stand on the terrain at their footprint, and receivers are placed receiver_height above the terrain. The calculation runs in a local metric CRS (the raster CRS when a raster is given, otherwise an estimated UTM zone). Azimuths are measured from true north and corrected for the difference between true north and grid north (meridian convergence).

Shadow test. For every receiver and every sun position a ray is cast towards the sun. The receiver is sunlit when the ray passes above every building wall it crosses and above the terrain. The ray is followed only while it is lower than the highest roof or terrain point of the scene, so rays towards a high sun end early and cost little. The test runs in ObjectNat’s Rust extension in parallel over receivers.

Day statistics. For each receiver and day the Rust extension returns the number of sunlit time steps and the longest uninterrupted sunlit run. These give three quantities, all in hours:

  • sun_hours: mean daily direct sun over the simulated days;

  • min_sun_hours: direct sun on the worst simulated day;

  • max_continuous_hours: the longest continuous sunlit period on the worst day (the minimum over days of the daily longest run). Insolation norms usually set a minimum for this quantity; SanPiN 1.2.3685-21 (Russia), for example, sets the minimum continuous insolation of dwellings and playgrounds.

Time steps are discrete, so every value is a multiple of time_step_min.

Periods. date=("2026-04-22", "2026-08-22") simulates every day_step-th day of the period and its last day; the scene is built once.

Ground insolation

objectnat.get_insolation() places receivers every grid_step metres inside area (by default the bounding box of the buildings), outside building footprints. Include the buildings around the area in buildings: their shadows reach into it.

The result is the receiver points with sun_hours, min_sun_hours and max_continuous_hours; sun_hours is 0 on ground that receives no direct sun on any simulated day. The attrs contain the simulated days, day_length_hours (the mean time the sun is above min_sun_elevation) and the grid, so objectnat.rasterize() turns any column into a raster for maps, GeoTIFF files and web maps (see Rasters). A raster shows the hours as a continuous colour scale; thresholds such as a norm are applied to the columns directly, for example receivers["max_continuous_hours"] >= 2.5.

objectnat.get_insolation(buildings, date, *, area=None, height_column='height', storeys_column=None, storey_height=3.0, default_height=None, terrain=None, terrain_height_column='height', terrain_cell=5.0, grid_step=2.0, time_step_min=10.0, day_step=7, min_sun_elevation=0.0, receiver_height=0.0, max_workers=None)[source]

Hours of direct sunlight on the ground during a day or a period, among buildings of known height and terrain.

The sun is followed through every simulated day in time_step_min steps (NOAA solar position for the date and the location of the area, with refraction), so its path changes with latitude and season. For every point of a regular grid and every sun position above min_sun_elevation a ray is cast towards the sun; the point is in the sun when the ray passes above every building wall it crosses and above the terrain. Buildings are extruded footprints with flat roofs standing on the terrain. The result shows how many hours each place gets and which places stay in shadow all the time.

Parameters:
  • buildings (geopandas.GeoDataFrame) – Building footprints (Polygon/MultiPolygon) in any CRS. Include the buildings around the area of interest too: shadows come from them.

  • date (datetime.date | str | tuple) – One day ("2026-06-21") or a period (start, end), both days included (("2026-04-22", "2026-08-22")). Every day is covered from sunrise to sunset.

  • area (gpd.GeoDataFrame | gpd.GeoSeries | shapely geometry | None, optional) – Where to place receivers. Shapely geometries are taken in the CRS of buildings. Defaults to the bounding box of the buildings.

  • height_column (str | None, optional) – Column with building heights in metres. Defaults to "height".

  • storeys_column (str | None, optional) – Column with the number of storeys, used (times storey_height) where the height is missing.

  • storey_height (float, optional) – Height of one storey in metres. Defaults to 3.

  • default_height (float | None, optional) – Height for buildings with neither value. Without it such buildings are skipped with a warning.

  • terrain (gpd.GeoDataFrame | str | os.PathLike | tuple | None, optional) –

    Ground heights in metres, one of:

    • a GeoDataFrame of polygons with the height in terrain_height_column (e.g. elevation zones); it is rasterized with terrain_cell cells, overlapping polygons take the highest value and places no polygon covers count as no data;

    • a GeoTIFF path (requires rasterio);

    • a tuple (heights, transform, crs) with a 2-D array, an affine transform (a, b, c, d, e, f) as in rasterio (x = a * col + c, y = e * row + f, north-up, no rotation) and a projected CRS.

    Rasters set the CRS of the calculation; NaN marks missing data. Without terrain the ground is flat.

  • terrain_height_column (str, optional) – Height column of a terrain GeoDataFrame. Defaults to "height".

  • terrain_cell (float, optional) – Cell size in metres used to rasterize a terrain GeoDataFrame. Defaults to 5.

  • grid_step (float, optional) – Receiver spacing in metres. Defaults to 2.

  • time_step_min (float, optional) – Time step in minutes; it must divide a day evenly. Defaults to 10.

  • day_step (int, optional) – For a period, simulate every day_step-th day (the last day of the period is always included). Defaults to 7.

  • min_sun_elevation (float, optional) – Sun positions lower than this (degrees) count as no sun, e.g. to leave out the low morning and evening sun as insolation norms do. Defaults to 0.

  • receiver_height (float, optional) – Height of the receivers above the ground in metres. Defaults to 0.

  • max_workers (int | None, optional) – Number of threads (None uses all cores).

Returns:

Receiver points in the CRS of buildings; points inside buildings are left out. Columns:

  • sun_hours: mean hours of direct sun per day (0 for ground that gets no direct sun on any simulated day);

  • min_sun_hours: hours on the worst day;

  • max_continuous_hours: the longest uninterrupted sunlit period on the worst day (the least of the daily longest periods), for which insolation norms usually set a minimum.

attrs contain days (the simulated dates) and day_length_hours (mean time the sun is above min_sun_elevation) and record the grid, so rasterize() turns any column into a Raster.

Return type:

gpd.GeoDataFrame

Raises:
  • ValueError – On missing CRS, empty input, unknown height columns, a bad time step or period.

  • ImportError – If terrain is a file path and rasterio is not installed.

Direct sun hours on 21 March and 21 June

Direct sun hours on 21 March and 21 June 2026 in a 500 × 400 m area (outlined), grid_step=2, time_step_min=10, receivers rasterized with objectnat.rasterize() on one colour scale. Building heights are storeys_count × 3 m.

Mean sun hours and longest continuous sun over a period

Period 22 April – 22 August 2026 with day_step=7 (19 simulated days), receivers rasterized with objectnat.rasterize() on one scale. Left: mean daily sun hours. Right: longest continuous sunlit period on the worst day.

Sun hours without and with a ridge south of the area, and their difference

The same area on 21 March on flat ground and with a synthetic ridge running west–east, 50 m high, its crest 50 m south of the area, given as a (heights, transform, crs) array (dashed contours in metres). The low equinox sun comes from the south. In the southern 40 m of the area, at the foot of the ridge, receivers lose 7 h a day on average, and two thirds of them get no direct sun at all; up to 180 m further north some places still lose 1–2 h. A hill under the area itself changes little, because buildings and receivers rise with the ground.

Facade insolation

objectnat.get_facade_insolation() places points along every wall of the buildings that intersect area (outer and courtyard walls) every spacing metres, 0.1 m in front of the wall, on every floor at window_height above the floor level. Walls shared with a neighbouring building get no points. The number of floors is the storeys value when given, otherwise the height divided by storey_height. A point counts the sun only while the sun is in front of its facade, at least min_facade_angle degrees (horizontal) away from the facade plane, and the ray towards the sun is not blocked by other buildings or the terrain. All buildings in buildings cast shadows, including those outside area.

The result has one point per wall position and floor with building (index label of the building), floor (1 is the ground floor), height above the ground, facade_azimuth (direction the facade faces, degrees clockwise from true north), sun_hours, min_sun_hours and max_continuous_hours. With norm_hours it also has meets_norm (max_continuous_hours >= norm_hours). Grouping the points by building and floor shows which floors and facades fail a norm; comparing runs with and without a planned building shows which windows it shades.

objectnat.get_facade_insolation(buildings, date, *, area=None, height_column='height', storeys_column=None, storey_height=3.0, default_height=None, terrain=None, terrain_height_column='height', terrain_cell=5.0, spacing=3.0, window_height=1.2, floors=None, time_step_min=10.0, day_step=7, min_sun_elevation=0.0, min_facade_angle=0.0, norm_hours=None, max_workers=None)[source]

Hours of direct sunlight on building facades, floor by floor: which windows get sun and which do not.

Calculation points are placed along every wall of the buildings in area (outer walls and courtyard walls) every spacing metres, on every floor at window_height above the floor level, just in front of the wall. Walls shared with a neighbouring building get no points. A point is sunlit when the sun is in front of its facade, at least min_facade_angle away from the facade plane, and the ray towards the sun passes above every other wall and the terrain, as in get_insolation(). All buildings cast shadows, including those outside area.

Parameters:
  • buildings (geopandas.GeoDataFrame) – Building footprints with heights, as in get_insolation().

  • date (datetime.date | str | tuple) – One day or a period (start, end), as in get_insolation().

  • area (gpd.GeoDataFrame | gpd.GeoSeries | shapely geometry | None, optional) – Buildings intersecting it get facade points (shapely geometries in the CRS of buildings). Defaults to all buildings.

  • height_column (str | None) – Building heights, as in get_insolation(). The number of floors is the storeys value when given, otherwise the height divided by storey_height (at least one floor).

  • storeys_column (str | None) – Building heights, as in get_insolation(). The number of floors is the storeys value when given, otherwise the height divided by storey_height (at least one floor).

  • storey_height (float) – Building heights, as in get_insolation(). The number of floors is the storeys value when given, otherwise the height divided by storey_height (at least one floor).

  • default_height (float | None) – Building heights, as in get_insolation(). The number of floors is the storeys value when given, otherwise the height divided by storey_height (at least one floor).

  • terrain (GeoDataFrame | str | PathLike | tuple | None) – Ground heights, as in get_insolation().

  • terrain_height_column (str) – Ground heights, as in get_insolation().

  • terrain_cell (float) – Ground heights, as in get_insolation().

  • spacing (float, optional) – Distance between points along a wall in metres; shorter walls get one point. Defaults to 3.

  • window_height (float, optional) – Height of the points above each floor level in metres. Defaults to 1.2.

  • floors (Sequence[int] | None, optional) – Only these floors (1 is the ground floor). Defaults to all floors.

  • time_step_min (float) – Sun sampling, as in get_insolation().

  • day_step (int) – Sun sampling, as in get_insolation().

  • min_sun_elevation (float) – Sun sampling, as in get_insolation().

  • min_facade_angle (float, optional) – Sun closer than this angle (degrees, horizontal) to the facade plane does not count. Defaults to 0: any sun in front of the facade counts.

  • norm_hours (float | None, optional) – Required continuous sunlight in hours; adds meets_norm (max_continuous_hours >= norm_hours).

  • max_workers (int | None, optional) – Number of threads (None uses all cores).

Returns:

One point per wall position and floor in the CRS of buildings with building (index label of the building), floor, height (above the ground), facade_azimuth (direction the facade faces, degrees clockwise from true north), sun_hours, min_sun_hours and max_continuous_hours (as in get_insolation()) and, with norm_hours, meets_norm. attrs contain days and day_length_hours.

Return type:

gpd.GeoDataFrame

Continuous facade insolation on floors 1, 5 and 10 and share of points meeting 2.5 h

Facade points (spacing=3) on 22 April 2026 coloured by the longest continuous sunlit period, for floors 1, 5 and 10, and the share of points with at least 2.5 h of continuous sun per floor (norm_hours=2.5). The number of points per floor is given in brackets. The 2.5 h threshold is an example value; use the norm and the calculation day that apply to the project.

Limitations

  • Roofs are flat. Pitched roofs, balconies, loggias, eaves and trees are not modelled.

  • Facade points lie in front of the outer wall surface; window reveals (the wall depth) are not modelled, so the sun angles that a deep window opening would cut off still count, unless min_facade_angle excludes them.

  • Only direct sunlight is counted; diffuse sky light and reflected light are not, and cloud cover is ignored.

  • Results are discrete in time (time_step_min) and space (grid_step, spacing); a continuous period can be under- or overestimated by up to one time step at each end.

  • The accuracy of the result is limited by the building heights: heights derived from storey counts (storeys × storey_height) can differ from surveyed heights by several metres.

Example notebook

Insolation: sun hours on the ground and on facades