diff --git a/docs/CITYJSON_INTEROP.md b/docs/CITYJSON_INTEROP.md index 107836e..03c4697 100644 --- a/docs/CITYJSON_INTEROP.md +++ b/docs/CITYJSON_INTEROP.md @@ -202,9 +202,10 @@ LIMIT 10; make test_full ``` -Four test files are gated on `require cityjson` +Five test files are gated on `require cityjson` (`cityjson_interop`, `cityjson_hollow_solid`, `cityjson_multisolid`, -`cityjson_delft_remote`), and one on `require spatial` (`spatial_coexist`). +`cityjson_3dbag_attributes`, `cityjson_delft_remote`), and one on +`require spatial` (`spatial_coexist`). Under `make test` / `make test_debug` they skip; `make test_full` does the setup that makes them execute, and treats a skip as a failure. It needs network access — the httpfs/spatial download on the first run, and the remote Delft fixture on diff --git a/docs/TESTING.md b/docs/TESTING.md index cc93117..1b75b44 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -523,7 +523,11 @@ FROM good WHERE n_parts = 1 AND b3_volume_lod22 > 0 AND b3_opp_grond > 0; ``` The automated version of this check is -[`test/sql/cityjson_delft_remote.test`](../test/sql/cityjson_delft_remote.test). +[`test/sql/cityjson_delft_remote.test`](../test/sql/cityjson_delft_remote.test), which extends +it to volume at LoD1.2 and LoD1.3, surface area against the five `b3_opp_*` attributes summed, +`ST_3DZMin` against `b3_h_maaiveld` and the LoD1.2 extrusion height against `b3_h_dak_70p`. +[`test/sql/cityjson_3dbag_attributes.test`](../test/sql/cityjson_3dbag_attributes.test) runs +the same comparisons offline, over the frozen nine-building slice. ## 14 — Transforms: rigid motions and the cube law @@ -1095,7 +1099,7 @@ The behaviours it demonstrates are covered automatically as follows: | §2 | `test/sql/cityjson_hollow_solid.test`, `test/sql/st_3d_hollow_solid.test`, `test/cpp/test_inner_shell.cpp` | | §3 | `test/sql/cityjson_multisolid.test`, `test/sql/st_3d_multisolid.test` | | §5, §6, §17 | `test/sql/geom_3d_accessors.test`, `geom_3d_measurements.test`, `geom_3d_serialize.test` | -| §8–§13 | `test/sql/cityjson_delft_remote.test` (the 3DBAG oracle), `test/sql/st_3d_validation.test`, `test/sql/geom_3d_distance.test` | +| §8–§13 | `test/sql/cityjson_delft_remote.test` and `test/sql/cityjson_3dbag_attributes.test` (the 3DBAG oracle, remote and offline), `test/sql/st_3d_validation.test`, `test/sql/geom_3d_distance.test` | | §14 | `test/sql/metamorphic_transforms.test` — **extended by this walkthrough** with the RotateX/RotateY cases from the quirks section | | §14 (kernel) | `test/cpp/test_measurements.cpp` — **added by this walkthrough**: far-from-origin precision | | §15 | `test/sql/st_transform.test`, `test/cpp/test_crs_transform.cpp` | diff --git a/docs/TEST_COVERAGE.md b/docs/TEST_COVERAGE.md index dd0ae1f..7f01787 100644 --- a/docs/TEST_COVERAGE.md +++ b/docs/TEST_COVERAGE.md @@ -12,7 +12,7 @@ Suite layout and the TDD workflow are in [AGENTS.md](../AGENTS.md); this file re | `test/cpp/` | `make test_cpp`, `make test_all`, `make test_full` | Kernel logic with no database: WKB parsing, model construction, payload round-trips, validation, triangulation, area/volume math | | `test/sql/` | `make test`, `make test_debug`, `make test_full` (release, gated tests included) | SQL surface: binding, null propagation, `TRY` semantics, result contracts, interop | -Four tests gated on `require cityjson` skip unless a **locally built** `duckdb-cityjson` is +Five tests gated on `require cityjson` skip unless a **locally built** `duckdb-cityjson` is staged for the sqllogic runner — the community-published extension emits an older column shape these tests no longer target. `make test_full` stages it (and fails the run on any skip); see [CITYJSON_INTEROP.md](./CITYJSON_INTEROP.md#running-the-gated-tests-under-sqllogic) @@ -25,13 +25,40 @@ Three independent sources, each authoritative for a different class of claim. ### 3DBAG published attributes — real-geometry measurement -`test/sql/cityjson_delft_remote.test` streams the 3DBAG Delft tile and compares -`ST_3DVolume` against `b3_volume_lod22` and `ST_3DFootprintArea` against `b3_opp_grond`. -This is the oracle of record for measurement on **real reconstructed geometry**, because -SFCGAL rejects most real roofs for non-planarity while this extension measures them by -triangulation. +3DBAG publishes, per building, what its own reconstruction pipeline measured. Those figures +are an independent toolchain's answer for the very geometry it shipped, which makes them the +oracle of record for measurement on **real reconstructed geometry** — the case SFCGAL cannot +serve, because it rejects most real roofs for non-planarity while this extension measures +them by triangulation. -Requires network access and the `cityjson` extension. +| Attribute | Oracles | +| --- | --- | +| `b3_volume_lod12`, `b3_volume_lod13`, `b3_volume_lod22` | `ST_3DVolume`, at each LoD | +| `b3_opp_grond` + `b3_opp_dak_plat` + `b3_opp_dak_schuin` + `b3_opp_buitenmuur` + `b3_opp_scheidingsmuur` | `ST_3DSurfaceArea` / `ST_3DArea` at LoD2.2 — every face of that solid carries exactly one of the five semantic roles, so they sum to its whole boundary | +| `b3_opp_grond` | `ST_3DFootprintArea` — the ground surface is horizontal, so its 3D area and its XY projection are one number | +| `b3_h_maaiveld` | `ST_3DZMin`, at every LoD | +| `b3_h_dak_70p` | `ST_3DZMax` at LoD1.2 — the height the block was extruded to | +| `b3_val3dity_lod22` | `ST_3DValidationReport` — one-way: what val3dity passes, this extension must pass | + +Two tests run those comparisons. + +- **`test/sql/cityjson_3dbag_attributes.test`** — offline, over the frozen nine-building slice + in `test/data/3dbag.city.jsonl`, at the tolerances that slice actually meets (0.1 % on + volume, 1 % on surface area, 2 mm on ZMin) plus string-exact per-building pins. Needs the + `cityjson` extension but **no network**, so it is the one that runs anywhere. +- **`test/sql/cityjson_delft_remote.test`** — the same comparisons at tile scale, streamed + from the remote Delft tile. At that scale the thresholds are fractions ("≥95 % of buildings + within 2 %") rather than bounds, because a tile contains reconstructions that are simply + broken. Requires network access. + +The val3dity cross-check is a fraction, not a bound, and deliberately: the two disagree on a +handful of tile solids that carry a single collapsed face. val3dity tolerates it; this +extension reports the duplicated edge it creates as non-manifold and refuses to measure the +solid, which is the documented no-silent-repair behaviour rather than a defect. + +Numeric pins in both files are formatted with `printf` and compared as text. sqllogic compares +numeric results with a **1 % relative tolerance**, so a bare `ROUND(...)` pin admits drift an +order of magnitude larger than the decimals it displays. ### PostGIS + SFCGAL — analytic measurement math @@ -111,6 +138,7 @@ over the whole frozen 3DBAG corpus. | `ST_AsWKBMultiCube(separation)` | Volume conditioning when a collection's parts are spatially separated — the per-shell reference point of DESIGN_DOC §8.2. Total volume stays 16 at separations of 1e4–1e10, and the 1e6 case is oracled against SFCGAL as `fixture:multi_cube_far` | | `test/data/multisolid.city.json`, `compositesolid.city.json` | `MultiSolid` / `CompositeSolid` import per solid, with shell grouping recovered from the sidecar (`test/sql/cityjson_multisolid.test`). Import does **not** raise on either class | | `test/data/unit_cube.city.json` | End-to-end `cityjson` → `three_d` smoke test | +| `test/data/3dbag.city.jsonl` | Nine real 3DBAG buildings at LoD1.2 / 1.3 / 2.2. The WKB the PostGIS oracle freezes as its `NL.IMBAG.Pand.*` rows comes from here, and `test/sql/cityjson_3dbag_attributes.test` measures the same nine against 3DBAG's published attributes offline | ## Open work diff --git a/test/sql/cityjson_3dbag_attributes.test b/test/sql/cityjson_3dbag_attributes.test new file mode 100644 index 0000000..343dfd4 --- /dev/null +++ b/test/sql/cityjson_3dbag_attributes.test @@ -0,0 +1,196 @@ +# name: test/sql/cityjson_3dbag_attributes.test +# description: three_d measurements cross-checked against 3DBAG's own published attributes, on the frozen local tile slice +# group: [sql] + +# ───────────────────────────────────────────────────────────────────────────── +# 3DBAG publishes, per building, the figures its own reconstruction pipeline +# measured: volume per LoD, the areas of ground / roof / wall surfaces, and the +# ground elevation. Those are an oracle computed by an independent toolchain on +# the same geometry, so they check `ST_3D*` measurement end to end. +# +# `test/data/3dbag.city.jsonl` is a frozen nine-building slice — the same nine +# solids the PostGIS oracle freezes as its `NL.IMBAG.Pand.*` rows +# (test/data/postgis_oracle/README.md). SFCGAL rejects eight of the nine for +# non-planarity and measures neither their area nor their volume, so for those +# this is the only oracle there is. It is also offline: unlike +# cityjson_delft_remote.test it needs no network, so the tolerances are pinned +# to what this frozen slice actually produces rather than left loose for drift. +# +# Which attribute checks which function: +# +# b3_volume_lod12/13/22 ST_3DVolume, per LoD +# b3_opp_grond + dak_plat + dak_schuin +# + buitenmuur + scheidingsmuur ST_3DSurfaceArea at LoD2.2 +# b3_opp_grond ST_3DFootprintArea +# b3_h_maaiveld ST_3DZMin +# b3_h_dak_70p ST_3DZMax at LoD1.2 +# b3_val3dity_lod22 ST_3DValidationReport +# +# The area identity holds at LoD2.2 only: the five `b3_opp_*` attributes +# describe the LoD2.2 model, and every face of that solid carries exactly one of +# those semantic roles, so they sum to its whole boundary. The LoD1.2/1.3 blocks +# extrude to a single roof height and their walls are a different surface. +# +# Requires the `cityjson` extension — a build of the sibling duckdb-cityjson +# checkout, which emits the per-LoD geometry_lod columns read below. See +# docs/CITYJSON_INTEROP.md. +# ───────────────────────────────────────────────────────────────────────────── + +require cityjson + +require three_d + +# One read yields every LoD present. Geometry lives on the `BuildingPart` +# features and the attributes on the parent `Building`, so each part is joined +# back to its parent. +statement ok +CREATE TABLE feats AS SELECT * FROM read_cityjsonseq('test/data/3dbag.city.jsonl'); + +statement ok +CREATE TABLE parts AS +SELECT p.id, + ST_3DFromWKB(p.geometry_lod1_2, p.geometry_properties_lod1_2) AS s12, + ST_3DFromWKB(p.geometry_lod1_3, p.geometry_properties_lod1_3) AS s13, + ST_3DFromWKB(p.geometry_lod2_2, p.geometry_properties_lod2_2) AS s22, + b.b3_volume_lod12, b.b3_volume_lod13, b.b3_volume_lod22, + b.b3_opp_grond, + b.b3_opp_grond + b.b3_opp_dak_plat + b.b3_opp_dak_schuin + + b.b3_opp_buitenmuur + b.b3_opp_scheidingsmuur AS b3_opp_totaal, + b.b3_h_maaiveld, b.b3_h_dak_70p, + CAST(b.b3_val3dity_lod22 AS VARCHAR) AS b3_val3dity_lod22, + len(b.children) AS n_parts +FROM feats p +JOIN feats b ON b.id = p.parents[1] +WHERE p.geometry_lod2_2 IS NOT NULL; + +# The slice is nine single-part buildings, each carrying all three LoDs, and +# every geometry imports. Attributes describe the whole Building, so a +# multi-part building would need its parts summed before any comparison below +# held — this slice has none, and the assertion keeps it that way. +query IIIII +SELECT count(*), count(s12), count(s13), count(s22), max(n_parts) FROM parts; +---- +9 9 9 9 1 + +# ───────────────────────────────────────────────────────────────────────────── +# Volume, per LoD. Measured agreement on this slice is within 0.035 % at every +# LoD; the 0.1 % bound leaves room for the vertex quantisation 3DBAG applies on +# export (scale 0.001) without admitting a real regression. +# +# Each assertion lists the buildings that fail, so a failure names its rows. +# ───────────────────────────────────────────────────────────────────────────── +query I +SELECT id FROM parts +WHERE abs(ST_3DVolume(s12) - b3_volume_lod12) > 0.001 * b3_volume_lod12 +ORDER BY id; +---- + +query I +SELECT id FROM parts +WHERE abs(ST_3DVolume(s13) - b3_volume_lod13) > 0.001 * b3_volume_lod13 +ORDER BY id; +---- + +query I +SELECT id FROM parts +WHERE abs(ST_3DVolume(s22) - b3_volume_lod22) > 0.001 * b3_volume_lod22 +ORDER BY id; +---- + +# ───────────────────────────────────────────────────────────────────────────── +# Surface area at LoD2.2 against the five semantic-surface areas summed. This is +# the only oracle `ST_3DSurfaceArea` has on real reconstructed geometry. +# Measured worst case on this slice is 0.55 %, on buildings where 3DBAG's +# published roof area disagrees with the roof it shipped; 1 % is the bound. +# ───────────────────────────────────────────────────────────────────────────── +query I +SELECT id FROM parts +WHERE abs(ST_3DSurfaceArea(s22) - b3_opp_totaal) > 0.01 * b3_opp_totaal +ORDER BY id; +---- + +# Footprint area against the published ground area. The ground surface is +# horizontal, so its 3D area and its XY projection are the same number, and +# agreement is near-exact — within 0.005 m² measured, 0.02 m² asserted. +query I +SELECT id FROM parts +WHERE abs(ST_3DFootprintArea(s22) - b3_opp_grond) > 0.02 +ORDER BY id; +---- + +# The footprint does not move between LoDs: the same ground surface is carried +# at every level, remeshed but not reshaped. +query I +SELECT id FROM parts +WHERE abs(ST_3DFootprintArea(s22) - ST_3DFootprintArea(s12)) > 0.01 + OR abs(ST_3DFootprintArea(s22) - ST_3DFootprintArea(s13)) > 0.01 +ORDER BY id; +---- + +# ───────────────────────────────────────────────────────────────────────────── +# Heights. ZMin is the ground elevation 3DBAG publishes, to within the 1 mm +# quantisation step of the file's transform. ZMax at LoD1.2 is the 70th +# percentile roof elevation the block was extruded to — reconstruction snaps +# that height, so it is checked at 5 cm, not at the quantisation step. +# ───────────────────────────────────────────────────────────────────────────── +query I +SELECT id FROM parts +WHERE abs(ST_3DZMin(s22) - b3_h_maaiveld) > 0.002 + OR abs(ST_3DZMin(s12) - b3_h_maaiveld) > 0.002 + OR abs(ST_3DZMin(s13) - b3_h_maaiveld) > 0.002 +ORDER BY id; +---- + +query I +SELECT id FROM parts +WHERE abs(ST_3DZMax(s12) - b3_h_dak_70p) > 0.05 +ORDER BY id; +---- + +# ───────────────────────────────────────────────────────────────────────────── +# Validity. `b3_val3dity_lod22` is val3dity's error list for the LoD2.2 solid, +# and `[]` means it found none. val3dity checks strictly more than this +# extension does, so the implication runs one way only: what val3dity passes, +# ST_3DValidationReport must also pass. All nine are clean under both. +# ───────────────────────────────────────────────────────────────────────────── +query I +SELECT id FROM parts +WHERE b3_val3dity_lod22 = '[]' AND NOT ST_3DValidationReport(s22).is_valid +ORDER BY id; +---- + +query I +SELECT count(*) FROM parts WHERE b3_val3dity_lod22 = '[]'; +---- +9 + +# ───────────────────────────────────────────────────────────────────────────── +# Per-building pins. Face counts are structural — they come from the file, not +# from a measurement. The measured figures are formatted to a fixed number of +# decimals and compared as text: sqllogic compares numeric results with a 1 % +# relative tolerance, which would wave through drift far larger than anything +# the assertions above allow, so every pinned number here is a string. +# ───────────────────────────────────────────────────────────────────────────── +query TIITTTTTT +SELECT id, + ST_3DNumFaces(s12), ST_3DNumFaces(s22), + printf('%.2f', ST_3DVolume(s22)), printf('%.2f', b3_volume_lod22), + printf('%.2f', ST_3DSurfaceArea(s22)), printf('%.2f', ST_3DFootprintArea(s22)), + printf('%.3f', ST_3DZMin(s22)), printf('%.3f', ST_3DZMax(s22)) +FROM parts ORDER BY id; +---- +NL.IMBAG.Pand.0703100000027940-0 10 10 1399.54 1399.74 876.41 196.79 2.560 10.400 +NL.IMBAG.Pand.0703100000028278-0 15 40 3474.99 3475.19 1805.90 581.67 3.722 11.074 +NL.IMBAG.Pand.0703100000028817-0 6 6 1545.20 1545.00 1162.05 336.02 2.547 7.439 +NL.IMBAG.Pand.0703100000030681-0 6 6 550.97 551.01 470.15 88.74 -1.666 4.622 +NL.IMBAG.Pand.0703100000031557-0 6 6 46.82 46.82 79.10 15.21 2.835 5.921 +NL.IMBAG.Pand.0703100000031696-0 6 6 60.34 60.36 94.10 15.46 2.412 6.349 +NL.IMBAG.Pand.0703100000035296-0 8 8 37.25 37.26 70.32 14.86 4.329 6.861 +NL.IMBAG.Pand.0703100000035935-0 6 6 375.06 375.19 420.82 143.92 3.296 5.902 +NL.IMBAG.Pand.0703100000193621-0 8 12 1483.16 1483.55 1049.01 251.22 3.279 15.133 + +statement ok +DROP TABLE parts; + +statement ok +DROP TABLE feats; diff --git a/test/sql/cityjson_delft_remote.test b/test/sql/cityjson_delft_remote.test index 8734bf5..bf040f5 100644 --- a/test/sql/cityjson_delft_remote.test +++ b/test/sql/cityjson_delft_remote.test @@ -5,12 +5,25 @@ # ───────────────────────────────────────────────────────────────────────────── # This test streams real 3D building models — the 3DBAG "Delft" tile, CityJSON # 2.0 / EPSG:7415 — from a remote server and checks three_d's measurements -# against 3DBAG's own published attributes (`b3_volume_lod22`, `b3_opp_grond`). +# against the figures 3DBAG's own reconstruction pipeline published for them: +# +# b3_volume_lod12/13/22 ST_3DVolume, per LoD +# b3_opp_grond + dak_plat + dak_schuin +# + buitenmuur + scheidingsmuur ST_3DSurfaceArea at LoD2.2 +# b3_opp_grond ST_3DFootprintArea +# b3_h_maaiveld ST_3DZMin +# b3_h_dak_70p ST_3DZMax at LoD1.2 +# b3_val3dity_lod22 ST_3DValidationReport +# +# `test/sql/cityjson_3dbag_attributes.test` runs the same comparisons offline, +# against a frozen nine-building slice and at far tighter tolerances. This file +# is the one that runs them at tile scale, where the thresholds have to be +# fractions rather than bounds. # # It needs BOTH: # 1. the `cityjson` extension staged for the sqllogic runner — a build of the # sibling duckdb-cityjson checkout, which emits the per-LoD -# `geometry_lod2_2` / `geometry_properties_lod2_2` columns aliased below +# `geometry_lod` / `geometry_properties_lod` columns read below # (the published community build's flat `geometry` will not bind), and # 2. network access to # https://cityjson.open3d.city/cityjsonseq/delft.city.jsonl @@ -36,23 +49,54 @@ statement ok CREATE TABLE feats AS SELECT id, parents, children, geometry_lod2_2 AS geometry, geometry_properties_lod2_2 AS geometry_properties, - b3_volume_lod22, b3_opp_grond -FROM read_cityjsonseq( - 'https://cityjson.open3d.city/cityjsonseq/delft.city.jsonl', - lod => '2.2') + geometry_lod1_2, geometry_properties_lod1_2, + geometry_lod1_3, geometry_properties_lod1_3, + b3_volume_lod12, b3_volume_lod13, b3_volume_lod22, + b3_opp_grond, + b3_opp_grond + b3_opp_dak_plat + b3_opp_dak_schuin + + b3_opp_buitenmuur + b3_opp_scheidingsmuur AS b3_opp_totaal, + b3_h_maaiveld, b3_h_dak_70p, b3_val3dity_lod22 +FROM read_cityjsonseq('https://cityjson.open3d.city/cityjsonseq/delft.city.jsonl') LIMIT 400; statement ok CREATE TABLE parts AS SELECT p.id, ST_3DTryFromWKB(p.geometry, p.geometry_properties) AS solid, + ST_3DTryFromWKB(p.geometry_lod1_2, p.geometry_properties_lod1_2) AS solid12, + ST_3DTryFromWKB(p.geometry_lod1_3, p.geometry_properties_lod1_3) AS solid13, + b.b3_volume_lod12, + b.b3_volume_lod13, b.b3_volume_lod22, b.b3_opp_grond, + b.b3_opp_totaal, + b.b3_h_maaiveld, + b.b3_h_dak_70p, + b.b3_val3dity_lod22, len(b.children) AS n_parts FROM feats p JOIN feats b ON b.id = p.parents[1] WHERE p.geometry IS NOT NULL; +# The measurement assertions below run over the LoD1.2, LoD1.3 and LoD2.2 solids +# of the same building, so restrict to the parts that carry, and import, all +# three. ST_3DVolume and ST_3DSurfaceArea raise rather than repair, so the +# volume and area comparisons are further restricted to the solids this +# extension reports as valid. +statement ok +CREATE TABLE triples AS +SELECT *, ST_3DValidationReport(solid).is_valid AS valid22 +FROM parts +WHERE n_parts = 1 + AND solid IS NOT NULL AND solid12 IS NOT NULL AND solid13 IS NOT NULL; + +statement ok +CREATE TABLE valid_triples AS +SELECT * FROM triples +WHERE valid22 + AND ST_3DValidationReport(solid12).is_valid + AND ST_3DValidationReport(solid13).is_valid; + # Sanity: we actually pulled a non-trivial number of buildings. query I SELECT count(*) > 100 FROM parts; @@ -108,17 +152,123 @@ SELECT bool_and((ST_3DZMax(solid) - ST_3DZMin(solid)) > 0) FROM parts; ---- true +# Every part carries all three LoDs, and all three import. +query I +SELECT count(*) > 100 FROM triples; +---- +true + +# GROUND TRUTH — volume at LoD1.2 and LoD1.3. The block models are a separate +# reconstruction from the LoD2.2 roof, so they exercise ST_3DVolume on geometry +# the LoD2.2 assertion above never sees. Measured agreement is within 0.08 %. +query I +SELECT (count(*) FILTER ( + WHERE abs(ST_3DVolume(solid12) - b3_volume_lod12) + <= 0.02 * b3_volume_lod12 + 0.1)) + >= 0.95 * count(*) +FROM valid_triples +WHERE b3_volume_lod12 > 0; +---- +true + +query I +SELECT (count(*) FILTER ( + WHERE abs(ST_3DVolume(solid13) - b3_volume_lod13) + <= 0.02 * b3_volume_lod13 + 0.1)) + >= 0.95 * count(*) +FROM valid_triples +WHERE b3_volume_lod13 > 0; +---- +true + +# GROUND TRUTH — surface area. Every face of the LoD2.2 solid carries exactly one +# semantic role, so 3DBAG's five published surface areas — ground, flat roof, +# sloped roof, exterior wall, party wall — sum to its whole boundary. This is +# the only check ST_3DSurfaceArea has against an independent measurement of real +# reconstructed geometry: SFCGAL rejects these roofs for non-planarity. Measured +# median disagreement is 0.007 %, with ~99 % of buildings inside 2 %. +query I +SELECT (count(*) FILTER ( + WHERE abs(ST_3DSurfaceArea(solid) - b3_opp_totaal) + <= 0.02 * b3_opp_totaal + 0.1)) + >= 0.95 * count(*) +FROM valid_triples +WHERE b3_opp_totaal > 0; +---- +true + +# GROUND TRUTH — ground elevation. b3_h_maaiveld is the height 3DBAG places the +# ground surface at, and every LoD is built down to it, so ST_3DZMin must return +# it — to within the 1 mm quantisation step of the tile's transform. On the +# extruded blocks that holds for every building, not a fraction of them. +query I +SELECT bool_and(abs(ST_3DZMin(solid12) - b3_h_maaiveld) <= 0.01 + AND abs(ST_3DZMin(solid13) - b3_h_maaiveld) <= 0.01) +FROM triples; +---- +true + +# At LoD2.2 it holds over the solids that reconstruct validly. It is stated over +# those rather than over all of them because a broken LoD2.2 roof can drag a +# vertex metres below the ground surface — the tile carries one such building, +# and this extension reports it as invalid rather than measuring it. +query I +SELECT bool_and(abs(ST_3DZMin(solid) - b3_h_maaiveld) <= 0.01) +FROM valid_triples; +---- +true + +# GROUND TRUTH — extrusion height. The LoD1.2 block is extruded to the 70th +# percentile roof elevation, so ST_3DZMax on it recovers b3_h_dak_70p. +# Reconstruction snaps that height, so this is a decimetre-scale agreement over +# most buildings rather than an exact one — measured 95th percentile 9 cm. +query I +SELECT (count(*) FILTER (WHERE abs(ST_3DZMax(solid12) - b3_h_dak_70p) <= 0.15)) + >= 0.90 * count(*) +FROM triples; +---- +true + +# The footprint does not move between LoDs: the same ground surface is carried +# at every level, remeshed but not reshaped. +query I +SELECT (count(*) FILTER ( + WHERE abs(ST_3DFootprintArea(solid) - ST_3DFootprintArea(solid12)) + <= 0.01 * ST_3DFootprintArea(solid12))) + = count(*) +FROM triples +WHERE ST_3DFootprintArea(solid12) > 0; +---- +true + +# CROSS-CHECK — validity. b3_val3dity_lod22 is val3dity's error list for the +# LoD2.2 solid; `[]` means it found none. val3dity checks strictly more than this +# extension does, so the implication runs one way: what val3dity passes, +# ST_3DValidationReport should pass too. It is a fraction rather than a bound +# because the two disagree on a handful of solids that carry a single collapsed +# face — val3dity tolerates it, this extension reports the duplicated edge it +# creates as non-manifold and refuses to measure the solid. Measured 98.5 %. +query I +SELECT (count(*) FILTER (WHERE valid22)) >= 0.95 * count(*) +FROM triples +WHERE b3_val3dity_lod22 = '[]'; +---- +true + # ───────────────────────────────────────────────────────────────────────────── # Deterministic per-building pins (selected by id, independent of row order). # NL.IMBAG.Pand.0503100000012869-0 is a small flat-roof house whose 3DBAG # LoD2.2 solid has 6 faces; published b3_volume_lod22 ≈ 19.52, b3_opp_grond ≈ 7.21. # ───────────────────────────────────────────────────────────────────────────── -query IIIII +# The measured figures are formatted and compared as text: sqllogic compares +# numeric results with a 1 % relative tolerance, so a bare ROUND(...) pin would +# wave through drift an order of magnitude larger than the rounding it shows. +query IITTT SELECT ST_3DNumFaces(solid), ST_3DIsClosed(solid), - ROUND(ST_3DVolume(solid), 1), - ROUND(ST_3DFootprintArea(solid), 1), - ROUND(ST_3DZMax(solid) - ST_3DZMin(solid), 2) + printf('%.1f', ST_3DVolume(solid)), + printf('%.1f', ST_3DFootprintArea(solid)), + printf('%.2f', ST_3DZMax(solid) - ST_3DZMin(solid)) FROM parts WHERE id = 'NL.IMBAG.Pand.0503100000012869-0'; ---- @@ -134,6 +284,12 @@ SELECT ROUND(ST_3DDistance( ---- true +statement ok +DROP TABLE valid_triples; + +statement ok +DROP TABLE triples; + statement ok DROP TABLE parts;