Infrared SDK
The Infrared SDK runs urban microclimate simulations on your own site model: wind, sun, thermal comfort and daylight. You use it from Python or TypeScript. A .NET SDK is coming. The SDK prepares the work on your machine. The Infrared cloud runs only the simulation models.
What goes in, what comes out
In: an area (one polygon), your model, weather and a time period. Your model has buildings, trees, ground materials, terrain and context geometry. Context geometry is far objects that only give shade. See Your model. You can also bring your own sensor points (see Bring your own sensors).
Out: ground maps, values on facades and roofs, values in rooms, and one value for each sensor point that you give. See Results and the ten analyses.
How people use it
Pick the case that is closest to yours. Each line shows what to read next.
| You want to | Typical setup | Read next |
|---|---|---|
| Compare design variants | Python in a notebook, your own model | How a run works, Your inputs, What comes back |
| Show results in a web app | TypeScript in the browser, one SDK worker | Where it runs, Serve many users, Facades and roofs |
| Run many sites | Python or Node.js on a server, a script | Tiling, Cost and failures |
| Work in Rhino or Grasshopper | Rhino 8 with the Python SDK (plug-in: work in progress) | Coordinates, Your inputs |
The chapters up to "The ten analyses" explain the concepts. The chapters after them are deep dives: read them when you need them.
The Rust core
One compiled core does the local work: tiles, geometry preparation and merge. The Python wheel and the TypeScript package share this core. The .NET SDK will share it too. All SDKs give the same results, and the local preparation is fast. See How a run works.
Measured numbers
Measured with the Python SDK 1.0.0 from PyPI on the production API, on a 32-core machine. The buildings were read before the timer. "Warm" is the second run of the same request: the models are warm and the geometry is already uploaded.
- Total time: the full SDK call. It plans the tiles, prepares the site, uploads, submits, waits, downloads all results and merges them.
- Cloud part: from the first job accepted to all jobs done (queue and simulation).
| Area | Analysis | Total time | Cloud part |
|---|---|---|---|
| 40 km², Hong Kong (24,444 buildings) | Sky view factor on the ground: 1 m grid, 169 tiles | 8.2 s warm (14.8 s first run, with a 24 MB upload) | 7.3 s |
| 6 km², Hong Kong (3,440 buildings) | Solar radiation on facades, June 08–18 h: 2.8 million sensors, 27 jobs | 2.7 s warm (3.9 s first run) | 2.0 s |
| 6 km², Hong Kong (3,440 buildings) | Sky view factor on facades: 2.8 million sensors, 27 jobs | 2.6 s warm (4.0 s first run) | 2.0 s |
| 6 km², Hong Kong (3,440 buildings) | Sky view factor on the ground: 1 m grid, 25 tiles | 3.3 s warm (8.5 s cold) | 3.0 s |
| 1 km², Vienna (4,212 buildings) | Sky view factor, wind speed and UTCI in one call | 8.7 s | first job after 2.9 s |
The largest run used less than 2 GB of memory on the machine.
Dates: Hong Kong 2026-10-08, Vienna 2026-10-07. Your times depend on your network, your machine and your site.
Install
pip install infrared-sdk
pip install "infrared-sdk[geodata]" # only to read Overture Maps
Python 3.9 or later. The package is infrared-sdk on PyPI.
npm install @infrared-city/infrared-sdk-ts
npm install hyparquet hyparquet-compressors # only to read Overture Maps
Node 18 or later. The package is @infrared-city/infrared-sdk-ts on npm.
An old .npmrc keeps you on 0.12.12
If an .npmrc maps @infrared-city to GitHub Packages, npm install
stays on 0.12.12. Version 1.0.0 is on npmjs.org, not on GitHub Packages.
Remove that line from .npmrc. You need no token for npmjs.org.
Quickstart
This run uses your own geometry: one box-shaped tower, in metres, with the
origin at the south-west corner of the area. my_tower is a placeholder.
import os
from infrared_sdk import InfraredClient, SvfModelRequest
from infrared_sdk.analyses.types import AnalysesName
lon, lat = 16.371, 48.208
polygon = {"type": "Polygon", "coordinates": [[
[lon, lat], [lon + 0.004, lat], [lon + 0.004, lat + 0.003],
[lon, lat + 0.003], [lon, lat]]]}
xy = [(100, 100), (120, 100), (120, 120), (100, 120)]
coordinates = [c for z in (0, 30) for x, y in xy for c in (x, y, z)]
indices = [0,2,1, 0,3,2, 4,5,6, 4,6,7, 0,1,5, 0,5,4,
1,2,6, 1,6,5, 2,3,7, 2,7,6, 3,0,4, 3,4,7]
my_tower = {"tower": {"coordinates": coordinates, "indices": indices}}
client = InfraredClient(api_key=os.environ["INFRARED_API_KEY"])
payload = SvfModelRequest(analysis_type=AnalysesName.sky_view_factors)
result = client.run_area_and_wait(payload, polygon, buildings=my_tower)
grid = result.physical_grid() # sky view factor, 0 to 100 %
The SDK writes INFO log lines, for example the default base URL. This is normal.
Save this as quickstart.ts and run it with npx tsx quickstart.ts. It is
an ES module. Set "type": "module" in package.json for top-level await.
import { InfraredClient, initializeCore, areaGridValuesF32, type AreaResult } from "@infrared-city/infrared-sdk-ts";
await initializeCore();
const lon = 16.371, lat = 48.208;
const polygon = { type: "Polygon", coordinates: [[
[lon, lat], [lon + 0.004, lat], [lon + 0.004, lat + 0.003],
[lon, lat + 0.003], [lon, lat]]] };
const xy = [[100, 100], [120, 100], [120, 120], [100, 120]];
const coordinates = [0, 30].flatMap((z) => xy.flatMap(([x, y]) => [x, y, z]));
const indices = [0,2,1, 0,3,2, 4,5,6, 4,6,7, 0,1,5, 0,5,4,
1,2,6, 1,6,5, 2,3,7, 2,7,6, 3,0,4, 3,4,7];
const client = new InfraredClient({ apiKey: process.env.INFRARED_API_KEY });
const result = await client.runAreaAndWait(
{ analysisType: "sky-view-factors" }, polygon,
{ buildings: { tower: { coordinates, indices } } },
);
const grid = areaGridValuesF32(result as AreaResult); // 0 to 100 %
No data yet? The SDK can read public buildings, trees and ground materials for an area. See Your model.
Versions
- Each SDK has its own SemVer version. Python and TypeScript both start at 1.0.0. Their versions can differ later.
- A major version can change the API. Read
UPGRADING.mdbefore you upgrade. - Pin the version in your project.
How a run works
You give an area, your geometry and the analyses. The SDK on your machine cuts the area into tiles, sends one job for each tile to the Infrared cloud and merges the tile results into one result. The cloud runs only the simulation models.
The inputs
- Area: one GeoJSON polygon in WGS84 (
[lon, lat]). - Your geometry: buildings, trees, ground materials, terrain, and context geometry. Context geometry is far objects that only give shade. The SDK does not analyse it. Only the sun and light analyses and the interior models take it, and it reaches 128 m past a tile.
- Weather: a weather file or a public weather station, and the time period.
- Analyses: one analysis or a list of analyses for the same area.
- Sensors (optional): the ground grid is the default. You can ask for
sensors on facades and roofs (
analysis_surfaces). Or give your own sensor points. Own points run as one job, not as an area run. See Bring your own sensors.
Use your own data. To start fast, you can also read public data for the area with the SDK.
The steps
- Plan the tiles. The SDK cuts the area into tiles of 512 m. All analyses except wind read 128 m of geometry past each tile. Wind analyses move 256 m from tile to tile, so the tiles overlap by half. The SDK skips empty tiles. One run has a maximum of 100 tiles.
- Prepare the geometry. The SDK prepares the site one time. Then it cuts your geometry for each tile.
- Upload the geometry. The SDK uploads the geometry of each tile one time. All analyses on that tile use the same upload.
- Submit the jobs. The SDK sends one job for each tile and analysis. Each job has an idempotency key. If a submit gets no answer, the SDK sends it again with the same key: the job does not run or bill two times. A job that failed on the server is sent again as a new job, with a new key.
- Wait. When the SDK has sent all jobs, it asks for their status: first after 0.5 s, then every 1 s, and after 10 s every 2 s. One request asks for the status of up to 50 jobs, so 81 jobs need 2 requests for each check, not 81.
- Download the results. The results come back in a binary format by default.
- Merge. The SDK joins the tiles into one result on your machine. It merges each result when its download is complete. You get the result when all tiles are merged.
Steps 2 to 4 do not wait for all tiles. Each tile goes on as soon as it is
ready. By default, up to 8 tiles are prepared, uploaded or sent at the same
time (max_workers, maxWorkers). This is not a limit on the jobs that run
in the cloud: more jobs can run at the same time. While the SDK prepares a
tile, the jobs of the tiles before it already run. In Python, the SDK
downloads each result while the other jobs still run. In TypeScript, the
downloads start when all jobs are done.
When a tile fails
A failed tile does not stop the other tiles. The SDK never gives you a
result with holes. run_area_and_wait (runAreaAndWait) sends the failed
tiles again one time, then raises an error that names them if a tile still
fails (Python: AreaRunError, with failed_tiles). For run_area
(runArea) and the retry keys, read Cost and retry.
Example: one run
import os
from infrared_sdk import InfraredClient, parse_epw
from infrared_sdk.analyses.types import (
AnalysesName, UtciModelBaseRequest, UtciModelRequest,
)
from infrared_sdk.models import Location, TimePeriod
client = InfraredClient(api_key=os.environ["INFRARED_API_KEY"])
payload = UtciModelRequest.from_weatherfile_payload(
payload=UtciModelBaseRequest(analysis_type=AnalysesName.thermal_comfort_index),
location=Location(latitude=48.21, longitude=16.37),
time_period=TimePeriod(start_month=7, start_day=15, start_hour=12,
end_month=7, end_day=15, end_hour=16),
weather_data=parse_epw("vienna.epw"),
)
result = client.run_area_and_wait(
payload, polygon,
buildings=buildings, # your meshes, in metres
vegetation=trees, # your trees
ground_materials=ground, # your ground layers
)
import { InfraredClient, initializeCore } from "@infrared-city/infrared-sdk-ts";
await initializeCore();
const client = new InfraredClient({ apiKey: process.env.INFRARED_API_KEY });
const result = await client.runAreaAndWait(
{
analysisType: "thermal-comfort-index",
latitude: 48.21, longitude: 16.37,
dateFilters: { period: { start: { month: 7, day: 15, hour: 12 },
end: { month: 7, day: 15, hour: 16 } } },
weather: client.weather.parseEpw(epwText),
},
polygon,
{ buildings, vegetation: trees, groundMaterials: ground },
);
Terrain goes in the analysis request as ground_geometry (groundGeometry).
Context geometry goes in the request of a ray-traced solar or interior
analysis as context_geometry (contextGeometry).
Reference
- Python: the client,
run_area_and_waitandrun_area. - TypeScript: the SDK reference,
InfraredClient.runAreaAndWaitandInfraredClient.runArea.
Coordinates
You give the area as a polygon in lon/lat. You give your geometry in metres. The SDK moves every layer into the frame of each tile. Most wrong results come from a wrong frame, so read this page once.
The rules
- The area is a polygon in lon/lat (WGS84,
[lon, lat]). Use one ring, no holes, no self-crossing. - The south-west corner of its bounding box is the origin (0, 0, 0) of your model: the smallest lon and the smallest lat.
- Your model is in metres. x is east, y is north, z is up. z is the height above the ground.
- Keep every coordinate below 100,000 m in size (absolute value). Do not send UTM or other absolute coordinates. The SDK refuses them.
- Trees and ground materials are in lon/lat, not in metres.
Buildings, terrain and context geometry share the metre frame.
The SDK moves it for you
Each tile has its own frame. The SDK moves every metre layer from the site frame into each tile frame. You do not do this.
A bare buildings map has no frame. The SDK reads it as "already in the
frame of the run polygon". An acquired buildings object keeps its origin.
So pass the object that you got:
- You get buildings for polygon A and run polygon B with the bare map: the whole city moves by the distance between the two south-west corners.
- You pass the acquired object: the SDK moves it from its own origin. One acquisition for a large polygon can serve several smaller runs.
What the SDK refuses
The SDK stops with an error, before you pay, when:
- the polygon is wrong, or a coordinate is not finite;
- a mesh has no
indices; - a ground layer name is unknown, or a terrain sheet is wrong;
- a tile coordinate is 100,000 m or more;
- you give more than 300,000 own sensor points.
Silent traps
The SDK does not catch these. The run is billed and the result is wrong.
| Trap | What happens |
|---|---|
| Lat and lon swapped | The SDK refuses a latitude above 90 in size. So it catches the swap in places like Tokyo or Sydney. It is silent for most of Europe and the Americas. |
| Y-up model | Many tools use y as height. The SDK needs z up. |
| Centimetres (or feet) | The SDK reads every number as metres. |
| Origin at the polygon centre | The origin must be the south-west corner of the bounding box. |
| Open meshes | A building needs a closed solid with a bottom face. A mesh with holes or without a bottom face gives wrong shade. |
Check your model in a viewer before you run. A building that stands at the right place and has the right height is a good sign.
Example: coordinates
buildings = client.buildings.get_area(polygon) # keeps its origin
result = client.run_area_and_wait(
payload, polygon,
buildings=buildings, # pass the object, not buildings.buildings
)
const buildings = await client.buildings.getBuildingsInArea(polygon);
const result = await client.runAreaAndWait(
request, polygon,
{ buildings }, // pass the object, not its inner map
);
Learn more
- Your inputs: all the layers and the weather.
- How a run works: the tiles and the steps.
- Python: the client. TypeScript: the SDK reference.
Tiling
The SDK cuts your area into square tiles. It sends one job for each tile. Tiles are 512 m wide. The step from tile to tile depends on the analysis. This page shows how the grid looks and what it means for your results.
Two tile families
- Solar family: all tiled analyses except wind. The step is 512 m, so tiles do not overlap. Each tile also reads geometry 128 m past its edge.
- Wind family: wind speed and pedestrian wind comfort. The step is 256 m, so neighbour tiles overlap by 50 % each way. Wind tiles read no extra margin.
Each family has its own grid. Analyses of one family share it. A run with a solar analysis and a wind analysis uses both grids.
Empty tiles and the tile limit
- A tile that does not touch your area is empty. The SDK skips it and sends no job.
- One run has a maximum of 100 non-empty tiles. A larger area needs an
explicit yes:
max_tiles_override(maxTilesOverride).
How the tiles join
Wind tiles overlap, so the SDK must choose which values to keep.
- Default: the SDK keeps the centre 256 m of each tile. The centres fit together with no gap.
- Directional blend: an option for wind speed only. It needs the wind
direction. In
merge_area_jobs, usestrategy="directional_blend"withwind_direction_deg(TypeScript:strategy,windDirectionDeg).
Far shading
A tile job only holds geometry up to 128 m past the tile edge. Wind tiles have a margin of 0 m.
- A tower in the margin is part of the job. Its shadow falls on the tile and the result counts it.
- A tall building farther away is not part of the job. Its shadow is missing from the result. The run gives no error.
- This is also true for
context_geometry(contextGeometry). Only the ray-traced solar models and the interior models take it. The SDK does not cut it. A mesh goes whole into each tile job whose margin it touches, and a mesh that touches no margin is dropped. No layer adds shade from beyond 128 m in 1.0.
Coming in 1.1: a context_reach_m setting (default 600 m) and a ring
mode for far geometry.
Advice: give far geometry as sparse, low-poly shapes. Each mesh costs upload size once for each tile that holds it.
Example: preview tiles
Count the tiles of each family before you run. This sends no job.
solar = client.preview_area(polygon, analysis_type="solar-radiation")
wind = client.preview_area(polygon, analysis_type="wind-speed")
print(solar.tile_count, wind.tile_count) # wind: about 4 times more
const solar = client.previewArea(polygon, { analysisType: "solar-radiation" });
const wind = client.previewArea(polygon, { analysisType: "wind-speed" });
console.log(solar.tileCount, wind.tileCount); // wind: about 4 times more
Learn more
- How a run works
- Cost and retry: how tiles become jobs and cost.
- Python: the client,
preview_area. - TypeScript: the SDK reference,
InfraredClient.previewArea.
Your inputs
Your own model is the normal input. Give the layers that you have. Each layer has a fixed shape. Weather and a time period come on top.
Your model, layer by layer
- Buildings: closed meshes
{id: {coordinates, indices}}in metres (x east, y north, z up). Each building is one closed solid with a bottom face. All outdoor analyses read them. Facade and roof sensors come from them. - Trees:
{id: GeoJSON Point}in lon/lat, withgenus,heightandcrownDiameter(metres). Thegenussets the crown shape. Height and diameter are optional: a tree with none gets 6 m height and 4 m crown, and gives no warning. An unknowngenusis a broadleaf tree. - Ground materials: one GeoJSON FeatureCollection in lon/lat for each
layer:
asphalt,concrete,soil,vegetation,water. An unknown layer name is an error. Only the thermal comfort analyses read them. - Terrain: a triangle mesh in the frame of the buildings, in
ground_geometry(groundGeometry). There is no input for a height map: make a mesh first. Maximum 500,000 triangles in one request.terrain_alignmentsets how your buildings and trees meet it:"as-is"(default),"auto-align"or"assume-aligned". Wind analyses refuse terrain. - Context geometry: far objects that only give shade, in
context_geometry(contextGeometry). The SDK does not analyse them. The four sun and light analyses and the interior models take them. Wind and thermal comfort do not. A request with context geometry also needs terrain, facade or roof sensors, or your own sensor points: on its own it is refused.
Meshes are in metres. See Coordinates for the frame.
Context geometry: the 1.0 limit
In 1.0, a context mesh reaches up to 128 m past a tile. An object that is farther away is not sent. It gives no shadow and no error. Keep far geometry sparse and low-poly: simple blocks for far buildings and hills.
Coming in 1.1: context_reach_m (default 600 m) and a ring mode.
Bring your own sensors
The ground grid is the default. You can choose where the values are:
- Facades and roofs:
analysis_surfaces(analysisSurfaces) is"facades","roofs"or"all".surface_grid_size(surfaceGridSize) sets the cell size in metres (default 2.0, minimum 0.25).surface_offset(surfaceOffset) sets the distance from the surface (default 0.1 m). This is an area run. See Facades and roofs. - Your own points:
sensor_points(sensorPoints) is a list of[x, y, z]points in metres, in the frame of your geometry. The list must not be empty and has at most 300,000 points for each job.sensor_normals(sensorNormals) is optional: one non-zero normal[x, y, z]for each point, in the same order. You cannot useanalysis_surfacesandsensor_pointsin one request.
Both work on the four sun and light analyses: solar radiation, sky view
factor, direct sun hours and daylight availability. Thermal comfort and
wind take no own sensors. The interior daylight factor takes
sensor_points and sensor_surfaces too. See
Interior.
Own points are not an area run: run_area and run_area_and_wait
(runArea, runAreaAndWait) refuse them. The SDK does not tile them. Send
your geometry with them in one request, and call
client.analyses.run_and_wait(request) (TypeScript:
client.runAndWait(request)). The result is a flat list with one value for
each point, under the key output.
request = SvfModelRequest(
analysis_type=AnalysesName.sky_view_factors,
geometries=my_tower, # your meshes, in metres
sensor_points=[[110, 90, 1.5], [110, 130, 15]],
sensor_normals=[[0, -1, 0], [0, 1, 0]],
)
result = client.analyses.run_and_wait(request)
values = result["output"] # one entry for each point
const result = await client.runAndWait({
analysisType: "sky-view-factors",
geometries: { tower: { coordinates, indices } },
sensorPoints: [[110, 90, 1.5], [110, 130, 15]],
sensorNormals: [[0, -1, 0], [0, 1, 0]],
});
Weather and time period
Thermal comfort, solar radiation and energy balance read weather.
Two sources
- Your own EPW file. Use any hourly
.epwfile with one year of data (8760 rows). The SDK refuses sub-hourly files. It reads and checks the file on your machine. Nothing is uploaded as a file. - The public catalog. It has 16,757 stations. Find the nearest one by location. A station holds one typical year (TMYx): each month is one real month from many years of records (2009 to 2023 for most stations).
The catalog has no forecast, no future climate and no single year that you choose. For those, use your own EPW file.
Time period
A time period is a date span with a daily hour range: start and end month, day and hour. 1 Jun to 31 Aug, 08 to 18 h, keeps 1,012 hours.
A winter window works too. 1 Dec to 28 Feb is one window of December, January and February (990 hours at 08 to 18 h). The values are in file order: January first.
Which column goes where
The SDK sends only the arrays that the analysis reads.
- Thermal comfort (UTCI and statistics): 7 columns. They are air temperature, humidity, wind speed, global, direct and diffuse radiation, and infrared from the sky.
- Solar radiation: direct and diffuse radiation.
- Energy balance (interior, Beta): air temperature and global radiation.
- Direct sun hours, daylight availability: no weather array. They need a time period and a location. Sky view factor and daylight factor need no weather input.
- Wind analyses: the wind comes from the weather file too, but you put it into the request yourself. Pedestrian wind comfort takes the hourly wind speeds and directions of your time window from the file. Wind speed takes one speed and one direction: pick them from the file or set them.
The SDK reads the other columns of the file but does not send them, for example dew point, pressure, sky cover, illuminance, rain and snow.
A gap in the file inside your window is an error before you pay. The SDK never fills a gap.
from infrared_sdk import parse_epw
stations = client.weather.get_weather_file_from_location(lat=48.21, lon=16.37)
rows = client.weather.filter_weather_data(
identifier=stations[0]["uuid"], time_period=winter, # winter: a TimePeriod, 1 Dec to 28 Feb
)
# or your own EPW file, read on your machine
weather = parse_epw("vienna.epw")
const stations = await client.weather.getWeatherFileFromLocation(48.21, 16.37, 100);
const hours = await client.weather.filterWeatherData(stations[0].uuid, {
period: { start: { month: 12, day: 1, hour: 8 },
end: { month: 2, day: 28, hour: 18 } },
});
// or your own EPW file (pass the text)
const weather = client.weather.parseEpw(await file.text());
Put the weather into the analysis request. See the example in How a run works.
No data yet? Public context
The SDK can read public data for your area:
| Layer | Public source |
|---|---|
Buildings (buildings.get_area, getBuildingsInArea) |
City overlay of Infrared where it exists, else Overture footprints |
Ground materials (ground_materials.get_area, groundMaterials.getArea) |
Roads from Infrared, with Overture water, land cover and land use |
Trees (vegetation.get_area, vegetation.getArea) |
Public Infrared tree data (no extra needed) |
Buildings and ground materials need the geodata extra: Python
pip install "infrared-sdk[geodata]", TypeScript
npm install hyparquet hyparquet-compressors. There is no public terrain.
Pass the buildings object itself to the run, not only its inner map. The object keeps its origin. See Coordinates.
Learn more
- Coordinates: the frame of your model.
- How a run works: the steps of one run.
- Python: the client. TypeScript: the SDK reference.
The ten analyses
SDK 1.0 has ten analyses. Eight are outdoor analyses. They run on an area, and the SDK cuts the area into tiles. Two are interior analyses. They are Beta. You can run several outdoor analyses in one call.
Outdoor wind
- Wind speed: the wind speed at each point, for one speed and one direction. Pick them from the weather file or set them. It takes no terrain.
- Pedestrian wind comfort: a comfort class for a criterion that you select. You give the hourly wind speeds and directions from the weather file. It takes no terrain.
Outdoor sun and light
- Solar radiation: the solar radiation over a time period.
- Sky view factor: how much open sky each point sees. It needs no weather and no time period.
- Direct sun hours: the hours of direct sun over a time period.
- Daylight availability: the share of daylight over a time period.
Only these four give values on facades and roofs. They also take your own
sensor points (sensor_points, up to 300,000 for each job) and give one value
for each point. See Bring your own sensors.
Outdoor thermal comfort
- Thermal comfort index (UTCI): the perceived outdoor temperature over a time period.
- Thermal comfort statistics: the percent of time in comfort, heat stress or cold stress.
Daylight factor and energy balance (Beta)
- Daylight factor: rooms: the daylight factor at sensor points in rooms, under a CIE overcast sky. A large floor runs in parts.
- Daylight factor: your sensors: the same overcast-sky method on
sensors that you give: your own points (
sensor_points), or your own horizontal surfaces, for example a roof (sensor_surfaces). It is not an area run.
Daylight availability (above) and the daylight factor are two different methods. Daylight availability uses the sun and the sky over a time window. The daylight factor uses a standard overcast sky and needs no time. - Energy balance: the monthly heating and cooling energy need of each zone. Run one building in one request.
Interior analyses do not run as an area run.
All analyses at a glance
| Analysis | Tells you | Values on | Needs | Tiling family | Unit |
|---|---|---|---|---|---|
Wind speed (wind-speed) |
Wind speed for one speed and direction | Ground | One wind speed and direction (from the file or set). No terrain. | Wind | m/s |
Pedestrian wind comfort (pedestrian-wind-comfort) |
Comfort class for a criterion | Ground | A criterion and the hourly wind speeds and directions from the weather file. No terrain. | Wind | Comfort class |
Solar radiation (solar-radiation) |
Radiation over a time period | Ground, facade, roof | Time period, diffuse horizontal and direct normal radiation | Solar | kWh/m² |
Sky view factor (sky-view-factors) |
Open sky at each point | Ground, facade, roof | No weather, no time period | Solar | % (0-100) |
Direct sun hours (direct-sun-hours) |
Hours of direct sun | Ground, facade, roof | Time period and location | Solar | h |
Daylight availability (daylight-availability) |
Daylight over a time period | Ground, facade, roof | Time period and location | Solar | % |
Thermal comfort index (thermal-comfort-index) |
UTCI over a time period | Ground | Time period and 7 weather columns | Solar | °C |
Thermal comfort statistics (thermal-comfort-statistics) |
Percent of time in comfort, heat or cold stress | Ground | Time period and the same 7 weather columns | Solar | % of time |
Daylight factor: rooms (daylight-factor) |
Daylight factor at sensor points (Beta) | Room | Walls, slabs, windows and sensors. No weather. | None (floor parts) | % |
Daylight factor: your sensors (daylight-factor) |
Daylight factor at points or on horizontal surfaces you give (Beta) | Your points, roof | Your points (sensor_points) or surfaces (sensor_surfaces), context geometry. No weather. |
None | % |
Energy balance (energy-balance) |
Monthly heating and cooling need (Beta) | Room | 2 weather series (default model). One building in one request. | None (one request) | kWh/m²·yr |
The tiling family sets the tile step and the context around each tile. See Tiling and context.
Learn more
- Your model: the inputs each analysis reads.
- Weather and time period: the weather columns.
- Results: read real values, for example UTCI.
- Interior analyses: daylight factor and energy balance.
- Facades and roofs: runs with values on surfaces.
- API reference: Python and TypeScript.
Interior (Beta)
Two interior models run on one building and not on an area: daylight
factor and energy balance. Both are in Beta. You send the walls, slabs
and windows. run_area refuses them.
Daylight factor: the inputs
The model gives the daylight factor in % under a CIE overcast sky. It has no date and no time.
barriers: the walls and slabs (category"wall"or"floor").openings: the windows (category"window"). A window can carry its own light transmittance from 0 to 1 (opening_factor=ininterior_entities). Without it,glazing_transmittanceapplies (default 0.63).spatial_volumes(optional): one closed volume for each room (category"space"), for results for each room.floors: the storeys to calculate, by index or by UUID.- The sensors: a grid on each floor (
grid_size, default 0.5 m;analysis_height, default 0.8 m), or your own sensors (see "Your own sensors" below). context_geometry: neighbours that only give shade. Without them the rooms read too bright.ground_geometryholds the terrain.room_reflectances: floor 0.2, walls 0.5, ceiling 0.7 by default.
The result is one value for each sensor point. With rooms, you also get the mean, minimum, maximum and the share of area at 2 % or more for each room.
Your own sensors
Bring your own sensors instead of the floor grid. This also works outside a room: the same overcast-sky method, for example on a roof.
sensor_points(sensorPointsin TypeScript): a list of[x, y, z]points in metres. The result has one value for each point.sensor_surfaces(sensorSurfaces): your own meshes by id. The model puts a sensor grid on them. The server refuses a surface that is not horizontal, so a facade does not work here. For facades, use an outdoor analysis withanalysis_surfaces.- If you send both,
sensor_pointswins andsensor_surfacesis ignored. The SDK refuses this. With either one,floorsis ignored: the SDK refusesfloors,floor_indexandfloor_uuidnext to them. - A request with your own sensors is one job. The SDK does not split it into parts.
- Add
context_geometryfor the neighbours that give shade. Barriers are optional when you bringcontext_geometry,ground_geometryor trees.
Parts of a large building
The SDK counts the sensors of each floor and packs whole floors into parts of about 300,000 points. A floor that is bigger than one part is split inside the floor. Each part is one job, and each job is billed. The SDK joins the parts exactly.
All parts must finish. If one part fails, you get a PartsRunError and no
result. Call again with retry_from=exc.schedule: the SDK sends only the
failed parts. A part that is sent again is a new job, so it is billed.
When the server supports it, the SDK uploads the scene one time and all parts use it. Else each part sends its own JSON.
To see the part count and the cost first, use
client.analyses.preview_parts(request). Energy balance has no parts: it
sends one request for each building.
Energy balance: the inputs
The model calculates the heating and cooling energy need of each zone (monthly method).
spatial_volumes: one closed volume for each zone (category"space"). This field is required.barriers: the walls ("wall") and every slab, the roof too ("floor").openings: the windows.context_geometryandground_geometry: neighbours and terrain. They give shade only withsolar_model="irradiance".- Weather: one year of hourly data from an EPW file, through
from_weather(parse_epw(...)). For a leap year (8784 hours), setyear. solar_model:"legacy-flat"is the default. It uses two series (dry_bulb_temperature,global_horizontal_radiation) and ignores shade."irradiance"is opt-in. It uses the shade geometry and needs latitude, longitude and four hourly series.energy_settings=EnergySettings(...): the U-values, glazing, set-points, gains, infiltration and thermal mass. Every field is optional, and an unset field uses the server default. The defaults includeu_values(ext_wall0.13,flat_roof0.25,ground_floor0.30 W/m²K),glazing(u_value1.1,shgc0.60,frame_fraction0.15),occupancy(heating_setpoint21 °C,cooling_setpoint26 °C,people_gains1.6 W/m²),ach_infiltration0.03 andconstruction_class="medium". See Material and building properties for all names, units and defaults.operation: when the plant runs. The default is continuous (24 hours, 7 days). For an office, useOperation.intermittent(hours=(8, 18), days="weekdays").
The ground reflectance for ground-reflected sun is ground_reflectance
(default 0.2). You cannot set a reflectance for each wall or window: the
properties apply to the whole request. For several buildings in one request,
each entry of buildings can override the request.
Keep one building whole in one request. A slab with heated rooms on both
sides is internal. If you send one storey alone, it loses heat through its
slabs. The result is the need in kWh/m²·yr (EUI_heat, EUI_cool) and 12
monthly values. It is not delivered energy: no boiler, heat pump or chiller
efficiency is applied.
Example: interior run
from infrared_sdk import PartsRunError, interior_entities, parse_epw
from infrared_sdk.analyses.types import (
AnalysesName, DaylightFactorModelRequest, EnergyBalanceModelRequest,
)
# my_barriers: interior_entities(...) of category "wall" and "floor"
windows = interior_entities(my_windows, category="window")
df = DaylightFactorModelRequest(
analysis_type=AnalysesName.daylight_factor,
barriers=my_barriers, openings=windows, floors=[0, 1],
context_geometry=interior_entities(my_neighbours),
)
try:
result = client.analyses.run_and_wait(df)
except PartsRunError as exc: # retry sends only the failed parts
result = client.analyses.run_and_wait(df, retry_from=exc.schedule)
eb = EnergyBalanceModelRequest.from_weather(
parse_epw("vienna.epw"), year=2021,
spatial_volumes=interior_entities(my_zones, category="space"),
barriers=my_barriers, openings=windows,
)
energy = client.analyses.run_and_wait(eb) # one job: the result dict
The TypeScript SDK has no typed request classes for these two models. Send
a plain object with the wire keys to client.runAndWait(request). A
daylight factor run gives a DaylightFactorResult, and
client.previewParts shows the parts. The TypeScript SDK has no typed
energy balance request, so use Python for the "irradiance" solar model.
const request = { "analysis-type": "daylight-factor",
barriers: myBarriers, openings: myWindows, floors: [0, 1] };
const result = await client.runAndWait(request) as DaylightFactorResult;
Learn more
- What comes back: the shape of an interior result.
- Python: the client.
- TypeScript: the SDK reference.
What comes back
An area run gives one merged result. A facade or roof run gives one value for each surface cell. An interior run gives one value for each sensor point. In all cases, read the values with the helper of the SDK. The helper gives you the real values, whatever type the server used to send them.
Three shapes
- Ground grid (area runs). The SDK merges the tiles and clips the result to your area. You get one map with cells of 1 m. A cell outside the area has no value.
- Facades and roofs (surface runs). You get one value for each cell of each surface, in columns.
- Interior points (daylight factor). You get one value for each sensor point. Each point has a room, or no room. See Interior.
| Python | TypeScript | |
|---|---|---|
| Ground grid | result.merged_grid, result.physical_grid() |
result.mergedGrid, areaGridValuesF32(result) |
| Surface cells | result.columns.values, columns.physical_values() |
columns.values, surfaceValuesF32(columns) |
| Interior points | result.columns.values, result.columns.room |
result.values, result.room |
Wire type and real values
The server stores each result in the smallest type that is accurate enough. The raw array keeps this wire type:
- f16 (half float): solar radiation, sky view factor, thermal comfort statistics, wind speed, and UTCI.
- f32: direct sun hours, daylight availability, and the class codes of pedestrian wind comfort.
Do not read the raw array. Use the helper. It handles every type, and it gives NaN to a cell with no value.
In TypeScript, an f16 grid is a Uint16Array of half-float bits, not of
numbers. areaGridValuesF32 and surfaceValuesF32 give you a Float32Array
of real values.
The legend range
The result holds the range of its own values: min_legend and max_legend
(minLegend and maxLegend in TypeScript). The SDK measures them from the
finished result. Use them for your colour scale. Class codes have no legend
range.
Example: read a result
result = client.run_area_and_wait(payload, polygon, buildings=buildings)
values = result.physical_grid() # float64, NaN = no value
print(result.min_legend, result.max_legend)
# a facade or roof run
cells = result.columns.physical_values()
import { areaGridValuesF32, surfaceValuesF32 } from "@infrared-city/infrared-sdk-ts";
const result = await client.runAreaAndWait(payload, polygon, { buildings });
const values = areaGridValuesF32(result); // Float32Array, NaN = no value
console.log(result.minLegend, result.maxLegend);
// a facade or roof run gives SurfaceColumns (see "Facade and roof runs")
const cells = surfaceValuesF32(surfaceResult);
Learn more
- Facade and roof runs: batches and render buffers.
- Interior: daylight factor and energy balance.
- Python: the client.
- TypeScript: the SDK reference.
Facade and roof runs
A facade or roof run puts sensors on the walls and roofs of your buildings. It
gives one value for each sensor cell. Only solar radiation, sky view factor,
direct sun hours and daylight availability run on surfaces. Set
analysis_surfaces (analysisSurfaces) to "facades", "roofs" or "all".
The sensors replace the ground grid.
Batches
- Sensors. The SDK puts a grid of sensor cells on the walls and roofs of the target buildings of each tile.
- Batches. The SDK puts the buildings into batches by sensor count. A batch has at most 250,000 sensors, counted exactly. The SDK groups the buildings by building id, not by place.
- One job for each batch. Each job is billed. A tile with no target building sends no job and costs nothing.
- The scene. When the server supports it, the SDK uploads the scene of a tile one time. The jobs of the tile use that upload.
- Shade. Neighbour buildings and the buildings of other batches only give shade. They get no result.
- Merge. The SDK joins the batches into one result.
To see the number of jobs before you run, use preview_area(..., payload=...)
and read would_bill_jobs. Without payload=, the facade count is too low.
Draw the result: render buffers
To draw the cells, you do not need one mesh for each cell. The render buffers are a few flat arrays that any renderer can read: deck.gl, three.js or raw WebGL. See Render buffers in detail.
A frame is one flat region (a wall or a roof part) with a regular grid of cells. The buffers hold:
outline: the triangles of each frame, in cell units. They follow the exact border of the surface.framesanddims: the corner and steps of each frame, and its columns, rows and first cell (cellStart).values: one value for each cell,f16orf32.validity: one bit for each cell. A cell with no value has value0and a clear bit. Values are never NaN. Test the bit, not the value.anchor: the centre of the run, in 64-bit floats. Keep it in the model transform of your scene.valueMin,valueMax,anyValid: the range for your colour scale.
The shader finds cell k = cellStart + i * nu + j from the outline point
(s, t): j = floor(s), i = floor(t). It reads values[k] and bit k & 7
of byte k >> 3 of validity. Draw frames with both faces. Keep one run to
one site, so the corners stay accurate.
Save, reload and free
The layout (frames and outline) depends only on the geometry. Save it one
time for each geometry. The values of each run are one blob with no
outline. It holds the layout_key of its layout. A live result needs no
layout: merge the run in the client that submitted it. Save the layout only to
draw later, or in another process. attach_values (attachValues) refuses a
layout that does not match the values. The Python SDK and the TypeScript SDK
can load the files that the other SDK saves.
To drop a schedule that you will not merge, call
client.forget_schedule(schedule) (client.jobs.captures.forgetSchedule).
Memory rules are in Render buffers in detail.
Example: facade run
from infrared_sdk.analyses.types import SvfModelRequest
from infrared_sdk.analyses.surface_columns import SurfaceColumns
from infrared_sdk.facade_layout import FacadeLayout, attach_values
payload = SvfModelRequest(analysis_type="sky-view-factors",
analysis_surfaces="facades", surface_grid_size=3.0)
result = client.run_area_and_wait(payload, polygon, buildings=my_buildings)
buffers = result.columns.render_buffers() # a live result
# later, from saved bytes
layout = FacadeLayout.from_bytes(saved_layout)
columns = SurfaceColumns.from_bytes(saved_values)
attach_values(layout, columns, expected_layout_key=columns.layout_key)
buffers = columns.render_buffers(layout=layout)
import { FacadeLayout, attachValues, surfaceRenderBuffers, type SurfaceColumns }
from "@infrared-city/infrared-sdk-ts";
const payload = { analysisType: "sky-view-factors",
analysisSurfaces: "facades", surfaceGridSize: 3 };
const columns = (await client.runAreaAndWait(payload, polygon,
{ buildings: myBuildings })) as SurfaceColumns;
const buffers = surfaceRenderBuffers(columns); // a live result
// later, from saved bytes
const layout = FacadeLayout.fromBytes(savedLayout);
attachValues(layout, columns, { expectedLayoutKey: savedLayoutKey });
const saved = surfaceRenderBuffers(columns, { layout });
layout.free();
Learn more
- What comes back: wire types and the helpers.
- Serve many users: a server that draws for many users.
- Python: the client.
- TypeScript: the SDK reference.
Where it runs
The SDK runs on your machine. Only the simulation models run in the Infrared cloud. This page shows where the SDK can run.
One core
The SDK has one core, written one time in Rust. Python and TypeScript use the same compiled core for the hard steps: tiling, geometry preparation, packing and merge. You get the same result in Python and in the browser.
Your machine sets the speed of these steps. The network and the cloud models set the rest. Measured on 2026-10-07 (1.0.0 packages, production API): one square kilometre of Vienna (4,212 buildings, own data) with sky view factor, wind and UTCI in one Python call took 8.7 s.
Python
- By default the SDK submits 8 tiles, polls with 5 threads and merges with 8 threads. The submit limit is 20.
- More than 8 threads do not help: 9.8 tiles/s with 8, 9.4 with 20.
Node.js
- Use Node 18 or later. The defaults are the same as in Python: submit 8, merge 8. The core uses one thread, and a network call does not block it.
- Call
await initializeCore()one time before a run call (runAreaorrunAreaAndWait). Without it, a run call throwsCoreNotReadyError. Only the public site reads (buildings.getBuildingsInArea,vegetation.getArea,groundMaterials.getArea) load the core by themselves. - For big merges, opt in to threads (Node 22 or later):
await initializeCore({ threads: 4 });
A 4 km facade merge took 5.5 s with 4 threads and 10.0 s without. Four is the best value measured. Threads do not speed up network calls. A failure on a thread ends the process, so use threads where a restart is acceptable.
Module format. The package is ESM. CommonJS require() works for the
root entry only. The types are in the package: use TypeScript 5.4 or later
(5.8 for CommonJS).
TypeScript in the browser
The browser runs the core on one thread and needs no special headers. Use a Web Worker to keep the page free.
- In the worker file, call
serveSdkWorker(). - On the page, call
createWorkerClient. Compile the WASM module one time and give it to the worker. CallinitializeCoreon the page and in each worker: each has its own core. - Put your API key in a proxy on your own origin. The browser never holds it.
- Optional: pass a
geometryUrlStorethat you own. A new worker then does not upload the tiles again.
// sdk.worker.ts
import { serveSdkWorker } from "@infrared-city/infrared-sdk-ts/worker";
serveSdkWorker();
// page
import { createWorkerClient } from "@infrared-city/infrared-sdk-ts/worker";
const sdk = createWorkerClient({
worker: new Worker(new URL("./sdk.worker.ts", import.meta.url), { type: "module" }),
module, // the compiled WASM module
config: { baseUrl: `${location.origin}/infrared-api` }, // your proxy
getToken: () => session.accessToken(), // your session, not the API key
});
For the proxy and the save and reload steps, read Serve many users.
A good worker pattern
Who does what. The worker prepares the site, uploads the geometry and
submits the jobs (runArea). It then downloads, decodes and merges the
results (mergeAreaJobs) and transfers the buffers to the page. When you use
runArea and mergeAreaJobs in a worker, poll the job status with a plain
InfraredClient on the page (same config and getToken). The worker client
has no runAreaAndWait. The page builds the render buffers.
Do
- Use one SDK worker for each signed-in session.
- Keep the heavy work in the worker and the API key in your proxy.
- Transfer the result buffers to the page. Do not clone big arrays.
- Use a
geometryUrlStoreto skip the upload after a reload. See Reuse uploads across reloads and workers. - Use one client for each process. In Python, keep
max_workersnear 8. - Free what you keep:
forget_schedule(forgetSchedule) andclose().
Do not
- Do not start one worker for each tile, or share one worker between two
clients (a second
createWorkerClienton the same worker throws). - Do not send a run again after a worker failure. A paid job can exist.
- Do not use
threadsin the browser. It refuses them. - Do not run many large merges in one process. A 4 km facade merge peaks at about 3 GB.
The sweet spot. One SDK worker, the default 8 jobs in flight, and 4 threads only in Node 22 or later for big merges. More workers use more memory: each has its own core and its own copy of the geometry.
Learn more
Serve many users
Use this guide when many people use your app to run facade or roof analyses and look at the results. You choose where the SDK runs. Both ways use the same SDK calls and saved formats. For the runtimes, read Where it runs.
Two ways
| A. In the browser (default) | B. On your server | |
|---|---|---|
| Who pays for the compute | Each user's device | Your server |
| Your server does | Hides the API key (a proxy), optional quotas | Holds the key, runs, merges and stores |
| Best for | Interactive apps | Batch jobs, no browser, shared results |
You can mix the two. A result that a server made can be drawn in a browser, and the other way round.
Never send the API key to a browser. In both ways the key stays on your server.
A. In the browser
The SDK runs in one Web Worker on the user's device. Use one client for each tab. It keeps the geometry once for all analyses of that user. For the worker code, read Where it runs.
The browser must call the API through a proxy on your own origin. The proxy:
- Checks that the user is signed in (your own check).
- Optionally counts the runs of the user against a quota.
- Replaces the
Authorizationheader with your key (X-Api-Key). - Forwards the request to
https://api.infrared.city/v2. - Also relays the storage URLs for upload and download, because the browser blocks them. The code below does not show this. See "Calling the API from a browser page" in the TypeScript README.
// A small proxy. Any server or edge function that can forward a request works.
// Add the relay for the storage URLs (step 5) yourself.
export default {
async fetch(request, env) {
const url = new URL(request.url);
if (!url.pathname.startsWith("/infrared-api/")) return new Response("Not found", { status: 404 });
const user = await verifySession(request.headers.get("Authorization"), env); // your check
if (!user) return new Response("Forbidden", { status: 403 });
const headers = new Headers(request.headers);
headers.delete("Authorization");
headers.set("X-Api-Key", env.INFRARED_API_KEY); // a secret on the server only
const target = "https://api.infrared.city/v2/" + url.pathname.slice("/infrared-api/".length) + url.search;
return fetch(target, { method: request.method, headers, body: request.body });
},
};
The SDK has no per-user API key. To limit each user, count in the proxy.
B. On your server
Use one client for each process. A second client holds the geometry again.
import os
from infrared_sdk import InfraredClient
client = InfraredClient(api_key=os.environ["INFRARED_API_KEY"])
import { InfraredClient, initializeCore } from "@infrared-city/infrared-sdk-ts";
await initializeCore(); // { threads: 4 } on Node 22+ for big merges
export const client = new InfraredClient({ apiKey: process.env.INFRARED_API_KEY });
- The client keeps the geometry of each facade or roof job in its own memory. Analyses on the same geometry share one copy.
- Submit and merge with the same client, in the same process. A schedule that another process merges gives a result with no outline. That process needs a saved layout.
Save and reload
Save two things in your own storage:
- The layout, one time for each geometry:
layout.toBytes()andlayout.layoutKey. - The values of each result:
surfaceColumnsToBytes(columns, { layoutKey })(Python:columns.to_bytes(layout_key=...)).
To reload, load both (FacadeLayout.fromBytes, surfaceColumnsFromBytes),
check them with attachValues, and call
surfaceRenderBuffers(columns, { layout }). To rebuild the layout of an older
run, pass the surfgridVersion (surfgrid_version) of that run. Leave
emitCellTris off: it makes the layout about three times larger. For the
code and the drawing steps, read
Render buffers in detail.
Reuse uploads across reloads and workers
The SDK keeps the URL of each uploaded tile geometry in memory. A new worker
or a page reload uploads all tiles again. A geometryUrlStore that you own
keeps the URLs, so the same geometry is not uploaded twice.
const geometryUrlStore = {
async get(key: string) { return await idbGet(key); }, // { url, expiresAt } or undefined
async set(key: string, url: string, expiresAt: number) {
await idbPut(key, { url, expiresAt });
},
};
serveSdkWorker({ geometryUrlStore /* , ...other options */ });
idbGet and idbPut stand for your own IndexedDB store.
- One store serves every worker of the session.
- The SDK makes the keys from a digest of the credentials. A renewed token gives a new key. Keep one store for each user.
- An entry expires after 23 hours. A refused URL is uploaded again by the SDK.
- An entry is a read link: delete the store at sign-out.
- Python keeps URLs in memory until they expire (the link lasts about 24 hours) and has no store option.
Clean up and limit
- Free the geometry of a schedule that you drop:
client.jobs.captures.forgetSchedule(schedule)orclient.forget_schedule(schedule). runAreaAndWaitfrees the jobs of a failed or aborted run itself.- At exit, call
client.jobs.captures.free()(TypeScript) orclient.close()(Python). - A merge of a large site uses much memory for a short time. A 4 km city facade run (about 400,000 surfaces) peaked at about 3 GB. Run 1 or 2 merges at a time in one process, and measure your own sites.
- On Node, a failure in a threaded core ends the process.
Checklist
- The API key is only on your server.
- Browser way: one worker client for each tab.
- Server way: one client for each process.
- The layout is saved one time for each geometry, with its
layoutKey. - Abandoned schedules go to
forgetSchedule(forget_schedule).
Cost and retry
One tile is one billed job. This page shows how to count the jobs before you run, what happens when a tile fails, and how the SDK keeps a retry from billing a job two times.
What is one job
- Grid analyses: one job for each tile.
- Facades: one job for each building batch.
- Daylight factor: one job for each part. Count the parts with
client.analyses.preview_parts(request)(Python) orclient.previewParts(request). - Several analyses: each family has its own grid, so each has its own jobs. Solar-family analyses on the same grid and layers share the uploads. An upload saves time. It does not change the job count.
There is no result cache. The same run again is billed again.
Count before you run
Use preview_area (previewArea). It sends no job.
- Always give the analysis:
analysis_type=(analysisType) orpayload=. Without it, the preview uses the wind grid. A solar count is then about 4 times too high, and you get a warning. - Python: price from
would_bill_jobs, not fromtile_count. For facades, givepayload=. - TypeScript:
previewAreahas no job count. It returnstileCountandestimatedCostTokensat the default price per job. This is right for a grid analysis. For facades, and to get the job count, useawait previewAreaBatches(input, polygon, options). It readsplannedJobCountfrom the plan thatrunAreawould send. Pass the sameinputand buildings as the run. For the live price, usepreviewAreaWithPricing. - One call prices one analysis. For three analyses, call it three times and add the results.
When a tile fails: retry keys
A failed tile does not stop the other tiles. The merge gives no partial map.
Python raises AreaRunError. TypeScript throws an Error.
run_area_and_wait(runAreaAndWait) sends the failed tiles again one time (retries=1). It raises only if the retry fails too.retries=0turns this off.- With
run_area(runArea), you retry yourself: wait for the jobs first, then useretry_from=schedule(retryFrom: schedule). The SDK sends only the tiles that need it.
Keys: no double bill
Each job has an idempotency key. One key gives at most one job.
- No answer (for example, the network drops it): the SDK sends the same key again, up to 6 sends in all. The server returns the job it already has.
- Failed or refused: the retry gets a new key and makes a new job.
No credits (402)
On HTTP 402 the SDK stops sending the other tiles and does not retry. Tiles sent before may still run and bill. Add credits, then retry.
Example: count and retry
preview = client.preview_area(polygon, analysis_type="solar-radiation")
print(preview.would_bill_jobs) # price from this number
schedule = client.run_area(payload, polygon, buildings=my_buildings)
# ... some tiles failed: send only those again
schedule = client.run_area(payload, polygon, buildings=my_buildings,
retry_from=schedule)
const preview = await client.previewAreaBatches(input, polygon, { buildings: myBuildings });
console.log(preview.plannedJobCount); // price from this number
let schedule = await client.runArea(input, polygon, { buildings: myBuildings });
// ... some tiles failed: send only those again
schedule = await client.runArea(input, polygon, {
buildings: myBuildings, retryFrom: schedule,
});
Learn more
- How a run works
- Tiling
- Python: the client,
preview_areaandrun_area_and_wait. - TypeScript: the SDK reference,
previewAreaandrunAreaAndWait.
Draw facade and roof results fast
A facade or roof run gives one value for each cell of each surface. To draw these cells, you do not need one mesh for each cell. Use the render buffers. They are a few flat arrays that any renderer can read.
Use the render buffers when you draw a facade or roof result on screen, in deck.gl, three.js or raw WebGL.
What the render buffers are
A surface result has many frames. A frame is one flat region (a wall or a roof part) with a regular grid of cells. The buffers hold:
| Buffer | Type | Meaning |
|---|---|---|
anchor |
f64 x 3 | Centre of the run. Add it back in 64-bit floats. |
frames |
f32 x 9 for each frame | corner, uStep and vStep, relative to anchor. |
dims |
u32 x 3 for each frame | Columns nu, rows nv, and cellStart (the first cell of the frame in values). |
outline |
f32 x 6 for each triangle | The triangles of each frame, as s0 t0 s1 t1 s2 t2. |
outlineOffsets |
u32 x (frames + 1) | Frame f owns the triangles outlineOffsets[f] to outlineOffsets[f + 1]. |
values |
f16 or f32 for each cell | The value of a cell. 0 when the cell has no value. |
validity |
u8, one bit for each cell | Cell k is bit k & 7 of byte k >> 3. A set bit means the cell has a value. |
valueMin, valueMax, anyValid |
numbers | The range over the cells that have a value, for your colour scale. |
Important points:
- The format does not depend on a renderer. It is plain arrays.
- One outline for each frame. You draw the outline triangles of a frame. The outline gives the exact border of the surface. There is no mesh for each cell, so the buffers are small.
- Outline coordinates are in cell units. A point
(s, t)of the outline is atcorner + s * uStep + t * vStep. A cell(i, j)coverssfromjtoj + 1andtfromitoi + 1. - Values keep their native type. The server sends half floats (
f16) for solar radiation and sky view factor. It sends 32-bit floats for sun hours and daylight availability. The buffers givef16forf16results andf32for all others. Nothing is widened to 64 bits. - Validity is a separate bit. A cell with no value (for example, a cell
under terrain) has
values[k] = 0and a clear validity bit. Values are never NaN or infinite. Test the bit, not the value.
Get the buffers
TypeScript
import {
InfraredClient, initializeCore, surfaceRenderBuffers, type SurfaceColumns,
} from "@infrared-city/infrared-sdk-ts";
await initializeCore();
const client = new InfraredClient({ apiKey: process.env.INFRARED_API_KEY });
// A facade run returns SurfaceColumns. `runAreaAndWait` is typed for every
// analysis, so narrow the result.
const columns = (await client.runAreaAndWait(
{ analysisType: "sky-view-factors", analysisSurfaces: "facades", surfaceGridSize: 3 },
polygon,
{ buildings },
)) as SurfaceColumns;
const buffers = surfaceRenderBuffers(columns);
console.log(buffers.valueDtype, buffers.values.length, buffers.valueMin, buffers.valueMax);
valueDtype is "f16" or "f32". For "f16", values is a Uint16Array of
half-float bits. Node 18 to 22 has no Float16Array, so the SDK always gives
bits.
Python
from infrared_sdk import InfraredClient
from infrared_sdk.analyses.types import SvfModelRequest
client = InfraredClient(api_key="...")
payload = SvfModelRequest(
analysis_type="sky-view-factors",
analysis_surfaces="facades",
surface_grid_size=3.0,
)
result = client.run_area_and_wait(payload, polygon, buildings=buildings)
buffers = result.columns.render_buffers()
print(buffers.values.dtype, buffers.values.shape, buffers.value_min, buffers.value_max)
For float16 input, buffers.values is a float16 array. Python names are
snake case: u_step, outline_offsets, value_min, any_valid.
Live result and saved layout
The outline comes from the layout of the surfaces: the frames and the borders, which depend only on the geometry. There are two ways to get it.
- Live result. After a run in the same client, the result holds the
outline. Call
surfaceRenderBuffers(columns)orcolumns.render_buffers(). You must merge the run in the client that submitted it. A schedule that you merge in another process has no outline, and the call throws an error that names the missing outline. - Saved layout. Save the layout once (
layout.toBytes()orlayout.to_bytes()). Later, load it (FacadeLayout.fromBytes,FacadeLayout.from_bytes) and pass it in. The outline of each surface comes from the layout, by surface id.
import { FacadeLayout, attachValues, surfaceRenderBuffers } from "@infrared-city/infrared-sdk-ts";
const layout = FacadeLayout.fromBytes(savedBytes);
attachValues(layout, columns, { expectedLayoutKey: savedLayoutKey }); // throws on a mismatch
const buffers = surfaceRenderBuffers(columns, { layout });
layout.free();
from infrared_sdk.facade_layout import FacadeLayout, attach_values
layout = FacadeLayout.from_bytes(saved_bytes)
attach_values(layout, columns, expected_layout_key=saved_layout_key) # raises on a mismatch
buffers = columns.render_buffers(layout=layout)
Save the values of each result as one blob, and link it to the layout by
layoutKey:
const blob = surfaceColumnsToBytes(columns, { layoutKey: layout.layoutKey });
// later:
const { columns, layoutKey } = surfaceColumnsFromBytes(blob);
attachValues(layout, columns, { expectedLayoutKey: layoutKey! });
blob = columns.to_bytes(layout_key=layout.layout_key)
# later:
columns = SurfaceColumns.from_bytes(blob)
attach_values(layout, columns, expected_layout_key=columns.layout_key)
The blob holds the values in their native type, and no outline. A Python
blob has no legend and no aggregates (Python columns have none). A reader
refuses a blob of a version it does not know. attachValues refuses a layout
that does not match the result. The saved outline is
quantized. It is at most nu / 131070 cells (nv / 131070 for the other
axis) from the live outline.
A layout from synthesizeFacadeLayout or FacadeLayout.toBytes() does not
hold the per-cell triangles unless you set emitCellTris: true
(emit_cell_tris=True). Do not set it to draw with render buffers. It makes
the layout about three times larger.
A layout saved by an older SDK (format version 2) has no outline. Loading it
fails with layout v2 has no outline; re-run the analysis. Save the layout
again.
Draw with deck.gl, three.js or WebGL
The renderer draws the outline triangles of each frame. The fragment shader finds the cell and reads its value. This is a summary, not a full renderer.
- Positions. For each outline vertex
(s, t)of framef, the position iscorner_f + s * uStep_f + t * vStep_f. All three are inframes, relative toanchor. Do this once on the CPU in 32-bit floats. The error is about 0.25 mm at 2 km from the anchor. - Cell coordinates. Give
(s, t)to the shader as a varying. Theoutlinearray is already a ready attribute: two floats for each vertex. - Frame data. Give
nu,nvandcellStartof the frame to each vertex as an unsigned integer attribute (flat varying). - Cell lookup. In the fragment shader:
j = clamp(floor(s), 0, nu - 1),i = clamp(floor(t), 0, nv - 1),k = cellStart + i * nu + j. - Value and validity. Upload
valuesas a texture (R16FwithHALF_FLOATforf16,R32Fforf32). Uploadvalidityas anR8UItexture. Read cellk, test bitk & 7of bytek >> 3, discard the fragment or draw a neutral colour when the bit is clear, and map the value withvalueMinandvalueMax. - Double-sided. Draw frames with both faces. A wall can face any way.
- Position in the scene. Put
anchorin the model transform in 64-bit floats (three.js: set the mesh position; deck.gl: useMETER_OFFSETScoordinates withanchoras the origin). Keep one run to one site. Frames that are kilometres apart make the f32 corners less accurate.
This code builds the vertex arrays from the buffers (TypeScript). It runs as it is:
import type { SurfaceRenderBuffers } from "@infrared-city/infrared-sdk-ts";
function vertexArrays(b: SurfaceRenderBuffers) {
const frames = b.dims.length / 3;
const vertices = b.outline.length / 2;
const position = new Float32Array(vertices * 3); // relative to b.anchor
const frameDims = new Uint32Array(vertices * 3); // nu, nv, cellStart
for (let f = 0; f < frames; f += 1) {
const [cx, cy, cz, ux, uy, uz, vx, vy, vz] = b.frames.subarray(9 * f, 9 * f + 9);
for (let v = 3 * b.outlineOffsets[f]; v < 3 * b.outlineOffsets[f + 1]; v += 1) {
const s = b.outline[2 * v];
const t = b.outline[2 * v + 1];
position.set([cx + s * ux + t * vx, cy + s * uy + t * vy, cz + s * uz + t * vz], 3 * v);
frameDims.set(b.dims.subarray(3 * f, 3 * f + 3), 3 * v);
}
}
return { position, cell: b.outline, frameDims }; // `cell` is (s, t) for each vertex
}
Memory
- Buffers are yours. Each array owns its memory. In TypeScript you can
transfer them to a worker (
postMessage(msg, [buffers.frames.buffer, ...])). They stay valid when WebAssembly memory grows. In Python they are NumPy arrays. - The outline arrays are your own.
buffers.outlineandbuffers.outlineOffsetsare the same objects ascolumns.outlineandcolumns.outlineOffsets. The SDK does not copy them. If you transfer them, the columns lose them. Do not change them. - Values are not widened.
f16takes 2 bytes for each cell,f32takes 4. - Free what the client keeps. The client keeps the capture of each facade
or roof job until you merge the run. If you will not merge a schedule, call
client.jobs.captures.forgetSchedule(schedule)(TypeScript) orclient.forget_schedule(schedule)(Python). When you finish with the client in TypeScript, callclient.jobs.captures.free(). In Python,client.close()(or leaving awithblock) clears the kept captures. Calllayout.free()for eachFacadeLayoutin TypeScript.
For servers that draw for many users, see Serve many users.
Configuration and limits
Environment variables
Each setting resolves in this order: constructor argument, environment
variable, default. The SDK does not read .env files.
| Variable | SDK | Sets | Default |
|---|---|---|---|
INFRARED_API_KEY |
both | Your API key | none (required) |
INFRARED_BASE_URL |
both | Gateway URL | https://api.infrared.city/v2 |
INFRARED_GEOMETRY_REF_ENABLED |
both | false turns off geometry reuse between jobs |
on |
INFRARED_APPLICATION |
Python | Calling surface (application=) |
sdk |
INFRARED_SDK |
Python | Calling library and version (sdk_id=) |
infrared-sdk/<version> |
INFRARED_QUIET |
Python | Hides the one-time start-up INFO log | unset |
INFRARED_SDK_DEBUG |
Python | More diagnostic output | unset |
INFRARED_OVERTURE_TRANSPORT |
Python | Overture read path: auto, s3 or https |
auto |
In TypeScript, pass env in the client config to give these values without
process.env. A value in env has priority.
Identify your application
The SDK sends two headers with each call: x-infrared-application (the
surface that made the call) and x-infrared-sdk (the library and its
version). If you build a plugin or a connector, set them. Then your traffic is
not counted as generic SDK traffic. Your value is added in front of the SDK
token, so the SDK version stays visible.
client = InfraredClient(api_key=key, application="qgis", sdk_id=f"infrared-qgis/{version}")
The TypeScript client sets x-infrared-sdk itself. You choose only the
surface, from a fixed list of names (default script).
const client = new InfraredClient({ apiKey, surface: "qgis" });
Limits
| Limit | Value | When it is broken |
|---|---|---|
| Non-empty tiles in one run | 100 | The run is refused before any job. Pass max_tiles_override (Python) or maxTilesOverride (TypeScript), and read the price first. |
| Own sensor points in one job | 300,000 | The payload is refused when you build it. |
| Facade sensors in one batch | about 250,000 (target) | The SDK plans more batches. |
| Terrain triangles | 500,000 | The server refuses the job. |
| Triangles in one tile scene | 5,000,000 (all layers) | The server refuses the job (HTTP 422). |
| Size of one request | 64 MiB | The server refuses the job (HTTP 413). |
context_geometry |
about 50,000 triangles for the whole site | The SDK logs a warning. Each tile job carries it. |
| Local coordinates | below 100,000 m | Use local metres, not UTM. |
Material and building properties
These are the properties you can set. Python uses snake case. The wire and
TypeScript use the kebab-case or camelCase name (ach_infiltration is
ach-infiltration; wall_albedo is wallAlbedo in TypeScript). In
TypeScript, the energy balance and daylight factor have no typed classes:
send the wire keys.
Energy balance (EnergySettings, see Interior). All
fields are optional. The settings apply to the whole request.
| Property | Default | Unit |
|---|---|---|
u_values.ext_wall, flat_roof, ground_floor |
0.13, 0.25, 0.30 | W/m²K |
glazing.u_value (centre of glass) |
1.1 | W/m²K |
glazing.shgc |
0.60 | 0 to 1 |
glazing.frame_fraction |
0.15 | 0 to 1 |
glazing.frame_u_value |
1.4 | W/m²K |
glazing.edge_psi |
0.06 | W/mK |
occupancy.heating_setpoint, cooling_setpoint |
21, 26 | °C |
occupancy.people_gains, lighting_gains, equipment_gains |
1.6, 1.5, 2.0 | W/m² floor |
occupancy.ach_natural |
0.5 | air changes per hour |
ach_infiltration |
0.03 | air changes per hour |
construction_class (thermal mass) |
"medium" |
very-light, light, medium, heavy, very-heavy |
thermal_bridge_surcharge |
automatic | W/m²K |
ground_reflectance (albedo of the ground) |
0.2 | 0 to 1 |
ground_level_z |
0.0 | m, absolute z |
operation (Operation.continuous() or .intermittent(hours, days)) |
continuous | hours [start, end], days "weekdays" or "all" |
solar_model |
"legacy-flat" |
"legacy-flat" or "irradiance" |
The model picks the U-value key from the height of a slab or wall in its zone
(roof at the top, ground floor at the bottom, else wall). An unknown
construction_class counts as medium. With an intermittent operation,
give the gains and ach_natural as averages for the hours the plant runs.
You cannot set a value for one wall or one window. There is no emissivity
setting in this model.
Daylight factor.
| Property | Default | Note |
|---|---|---|
room_reflectances (floor, walls, ceiling) |
0.2, 0.5, 0.7 | Set it for the whole request. |
glazing_transmittance |
0.63 | For a window with no opening_factor. |
opening_factor (in interior_entities) |
none | Light transmittance of one window, 0 to 1. |
exterior_ground_reflectance |
none | The server reads it, but it has no effect on the result. |
Outdoor thermal comfort (thermal-comfort-index and
thermal-comfort-statistics only; the other models refuse these fields).
Leave a field unset to use the model default. Python names:
| Property | Range | Note |
|---|---|---|
wall_albedo |
0 to 1 | All walls. |
wall_absorptivity |
0 to 1 | All walls. Server default 0.75. A building can carry its own absorptivity on its entry in buildings. |
ground_albedo |
0 to 1 | All ground, over the table of each material. |
ground_dt_max |
0 to 50 K | Largest day lift of the ground surface temperature. |
canopy_transmissivity |
0 to 1 | All trees. A tree can carry its own transmissivity in its GeoJSON properties. The built-in leaf-on value is 0.03 (palm 0.30). |
The first matching value wins: the entry of one building or tree, then the request-wide field, then the built-in value.
Ground materials are fixed on the server. The SDK layers are asphalt,
concrete, water, soil and vegetation. Each name has a built-in albedo
and a temperature lift in the model, and the SDK cannot read them. An
albedo or dt-max in the properties of a ground feature is ignored: only
the geometry and the name count. To change the albedo, use ground_albedo
for the whole request. The SDK has no field for emissivity (the wall
emissivity is fixed on the server at 0.90).
Tree leaf-off values are in Leaf-off vegetation.
Leaf-off vegetation
A tree with no leaves lets more sun through. The SDK decides leaf-on or leaf-off for each sun hour, from its month and the site latitude.
- North of 23.5 deg N: leaf-off from November to March. South of 23.5 deg S: from May to September. In the tropics, a tree is always leaf-on.
- It changes
daylight-availability,direct-sun-hoursandsolar-radiation.sky-view-factorsalways uses leaf-on. - A deciduous tree lets 0.45 of the direct beam through when bare. Evergreen trees (conifer, palm) keep their leaf-on value all year.
- An unknown genus counts as broadleaf deciduous.
- To change one tree, set
"transmissivity-leaf-off"(0 to 1) in its GeoJSONproperties:
for f in trees.features:
if f["properties"].get("genus") == "tilia":
f["properties"]["transmissivity-leaf-off"] = 0.6
Grid images
- Python:
sdk.weather.gen_grid_image(grid=..., analysis_type=..., criteria=...)gives a PNG in the process. - TypeScript:
await renderGridPng(grid, { analysisType, criteria })gives a PNG, one pixel per cell, up to 960 px on the long side.
TypeScript project setup
- Use TypeScript 5.8 or later. Types compile with
strictand withoutskipLibCheck. ESM-only projects also work from 5.4. - Set
moduleandmoduleResolutiontoNodeNextorBundler.Node10does not work, because it cannot read the packageexports. - CommonJS
require()works for the root entry only, and needs TypeScript 5.8. - If you set
libby hand, keepDOMor add@types/node.