## [4.18.0] - 2026-09-20
Every tool that publishes an area, a length, a density or a rate now
measures it on the ground rather than in the layer's own coordinates.
Thirteen algorithms and one engine module changed. No tool was added,
removed or renamed, and no input or output schema moved.
### Fixed
- **Thirteen algorithms were publishing the projection's own
coordinate units under a metre's name.** `QgsGeometry.area()` and
`.length()` return the layer's coordinate units, and two independent
errors hide in that. The unit may not be the metre at all: a
California state-plane layer (EPSG:2229) is in US survey feet, so a
100-unit square is 929.041 m2 of ground while the layer reports 10000
- 10.76x, the square of the 0.30480060960122 factor. And a conformal
projection carries its own scale factor: EPSG:3857 is a metre CRS, but
they are metres on the sphere at the equator, so at 41 N a 20-unit box
is 227.835 m2 of ground against 400 square units in the layer -
1.757x, `1/cos(41 degrees)` squared. Lengths err by the first power,
so the same box is 1.325x in length and a foot layer's lengths are
3.28x. Nothing errors, no column is NULL and every feature count is
right, which is why this class survived a green test suite for three
releases while the manual and the changelog described it.
- **Building Form Metrics** - `area_m2`, `perim_m` and `court_m2` are
ground quantities, so `compact` and `convexity`, which are ratios of
two of them, cancel the error and were left alone.
- **Morphological Tessellation** - `cell_m2` is the cell's ground
area, while the cell's own layout stays in map units.
- **Spacematrix** - `fp_m2` and `gfa_m2` are ground areas; `gsi`,
`fsi`, `osr` and `levels` are shares of the block built from them, so
the conversion cancels in each and is asserted to.
- **Dasymetric Density Grid** - `dens_ha` is features per ground
hectare: the cell is a map-unit square and the feature count is gated
by it, so only the denominator moves.
- **Frontal Area Index** - `lambda_f` is a frontal area over a cell
area, both of them ground, so a metre of ground holds 1.325x as many
of them; `lambda_p` is a packing density and is conserved.
- **Green Space Access** - the park-hierarchy minimum sizes are
hectares, which are a planning standard and not a coordinate-unit
number, so a patch has to be measured on the ground before it is
compared with one. On EPSG:3857 at 41 N the unconverted reading let
patches 43 percent under the standard qualify.
- **Urban Green Connectivity** - the patch `area_m2` and the
connectivity index built on it.
- **Land-Use Balance** - `area_m2` and the `m2_capita` derived from
it. `balance_m2` is a ground area less a per-capita requirement that
is not an area of the city, so it moves without a single factor and is
not checked as one.
- **Residential Capacity** - each parcel's `buildable_m2` and the
district roll-up.
- **Service Areas** - the isochrone lengths and the pedshed areas.
- **Walkability Audit** - `int_km2` is a junction count per ground
km2, and `blk_len` and `slope_pct` are ground metres. The audit radius
stays in map units, because it is also what gates the junction and
destination counts around each segment.
- **Walking Slope Comfort** - `slope_pct` and `max_pct` are the same
rise over a run 1.325x shorter, so the gradient steepens. `climb_m`
and `descent_m` must *not* move: an elevation is a raster value and no
projection rescales it, so dividing them by the horizontal scale would
be the horizontal answer to a vertical question.
- **Road Noise Screening** - the acoustics were spread over a distance
the sound never travelled. The 25 m reference of the RLS-90 source
term, the 20 lg r spreading and the floor on r are all physical
distances, and at 41 N the unconverted version used a distance 1.325x
the real one. The grid, the cell size and the source cutoff stay in
the layer's own coordinates, which is what their labels say.
### Added
- **A check that can fail, for the whole family.** A per-row
re-derivation - measure each output row's own geometry and compare -
reaches exactly one shape of this defect: a tool that publishes its
own feature's area. Most of these tools publish a sum of
intersections, a network cost, a decibel level or a maximum, and for
those there is nothing to compare row by row. So every registered tool
is now run a second time on the same city rigidly translated to 29 E
41 N, where a map unit is 0.7547 of a ground metre and nothing else
about the run changes. Each column carries the movement its units
predict - a share or a ratio is unchanged, a ground area moves by cos
squared, a length by cos, a per-ground-area count or a
per-ground-metre rate by the inverse - and the five factors (0.57,
0.755, 1.0, 1.33, 1.76) are far enough apart that a misclassification
announces itself. A column still in the layer's own coordinate units
reads identically in both runs, which is what makes the comparison
falsifiable rather than decorative. Columns are compared as the sum of
absolute values over the column, not row by row, so row order and the
`round(x, 1)` most of these tools format with cannot affect the
verdict.
- **A `vertical` column kind**, for a quantity that comes off a raster
and must not scale with a projection at all.
### Changed
- **`planx:preparenetwork` was deliberately left alone.** Its
`length_m` is the one column in this family that is documented as
being in the source CRS, and the whole network family reads its costs
in that same unit, so converting it in isolation would silently
rescale every break, isochrone and cost downstream. The honest fix is
a rename plus a documented unit switch, which is a separate breaking
change and is not in this release.
- **The map-unit-by-design boundary is now written down.** A parameter
whose label says map units - a walking radius, a grid cell size, a
noise cutoff, an extent, a tessellation cell - stays in map units, and
a quantity compared against a published planning threshold is measured
on the ground. Where the two meet in one tool, both are named in the
source beside the conversion.
- **Six column groups are deliberately *not* checked**, each named
with the measurement that showed why, so a later reader does not
helpfully re-add one: a truncated count, a difference of two
quantities that do not scale together, a non-linear function of a
moving quantity, a count of features in a spatial partition (the
partition moves), a maximum over samples whose spacing moves, and a
column that is identically zero in both runs - two zeros agreeing with
each other are a green line no defect could turn red.
## [4.17.0] - 2026-09-20
Coseismic Landslide Screening (`planx:coseismiclandslide`) is new. One
algorithm added, none removed. It closes the ground-failure triangle:
ground motion says how hard the ground shakes, liquefaction asks
whether saturated soil loses its strength, and neither asks whether
the hillside itself moves. An earthquake triggers landslides on slopes
that were stable before it, and in a hilly city they are a large share
of the physical damage. The answer is a distance in centimetres - the
only quantity in this group that is a length rather than a probability
or a volume.
### Added
- **`planx:coseismiclandslide` - Coseismic Landslide Screening.**
Newmark's sliding-block calculation in the disposable-parameter form
of Jibson (2007) Equation 8. A block of ground on a slope stays put
until the shaking exceeds the acceleration that would just hold it
there, and how far it then travels is the displacement. The method
needs no constitutive model of the soil and no pore-pressure time
history, which is why four decades on it is still what regional
coseismic-landslide screening is built on.
- **The critical acceleration is the whole problem, and a city almost
never holds it.** It is a shear-strength property of the ground, so
this tool offers three routes and makes the user choose rather than
picking one: a **field** of critical accelerations the user already
has, which is the best route whenever it is available because it is a
measurement; a **geologic group** plus groundwater state through the
Hazus chain; or somebody else's **susceptibility category**. Every row
records which route it took in `ac_src`, because the same 0.25 g is a
measurement, a table lookup off a geological map, and a table lookup
off somebody else's susceptibility map, and the three say different
things about the ground.
- **No route has a default, and leaving one unset stops the run.** The
groundwater state on the Hazus route has no default either, and it is
not a detail: dry and wet are up to four susceptibility categories
apart at one slope and group - a factor of eight in the acceleration
the displacement is computed from - and a third of Table 4-14's cells
change between them.
- **The DEM is required on every route, including the ones that do not
use the slope to choose the acceleration.** The slope angle is part of
reading the answer whichever route supplied it, and a feature whose
sample point falls off the DEM or on a nodata cell stops the run
rather than being skipped - a missing terrain input is not a missing
hazard. A geographic CRS is refused for the same reason the other
terrain tools refuse one: its pixels are degrees.
- **Below Table 4-15's slope bound, Hazus establishes no susceptible
deposit, and the tool reports an EMPTY critical acceleration rather
than a zero.** The distinction is load-bearing. Zero is the value
Equation 8 diverges on, so a zeroed row would be the model's loudest
answer arriving disguised as its safest; the two are indistinguishable
in the displacement column, which reads 0.0 for both. The run log
counts the "never evaluated" rows separately and `ls_notes` carries
the reason on each one.
- **A zero critical acceleration is refused, not substituted.** The
`(ac/amax)^-1.478` term diverges as the ratio goes to zero, because a
material with no strength to mobilise is not a slope with a
displacement, it is a flow - and a flow is the liquefaction question,
not this one. The refusal is in the engine, in the run, and asserted
by a test that names what diverges.
- **The two non-sliding cases are reported as what they are rather
than clipped into silence.** Where `ac >= amax` the block genuinely
does not move: the displacement is 0.0, the log displacement is empty,
and the row notes say so. Where the ratio falls below 0.05 the
equation is extrapolating and the row says that instead - the value is
reported as the equation gives it, not floored to a "plausible"
ceiling.
- **Table 4-15's acceleration bound is applied as a floor on the Table
4-16 category value, and it changes exactly one cell of the whole
table.** Three groups by two moisture states by six slope bands is 36
cells, and the floor overrides exactly one of them: group B, wet,
above 40 degrees, where the category is X at 0.05 g and the bound is
0.10 g. `ac_table_floor_conflicts()` asserts which cell that is
instead of assuming it, so a future edit to either transcribed table
cannot move it silently, and every row the floor touches says so in
its notes.
- **Table 4-17's area fraction is reported in its own column and never
multiplied into the displacement.** The displacement is a property of
the sliding block and the fraction is a property of the map unit;
folding them together would produce a number that is neither.
- **The susceptibility-category route accepts Roman numerals only.** A
numeric column cannot say whether 1 means the least susceptible, as
Hazus counts it, or the most, and guessing that wrong reverses the map
while leaving every count and every schema check green - the failure
mode a value check cannot see. Digits are refused with the list of the
values found, which is what a user needs in order to fix the column.
- **One output schema for all three routes**, with the columns a route
does not compute left empty rather than absent, so a saved Processing
model that reads a column keeps working when the route changes - and a
model that reads a column which is empty for this route can see that
it is empty, which a missing column could never tell it. Twelve
computed columns: the median displacement and its 90th percentile, the
log displacement, the acceleration and its route, the ratio the
equation actually turns on, the Hazus category and area fraction, and
the shaking, magnitude and slope the row was evaluated at - so any row
can be re-derived from the file it is in. `RENDER_FIELD` is set to
`nw_disp_cm` explicitly rather than left to the renderer's preference
list, which contains no token any of these columns carries: left to
itself the tool would write a correct table with no colour scale at
all.
### Changed
- `provider.py` registers the tool last in Seismic Risk (19), after
the four existing tools, and the manual's group-19 card order matches.
- All three gate floors moved from 74 to 75
(`smoke_provider_catalog.py`, `qgis_runtime_algorithm_matrix.py`,
`tests/smoke_plugin.py`).
- `metadata.txt` is 4.17.0, with a new changelog block, the
coseismic-landslide clause added to the description's seismic
parenthetical, and the about text describing five chained seismic
tools. No literal percent sign.
- The manual gained a nine-section card for the new tool - overview,
theoretical background, eight numbered equations, input requirements,
11 parameters, outputs, symbolic representation, a six-part
interpretation guide and four references - plus its table-of-contents
entry, and the Seismic Risk group preface now says five tools and
describes where the new one sits. Counts are read off the built manual
rather than estimated: 336 to 344 numbered display equations, 398 to
402 reference entries, 305 to 307 with DOIs, and 74 to 75 algorithms
in each of the four places the figure appears in the manual and again
the four places it appears in the README. The manual's own footer,
title and introduction carry the version, so all three moved to 4.17.0
with the algorithm count - the gate that scans for that is exact, and
a manual one release behind is the failure it was written to catch.
- `docs/METHODS.md` gained the coseismic-landslide paragraph - Jibson
Equation 8 with its dispersion and the two non-sliding cases, the
three routes to the critical acceleration, the Hazus Tables 4-14 to
4-17 chain and its one floor conflict, and the D8 slope with the pixel
left in the DEM's own units - and its closing caveat now says that the
landslide displacement is a United States calibration with no Turkish
landslide inventory behind it.
- `THIRD_PARTY_NOTICES.md` gained three sections: the Jibson (2007)
coefficients with the open-access source they were transcribed from,
the Hazus Section 4.2.2.2 tables, and the USGS Open-File Report 98-113
Arias-intensity model that is deliberately **not** implemented.
### Notes
- **The paper was not read, and the coefficients say so.** Jibson
(2007) is paywalled; its Equation 8 coefficients were transcribed from
Yiğit (2026), *Pamukkale Üniversitesi Mühendislik Bilimleri Dergisi*
32(1), 191-199, an open-access article that reproduces the published
Newmark regressions beside its own refits of the same data. That
side-by-side is what makes it usable as a transcription rather than
only as a citation: its own fit of the same data prints a different
exponent (1.3593 against 2.335), and its Equation 9 reproduces the
Arias-intensity form digit-for-digit including the 0.616 dispersion.
The provenance record in `engine/provenance.py` names the
transcription source and the fact that the original was not read.
- **Jibson Equation 9 and the Arias-intensity regression of Jibson,
Harp & Michael (1998) are deliberately not implemented, and the manual
says why.** Both are Arias forms — Equation 9 is `0.561 log10 I_a -
3.8331 log10(ac/amax) - 1.474` — and Arias intensity is the total
energy the shaking deposits, which Ground Motion Scenario does not
produce and no other tool in this plugin does. Deriving one would mean
a ground-motion model of its own. Equation 8 is the only regression in
the paper that takes exactly the two quantities the seismic chain
already produces, the critical-acceleration ratio and the magnitude,
with nothing to import from outside it. The USGS Open-File Report
98-113 is listed in the manual's references as the model most often
misremembered as the one this tool uses.
- **The runtime matrix grew a check that re-derives its rows instead
of snapshotting them.** `_expect_coseismic_rows` puts every output row
back on the synthetic surface's own closed form - the slope is
recomputed from the terrain's analytic gradient at the row's own
sample x - then re-runs the Hazus chain and the displacement from the
tool's parameters and compares. It tests the tool's wiring, not the
engine's constants, which is the right division: mutating a constant
would move the run and the check together and stay green, and that is
`test_engine.py`'s job. So the check was falsified with wiring
mutations, and both were caught. Scaling the slope by 57 instead of
using the right conversion failed with `row 1: slope_deg is
6.301205433 where the synthetic surface gives 6.333903108`; writing
the wrong route prefix into `ac_src` failed with `row 1: ac_src is
'hazus-cat:V', which is not one of the routes this run takes (field)`.
Both were reverted, and the file was restored to green.
- **A check that only ever passes is a check nobody has tested, so the
new one carries its own guards against going quiet.** It reads the
grid the terrain was sampled on from a module-level record filled in
by the fixture builder rather than inferring it from an output row,
and complains out loud if that record is empty - because the slope
angle alone cannot be inverted back to a cell, and a slope check that
quietly stops checking is exactly the failure the fixture was rebuilt
to close. It also asserts that the run actually touched every route it
was asked for, all ten Table 4-16 categories on the category route,
all six slope bands on the group route, and that the sampled slopes
span at least 15 degrees, so a future fixture edit that stops reaching
a branch fails instead of narrowing coverage in silence.
- **Two defects in the test harness itself were found by this work and
fixed.** Every fixture raster had been written at `GDT_Float32` while
the closed form is float64, so the synthetic surface carried about
1e-7 of relative error - roughly 5e-7 degrees of slope - which is
enough to make a 1e-9 tolerance meaningless. The terrain is now
written in double precision, and the tolerance stays where it was
rather than being loosened to accommodate the fixture. Separately, an
extra run's failure message was overwriting the primary run's: the
case loop assigned `result.error` unconditionally, so when both failed
the first and more fundamental complaint was lost. The two are now
joined in the order they were found.
- **The engine suite stands at 731 checks**, up from 702, including
the ratio exponents against the values the open-access source prints,
the 0.424 magnitude term and the 0.454 dispersion, the divergence
refusal at a zero critical acceleration, the `ac >= amax` case
returning a genuine zero with empty log displacement, the one-cell
Table 4-15 floor conflict, and the category matcher accepting "very
high" and refusing "1".
- Push, tag and the plugins.qgis.org upload remain manual, as for
every release. The version is committed and tagged locally; it is not
published.
## [4.16.0] - 2026-09-19
Liquefaction Screening (`planx:liquefaction`) is new. One algorithm
added, none removed. It is the one secondary ground-failure hazard
that runs from the layers a city usually has, and it carries two
published models behind one interface: a global regression from a DEM
and a shaking level, and the Hazus susceptibility tables for map units
somebody has already classified - which it refuses to run without.
### Added
- **`planx:liquefaction` - Liquefaction Screening.** The rest of the
seismic chain assumes the ground stays where it is: ground motion says
how hard it shakes, debris and casualties say what that does to
buildings. Neither asks whether the ground itself fails. Saturated
sand loses its strength under cyclic shaking, and the resulting hazard
is on a different scale, from a different set of inputs, and - unlike
collapse - not correlated with building age. The research note behind
the tool is a negative result worth keeping: almost no published
ground-failure method works from shaking alone, because they generally
want a modelled water table, a rainfall climatology or a
susceptibility surface. Zhu et al. (2015) is the exception, and its
two extra inputs are both derivable from a DEM.
- **Regression mode - Zhu, Daley, Baise, Thompson, Wald & Knudsen
(2015)**, *Earthquake Spectra* 31(3), 1813-1837, Table 3, the global
model the literature calls GGM1. A logistic regression on a
magnitude-weighted shaking level, a topographic wetness index and
slope-derived Vs30. It needs a DEM and a PGA and nothing else: the
wetness index and the slope come out of one D8 pass on the DEM, and
the velocity comes from the slope through a published table - or from
a Vs30 field or raster, which wins over everything when it is there.
- **The magnitude term is inside the logarithm, and the test suite
says so.** The published expression is the log of PGA times a power of
magnitude, not a plain log of PGA with magnitude multiplied in beside
it. The correct form scales magnitude by 2.067 x 2.56 = 5.29; the form
that writes the power as its own additive term scales it by 2.56. Both
produce a smooth, monotone, plausible surface, so the identity is
asserted rather than trusted.
- **That power of magnitude turns out to have a name.** `Mw^2.56 /
10^2.24` is the reciprocal of the magnitude scaling factor published
by Youd et al. (2001) in the NCEER/NSF workshop summary - their
Equation 24, `MSF = 10^2.24 / Mw^2.56` - so the quantity the model
takes the logarithm of is the *magnitude-weighted* PGA, and the
exponent is a citation rather than a bare constant. At Mw 7.5 the
factor is exactly 1.0, which is why the model reads as plain "PGA" in
the middle of its own range and as something else entirely at Mw 9.
- **The percent-g division is not applied, and the clip ceiling is
reported in the parameter's own unit.** The published term divides by
100 because the ShakeMap grid it was fitted on is in percent g; a
field already in g must not be divided again, and applying it would
move the logit by `2.067 x ln(100) = 9.52` - enough to turn a 2
percent probability into 0.9999. The source's clip ceiling of 270 is
reported and applied as 2.7 g.
- **A coefficient the literature prints wrong by a factor of ten.**
Independent reproductions print the site-velocity coefficient as
0.4784; the USGS `gfail` implementation carries 4.784. This tool uses
4.784, and the reason is arithmetic: `ln(Vs30)` spans about 5.2 to 6.6
over real sites, so 0.4784 removes at most 0.7 logit units of site
term where 4.784 removes up to 6.7. With 0.4784 the intercept of 24.10
dominates every other term and the logistic returns a probability of
essentially 1 everywhere - a model that cannot rank anything. The
coefficient is one named constant, the sensitivity it implies is
pinned by a test, and the number is visible in the engine.
- **The wetness index, and the unit error a logarithm hides.** `CTI =
ln(a / tan β)` with `a` the D8 accumulation times the pixel size,
which must be in metres: the index is the log of a metric *area*, so a
DEM in feet inflates every cell by `ln(3.2808) = 1.188` - a uniform
offset the CTI coefficient turns into 0.42 logit units of pure unit
error. No range check can see it, because 1.19 is an ordinary index
value, and no map looks wrong, because the offset is constant. The
DEM's own linear unit is therefore read and converted explicitly, and
a DEM whose CRS declares no linear unit at all is refused. The slope
argument of the same model is safe by contrast, because a gradient is
a ratio of two lengths in one unit - that asymmetry is the whole trap.
The same class as 4.15.1's raw-geometry areas, fixed here before it
shipped rather than after.
- **A flat or ponded cell is not a missing cell.** It has no downslope
neighbour, so its index is genuinely infinite - the wettest possible
answer, which the model's own ceiling of 15 evaluates. Those rows are
counted and reported separately from a cell that is off the grid or on
nodata, which stops the run: a missing terrain input is not a missing
hazard.
- **Site velocity comes from a table, because the report publishes a
table.** A common misconception about USGS Open-File Report 2007-1357
is corrected in the engine: there is no two-branch `log(Vs30) = a + b
log(slope)` regression in it. What is published is slope ranges mapped
to NEHRP classes, with the method statement that a slope inside a
window takes a velocity interpolated between the subdivided class
boundaries - so the bin edges transcribe as the nodes of a
piecewise-linear map. Outside the published nodes the value is held
flat at the table's own endpoint and the run reports how many rows
that affected, rather than extrapolating a curve nobody published.
- **The tectonic setting has no default, because the two columns are
not close.** At one slope the active-margin and stable-continent
columns differ by up to a factor of 1.96 in velocity, which the site
coefficient turns into about 3.2 logit units. The regression mode
refuses to run until the setting is chosen, and the log says which
column was used and whether it changed any row.
- **Susceptibility mode - Hazus 6.1 Earthquake Model Technical Manual,
Section 4.2.2.1**, Equations 4-9 to 4-11 and Tables 4-10 to 4-13,
transcribed from the manual. It reads a polygon layer of map units
somebody has already classified Very High, High, Moderate, Low, Very
Low or None, and gives each unit the probability and the expected
settlement in inches. Both correction polynomials are applied exactly
as printed, reference-value residual and all: K_M at Mw 7.5 comes out
1.0147 and K_W at the 5 ft reference depth 1.04 rather than 1.0.
Renormalising them would move every number in the tool about 5 percent
away from the published method to fix a cosmetic inconsistency in the
source, and Hazus's own figure plots a marker at Mw 7.5 that reads
1.01. The sixth row of Table 4-11 is not a line at all - the "None"
category is a constant zero - and it is kept in the same table as a
degenerate line so that every category reaches the same code path
instead of a key that a user-supplied category could fall through.
- **The susceptibility mode refuses to run without a classified layer,
and refuses a category it does not recognise**, listing the values it
found. Hazus itself assumes no ground failure when it has no input,
which makes an empty map of nowhere-liquefies the most dangerous
answer this family of tools can give: an unrecognised category read as
zero would report "no liquefaction hazard" for a unit nobody ever
classified. Case and separators are ignored, so "very high" and
"Very-High" are accepted; "VH" and "1" are not.
- **One output schema for both modes**, with the columns a mode does
not compute left empty rather than absent, so a saved Processing model
that reads `liq_prob` keeps working when the combo box moves - and a
model that reads a column which is empty for this mode can see that it
is empty, which a missing column could never tell it. Fifteen columns:
the probability and its 0.81 area reading kept strictly separate, the
Hazus terms, the inputs the logit actually used (`pga_g`, `mw`, `cti`,
`slope_deg`, `vs30_ms`, `vs30_src`) so a run is auditable from the
layer alone, and `liq_notes` carrying the per-row remarks - a clipped
CTI, a clipped PGA, a velocity held at a table endpoint, a footing on
the wettest possible cell. `RENDER_FIELD` is set to `liq_prob`
explicitly rather than left to the renderer's preference list, because
the next column added to this table is one rename away from silently
taking the colour scale off the probability.
- **Shaking with no field is a refusal, not a substitute.** A blank or
zero PGA row stops the run: the model takes `ln(PGA)`, and unlike the
velocity there is no reference shaking level to fall back to, so
substituting one would move the answer by orders of magnitude.
**Ground Motion Scenario** writes the column this tool wants.
### Changed
- **`engine/hydro.py` gained the two terrain quantities this tool
needed, and the D8 pass they share.** `slope_radians(dem, pixel)` and
`cti(dem, pixel)` are new; the steepest-descent walk behind them was
factored out into `d8_steepest`, which returns the direction grid and
the gradient together, so the flow direction, the slope and the
wetness index cannot drift apart. The gradient is what makes the
pixel-size argument unit-aware at one point instead of three.
- **The geographic-CRS refusal in `algorithms/_raster.py` now names
the layer and the reason.** It said the DSM needs metres "for
shadow/SVF math"; the same reader now feeds a wetness index, whose
pixel size is a *length* rather than the dimensionless gradient the
DSM tools use, so the message states the CRS and the layer instead of
one tool family's use case.
- `provider.py` registers the tool, in Seismic Risk (19), between
Ground Motion Scenario and Seismic Collapse and Debris Spread.
- Both gate floors moved from 73 to 74 (`smoke_provider_catalog.py`,
`qgis_runtime_algorithm_matrix.py`), and `tests/smoke_plugin.py`'s
hard algorithm-count assertion with them.
- `metadata.txt` is 4.16.0, with a new changelog block, the
liquefaction clause added to the description's seismic parenthetical,
and the about text describing four chained seismic tools. No literal
percent sign.
- The manual gained a nine-section card for the new tool - overview,
theoretical background, thirteen numbered equations, input
requirements, 13 parameters, outputs, symbolic representation, a
four-part interpretation guide and seven references - plus its
table-of-contents entry, and the Seismic Risk group preface now says
four tools and describes where the new one sits. Counts are read off
the built manual rather than estimated: 323 to 336 numbered display
equations, 391 to 398 reference entries, 300 to 305 with DOIs, and 73
to 74 algorithms in each of the four places the figure appears in the
manual and again the four places it appears in the README. The
manual's own footer, title and introduction carry the version, so all
three moved to 4.16.0 with the algorithm count - the gate that scans
for that is exact, and a manual one release behind is the failure it
was written to catch.
- `docs/METHODS.md` gained the two seismic paragraphs this tool needs
- the Zhu regression with the exponent trap, the percent-g division,
the metric-CTI trap, the Vs30 table and its tectonic divergence, and
the Hazus Equations 4-9 to 4-11 with their tables - and its closing
caveat now says that the liquefaction regression is a United States
calibration with no Turkish liquefaction inventory behind it.
- `THIRD_PARTY_NOTICES.md` gained three sections: the Hazus Section
4.2.2.1 tables, the Zhu et al. (2015) coefficients with their `gfail`
provenance and the 0.4784/4.784 discrepancy recorded next to them, and
the USGS slope-to-Vs30 table including the Youd et al. (2001)
identification of the magnitude term.
- `docs/icons/` gained the four tool icons the manual references and
it did not carry - `tool_groundmotion.png`, `tool_liquefaction.png`,
`tool_parkingbalance.png` and `tool_seismicimpact.png`. Cards added in
4.14.0 and 4.15.0 were rendering with a broken image beside them.
- `docs/TRAPS.md` (monorepo) gained **5.9**, a logarithm turns a unit
error into an additive constant and no range check can see it, with
the 1.188/0.42 arithmetic, the contrast against 5.8's area multiplier,
and the analytic-plane test in both unit systems.
### Notes
- **Three citations corrected, and one of them was a paper that does
not exist.** The model had been attributed to "Zhu, Baise & Thompson
(2017), BSSA 107(1), Table 3" in the tool's help, its metadata and the
methods file. Verified against Crossref: the 2017 follow-up is *BSSA*
107(3), 1365-1385, and the coefficient set belongs to the 2015
*Earthquake Spectra* paper. Worse, the engine's provenance note
credited the coefficient cross-check to "Baird, Geyin & Maurer (2018)"
- no such paper exists; a Crossref author search turns up only
**Geyin, Baird & Maurer (2020)**, *Earthquake Spectra* 36(3),
1386-1411, whose Table 3 carries the reproduction in question. A year
and an author order written from memory became a citation to nothing,
which is why the rule is to read them rather than recall them.
- **The same reproduction supplied the honest performance figure.**
That 2020 assessment compares 23 liquefaction models against 15,223
case histories from 24 earthquakes and reports a global median
area-under-curve of **0.54** (95 percent confidence interval 0.47 to
0.61) for GGM1 - barely better than random guessing at global scale,
with considerably better performance on regional datasets. That number
is in the manual's limitations, with the caveat that the assessment
prints the 0.4784 coefficient, so its figure may describe a saturated
version of the model rather than the one implemented here. Either way
the conclusion is the same and is stated wherever the output is:
**this ranks places within a city, it does not predict a calibrated
probability.**
- **The runtime matrix gained a capability it did not have:
per-extra-run value expectations.** Until this release an extra run
was judged only by whether it produced populated outputs, so a branch
writing the right number of rows with the wrong numbers passed every
check. `EXTRA_VALUE_EXPECTATIONS` and `check_extra_values` now let an
extra run assert what its numbers must be, and the liquefaction
susceptibility case uses it to re-derive K_W, K_M, the clipped
conditional, the probability and the settlement from the engine's own
published tables and compare them row by row against the written
GeoPackage.
- **Falsified before trusted.** Perturbing the shared constant would
have moved the run and the check together and stayed green, so the
expectation alone was perturbed - `hazus_groundwater_factor(1.524 +
0.1)` - and the case failed with the exact message intended: `row 1:
K_W is 1.040000 where 1.524 m gives 1.047218 - Equation 4-11 takes
feet`. Restoring the file returned it to green. A check that has never
failed is a check that has never been tested.
- **The susceptibility extra run needed a fixture that speaks the
model's vocabulary, and building one was the right answer rather than
weakening the check.** It first refused with `The category column
holds 4 value(s) this model does not define: 'commercial' x8, 'green'
x5, 'residential' x10, 'school' x2` - the demo city's `category`
column holds land-use words, and the tool refuses them on purpose. A
new `hazus_units` fixture carries a `liq_category` column cycling the
six names the tables are written for, which is the only way one run
can touch all six branches. The fixture's spread is itself asserted,
so a future edit that stops reaching every category fails rather than
quietly narrowing coverage.
- **These two modes are not comparable, and the manual says so in its
own section.** The regression asks whether a place is
liquefaction-prone given its terrain and shaking; the susceptibility
mode asks what the chance is that a map unit somebody already
classified lapses. Different evidence, different footing - and the
susceptibility run is only ever as good as the classification it was
handed. Putting the two on one map and comparing the values ranks the
two datasets, not the two places.
- **The engine suite stands at 702 checks**, including the exponent
identity (a sensitivity of 5.29152 against the 2.56 the wrong form
gives), the percent-g trap, the analytic CTI on a plane and the same
plane in feet differing by exactly `ln(3.28084)`, the Vs30 piecewise
interpolation and its clamping flag, Table 4-11's zero crossings
reproducing Table 4-12's thresholds, and the category matcher
accepting "very high" and refusing "VH".
- Push, tag and the plugins.qgis.org upload remain manual, as for
every release. The version is committed and tagged locally; it is not
published.
## [4.15.1] - 2026-09-19
Three tools reported areas and lengths in the layer's own coordinate
units while documenting them as metres. Fixed here, with both error
classes named, so the next one is checked rather than found. No
algorithm added or removed, no parameter changed, no output column
renamed - the values that move are the ones that were wrong.
### Fixed
- **`planx:seismicdebris` now reports ground metres.** The tool fed
`QgsGeometry.area()` - the layer's own square units - straight into
`seismic.debris_extent`, which documents its outputs as cubic metres
and tonnes, and into `footprint_area`, which documents square metres.
Two different things break that assumption, and they are not the same
defect:
- **The coordinate unit is not always the metre.** On EPSG:2229 -
California zone 5, US survey feet - a square 100 units on a side is
929.041 m² of ground. The layer's own `area()` says 10000. A 10.76x
figure, the square of the 3.28 foot factor.
- **A conformal projection carries its own scale factor.** EPSG:3857
is a metre CRS, so its units look like metres, but they are metres on
the sphere *at the equator*. At 41°N a 20-unit box is 227.835 m² of
ground against 400 square units in the layer - 1.757x, `1/cos(41°)`
squared. This is the 1.76x recorded as an open defect in 4.15.0, still
in a metre CRS.
Both scale a plausible number by a constant, which is why neither
looks like an error in a result table.
- **`debris_vol_m3`, `debris_pile_m3` and `debris_mass_t` move with
it**, being built from that area. The **radius does not**: it is
driven by height alone, as the 4.15.0 note said. On the demo city -
which sits at the EPSG:3857 origin, the one place on that grid where
the scale is exactly 1 - every number is unchanged, which is also why
the runtime matrix never saw this.
- **Street corridor widths are now metres too.** The debris envelopes
buffered by half the debris width in layer units: on a foot layer a 5
m road was cleared by a 5-foot envelope. Same conversion for the reach
along the street axis, the ROI hull and the morphological opening
behind `OUT_NAVIGABLE`, where `MIN_CLEAR_WIDTH` is a metre threshold
by definition.
- **`planx:streetmorphology` - the defect carried from 4.14.0, fixed
in the same release.** `total_length_km`, `avg_segment_length_m` and
`intersection_density_km2` divided raw network units by 1000 and raw
hull units by 1e6. On a foot layer those figures were wrong by 3.28
and 10.8; on EPSG:3857 at 41°N they were wrong by 1.33 in length and
1.76 in area, on a metre layer that gave no sign of it.
- **`planx:seismicimpact` had the same defect in the same release it
was written.** The occupant estimate reads the footprint from the
geometry when the layer has no footprint column, which is exactly the
fallback path, and that read was raw. The casualties and the shelter
need inherited the area error the debris tool had just been fixed for.
- **New module `planx/algorithms/_units.py`.** `GroundUnits` binds a
`QgsDistanceArea` to the layer's CRS and ellipsoid and exposes
`area()` (m²), `length()` (m), `per_metre()` (layer units per ground
metre, measured along the geometry where it has length) and `scale()`
(layer units per ground metre at the centre of an extent). One
mechanism covers both defects, because both are the same question: how
long is a layer unit on the ground here. A CRS with no ellipsoid and a
geographic CRS both raise rather than guess.
### Notes
- **The runtime matrix is structurally blind to this class, and still
is.** Its fixture city is generated at `(0, 0)` in EPSG:3857
(`engine/demo.py`), where the projection's scale factor is exactly 1 -
so 73 cases ran green over a defect that any real city would have
shown. `tests/smoke_plugin.py` now carries the check the matrix
cannot: a 20-unit box at 41°N on EPSG:3857 against `(20*cos 41°)²`, a
100-foot square at Los Angeles on EPSG:2229 against
`(100*0.30480060960122)²` with its `per_metre` reciprocal, and a 1000
m square on EPSG:32635 that must stay near 1e6 rather than be
corrected away. Each expectation is written out from the CRS
definition, not read back from the module under test.
- **The same class is still live in about a dozen other tools** that
write an `area_m2` or a `km2` column from raw geometry -
`building_metrics`, `green_connectivity`, `landuse_balance`,
`residential_capacity`, `spacematrix`, `tessellation`, `density_grid`,
`growth_sim`, `landcover_change`, `sprawl_metrics`, `walkability`
among them. They are not touched here: it is a wide pass that changes
reported values in most of the plugin's analytical tools, and it
deserves its own release rather than a footnote to this one.
- **A value check must not read its own output through QGIS, and the
matrix now says so.** The new debris check first read `OUT_BUILDINGS`
with a `QgsVectorLayer` and the case went red on both runtimes with
`Creation of data source failed (OGR error: A file system object
called '...gpkg' already exists.)` - because the matrix's extra runs
re-create a destination the primary run already wrote, and on Windows
an open QGIS datasource keeps the file from being replaced. Measured:
with no prior read the re-create and the delete both succeed; after
one read through QGIS both fail with `winerror 32` whether the layer
wrapper is kept, `del`-ed, or `del`-ed plus `gc.collect()`. All three
value checks now read their output with `sqlite3` (`_read_gpkg`),
which holds no GDAL handle and reads the columns exactly as any other
program would. The trap was latent in the two checks that already
existed; it needed only an extra run on their output to fire.
- **New value check for the debris identity.** `debris_vol_m3` must
equal `footprint_area × height_m × SOLID_VOLUME_RATIO ×
f(damage_state)` for every building, feature by feature - the identity
the footprint column exists to make checkable, and the one that breaks
first if the reported area and the area the volume was built from ever
stop being the same number.
- **`tests/smoke_plugin.py` was hanging, and no longer is - and the
cause is not the one the first reading suggested.** Run by hand it
printed its verdict and then never returned; run under `pf verify` it
finished normally. The difference is the QGIS profile, not the test:
both runs hand `QgsApplication` a throwaway profile of their own, but
only `pf verify` also sets `QGIS_CUSTOM_CONFIG_PATH` (TRAPS 4.6).
Measured on 3.44 - the module as it stood printed `PASS` and exited 0
with that variable set, and printed the same `PASS` and then hung
until killed without it - so the block is inside QGIS's own shutdown
path, in the real user profile, and a profile folder passed to the
constructor does not avoid it. It now exits through `os._exit` from
the frame that still holds the application, as
`qgis_runtime_algorithm_matrix.py` already did, which short-circuits
teardown on either environment; the failure path exits 1 with its
traceback and no hang, which is what `pf verify` reads. The teardown
half of the hazard (`exitQgis` blocking once a provider is loaded)
stays guarded the way the matrix guards it.
## [4.15.0] - 2026-09-19
Seismic Human Impact (`planx:seismicimpact`) is new. One algorithm
added, one existing tool gained an output column, none removed. The
Seismic Risk group now runs end to end: how hard the ground shakes,
what that does to buildings and streets, and who is hurt when it does.
### Added
- **`planx:seismicimpact` - Seismic Human Impact (Casualties and
Shelter).** The two existing seismic tools stop at physics. This one
carries their result into the two Hazus models that end in people -
Section 12 for casualties and Section 13 for displacement and shelter
- and it reads the four damage-state probabilities (`prob_slight`,
`prob_moderate`, `prob_extensive`, `prob_complete`) that Seismic
Collapse and Debris Spread writes, so the chain runs without a file in
between. Any layer carrying those four columns works too.
- **Casualties from the whole damage distribution, not from collapse
alone.** Hazus's event tree is implemented as the manual writes it:
every damage state contributes its own four injury rates, and only the
Complete state splits, into complete-but-standing and collapsed, by
the Table 12-8 probability of collapse given Complete. That split
carries almost all of the deaths - for a C1L frame the collapsed
branch's severity-4 rate is 10 per occupant against 0.01 for the
intact branch - so a model without it would report Complete damage as
nearly harmless.
- **Four severities, in Hazus's own order** - treatable injuries,
injuries needing hospitalisation, life-threatening injuries, instant
deaths (Table 12-1) - written out per building as `cas_sev1` to
`cas_sev4` and summed into `cas_total`. People outside the building
are counted separately on the manual's outdoor tables, which drop the
slight-damage branch and do not split on collapse: falling parapets
and glazing hurt people on the pavement whether or not the floors came
down.
- **Who is in the building is a parameter, not an assumption.** Hazus
Table 12-2 gives the indoor and outdoor share of every occupancy class
at 2 a.m., 2 p.m. and 5 p.m., and those shares decide the outcome: a
residential building is 98.9 percent occupied at 2 a.m. and 52.5
percent at 2 p.m., a school is empty at 2 a.m., and a hotel is a fifth
full at 2 p.m. A night earthquake and a day one are two runs of this
tool, and the help recommends both.
- **Occupants** come from a population field when the layer has one,
or - when it does not - from footprint area times storeys divided by
an area per occupant, 30 m² by default. That divisor is stated as the
tool's own screening assumption rather than a Hazus figure, in the
parameter description and in the run log, together with which of the
two sources produced the count. A dwelling-units field drives the
Section 13 equations when the layer has one; without it a residential
building counts as one dwelling and a non-residential building as
none, which is what a non-residential occupancy is.
- **The floor-area source reads the footprint from the geometry, or
from a column when the buildings are points.** A point has no area,
and the tool upstream of this one writes points, so the footprint is
taken from a field when the geometry gives nothing - named by the new
optional **Footprint area field** parameter, or found automatically in
`footprint_area`, `area_m2`, `area`, `shape_area` so the
debris-to-casualties chain runs with nothing to configure. With no
footprint anywhere the run **stops** and says so: the floor-area
estimate would be zero at every building, and a run reporting a city
of nobody is exactly the answer this tool refuses to give quietly.
When only some buildings lack one, it warns with the count instead.
- **Shelter** is Hazus Equations 13-1 to 13-5 with Table 13-1's
single-family and multi-family weights: `displaced_hh` is the
households that lose their home, and `public_shelter` is the people
who will seek publicly provided shelter. The demographic filter is
implemented in full, with Table 13-2's category weights and four
modifiers that default to 1.0. That default is deliberate and is said
wherever the number appears: at neutral the filter changes nothing, α
is exactly 1.0, and the answer is an **upper bound** in which every
displaced person seeks public shelter. Hazus's own Table 13-3 factors
run from 0.13 to 0.62, so a real estimate sits well below the default.
- **No damage columns, no answer.** If the four probability columns
are absent and no damage-state column is named, the tool raises with
the reason rather than returning zeros, because a zero casualty count
is indistinguishable from a good outcome. A single damage-state column
is accepted as a fallback and the run log says that it discards the
distribution. Neither `displaced_hh` nor `public_shelter` is an error
case for a non-residential building - both are zero, which is what the
equations give.
- **`planx:seismicdebris` now writes `footprint_area`** - the
building's own `QgsGeometry.area()`, kept so the casualties tool can
read a footprint back off its point output. It is not called `area_m2`
because it is in the layer's area unit, not necessarily square metres.
Placed next to `height_m`, which it pairs with; it carries no renderer
token, so `collapse_prob` is still the field the default renderer
colours by, and the field-order contract in that method's docstring
and `tests/smoke_plugin.py` still holds.
- Output is one feature per building with its geometry and every input
attribute preserved, so the layer joins straight back onto the damage
and debris outputs, plus `occ_class`, `bldg_type` (the resolved Hazus
type), `occupants`, the four severity counts, `cas_total`,
`displaced_hh` and `public_shelter`. `RENDER_FIELD` is set to
`cas_total` so the default renderer colours by it rather than by
whichever numeric field the generic candidate list happened to match
last.
### Changed
- `metadata.txt` is 4.15.0, with the description and the about text
now describing three chained seismic tools and a 4.15.0 changelog
block. No literal percent sign.
- The manual gained a nine-section card for the new tool - overview,
theoretical background, ten numbered equations, input requirements, 22
parameters, outputs, symbolic representation, an eight-part
interpretation guide and three references - plus its table-of-contents
entry and a Seismic Risk group preface that now says three tools and
describes what the new one closes. Counts are read off the built
manual rather than estimated: 314 to 323 numbered display equations,
388 to 391 reference entries, 300 with DOIs, 72 to 73 algorithms in
the four places the figure appears. The debris card's output row also
gained `footprint_area` - and, in the same row, the four `prob_*`
columns, which were added to that tool's schema for the casualty model
in this release and had not been listed. A field list a reader takes
as complete, and which is not, is a defect of a different kind from a
wrong number - and it was the second one this pass turned up. The
first was this file: 4.15.0 credited the new tool with 22 parameters
when its manual table had 21 rows and `initAlgorithm` defined 21. The
parameter added here makes the figure 22 and true, which is a
coincidence, not a correction. Both counts were then re-derived by
reading the parameter table and `initAlgorithm` against each other -
22 rows, 22 parameters, same order - rather than by trusting either
figure.
- The README's tool table had **no seismic rows at all** - nor the two
parking tools - so it listed 68 of the 73 algorithms while claiming
72. It now carries a Seismic Risk group with all three tools and the
two parking tools under Travel Demand, and has exactly one row per
algorithm. Its algorithm count, its "seventy-three tools" line and its
engine-check figure (627 to 670, which is what the suite actually
runs) are current.
- `docs/METHODS.md` gained a **Seismic Risk section, which it did not
have**: the ground-motion functional form and site term, the fragility
and debris equations, the casualty event tree and the shelter
equations with their sources. The group was previously undocumented
there even though the README points at that file for methods and
sources. The two parking tools' methods were added to its Travel
Demand section in the same pass.
- Both gate floors moved from 72 to 73 (`smoke_provider_catalog.py`,
`qgis_runtime_algorithm_matrix.py`).
- `THIRD_PARTY_NOTICES.md` gained a Hazus casualty and shelter section
alongside the existing fragility one, naming Sections 12 and 13, the
figures and tables transcribed, and the two limits that are not
FEMA's: the rates are applied to Turkish stock without calibration,
and the neutral shelter modifiers are not Table 13-3.
### Notes
- These are **orders of magnitude, not counts**. The casualty rates
were fitted to United States earthquakes and the shelter factors to
United States Red Cross shelter data, and the damage distribution
arrives with the uncalibrated-to-Turkish-stock caveat the debris tool
already carries. The tool says so in its help, in the manual and in
the run log.
- Bridges and the population on the street away from any building are
left out rather than approximated: both need an inventory the plugin
does not model - a bridge layer carrying damage states, and a count of
people in the street. Hazus's bridge rows and its street-population
parameters are therefore not implemented.
- `displaced_hh` and `public_shelter` answer different questions. Most
displaced households go to family, friends or a hotel; only the
fraction α that stays in the area seeks a public shelter, which is why
the neutral default is an upper bound and not a forecast.
- **Three defects the runtime matrix caught, and the engine suite
could not.** The first two were reported as the same `FAIL (72/73)` on
both runtimes, and the first was hiding the second - so the count
staying the same between runs was not evidence that nothing had moved.
The third was hiding behind both, and behind a value check that could
not see it.
- **The tool failed on its own primary workflow.** Its first input
is the four damage-state columns the debris tool writes, and those
four columns are only the damaged states: the undamaged share of a
building is their complement. The engine's event tree indexes the
distribution by state, so reading those four as a distribution raised
`KeyError: 'none'` - the tool failing on the one workflow it exists
for, while 665 engine checks passed, because every one of them
supplied all five states by hand. `engine/impact.py` now has
`fill_undamaged`, which completes a four-state distribution with its
complement (clipped at zero, so a row summing above 1.0 gets no
undamaged share rather than a negative one, and the run log still
reports those rows), and the tool calls it. Five checks were added for
the new function, including one that the filled distribution
reproduces the hand-derived C1L counts. It is the second release in a
row where the matrix's chained run found what the unit suite could
not.
- **The chained run modelled a population of nobody and passed.**
With the harness fixed the case ran, and the run log read `Population
modelled: 0 people across 70 feature(s)` while the same run reported
17.7 displaced households - the tool's own "no silent zeros" rule,
broken, with the value check green. The cause is the seam between two
tools: `planx:seismicdebris` writes its buildings as **point
centroids**, because the debris envelopes need a point to hang off,
and a centroid has no area, so `QgsGeometry.area()` is 0.0 for every
building and the floor-area occupant estimate multiplies out to zero
everywhere. Casualties went to zero with it. Shelter did not, because
a residential building with no dwelling-unit column counts as one
dwelling and the displaced-household equation divides a default
dwelling count by an occupant count it never needed - which is how a
run with no people in it still produced a number, and how the loose
end was spotted.
- The fix is a footprint that survives the handover:
`planx:seismicdebris` now writes `footprint_area`, and the casualties
tool reads a footprint from a column when the geometry gives none,
auto-detected or named. It **raises** when no building has a footprint
at all rather than returning the zero-population answer.
- The value check needed a floor as well as a ceiling. Every bound
it asserted was an upper bound - casualties below occupants, shelter
below occupants - and zero occupants satisfies all of them, so the
defect was invisible to the check written to catch physical
impossibilities. It now also requires the run to put somebody in a
building. `planx:seismicimpact` is the only entry in
`VALUE_EXPECTATIONS` with a chained case behind it, so this was also
the only case in the matrix where a zero-population run was possible.
- The run log now names the source: `Footprint area: 0 building(s)
from their geometry, 70 from column 'footprint_area'`.
- **The matrix's own resolver could not reach the tool's other two
input paths.** With the first fixed, the next run failed in 0.10 s
before the tool started: `KeyError: 'buildings_occupancy'`. The
harness resolves a `fixture:` token against the file dictionary, which
holds only the GTFS zip; the occupancy fixture is a layer and lives in
the layer dictionary. Every `fixture:` token in the file until this
release named that one zip, so the branch had never had to look
elsewhere. The two coverage cases added here - a population field
against an occupancy field, and a single damage-state column - were
the first to name a layer, and had therefore never executed once.
`_resolve_special` now looks in both dictionaries and raises naming
what it does hold.
- The engine suite stands at 670 checks, including the Section 12 rate
tables, the collapse split, the Table 12-2 occupancy shares, the
Section 13 weights and the identities that α is exactly 1.0 at neutral
modifiers and that a non-residential building contributes no shelter
need.
- **Still open, found while fixing the above.** `planx:seismicdebris`
feeds `QgsGeometry.area()` - the layer's own area unit - straight into
`seismic.debris_extent`, which documents its outputs as cubic metres
and tonnes. On a metre grid the two agree; on a layer projected in
feet, or one still in degrees, `debris_vol_m3`, `debris_pile_m3` and
`debris_mass_t` are inflated by the square of the unit factor. It is
the same class as the `streetmorphology` defect below: a unit
assumption that holds for the demo city and is never checked. The
radius is unaffected, being driven by height alone. Not fixed here -
it changes reported values, and the same release should carry both.
The constraint is now stated on `debris_extent` itself, and on the new
`footprint_area` column, so the next reader meets it at the boundary
rather than in a bug report. **Fixed in 4.15.1**, which also names the
two error classes separately - the projection's own scale factor is
the one that bit here, on a metre CRS.
- **Still open, carried from 4.14.0.** `planx:streetmorphology`
divides raw network length by 1000 and hull area by 1e6 to report
`total_length_km` and `intersection_density_km2`, so on a layer
projected in feet those two figures are wrong by 3.28 and 10.8. It
remains a different tool's defect and belongs in a release that
changes reported values and says so. **Fixed in 4.15.1**, in the
release that changed reported values and said so.
## [4.14.0] - 2026-09-19
Ground Motion Scenario (`planx:groundmotion`) is new. One algorithm
added; none changed, none removed. The seismic group now holds a chain
rather than a single tool: ground motion in, debris out.
### Added
- **`planx:groundmotion` - Ground Motion Scenario.** Seismic Debris
could always be driven by a joined PGA field, but nothing in the
plugin produced one: the only ways in were a ShakeMap or an AFAD
raster from outside QGIS, or the debris tool's own magnitude guess,
which has no distance, site or fault term. This tool computes the
shaking itself, from Akkar, Sandikkaya & Bommer (2014) - one of the
four ground-motion models in the logic tree of Turkey's 2018 national
seismic hazard map - with the authors' own published coefficients.
- **`SCENARIO`** - A, a point source (an epicentre plus a focal
depth), or B, an extended rupture (a fault trace). An extended rupture
has no single hypocentre, so geometry B offers Joyner-Boore distance
only; a point source has no rupture surface, so its Joyner-Boore and
epicentral distances are the same number. The tool raises with the
reason rather than inventing the distance it cannot support.
- **`DISTANCE_METRIC`** - Joyner-Boore, epicentral or hypocentral. The
choice is written to every output row as `dist_metric` and the
distance itself as `r_km`, so a result joined back onto a map can
never be read as the wrong distance.
- **`FAULT_MECHANISM`** - strike-slip (the model's reference), normal,
reverse. **`EPSILON`** shifts every value by a chosen number of total
standard deviations, which is how a scenario is pinned to a rarer
level than the median.
- **`VS30`** and **`VS30_FIELD`** - site velocity as a constant or per
receiver. The default is 750 m/s, the model's own reference velocity
where its site term is exactly zero, so a default run reports
rock-reference motion and every bit of soil amplification in a result
is a choice the user made. The value actually used is written to a
`vs30` column; a receiver whose field is empty falls back to the
constant and is flagged in `caveat` instead of quietly becoming rock.
- **`SPECTRAL_PERIODS`** - any periods the model tabulates, in
seconds, added to the output as `sa_<T>_g` columns. It does not
interpolate: a period the paper does not publish is refused, with the
nearest tabulated periods named in the error. An interpolated value
would be one no source supports.
- Output is the receiver layer returned with its own geometry and
attributes plus `pga_g`, `pgv_cms`, `sa_<T>_g`, `r_km`, `dist_metric`,
`vs30`, `pga_rock_g` (the rock PGA the site term is conditioned on, so
the amplification applied to each receiver is auditable), `sigma_pga`
and `caveat`.
- **`caveat`** names the applicability limits each row breaks -
`mag_below`, `mag_above`, `dist_above`, `vs30_below`, `vs30_above`,
`depth_above`, `vs30_missing`, `vs30_capped` - and the run log repeats
them as warnings. The number is still computed and still written; the
point is that it is labelled rather than silently extrapolated, and
the paper's own magnitude, distance, depth and site ranges are the
ones the flags use.
### Changed
- Distances are converted from the receivers' own CRS units into
kilometres. Every term of the model is in kilometres - the distance,
the focal depth, the `a6` site-scale constant added to `R²` - and the
receiver coordinates arrive in whatever the layer's projection uses.
Assuming metres is right for UTM and wrong by 3.28 for a state-plane
CRS, and a factor of 1000 is wrong enough to read the demo city's
750-metre span as 472 km, which is what an early build of this tool
did: the matrix case reported receivers up to 472 km away from an
epicentre 500 m from most of them, and stayed green because a wrong
number is still a number. The scale is now read off the CRS, and the
engine's distance functions take it as a required argument with no
default, so the mistake cannot be made silently again.
- A fault trace is reprojected into the receivers' CRS before
distances are measured. It previously kept its own layer's
coordinates, so a trace in a different projection - or one in feet -
produced distances mixed from two unit systems.
- The Seismic Risk group's manual section no longer describes a single
tool, and the debris card's ground-motion paragraph now points at this
tool as the in-plugin route to a PGA field.
- Manual counts are now 314 numbered display equations, 388 reference
entries, 298 with DOIs; the version literal, the README and the
metadata are current. 72 algorithms, 19 groups.
- The pure engine suite gained 33 checks for the ground-motion engine
and stands at 627, including a committed fixture of pyGMM's own
expected outputs that pins the functional form, the site term and the
row indexing, and checks that state the unit contract in metre-scale
coordinates - synthetic coordinates small enough to pass as either
unit are how the 1000× error survived the suite. See
`THIRD_PARTY_NOTICES.md`, which is new and covers the ASB2014
coefficients, the pyGMM test values and the Hazus fragility curves.
- `tests/smoke_provider_catalog.py` counted reference entries carrying
a DOI by counting `doi.org` *occurrences*, so one listing with two
DOIs paid for one listing with none and the README's "with DOIs"
figure read one higher than the manual supports. It now counts
entries, and the README states 298.
### Notes
- The model has no raster output and no raster site sampling. QGIS's
own *Create grid* into the receivers parameter gives a scenario grid,
and it keeps each run's inputs visible in the model designer; a hidden
raster path would have been a second, less inspectable way to ask the
same question.
- `tests/qgis_runtime_algorithm_matrix.py` now checks values, not only
presence, for tools where a silent unit error is the plausible
failure. It previously asked one question of every output - was
anything produced? - which no unit bug can ever fail. The
ground-motion case asserts that the demo city's receiver distances are
consistent with a city under a kilometre across, and fails the case if
they are not.
- **Known follow-up, not fixed here.** `planx:streetmorphology`
divides raw network length by 1000 and hull area by 1e6 to report
`total_length_km` and `intersection_density_km2`, so on a layer
projected in feet those two figures are wrong by 3.28 and 10.8. It is
the same class of defect as the one this release fixed in the
ground-motion tool, but it is a different tool, and correcting it
changes reported values, so it belongs in a release that says so.
- These are median values from a shallow-crustal model for Europe and
the Middle East: not design levels, not a code check, and not a
substitute for the national hazard map. The model carries no basin
term and no directivity, so deep soft-soil amplification and
forward-directivity pulses are not represented.
## [4.13.0] - 2026-09-19
Seismic Collapse and Debris Spread (`planx:seismicdebris`) rebuilt on
the Hazus fragility curves. One algorithm changed; none added, none
removed. All five previous outputs keep their meaning, three new
parameters are additive and the defaults reproduce the old
single-realisation workflow.
### Fixed
- **The collapse probability saturated.** The model scaled a
construction-year baseline by `exp(0.8 (Mw - 7.0))` and clipped at
1.0, so the pre-1985 stock hit certainty at roughly Mw 7.2 and the
1986-2000 stock at Mw 7.8 - the band a Marmara scenario occupies. The
tool reported `collapse_prob = 1.0` for the entire oldest tier and
carried no information about it. Probabilities now rise smoothly and
separately with magnitude, from a lognormal fragility rather than an
exponential clip.
- **Debris came only from total collapse.** A building left standing
while losing its facade, infill walls and parapets put nothing in the
street, which under-counted the most common form of post-earthquake
obstruction. Debris is now generated from the sampled damage state
through a released-material fraction (0 for none and slight, 0.10
moderate, 0.35 extensive, 1.00 complete), so partial damage
contributes to blockage.
- **Source A accepted a non-polygonal layer and returned a zero-area
network.** Everything downstream - blockage, corridors, navigable core
- is areal, so the run looked successful and meant nothing. It now
raises with the message naming sources B and C.
### Added
- **`PGA_FIELD`** (optional, recommended) - a peak-ground-acceleration
column in g on the buildings layer. Supplying it drives fragility from
real site shaking and bypasses the magnitude path entirely, which is
the only route with distance and site effects; buildings with a null
or non-positive value fall back to the nominal intensity and the log
counts them.
- **`BUILDING_TYPE`** - 13 Hazus structural types (C1, C2, S1, S2, S3,
S4, W1, W2, RM1, RM2, PC1, PC2, MH). The height class is appended from
the floor-count field, so C1 with five storeys reads the `C1M` row.
Three Hazus types are deliberately absent: C3, S5 and URM have no
Moderate- or High-Code curve in the manual, and a row invented to fill
the gap would be worse than the absence. The help text says what that
costs.
- **`REFERENCE_PGA`** - the nominal site PGA at Mw 7.0 (default 0.35
g) used only when no PGA field is given. Exposed rather than buried,
because it is a user assumption, not a physical constant.
- **`VOID_RATIO`** (default 0.35) and **`DEBRIS_DENSITY`** (default
1.8 t/m3) - the pile's air fraction and the material density.
`debris_vol_m3` keeps its meaning as solid material volume; the two
new columns are `debris_pile_m3` (bulked, what competes with the
street for space) and `debris_mass_t` (tonnage to haul).
- **`MIN_CLEAR_WIDTH`** (default 3.5 m) and the **`OUT_NAVIGABLE`**
output - the corridor network morphologically opened at the clear
width, so a sliver of pavement beside a debris pile stops counting as
an evacuation route. Unblocked is not the same as passable, and the
corridors layer alone over-stated the evacuation map invisibly. The
log reports the street area lost to pinching.
- **`SIMULATIONS`** (default 0) - reruns the scenario under N seeds
and writes an empirical `collapse_freq` per building plus mean,
standard deviation and 5th/50th/95th percentiles of blocked street
area. This replaces the manual's advice to repeat the tool by hand
with one parameter. `collapse_freq` is NULL when the parameter is left
at 0.
### Changed
- Damage state and `collapse_prob` now come from the Hazus
equivalent-PGA structural fragilities (Technical Manual 6.1, Tables
5-37 to 5-40: High-, Moderate-, Low- and Pre-Code), with the manual's
uniform dispersion of 0.64 - the root-sum-of-squares of its capacity
(0.4) and demand (0.5) dispersions. The medians were transcribed from
the published manual, not recalled, and the embedded table was
verified row for row against a parse of the source PDF.
- The four construction-year tiers keep their breakpoints but are now
*Hazus design levels* rather than probabilities: 1985 and earlier
Pre-Code, 1986-2000 Low-Code, 2001-2018 Moderate-Code, newer
High-Code. This is an analogy to Turkish regulation years, not a
calibration, and the engine, the manual and the reference list all say
so.
- `collapse_prob` is still the last preferred-token numeric field, so
the default graduated renderer still colours the map by it.
`tests/smoke_plugin.py` now asserts that against the algorithm's own
field list rather than a copied one, because adding `debris_pile_m3`,
`debris_mass_t`, `collapse_freq` and `damage_state` moved that choice
and nothing else in the suite would have noticed.
- Manual card rewritten to the new chain: 12 display equations, the
design-level and released-material tables, the navigable-core
definition, and a validation table of the C1M damage distribution by
design level. Manual counts are now 305 numbered display equations,
386 reference entries, 298 with DOIs; the README, the version literal
and the changelog are current.
- The runtime matrix now covers the seismic tool through the
centerline network sources it can actually express. It previously
bound the demo city's line layer to source A, a polygon input, so
blockage, corridors and the navigable core all had zero area while the
case read green.
### Notes
- The engine suite stands at 596 checks.
`tests/qgis_runtime_algorithm_matrix.py` gained extra-run
verification: an extra run whose output is empty now fails the case
instead of counting as coverage.
- The Hazus curves are American: a western-United States reference
spectrum (Mw 7.0, Site Class D, distance at least 15 km) and design
levels tied to United States code eras. Turkish building stock, and
especially its unreinforced masonry and infilled frames, is not
represented by the available type list. Treat the output as
comparative screening and do not quote it as a Turkish loss estimate.
Calibrating these curves to Turkish damage observations is a separate
piece of work and is not done here.
- The same constants still live in `planx_urban_resilience`'s
`processing/seismic/monte_carlo_debris.py`. The two plugins are
separate repositories, so the engine change cannot be shared; that
copy will drift from this one unless it is ported in its own release.
## [4.12.0] - 2026-09-19
Parking demand and supply balance. Two new algorithms, both in the
Travel Demand group. Purely additive: no existing algorithm's
parameters, ids or default behaviour changed.
### Added
- **Parking Demand Estimator** (`planx:parkingdemand`) - estimates the
parking spaces each zone demands from its land-use category and size,
through an editable per-category rate table. Each entry is
`category=basis:rate`, where the basis (`unit`, `sqm`, `seat`) states
what one rate unit counts against, because parking standards are not
written on one common denominator. A zone whose category matches no
rate row demands zero and is listed separately in the log, so a gap in
the rate table is never reported as a zone that needs no parking. The
shipped rates are illustrative values written in the ITE convention,
not figures taken from it, and the help text says so.
- **Parking Supply-Demand Balance** (`planx:parkingsupplybalance`) -
compares the demand each zone carries against a counted parking
inventory within an access radius, and reports the surplus or deficit.
Reach follows the street network when one is supplied and is
straight-line otherwise, and the output records which was used. Every
zone is classified `counted`, `zero supply found` (a real deficit) or
`supply data absent` (a coverage gap in the inventory), and the
unsurveyed case gets a NULL balance so the column cannot be summed
into a false shortfall.
### Changed
- The manual documents both new tools to the same depth as the rest,
and its counts, version literal and search index are current: 71
algorithms, 19 tool groups, 300 numbered display equations, 386
reference entries.
- README and Studio dock counts updated to 71. The engine suite stands
at 570 checks.
### Notes
- Mixed-use and shared-parking reduction factors are deliberately not
implemented. The demand tool reports the sum of the single-use
requirements, which is the quantity any shared-parking argument has to
answer.
## [4.11.2] - 2026-09-19
Quality infrastructure. No algorithm behaviour changed and no
algorithm was added.
### Fixed
- The Help button was dead for all 69 algorithms. `DOC_BASE_URL` still
named the retired GitHub Pages host, so every Help link and both
Studio dock documentation actions opened a 404 while `metadata.txt`
advertised the live host. The URL is now defined once and imported by
the dock.
- `tests/test_engine.py` (528 checks, no QGIS imports) was filed under
`tests_qgis` with `tests_pure = []`, so no CI-reachable runner
executed it. It is now filed as a pure suite.
- The manual advertised a stale version and eight stale Processing IDs
(`planx:buildingformmetrics`, `planx:morphologicaltessellation`,
`planx:spacematrixdensity`, `planx:streetnetworkmorphology`,
`planx:multiamentiyaccess`, `planx:frontalareaindex`,
`planx:noisescreening`, `planx:airscreening`), so documented IDs did
not match the registry.
- The README claimed counts nothing checked: it said 285 display
equations against the manual's 289, and an undefined "~600 citations".
Both are now counted and true.
### Added
- `tests/smoke_provider_catalog.py`: 15 pure catalog gates tying the
algorithm registry to the manual (anchors one-to-one in both
directions, the 19 groups as an exact set, stable ids, one icon per
algorithm, version literals, help host versus `metadata.txt`
homepage).
- `tests/qgis_runtime_algorithm_matrix.py`: builds a fixture city,
auto-wires each algorithm's declared parameters, executes all 69 on
QGIS 3.44 LTR and 4.2, and verifies that every declared output is
actually populated. Reports the wiring chosen and the algorithm's own
log per case.
## [4.11.1] - 2026-09-17
- Upgraded official plugin icon to high-end tactile 3D brand identity
(isometric 45°, slim teal pedestal, full bleed transparent canvas).
- Synchronized documentation, repository and issue tracker endpoints
to GeoPhilo and GitLab.
- Fixed field preservation and fid handling in network preparation and
service areas algorithms.
yes
geo140195philo
2026-09-20T14:41:01.927229+00:00
3.28.0
4.99.0
None
no
Plugin Tags