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.
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 withterrain_cellcells; 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_minsteps (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 abovemin_sun_elevationa 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 ofbuildings. 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 (timesstorey_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 withterrain_cellcells, 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 everyday_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 (Noneuses 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 (0for 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.
attrscontaindays(the simulated dates) andday_length_hours(mean time the sun is abovemin_sun_elevation) and record the grid, sorasterize()turns any column into aRaster.- Return type:
gpd.GeoDataFrame
- Raises:
ValueError – On missing CRS, empty input, unknown height columns, a bad time step or period.
ImportError – If
terrainis a file path andrasteriois not installed.
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.¶
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.¶
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) everyspacingmetres, on every floor atwindow_heightabove 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 leastmin_facade_angleaway from the facade plane, and the ray towards the sun passes above every other wall and the terrain, as inget_insolation(). All buildings cast shadows, including those outsidearea.- Parameters:
buildings (
geopandas.GeoDataFrame) – Building footprints with heights, as inget_insolation().date (
datetime.date | str | tuple) – One day or a period(start, end), as inget_insolation().area (
gpd.GeoDataFrame | gpd.GeoSeries | shapely geometry | None, optional) – Buildings intersecting it get facade points (shapely geometries in the CRS ofbuildings). Defaults to all buildings.height_column (
str|None) – Building heights, as inget_insolation(). The number of floors is the storeys value when given, otherwise the height divided bystorey_height(at least one floor).storeys_column (
str|None) – Building heights, as inget_insolation(). The number of floors is the storeys value when given, otherwise the height divided bystorey_height(at least one floor).storey_height (
float) – Building heights, as inget_insolation(). The number of floors is the storeys value when given, otherwise the height divided bystorey_height(at least one floor).default_height (
float|None) – Building heights, as inget_insolation(). The number of floors is the storeys value when given, otherwise the height divided bystorey_height(at least one floor).terrain (
GeoDataFrame|str|PathLike|tuple|None) – Ground heights, as inget_insolation().terrain_height_column (
str) – Ground heights, as inget_insolation().terrain_cell (
float) – Ground heights, as inget_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 inget_insolation().day_step (
int) – Sun sampling, as inget_insolation().min_sun_elevation (
float) – Sun sampling, as inget_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; addsmeets_norm(max_continuous_hours >= norm_hours).max_workers (
int | None, optional) – Number of threads (Noneuses all cores).
- Returns:
One point per wall position and floor in the CRS of
buildingswithbuilding(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_hoursandmax_continuous_hours(as inget_insolation()) and, withnorm_hours,meets_norm.attrscontaindaysandday_length_hours.- Return type:
gpd.GeoDataFrame
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_angleexcludes 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.