Configuration¶
One pydantic model, serialised verbatim into every run's manifest alongside the pinned Overture
release, the Google Earth Engine collection identifiers and date ranges, the resolved versions of
momepy, neatnet and geopandas, and a run timestamp.
DATA_DIR is resolved once, here. No other module reads os.environ, builds a path relative
to __file__, or joins its own paths — sources ask for settings.source_dir(name) instead. A
missing or unreachable DATA_DIR fails at config load, which is a much better place to find out
than three stages in.
Almost every field on this page carries its own docstring giving the value's provenance: a published threshold and its paper, or a fixture measurement and what it separates. Where a threshold was swept rather than chosen, the docstring says which sweep and at what operating point.
Two options ship switched off, and why
A threshold in this package is swept against a reference and chosen at an operating point, never picked because it looks reasonable. Anything whose threshold has not been swept ships disabled, and two options are currently in that state:
ucp.measure_on = "enclosures"measures the parameters on street-bounded blocks and transfers them to the units being classified. A street canyon has to be measured against streets, and a grid cell is bounded by none — on one Istanbul extent the aspect ratio is missing from 10.8% of built grid cells against 0.9% of enclosures. Default"units".classification.modal_filterreplaces an isolated unit's label with the one most of its neighbours carry — a minimum mapping unit, standard in this literature. Default off.
Switching either on changes labels, so a run with one enabled is not comparable with a run at the defaults until that threshold has been calibrated.
lczkit.config
¶
Pydantic configuration model for lczkit runs.
DATA_DIR is resolved exactly once, here, via Settings.load(). Every other module reaches
data through settings.input_dir, settings.output_dir, settings.source_dir(name), and
settings.run_dir — nothing else reads os.environ or builds a path from __file__ or the
current working directory.
NodataPolicy
module-attribute
¶
What a nodata cell means for a given land-cover product.
"exclude" — the product made no observation here, so the cell leaves the denominator entirely
and the remaining fractions still sum to 1.0. "assign" — the product deliberately masks this
surface, and the mask itself carries meaning, so the cell counts towards a named class.
The distinction is not cosmetic. Lang et al. (2023), 10.1038/s41559-023-02206-6, mask built-up
areas, snow, ice and permanent water out of the ETH canopy height product and set those cells to
255. That is a deliberate removal of surfaces known to carry no canopy, not a gap in coverage:
over the Berlin test fixture 93% of built-up cells and 78% of the whole tile are 255, and reading
them as "exclude" reports central Berlin as ~96% tree cover instead of ~22%.
UnmappedPolicy
module-attribute
¶
What to do with a raster value no class mapping covers.
"raise" is the default: an unmapped value means the configured mapping does not match the
product actually on disk, and silently dropping or lumping those cells would produce a quietly
wrong map.
GeeAssetType
module-attribute
¶
Whether an Earth Engine asset is a collection to filter and mosaic, or a single image.
Both occur among the MVP datasets: ESA WorldCover is a catalogued ImageCollection, while ETH
canopy height is a single Image published as a user asset. Loading one as the other is an
immediate EEException, so the kind is declared rather than probed.
OvertureConfig
¶
Bases: BaseModel
Configuration for OvertureSource, the Overture Maps vector source.
release
class-attribute
instance-attribute
¶
Pinned Overture release string, e.g. "2026-07-22.0". Never "latest" — OvertureSource
raises if this is unset.
source_dir_name
class-attribute
instance-attribute
¶
Name of the subdirectory under input/ that OvertureSource caches into. Matches the
directory already used by other projects sharing DATA_DIR, rather than the plain
"Overture".
CleaningConfig
¶
Bases: BaseModel
Configurable thresholds for the building-cleaning pipeline.
None of these have a literature-derived default — Majer & Fleischmann (arXiv:2603.00132) Supplementary D, the cleaning specification these operations follow, describes them only qualitatively. They are left unset here; the cleaning pipeline raises if used before being explicitly configured.
building_max_area_m2
class-attribute
instance-attribute
¶
Footprints larger than this are dropped as implausible.
building_min_area_m2
class-attribute
instance-attribute
¶
Footprints smaller than this are dissolved into a touching larger neighbour on
buildings_topo. Those touching nothing are kept: small is not spurious.
building_road_buffer_m
class-attribute
instance-attribute
¶
Half-width of the road buffer the buildings_topo street rule measures against, in metres.
Derived from the fixtures rather than the literature. At 4.0 m the overlap fraction separates perimeter blocks from structures standing in the roadway; at 2.0 m the distribution is too compressed to separate anything (p95 = 0.46) and at 8.0 m it swallows the blocks (p90 = 0.98).
building_road_overlap_limit
class-attribute
instance-attribute
¶
Fraction of a footprint inside the road buffer above which it is dropped rather than trimmed.
Also fixture-derived: 0.5 is where the median dropped footprint falls below the median building
(230 m² on Berlin), recovering 95% of the area the old centreline rule destroyed. Both this and
building_road_buffer_m were measured on two European cities and should be re-derived for a
city whose fabric or road-centreline generalisation differs.
building_merge_limit_m2
class-attribute
instance-attribute
¶
geoplanar.merge_overlaps' merge_limit — overlapping polygons smaller than this are
merged into a neighbour regardless of overlap size.
building_overlap_limit
class-attribute
instance-attribute
¶
geoplanar.merge_overlaps' overlap_limit (0-1 ratio) — polygons larger than
building_merge_limit_m2 are merged only if the shared overlap exceeds this fraction of
their area.
street_tile_size_m
class-attribute
instance-attribute
¶
Edge length of the tiles street simplification is chunked into, in metres.
None runs neatnet over the whole extent. Set it to engage the tiled path, which is
required above roughly 50 km2, where whole-extent simplification stops completing in usable
time (100 km2 measured at 50 minutes on one core, 256 km2 abandoned after 4h12m).
2000.0 m is the fixture-derived working value: tiles small enough to keep the largest face- artifact component tractable, large enough that seam handling stays a small share of the work.
street_tile_buffer_m
class-attribute
instance-attribute
¶
Working margin added around each tile before simplification, in metres.
Each tile is simplified over core + buffer and contributes only its core, so a feature at
the seam is decided with its neighbourhood present. Measured on Berlin: raising this from
300 m to 600 m cut spurious linework — geometry the tiled run invents that the whole-extent
run does not have — from 1.23 km to 0.11 km, and 900 m bought nothing further. Below about
300 m the buffer stops containing a dual carriageway's artifact and seams degrade sharply.
street_tile_workers
class-attribute
instance-attribute
¶
Processes to run tiles across. None uses every core the process is allowed.
street_artifact_threshold
class-attribute
instance-attribute
¶
Face-artifact index above which a face is ordinary fabric rather than a road artifact.
None derives it from the data — pooled across tiles on the tiled path, whole-network on
the untiled one. Setting it pins the value, which is what an A/B between two thresholds
needs, and lets a recorded run restate its threshold instead of re-deriving it.
A threshold on a neatnet index, not a fallback: neatnet's own fallback for a distribution
with no valley stays in lczkit.cleaning.streets.ARTIFACT_THRESHOLD_FALLBACK.
ArealTierConfig
¶
Bases: BaseModel
One areal raster tier of the height cascade (tiers 2-4).
Areal products assign a neighbourhood mean to individual buildings, which is a
categorically weaker measurement than a per-building height. Everything product-specific
lives here rather than in code: none of these three products is present on this system, and
none of their documentation is in docs/references/datasets/, so hardcoding a band number,
a unit scale or a nodata value would be guessing at a product nobody has read the manual for.
name
instance-attribute
¶
The tier's height_source tag, e.g. "ghsl". Must be unique within a cascade.
source_dir_name
instance-attribute
¶
Subdirectory under input/ holding this product, resolved via settings.source_dir().
enabled
class-attribute
instance-attribute
¶
Whether this tier takes part in the default cascade.
Deliberately distinct from filename is None, which says the product is not there. This
says it is available and switched off, and the two must stay distinguishable in the manifest:
a tier that never fired because nobody placed the data is a different fact from one that never
fired because it was measured and rejected.
filename
class-attribute
instance-attribute
¶
COG filename within input/<source_dir_name>/. None means the product is not available
and the tier is skipped entirely — the cascade is shorter, not broken.
scale
class-attribute
instance-attribute
¶
Multiplier converting raw raster values to metres (e.g. 0.1 for a decimetre product).
nodata
class-attribute
instance-attribute
¶
Override the raster's own declared nodata value. None uses whatever the file declares.
min_height_m
class-attribute
instance-attribute
¶
Sampled values at or below this are treated as "no building here" rather than as a height, so the next tier gets a chance. Zero is the neutral choice: an areal height product reports 0 for cells with no built-up volume.
It stays zero for every shipped tier, including Open Buildings 2.5D, whose sampled heights are 19% below 2 m against about 1% for the coarse products. A floor tuned until that stopped hurting would be a threshold no documentation supports, and it would be copied into other pipelines and outlive its justification. It also treats the wrong thing: the measured damage is within-unit dispersion (CV 0.441 against reality's 0.195), and clipping one side of an over-wide distribution leaves the other side where it was. The route to rescuing a fine-resolution product is shrinkage toward the unit mean, which is not implemented.
confidence
class-attribute
instance-attribute
¶
height_confidence written for every building this tier resolves. No default: see
HeightConfig.
Wsf3dConfig
¶
Bases: BaseModel
Tier 3 — WSF-3D V02 building height (DLR, TanDEM-X derived), CC-BY-4.0.
Parameters from DLR's own README_BuildingHeight.txt: 2.8 arcsec (~90 m at the equator),
"provided in meter with a gain factor of 0.1 for storage optimization (Int16)", nodata
-32767. The gain is why the tier carries scale=0.1; reading it as metres would report a
ten-storey block as a kerbstone.
url
class-attribute
instance-attribute
¶
The global product, a tiled GeoTIFF with overviews — read by window, never clipped.
GhslProductConfig
¶
Bases: BaseModel
Tier 4 — GHS-BUILT-H ANBH R2023A (JRC), free reuse with attribution.
ANBH, not the AGBH published beside it: the GHSL Data Package 2023 (p. 26) defines
ANBH = BUVOL / BUSURF, building volume over built-up surface, so it is the mean height of
the built fabric rather than a height averaged over open ground. Float32 metres, nodata 255,
100 m World Mollweide (p. 36).
OpenBuildings25dConfig
¶
Bases: BaseModel
Tier 2 — Google Open Buildings 2.5D Temporal v1, CC-BY-4.0, via Earth Engine.
The highest-value tier for exactly the regions where tier 1 fails, and the only one with no
public bucket behind it. building_height is metres above terrain in [0, 100] at an
effective 4 m resolution, annual 2016-2023, over Africa, South and South-East Asia, Latin
America and the Caribbean.
The two request caps are Earth Engine's, and they are here rather than in code because they are a property of the service, not of this package — when they move, this moves.
year
class-attribute
instance-attribute
¶
Latest annual epoch. Pinned rather than "latest" for the same reason the Overture release is: a run that silently changes epoch is a run nobody can reproduce.
scale_m
class-attribute
instance-attribute
¶
The product's effective resolution. Requesting the underlying 0.5 m grid would multiply the payload sixty-four-fold for detail the model does not carry.
HeightProductsConfig
¶
Bases: BaseModel
Where the areal height products come from, pinned for the manifest.
Separate from HeightConfig, which says how a tier reads a raster. This says how the
raster gets onto disk — release strings, URLs, the Earth Engine collection and epoch. Both
are serialised into the run manifest; only together do they make a cascade reproducible.
HeightConfig
¶
Bases: BaseModel
Configuration for the building-height cascade.
Tiers run in the order they appear: Overture attributes first, then areal_tiers in list
order. Adding a fifth areal product is an entry in that list, not a code change.
The three *_confidence values have no default for the same reason CleaningConfig's
thresholds have none: no published number defines them. height_confidence is an ordinal
ranking of measurement quality, not a calibrated probability, and a plausible-looking invented
default is the worst failure mode available here — nothing would crash, and the map would
carry a quietly wrong quality claim. Set them explicitly and they are
serialised into the run manifest, where the choice is visible and reproducible.
storey_height_m
class-attribute
instance-attribute
¶
Metres per storey for the num_floors fallback. Varies regionally and is a real error
source; 3.0 m is a conventional default and should be set per city where one is known.
overture_height_confidence
class-attribute
instance-attribute
¶
height_confidence for buildings resolved from Overture's height attribute — except
where Overture itself supplies a per-building confidence, which is preferred over this.
overture_num_floors_confidence
class-attribute
instance-attribute
¶
height_confidence for buildings resolved from num_floors x storey_height_m.
areal_tiers
class-attribute
instance-attribute
¶
areal_tiers: list[ArealTierConfig] = Field(default_factory=_default_areal_tiers)
Tiers 2-4, in cascade order. The default is coarse — see _default_areal_tiers.
GeeAssetConfig
¶
Bases: BaseModel
Earth Engine coordinates for one land-cover dataset.
Serialised verbatim into the run manifest, so the collection ID and date range a run used are part of its record.
collection_id
class-attribute
instance-attribute
¶
Full asset ID, e.g. "ESA/WorldCover/v200". None means no Earth Engine asset has been
verified for this dataset and EarthEngineSource refuses to guess at one.
asset_type
class-attribute
instance-attribute
¶
asset_type: GeeAssetType = 'image_collection'
See GeeAssetType.
band
class-attribute
instance-attribute
¶
Band name within the asset, e.g. "Map".
start_date
class-attribute
instance-attribute
¶
Inclusive ISO date passed to filterDate. Required for an image_collection; recorded but
not applied for a single image, which has no collection to filter.
end_date
class-attribute
instance-attribute
¶
Exclusive ISO date passed to filterDate. Same caveat as start_date.
scale_m
class-attribute
instance-attribute
¶
Reduction scale in metres. Should match the product's native resolution — a coarser scale silently resamples and changes every fraction.
required_fields
¶
Fields EarthEngineSource cannot run without, given this asset's kind.
Source code in src/lczkit/config.py
LandCoverDatasetConfig
¶
Bases: BaseModel
One land-cover product and the mapping from its raw values to fraction classes.
Everything product-specific lives here rather than in code: the class-to-fraction mapping is
config, never hardcoded. LocalRasterSource and EarthEngineSource read
the same instance, which is what makes the two backends return schema-identical tables.
A dataset is either categorical (value_classes) or binned (bins + bin_classes),
never both. Classes are disjoint and their fractions sum to 1.0 over the cells that count.
name
instance-attribute
¶
Short identifier, e.g. "worldcover". Used in cache filenames and error messages, and
must be unique within LandCoverConfig.datasets.
source_dir_name
instance-attribute
¶
Subdirectory under input/ holding this product, resolved via settings.source_dir().
filename
class-attribute
instance-attribute
¶
COG filename within input/<source_dir_name>/. None means the product is not available
locally; LocalRasterSource.from_settings refuses to build a source for it.
band
class-attribute
instance-attribute
¶
1-based raster band to read from the local COG.
column_prefix
class-attribute
instance-attribute
¶
Prefixed to every class name to form the output column names. Distinct prefixes are how
two datasets that both emit a tree class stay joinable on unit_id without collision.
classes
instance-attribute
¶
The full, ordered output class list. Fixes the output schema to config rather than to whichever classes happen to occur in a given city, so a class with no cells still gets a column holding 0.0 and two cities produce the same columns. The height-tier fractions are fixed to the configured cascade for the same reason.
value_classes
class-attribute
instance-attribute
¶
Categorical mapping from raw raster value to class name.
bins
class-attribute
instance-attribute
¶
Ascending breakpoints for a continuous product. A value v falls in bin i where
bins[i-1] <= v < bins[i], giving len(bins) + 1 bins.
bin_classes
class-attribute
instance-attribute
¶
Class name per bin, lowest first. Must be len(bins) + 1 long.
nodata
class-attribute
instance-attribute
¶
Override the raster's own declared nodata value. None uses whatever the file declares.
nodata_policy
class-attribute
instance-attribute
¶
nodata_policy: NodataPolicy = 'exclude'
See NodataPolicy.
nodata_class
class-attribute
instance-attribute
¶
Class nodata cells count towards. Required when nodata_policy is "assign", and
forbidden otherwise.
unmapped_policy
class-attribute
instance-attribute
¶
unmapped_policy: UnmappedPolicy = 'raise'
See UnmappedPolicy.
unmapped_class
class-attribute
instance-attribute
¶
Class unmapped values count towards. Required when unmapped_policy is "assign", and
forbidden otherwise.
gee
class-attribute
instance-attribute
¶
gee: GeeAssetConfig = Field(default_factory=GeeAssetConfig)
Earth Engine coordinates for the same product.
LandCoverConfig
¶
Bases: BaseModel
Configuration for the land-cover fraction sources.
Datasets are an ordered list so adding a third product is a config entry, not a code change —
the same shape as HeightConfig.areal_tiers.
datasets
class-attribute
instance-attribute
¶
datasets: list[LandCoverDatasetConfig] = Field(default_factory=_default_land_cover_datasets)
The configured products, by name.
gee_project
class-attribute
instance-attribute
¶
Google Cloud project Earth Engine bills against. Read from GEE_PROJECT_NAME by
Settings.load().
gee_batch_size
class-attribute
instance-attribute
¶
Units per reduceRegions call. A few thousand keeps a request under Earth Engine's
element-count and payload limits.
gee_max_units
class-attribute
instance-attribute
¶
Refuse an Earth Engine run covering more than this many units. None means no ceiling.
max_raster_cells
class-attribute
instance-attribute
¶
Refuse a local read whose covering window exceeds this many cells, rather than exhausting memory. 200M cells is ~200 MB for a uint8 product, or a ~450 x 450 km extent at 10 m.
dataset
¶
dataset(name: str) -> LandCoverDatasetConfig
The configured dataset called name, or a KeyError naming what is available.
Source code in src/lczkit/config.py
SemanticGroupConfig
¶
Bases: BaseModel
One functional group of Overture attribute values, and which LCZ class it is evidence for.
Transcribed from docs/references/tables/overture_lcz_semantic_mapping.md, which
tests/test_ucp_semantics.py parses and asserts against cell for cell, in the same way the
Stewart & Oke property table and the Bechtel similarity matrix are. Every value in it was taken
from what is present in the pinned release rather than from the schema documentation.
A group matches a building on subtype or class — the two are independently nullable and
a feature carrying only one is still classifiable. Groups are not a partition and their
fractions do not sum to one: retail is genuinely evidence for both large-low-rise form and
commercial function, and appears in both.
lcz_hint
class-attribute
instance-attribute
¶
Which LCZ class this group is evidence for, for the manifest and the docs. Documentation only — nothing keys behaviour off it, because a group is evidence and not a label.
UcpConfig
¶
Bases: BaseModel
Configuration for the urban canopy parameters.
Two kinds of value live here. The *_classes lists are vocabulary — which land-cover class
feeds which Stewart & Oke surface fraction, and which Overture attribute values count as
industrial — and they belong in config rather than in code. The two street_profile_* values
are momepy's own defaults, restated so they reach the run manifest instead of staying implicit
in a library signature.
street_profile_distance_m
class-attribute
instance-attribute
¶
Spacing between the perpendicular ticks momepy.street_profile() measures along. momepy's
own default.
street_profile_tick_length_m
class-attribute
instance-attribute
¶
Length of each tick. A tick reaching no building reports this as the street width, so it is also the assumed width of an open street. momepy's own default.
min_building_height_m
class-attribute
instance-attribute
¶
Lower bound applied to building height before taking logs for the geometric mean.
A numerical guard, not a scientific threshold: log(0) is negative infinity and would take a
whole unit's height of roughness elements to zero on the strength of one bad row. Overture
heights are not validated upstream and a zero or negative value does occur. 0.1 m sits below
any plausible building, so the floor changes nothing except in the case it exists to catch —
but it is the value such a building is then counted as, which is why it is configurable
rather than buried in the code.
measure_on
class-attribute
instance-attribute
¶
Which units the parameters are measured on, as distinct from classified on.
"units" measures on whatever UnitsConfig.strategy produced, and is what every published
figure was computed with. "enclosures" measures on street-bounded enclosures and moves the
result onto the target units, area-weighted.
The reason is aspect_ratio. A street canyon has to be measured against streets, and a
100 m grid cell is not bounded by any — so H/W, which is 3 of the 17 applied weight units and
the only dimension separating LCZ 8 from LCZ 3 and 6, is null on 10.8% of one Istanbul extent's
built grid cells against 0.9% of its enclosures. On the densest decile the enclosures also put
82.2% of cells inside LCZ 2's published H/W band against the grid's 70.2%.
Defaults to "units" because no accuracy claim is attached: this has not been calibrated
against a reference, so turning it on makes a run incomparable with one at the defaults.
land_cover_dataset
class-attribute
instance-attribute
¶
Which LandCoverConfig.datasets entry supplies the surface fractions. The ETH canopy
dataset is a second, competing tree estimate rather than a full land-cover product and reads
high, so WorldCover is the default.
tree_classes
class-attribute
instance-attribute
¶
Land-cover classes counting as tree cover.
pervious_classes
class-attribute
instance-attribute
¶
Land-cover classes counting as pervious before tree and water are folded in — see
lczkit.ucp.surface.
impervious_classes
class-attribute
instance-attribute
¶
Land-cover classes counting as impervious, buildings included. The building share is
subtracted in lczkit.ucp.surface, since a raster's built-up class contains the roofs.
water_classes
class-attribute
instance-attribute
¶
Land-cover classes counting as water.
industrial_building_subtypes
class-attribute
instance-attribute
¶
Overture building subtype values counting as industrial.
industrial_building_classes
class-attribute
instance-attribute
¶
Overture building class values counting as industrial.
warehouse is deliberately absent. The problem this parameter exists to solve is that a
distribution warehouse and a refinery are geometrically identical — the warehouse being the
LCZ 8 case and the refinery the LCZ 10 one. Counting warehouses as industrial would push
exactly the units the rule is meant to keep apart towards LCZ 10.
industrial_land_use_subtypes
class-attribute
instance-attribute
¶
Overture land-use subtype values counting as industrial. Empty by default: Overture files
industrial parcels under subtype='developed', which also covers commercial and retail, so the
subtype alone carries no industrial signal.
industrial_land_use_classes
class-attribute
instance-attribute
¶
Overture land-use class values counting as industrial.
brownfield — disused industrial land — is the obvious candidate to add and is deliberately
left out: it describes what a parcel was, and a brownfield site has no heat output, no
industrial buildings and often no buildings at all. Add it only for a city where derelict
industry is still the dominant surface.
semantic_groups
class-attribute
instance-attribute
¶
semantic_groups: list[SemanticGroupConfig] = Field(default_factory=_default_semantic_groups)
Functional groups read out of Overture's subtype and class.
The four industrial_* vocabularies above are not replaced by this and are not derived from
it. They feed industrial_fraction_of_building_area, which is the column the calibrated LCZ 10
threshold of 0.45 was swept against, and repointing that at a differently-scoped group would
silently invalidate the calibration. heavy_industry here is the same idea with a slightly
wider vocabulary (storage_tank, silo, land-use works) and it is reported beside the
original rather than in place of it.
Set to [] to skip the semantic layer entirely, which costs one overlay per group.
SemanticRuleConfig
¶
Bases: BaseModel
One functional assignment rule keyed on a semantic evidence column.
The same mechanism as the LCZ 10 rule, generalised: a unit over min_fraction takes lcz
whatever the distance metric said, and the displaced answer is kept as lcz_secondary.
All of these ship disabled, and that is deliberate rather than cautious. A threshold here is calibrated against a reference and chosen at an operating point, never picked because it looks reasonable. The values below are placeholders marking where a calibrated number goes, and enabling one before that would put an invented number into a published label.
column
instance-attribute
¶
The parameter column to threshold, e.g. sem_lightweight_buildings_of_building_area.
max_mean_building_area_m2
class-attribute
instance-attribute
¶
Optional gates on mean_building_area_m2.
Available to a rule although it is not a metric dimension: mean_building_area_m2 carries
zero weight in every preset, so the distance metric cannot act on it. A rule is not the metric,
and "large low-rise" is a claim about building size that the semantic evidence alone cannot
make.
reason
class-attribute
instance-attribute
¶
Why this rule exists, for the manifest. A rule with no recorded reason is one nobody can audit later.
ClassificationConfig
¶
Bases: BaseModel
Configuration for the prototype-distance classifier.
Every threshold the classifier applies lives here and is serialised into the run manifest.
Two of them - natural_dominant_fraction and natural_negligible_fraction - move the
prototype table itself, because the tree and water ranges they generate are lczkit's own
rather than Stewart & Oke's and there is no published value to defer to.
weight_preset
class-attribute
instance-attribute
¶
Named entry in lczkit.classify.weights.PRESETS. "bernard2024_partial" is Bernard et al.
(2024)'s published built-type default with the dimensions this package cannot compute left
out; "equal" is the uniform comparison.
The _partial is load-bearing rather than decorative: SVF (weight 4) and z0 (weight 0.5) are
deferred and FI/FP are zero-weighted, so 17 of the published 21.5 weight units are applied and
the effective metric has three non-zero dimensions, of which FB is roughly 47%. Calling it
bernard2024 would claim a metric this package does not implement. The unapplied dimensions
are recorded in the run manifest.
built_min_building_fraction
class-attribute
instance-attribute
¶
Building surface fraction at or above which a unit is classified against the built prototypes rather than the natural ones.
Not an invented number. Every built class in Stewart & Oke's table has a building surface fraction of at least 10%, and every natural class at most 10%, so 10% is the boundary the published table draws itself. It is configurable because a city's footprint completeness can shift the measured fraction even where the real one is unchanged.
reachable_natural_classes
class-attribute
instance-attribute
¶
Which natural classes the gate may assign, by Stewart & Oke label.
C (bush, scrub), D (low plants) and F (bare soil or sand) are mutually indistinguishable in
open ground with the parameters this package computes: the published table separates them only
by sky view factor, aspect ratio and height of roughness elements, all building-derived and all
null where nothing is built, and the default WorldCover mapping folds shrubland, grassland and
bare ground into a single pervious class. Rather than let tied prototypes be resolved by index
order, C and F are excluded and the exclusion is recorded in the manifest. Distances to the
excluded classes are still computed and reported.
The two exclusions are not the same kind of thing, and only one of them is a policy choice.
C is genuinely excluded by this setting: its box differs from D's on aspect ratio (0.25-1.0 against at most 0.1) and on Hr (at most 2 m against at most 1 m), both of which carry weight 1.0 in the natural vector. Where those two are non-null C wins outright, so adding it back here changes labels. The tie is real only where both are null, which for a buildingless unit is the common case - hence the exclusion.
F is excluded by arithmetic, not by this list. D's box contains F's in every dimension - identical on aspect ratio, building, impervious, pervious, tree and water, and wider on Hr (at most 1 m against at most 0.25 m) - so d(F) >= d(D) for every possible unit, and ties break to the lower code. Adding "F" here cannot make F reachable. Reaching it needs a land-cover mapping that emits bare ground separately and a surface fraction carrying it, not a config change. The manifest records F as dominated rather than excluded, so the distinction survives into the run record.
natural_dominant_fraction
class-attribute
instance-attribute
¶
Tree or water cover at which a unit reads as LCZ A or LCZ G. See
docs/references/tables/lczkit_natural_class_ranges.md - lczkit's own, not Tier 1.
semantic_rules
class-attribute
instance-attribute
¶
semantic_rules: list[SemanticRuleConfig] = Field(default_factory=_default_semantic_rules)
Functional rules read off Overture's semantic attributes, all shipped disabled pending
calibration. See SemanticRuleConfig, and lczkit.classify.rules.apply_semantic_rules.
natural_negligible_fraction
class-attribute
instance-attribute
¶
Tree or water cover a natural class treats as absent. Reuses the 10% boundary Stewart & Oke apply to building and impervious cover throughout their natural rows.
lcz10_industrial_column
class-attribute
instance-attribute
¶
Which industrial share the LCZ 10 rule reads.
Named explicitly because the two are not interchangeable and the threshold below is calibrated against one of them.
industrial_fraction_of_building_area is Bernard et al. (2024)'s FIND/B, so their published
0.33 transfers to it directly. It is the default on both theory and measurement: on the
Rotterdam fixture it selects 95 cells against a reference of 88, where the unit-area share
selects 196 at its own best operating point. Getting the rate right matters when precision
cannot be improved (see the threshold below), because the remaining choice is how much of the
map to label.
The unit-area share is the alternative, and is the more saturated of the two at a 100 m cell —
42.6% of cells holding any industrial ground read exactly 1.0, against 12.6% for FIND/B.
lcz10_min_industrial_fraction
class-attribute
instance-attribute
¶
lcz10_industrial_column above which a unit is assigned LCZ 10.
Deliberately set to under-trigger: Overture cannot distinguish heavy from light industry, so a missing LCZ 10 is a visible gap while a light-industrial estate mislabelled as heavy industry is an invisible error that propagates into any model consuming the map.
Calibrated, not picked. It was swept over nineteen settings against the Rotterdam
reference, and the precision/recall curve is written into the run record. 0.45 is that sweep's
operating point, the precision maximum over FIND/B. Bernard's published 0.33 sits just below
it and performs comparably (22.4% precision, 27.3% recall against 23.2% and 25.0%), so this is
not a number in tension with the paper it comes from.
Read the curve before trusting it. Precision is roughly flat — 16.7% to 23.2% across the
whole range — so this threshold governs how much of the map carries LCZ 10 and not how often
that label is right. The rule fires plausibly, not accurately, and it is scored against
lcz_v3, a comparator carrying its own error and coarser than the ground in this very city.
lcz1_min_height_m
class-attribute
instance-attribute
¶
Optional floor on Hr below which the LCZ 1 (compact high-rise) distance is discarded.
Bernard et al. (2024) Sect. 2.3 apply the equivalent constraint on mean building levels,
reporting that without it GeoClimate produced LCZ 1 across European cities where no urban
researcher would place any. lczkit has no reliable per-unit storey count, so the hook is
exposed against Hr instead and defaults to off - an untested constraint applied by default
would be a worse failure than the over-prediction it guards against.
modal_filter
class-attribute
instance-attribute
¶
Whether to replace an isolated unit's label with the modal label of its neighbours.
Off, and that is deliberate rather than cautious. Every unit is classified independently of its neighbours, so a cell whose parameters wobble across a prototype boundary takes a different label from the fabric it sits in — salt-and-pepper at a grain Stewart & Oke never intended a class to be read at, given an LCZ patch is a neighbourhood and a So2Sat patch is 320 m across. A spatial filter is the standard answer in this literature.
It stays off because modal_filter_min_like_neighbours has not been calibrated against a
reference, and because every published figure was measured without one. A run with it on
is not comparable with a run at the defaults.
See lczkit.classify.smoothing.
modal_filter_min_like_neighbours
class-attribute
instance-attribute
¶
A unit with fewer than this many contiguous neighbours sharing its label is isolated.
A placeholder marking where a swept number goes, not a calibrated value. Two is the weakest setting that does anything at all on a Queen-contiguous grid, chosen so a caller who enables the filter without sweeping it does the smallest thing rather than the boldest.
OutputConfig
¶
Bases: BaseModel
Configuration for what a run writes into output/lczkit/<run_id>/.
break_count
class-attribute
instance-attribute
¶
Number of classification breaks precomputed per continuous variable. The map site renders choropleths from these and never recomputes a quantile at site-build time.
break_method
class-attribute
instance-attribute
¶
How the breaks are derived. Only quantiles are implemented; the field exists so the manifest states the method rather than leaving a consumer to assume one.
viz_significant_figures
class-attribute
instance-attribute
¶
Significant figures floats are rounded to in units_viz.parquet.
viz_distance_scale
class-attribute
instance-attribute
¶
Multiplier applied to the 17-way distance vector before it is stored as int16 in
units_viz.parquet. Distances are small positive reals, so 1000 keeps three decimal places
inside the int16 range.
gis_format
class-attribute
instance-attribute
¶
A second copy of the unit table in a format every GDAL build reads, written beside the GeoParquet rather than instead of it.
units.parquet is not the problem this solves. It is valid GeoParquet 1.0.0 and carries
the run's CRS as PROJJSON with an EPSG authority code — verified on the published Bogota and
Nairobi runs, both EPSG:32618/EPSG:32737. What varies is the reader: GDAL's Parquet
driver is an optional build component, so a QGIS built without it opens the file as a
non-spatial table or not at all, and the failure looks like a missing CRS rather than a
missing driver. GeoPackage has no such conditional — it is SQLite, in GDAL's core, and its
CRS lives in a table rather than in file metadata a driver has to know how to parse.
Cost, measured on the 116 491-unit Bogota run at four extents before this was made the
default: 0.22 s / 5.6 MB at 10 000 units to 1.83 s / 67.3 MB at 116 491, exponent 0.86 —
sublinear, and 0.3% of a run that takes ten minutes. Set "none" to skip it.
Only the unit table. The context layers under layers/ stay GeoParquet-only: they are the
site's basemap material, they carry the same CRS, and buildings.parquet alone is 477 MB on
that run.
VizConfig
¶
Bases: BaseModel
Configuration for the static map site lczkit.viz.build_site writes into a run.
Zoom ranges are here rather than derived because they have to be chosen deliberately and recorded — a tileset is only reproducible if the zooms that made it are written down, and tippecanoe's cost and the site's size both scale with the range.
unit_max_zoom
class-attribute
instance-attribute
¶
Zooms for the unit tileset. A 100 m cell is about 21 px at z14 and 1 px at z10, so z14 is where the grid stops gaining detail and z10 is where a whole metropolitan extent fits on a screen. MapLibre overzooms past the maximum for free, so z14 is not a limit on how far in a reader can go.
basemap_max_zoom
class-attribute
instance-attribute
¶
The basemap stops one zoom short of the units. It is a flat wash drawn under a translucent unit fill, so its detail at z14 is invisible and expensive — dropping the top level halved a measured land-use tileset, 2.66 MB to 1.45 MB, because the maximum-zoom level is the one tippecanoe keeps at full precision. MapLibre overzooms it for free past the maximum.
basemap_simplification
class-attribute
instance-attribute
¶
tippecanoe's --simplification for the basemap only. Context geometry is the one thing in
the site that carries no measurement, so it is the only thing that may be simplified for
display; the unit and building tilesets are left at tippecanoe's faithful default.
basemap_layers
class-attribute
instance-attribute
¶
Which persisted context layers to draw, in draw order.
Land use is available and off by default, on a measurement. At 9 km² it was 1 864 polygons
carrying 401 162 vertices — 94% of the basemap's bytes and most of its build time — for a dark
wash underneath an 82%-opacity unit fill. Water and streets are what actually orient a reader
on a city map: rivers and arterials. Add "land_use" back if a run wants it.
building_max_zoom
class-attribute
instance-attribute
¶
Buildings are only ever seen extruded, which is a high-zoom view; tiling them from z10 would triple the tileset for pixels nobody looks at.
include_buildings
class-attribute
instance-attribute
¶
Whether to tile the building footprints. Off by default, and the default is measured: 891 994 Berlin footprints tile to tens of megabytes, two to three times the rest of the site combined. A reader who wants extrusions can turn it on and pay for it knowingly.
render_columns
class-attribute
instance-attribute
¶
render_columns: list[str] = Field(default_factory=lambda: ['lcz_primary', 'lcz_secondary', 'uniqueness', 'height_completeness', 'building_surface_fraction', 'impervious_surface_fraction', 'pervious_surface_fraction', 'tree_fraction', 'water_fraction', 'height_of_roughness_elements_m', 'aspect_ratio', 'street_openness', 'mean_building_area_m2', 'industrial_fraction_of_building_area'])
Attributes carried at every zoom, because a choropleth needs them while the map is zoomed out. Everything else in the viz table rides in a second tileset built at the maximum zoom only.
The split exists because MVT repeats the whole attribute table in every tile at every zoom: at metropolitan scale a 38-column unit table costs more tiled than 892 000 building footprints do. Columns named here that a run did not produce are skipped rather than raising — a run over a small extent can legitimately lack a parameter.
render_column_prefixes
class-attribute
instance-attribute
¶
Column families carried at every zoom, named by prefix because their members are not known until a run has chosen a cascade.
The height tier fractions are height_frac_<source> for whichever sources fired — wsf3d,
ghsl, unresolved, and the two Overture tiers — so render_columns cannot name them without
naming a cascade. They belong at every zoom rather than in the click-detail tileset because
they are first-class layers and not diagnostics: a selectable layer must paint while the map
is zoomed out, and it must paint from tiles already in memory, since a view change that
refetched would make the site slow.
detail_max_features
class-attribute
instance-attribute
¶
Above this many units the click-detail tileset is skipped and the sidebar falls back to the render attributes. A guard on the one part of the site whose size is unbounded in extent.
online_basemaps
class-attribute
instance-attribute
¶
Remote raster grounds the site offers, by key from lczkit.viz.basemaps.PROVIDERS.
Empty by default, and that default is load-bearing rather than cautious. A built site opens with no network and stays valid years from now, which is what makes one archivable beside a paper; a remote tile source breaks both of those, and outlives the run only as long as the provider chooses to serve it. With this empty the emitted site contains no external reference at all, and a test asserts it.
Name one or several and the site gains a base-map picker: each is a selectable ground under the units, drawn beneath everything else, hidden until chosen, with the provider's attribution shown and its tiles treated as fallible — a failure leaves every other layer working and says so. The run's own Overture water and streets stay in the site and stay switched on, so an archived copy opened offline still shows the linework the classification was computed from.
Order is the order they appear in the picker. Selecting a provider takes on its tile usage
policy, and two of them need an API key — see each one's terms.
online_basemap
class-attribute
instance-attribute
¶
Deprecated single-value spelling of online_basemaps, folded into the front of it.
Kept because it is what runs before this field existed recorded in their manifests, and
build_site re-validates an archived manifest to rebuild a site. Pydantic ignores unknown
fields by default, so dropping this would make an archived run's configured ground disappear on
rebuild with nothing raised — a silent change to an artefact, which is worse than a stale name.
Read basemap_keys, never this field.
maptiler_key
class-attribute
instance-attribute
¶
The MapTiler API key, resolved from MAPTILER_API_KEY by Settings.load().
exclude=True is the whole point of the field's shape. The manifest is
settings.model_dump() verbatim, and lczkit.viz.build_site copies the manifest into the
published site, so an ordinary field would put the key in two shipped files rather than the one
that needs it. Excluded, it reaches style.json's tile URLs and nothing else, and a test
asserts the manifest never carries it.
That bounds the exposure; it does not remove it. MapLibre fetches tiles from the browser, so the key is in plain text in any site built with a MapTiler ground, and anyone holding the directory holds the key. Restrict it by origin at the provider, or hand out a site built without one.
basemap_keys
property
¶
The configured provider keys in picker order, with the deprecated singular folded in.
The single place the two spellings are reconciled, so nothing downstream has to know that
online_basemap exists.
UnitsConfig
¶
Bases: BaseModel
Which spatial units the pipeline computes on.
There is no auto-selection, and in particular none by region. Enclosures lead the grid on both criteria outside Europe and North America and lose inside it, but region is not the mechanism — natural-class share and patch heterogeneity are — so a rule keyed on continent would be wrong at every boundary. The trade-off is documented, the choice is the caller's, and which strategy ran is recorded in the manifest.
strategy
class-attribute
instance-attribute
¶
grid (default), enclosure, or patch.
grid— 100 m cells. What every published LCZ map, validation dataset and WRF workflow uses, and what every published figure here is measured on.enclosure— street-, rail- and water-bounded blocks. Measured against the grid over fifteen cities and not adopted: built-class agreement +3.8 (12/15) but overall −0.2 (8/15), and adoption required a lead on both.patch— enclosure seeds merged to LCZ-patch scale. Seelczkit.units.patches; the block is a much smaller object than a patch (median 0.04 ha against WUDAPT's 2.2–52 ha), and this is the strategy that addresses that rather than assuming a thinner barrier set will.
cell_size_m
class-attribute
instance-attribute
¶
Grid cell side. 100 m is not arbitrary — it is what the validation references and the downstream WRF tooling assume — so moving it makes a run incomparable with a published figure.
patch_min_area_m2
class-attribute
instance-attribute
¶
strategy="patch" only. A floor, not a centre: the median lands near twice it.
patch_max_area_m2
class-attribute
instance-attribute
¶
strategy="patch" only. None removes the ceiling, which lets a merge chain swallow a whole
estate into one unit.
patch_merge_on_morphology
class-attribute
instance-attribute
¶
Whether the patch merge compares neighbours on building surface fraction and height, or merges on size alone. On costs one overlay against the building layer and is what makes a patch homogeneous rather than merely large; off is supported and worse, and needs no buildings.
drop_pedestrian_barriers
class-attribute
instance-attribute
¶
Exclude footway/steps/path/cycleway/bridleway from the barrier set for enclosure and
patch. On by default: these are 50–73% of the mapped network in Berlin, Hong Kong and Milan
and 3.5–7.5% elsewhere, so leaving them in makes the partition largely a measure of how
thoroughly a city's footpaths have been surveyed. Ignored by grid, which takes no barriers.
WudaptConfig
¶
Bases: BaseModel
The WUDAPT LCZ training areas, the third reference and the only globally available one.
A vector product rather than a raster, so unlike _default_reference_dataset it is not a
LandCoverDatasetConfig — there is no exactextract path to reuse. See
lczkit.validation.wudapt for what each of these gates costs.
source_dir_name
class-attribute
instance-attribute
¶
Subdirectory under input/ holding the WUDAPT export.
filename
class-attribute
instance-attribute
¶
The LCZ Generator export, e.g. LCZ-Generator_training_areas_2024-10-01.gpkg.
Unset by default and deliberately not defaulted to whatever is on disk, for the same reason
OvertureConfig.release refuses to track "latest": the export is dated, contributors keep
adding to it, and a validation figure that does not name the export it was measured against
cannot be reproduced. A caller pins it.
layer
class-attribute
instance-attribute
¶
GeoPackage layer. None takes the first, which is correct for the published export — its
only other layer is a QGIS layer_styles table carrying no geometry.
class_column
class-attribute
instance-attribute
¶
WUDAPT's own LCZ column. Integer, agreeing with Demuzere's coding over 1-17.
require_qc
class-attribute
instance-attribute
¶
Keep only polygons passing all three LCZ Generator QC flags.
Off by default, and measured rather than assumed. The gate is expensive — over 200 000 polygons the three flags pass individually at 62.3% / 84.0% / 81.5% and jointly at 48.2%, so it halves the reference — and what it buys, measured against So2Sat labels on the 30 km windows, is Cairo 26.3% → 26.7%, Mumbai 47.4% → 50.3%, Jakarta 70.7% → 68.8%. Inert on two cities and harmful on the third, for half the labelled ground.
WudaptSelection.qc_pass_fraction reports the rate whether or not the gate is on, so the cost
of changing this is visible without a second run.
min_oa
class-attribute
instance-attribute
¶
Minimum LCZ Generator overall accuracy for the polygon's submission. None disables.
A property of the submission, not of the polygon — see wudapt.priority_order — and it does
not select for what a validation reference needs. Gating at 0.7 moves agreement
with So2Sat by Cairo 26.3% → 19.1%, Mumbai 47.4% → 47.3%, Jakarta 70.7% → 69.6%: worse on
all three, and much worse on the city that needed help most. The LCZ Generator's cross-validated
accuracy scores a submission against itself, so a high oa means a self-consistent
contributor rather than one who agrees with an independent expert.
min_area_m2
class-attribute
instance-attribute
¶
Drop polygons below this. 4.7% of the file is under 1000 m², smaller than a 100 m cell, and five features have exactly zero area.
max_area_m2
class-attribute
instance-attribute
¶
Drop polygons above this. The largest in the file is 18 680 km² — a whole sea digitised as one LCZ G polygon — which is a legitimate label and a poor reference for a city window.
citation
class-attribute
instance-attribute
¶
Bechtel et al. (2015), IJGI 4(1), 199-219 — the WUDAPT Level 0 protocol.
Recorded apart from reference_citation and ground_truth_citation because which file filled
the reference role is part of the measurement. WUDAPT is additionally not independent of
lcz_v3 — these training areas
are the training data behind the Demuzere global map — so agreement between the two is not a
ceiling in the sense So2Sat gives one.
ValidationConfig
¶
Bases: BaseModel
Configuration for agreement against a reference LCZ map.
Agreement is reported in the style of the lczexplore package: per-class figures and a
confusion matrix, never a single headline number.
reference
class-attribute
instance-attribute
¶
reference: LandCoverDatasetConfig = Field(default_factory=_default_reference_dataset)
The reference map, described as a categorical raster. See _default_reference_dataset.
reference_citation
class-attribute
instance-attribute
¶
Demuzere et al. (2022), ESSD 14, 3835-3873. Recorded separately from the file actually read: the copy on this system is version 3 of the map and the paper describes an earlier one, so conflating the two in the manifest would misstate what a run was validated against.
ground_truth_citation
class-attribute
instance-attribute
¶
Zhu et al. (2020), IEEE GRSM 8(3), 76-89. So2Sat LCZ42, the hand-labelled reference.
Recorded apart from reference_citation because the two are not the same kind of thing:
lcz_v3 is a model output with its own error, these are human labels. Where both are
available the labels are primary and lcz_v3 is a comparator whose agreement with them is the
ceiling on any score against it.
wudapt
class-attribute
instance-attribute
¶
wudapt: WudaptConfig = Field(default_factory=WudaptConfig)
The WUDAPT training areas — hand labels too, but a different kind of object from So2Sat:
irregular, overlapping, spanning four decades, and covering every city this package has been
run on rather than the 51 So2Sat sampled. See WudaptConfig and lczkit.validation.wudapt.
min_reference_coverage
class-attribute
instance-attribute
¶
Fraction of a unit the reference map must actually cover for that unit to enter the agreement statistics. A unit half outside the map's extent would otherwise contribute a majority computed from a corner of itself.
height_completeness_deciles
class-attribute
instance-attribute
¶
Strata for the height-completeness breakdown. Deciles by default, configurable so a run with few units can widen the bins rather than report noise.
Settings
¶
Bases: BaseModel
Resolved configuration for a single lczkit run.
Construct via Settings.load(), not directly — that is what resolves DATA_DIR from
the environment and creates the run's output directory.
input_dir
property
¶
$DATA_DIR/input/ — organised by data origin, owned by other projects too.
output_dir
property
¶
$DATA_DIR/output/ — organised by the tool that produced the results.
run_dir
property
¶
$DATA_DIR/output/lczkit/<run_id>/ — this run's own output directory.
tile_cache_dir
property
¶
$DATA_DIR/output/lczkit/_cache/tiles/ — memoised per-tile street simplification.
Not under input/, deliberately. Writes to input/ are confined to the source
implementation owning each subdirectory, and nothing else in the package writes there at
all. A simplified tile is derived by lczkit's own cleaning from data a source already
fetched — it is not source data, and input/ may be shared with other projects that must
not have to reason about lczkit's intermediates.
The cache therefore lives in lczkit's own output tree, a sibling of the run directories
rather than inside any one of them, since a tile outlives the run that computed it. Cache
keys carry the same discipline OvertureSource uses — see
lczkit.cleaning.pipeline.tile_fingerprint.
source_dir
¶
Return input/<name>/, the directory a source implementation owns.
Only the source implementation for name writes here; nothing else in the package
writes under input/ at all.
Source code in src/lczkit/config.py
load
classmethod
¶
load(*, run_id: str | None = None, dotenv_path: Path | str | None = None, create_run_dir: bool = True) -> Settings
Load .env, resolve DATA_DIR, and create output/lczkit/<run_id>/ if absent.
Also picks up GEE_PROJECT_NAME into land_cover.gee_project, and MAPTILER_API_KEY into
viz.maptiler_key. Unlike DATA_DIR both are optional — only the Earth Engine backend and
the MapTiler base maps need them, and each raises its own message when it is missing — so an
absent value is not an error here. An absent variable leaves whatever is already
configured alone: assigning os.environ.get(...) unconditionally would overwrite a value
supplied by a config file with None, which is a silent discard rather than a precedence
rule.
create_run_dir=False resolves everything and touches nothing, for callers that only want
to read the resolved configuration — lczkit run --dry-run. The default creates it,
because every caller that goes on to run a pipeline needs it to exist.
Never creates or modifies anything under input/. Raises ValueError with a clear
message if DATA_DIR is unset; raises a pydantic.ValidationError (also with a
clear message) if it is set but does not exist.
Source code in src/lczkit/config.py
maptiler_key
¶
MAPTILER_API_KEY from the environment, stripped, or None if it is unset or blank.
Separate from Settings.load because lczkit site build needs the key and does not need
DATA_DIR: it works off a run directory given on the command line, and Settings.load raises
without DATA_DIR. Requiring one to get the other would make rebuilding an archived site
depend on an unrelated variable. Settings.load calls this too, so there is one definition of
where the key comes from.
The strip is not cosmetic. A trailing space in a .env line is invisible in an editor and
survives into the tile URL, where it makes every request 403 — a broken base map whose cause is
unreadable from the symptom.
It reads the environment, which the rest of the package must not do: os.environ is touched
in this module and nowhere else.
Source code in src/lczkit/config.py
Presets¶
A Settings produced by Settings.load() is deliberately not runnable — it has no bbox and no
release. Presets close that gap with the exact constants a published figure was produced under.
lczkit.presets
¶
Named, complete run configurations — the settings a run needs that have no safe default.
Settings.load() cannot produce a runnable configuration on its own, and that is deliberate.
CleaningConfig's eight numeric fields and HeightConfig's two confidences all default to None
and raise at call time, because each is a threshold someone measured and an invented default would
travel into every run's manifest looking like a measurement. See those models for the argument.
A preset is where those values live, so that lczkit run and the published sites cannot drift
apart. Modelled on lczkit.classify.weights, which has the same shape for the weight vectors.
One preset, and that is the honest number. published is what the three published sites were
built with. A second name would imply a second measured configuration exists.
OVERTURE_RELEASE
module-attribute
¶
The release the committed fixtures were built from, so only the extent differs between a run
here and the offline numbers. Never "latest": a floating release is not reproducible.
AREAL_CONFIDENCE
module-attribute
¶
height_confidence per areal tier, descending with coarseness below tier 1's 0.9 / 0.6.
Ordinal, with no published number behind it — the same standing as the two Overture confidences
beside them, and set here rather than defaulted in lczkit.config for exactly the reason
HeightConfig gives: an invented default would travel into every run's manifest as if it were
measured. The choice is recorded in the manifest where it is visible.
RunPreset
dataclass
¶
RunPreset(name: str, description: str, overture_release: str, cleaning: CleaningConfig = _published_cleaning(), heights: HeightConfig = _published_heights(), land_cover: LandCoverConfig = LandCoverConfig(), ucp: UcpConfig = UcpConfig())
A complete set of the configuration a run cannot default its way into.
apply
¶
Write this preset over settings, in place, and return it.
Each section is copied rather than shared, so two runs configured from one preset cannot mutate each other's settings through it.
Source code in src/lczkit/presets.py
preset
¶
preset(name: str) -> RunPreset
The preset called name, or a KeyError naming the ones that exist.
Source code in src/lczkit/presets.py
apply_preset
¶
Apply the named preset to settings in place, returning it for chaining.
Places — the general locator¶
Every urban region on earth, by name, from the Global Urban Polygons and Points Dataset (GUPPD) —
a gazetteer from NASA's Socioeconomic Data and Applications Center and the European Commission's
Joint Research Centre: 5 558 regions across 173 countries, in one 564 KB table. This is what
lczkit run --city and lczkit cities resolve against, and it is a locator, not a reference —
nothing here labels or validates anything.
A name is not unique (149 of the 5 558 are shared), so an ambiguous query is refused with the candidates rather than answered with the first match. Getting that wrong would run the wrong continent and record a manifest that looks entirely correct.
lczkit.places
¶
Any city in the world, by name — the general locator a run's extent comes from.
A run needs an extent. --bbox has always been the general answer and needs nothing on disk, and
lczkit.cities is the other end of the range: 28 named cities whose windows are pinned to where
the So2Sat labels are dense, so a run is comparable with a recorded agreement figure. Neither is
what someone wanting a map of their own city reaches for — the first asks them to find four numbers
and the second only knows 28 places, and only if the label archive is on disk.
This module is the middle. NASA/JRC's GUPPD ships one small table of every urban region on earth — 5 558 of them across 173 countries, with a name, an ISO code, a country and a bounding box — and it is 564 KB. Nothing in this package read it until now.
It is a locator, not a reference. Nothing here labels, validates or measures anything; it turns
a name into four numbers and records which row it came from. The hand-labelled LCZ sets stay where
they are, in lczkit.validation, reached by the sweep scripts and not by anything on the path from
a city name to a map.
Sizing, because it decides whether a plain --city run is a sensible default. Measured over
the shipped table: the median urban region is 80 km², the 90th percentile 412 km², and only 239
of 5 558 exceed 900 km² — the extent Berlin's 9.8-minute benchmark was measured over. So the
ordinary case is minutes and the tail is real: Jakarta's region is 17 661 km². shrink is how a
caller trims one, and the command line says the area before it starts.
GUPPD_SOURCE_DIR_NAME
module-attribute
¶
Subdirectory under input/. GUPPD is filed under the agency rather than under its own name.
GUPPD_BOUNDS
module-attribute
¶
The post-processed bounds table within input/NASA/, keyed on SMOD_ID.
The full GUPPD release beside it is a 117 MB GeoPackage of urban-region polygons. Reading the CSV rather than the polygons is deliberate: a locator needs a rectangle, and the rectangle is what the CSV holds, so resolving a name costs a 564 KB read rather than opening a spatial file.
Place
dataclass
¶
Place(smod_id: str, name: str, iso: str, country: str, bbox: BBox)
One GUPPD urban region: what it is called, where it is, and which row said so.
smod_id
instance-attribute
¶
GUPPD's own identifier, e.g. "30_3528". Recorded in the run manifest, because a name is
ambiguous — 149 of the 5 558 names are shared by more than one region — and this is not.
iso
instance-attribute
¶
ISO 3166-1 alpha-3, e.g. "DEU". What --country matches against, alongside the name.
bbox
instance-attribute
¶
bbox: BBox
The region's bounding box in lon/lat degrees, as (west, south, east, north).
area_km2
property
¶
Roughly how much ground the bbox covers, for deciding whether to shrink it.
A cosine-corrected rectangle rather than a projected area: the answer is used to print an
order of magnitude and to decide whether to mention --extent-km, and reprojecting 5 558
rectangles to answer that would be work spent on a digit nobody reads.
label
property
¶
"Berlin, Germany (DEU)" — how a place is named in output and error messages.
normalise
¶
A name reduced to what two spellings of it have in common.
Accents are stripped, case is folded and everything that is not alphanumeric collapses away, so
bogota finds Bogota, sao paulo finds São Paulo and washington d.c. finds
Washington D.C.. Applied to both sides, never to stored data — the table keeps its own
spelling, which is what gets printed back.
Source code in src/lczkit/places.py
load_places
¶
Every GUPPD urban region, in file order.
Raises FileNotFoundError naming the path when the table is not on disk, and saying that
--bbox needs nothing — the alternative is a bare csv error several frames down that names
neither the dataset nor the way round it.
Source code in src/lczkit/places.py
in_country
¶
places narrowed to one country, matched on the ISO code or the country name.
Either in full or as a prefix, under normalise — so GBR, gb and "united kingdom" all
reach the United Kingdom. None returns everything, so a caller can pass an optional flag
through without branching.
Source code in src/lczkit/places.py
find
¶
Regions whose name matches query, exact matches first.
Two tiers rather than a score: a name that is the query outranks every name that merely
contains it, so london returns London GBR and London CAN ahead of East London ZAF, and
cambridge returns both Cambridges and nothing else. Within a tier the file's order is kept,
so the answer does not depend on a sort that was never specified.
An empty query returns every region in country, so lczkit cities --country KEN is a
listing rather than a no-op. in_country is what narrows it.
Source code in src/lczkit/places.py
place
¶
The single region query names, or a LookupError saying why there is not one.
An ambiguous query is an error, and the message lists the candidates with their countries. Silently taking the first would put a run over the wrong continent and record a manifest that looks entirely correct — there are two Cambridges and three Londons in this table, and nothing about a bbox afterwards would say which one was meant.
Source code in src/lczkit/places.py
City registry — the comparable-extent locator¶
The 28 study cities the validation sweeps run over — those with enough hand-drawn So2Sat LCZ42
labels to validate against — with the window resolution used to pick a comparable extent in each.
Reached by lczkit run --city ... --so2sat-window, which is a flag rather than a fallback: this
window and the GUPPD region of the same city are different ground, and only this one makes a run
comparable with a published agreement figure.
lczkit.cities
¶
The So2Sat study cities, and how each one's window is found.
A run needs an extent. --bbox is the general answer and needs nothing on disk; this module is the
convenience that lets a caller say --city berlin and get the same 30 km window the published
agreement figures were measured over, so a run stays comparable with them.
It reads input/So2Sat-LCZ42/, so it is the one locator that needs DATA_DIR populated. --bbox
does not, and lczkit.places covers every other city.
WINDOW_KM
module-attribute
¶
Side of the square window, in kilometres. ~900 km2, matching the measured Berlin extent.
City
dataclass
¶
One So2Sat city: its labels, its country, and the region it speaks for.
iso
instance-attribute
¶
ISO 3166-1 alpha-3, so this registry can be matched against the GUPPD gazetteer.
Added because the two locators otherwise collided on a name. Three of these keys name a city
that exists in more than one country — London (GBR and CAN), Santiago (CHL and PHL) and
Los Angeles (USA and Chile's Los Ángeles) — so lczkit cities marked rows that carry no
So2Sat window at all, and --city london --country CAN --so2sat-window would have run
London, UK's window while the caller asked for Canada. Silent wrong ground, which is the one
failure the two-locator design exists to prevent.
densest_window
¶
The side_km square holding the most labelled patch centres, as a lon/lat bbox.
Searched over a quantile grid of candidate centres rather than optimised: the objective is piecewise constant and this is deterministic, which matters more here than optimality — a window that moved between runs would make two runs of the same city incomparable.
Centres, not areas. So2Sat patches are 320 m squares on a 100 m stride and overlap about
sevenfold, so counting area would measure the sampling density rather than the city; the same
reason lczkit.validation.labelled anchors each label on its patch centre.
Source code in src/lczkit/cities.py
patches_path
¶
Where target's labelled patches live under input/.
Source code in src/lczkit/cities.py
so2sat_window
¶
target's densest labelled window — the extent the published figures were measured over.
Raises FileNotFoundError naming the path when So2Sat is not on disk, because the alternative
is a pyogrio error several frames down that does not say which city or which directory.
Source code in src/lczkit/cities.py
shrink
¶
A concentric window of roughly extent_km on a side.
Kept because a full 30 km window is a multi-hour run, and the first thing anyone does with a new command line is try it on something small.