Client
Infrared SDK client.
Provides InfraredClient, the main entry point for the Infrared
City API. Supports area-level tiled orchestration, webhook integration,
and building/vegetation/ground-material queries.
BASE_URL_ENV_NAME
module-attribute
BASE_URL_ENV_NAME = 'INFRARED_BASE_URL'
Name of the environment variable that supplies the API base URL.
API_KEY_ENV_NAME
module-attribute
API_KEY_ENV_NAME = 'INFRARED_API_KEY'
Name of the environment variable that supplies the API key.
DEFAULT_BASE_URL
module-attribute
DEFAULT_BASE_URL = 'https://api.infrared.city/v2'
API base URL used when no valid base_url is given.
InfraredClient
Main client for the Infrared City API.
Supports area-level orchestration via run_area /
run_area_and_wait, and webhook management via
self.webhooks.
Resources for agents and integrators: the SDK documentation at https://infrared.city/docs/sdk/, and the agent skills (Claude Code / Cursor / Codex / Copilot / Windsurf) and runnable cookbook notebooks in https://github.com/Infrared-city/infrared-skills.
A single INFO log line on first instantiation per process surfaces
these links to debug sessions. Set INFRARED_QUIET=1 to silence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
api_key
|
str
|
API key. When omitted it is read from the |
None
|
logger
|
Logger
|
Logger for client messages. Defaults to this module's logger. |
None
|
base_url
|
str
|
Absolute |
None
|
transport
|
(json, binary)
|
Default request representation for runs started by this client.
|
"json"
|
execution_config
|
ExecutionConfig
|
Execution mode. |
exec_async
|
application
|
str
|
Calling surface reported in |
None
|
sdk_id
|
str
|
Calling library and version reported in |
None
|
jobs_service_client
|
JobsServiceClient
|
Job client to use instead of the one this client creates. A client
you inject is not closed by |
None
|
analysis_service_client
|
AnalysisServiceClient
|
Analysis client to use instead of the default. |
None
|
weather_service_client
|
WeatherServiceClient
|
Weather client to use instead of the default. |
None
|
vegetation_service_client
|
VegetationServiceClient
|
Vegetation client to use instead of the default. |
None
|
ground_materials_service_client
|
GroundMaterialsServiceClient
|
Ground-material client to use instead of the default. |
None
|
landuse_service_client
|
LandUseServiceClient
|
Land-use client to use instead of the default. |
None
|
buildings_service_client
|
BuildingsServiceClient
|
Buildings client to use instead of the default. |
None
|
webhooks_service_client
|
WebhooksServiceClient
|
Webhooks client to use instead of the default. |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
api_key |
_SharedApiKey
|
Holder of the API key. Copying or pickling the client drops the key; create a new client instead of reusing a copy. |
telemetry |
Telemetry
|
The resolved |
execution_config |
ExecutionConfig
|
Execution mode. |
transport |
str or None
|
Default transport, or |
base_url |
str
|
The resolved API base URL, without a trailing |
jobs |
JobsServiceClient
|
Job submission, status and result access. |
analyses |
AnalysisServiceClient
|
Single-tile analysis execution. |
weather |
WeatherServiceClient
|
Weather catalog access. |
vegetation |
VegetationServiceClient
|
Vegetation (tree) data access. |
ground_materials |
GroundMaterialsServiceClient
|
Ground-material data access. |
landuse |
LandUseServiceClient
|
Land-use data access. |
buildings |
BuildingsServiceClient
|
Building data access. |
webhooks |
WebhooksServiceClient
|
Webhook management. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If no API key is given and |
Notes
An injected service client (the *_service_client arguments) keeps
whatever telemetry it was constructed with on its own session, the same
contract as its api_key. The area paths (run_area,
merge_area_jobs, check_area_state) build their own per-thread
clients and use the client-level value resolved here, not the injected
client's. So an injected jobs_service_client carrying a different
application reports its own on client.jobs.submit() and this
client's on an area run. Pass application / sdk_id here rather
than pre-labelling an injected client if you want one value everywhere.
close
close() -> None
Close the resources this client owns (sessions it created itself).
Service clients passed in through the *_service_client arguments
are left open.
preview_area
preview_area(
polygon: dict,
max_tiles_override: Optional[int] = None,
analysis_type: Optional[str] = None,
*,
payload: Optional[AnalysesUnion] = None,
buildings: Optional[Mapping[str, dict]] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
max_sensors_per_job: Optional[float] = None,
terrain_context_margin_m: Optional[float] = None,
) -> AreaPreview
Preview tiling for a polygon without running any analyses.
Examples:
# Wrong for solar — uses wind grid (256 m step)
preview = client.preview_area(polygon)
# Right — solar-radiation grid (512 m step)
preview = client.preview_area(
polygon, analysis_type="solar-radiation"
)
Wire-format analysis names (kebab-case, matching the API):
"wind-speed", "pedestrian-wind-comfort",
"solar-radiation", "direct-sun-hours",
"daylight-availability", "sky-view-factors",
"thermal-comfort-index", "thermal-comfort-statistics".
Warnings
The default grid is wind (256 m step). Calling preview_area(polygon)
with no analysis_type returns the wind-grid tile count for backwards
compatibility. Solar / daylight / thermal-comfort analyses run on a 512 m
grid (~4x fewer tiles per area), so the default preview over-counts tiles
by ~4x and under-estimates cost for solar-family workflows. Always pass
analysis_type when you know which analysis you will run, with the same
value your run_area payload uses, so the estimate matches what the run
actually submits (and charges). Omitting analysis_type emits a
UserWarning at runtime.
A facade estimate needs payload= (and usually buildings=). Requests
with analysis_surfaces set are transparently split into multiple
separately billed sub-jobs when a tile's estimated synthesized sensor count
exceeds the server cap. Without payload=, this preview has no geometry
to split and reports one job per tile -- an UNDER-estimate for a facade
run. Pass the real payload (and buildings) to get the real count. See the
README's "Facade & Terrain Analysis" section. Note that a repeated facade
run still bills in full even when nothing on the client changed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
polygon
|
dict
|
A GeoJSON Polygon object. |
required |
max_tiles_override
|
int
|
Override the default maximum number of non-empty tiles. |
None
|
analysis_type
|
str
|
Wire-format analysis name (see warning above). Selects the tile
grid: wind types use a 256 m step (50 % overlap); solar /
daylight / thermal-comfort types use a 512 m step (no overlap,
~4x fewer tiles per area). |
None
|
payload
|
AnalysesUnion
|
The SAME payload you would pass to |
None
|
buildings
|
mapping
|
The same per-analysis layers |
None
|
vegetation
|
mapping
|
The same per-analysis layers |
None
|
ground_materials
|
mapping
|
The same per-analysis layers |
None
|
max_sensors_per_job
|
float
|
Per-job sensor cap for a facade payload, in retained sensors: a
whole number from 1 to 250 000. A fractional value is rounded
down with a |
None
|
terrain_context_margin_m
|
float
|
Terrain reach in metres, as for |
None
|
Returns:
| Type | Description |
|---|---|
AreaPreview
|
|
Raises:
| Type | Description |
|---|---|
PolygonValidationError
|
If |
ValueError
|
If |
Warns:
| Type | Description |
|---|---|
UserWarning
|
If neither |
merge_buildings
staticmethod
merge_buildings(buildings_map: Dict[str, Optional[dict]]) -> dict
Combine per-tile buildings dicts into one for visualization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
buildings_map
|
dict[str, dict or None]
|
Maps tile id to that tile's buildings dict, or |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The combined buildings dict: list values are concatenated and
other keys are taken from the last dict that has them. An empty
dict if every value is |
run_area
run_area(
payload: AnalysesUnion,
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
retry_from: Optional[AreaSchedule] = None,
known: Optional[Dict[str, Any]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
) -> AreaSchedule
run_area(
payload: List[AnalysesUnion],
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
retry_from: Optional[AreaSchedule] = None,
known: Optional[Dict[str, Any]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
) -> List[AreaSchedule]
run_area(
payload: Union[AnalysesUnion, List[AnalysesUnion]],
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
retry_from: Optional[AreaSchedule] = None,
known: Optional[Dict[str, Any]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
) -> Union[AreaSchedule, List[AreaSchedule]]
Submit tiled analysis jobs over a polygon.
max_workers sizes the submit pool: how many tile POSTs are in
flight at once. The default is
DEFAULT_SUBMIT_WORKERS (8),
where throughput measurably stops improving; a caller who has
measured their own workload may ask for up to
MAX_SUBMIT_WORKERS (20). It is
NOT the merge pool — merge_area_jobs sizes that one separately
— and it does nothing for the first few seconds of a run, which
are serial layer preparation.
Layer parameters (buildings, vegetation, ground_materials): None
or {} skips injection (empty per tile), a non-empty mapping uses
the provided data. Do not mutate the layer mappings while a call runs.
buildings also accepts the AreaBuildings
object buildings.get_area returns, and then re-anchors from the
frame that object RECORDS — so acquiring once for a large polygon and
running several sub-areas places the buildings correctly. A bare map
carries no frame and is read as being in the run polygon's own, which
is why the README says to acquire with the polygon you run.
buildings entries must carry both coordinates and indices
— as DotBimMesh or as a dict; the server discards a mesh missing
either, before compute and after the charge, so one is refused here.
vegetation features may be Point, Polygon or
MultiPolygon; a polygon is seated at its outer ring's centroid.
Any other vegetation geometry type raises.
retry_from (resubmit only the failed tiles/batches of a prior
run, carrying the succeeded jobs forward) resubmits
failed_submissions only; uncertain_submissions (a tile whose
request may already have created a job) are carried forward
untouched and never resubmitted. retry_from describes exactly one
payload's prior run, so it is not supported with a multi-payload
list: run_area([A, B], polygon, retry_from=schedule_B) raises
ValueError. Retry each payload separately —
run_area(B, polygon, retry_from=schedule_B). The retry payload
must also match the original run's config_hash and buildings map
(both guarded — a mismatch raises).
terrain_context_margin_m widens how far ground_geometry is
sliced beyond each tile. The default (None) covers the buildings
and trees the tile actually analyses and nothing more. This extent is
independent of terrain_alignment: the SDK's supplied-scene
default is as-is and does not seat or validate those solids.
Long-range relief belongs in context_geometry.
Raise it only for a site where distant terrain genuinely shades the
tile (a valley, an escarpment); the payload grows roughly with the
square of the reach. Values below the tile config's own context
margin are floored to it, so this can never strand a building or tree
that was admitted to the tile on absent ground.
An interrupt (KeyboardInterrupt, SystemExit, a timeout that
cancels the calling thread) part-way through submission still
propagates -- this method never swallows it, and never cancels a
tile already accepted server-side. It DOES stop a tile the
submit pool had not yet reached: nothing new goes out over the
network once the interrupt lands. A tile whose request was already
in flight when the interrupt landed still runs to completion and
is recorded normally -- only work that had not started is
cancelled. What the interrupt adds: the exception carries a
partial_schedule attribute -- an
AreaSchedule (a list of them for
a multi-payload call), built from every tile that had an outcome
before the interrupt landed. Pass it straight to
run_area(..., retry_from=exc.partial_schedule) to resubmit only
what is still missing; the tiles already accepted are carried
forward, not re-billed. partial_schedule is only set when at
least one tile had reached the submit pool; an interrupt during
local planning (tiling, site preparation), before any request was
sent, leaves nothing to resume and nothing is attached.
on_accepted(job_id, tile_key), when given, is called once for
each job the submit loop records, at the moment it records it, so a
caller can store accepted job ids before this method returns (a
process that is killed then does not lose them). tile_key is the
schedule key (tile_id or {tile_id}#batch{i}); for a
multi-payload list the same key can occur once per payload. It is
not called for a failed or uncertain submission, nor for jobs that
retry_from carries forward. It is a read-only observer: it is
called from the submit worker threads, one call at a time, and an
exception from it is logged and ignored, so it never changes the
schedule or what is billed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
AnalysesUnion or list of AnalysesUnion
|
The analysis to run, or a list of analyses to run over the same
polygon. Only area analyses are accepted (not the interior
models), and |
required |
polygon
|
dict
|
A GeoJSON Polygon object that defines the area. |
required |
buildings
|
AreaBuildings or mapping
|
Building meshes, as the |
None
|
vegetation
|
mapping
|
Vegetation features, keyed by id. See above. |
None
|
ground_materials
|
mapping
|
Ground-material layers: a mapping of material name to a GeoJSON
FeatureCollection, for example the |
None
|
max_tiles_override
|
int
|
Override the default maximum number of non-empty tiles. |
None
|
max_workers
|
int
|
Size of the submit pool; see above. Defaults to 8. |
DEFAULT_SUBMIT_WORKERS
|
webhook_url
|
str
|
URL to notify about the submitted jobs. On a retry, the value
saved in |
None
|
webhook_events
|
list of str
|
Webhook events to subscribe to. On a retry, the saved value is
used when this is |
None
|
retry_from
|
AreaSchedule
|
A schedule from a previous call, to resubmit only its failed tiles. See above. |
None
|
known
|
dict
|
A job id to status map from a previous |
None
|
max_sensors_per_job
|
int
|
Per-job sensor cap for facade ( |
None
|
terrain_context_margin_m
|
float
|
How far, in metres, ground geometry is sliced beyond each tile. See above. |
None
|
transport
|
(json, binary)
|
Request representation. |
"json"
|
on_accepted
|
callable
|
Called as |
None
|
Returns:
| Type | Description |
|---|---|
AreaSchedule or list of AreaSchedule
|
The record of the submitted jobs, to pass to
|
Raises:
| Type | Description |
|---|---|
PolygonValidationError
|
If |
ValueError
|
If a payload is not an area analysis or is a |
KeyboardInterrupt
|
If the call is interrupted; see above for |
check_area_state
check_area_state(
schedule: AreaSchedule, *, known: Optional[Dict[str, Any]] = None
) -> AreaState
Query job statuses and return the area state.
known is the answer from a PREVIOUS call: a job id ->
status map, updated IN PLACE. A job whose recorded status is
terminal (succeeded or failed) is not asked about again —
pass the same dict across repeated calls to skip re-querying every
finished job. Also read by a run_area(retry_from=...) call that
passes its own known, so a retry plan built from a prior
poll issues no extra status requests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
schedule
|
AreaSchedule
|
The schedule returned by |
required |
known
|
dict
|
A job id to status map from a previous call, updated in place with the statuses found by this call. |
None
|
Returns:
| Type | Description |
|---|---|
AreaState
|
A snapshot of the job statuses: per-job states, counts per
status, and whether the area is complete. A job whose status
could not be read is reported as |
merge_area_jobs
merge_area_jobs(
schedule: AreaSchedule,
max_workers: int = DEFAULT_MERGE_WORKERS,
*,
strategy: str = "default",
wind_direction_deg: Optional[float] = None,
_known_states: Optional[Dict[str, Any]] = None,
) -> Union[AreaResult, SurfaceAnalysisResult]
Download and merge results for succeeded jobs in a schedule.
Each tile is merged as soon as its download completes; the SDK
keeps no copy of it. merged_grid keeps the
dtype the server sent: float16, float32, int16 (UTCI/TCI,
with value_divisor and valid) or, for a run that mixes
dtypes, float64. Use AreaResult.physical_grid() for physical
values.
Can be called at any time; it does not require every job to be finished.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
schedule
|
AreaSchedule
|
The schedule returned by |
required |
max_workers
|
int
|
Width of the DOWNLOAD pool, separate from the submit
pool |
DEFAULT_MERGE_WORKERS
|
strategy
|
str
|
|
'default'
|
wind_direction_deg
|
float
|
Meteorological wind-from direction in degrees (0=N, 90=E, 270=W). |
None
|
Returns:
| Type | Description |
|---|---|
AreaResult or SurfaceAnalysisResult
|
An |
Raises:
| Type | Description |
|---|---|
AreaRunError
|
If every job failed, or if any tile did not contribute a result, so that a grid with holes is never returned silently. |
ValueError
|
If |
forget_schedule
forget_schedule(schedule: AreaSchedule) -> None
Release the captures this client kept for the jobs of schedule.
Call it for a schedule you will never merge. The client keeps the
capture of every facade or roof job until the join consumes it, and a
schedule you drop cannot tell the client it is done. Jobs over the same
geometry share one stored copy, so this frees only the copy that no
other job of this client still uses. After this call
merge_area_jobs(schedule) still merges, but without the outline
(columns.render_buffers() then raises ValueError).
Warnings
A schedule made with retry_from=other carries the same job ids as
other for the jobs it did not resubmit. A job id holds one
reference, so forgetting one of the two releases those jobs for both.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
schedule
|
AreaSchedule
|
The schedule whose captures are released. |
required |
run_area_and_wait
run_area_and_wait(
payload: AnalysesUnion,
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
job_timeout: int = 300,
area_timeout: int = 3600,
on_progress: Optional[Callable[[AreaState], None]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
retries: int = 1,
) -> Union[AreaResult, SurfaceAnalysisResult]
run_area_and_wait(
payload: List[AnalysesUnion],
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
job_timeout: int = 300,
area_timeout: int = 3600,
on_progress: Optional[Callable[[AreaState], None]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
retries: int = 1,
) -> List[Union[AreaResult, SurfaceAnalysisResult]]
run_area_and_wait(
payload: Union[AnalysesUnion, List[AnalysesUnion]],
polygon: dict,
*,
buildings: Optional[
Union[AreaBuildings, Mapping[str, Union[DotBimMesh, dict]]]
] = None,
vegetation: Optional[Mapping[str, dict]] = None,
ground_materials: Optional[Mapping[str, dict]] = None,
job_timeout: int = 300,
area_timeout: int = 3600,
on_progress: Optional[Callable[[AreaState], None]] = None,
max_sensors_per_job: Optional[int] = None,
terrain_context_margin_m: Optional[float] = None,
max_tiles_override: Optional[int] = None,
max_workers: int = DEFAULT_SUBMIT_WORKERS,
webhook_url: Optional[str] = None,
webhook_events: Optional[List[str]] = None,
transport: Optional[str] = None,
on_accepted: Optional[Callable[[str, str], None]] = None,
retries: int = 1,
) -> Union[
AreaResult,
SurfaceAnalysisResult,
List[Union[AreaResult, SurfaceAnalysisResult]],
]
Submit area jobs, poll until complete, merge and return results.
max_workers reaches the SUBMIT pool only. The merge download pool
keeps its own width: it always runs at
DEFAULT_MERGE_WORKERS (8) and
is not reachable from here. A caller who needs to size the merge
pool runs the three steps themselves — run_area,
check_area_state and your own polling loop, then
merge_area_jobs(schedule, max_workers=...) — which is the same
sequence this method performs. This method starts bounded download,
decode and per-tile folding when polling confirms a successful job.
It returns results only after the final states and merge checks pass.
retries (default 1; 0 turns it off): resubmit failed,
compute-failed and uncertain keys with the terminal states already
learned, up to retries rounds. An incomplete merge raises only
after the last round. No retry after a 402 abort.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
AnalysesUnion or list of AnalysesUnion
|
The analysis to run, or a list of analyses to run over the same
polygon. See |
required |
polygon
|
dict
|
A GeoJSON Polygon object that defines the area. |
required |
buildings
|
mapping
|
Input layers, as for |
None
|
vegetation
|
mapping
|
Input layers, as for |
None
|
ground_materials
|
mapping
|
Input layers, as for |
None
|
job_timeout
|
int
|
Accepted for compatibility; it has no effect on this method. |
300
|
area_timeout
|
int
|
Seconds to wait for all jobs to finish in each round. Defaults to 3600. |
3600
|
on_progress
|
callable
|
Called with an |
None
|
max_sensors_per_job
|
int
|
Per-job sensor cap for facade payloads; see |
None
|
terrain_context_margin_m
|
float
|
Terrain reach in metres; see |
None
|
max_tiles_override
|
int
|
Override the default maximum number of non-empty tiles. |
None
|
max_workers
|
int
|
Size of the submit pool; see above. Defaults to 8. |
DEFAULT_SUBMIT_WORKERS
|
webhook_url
|
str
|
URL to notify about the submitted jobs. |
None
|
webhook_events
|
list of str
|
Webhook events to subscribe to. |
None
|
transport
|
(json, binary)
|
Request representation; see |
"json"
|
on_accepted
|
callable
|
Called as |
None
|
retries
|
int
|
Number of retry rounds; see above. Defaults to 1. |
1
|
Returns:
| Type | Description |
|---|---|
AreaResult or SurfaceAnalysisResult or list
|
The merged result: an |
Raises:
| Type | Description |
|---|---|
AreaTimeoutError
|
If |
AreaRunError
|
If, after the last retry round, every job failed or any tile did not contribute a result. |
ValueError
|
For the invalid inputs described under |