From b0d3917d64b172ea676e04d8e34f6e46486a2472 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 20:38:34 +0200 Subject: [PATCH 01/28] =?UTF-8?q?Research:=20the=20S=C3=A3o=20Francisco=20?= =?UTF-8?q?basin=20as=20rasputin's=20target=20(DEMs,=20inputs,=20size,=20C?= =?UTF-8?q?RS,=20roadmap)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/research/sao-francisco-basin.md | 139 +++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 docs/research/sao-francisco-basin.md diff --git a/docs/research/sao-francisco-basin.md b/docs/research/sao-francisco-basin.md new file mode 100644 index 00000000..f3cab468 --- /dev/null +++ b/docs/research/sao-francisco-basin.md @@ -0,0 +1,139 @@ +# The São Francisco basin as rasputin's target + +Status: research note, 2026-09-27, main session. Ola's aim (2026-09-27): +"sub second is very good for smaller catchments, and of course faster is +better. But the important thing is huge catchments. I want to construct the +San Fransisco Basin in Brazil eventually, which is about the size of Norway. +And I think we can get 50m rasters for the DEM." + +Facts below are from the sources listed at the end, checked by web search on +2026-09-27. Figures marked *computed* were computed here with pyproj; figures +marked *inferred* are extrapolations, not measurements. + +## The basin + +- **Area: 636,920 km²** (OAS). Norway is about 385,000 km² with Svalbard and + Jan Mayen and about 324,000 km² without (from memory, not checked here), so + the basin is roughly 1.65 to 2 times Norway. +- It drains parts of Minas Gerais, Goiás, Bahia, Pernambuco, Alagoas, Sergipe + and the Federal District. +- Roughly latitude 7°S to 21°S and longitude 36°W to 48°W (approximate + bounding box, used for the computations below). *Computed:* about + 1,280 km × 1,620 km in a projected CRS. + +## Elevation data + +| DEM | spacing | what it is | licence | +|---|---|---|---| +| **ANADEM** (ANA / UFRGS, 2024) | 30 m (1″) | Copernicus GLO-30 with vegetation bias removed for South America (Landsat-8, Sentinel-2, GEDI lidar). Mean bias 9.6 m (COPDEM) → 1.5 m; in forest 14.3 m → 0.4 m | free and open source per its authors; exact licence to be read from the repository | +| Copernicus GLO-30 | 30 m (1″) | a surface model (tree canopy and buildings included) | free (Copernicus licence) | +| Copernicus GLO-90 | 90 m (3″) | the same at 3″ | free | +| FABDEM | 30 m | Copernicus with forests and buildings removed (Bristol / Fathom) | CC BY-NC-SA 4.0: **non-commercial only** | +| IBGE MDE | 1:25,000 and 1:50,000 scale | national mapping agency, aerial restitution | coverage is partial | + +- **No Brazil-wide 50 m DEM turned up.** A 50 m grid would most likely be + ANADEM or Copernicus resampled from 30 m, or IBGE's 1:50,000 material where + it exists. Where Ola's 50 m source is from is a question for Ola. +- **For hydrology, ANADEM is the obvious first choice.** It is a terrain model + rather than a surface model, it was built for South America by the national + water agency's partners, and it is free. It comes in MGRS tiles (GitHub) and + on Google Earth Engine. +- All of these are in **geographic coordinates** (EPSG:4326, arc-seconds), not + a projected grid. *Computed* (pyproj, WGS84): at 15°S, 1″ is 30.7 m + north–south and 29.9 m east–west, so the cells are not square in metres. + +## Other inputs for the basin + +- **Basin outline:** ANA's *Base Hidrográfica Ottocodificada* (BHO) has + Otto-Pfafstetter basins at several levels as GeoPackage, including the São + Francisco. That gives the confining polygon directly, without auto-catchment. + BHO's drainage lines (`GEOFT_BHO_TRECHO_DRENAGEM`) are candidate river + polylines for 16b. +- **Land cover:** CORINE does not cover Brazil. **MapBiomas** (annual, 30 m, + 1985 onward, CC BY 4.0) is the Brazilian equivalent. It is a **raster**, not + vector polygons like CORINE's GeoPackage, so 16b would need a + raster-to-polygon step, or a separate path, for it. + +## Size, against rasputin today + +| grid | nodes inside the basin | float32 DEM | +|---|---:|---:| +| 30 m | 708 M | 2.8 GB | +| 50 m | 255 M | 1.0 GB | +| 90 m | 79 M | 0.3 GB | + +*Computed* from the area. The bounding box holds more (about 830 M nodes at +50 m). Today's 1 m benchmark tile is 25.5 M nodes, so the basin at 50 m is +about **10×** the tile in nodes and at 30 m about **28×**. + +*Inferred, not measured:* the scan visits every domain node about 5.6 times +over a refine (serial profile) at roughly 9 ns per visit on one thread. At +50 m that is on the order of 10-15 s of scan on one thread and a few seconds on +8. The triangle count, and so the serial phase and output size, depends on +relief and on the tolerance, not on the node count. It cannot be extrapolated +from the tile, which is a coastal fjord tile at 10 m with 1 m tolerance. +Measuring it on a real piece of the basin is the first thing to do. + +The DEM fits in memory on a 32 GB machine at 30 m and at 50 m. Memory does not +force streaming at this size; parallel scaling and the serial phase set the +time. + +## What this means for the roadmap + +In roughly the order the basin needs them: + +1. **A DEM in several tiles (ROADMAP gap 6).** ANADEM comes as MGRS tiles and + Copernicus as 1°×1° tiles. The basin spans about 14° × 12°, so on the order + of 150 1° tiles. Gap 6 today assumes aligned tiles in one CRS. +2. **Geographic DEMs and the computation CRS** (ROADMAP's "inputs in their own + CRS"). rasputin's core expects a projected grid. Two routes: + - resample the DEM onto a projected grid in Python before meshing (pyproj + plus NumPy, chunked; no GDAL), or + - mesh in a projected CRS while sampling the geographic grid. + + **One CRS for the whole basin is a real choice.** *Computed:* UTM zone 23S + reaches a scale error of 1.07 % at the basin's east edge, and the Brazil + Polyconic (EPSG:5880) 4.6 %. A Lambert conformal conic fitted to the basin + (standard parallels 10°S and 18.5°S, central meridian 42°W) stays within + **0.46 %** over the whole bounding box. +3. **21b's integer incircle does not apply to non-square cells.** QW2 needs + `dx == dy`. On a geographic grid, or a projected grid resampled with + non-square cells, refine falls back to the filtered kernel. That is correct + but loses 21b's gain. Resampling to a square projected grid keeps it. +4. **Parallel refine (21d) matters more here than on the benchmark.** At + 10-28× the nodes, the scan and the serial phase both grow. Ola's + determinism ruling of 2026-09-27 ("very large areas … we should not rely on + bit-identical outputs") was made with exactly this case in mind. +5. **Domain decomposition** (21's option D, deferred to the mosaic work) is + the natural fit for a basin of this size: split the basin into parts, mesh + them in parallel, and keep shared boundaries. The tile-parallel greedy + insertion prior art (Remote Sensing 12(3):437, 2020) is the reference. +6. **16b with the basin's own data:** BHO rivers as polylines, and MapBiomas + land cover, which needs the raster step above. + +## Open questions for Ola + +1. Where does the 50 m DEM come from? If it is a resampling of ANADEM or + Copernicus, meshing the 30 m source directly may be as cheap: refinement + visits nodes, and the output size is set by the tolerance. +2. What tolerance is intended for the basin? It sets the triangle count more + than anything else. +3. Commercial use? FABDEM is non-commercial only; ANADEM and Copernicus are + the safe choices if that matters. + +## Sources + +- OAS, *São Francisco River Basin* brochure: + http://www.oas.org/en/sedi/dsd/iwrm/Past_Projects/Documents/Sao_Francisco_Brochure.pdf +- "ANADEM: A Digital Terrain Model for South America", Remote + Sensing 16(13):2321, 2024: https://www.mdpi.com/2072-4292/16/13/2321 ; + data: https://github.com/HGE-IPH/anadem , https://hge-iph.github.io/anadem/ +- Copernicus DEM: https://dataspace.copernicus.eu/explore-data/data-collections/copernicus-contributing-missions/collections-description/COP-DEM ; + https://registry.opendata.aws/copernicus-dem/ +- FABDEM licence: https://www.fathom.global/insight/fabdem-download/ +- IBGE digital elevation model: + https://www.ibge.gov.br/en/geosciences/digital-surface-models/digital-surface-models/19081-digital-elevation-model.html +- ANA BHO: https://metadados.snirh.gov.br/geonetwork/srv/api/records/0c698205-6b59-48dc-8b5e-a58a5dfcc989 +- MapBiomas Collection 9 (FAO catalogue): + https://data.apps.fao.org/catalog/iso/416a2581-b5e2-43a5-bb93-a2fc26cd1d68 +- Tile-parallel terrain simplification: https://www.mdpi.com/2072-4292/12/3/437 From 59024e3cfc430db298f98f4584bbf0f96ee5ae91 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 20:45:31 +0200 Subject: [PATCH 02/28] =?UTF-8?q?S=C3=A3o=20Francisco=20note:=20Ola's=2050?= =?UTF-8?q?=20m=20was=2030=20m?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/research/sao-francisco-basin.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/research/sao-francisco-basin.md b/docs/research/sao-francisco-basin.md index f3cab468..7b7ad9b1 100644 --- a/docs/research/sao-francisco-basin.md +++ b/docs/research/sao-francisco-basin.md @@ -113,9 +113,9 @@ In roughly the order the basin needs them: ## Open questions for Ola -1. Where does the 50 m DEM come from? If it is a resampling of ANADEM or - Copernicus, meshing the 30 m source directly may be as cheap: refinement - visits nodes, and the output size is set by the tolerance. +1. ~~Where does the 50 m DEM come from?~~ **Answered by Ola, 2026-09-27:** + the 50 m "was probably referring to 30m". The working assumption is a 30 m + DEM (ANADEM first): 708 M nodes inside the basin, 2.8 GB as float32. 2. What tolerance is intended for the basin? It sets the triangle count more than anything else. 3. Commercial use? FABDEM is non-commercial only; ANADEM and Copernicus are From 42b4b0674403e989d0b0b18b33e191f877d9ef48 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 22:17:08 +0200 Subject: [PATCH 03/28] Increment 15: design for a multi-tile, geographic DEM and a computation CRS (the basin) Redesigns the parked increment 15 (f8cedbe) for Ola's two archives, with Norway first per Ola's priority ruling of 2026-09-27: 15a (many tiles, one projected CRS) and 15b (the domain in its own CRS) ship on their own; 15c (geographic DEMs, the lattice frame, --out-crs) and 15d (window decoding, basin memory) follow. Measured on the DTM10 archive (8 of 254 tiles half a cell off the main lattice; 51-node overlaps equal) and on ANADEM by range reads (EPSG:4326, 0.970 arc-second lattice shared by all tiles, seams equal, bilinear resampling up to 13 m off the source). Probe scripts under docs/increments/15-probes/. Ten questions for Ola. Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 1032 +++++++++++++++++++++ docs/increments/15-probes/anadem_probe.py | 222 +++++ docs/increments/15-probes/dtm10_probe.py | 111 +++ 3 files changed, 1365 insertions(+) create mode 100644 docs/increments/15-dem-mosaic.md create mode 100644 docs/increments/15-probes/anadem_probe.py create mode 100644 docs/increments/15-probes/dtm10_probe.py diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md new file mode 100644 index 00000000..d732634a --- /dev/null +++ b/docs/increments/15-dem-mosaic.md @@ -0,0 +1,1032 @@ +# Increment 15 — a DEM in many tiles, inputs in their own CRS, and the computation frame + +Status: **design, not yet reviewed by Ola.** Written by `@architect` before +`@tester`, per `docs/increments/README.md` step 1, on branch +`increment15-dem-mosaic`. Nothing here is implemented. The questions for Ola +are last. Each has a recommendation, and the rest of the file assumes it. + +## Ruled by Ola + +- **2026-09-27, priority:** "My priorities are to get the Norwegian cases + sorted first, so let's finish this and turn to the main topics." So the + increment is split so that **the Norwegian case ships first and on its own** + (15a, 15b): many aligned tiles in one projected CRS, as in Ola's DTM10 + archive, and a domain or features in their own CRS. The geographic DEM and + the basin-wide computation frame for the São Francisco basin (15c, 15d) are + designed here but come **after** the Norwegian sub-increments. Nothing in + 15a or 15b depends on them. + +## Scope + +**15a and 15b (Norway, first).** Close `ROADMAP.md` gap 6 (a DEM in several +tiles) for tiles in one projected CRS, and "inputs in their own CRS" for a +projected DEM. After them, + +```sh +rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20220924/ \ + --domain catchment_wgs84.geojson --tolerance 1 --out catchment.vtk +``` + +selects the tiles the catchment needs out of 254, stitches them, transforms the +catchment from EPSG:4326 into the DEM's EPSG:25833, and meshes it. `--dem +file.tif` alone behaves exactly as today. + +**15c and 15d (the basin, after Norway).** Geographic DEMs (ANADEM, EPSG:4326), +the computation frame, `--out-crs`, and decoding only the needed part of a +2.5 GiB tile. They settle the question Ola put on 2026-09-26, "The questions +will be in which coordinate system we shall do the math" +(`16-domain-polygon.md`, "Ruled by the user"). + +**Not in this increment.** Domain decomposition (increment 21's option D), +a block-sparse raster in C++, tiles in more than one CRS, and resampling. Each +is named in "Not in scope" with the reason it waits. + +## What this builds on + +The parked design, `git show f8cedbe:docs/increments/15-dem-repository.md` +(branch `increment15-dem-repository`, never merged), called "the parked design" +below: rulings R1-R8, questions U1-U4. It was written for Ola's DTM10 archive +before anyone had read the archive's headers. They have now been read (N1-N5), +and ANADEM's too (B1-B9). + +What is carried across, and what changes: + +| parked | here | why | +|---|---|---| +| R1: repository protocol, pure `plan_mosaic`, `assemble` | carried (R1) | still right; a test double is a dict | +| R2: paths only in `io/repository.py` | carried (R2), still a question (Q3) | plus a new `dem_input.py`, so `cli.py` (1101 lines) does not grow | +| R3: `read_meta` split out of `decode_dem` | carried (R3); window decoding added in 15d | an ANADEM tile is 2.5 GiB | +| R4: every tile in the directory on one lattice, or refuse | **changed** (R4): only the **selected** tiles must share a lattice | 8 of Ola's 254 tiles are half a cell off (N2); the parked rule refuses the whole archive | +| R4: canvas origin from the tiles as decoded | **changed** (R4): each lattice has a **reference node**, and every node has a global index | frame coordinates must not depend on the window, for domain decomposition (R12) | +| R5: valid beats NoData, disagreement refused, order-independent | carried (R5), now **with real data behind it** | DTM10 (N3) and ANADEM (B4) overlaps agree bit for bit | +| R5: gaps stay NaN | **changed** (R5): a node the request needs that no tile covers is refused | Ola's ruling, `18-row-span-scan.md` R6: "a missing tile inside the extent is a data error" | +| R6: `--bbox` in the DEM's CRS, no transform | carried (R6); the domain is transformed in 15b (R9) | "inputs in their own CRS" | +| R6: `GeoPolygon` lands with the clip | superseded: 16 shipped `DomainPolygon`; 15b gives it a transform | | +| R7: `MAX_MOSAIC_BYTES = 1 GiB`, the canvas copied twice | **changed** (R7): cap at half of physical memory; one canvas, not two | a Norwegian catchment's box passes 1 GiB (R7) | +| R7: window decoding later | 15d (basin) | DTM10 tiles are 100 MB decoded; ANADEM's are 2.5 GiB | +| R8: `--dem` repeatable, `--bbox`, `dem_tiles` | carried (R11) | | +| U1-U4 | carried to the questions (Q1-Q4), U1 now with measurements | | + +## What was measured + +On `increment15-dem-mosaic` at `59024e3`: tifffile 2026.9.20, pyproj 3.8.0 +(PROJ 9.8.1), numpy 2.5.3, Python 3.14.7, on Ola's Mac (32 GiB, +`sysctl hw.memsize` = 34359738368; 10 cores). The scripts are checked in, +so each number can be re-run: + +- `docs/increments/15-probes/dtm10_probe.py headers|seams DIR` (N1-N3), run + on `../rasputin_data/DTM10_UTM33_20220924`; +- `docs/increments/15-probes/anadem_probe.py headers|seams|resample|frame` + (B2-B7). It reads ANADEM by HTTP range requests, so no tile is downloaded + whole, and it needs network access. + +They are measurement scripts, not production code, and nothing imports them. + +### Norway: the DTM10 archive + +- **N1. What the archive is.** 254 `.tif` files (each with a `.tfw` and a + `.aux.xml` beside it), 19 GB. Every file passes `io/geotiff.py`'s own header + checks (the probe calls them, so a refusal would be `decode_dem`'s): + EPSG:25833, 10 m in both axes, area-registered, float32, LZW, tiled, NoData + −32767 from tag 42113. 244 are 5051 × 5051 nodes; the rest 5052 or 5053 on a + side, and two are smaller (2881 × 3521, 4103 × 3511). Header-only read of all + 254: 0.56 s wall. +- **N2. 246 tiles share one lattice; 8 do not.** Taking the north-west-most + node (x −100250, y 7950250) as reference, 246 tiles sit at integer node + offsets. **Eight are half a cell (5 m) off east-west**, on integer rows: + 7304_1, 7507_4, 7606_2, 7707_1, 7707_3, 7807_2, 7807_3, 7808_3. Their nodes + are at x = …745 instead of …750, and they are 5052 wide and mostly 5053 high. + They look like a second production run. Under the parked R4 (whole directory + on one lattice) `--dem DIR` would refuse the whole archive. +- **N3. Neighbours overlap by 51 nodes, and the overlaps agree.** A 5051-node + tile is 50 km plus 510 m, so each edge neighbour shares 51 rows or columns + (a few share 52 or 53; one pair 53). Over 4 neighbour pairs of the first + tile, every overlapping cell is equal: 257 601 of 257 601 on each edge + overlap, 2601 of 2601 on each corner. The control shifts the comparison by + one node: on the two edge overlaps it finds a maximum difference of 18.0 m + and 46.3 m, so the probe can fail there. On the two 51 × 51 corner overlaps + the shifted comparison is also all equal: those corners are flat (sea, most + likely), so for them the control is **not** able to fail. That is 4 pairs of + 869 overlapping pairs. +- **N4. Size.** The union of all 254 tiles spans 155 051 × 125 051 nodes: + 19.4 G nodes, 72 GiB as float32. The tiles hold 6.45 G nodes (24 GiB), so + most of the box is sea or outside Norway. A 2 × 2 block of tiles is + 10 051² = 101 M nodes, 404 MB. +- **N5. The committed benchmark tile is a different release from the + archive's.** `tests/fixtures/dem_archive/7908_3_10m_z33.tif` and the + archive's file of the same name have the same tie point and shape, but + 910 706 of 25.5 M cells differ, by up to 46.1 m, and 2868 cells are valid in + the archive and NoData in the fixture. So a real multi-tile fixture must be + cut from one release, and mixing the committed tile with archive neighbours + is a mixing of releases. + +### The basin: ANADEM + +- **B1. Where ANADEM lives.** `https://hge-iph.github.io/anadem/` lists 53 + tiles, `anadem_v1_.tif` under + `metadados.snirh.gov.br/files/anadem_v1_tiles/`, named by MGRS grid-zone + designator (17L … 25M). The server accepts range requests. Tiles are 0.58 to + 2.09 GB compressed (`curl -I`). The GitHub repository's `LICENSE` is MIT and + speaks of "the Software" (see Q10). +- **B2. What an ANADEM tile is.** Every tile near the basin: EPSG:4326 + (`GTModelTypeGeoKey` 2, `GeographicTypeGeoKey` 4326), **area-registered**, + float32, Deflate, tiled 512 × 512, NoData −9999 in tag 42113, and 4 to 6 + extra pages, all reduced-resolution overviews. Spacing + **0.00026949458523585647° in both axes**: 0.970″, not 1″. It is 30 m of + equator in degrees (`d·π·a/180 = 30.000000000000004`), so ANADEM is **not on + Copernicus's 1″ lattice**. A tile's header costs 64 KiB of reads. +- **B3. The tiles are huge and share one lattice.** They are 6° grid zones, + not 1° tiles: up to 674 M cells, 2.5 GiB as float32. The basin needs about + six, not the research note's "~150". Every tile's offset from 23L is an + integer number of cells to within 2.2e-11 cells, with identical spacing. + Neighbours overlap by 8 or 9 cells, and 24L/24K by 87 rows. Band edges sit + near 8.13°S and 16.26°S, not on MGRS's 8° and 16°. +- **B4. Overlapping cells agree bit for bit.** 23L|24L (the 42°W seam, 4096 + cells) and 23L|23K (the 16.26°S seam, 4088 cells): all equal. The control can + fail: shifted by one column, 34 of 3584 equal and a maximum difference of + 19.9 m; shifted by one row, 129 of 3577 and 3.4 m. In the 87-row 24L|24K + overlap, 40 776 of 44 544 cells are valid in one tile only, and the 3768 + valid in both are equal. So **valid-beats-NoData is needed on real data**. + Three samples of three seams, not the archive. +- **B5. Size.** ANADEM's cell at 7°S / 14°S / 21°S is 29.78 / 29.12 / 28.02 m + east-west and 29.80 / 29.82 / 29.84 m north-south. The basin (636 920 km², + OAS) is **734 M nodes**. Its bounding box (48–36°W, 21–7°S, the research + note's approximation) is **2.31 G nodes, 8.6 GiB as float32**. The basin is + mostly in 23K, 23L, 24K and 24L, with slivers of 23M and 24M. Whether any of + it lies west of 48°W (22K, 22L) is to be read from the BHO polygon; not + checked. +- **B6. What resampling costs.** A real 1536² window of 23K over the Serra do + Espinhaço (43.86–43.45°W, 19.16–18.74°S, heights 592–1682 m, no NoData) was + resampled bilinearly onto square grids in the research note's basin LCC. The + resampled surface was then compared with the source at every inner source + node: + + | target spacing | max | p99.9 | p99 | median | + |---:|---:|---:|---:|---:| + | 30 m | 13.38 m | 5.43 m | 3.07 m | 0.41 m | + | 20 m | 14.06 m | 3.77 m | 2.13 m | 0.28 m | + | 10 m | 8.91 m | 1.96 m | 1.11 m | 0.14 m | + + Control: resampling onto the source's own nodes gives 9.7e-10 m. So a mesh + built to `--tolerance 1` on a resampled grid can be 13 m off the DEM it came + from, and a finer target grid does not remove that. +- **B7. How far a straight edge bends between frames.** Take an edge that is + straight in longitude and latitude, and write it in the basin LCC. Its + midpoint lies off the straight LCC edge by 5 mm at 1 km, 0.14 m at 5 km, + 2.2 m at 20 km and 13.6 m at 50 km (worst direction, at 14°S; within 10 % of + that at 7°S and 21°S). The error grows with the square of the edge length. +- **B8. What refuses an ANADEM tile today.** `io/geotiff.py`'s private checks + on 23L's header: `_single_page` passes (the extra pages are reduced), + `_check_page` gives float32, `_placement` gives the node grid, `_nodata` + gives (−9999, "tag"), and `_projected_epsg` refuses: "GeographicTypeGeoKey + (2048) = 4326 is a Geographic 2D CRS; a projected CRS is required". Nothing + else stands in the way. +- **B9. The core's indices hold at basin size, by grep.** + `include/terrain/raster/` indexes with `std::size_t` (`geometry.hpp:60`), and + `RowSpan` carries `uint32` per axis (`mesh/row_spans.hpp:31`). A grep for + 32-bit flat node indices in `include/terrain/` found none. A run on 2.3 G + nodes is @perf's (15d acceptance). + +Not measured: Deflate or LZW decode throughput for a whole tile, the triangle +count at basin scale, and the BHO polygon's CRS (SIRGAS 2000, EPSG:4674, is +likely, not checked). + +## Prior art: legacy and literature + +### Literature + +- **The refinement method is unchanged**: greedy insertion, Garland and + Heckbert, "Fast polygonal approximation of terrains and height fields", + CMU-CS-95-181, 1995 (increments 14 and 14b). This increment changes which + grid refinement runs on and in which frame, not the method. +- **Barycentric coordinates are invariant under affine maps** (textbook; e.g. + Farin, *Curves and Surfaces for CAGD*, 5th ed., 2002; recalled, not reread). + R8 rests on it. At a given point, the vertical error of a linear triangle is + the same in any affine image of the plane. So a sup-norm guarantee computed + in an affine frame of the DEM's lattice is exactly the guarantee in the DEM's + own coordinates. +- **Bilinear interpolation error** is O(h²·|f''|) for smooth f (e.g. Ciarlet, + *The Finite Element Method for Elliptic Problems*, 1978; recalled). Terrain + is not smooth at 30 m, so the bound says little. B6 measures instead. +- **DEM resampling and reprojection change terrain.** Usery, Finn, Scheidt, + Ruhl, Beard and Bearden, "Geospatial data resampling and resolution effects + on watershed modeling", *J. Geographical Systems* 6:289-306, 2004; Kienzle, + "The effect of DEM raster resolution on first order, second order and + compound terrain derivatives", *Transactions in GIS* 8(1):83-111, 2004. Both + recalled, not reread. They study derived attributes (slope, flow). Our + concern is narrower: resampling changes the surface the sup-norm is measured + against. **Difference:** we do not resample (R8). +- **Mosaicking and warping practice, for reference only (no GDAL).** + `gdalbuildvrt` composites overlapping sources in order, later over earlier. + `gdalwarp` maps every destination pixel back through the inverse transform, + approximated by linear interpolation along rows within an error threshold + (default 0.125 pixel). GDAL documentation, recalled. **Differences:** no order + precedence, since disagreement is refused and valid beats NoData in any + order (R5); and no warp (R8). +- **Projection choice**: Snyder, *Map Projections — A Working Manual*, USGS + Professional Paper 1395, 1987, recalled. A Lambert conformal conic suits an + extent wide east-west at mid latitudes, with the standard parallels about one + sixth of the latitude range in from each edge. The research note's LCC (10°S + and 18.5°S over 7–21°S) is close to that rule (9.3°S, 18.7°S). Here it is + only a suggested **output** CRS (R10). +- **ANADEM**: Laipelt et al., "ANADEM: A Digital Terrain Model for South + America", *Remote Sensing* 16(13):2321, 2024. The grid facts used here come + from the tiles' headers (B2), not from the paper. +- **Domain decomposition** prior art (tile-parallel terrain simplification, + *Remote Sensing* 12(3):437, 2020; Linardakis and Chrisochoides 2006) is cited + in `21-parallel-refine.md`. Not designed here (R12). + +**Novelty: none claimed.** One claim could look tempting later: an exact +sup-norm guarantee against a geographic DEM's own nodes, with the mesh written +in a projected CRS. It is not made. Before anyone makes it, search Google +Scholar and IEEE Xplore for "TIN generation geographic coordinates DEM +reprojection error", "terrain simplification latitude longitude grid +projection" and "greedy insertion DEM geographic lattice". No web search tool +was available to this round; ANADEM's facts were read with `curl` and range +requests. + +### Legacy + +```sh +$ grep -rlE 'RasterRepository|get_intersections|add_raster|mosaic' legacy/ +legacy/bindings.cpp +legacy/rasputin/mesh.py +legacy/rasputin/application.py +legacy/rasputin/reader.py +legacy/rasputin/web_visualize.py +legacy/tests/test_gml_repository.py +legacy/tests/test_land_cover_repository.py +legacy/tests/test_raster_repository.py +$ grep -rliE 'reproject|resampl|Transformer|to_crs|transform\(' legacy/ +legacy/rasputin/globcov_repository.py +legacy/rasputin/reader.py +legacy/rasputin/application.py +legacy/rasputin/avalanche.py +legacy/rasputin/geometry.py +legacy/rasputin/gml_repository.py +legacy/tests/test_mesh.py +legacy/tests/test_land_cover_repository.py +legacy/tests/test_gml_repository.py +``` + +- **The repository** (`legacy/rasputin/reader.py:429-468`) was reported on in + `11-raster-ingestion-prior-art.md` §3.4, §4.12, §4.13, §5.8 and §8.4, and + the parked design ruled on it. That ruling is carried unchanged. Carried: + the repository over a directory, header-only footprints, selection of the + covering tiles. Not carried: `glob` order, the unchecked CRS, polygon + subtraction, the `1e-10` stop area, `not touches`. +- **The frame, new here.** `legacy/rasputin/application.py:101-123` transformed + the domain into the raster's CRS, meshed there, and transformed the finished + mesh's vertices into a caller-chosen `target_coordinate_system`. That is the + shape of R8-R10: compute in the DEM's own frame, bring vectors in, send the + result out. **Carried.** Not carried: every CRS was spelled `+init=` and no + `Transformer` passed `always_xy` (increment 11 §9); z went through + `proj.transform(x, y, z)`, which is harmless for a 2D CRS but says nothing; + the bending of edges (B7) was never measured or recorded; and the output CRS + was a proj4 string. +- `legacy/rasputin/geometry.py:235-241`, `GeoPolygon.transform`: the domain's + transform, with the same `+init=` caveat. R9 carries the operation into + `DomainPolygon` through the one reprojection helper. +- `globcov_repository.py`, `gml_repository.py` and `avalanche.py` transform + land cover and avalanche inputs point by point. That is 16b's concern. + +A new `@migration-expert` pass is not needed: no legacy constant is re-derived. + +## Rulings + +Sub-increment tags say where each piece lands: **[15a]** and **[15b]** are +Norway, **[15c]** and **[15d]** the basin. + +### R1. The interface: a narrow repository, pure planning, assembly beside it [15a] + +Carried from the parked R1 with two additions, the lattice key and the +reference node (R4). + +``` +DemRepository (Protocol) storage only + footprints() -> tuple[TileFootprint, ...] header-only, sorted by name + load(name: str) -> DemTile whole tile, decoded + [15d: load(name, window)] + +plan_mosaic(footprints, bounds, needed=None) -> MosaicPlan pure, no pixels +assemble(plan, load) -> Mosaic pixels, no files +``` + +- `TileFootprint`: `name: str`, `meta: RasterMeta`. +- `MosaicPlan` (frozen): the mosaic's `RasterMeta`; the lattice's reference + node and the window's global index offset `(row0, col0)` (R4); per selected + tile, its name, its expected `RasterMeta`, and two index windows (where it + sits in the canvas, and which part of it is used). Every refusal that + headers can decide fires in `plan_mosaic`, before any pixel is read. +- `Mosaic`: the assembled `DemTile` plus the plan, as provenance. +- The repository does not select or assemble. Those steps are the same grid + arithmetic for every storage backend. The protocol stays at two methods, and + sync: `load` is blocking I/O, and an async caller wraps it in + `asyncio.to_thread`. +- **A declarative front, `dem_input.py`.** `DemRequest` (frozen Pydantic: + `sources: tuple[Path, ...]`, `bounds: Bounds | None`, `nodata: float | None`; + 15b adds the domain) goes in, and `open_dem(request) -> DemInput` returns the + tile, the plan and the provenance strings. `cli.py` parses flags into a + `DemRequest` and calls `open_dem`. A GUI backend or an API worker builds the + same request without Typer. `cli.py` is 1101 lines today, and this keeps the + mosaic out of it. + +### R2. Homes, and where paths live [15a] + +| module | holds | imports | +|---|---|---| +| `io/geotiff.py` | `read_meta(stream)`, the header phase (R3) | as today | +| `io/repository.py` | `DemRepository`, `TileFootprint`, `TiffDemRepository` | `io.geotiff`, `io.models` | +| `tin_engine/mosaic.py` | `Bounds`, `IndexWindow`, `MosaicPlan`, `Mosaic`, `MosaicError`, `plan_mosaic`, `assemble` | `io.models`, numpy, shapely | +| `tin_engine/dem_input.py` | `DemRequest`, `DemInput`, `open_dem` | the three above | +| [15b] `tin_engine/crs.py` | `parse_crs`, `reprojector`: the one `Transformer.from_crs` site | pyproj | +| [15c] `tin_engine/frame.py` | `LatticeFrame`, `frame_for` | `io.models`, `crs` | + +- Paths live in `io/repository.py` and `dem_input.py` (which only builds the + repository), and nowhere else below `cli.py`. `read_meta` and `decode_dem` + still take streams. Where the repository lives is still the parked U3, now + Q3. +- `TiffDemRepository.from_directory` lists `*.tif` and `*.tiff` + (case-insensitive), not recursively. That skips DTM10's `.tfw` and + `.aux.xml` side files, which carry nothing the GeoTIFF tags do not already + hold. Paths are resolved, deduplicated and sorted by their string form. +- A file in the listing that fails `read_meta` is refused, naming the file, + and is not skipped. +- Footprints are read once, lazily, and cached. `assemble` checks that each + loaded tile's `meta` equals the planned one ("changed since it was listed"). + +### R3. The header phase [15a], and window decoding [15d] + +- [15a] Carried from the parked R3: `_header(tif, nodata) -> (RasterMeta, + dtype)` holds everything `decode_dem` does before `page.asarray()` + (`io/geotiff.py:70-87`). `read_meta(source, *, nodata=None)` calls it and + returns, and `decode_dem` calls it and then decodes. The header phase keeps + the codec refusal. +- [15d] `decode_dem(source, *, window=None)`: with an `IndexWindow`, decode + only the TIFF blocks (or strips) that meet it, and return a `DemTile` of the + window, with its own `x_min`/`y_max`. Both archives are tiled 512 × 512 (N1, + B2), so the blocks come from `page.dataoffsets` and `page.decode`, decoded on + a thread pool: zlib and LZW release the GIL. A window's `DemTile` must equal + the whole tile's `DemTile` sliced to the window, `meta` included (T-window). + +### R4. Planning: the selected tiles on one lattice, or refuse [15a] + +**What changes from the parked R4.** The parked design required every tile in +the repository to share one lattice. Ola's archive has two lattices (N2), so +that rule refuses the whole archive for a catchment that touches none of the +eight odd tiles. Here: + +1. **Footprints are grouped into lattices.** Two tiles are on the same lattice + when they have the same `epsg`, `delta_x`, `delta_y`, `pixel_is_area` and + `nodata`, and their node offsets are integers within `ALIGN_TOLERANCE = 1e-6` + cells. The measured noise is at most 2.2e-11 cells (B3), and exactly 0 for + DTM10. Grouping looks at headers only and is cheap: 254 headers take 0.56 s. +2. **Each lattice has a reference node**: its north-west-most node, the minimum + node `x_min` and maximum node `y_max` over its tiles, each taken as decoded. + A node's **global index** `(R, K)` counts from it. For DTM10's main lattice + that is (x −100250, y 7950250). The reference depends only on which tiles + the repository holds. It does not depend on the request or on file order. +3. **Selection** is by the request's box in the DEM's CRS. A tile is selected + when its node rectangle meets the box, closed on both sides. **All selected + tiles must be on one lattice** (`refuses_mixed_lattice`). The message names + a tile from each lattice, and the difference: "7707_1 is 0.5 cell (5 m) + east-west off 7707_2's lattice". Different CRS, spacing, registration or + NoData are named as such (the parked `refuses_mixed_crs_mosaic`, + `refuses_mixed_spacing`, `refuses_mixed_registration`, + `refuses_mixed_nodata`). +4. **The window.** `c0 = floor((bx_min − X_ref) / dx)`, + `c1 = ceil((bx_max − X_ref) / dx)`, rows likewise, clamped to the lattice's + union. The mosaic's node grid is that index window, snapped outward, so it + covers the box. From here on everything is integer index arithmetic. The + mosaic's `x_min` is computed as `X_ref + c0 · dx`, and its `y_max` likewise. + For DTM10 (integers times 10) that is exact. The parked design's + measurement 4 (split and re-stitch reproduces the origin bit for bit) is + pinned again as M9. +5. **Coverage** (changed from the parked design's "gaps stay NaN", after Ola's + ruling in 18 R6). A node the request **needs** that no selected tile covers + is refused (`refuses_uncovered_nodes`), naming the uncovered area in the + DEM's CRS. The needed region is: + - with `bounds` alone, the whole window; + - with a domain [15b], the domain polygon grown by one cell. Only it is read + (bilinear z of a boundary vertex reads the four nodes around it; 16 R2). + Canvas cells outside the needed region that no tile covers stay NaN, as + filler, and are never read. + + The check is shapely's `covers(union of tile node rectangles, needed)`, in + the DEM's CRS. It is exact enough for rectangles on one lattice: every + corner is a node coordinate. +6. **Refused as well:** an empty repository; a box that meets no tile; a window + under 2 × 2 nodes; a canvas over the memory cap (R7). + +`plan_mosaic` never sees a path, a stream or a pixel. Its test double is a list +of `TileFootprint`. + +### R5. Assembly: stitching and overlaps [15a] + +Carried from the parked R5 almost unchanged. + +- The canvas is allocated once, NaN-filled, in `np.result_type` of the + selected tiles' dtypes (float32 for both archives). +- Tiles are loaded one at a time in plan order. Each tile's used window is + merged into the canvas, and the tile is dropped. +- **Overlaps**, per node: + - one value NoData (NaN or the sentinel), the other valid: **valid wins**; + - both NoData: NoData; + - both valid and equal (`==`): accepted; + - both valid and different: **refused** (`refuses_overlap_disagreement`), + naming both tiles, the number of disagreeing nodes and the largest + difference. That is the parked U1 (a), now Q1, with measurements: DTM10 + (N3) and ANADEM (B4) overlaps agree bit for bit where sampled. +- The result does not depend on tile order (M10). +- **One tile, no bounds: the loaded `DemTile` is returned as it is.** No + canvas, no copy, so `--dem file.tif` stays bit-identical to today, memory + included. +- The mosaic is one regular node grid, so `raster.to_core`, `grid_domain`, + `refine` and `elevation.trim` run unchanged. Refinement's lattice rule + (increment 14 R1-R2) holds. + +### R6. Which region to mesh [15a] + +The union of the selected lattice by default. `--bbox XMIN YMIN XMAX YMAX` in +the DEM's CRS, as in the parked R6 (Q2). With a domain [15b], the domain's +bounds in the DEM's CRS take `--bbox`'s place, and the two flags exclude each +other (16 R1 already said so). + +Without `--bbox` or `--domain`, `--dem DIR` on Ola's archive plans the 72 GiB +union box of the main lattice, and the cap refuses it (R7). That refusal is +correct, and its message points at `--bbox` and `--domain`. + +### R7. Memory + +**[15a] One canvas, capped at half of physical memory.** + +- **No second canvas copy.** `DemTile`'s validator copies its array + (`io/models.py:82`), so the parked design peaked at two canvases. `assemble` + builds the canvas privately, so it can hand it over without a copy: a + private constructor in `io/models.py`, `_adopt(meta, array)`. It runs the + same shape and dtype checks, sets the array read-only, and stores it. Its one + caller is `assemble`, on a buffer that `assemble` allocated and never + returns writable. The concurrency rule (increment 11 §7) survives: nobody + else holds a writable reference. +- **The cap** is `physical memory / 2` (`os.sysconf("SC_PHYS_PAGES") * + os.sysconf("SC_PAGE_SIZE")`, which works on macOS and Linux), checked in + `plan_mosaic` before any pixel is read. On Ola's Mac that is 16 GiB, about + 4.3 G float32 nodes. The peak is then the canvas plus one decoded tile (100 + MB for DTM10) plus the mesh. The parked 1 GiB (268 M nodes) would refuse a + large Norwegian catchment's box outright, and the fixed number did not know + the machine. +- **What fits.** A 2 × 2 DTM10 block: 404 MB. A 164 km × 164 km box at 10 m: + 1 GiB. All of Norway at 10 m (72 GiB box, 24 GiB of tiles): no. That is DD's + or a block-sparse raster's problem (R12), not this increment's. + +**[15d] The basin.** Its box is 8.6 GiB at float32 (B5), under the 16 GiB cap +on Ola's Mac. So **the basin is meshable in one piece with a dense canvas on +a 32 GiB machine**, with two conditions: + +- **Window decoding (R3).** Without it, a request touching 23L decodes all + 2.5 GiB of it, and assembly holds the canvas plus a whole tile. With it, the + transient is one tile's window. +- **What must be held:** the canvas (8.6 GiB), the mesh, and refine's + per-triangle state. **What is streamed:** the tiles, one window at a time, + and within a window, blocks on a thread pool. Nothing streams during refine: + the scan runs on many threads over a read-only raster (14 R7), and 18 R6 + names lazy decode inside the scan as a hazard. Nothing here decodes lazily. +- On a 16 GiB machine the cap is 8 GiB and the basin box is refused. The + answers to that are a block-sparse raster that holds only the 512² blocks + meeting the basin (about 2.7 GiB, since the basin is 32 % of its box), or + domain decomposition. Neither is designed here (Q9). + +### R8. The computation frame [15c; basin, after Norway] + +**The question.** The core wants numbers in one Cartesian frame, roughly in +metres, with square cells if 21b's integer incircle is to apply (`dx == dy`, +`21-parallel-refine.md` QW2). ANADEM gives degrees, with cells 2.4 % taller +than wide at 14°S and 6.5 % at 21°S (B5). Three routes: + +- **(A) Resample onto a square grid in a projected CRS** (the basin LCC, UTM + 23S or Polyconic), in Python, chunked. Everything downstream is unchanged. + But the tolerance is then measured against the resampled grid, not the DEM: + on real terrain, 13 m off at the DEM's own nodes (B6). This also breaks 16 + R0 ("a DEM value is the exact height at a point") for the source, and it + costs a second full-size array plus 2.3 G inverse transforms. +- **(B) Mesh in a projected CRS and sample the geographic grid.** The DEM's + nodes are then a curved grid in the mesh's frame. Increment 18's row-span + scan, and 14's lattice rule that every inserted vertex is a DEM node, both + assume the nodes form a regular grid in the frame. This route rewrites the + scan. Rejected. +- **(C) The lattice frame. Recommended (Q6).** Mesh in an affine image of the + DEM's own lattice, bring vectors in by transform (R9), and send the mesh out + by transform (R10). Nothing is resampled. The legacy did this + (`legacy/rasputin/application.py:101-123`). + +**Why (C) keeps the guarantee exactly.** The frame is an affine map of +(longitude, latitude). Barycentric coordinates are affine-invariant, so at every +DEM node the vertical error of the mesh in the frame equals its error in +longitude and latitude. The sup-norm tolerance holds at the DEM's own nodes +with no loss. What the frame changes is only horizontal: triangle shape, and +which triangulation is Delaunay, are judged in the frame, not on the ground. + +**The frame, precisely.** `LatticeFrame` (frozen) holds the DEM's CRS, the +lattice's reference node in it, the DEM spacing `(du, dv)`, and the frame +spacing `(hx, hy)`: + +- **For a projected DEM it is the identity**: `hx = du`, `hy = dv`, and the + frame's origin is the DEM's origin. Everything Norway does is bit-identical + to having no frame at all. +- **For a geographic DEM:** `hx = round_to(du · π·a/180, 2⁻¹⁰)`, and `hy` from + `dv` likewise, where `a` is the CRS ellipsoid's semi-major axis. For ANADEM + that gives `hx = hy = 30.0` exactly (B2). The node with global index + `(R, K)` has frame coordinates `(K · hx, −R · hy)`, and `RasterMeta` handed + to `to_core` carries `x_min = c0 · hx`, `y_max = −r0 · hy`, `delta = h`. +- **Exact.** `h` has at most 15 significant bits and `|K|` stays below 2²¹ + (all of South America is 1.3 M columns). So `K · h` is an exact double, + whatever the window. The same node has the same frame coordinates in every + mosaic and every subdomain (R12). 21b's condition "significant bits of dx + plus `bit_width(max(rows, cols) − 1)` ≤ 53" holds: 4 + 16 for the basin. +- **Square when the DEM's cells are square in degrees**, as ANADEM's are + (`du == dv`). Then `hx == hy`, and 21b's integer incircle applies. The price + is anisotropy: a frame unit is `cos φ`-dependent on the ground. At 21°S it + is 0.934 m east-west against 0.995 m north-south, a ratio of 1.065. That + moves a 45° angle by at most 1.8°. Scale does not matter: the tolerance is + vertical. +- **Anisotropy limit** `MAX_FRAME_ANISOTROPY = 1.10` (Q8): the ratio of ground + metres per frame unit, east-west against north-south, over the request's + extent. Above it, `frame_for` refuses. The basin passes at 1.065. A + geographic DEM of Norway (60–71°N, ratio 2 to 3) is refused, and should be: + the triangles would be judged in a frame stretched two- to threefold. +- **A geographic DEM with `du ≠ dv`** gives `hx ≠ hy`. The kernel then takes + the filtered incircle, which is correct but slower. It is not refused. + +**Who chooses the frame: nobody.** It is a function of the DEM's lattice +alone. It does not depend on the domain, on the request, or on an option. That +is deliberate (R12). + +**The boundary rule, restated.** `project_structure.md`'s `raster` section +says "every number that crosses is metres in a projected CRS, because nothing +on either side can tell". Its reason was a 2:1 anisotropy nobody could see +(increment 11 §3). In 15c it becomes: **every number that crosses is in the +computation frame, which is Cartesian, affine to the DEM's lattice, and within +`MAX_FRAME_ANISOTROPY` of square on the ground over the request's extent.** A +projected DEM satisfies it as today. `to_core(tile, frame)` refuses a +geographic tile without a frame: that is the gate, and it is tested (T-gate). + +**Resampling is designed out, not forgotten.** It is the only route for tiles +in more than one CRS, and for N2's half-cell tiles if they must be meshed +together with their neighbours. If Ola wants either, it is its own increment, +and its guarantee is against the resampled grid, stated as such. + +### R9. Inputs in their own CRS + +**[15b] For a projected DEM (Norway).** The domain, and later 16b's features, +come in any CRS pyproj can parse. They are transformed into the DEM's CRS +before anything else sees them. + +- **One helper**, `crs.reprojector(src, dst) -> Callable[[xy], xy]`, holds the + only `Transformer.from_crs` in `src_python/`, always with `always_xy=True`. + Increment 11 §9's grep test already guards every `from_crs` call site; it + now has one real site to find. A GeoTIFF's model space is (x = easting or + longitude, y = northing or latitude) whatever the EPSG axis order, so + `always_xy` is the convention the tiles themselves use. +- `DomainPolygon` keeps its CRS (`epsg: int` becomes a `crs: str`, the text + pyproj parsed, since a domain may have no EPSG code). A new + `DomainPolygon.to_crs(dst) -> DomainPolygon` transforms **vertices only**, + and rings stay straight in the destination. This is Ola's "already discrete" + direction (16, direction 3): vertices are used as given, and an edge is the + straight segment between them in the frame the mesh is built in. Within one + UTM zone the edges bend by millimetres per kilometre of edge (B7's order of + magnitude). Not densified. +- `check_crs`'s must-match rule (`domain.py:97`) is replaced by the transform. + It was kept as "one replaceable function at the Python boundary" for exactly + this (16, "Ruled by the user"). The extent check (16 R1) now runs after the + transform, against the mosaic's coverage (R4 point 5), not one tile's + rectangle. +- A GeoJSON file without a `crs` member is EPSG:4326 (RFC 7946), and is now + transformed rather than refused. +- **Provenance:** the `.vtk` records `domain_crs` (the input's CRS) and + `domain_transform` (the pyproj transformer's `description`), so a datum + shift chosen by PROJ is on record. +- Snapping: the noder's 1 mm grid (16 U6) applies after the transform, in the + DEM's CRS, as today. + +**[15c] For a geographic DEM.** Vectors go through the same helper into the +DEM's geographic CRS (longitude, latitude), then through the frame's affine map +into the frame. A domain given in longitude and latitude, like BHO's is +likely to be, has edges straight in the frame, exactly. + +### R10. The output CRS [15c; basin, after Norway] + +- **`--out-crs TEXT`** (anything `pyproj.CRS.from_user_input` accepts). + - Projected DEM: optional, and the default is the DEM's CRS. The output is + unchanged from today, and nothing is transformed. + - Geographic DEM: **required** (Q7). The refusal message prints a suggested + LCC fitted to the domain's extent (Snyder's one-sixth rule, central + meridian at the middle), so the user can copy it. +- **What is transformed:** vertex (x, y) only, frame → DEM CRS → `--out-crs`. + z is untouched. Triangles and edges keep their indices. +- **What it costs, recorded rather than hidden.** An edge straight in the + frame is not straight in the output CRS (B7: 0.14 m on a 5 km edge). The + vertical effect at a point is about the horizontal displacement times the + triangle's slope. Per triangle, `δ_T = max over its edges of |T(midpoint in + frame) − midpoint of T(ends)|`, and `ε_T = δ_T · |∇z_T|` in the output CRS. + That is one extra transform per edge. The `.vtk` records + `max_reprojection_z_error_estimate` = max over triangles of `ε_T`. It is an + estimate (midpoints, not a bound), and it says so. When `--out-crs` is the + DEM's own CRS, the output map is affine, and the field is exactly 0. +- **Recorded CRS:** `EPSG:n` when pyproj finds an exact EPSG code, otherwise + single-line WKT2 (ASCII, which `write_vtk` requires). +- `--stats` (17) measures angles and areas in the output coordinates, since + that is the mesh the user gets. + +### R11. The CLI and what the file records [15a, 15b] + +Carried from the parked R8: + +- **`--dem` becomes repeatable**: exactly one directory, or one or more files. + A directory mixed with files, or two directories, is a usage error. +- **`--bbox XMIN YMIN XMAX YMAX`**, only with `--dem`; excludes `--domain` + [15b]. +- Refusals from `plan_mosaic` and `assemble` (`MosaicError`, a `ValueError`) + become `typer.BadParameter(param_hint="--dem")`, like `GeoTiffError` today. +- **Fields:** `crs` as today; for a mosaic, `elevation_source` is prefixed + with `mosaic of N tiles, R x C nodes; `, and `dem_tiles` lists the file names + (`; `-separated, escaped with `encode("ascii", "backslashreplace")`). [15b] + adds `domain_crs` and `domain_transform`. [15c] adds `computation_frame` + (for example `lattice of EPSG:4326, h = 30 m, reference lon -48.00089 lat + -8.12998`) and `max_reprojection_z_error_estimate`. +- stderr: one line, `mosaic of N tiles, R x C nodes`. + +### R12. What domain decomposition needs from this, and what must not block it + +Domain decomposition is not designed here (`21-parallel-refine.md` Q5 moved +it to the large-area work). What 15 settles so that it stays open: + +- **One frame for the whole DEM.** It is a function of the lattice alone + (R8), never of the domain, the window or an option. Every subdomain uses the + same frame. +- **Global node identity.** `(R, K)` from the lattice's reference node (R4). + Two subdomains that share a seam agree on every seam node's index, and, by + R8's exactness, on its frame coordinates bit for bit. +- **The raster for a subdomain is a plan.** `plan_mosaic(footprints, bounds)` + is pure. A subdomain's box gives its own window, under its own memory cap. +- **Overlaps resolve the same way in every subdomain**, because R5 does not + depend on order. +- **Vectors are transformed once**, before any partition, so a domain vertex on + a seam has one frame position. +- **The output CRS is explicit** (R10), not fitted per domain. A fitted + default would put two subdomains, or two catchments, in different CRSs. + +Choices rejected partly because they would block it: resampling per window +(neighbouring windows would disagree at seams); a computation CRS fitted to +each domain; order-dependent overlap resolution; a frame origin at each +mosaic's corner (the same node would get different coordinates); decoding +lazily inside the parallel scan. + +## Invariants + +- **I1. One lattice per mosaic.** Every node of an assembled mosaic lies on + the selected lattice. Any selected tile off it is a refusal, never resampled + or snapped. +- **I2. Order independence.** `plan_mosaic` and `assemble` give the same + result for every permutation of the footprints. +- **I3. Valid beats NoData; disagreement refuses.** No overlapping node is ever + silently chosen between two different valid values. +- **I4. Coverage.** Every node the request needs comes from a tile. NaN filler + exists only outside the needed region. +- **I5. Split and re-stitch is the identity.** A grid cut into tiles + (any overlap, either registration) and planned without bounds reassembles to + the original `DemTile`, `meta` and array equal with `==`. +- **I6. Header before pixels.** Every refusal that headers can decide fires + before any `load`. +- **I7. Single-file path unchanged.** `--dem file.tif` output and memory are + bit-identical to before 15a. +- **I8. [15b] One reprojection site**, with `always_xy=True`. A vector's + vertices are transformed exactly once. +- **I9. [15c] The frame is a function of the lattice alone**, and exact: a + node's frame coordinates are `(K · hx, −R · hy)` bit for bit, in every + window. +- **I10. [15c] No degrees cross into `_core`.** `to_core` refuses a geographic + tile without a `LatticeFrame`. +- **I11. [15c] The guarantee is unchanged.** With the lattice frame, + `--tolerance` holds at every valid DEM node inside the domain, measured in + the DEM's own coordinates. + +## Degeneracy policy + +- **Overlap width.** Any width, including none (area-registered neighbours + that abut: disjoint node sets), one shared line (point-registered), 51 nodes + (DTM10), 87 rows (ANADEM 24L/24K), or a tile wholly inside another. One rule + covers all (R5). +- **The same file twice** under two names: every overlap is equal, so it is + accepted. The recorded tile list shows both. +- **A tile touching the box on one node line** is selected, and contributes + that line. +- **Float noise in tile offsets** up to `ALIGN_TOLERANCE = 1e-6` cells. Measured + noise is at most 2.2e-11 (B3). +- **Half-cell offsets** (N2): refused by name, not snapped. Snapping would move + every value of a tile by 5 m, which is resampling. +- **Spacings not exact in binary** (0.1 m, or ANADEM's degrees): the tolerance + above, then integer windows (parked M3). +- **A domain vertex on a tile seam, or a bilinear cell straddling a seam:** + nothing special. The canvas is one array. +- **[15b] Axis order.** Every transform is `always_xy`. A GeoJSON with + latitude first is a wrong file, not a case, and is refused by the extent + check. +- **[15b] Datum shifts** (SIRGAS 2000 or ETRS89 to WGS 84): whatever + transformation PROJ picks, recorded in `domain_transform`. +- **[15c] The antimeridian and the poles.** A lattice whose extent crosses + ±180° is refused. The poles are refused by the anisotropy limit. +- **[15c] `du ≠ dv`** (Copernicus above 50° uses wider longitude spacing): + allowed, with the filtered incircle. Tiles of different spacing in one + request are refused as mixed spacing. + +## Not in scope + +- **Tiles in more than one CRS, and the eight half-cell tiles meshed with + their neighbours.** Both need resampling, and R8 explains what that costs. + A catchment inside the eight's own lattice meshes; one straddling the two + lattices is refused by name. +- **Domain decomposition**, and all of Norway at 10 m in one mesh (R7, R12). +- **A block-sparse raster in C++** (18 R6's `row_segments` source). Needed + only to mesh the whole basin on a 16 GiB machine; Q9. +- **Recursive directory walks, remote storage, an async repository API.** +- **Vertical datums.** Heights pass through unchanged, as today. + +## Sub-increments and LOC + +Counted in `CLAUDE.md` §2's unit (production lines, comments and docstrings +excluded, tests excluded). Estimates. Increment 10 came in 39 % over its +estimate and 14 came in 17 % over; the worst case below applies 39 %. + +**Norway first (Ola, 2026-09-27).** 15a and 15b ship on their own and in this +order. 15c and 15d follow later. + +| | what | est. | worst | +|---|---|---:|---:| +| **15a** | **Norway: many tiles, one projected CRS** | | | +| | `io/geotiff.py`: `_header`, `read_meta` | 20 | | +| | `io/repository.py`: protocol, footprint, listing, cache, load | 60 | | +| | `mosaic.py` types: `Bounds`, `IndexWindow`, placements, plan, `Mosaic`, error | 40 | | +| | `plan_mosaic`: lattice grouping, reference node, selection, coverage, cap | 105 | | +| | `assemble`: canvas, overlap rule, single-tile fast path, meta check | 55 | | +| | `io/models.py`: `_adopt` | 15 | | +| | `dem_input.py`: `DemRequest`, `open_dem` | 35 | | +| | `cli.py`: `--dem` list, `--bbox`, refusals, fields | 45 | | +| | **15a total** | **375** | **520** | +| **15b** | **Norway: the domain in its own CRS** | | | +| | `crs.py`: `parse_crs`, `reprojector` | 30 | | +| | `domain.py`: `crs: str`, `to_crs`, extent against coverage; `check_crs` removed | 40 | | +| | `dem_input.py`: domain to bounds and the needed region | 20 | | +| | `cli.py`: `--domain` with a mosaic, `domain_crs`, `domain_transform` | 25 | | +| | **15b total** | **115** | **160** | +| **15c** | **Basin, after Norway: geographic DEMs and the frame** | | | +| | `io/geotiff.py`: accept geographic 2D, degree axes | 30 | | +| | `io/models.py`: `RasterMeta` CRS kind | 10 | | +| | `frame.py`: `LatticeFrame`, `frame_for`, anisotropy, affine maps | 95 | | +| | `raster.py`: `to_core(tile, frame)`, the gate | 15 | | +| | output: `--out-crs`, transform, bending estimate, WKT2 field, stats on output | 85 | | +| | **15c total** | **235** | **325** | +| **15d** | **Basin, after Norway: window decoding and basin memory** | | | +| | `io/geotiff.py`: window decode, block selection, thread pool | 55 | | +| | `io/repository.py`, `assemble`: windows through `load` | 20 | | +| | **15d total** | **75** | **105** | + +Each is under 700 even at its worst case. 15a and 15b together would be 490, +or 680 at the worst case: too close to the ceiling to merge, and Ola asked for +Norway to ship on its own anyway. No sub-increment touches C++, so the +refine/mesh acceptance rule (README, "Acceptance") does not formally apply. The +basin run in 15d's acceptance is @perf's all the same, because it is the first +run at 30× the benchmark. + +**Documentation changed in the same PRs** (not counted): +`project_structure.md` (layout; the `raster` boundary rule in 15c, R8); +`11-raster-ingestion.md` §3 and §10 (15c; `GeoPolygon` superseded); +`16-domain-polygon.md` U1 (15b: the must-match rule replaced); `io/__init__.py`'s +docstring (Q3); `ROADMAP.md` rows 15a-15d and the "inputs in their own CRS" +item. + +## Tests for `@tester` + +Micro-TIFFs come from `tests/python/geotiff_fixtures.py`. Planning and assembly +tests build `RasterMeta` and `DemTile` directly and use a dict as the +repository. The parked design's lists M1-M12, F1-F4, G1-G2 and C1-C6 are +carried as written there, with the changes below. + +**Invariant-critical, and mutation testing required:** + +- **15a: `test_mosaic.py`** (planning and assembly). The index arithmetic, the + lattice grouping and the overlap rule are where a wrong answer is a silently + wrong terrain. +- **15c: `test_frame.py`.** The frame's exactness and the no-degrees gate are + where a wrong answer is a silently wrong guarantee. + +15b and 15d are ordinary suites. 15b's axis-order cases (below) are the ones +that matter there. + +**15a, changed or new against the parked list:** + +- M2 becomes lattice grouping: two lattices in one repository are accepted; + a request inside either one plans; a request straddling both is refused, + naming a tile of each and the offset (0.5 cell, as in N2). +- New M13, the reference node: the same tile's global index is the same + whether planned alone, with neighbours, or under any `bounds`. The mosaic's + `x_min` equals `X_ref + c0 · dx` bit for bit. +- New M14, coverage: a hole in the tile set inside the box is refused, naming + the area. The same hole outside the needed region is NaN filler and is + accepted. +- M5 becomes the cap: set from a patched physical-memory function, not a + constant, and `load` is never called. +- New M15: `_adopt` is reachable only from `assemble`. The mosaic's array is + read-only, and `np.shares_memory` holds between the canvas and the result + (no copy), while the single-tile path still returns the loaded tile itself. +- T-real (the parked design's, `needs_codecs`): the committed benchmark tile + cut into quadrants, 1-pixel overlap, meshes exactly like the whole tile. + +**15b:** + +- The domain in EPSG:4326 GeoJSON (no `crs` member), in EPSG:25832, and in + WKT with `--domain-crs EPSG:3035`, each over a 25833 mosaic: the transformed + vertices equal pyproj's own `always_xy` transform, and the mesh is built. +- **Axis order, able to fail:** a test transform without `always_xy` puts a + vertex outside the DEM, and the extent check refuses it. Increment 11 §9's + grep test finds exactly one `from_crs` site. +- `domain_crs` and `domain_transform` are recorded. A domain already in the + DEM's CRS is not transformed, and the mesh is bit-identical to 16's. + +**15c:** + +- T-frame: for random lattices (spacing, offset, size), `(K · h, −R · h)` + equals the planned `RasterMeta`'s node coordinates bit for bit, in every + window. +- T-invariance: an analytic plane `z = a + b·lon + c·lat` on a geographic + micro-mosaic meshes to two triangles with zero error, at any tolerance. +- T-gate: `to_core` of a geographic tile without a frame raises. +- T-anisotropy: a geographic extent reaching 20°S passes, and one reaching 30°S + or 60°N is refused (the ratio is about 1/cos φ: 1.06, 1.15, 2.0). +- T-out: `--out-crs` equal to the DEM's CRS gives + `max_reprojection_z_error_estimate` exactly 0; an LCC gives a positive value + that grows with edge length. + +**15d:** + +- T-window: for every window, including windows on block edges and one-node + windows, `decode_dem(s, window=w)` equals `decode_dem(s)` sliced to `w`, + `meta` included. Tiled and stripped micro-TIFFs. +- The mosaic built with windows equals the one built from whole tiles. + +## Test data + +**Norway (15a, 15b).** + +- *Synthetic:* micro-TIFF mosaics, 2 × 2, overlap 0, 1 and 3, point- and + area-registered; a two-lattice repository with one tile shifted half a cell. +- *Real, split:* the committed benchmark tile cut into quadrants (above). +- *Real, seam:* an extract of a real DTM10 edge overlap from Ola's archive, + for example 6400_1 | 6400_4 (N3's first edge pair). Two windows of about + 256 × 307 nodes (256 plus the 51-node overlap), with their real + georeferencing, Deflate-compressed: well under 1 MB. **Cut both from the + archive, never one from the committed tile** (N5). Same attribution as the + committed tile. The extraction script is test support and goes under + `tests/`, as @tester's. +- *Real, two lattices:* an extract of one of N2's eight tiles and an aligned + neighbour, for `refuses_mixed_lattice`. @tester picks the pair with + `dtm10_probe.py headers`. +- *@perf, Norway:* the 2 × 2 block 7908_3, 7908_2, 7808_4, 7808_1 from the + archive (all on the main lattice): 10 051² = 101 M nodes, 404 MB float32, + 4× the benchmark tile. Local only, not committed. + +**The basin (15c, 15d; after Norway).** + +- *Synthetic:* a geographic micro-TIFF with ANADEM's exact header numbers + (EPSG:4326, area-registered, spacing 0.00026949458523585647, tie point + (−48.00102804928458, −8.129843152810082), NoData −9999). `micro_tiff` needs + a geographic-CRS parameter, which is test code. +- *Real, seam:* extracts of 23L | 24L (the 42°W seam, bit-equal overlap) and + 24L | 24K (the 87-row overlap with NoData on one side), cut by range reads + as in `anadem_probe.py seams`. Four 512² blocks, about 2.4 MB compressed. + Whether they may be committed is Q10. +- *@perf, a real piece of the basin:* **46–44°W × 17.26–15.26°S**, across + the 23L/23K seam, over the São Francisco near Pirapora–Januária and the + Paracatu. It is believed to lie inside the basin; @perf checks that against + BHO. 7422 × 7422 = **55 M nodes, 210 MiB float32**, about 2.2× the + benchmark. About 240 blocks by range reads, roughly 150 MB of download + (from the measured 0.5–0.85 MB per block). The Espinhaço window of B6 (2.4 M + nodes, 45 km, steep) is a quick steep-terrain case. +- *@perf, the whole basin:* the four main tiles plus slivers, about 8 GB of + download; the box is 8.6 GiB at float32 (B5). + +## Acceptance + +- **15a:** `rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20220924/ --bbox + … --tolerance 1` on the 2 × 2 block writes a `.vtk` that ParaView opens, + with `dem_tiles` listing four files. The same box over one of N2's odd tiles + and a neighbour is refused, naming both. The quadrant split of the benchmark + tile meshes identically to the tile. `--dem file.tif` output unchanged. +- **15b:** a catchment polygon in EPSG:4326 over the archive meshes, with + `domain_crs` and `domain_transform` recorded. +- **15c:** the basin piece above meshes with `--out-crs` set to the basin + LCC, and records `computation_frame` and the reprojection estimate. +- **15d:** @perf meshes the basin piece and then the whole basin on Ola's Mac, + recording peak memory, decode time and refine time, under + `docs/benchmarks//`, with the power state. +- For all: every gate in `CLAUDE.md` §4 green, and CI green. + +## Questions for Ola + +Q1-Q5 are about Norway and are needed before 15a starts. Q6-Q10 are about +the basin and can wait until after Norway. + +**Q1 (the parked U1). Two tiles give different valid values at the same node.** +Now measured: DTM10 overlaps agree bit for bit on 4 of 869 pairs (N3), and +ANADEM's on 3 seams (B4). But the committed benchmark tile and the archive's +tile of the same name differ at 910 706 nodes (N5), so two releases in one +directory is a real case. +- **(a) Refuse, naming both tiles, the count and the largest difference. + Recommended.** Valid still beats NoData, and the result does not depend on + tile order. +- (b) First tile by sorted name wins, silently. That is what the legacy did in + effect. +- (c) Refuse by default, and add a `--overlap first` flag. About 15 lines more. + +**Q2 (the parked U2). `--bbox`.** +- **(a) The union by default, plus an optional `--bbox` in the DEM's CRS. + Recommended.** Without it, `--dem DIR` on your archive is refused by the cap + (72 GiB box), and the only way round would be copying files into a smaller + directory. It is about 20 lines. +- (b) The union only. + +**Q3 (the parked U3). Where the repository and its paths live.** `io/__init__.py` +says nothing in `io/` opens a file. +- **(a) `io/repository.py`, with `io/`'s rule narrowed to "decoders and + encoders take streams; `io/repository.py` is the one module that opens + files, read-only". Recommended.** Storage access sits next to the formats it + reads. +- (b) A top-level `tin_engine/dem_repository.py`, leaving `io/`'s rule as it is. +- (c) Paths in `cli.py`. Not recommended: `cli.py` is 1101 lines, and a GUI + or API worker could not reuse it. + +**Q4 (the parked U4). Tiles with different NoData sentinels.** Your ruling +C2 (b) in increment 18 already assumed (a): "differing sentinels are refused". +Both real archives have one sentinel each (N1, B2). +- **(a) Refuse. Recommended; please confirm.** +- (b) Rewrite each tile's sentinel to NaN and give the mosaic no sentinel. + About 10 lines. + +**Q5 (new). The eight half-cell tiles in your archive (N2).** 7304_1, +7507_4, 7606_2, 7707_1, 7707_3, 7807_2, 7807_3 and 7808_3 sit 5 m east-west +off the other 246, and are one or two nodes larger. +- **(a) Refuse a request that mixes them with the main lattice; a request + inside their own lattice meshes. Recommended.** Nothing is resampled. You + may know where they came from: a re-delivery of those sheets on a shifted + grid would explain it, and a fresh download of them might be on the main + lattice. +- (b) Resample them onto the main lattice (bilinear, a half-cell shift, so + every value becomes the average of two). That is resampling (R8), and its + own increment. + +**Q6 (the basin). The computation frame.** +- **(a) The lattice frame: mesh in an affine image of the DEM's own grid, + transform the domain in and the mesh out. Recommended.** The tolerance holds + exactly at the DEM's own nodes. Triangle shapes are judged in a frame up to + 6.5 % out of square on the ground at the basin's south edge. +- (b) Resample onto a square grid in a projected CRS first. Shapes are judged + on the ground, but the tolerance then refers to the resampled grid: 13 m off + the DEM at worst on real Espinhaço terrain (B6). It also costs a second + array the size of the basin's box. + +**Q7 (the basin). The output CRS for a geographic DEM.** +- **(a) Required as `--out-crs`, with a suggested basin-fitted LCC in the + refusal message. Recommended.** The output CRS is a contract with whatever + reads the mesh, and a CRS fitted to each domain would put two catchments, or + two subdomains, in different CRSs. +- (b) Fit an LCC to the domain automatically, and record it as WKT2. +- (c) Write longitude and latitude. Exact, with no bending, but in degrees. + +**Q8 (the basin). The anisotropy limit and the square frame.** +- **(a) A square frame (21b's integer incircle applies) and a limit of 10 %: + the basin passes (6.5 %); geographic DEMs above about 25° latitude are + refused. Recommended.** +- (b) A frame fitted to the extent's mean latitude: shapes truer (±3 % over + the basin), cells not square, so the filtered incircle is used and 21b's + gain (−15 % refine at 8 threads) is lost for geographic DEMs. + +**Q9 (the basin). Memory for the whole basin.** Its box at 30 m is 8.6 GiB, +and the design caps the canvas at half of physical memory. +- **(a) A dense canvas, as designed: the basin fits on your 32 GiB Mac. + Recommended for now.** Is 32 GiB the machine the basin must run on, or must + it also run on a smaller one? +- (b) A block-sparse raster in C++ holding only the 512² blocks that meet the + basin (about 2.7 GiB). Its own increment: a new `RasterSource`, the concept + change 18 R6 describes, and a binding. +- (c) Go to domain decomposition, which also solves memory. + +**Q10 (the basin). May an ANADEM extract be committed as a test fixture?** +ANADEM's repository is MIT-licensed, but that licence names "the Software", +and the data derives from Copernicus GLO-30, which carries its own attribution +terms. +- **(a) Commit about 2.4 MB of extracts with both notices, after you or the + authors confirm the data licence. Recommended.** +- (b) Download on demand in a test marked `network`, skipped in CI. + +Still open from the research note, for @perf's basin run: the tolerance +intended for the basin (it sets the triangle count more than anything else), +and whether commercial use matters. diff --git a/docs/increments/15-probes/anadem_probe.py b/docs/increments/15-probes/anadem_probe.py new file mode 100644 index 00000000..ec508c49 --- /dev/null +++ b/docs/increments/15-probes/anadem_probe.py @@ -0,0 +1,222 @@ +"""Increment 15's measurements on ANADEM v1, by HTTP range reads (no full download). + + python docs/increments/15-probes/anadem_probe.py headers # B2, B3: CRS, lattice, overlaps + python docs/increments/15-probes/anadem_probe.py seams # B4: do overlaps agree? + python docs/increments/15-probes/anadem_probe.py resample # B6: resampling error + python docs/increments/15-probes/anadem_probe.py frame # B5, B7: sizes, edge bending + +Needs network access to metadados.snirh.gov.br, plus tifffile, numpy and pyproj. +It is a measurement script, not production code, and nothing imports it. +""" + +from __future__ import annotations + +import io +import math +import sys +import urllib.request + +import numpy as np +import pyproj +import tifffile + +URL = "https://metadados.snirh.gov.br/files/anadem_v1_tiles/anadem_v1_{}.tif" +BASIN_TILES = ("22L", "23K", "23L", "23M", "24K", "24L", "24M", "25L", "25M") +NODATA = -9999.0 +LCC = "+proj=lcc +lat_1=-10 +lat_2=-18.5 +lat_0=-14 +lon_0=-42 +ellps=WGS84 +units=m +no_defs" + + +class RangeFile(io.RawIOBase): + """A seekable read-only file over HTTP range requests, cached in 64 KiB blocks.""" + + BLOCK = 1 << 16 + + def __init__(self, url: str) -> None: + self.url, self.pos, self.cache, self.fetched = url, 0, {}, 0 + head = urllib.request.urlopen(urllib.request.Request(url, method="HEAD"), timeout=30) + self.size = int(head.headers["Content-Length"]) + + def seekable(self) -> bool: + return True + + def readable(self) -> bool: + return True + + def tell(self) -> int: + return self.pos + + def seek(self, offset: int, whence: int = 0) -> int: + self.pos = {0: offset, 1: self.pos + offset, 2: self.size + offset}[whence] + return self.pos + + def _block(self, b: int) -> bytes: + if b not in self.cache: + start, end = b * self.BLOCK, min((b + 1) * self.BLOCK, self.size) - 1 + req = urllib.request.Request(self.url, headers={"Range": f"bytes={start}-{end}"}) + self.cache[b] = urllib.request.urlopen(req, timeout=60).read() + self.fetched += end - start + 1 + return self.cache[b] + + def readinto(self, buf) -> int: # type: ignore[no-untyped-def] + n, out = min(len(buf), self.size - self.pos), bytearray() + while len(out) < n: + b, o = divmod(self.pos + len(out), self.BLOCK) + out += self._block(b)[o : o + n - len(out)] + buf[:n] = out + self.pos += n + return n + + +def open_tile(name: str) -> tuple[io.BufferedReader, tifffile.TiffFile]: + f = io.BufferedReader(RangeFile(URL.format(name)), buffer_size=1 << 16) + return f, tifffile.TiffFile(f) + + +def header(name: str) -> tuple[float, float, int, int, float]: + """(lon, lat) of the upper-left corner, rows, cols, spacing.""" + _, tif = open_tile(name) + page = tif.pages.first + tie, scale = page.tags[33922].value, page.tags[33550].value + return tie[3], tie[4], page.imagelength, page.imagewidth, scale[0] + + +def block(name: str, block_row: int, block_col: int) -> np.ndarray: + """One 512 x 512 TIFF block, decoded, as float64.""" + f, tif = open_tile(name) + page = tif.pages.first + index = block_row * -(-page.imagewidth // 512) + block_col + f.seek(page.dataoffsets[index]) + data = f.read(page.databytecounts[index]) + return np.asarray(page.decode(data, index)[0], dtype=np.float64).reshape(512, 512) + + +def headers() -> None: + rows = {} + for name in BASIN_TILES: + f, tif = open_tile(name) + page, keys = tif.pages.first, tif.geotiff_metadata or {} + rows[name] = header(name) + print( + name, page.shape, page.dtype, page.compression.name, "tiled", page.tilewidth, + "pages", len(tif.pages), "reduced", [p.is_reduced for p in tif.pages[1:]], + "nodata", page.tags[42113].value if 42113 in page.tags else None, + "model", keys.get("GTModelTypeGeoKey"), "raster", keys.get("GTRasterTypeGeoKey"), + "geographic", keys.get("GeographicTypeGeoKey"), "bytes read", f.raw.fetched, + ) + x0, y0, _, _, d = rows["23L"] + print(f"spacing {d!r} deg = {d * 3600:.6f} arc-seconds") + boxes = {} + for name, (x, y, r, c, dd) in rows.items(): + kx, ky = (x - x0) / d, (y0 - y) / d + boxes[name] = (round(ky), round(ky) + r, round(kx), round(kx) + c) + print( + f"{name}: same spacing {dd == d}; offset cols {kx:.6f} rows {ky:.6f} " + f"(off-integer {abs(kx - round(kx)):.1e}, {abs(ky - round(ky)):.1e}); " + f"lon [{x:.4f}, {x + c * d:.4f}] lat [{y - r * d:.4f}, {y:.4f}]; " + f"{r * c / 1e6:.0f} M cells, {r * c * 4 / 2**30:.2f} GiB float32" + ) + for a, b in (("23L", "24L"), ("23L", "23K"), ("23L", "23M"), ("24L", "24K"), ("24L", "24M"), + ("23K", "24K"), ("22L", "23L"), ("24L", "25L"), ("23M", "24M")): + ra, rb = boxes[a], boxes[b] + print(f"{a}/{b}: overlap rows {min(ra[1], rb[1]) - max(ra[0], rb[0])}, " + f"cols {min(ra[3], rb[3]) - max(ra[2], rb[2])}") + + +def compare(label: str, a: np.ndarray, b: np.ndarray) -> None: + va, vb = a != NODATA, b != NODATA + both = va & vb + diff = np.abs(a[both] - b[both]) + worst = diff.max() if diff.size else None + print(f"{label}: {a.size} cells, both valid {int(both.sum())}, " + f"one valid {int((va ^ vb).sum())}, equal {int((a[both] == b[both]).sum())}, " + f"max |diff| {worst}") + + +def seams() -> None: + # 23L | 24L at 42 W: 24L starts 22264 columns east of 23L, same rows. Row block 29. + a, b = block("23L", 29, 43), block("24L", 29, 0) + c0 = 22264 - 43 * 512 + compare("23L|24L aligned ", a[:, c0 : c0 + 8], b[:, 0:8]) + compare("23L|24L shifted 1 column ", a[:, c0 + 1 : c0 + 8], b[:, 0:7]) + # 23L | 23K: 23K starts 30163 rows south and 1 column west of 23L. + tr = 30163 // 512 + r0 = 30163 - tr * 512 + a, b = block("23L", tr, 20), block("23K", 0, 20) + compare("23L|23K aligned ", a[r0 : r0 + 8, 0:511], b[0:8, 1:512]) + compare("23L|23K shifted 1 row ", a[r0 + 1 : r0 + 8, 0:511], b[0:7, 1:512]) + # 24L | 24K: 87-row overlap, same columns. + a = np.vstack([block("24L", tr, 5), block("24L", tr + 1, 5)])[r0 : r0 + 87] + compare("24L|24K 87 rows ", a, block("24K", 0, 5)[0:87]) + + +def bilinear(z: np.ndarray, fx: np.ndarray, fy: np.ndarray) -> np.ndarray: + c = np.clip(np.floor(fx).astype(int), 0, z.shape[1] - 2) + r = np.clip(np.floor(fy).astype(int), 0, z.shape[0] - 2) + tx, ty = fx - c, fy - r + return (z[r, c] * (1 - tx) * (1 - ty) + z[r, c + 1] * tx * (1 - ty) + + z[r + 1, c] * (1 - tx) * ty + z[r + 1, c + 1] * tx * ty) + + +def resample() -> None: + """3 x 3 blocks of 23K over the Serra do Espinhaco, resampled onto square LCC grids.""" + r_block, c_block = 18, 30 + s = np.vstack([np.hstack([block("23K", r_block + i, c_block + j) for j in range(3)]) + for i in range(3)]) + x, y, _, _, d = header("23K") + lon0 = x + d / 2 + c_block * 512 * d # area-registered: nodes at cell centres + lat0 = y - d / 2 - r_block * 512 * d + print(f"window lon [{lon0:.4f}, {lon0 + 1535 * d:.4f}] " + f"lat [{lat0 - 1535 * d:.4f}, {lat0:.4f}], " + f"NoData cells {int((s == NODATA).sum())}, z [{s.min():.1f}, {s.max():.1f}]") + fwd = pyproj.Transformer.from_crs("EPSG:4326", LCC, always_xy=True) + inv = pyproj.Transformer.from_crs(LCC, "EPSG:4326", always_xy=True) + rr, cc = np.mgrid[0:1536, 0:1536] + lon, lat = lon0 + cc * d, lat0 - rr * d + px, py = fwd.transform(lon, lat) + m = 40 + inner = (slice(2 * m, -2 * m), slice(2 * m, -2 * m)) + for h in (30.0, 20.0, 10.0): + xs = np.arange(px[m:-m, m:-m].min(), px[m:-m, m:-m].max(), h) + ys = np.arange(py[m:-m, m:-m].max(), py[m:-m, m:-m].min(), -h) + gx, gy = np.meshgrid(xs, ys) + glon, glat = inv.transform(gx, gy) + resampled = bilinear(s, (glon - lon0) / d, (lat0 - glat) / d) + back = bilinear(resampled, (px[inner] - xs[0]) / h, (ys[0] - py[inner]) / h) + e = np.abs(back - s[inner]) + print(f"h = {h:4.0f} m: {resampled.size / 1e6:.2f} M target nodes; |resampled - source| at " + f"source nodes: max {e.max():.2f} m, p99.9 {np.quantile(e, 0.999):.2f}, " + f"p99 {np.quantile(e, 0.99):.2f}, median {np.median(e):.3f}") + ident = bilinear(s, (lon[100:-100, 100:-100] - lon0) / d, (lat0 - lat[100:-100, 100:-100]) / d) + control = np.abs(ident - s[100:-100, 100:-100]).max() + print(f"control, resampled onto its own nodes: max {control:.1e}") + + +def frame() -> None: + d = 0.00026949458523585647 + geod = pyproj.Geod(ellps="WGS84") + for lat in (-7, -14, -21): + ew = geod.inv(-42, lat, -42 + d, lat)[2] + ns = geod.inv(-42, lat, -42, lat + d)[2] + print(f"lat {lat}: cell E-W {ew:.3f} m, N-S {ns:.3f} m, ratio {ns / ew:.4f}") + cols, rows = 12 / d, 14 / d + print(f"basin bbox 48-36 W, 21-7 S: {cols:.0f} x {rows:.0f} = {cols * rows / 1e9:.2f} G nodes, " + f"{cols * rows * 4 / 2**30:.1f} GiB float32") + cell = geod.inv(-42, -14, -42 + d, -14)[2] * geod.inv(-42, -14, -42, -14 + d)[2] + print(f"basin 636 920 km2 at the 14 S cell area: {636920e6 / cell / 1e6:.0f} M nodes") + print(f"frame spacing d * pi * a / 180 = {d * math.pi * 6378137.0 / 180!r}") + fwd = pyproj.Transformer.from_crs("EPSG:4326", LCC, always_xy=True) + for lat in (-7, -14, -21): + for km in (1, 5, 20, 50): + worst = 0.0 + for deg in range(0, 180, 15): + a, half = math.radians(deg), 0.5 * km * 1000 / 111320 + lo = [-42 - half * math.cos(a), -42 + half * math.cos(a), -42.0] + la = [lat - half * math.sin(a), lat + half * math.sin(a), float(lat)] + (x0, x1, xm), (y0, y1, ym) = fwd.transform(lo, la) + worst = max(worst, math.hypot(xm - (x0 + x1) / 2, ym - (y0 + y1) / 2)) + print(f"lat {lat}, edge {km:2d} km: straight lattice edge vs straight LCC edge, " + f"midpoints {worst:.4f} m apart") + + +if __name__ == "__main__": + {"headers": headers, "seams": seams, "resample": resample, "frame": frame}[sys.argv[1]]() diff --git a/docs/increments/15-probes/dtm10_probe.py b/docs/increments/15-probes/dtm10_probe.py new file mode 100644 index 00000000..f5cd42e8 --- /dev/null +++ b/docs/increments/15-probes/dtm10_probe.py @@ -0,0 +1,111 @@ +"""Increment 15's measurements on Ola's DTM10 archive (254 UTM33 tiles). + + python docs/increments/15-probes/dtm10_probe.py headers DIR # N1, N2, N4: headers, lattice + python docs/increments/15-probes/dtm10_probe.py seams DIR # N3: do overlaps agree? + +Header-only for `headers`, through io/geotiff.py's own private checks, so a +refusal here is the refusal `decode_dem` would raise. A measurement script, not +production code; nothing imports it. +""" + +from __future__ import annotations + +import collections +import sys +from pathlib import Path + +import numpy as np +import tifffile + +from tin_engine.io import geotiff as g + + +def metas(directory: Path) -> dict[str, tuple]: + out, refused = {}, collections.Counter() + for path in sorted(directory.glob("*.tif")): + with path.open("rb") as stream, tifffile.TiffFile(stream) as tif: + page, pages = tif.pages.first, tuple(tif.pages) + try: + g._single_page(pages) + dtype = g._check_page(page) + tie, scale = g._georeferencing(page) + keys = tif.geotiff_metadata or {} + x, y, dx, dy, area = g._placement(tie, scale, keys) + epsg = g._projected_epsg(keys) + nodata, source = g._nodata(page, dtype, None) + except g.GeoTiffError as error: + refused[str(error)[:90]] += 1 + continue + out[path.name] = (x, y, dx, dy, page.imagelength, page.imagewidth, epsg, area, + nodata, source, str(dtype), page.compression.name, page.is_tiled) + for text, n in refused.items(): + print(f"REFUSED x{n}: {text}") + return out + + +def headers(directory: Path) -> None: + m = metas(directory) + print(f"{len(m)} tiles read") + for i, label in ((6, "epsg"), (2, "dx"), (3, "dy"), (7, "area"), (8, "nodata"), + (9, "nodata source"), (10, "dtype"), (11, "compression"), + (12, "tiled"), (4, "rows"), (5, "cols")): + print(f"{label}: {collections.Counter(v[i] for v in m.values()).most_common(6)}") + x0 = min(v[0] for v in m.values()) + y0 = max(v[1] for v in m.values()) + dx = next(iter(m.values()))[2] + off = [((v[0] - x0) / dx, (y0 - v[1]) / dx) for v in m.values()] + worst = max(max(abs(a - round(a)), abs(b - round(b))) for a, b in off) + print(f"reference node x {x0}, y {y0}; worst off-integer offset {worst:.1e} cells") + boxes = {k: (round((y0 - v[1]) / dx), round((y0 - v[1]) / dx) + v[4], + round((v[0] - x0) / dx), round((v[0] - x0) / dx) + v[5]) for k, v in m.items()} + rows = collections.Counter() + names = sorted(boxes) + for i, a in enumerate(names): + for b in names[i + 1 :]: + ra, rb = boxes[a], boxes[b] + h = min(ra[1], rb[1]) - max(ra[0], rb[0]) + w = min(ra[3], rb[3]) - max(ra[2], rb[2]) + if h > 0 and w > 0: + rows[(min(h, w), "corner" if h < 1000 and w < 1000 else "edge")] += 1 + print("overlapping pairs by (overlap width in nodes, kind):", sorted(rows.items())) + span = (max(b[1] for b in boxes.values()), max(b[3] for b in boxes.values())) + print(f"union bbox {span[0]} x {span[1]} nodes = {span[0] * span[1] / 1e9:.2f} G, " + f"{span[0] * span[1] * 4 / 2**30:.1f} GiB float32") + covered = sum(v[4] * v[5] for v in m.values()) + print(f"sum of tile nodes {covered / 1e9:.2f} G ({covered * 4 / 2**30:.1f} GiB float32)") + + +def seams(directory: Path) -> None: + m = metas(directory) + x0 = min(v[0] for v in m.values()) + y0 = max(v[1] for v in m.values()) + dx = next(iter(m.values()))[2] + boxes = {k: (round((y0 - v[1]) / dx), round((y0 - v[1]) / dx) + v[4], + round((v[0] - x0) / dx), round((v[0] - x0) / dx) + v[5]) for k, v in m.items()} + names, done = sorted(boxes), 0 + for i, a in enumerate(names): + for b in names[i + 1 :]: + ra, rb = boxes[a], boxes[b] + r0, r1 = max(ra[0], rb[0]), min(ra[1], rb[1]) + c0, c1 = max(ra[2], rb[2]), min(ra[3], rb[3]) + if r1 - r0 <= 0 or c1 - c0 <= 0 or min(r1 - r0, c1 - c0) > 1000 or done >= 4: + continue + za = tifffile.imread(directory / a)[r0 - ra[0] : r1 - ra[0], c0 - ra[2] : c1 - ra[2]] + zb = tifffile.imread(directory / b)[r0 - rb[0] : r1 - rb[0], c0 - rb[2] : c1 - rb[2]] + nd = m[a][8] + va, vb = za != nd, zb != nd + both = va & vb + diff = np.abs(za[both].astype(float) - zb[both]) + print(f"{a} | {b}: {za.shape} overlap, both valid {int(both.sum())}, one valid " + f"{int((va ^ vb).sum())}, equal {int((za[both] == zb[both]).sum())}, " + f"max |diff| {diff.max() if diff.size else None}") + if za.shape[0] > 1 and za.shape[1] > 1: # the probe can fail: shift by one + s = np.abs(za[1:, :].astype(float) - zb[:-1, :]) if za.shape[0] <= za.shape[1] \ + else np.abs(za[:, 1:].astype(float) - zb[:, :-1]) + print(f" shifted by one node: max |diff| {s.max():.2f}, " + f"equal {(s == 0).mean():.1%}") + done += 1 + + +if __name__ == "__main__": + {"headers": headers, "seams": seams}[sys.argv[1]](Path(sys.argv[2])) From c2994255a6412906f0be3a83a7d72ad1676ecef9 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 22:18:08 +0200 Subject: [PATCH 04/28] 15: BHO CRS checked, ANADEM data licence still open; ROADMAP: increment 15 row and Ola's order (Norway first) Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 10 +++++++++- docs/increments/15-dem-mosaic.md | 7 +++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9998226b..4870ab41 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -43,6 +43,7 @@ increment that most needs a picture to check against | 20b | Minimum insertion distance from constraints: when refinement's worst node lies within ε = clamp(tol / slope, cell/100, cell/2) of a constraint segment, insert the foot on the segment (off-node, bilinear z) instead; a footed node still above tolerance is inserted after all, so the tolerance guarantee is unchanged. Removes the 0.0117° needle at 1 m (increment 20's C4) | shipped with 20 (#97) | `docs/increments/20b-min-insertion-distance.md` | | — | `tools/bench.py`: the 1 m benchmark and the thread-scaling sweep from one checked-in command, with power state, quality and commit recorded per run; the one-off scripts in `docs/benchmarks/2026-09-26/` are its specification. Rule 2's acceptance run needs it | shipped with branch `tools-bench`'s PR | `docs/benchmarks/bench-py.md` | | — | The serial phase: profile refine's serial insert-and-flip phase, then parallelise what the profile blames. Scaling tops out at about 2.0-2.2×. Profiled 2026-09-27: serial part about a third of 1-thread refine, mostly Lawson legalisation; the scan stops near 5× from load imbalance | profiled; designed as increment 21 (Ola's rulings 2026-09-27: L1 determinism, one path, at most 2 % more triangles). **21a shipped with branch `increment21a-quick-wins`'s PR**: dynamic scan blocks, active merge, reused flip stack; mesh bit-identical; on AC refine -10 to -15 % at 8 threads, ceiling 2.1x -> 2.3x (`docs/benchmarks/2026-09-27/21a-acceptance.md`). **21b shipped with branch `increment21b-lattice-incircle`'s PR**: an int64 lattice incircle answers 99.97-100 % of refine's incircle tests; mesh bit-identical; on battery refine -15 % at 8 threads, ceiling 2.3x -> 2.5x (`docs/benchmarks/2026-09-27/21b-acceptance.md`). 21c measurements next, then 21d | `docs/increments/21-parallel-refine.md`, `docs/benchmarks/2026-09-27/serial-profile/README.md` | +| 15 | A DEM in several tiles, and the domain in its own CRS: 15a Norway, many tiles in one projected CRS (Ola's 254 DTM10 UTM33 tiles; only the selected tiles must share a lattice, since 8 of them sit half a cell off); 15b the domain polygon reprojected into the DEM's CRS; 15c and 15d the São Francisco basin (geographic DEMs meshed in the DEM's own lattice frame, not resampled; window decoding and memory) | designed; 15a next (Ola, 2026-09-27: Norway first) | `docs/increments/15-dem-mosaic.md` | | 16b | Interior polygons and polylines as constraints ("terrain polygons": lakes, land cover, roads, rivers): `--features PATH`, a GeoJSON `FeatureCollection`, each feature naming a vocabulary property; closed or open `Breakline`s, crossings noded, off-node vertices with bilinear z. **Its working example is real data** (Ola, 2026-09-27): CORINE Land Cover 2018 over the benchmark tile `7908_3_10m_z33.tif`. The source is Ola's local copy, `rasputin_data/corine_sql/.../U2018_CLC2018_V2020_20u1.gpkg` (8.2 GB, EPSG:3035, a sibling of this repository, which also holds the 254-tile DTM10 archive for gap 6). It reads without GDAL: sqlite3 over its R-tree, the GeoPackage blob header stripped, `shapely.wkb`, then `pyproj` to 25833, so CRS stops in Python as before. Probed 2026-09-27 in 0.2 s: 60 polygons in 8 classes (heath, bare rock, sparse vegetation, bogs, intertidal flats, water, sea, urban), 11 068 vertices clipped to the tile, median segment 54 m against 10 m cells. The EEA's public ArcGIS service (`image.discomap.eea.europa.eu`, `Corine/CLC2018_WM`) returns the same 11 068 clipped vertices and is the route for anyone without the file. What it forces on 16b's design: neighbouring polygons share their boundaries, so each shared edge arrives twice; the polygons run past the domain and must be clipped; the extract is committed as a fixture with the Copernicus attribution. Placed before 20c because 20c may split constraint segments and should be designed and measured on inputs that have interior ones | designed in 16's R6, no increment file yet; after the serial phase, before 20c | `docs/increments/16-domain-polygon.md` (R6) | | 20c | Soft quality criterion: a penalty that each Steiner node or constraint split must pay for in angle gained, instead of 20's hard 25°; applied at the start and during DEM refinement; may split constraint segments when that improves the mesh. Ola's rulings on 20's C1-C3 | to design after 16b (`@architect` measures cost against 20 first) | `docs/increments/20-start-quality.md` (Ola's rulings) | | — | Auto-catchment: the watershed upstream of a coordinate, computed from the DEM and handed to `--domain`, so a catchment no longer has to be supplied as a file (Ola, 2026-09-27: "not far into the future"). The textbook route is depression handling (Priority-Flood, Barnes, Lehman and Mulla 2014), D8 flow directions (O'Callaghan and Mark 1984) and accumulation, the pour point snapped to the strongest flow nearby, the upstream cells traced and their outline turned into a polygon; the literature check is `@architect`'s. Open for its design: whether it runs in the C++ core (a 10 m tile is 25 M cells); how a stair-stepped cell outline becomes a domain polygon, which meets input coarsening; and that a real catchment crosses tile edges, so it needs gap 6 (a DEM in several tiles) first. Legacy has nothing on it (`grep -rliE "watershed|flow.?acc|flow.?dir|pour.?point|catchment" legacy` returns no files) | to design; after gap 6, which it needs; placed after 20c, can move ahead of it on Ola's word | none yet | @@ -86,7 +87,14 @@ as #93. extent from its header, selecting the tiles that cover the area, refusing mixed CRS, cell size or grid alignment, and stitching the selection into one grid. Aligned tiles are one bigger grid, so everything downstream is - unchanged. Planned after increment 14; no record yet. + unchanged. **Designed as increment 15** (`docs/increments/15-dem-mosaic.md`): + 15a and 15b are Norway (many tiles in one projected CRS; the domain in its + own CRS), 15c and 15d the São Francisco basin (geographic DEMs; memory). + +**Order of work, ruled by Ola on 2026-09-27** ("My priorities are to get the +Norwegian cases sorted first"): 15a and 15b (Norwegian multi-tile DEM), then +16b (CORINE terrain polygons over Norway), then auto-catchment. The basin's +15c and 15d follow; 21d and domain decomposition come after. Open and not MVP-blocking: **inputs in their own CRS**. The user (2026-09-26): "I don't think the domain CRS should have to match the DEM CRS in the future. diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index d732634a..20280547 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -188,8 +188,11 @@ They are measurement scripts, not production code, and nothing imports them. nodes is @perf's (15d acceptance). Not measured: Deflate or LZW decode throughput for a whole tile, the triangle -count at basin scale, and the BHO polygon's CRS (SIRGAS 2000, EPSG:4674, is -likely, not checked). +count at basin scale. The BHO polygon's CRS was checked afterwards by the main +session (web, 2026-09-27): ANA's metadata gives SIRGAS 2000, **EPSG:4674**, +geographic. ANADEM's *data* licence is still not found: no statement beyond the +repository's MIT software licence turned up, so Copernicus GLO-30's terms for +modified data apply at least (its notice, plus a notice of modification). ## Prior art: legacy and literature From 351a57e0d4f199db44f19e27f1fdb5c975b59eba Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 22:20:40 +0200 Subject: [PATCH 05/28] 15: Ola's rulings on Q1-Q5 Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 20280547..3c098684 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -15,6 +15,18 @@ are last. Each has a recommendation, and the rest of the file assumes it. the basin-wide computation frame for the São Francisco basin (15c, 15d) are designed here but come **after** the Norwegian sub-increments. Nothing in 15a or 15b depends on them. +- **2026-09-27, Q1-Q5: "yes to all five, carry on"**, to the recommendations + in "Questions for Ola": + - **Q1:** two tiles that disagree at an overlapping node are refused, + naming both tiles, the node count and the largest difference. + - **Q2:** `--bbox` stays. + - **Q3:** the repository lives in `io/repository.py`, the one module in + `io/` that opens files. + - **Q4:** tiles with different NoData sentinels are refused. + - **Q5:** a request mixing the eight half-cell tiles with the main lattice + is refused; a request inside either lattice is meshed. + + Q6-Q10 (the basin) wait until after the Norwegian sub-increments. ## Scope @@ -933,7 +945,7 @@ that matter there. `docs/benchmarks//`, with the power state. - For all: every gate in `CLAUDE.md` §4 green, and CI green. -## Questions for Ola +## Questions for Ola (Q1-Q5 ruled 2026-09-27, see "Ruled by Ola"; Q6-Q10 open) Q1-Q5 are about Norway and are needed before 15a starts. Q6-Q10 are about the basin and can wait until after Norway. From 2696bc28ffe51536f642a699463870019f0debc2 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 22:43:46 +0200 Subject: [PATCH 06/28] red: increment 15a (multi-tile DEM, one projected CRS) The failing suites for 15a (docs/increments/15-dem-mosaic.md, "Tests for @tester"), with Ola's rulings Q1-Q5: - test_mosaic.py (invariant-critical): planning and assembly from RasterMeta and DemTile, a dict as the repository. M1-M15: lattice grouping and the half-cell refusal (Q5), alignment tolerance, bounds snapping, the cap from physical memory, order independence, the reference node, coverage, the overlap rule (Q1: agree accepted, disagree refused with names, count, largest difference; one ulp is a disagreement), mixed NoData (Q4), split and re-stitch, and _adopt without a copy. - test_io_repository.py: F1-F4, header-only and cached footprints, and Q3's guard that no io/ module but repository.py opens a file. - test_io_read_meta.py: G1-G2. - test_dem_input.py: DemRequest/open_dem, and real DTM10 extracts. - test_cli_mesh_mosaic.py: C1-C6, --bbox (Q2), T-real on the benchmark tile. Fixtures: tests/fixtures/dtm10/{seam,lattices}, cut from Ola's DTM10 archive (one release, N5) by tests/fixtures/dtm10/extract.py: 6400_4 | 6400_1 (51 agreeing columns) and 7707_1 over 7707_2 (5 m apart), 0.6 MB. Every module is imported in a fixture, so each test fails on its own and collection is unaffected. Run green against a scratch implementation that is not committed; test_mosaic.py killed 39 of 39 of its mutants. Two corrections to R4 (selection by index window, coverage per node) and the open names are in the new "Pinned by the red suite (15a)" subsection. Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 130 ++- tests/fixtures/dtm10/extract.py | 110 +++ .../dtm10/lattices/7707_1_10m_z33.tif | Bin 0 -> 31660 bytes .../dtm10/lattices/7707_2_10m_z33.tif | Bin 0 -> 32113 bytes tests/fixtures/dtm10/seam/6400_1_10m_z33.tif | Bin 0 -> 276721 bytes tests/fixtures/dtm10/seam/6400_4_10m_z33.tif | Bin 0 -> 280085 bytes tests/python/mosaic_fixtures.py | 157 +++ tests/python/test_cli_mesh_mosaic.py | 369 +++++++ tests/python/test_dem_input.py | 254 +++++ tests/python/test_io_read_meta.py | 120 +++ tests/python/test_io_repository.py | 289 ++++++ tests/python/test_mosaic.py | 901 ++++++++++++++++++ 12 files changed, 2328 insertions(+), 2 deletions(-) create mode 100644 tests/fixtures/dtm10/extract.py create mode 100644 tests/fixtures/dtm10/lattices/7707_1_10m_z33.tif create mode 100644 tests/fixtures/dtm10/lattices/7707_2_10m_z33.tif create mode 100644 tests/fixtures/dtm10/seam/6400_1_10m_z33.tif create mode 100644 tests/fixtures/dtm10/seam/6400_4_10m_z33.tif create mode 100644 tests/python/mosaic_fixtures.py create mode 100644 tests/python/test_cli_mesh_mosaic.py create mode 100644 tests/python/test_dem_input.py create mode 100644 tests/python/test_io_read_meta.py create mode 100644 tests/python/test_io_repository.py create mode 100644 tests/python/test_mosaic.py diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 3c098684..8f6d592c 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -402,7 +402,9 @@ eight odd tiles. Here: that is (x −100250, y 7950250). The reference depends only on which tiles the repository holds. It does not depend on the request or on file order. 3. **Selection** is by the request's box in the DEM's CRS. A tile is selected - when its node rectangle meets the box, closed on both sides. **All selected + when its node rectangle meets the box, closed on both sides (corrected by + the red suite: when its node *index range* meets the snapped window; see + "Pinned by the red suite (15a)"). **All selected tiles must be on one lattice** (`refuses_mixed_lattice`). The message names a tile from each lattice, and the difference: "7707_1 is 0.5 cell (5 m) east-west off 7707_2's lattice". Different CRS, spacing, registration or @@ -429,7 +431,9 @@ eight odd tiles. Here: The check is shapely's `covers(union of tile node rectangles, needed)`, in the DEM's CRS. It is exact enough for rectangles on one lattice: every - corner is a node coordinate. + corner is a node coordinate. (Corrected by the red suite: the check is per + node, since abutting area-registered tiles leave a gap between their node + rectangles; see "Pinned by the red suite (15a)".) 6. **Refused as well:** an empty repository; a box that meets no tile; a window under 2 × 2 nodes; a canvas over the memory cap (R7). @@ -888,6 +892,128 @@ that matter there. `meta` included. Tiled and stripped micro-TIFFs. - The mosaic built with windows equals the one built from whole tiles. +### Pinned by the red suite (15a) + +Names and behaviours the design left open, fixed by the red commit's suites +(`tests/python/test_mosaic.py`, `test_io_repository.py`, `test_io_read_meta.py`, +`test_dem_input.py`, `test_cli_mesh_mosaic.py`). All were run green against a +scratch implementation that was not committed; `test_mosaic.py` also killed +39 of 39 mutants of it. + +**Two corrections to R4.** Both were found by the scratch implementation +failing M1's area-registered case. + +- *Selection* (point 3) is by **index window**, not by the tile's node + rectangle meeting the box. A tile is selected when its node index range + meets the outward-snapped window, closed on both sides. The rectangle rule + leaves nodes uncovered: area-registered neighbours at x 0..50 and 60..110 + with `bx_max = 55` snap to column 60, which only the east tile holds, and + that tile's rectangle does not meet the box. +- *Coverage* (point 5) is **per node**: a node of the window that no selected + tile covers is uncovered. The design's check was shapely's `covers` over + the union of node rectangles, and it would refuse every pair of abutting + area-registered tiles: their node rectangles are one spacing apart, so the + union has a gap where no node lies. + +**`tin_engine/mosaic.py`.** + +- `ALIGN_TOLERANCE = 1e-6`; `MosaicError(ValueError)`; + `physical_memory() -> int`, which is `SC_PHYS_PAGES * SC_PAGE_SIZE`, looked + up at call time so a test can patch it. +- `Bounds(x_min=, y_min=, x_max=, y_max=)`: a non-finite, inverted or + zero-extent box raises `ValueError`. +- `IndexWindow(row0, col0, rows, cols)`. `TilePlacement(name, meta, canvas, + source)`: `canvas` is in canvas indices, `source` in the tile's own. + Without bounds, `source` is the whole tile. +- `MosaicPlan(meta, reference, window, tiles)` is a frozen **Pydantic** model, + because the tests reorder `tiles` with `model_copy`. `reference` is + `(X_ref, Y_ref)`. `window` is the mosaic's first node as a global index, + plus its shape. `tiles` is sorted by name. A tile's global origin is + `window.row0 + canvas.row0 - source.row0`, and the same for columns. +- `Mosaic(tile, plan)`. `plan_mosaic(footprints, bounds=None, needed=None)` + takes exactly those parameters. `needed` is a shapely geometry in the DEM's + CRS, and it is closed: a node on its boundary is needed. Growing a domain by + one cell is the caller's job (15b). +- **The cap** counts **4 bytes a node** (float32), because headers do not give + the decoded dtype, and refuses when `rows * cols * 4 > physical_memory() // + 2`. A float64 mosaic is therefore under-counted by 2×. The cap is on the + window, not on the lattice's union. +- **The mosaic's `RasterMeta`**: `nodata_source` comes from the first selected + tile by name, and `vertical_unit_assumed` is true if any selected tile's is. +- **NoData against NoData:** two NaNs give NaN, and two sentinels give the + sentinel (I5 needs both). NaN against the sentinel is NoData, and which of + the two is not ruled, only that every order gives the same answer. +- **What messages name** (a case-insensitive substring match): + - a lattice straddle: both tile names, `0.5 cell`, and `east-west` or + `north-south`; + - a CRS difference: both EPSG codes; + - a spacing difference: `spacing` and the odd value; + - a registration difference: `registration`; + - a NoData difference: `nodata` and both values, with `None` for an absent + sentinel; + - an overlap disagreement: both names, the count as a whole number, and the + largest difference; + - uncovered nodes: the node bounding box of the uncovered nodes, with each + coordinate written out, not in scientific notation; + - a changed tile: `changed since it was listed`; + - the cap: `--bbox`. + +**`io/models.py`.** `DemTile._adopt(meta, array)` is a classmethod. It raises +`ValueError` on a shape, dtype, ndim or non-C-contiguous mismatch, sets the +**passed** buffer read-only, and keeps it without a copy. Only `io/models.py` +and `mosaic.py` contain the string `_adopt`. + +**`io/repository.py` and `io/geotiff.py`.** + +- `read_meta(source, *, nodata=None)`. +- `TileFootprint(name=, meta=)`, where `name` is the file's name + (`path.name`). +- `TiffDemRepository(paths, *, nodata=None)` and + `TiffDemRepository.from_directory(directory, *, nodata=None)`. Construction + reads no file. +- Refusals: + - two paths with one file name: a `ValueError` naming the name; + - an empty directory: a `ValueError` naming the directory; + - a file that fails `read_meta`: a `GeoTiffError` naming the file; + - `load` of an unknown name: `KeyError`. +- Every `open` in `repository.py` uses mode `"rb"`. +- No other `io/` module calls a file opener. The guard is an AST scan whose + own scanner is tested on planted source. +- The docstring of `io/__init__.py` names `repository.py` and no longer says + "Nothing here opens a file." + +**`tin_engine/dem_input.py`.** `DemRequest(sources=, bounds=, nodata=)` is +frozen. A directory mixed with files, or two directories, raises a +`ValueError` whose message contains "director". No sources also raises. +`DemInput` has `tile`, `plan` and `label`. `label` is the directory's name, or +the stem of the first file as given. + +**`cli.py`.** + +- `--bbox` needs a short `metavar` (the scratch used `BOX`). Typer's default + `` widens the help's type column until + `--no-constraint-feet` is cut off at 80 columns, which fails the existing, + unedited `test_cli_constraint_feet.py::TestTheFlag::test_the_help_names_the_flag`. +- `--bbox` without `--dem`, or with an invalid box, is a usage error naming + `--bbox`. `--bbox` on a single file meshes a window of that file. +- **`dem_tiles`** holds the selected tiles' names, sorted and `; `-joined. It + is recorded whenever `--dem` is a directory or several files, even when only + one tile is selected, because the label is then the directory's and the file + used must be on record. It is never recorded for a single file. +- The `mosaic of N tiles, R x C nodes; ` prefix and the stderr line appear only + for N ≥ 2. + +**Fixtures.** `tests/fixtures/dtm10/`, cut by `tests/fixtures/dtm10/extract.py` +from Ola's archive (`DTM10_UTM33_20220924`), one release, © Kartverket, CC BY +4.0: + +- `seam/`: 6400_4 | 6400_1, rows 3072-3327, 307 columns each, with a 51-column + overlap. It agrees bit for bit, and a one-column shift disagrees; the script + asserts both. +- `lattices/`: 7707_1 over 7707_2, 96 × 96 each, 5 m east-west apart. + +Deflate, 0.6 MB together. + ## Test data **Norway (15a, 15b).** diff --git a/tests/fixtures/dtm10/extract.py b/tests/fixtures/dtm10/extract.py new file mode 100644 index 00000000..3206b0ee --- /dev/null +++ b/tests/fixtures/dtm10/extract.py @@ -0,0 +1,110 @@ +"""Cut increment 15a's real multi-tile fixtures out of Ola's DTM10 archive. + + python tests/fixtures/dtm10/extract.py ../rasputin_data/DTM10_UTM33_20220924 + +Test support, not production code (`15-dem-mosaic.md`, "Test data"). Every +window is cut from the **archive**, never from the committed benchmark tile, +which is a different release (N5). Source: Kartverket's DTM10, UTM 33, the +archive's 2022-09-24 release, © Kartverket, CC BY 4.0. + +Two fixture directories, each usable as ``--dem DIR``: + +- ``seam/``: 6400_4 | 6400_1, N3's first edge pair. East-west neighbours whose + last and first 51 columns are the same nodes. Rows 3072-3327 of both, and + the 307 columns nearest the seam of each (256 own plus the 51-column + overlap). Every overlapping node is valid in both and equal (asserted here). +- ``lattices/``: 7707_1, one of N2's eight tiles half a cell east-west off the + main lattice, over 7707_2, its aligned southern neighbour. 7707_1's last 96 + rows and 7707_2's first 96, so they share 51 node rows (in y), in columns + that meet in x but are 5 m apart. + +Written with the tiles' own georeferencing (tie point shifted to the window, +area-registered, EPSG:25833, NoData -32767 in tag 42113), float32, Deflate, so +reading them needs no `imagecodecs`. The file names are the source tiles'. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import numpy as np +import tifffile + +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE.parents[1] / "python")) + +from geotiff_fixtures import ( # noqa: E402 + GEOGRAPHIC_TYPE, + GT_MODEL_TYPE, + GT_RASTER_TYPE, + METRE, + PIXEL_IS_AREA, + PROJ_LINEAR_UNITS, + PROJECTED_CS_TYPE, + micro_tiff, +) + +EPSG = 25833 +ETRS89 = 4258 +NODATA = -32767 +KEYS = { + GT_MODEL_TYPE: 1, + GT_RASTER_TYPE: PIXEL_IS_AREA, + GEOGRAPHIC_TYPE: ETRS89, + PROJECTED_CS_TYPE: EPSG, + PROJ_LINEAR_UNITS: METRE, +} + + +def cut(source: Path, rows: slice, cols: slice, target: Path) -> np.ndarray: + """Write `source[rows, cols]` to `target` with the window's own tie point.""" + with tifffile.TiffFile(source) as tif: + page = tif.pages.first + tie = page.tags[33922].value + scale = page.tags[33550].value + assert tif.geotiff_metadata["ProjectedCSTypeGeoKey"] == EPSG + assert int(tif.geotiff_metadata["GTRasterTypeGeoKey"]) == PIXEL_IS_AREA + assert page.tags[42113].value.strip() == str(NODATA) + array = page.asarray() + window = np.ascontiguousarray(array[rows, cols]) + x0 = tie[3] + cols.start * scale[0] + y0 = tie[4] - rows.start * scale[1] + stream = micro_tiff( + window, + tiepoint=(0.0, 0.0, 0.0, x0, y0, 0.0), + scale=tuple(scale), + geokeys=KEYS, + nodata=str(NODATA), + compression="deflate", + ) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_bytes(stream.getvalue()) + return window + + +def main(archive: Path) -> None: + name = "{}_10m_z33.tif".format + rows = slice(3072, 3328) + west = cut( + archive / name("6400_4"), rows, slice(5051 - 307, 5051), HERE / "seam" / name("6400_4") + ) + east = cut(archive / name("6400_1"), rows, slice(0, 307), HERE / "seam" / name("6400_1")) + overlap_west, overlap_east = west[:, -51:], east[:, :51] + assert (overlap_west != NODATA).all() and (overlap_east != NODATA).all() + assert np.array_equal(overlap_west, overlap_east), "the seam must agree bit for bit" + # The control: shifted by one column, the overlap must disagree somewhere. + assert not np.array_equal(west[:, -52:-1], overlap_east) + + cols = slice(2000, 2096) + north = cut( + archive / name("7707_1"), slice(5053 - 96, 5053), cols, HERE / "lattices" / name("7707_1") + ) + south = cut(archive / name("7707_2"), slice(0, 96), cols, HERE / "lattices" / name("7707_2")) + assert (north != NODATA).mean() > 0.9 and (south != NODATA).mean() > 0.9 + for path in sorted(HERE.glob("*/*.tif")): + print(f"{path.relative_to(HERE)}: {path.stat().st_size} bytes") + + +if __name__ == "__main__": + main(Path(sys.argv[1])) diff --git a/tests/fixtures/dtm10/lattices/7707_1_10m_z33.tif b/tests/fixtures/dtm10/lattices/7707_1_10m_z33.tif new file mode 100644 index 0000000000000000000000000000000000000000..88c43a0a97d61ce632cb7cd10ebc97de318ac9e4 GIT binary patch literal 31660 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 adz(Mj!^PRZ_G8Td{0Uc=@c#gN95uQGVV=VP literal 0 HcmV?d00001 diff --git a/tests/fixtures/dtm10/lattices/7707_2_10m_z33.tif b/tests/fixtures/dtm10/lattices/7707_2_10m_z33.tif new file mode 100644 index 0000000000000000000000000000000000000000..7cd215b113a20ce38d08ee0361b3911834009a61 GIT binary patch literal 32113 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 I2WaPtsU`TJzyJUM literal 0 HcmV?d00001 diff --git a/tests/fixtures/dtm10/seam/6400_1_10m_z33.tif b/tests/fixtures/dtm10/seam/6400_1_10m_z33.tif new file mode 100644 index 0000000000000000000000000000000000000000..3c5310d65ecd9124ab6d8021d0acb44862361656 GIT binary patch literal 276721 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 o*6|Vf7Je4d!SVlAak2RhezoQ_ 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

8; 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%0AU8p36sN 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%0A(Ih-lKHe&te{5pt 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 literal 0 HcmV?d00001 diff --git a/tests/fixtures/dtm10/seam/6400_4_10m_z33.tif b/tests/fixtures/dtm10/seam/6400_4_10m_z33.tif new file mode 100644 index 0000000000000000000000000000000000000000..2c799ff1bc0029bab2a83b6ac8a83c16fc5abf01 GIT binary patch literal 280085 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

  • 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

    _xhxIUuaY>kZXj-D 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 UPXnin){uCQ?FCuzU|}` zv63`4$zMQPh6(9xAGl&4Tfb(2e}G(3YGQ{yQ2N)SmNbE6|C?Wv%^lu|m3 zXd&MiANwg#)UUxLV=qPecO_}XBuP5>NQNo{RmsytgOo=r(~Mea^4uj&ZyTg3szaI# zW4SNeE>G;GAqAWNsTXueInRJf?(5O+3HoGzQkyd8>r(I&ZSp>#N!}$Y zw5CU%wwEhXpBg2a@=TW8b5&{dR((ntr%TrU+SIP7NozT`_FmEBwM&~;{ij8N4LmP} z@%)#fNi%p>ikzZNJGWTT#9QNe=ZvS`%LQ~rris2}OeD{Klc|626w0faLN9JiChKR@ zDC^gB8ozZ0{d1o|SKX(PLCI9o&YwtgUKG$mMF)~uph@@K`6lsPgK%1rz;mY4W<8R1 z(kC&0j!tu?l_zP^owr(a%)yAx-Wg8%oNrt6hLQTVVH7=T7|k^*g|&PUdLI?QdvXEf zdAGE`T!e<1Wf(fR7CF*&csaBdwm++J_+J(3URATx9+E)?}`+p+)0Mvh(u&fjfbgs9Q^s~SfBG}u|*u_mc^kX zFAirM;xP~LP>76&qgygm#--r>(j+v?Ccy0o&skZ#CtAcJ`UKxN*NNd;#5raXi<=k3 z*sK?ehsR?fZN<;&zchS`Nr&a!G>lr4hO#l4m@zOL<*m6mv9Ay}t`ifd>xJAWib$v#Mm}63Nubd z!R<>Vn(l|AFT!E+I0khgEZu(r=ig>8K;gVVZs33 zTaDteia!T)N5{ZdIUIZ11CUnag8(y>_T=ZW`@UMRRS9>X8|;MysFT$c^x&q)yOqyuZA@ zb8T=;%L?lpMq#$J3Fa#rVY1F}EO*vNReuA_+-!(pIwRrUHX6(QM?tawNaSakVV?Ua zj4d_c`?VoDLv-=bOatS0s$kh11+2Uz2i?fQ*ySpT@D&osv+jdu1Aen9U%#`}`Cr*k z^-t{8?AL5|{Y&=s>SH!D@D5ve^gs3}{yZDwafo#-+RlzmT*K66u4Tv6H!=2M9Xltx zl-&)V&$KtpU@OfgvPmkn?9$65c5LA|W*=k5o*q_Y{Y*ZSvcV~`FWX8IXBN|_O>^mz z?Kd+CDnPcWZ)D+oj3ic{=qmJH{F$X?zW?%PCYu#H_5|FinP~P ziVg@;bmW~RNo|m%O}$dI>6j9o*H@+mc|)k9P=;FeN>X5iG$kD7dF(lN{yjrT`Mx&k zU)QCC(BZTu$ABE1wMn%=l?HuKr?hX{H0-7hxqi_m_uD*+@gC{I_k;0*3hhZ$q;*d6 zWOZ~1jpIzZxnG-99%$2mJ$iISLX&(C>QL@24O%o>hl=`ZlF=L;3L9WR!BTuXtW=?A z<{A{-UxVtbjp?9y82LWPpc9W;>A;yT%01RiqmE3awOX|ze0LYFsnQtUZun=oM zmtfkd3V8NbBW*bM^+RefIkpN-4=a)Ls1hY1Rrs*G3VGiu(WG3C3zv$qj_(H6=>@3Q zFMz|80-OpifNyI)thVIf?Sd>M=%(Y~G48=~Qn6rH3WjK;;_|^1SZ+_jqrs^tnwbuH zg>0;;;@Rg`CW23-W6r`ftguak_JdSB9-IQ{x+G-A$K%!bI2bv_0Zp+mUBNl=Fb$*ULFxQr zn4T`ez`glM>&b;lV-~9Uw)tvGBKB^Jhq`GTzC96RL{T)xAC890{Yd2giiFmJC`^42 zfu>!2*W42h34h*~=fq-ZR6P3DC8OX>8umO%$F0NZcs?`}-rF;)tkM+W$7L71G;@J!mU>o5iUM(!P~PA*d}iWQIrkFwA-TatS#2@ ze0Jip1$LbtiRZN=u_|dee)ToPltu%b*=d0CtwzvyABl!Dqw!$;XlQL7jm;O#V5>A5 zkAIn9ldBbQ_;@voKGw_D z-CM{cX3t_q8J$ewd?hpPjbe|~!4!gxnfjigtj*^oiEkaC?Cg#7L}4*~O`Ay{Y&z&| zMia#)H_`ZiMI`dfq>VE}C|K5;4lH-4G7C^ym?P=mP^Z0JQdIq2k~-!|(gl$;wV#rq z$!WuA*~($G>dIi6&$HKN6W$?7n$`!%Q1Xr;^y-%kNr%YLu4OXxdb~c}ZPla4Et)j% zfj+rE(xuybHR-jB28|r9PBZxqxW-J4G`;yQ`CFBCx2n>qb`^54P^94P!^k*RmOR_F zX~zotVS*BU8qSn zBGu?{;V61okU(b(is|f{I%=}-q?r*@=wjw{ijnFe?d0j?l{$?+PM=EoPp4ALl&Lh> zc?x0jBuX-`pouV}yBJO*@9EO$SVP*DZcJk?snensby6=^Bb6Ktx^1sVTh{5*xqb#@ zZf8JOiuGyCH65BeLziqUb!f_t;dJ%IaO(NN+4ryjD$5Gc*;|0t!v*-^@juDBSNwiFXIyMTa$?`Ytmuwk&1-g6g&=1#q)g5r;91L>zIn}xfxKKmJR6>IdC|^ zcg&?(JgH>DEF}|tzVo;1vS9l#1M<()pqrG8W8>oRQ#uaIrpCd3LmYPL#G&>o?_8(i zaJnQOMS+}O_u{esLp+?V6CqcUh@mGEapGwLR`C3#u{#zSpJVZPMl8=AJhOEqz+*!y zUVcr-xyp1bw$4EO@l-@wq~PR)6qxa>b|fMj4{zo|Z%_f&1QnyezZea@#aLuhj2|z3D%rtYoqWvItp1f(RkbwiTAT2V5S^_ zuH8{E<>w`mj>mY5cnH%|aAtZso~UHuUr7d@j?Kj39T^DRm5KuH6~oO_5X!sk=j1rF zR>ojqbOf{x1fx~g4=XjtWA(PN5Wglo-6+D|N>_LWx#IPCS8Od6p=ytS`(r)OZQ+Fr zBYjZz+7~hp1MxdD2>liWBI8g1*1Gv&caT3$7Y9IB6o`%10a(pBx31j}Ydm}~;;$Fn z;>SVYxlY*b3F$}=T%Y2GiuZ(lQH1uVF1Qxp1POCHL`d4g_q8o#W9%?&wl(jW=2)R^ zikmtkAUD7W;pv7rZe@trF~hO_sxi*Aj>0Z=GYrZzL&11+$a|S#M)FAXnQ9Evcmo(g z7cw4NkU67<(dJ5cy=EwO{E)$({gSAdF#rl*-|BDNCAmMN3kurX)RSm87Y+2GJRPNm_G6k|x}eq_Hcd zXxt7-3is#vOjeTm_m!r*BZtxNm%NAmkfYN>6luyzL%M&;h*l*T&@Tmjns{4--qh&Q zVSRnN`B9txj#H?v~^f>BC|Ln*Uvi`spgrD4w-s|7lQBjuJV% zQ6iHM8no$-4&7E)rL-9a)VWxLPVCX5A6X{s&XG2inYpkX47 z8k9~m=2?(-!zh}aYe9V~wJGneIyqWt(Zp@4w0y2QN!M!7b8B_-9j`<3=L|@Bnh}l7 z8bPJbMs#trK3$X1B*!{k>i(feCH?s(aJK;cHx)p=r2vMUX?0jp7_ToC-bjG|1JYquezMQ#*37kb-p{$=Gx#36eeum>0}D>f%^@=i6l3!C0()9ft?clkhAj9q0WrfGuhG z{xBK0b&}CBFd32GlkmMP8M`<0O~Q$1B*Pr6c#x0a>4h*YErNVU5wiXjV!RaZ<^2nx z-ajAPZ)RiVn>6%hB=9~M2ixKp{FxVx{&q1~njQtom?*579ErfW5!l)pfq_Ca_6>QgBOxKh`gCNEsD_Ub_hR z>IeD1-2UQjjzrh>auqgH`m!f|Y;SbG2`5*{S!;Amb@pd*&JoUv!%FjX%WZ z-QLCGXKiDz7p!Fu>z6Ufo~7(d&`Ne~+X}Yy_;Pkwy_fC#xsYj5E6a5(WYeAFSmXqE zHpjz^t#MLf{l*Psn$I6ndQ=M+F)uDd*+^tVipykID$#;etef80yk%?Ng z@02R_7aLHXpC-Lj(qLxdOX;Jx9`ftrNdUj(P-Q>rLb<^nj_DQ6qHJMUoO{BX2TBtE3p5zsX1~l8# z#Dm5(bH5s`lUAp!Y9*R*PniaeP^Z$P8npDb7HLIjQmUs01ux*cs?rG3RW+mg|3*?= zs|kgPP3T0HF};vCrLtWGz=8toEh~VilXt}G0;CNog0Q3n25M!{cw2_xP37qGpbV+E zOX1m40_9~zu%A*03#$TbdXR@F{d2HdISa$SW#FuA1}c2hartQ)Zk$ZR6ZbUybW6ki z8>y(#OM{zTI;NCnpy_ue3^}LPyXL`Ly#OzHAHDuG569ES&$|P16zmH4(LUoIWt5P^>JFeJ+aVPl*J zQa%7zr34(b5U}%*0NpiXF{;@eZ`hOHp~vj@FPLk zY8?n$-Z$5H`@#RM4_YVq;BA}_&hbOl%Ll`)ebB$w2j820agKZu&^#Wia=kE(xuZ*i zVNtCJwPIJ~G&|#29|wHiZ-?`+M}6-YbX~AQZjw1}hZ*C$wGsBb9*%;k!}<3@V_e^E ziU$9YSQ#@4lHQ|G$a`h@fl<(ZYXbFoBXDwuAv6c;V~D;s?7pkuR+19;2?}tTDu-bW zJeO^ef<)Fp9J}5RjZ6NqFRDM8aPbSP8}f-QO@6@w_g-ZYR>#?5nLSK$_-1CiX(co6 zT*zjfo5#{;E@Erqma@eCi`nL&h0I)g0aG=f#7dUtvd4;I_O)j`I}~ZhX2$BV{5!HN z=*CCdrS^<=ncN}k;fJYX;B4x%u7#?UD`>*L94Z)`L8gD>=;xRa+Ba+*8K~>gy8%*k z@#Fv+IC20zKRA#co*qQ+CJ&*v-U^g>M44Qxhtb<$X=;2cLz#zV$m8TN8kI1FTGfXlxE!<=AE-C4$OjpVk zsOf|fEsNHm10ys^d{v!V9kuC1s5U(gP~lxxfj$pbq-{skh~3hst%o&ezmg_de>9+h zd`}GNr$zQ{npD1CgS_u(lKl#Oy3wjlGuG*m+yx_AcEXrDcM+wxWzY-bMyj~mP3w+N zrSbQsQeMn7I`m;0y;tE!=u}c1I+@f5w$pjl0=gd*K^I;Ik^1$qv_;K=RL|+r+=2YQ zD;4P|e=a<)s?m^jYBb}D8r`krkH<}$Uia4}|Mj}GAYw=HabJ9{QK&!ZtYzHU7z%7@B}b z{S$EHW;}w=$3y9Vjz2ygHGB^^KPw*hMS?I`lZ4%$pYlD%EQ*d8E9CUjFTUepmiw; zCC8Fry(I}7S0%yidJ>kLO~dD-IhfNgA70b)Vc&iIdMGao|K0=Tr>hWW`8)&3)g36!eow z$5j0cY-vcxPvs2w{K>$cvJA|0OhmpKVHkzD4`C<@4S@Ve5BTzU zd8mL8TOy$S1z=<~4jwDL5V^t&W`DgfcFQ;f8+l=>pAQVp1CiJgjOnWa5t9*!K%W2v zm-}N&r5~K~{VE8#3$p5y$xAmr&+&BU&M)O=2YlJ?YBVd+460;>n z!{zcQOnJ>6*}PHkSTG73c_z~e8v(1|hIkOIhe6VMxPL|q$GVlVV7(H&$0_29`A`Hd zmBIHJ(l9X^1o@>Bh>ZHf1l_N!rS=m$vF;^X+xUPvNnU0N_Ybpa<94uzhc_^zfh*aH z!#!+9>m+tpZZ^C4dj^|vc{01}*~XY&72A9^k?AiCW;d$9Hi^fuNNWSOlgYEHlLJ`h ziPvPc@D;h)+^4^f*N}Wq7tNerNw;N+X!YAdI_#EAPY%Y=70Yq7>75PDWkcxHuYMGE zzCRV$4xry!s$}7+PXpqOXoik4IqV%y^UU>#rt8p{#acAyIp6X2>XB2HJn5Eck@qY$ zT6Ij%~h=4#WVc?MKDQG?F7b8lv)Pig(N zC^b}_`b^U#V?T9Dtk$HuNPTKD(xFmUJ<2(1KzC*irvYNn3+-rHk=98KN>gZ!-c-6K zJCzP{=f7jibPByYl?X1}#Jyj-I&G>`rzNSnG{1K^N&jcc_XJbEiH;=OE2gyP-AG#X zY9u}0QwU$qrsoocXt-2>wuyxZ{Zs_^ZAC~nFGi7e3Eo>4<3n)~Y$S@X?_?nyM;G9b zkPpj!Iau73g(=rEF+MXB`7<*x+aME5GqZ68Ir!3=gR5$}7`Pw@Du;5AaW@y2Cl>J6 zXFj?b@~~7QAMPUx@GPMSGYd-4(XSY-oTu_-d6;`YADe1&;Z>i8;@t_TE=|A)r35TE z7?19c@z}919x1)?_@)bSokmpWm-89wPm7Bg?B__ z-aG%rAxkw5i+=J=bbbmn7i43WZXO=i@O?5i9}hZmF>ZAhf+wdz?`$$&FGz;Q`y_a+ zOM<&=688BdK}M8>N&4w{KQARI+}%TzXS=g%|4uoQ;72N<5; zbw`}34-$R7v8U7<(+-YDiOYD@^6gUJj5A8?hhR;Al$7})Wr`p4()^Hg+z(#NA19{z zqbt}SrAPemf%A*xeUNd~8c~@Gkp1DhVAdokgsh9 z@A{EgS80sPeTL)hF+F&+YU5kI7B0L~hrfX;tc#Vf=7&5M-x-X;KhkJYkiv;a1L3+! z0?obOnP~1OW-;k}*~2w5?AVpQY|z%%BNeN9KltYVU!st}A57l%TlZCGg6@2ARN~#}y_UT7sHVvXnhlW$y z-;oqN-;`uCP3X%hGwN^cPHM--(eE02`flw)f3;L7c-Ih$i#H{4f+|_%s!>Y3IxT3_ zq)UOibWT-=Z<%^jd6qk~XloHud#I&{-VlU!{z zC@5T&mR?k%Oicy)_)3Xde(KV7z6BaDF`#z-xe|E>lypLeo{!NajeqL2Y?%iAKA=i% zjykm9gdQzyPex2GRdONC{z6_f}Zl|WwaM5JylblOm|CrL6d{sJdR*j~8)u2^o8g#f#jf4$q z6k4KAYnN(JN0>G(=3VnpwKk2g(5LnJM&v9qp@)M_DEQw98adR2oIjaRrgjm;hYGOk zPCnMO6`-WG5S~1nO*&MFmNSKjPANjiq9Tm3EQA?%Y8&t7dZ&>=6noXT*y0RG5Y5eW9jW;{5e?)^ND3> z8ean2O+^SvFNW{bVzevs{gZRZvpE~u=QD7wG!aq;2^g)C05KBKtCj%&`|{eB z#>44pJY+aGrfMa@K9ct`uWY<3<2h?x4$fHTaBrQB*D)Cg`IG`_o{=Oi zh#Q=afQ#7({l&X2fBZLe^09D6E_Yek&_B)dCGT?ICMTmiKN+9qCh;tmgu@z1=$W4g z4ZbC)jZ6N&&0yV!6ijC+s2P;VUw;X3>ywBq*%Z7_Peprk8fG|TV7+lBj=fF6%a_UU zGEBfV$r!wmje$DvyK=K)(c>P2Wh_E)M#tSyJVmnC5_K7CE?;Y5Cv`$`1$f1>m2!s zxgEOCa_(GWFQ=Sl=Fe_HmpVlRhNP;>UlX9~W+t-OSU}{_G(6bsQkY&8sPXM;9qftRx59 zEc)o1PYH^N^kGLZHTv7p#wpVDd!{;Fd?-cv^ZU`m@V=xM;YCiVV`+q<8NJjUO?^j< zq_KU*QebWbg?}!ln^k2ru%MjW-f7ZiJ0)7DrA)i*73sIE8i^07QDcQV)o;-vcT07; z%3t>uQCc)$rW#$#(xSFhMatYejBYu|QLfoAdVN8j=ILpY=`CHl@lBh4{#2%fCyJz@ zu0Z>Gm5H8fP}v)OVsShd9?>ONzF$6lp-NH4AgKmygrzEITzNIF0 zKj4q|qb7}5qD?*l`m``{I4Q~+Q-`K8Z7DV(3H^~2HQJ2kd@g|Q72c1e^KpT5sK>kj z6$kl&LKv+pz!{waj2c>qxZC--XOIuMX?ggfmj|unTx{ieq<(xhwhYRF=_c;Pxj!nE z|6fNfMBRu&$g=|cP0z#Q@B-9@l;Fh7a`--}#FVp@_&Bl(X$F;OdRd7*W)+Z%D@Chw z2`Uy9;_9-OC{iO+*eJTNRLJGY8rs9oc8oa)y!GmYZP5c}NhH-~BHVN}ri}A#nhx47W z2s{^$qpP_0%Z!75X)MGWVi7(l2K^t1Vbzj|0#n{~?_LQB{;4hs5_uNE8 z-{)OfE*4Emk?_$ELD67;ES%$x>ThnCt?G_xGd!WF>5Xq1emFG2AFnR><8@;IG}Z;+ zr%WK+a)Z!uDG-0{0>C8wQGcHEZo3}>ct-o#$+K9GAM8%~qHd!v)^djZnd=S7jot|J z9FG-xUYNg=ch@~0usG?4TLTzsG6Y=k=bPzA7x;#_z-zuU(!V;QN6P^_URuMU*#cYM z8YAR4&t%6&U}GTP0Q!x>*8!GL3$cdoEuPy>n8RGh3`Pq^!%%S~`l^pWM!i1Lp6j4* zKTXuds-t$YG9LHJ!(V3@S~Q1Z{)xfR@*4z~E&bpzw=a(T`^CP0|H5QU9I5g`mno-Hr~!8qkAEwTpLQiZ$#2b zu`6xdFpBC<_o3w zzjEp~q>L_h7gE-FPs(|sLVCH1WYH_fw@_sYKchjLcc{^D-v9c1)+CFsnsoHE7JWLc zLK0h5Xn3d!<(eqbzYWSHNGa3zX72mP=#sdUd-MTHw6w1R?OCKi^EfNU1?iA&p%zUV zU`RIC3`kQ^mlou5p8V9LkvG)nhrSxQ7OBwv<*IZeRF6WI{O|XO>CphbHx~TSq1EH` zNn(dB{rTlc_bw!n%BxA#sWF8%FDNZq;5Ek=6{$%-dfF+Jt%@yU92hEV>ktNXwr8jE&4e?mnQ7dqI*jlUzx{qMjpy9=0VjjA2a6W zVV*=DPF>5zg{QfA+b<8#obu2cp9l6Y54LsrFpJDZO=k`sEX_qwWIjH4<>UE@e8^-M z;E`bgEPZovvLOeGhWvWDV)Xr2hSxS#P`zD^Ugv6@n^}e1!)st{ScC27DzW`yB?cZX z!|G|puoLIvzZn@QRZhoi+Z4EcOMpRi0(@BlHjGR_-rxj$9LO&n5^(Z3XW7~m$a-a> zDwO*>%0iic7IZSR;4vc;HqmK#$yu~`GVgxdV)0ii0hdmuBK%Z37D{E}FZX_M465)9` z0Xr5aK=O71DtPvK?U{o4a;dnzClwD%Qu*_lg0jRUxT+=tHj>pWVL{$El zgb_SzO7o0a)EkfVx;VJX$0Fr*EF@0HA#!6p#vF-9#mPh{k4c5~w$0Id- z9IEv_@pb1|l(aCMza_%QIj)c>c7dj@3zQ0+Fv`~fUas~~NVh@8ax2WVGw08RF&2*> z0flkK7`DY2S=@=O`DcM2cg7%p*BBIhwt#Y^IR0D>k664`{iV&^rwtDy$E1UuiTh>qaDjRV$CK4#|;eNHK(^FQmTb96bK zTUSDB9>h>{$}kG`k)ga#LrJ1|7$weApzHx^^z(@pnZD4WE=6s68>>k<2bE~Z91@2hx(7zr`Go2bU%o1lAKLf zLUhP`wiXQ>twAw2RH$RHGG+4Kx?q_Wxy{idskb_`WuYeZ;|}fpLf!>`>65MBXbK$b zOuL%>>B^5n8mKmro_y%0=#t4)b$TjE{+LElqUoe)ASM3Xg%kf zxz9Ax%bG%pwjDHmaWVNy1QV6okP^+7yQ6gI=}2u-ch{x1K?bC8)sW0-1Z~zG zN%;pyQN)pEp7*)f$PdT9 zd6+Mgho?vK@ZF^l`X>sp`(FX%a`}ciCkG{0a$x_3bB%A0Yc=z6W_vzPZ_2|u`&_=O z=iuiF?#e>)(DAAS3tp6CU33)&&gHqRsS3%tRag*Mg}HkxG0e6C(G{gw=23);3v=Nn znT-~IzIl#I!6^Ggs3axeMs5Ozq$WVZk?#Pq33yYKfS`v7Sbi)SPr0ueq?Cc3FVb){ zIUUb=C%aLQ2HR07n0-76$L$g@PAe9VTjO!*0`H`Sya!%PLlf_yjeRpw$65H~P!`5~85;hBB~}PVmj~0V_(H$JKs~U-c7{6nk3ArNQPue0+t!@ z$M-h@duGI8eOfHK`MKUsh{5{T(MYq1!J(-!m{k#jO}4R6JsyMceZukAJ_r@hd~xdK zco<*ugmR?^>VLRn@|f{B@i`dlj3cq5Ga7O$#L!+511FOxynPpm!CPZdBk)}Klz-hF zF^K&e138CiRIiRi)UPl&PUZY7358K@2xcD*#;ZpG=;`)H_*h>oE%(KY58jB5@WImy z-jG}8ftvdQ<}P-DnUxdDUpk;d)}B~-H?%}I&F-bj>b6CY>dMe zCUDz15{E;~5%ZrFK37|zL5?$zEKq;Y0_z@)205DIOsp~bOBi6~H*E~~tBE~dRq?T1 z0XZaxmxqVo!JolMPalM~j=$`##vhimL|Rjgi_vR(8*O16d2=6j<l%cgk@C zv`OlkCe59xMMJf;>A_}g@_MXIqD|TqoS{YjhqcJiQH!oOX_8l=Hd$-y&}sg8?Kf0O zE>(?89;?&VNh;(zM}w~S*Q383I+QS9gQohbQQQM%>OWM4WQS;z5N}4GUVD*YpLA-t zR7RItyGb!(3SC(>g^oy1CHbw>X!HGPH2T4GvOUs6brX8%{D>Lk#W^-tXF5$eJDrSL zcy?=^O0O1n(>(o2)T&cOS7ry&taDbBv|Ep+zR;$Rdv&PumjQJZ8q+Jsk<|aiXqx9~ zNkf9H>CO*Znls#yvzp2TX z`5_fs=ASpEss924mj+D6G6BMpZ`~{M6$S`Xd(DB*x=qv1nc$k0lQA*u%5!3C>WNP4P(P zOe^tR`a5j-B2rLe>|dD zM_+fA`QCyVH|nul5%O&9uKvtn(@z>~_KlLC-Jsc3>*!_wY4mJt2OS+!M{>>O^kr`z z>D~>cz;GXGoaIUDmw3|iA+9uWuo*q|89^RlBj}Atnl@zhqw|Vag}l&(LTCR*VcCbJ z!r{5gg|ws%g2ilE`nKPMPTw9)ahBHfu|0@9sE`t8*V65;Wu*VDnBtC?l0{1~?J*3X zCPg)RaC|Tg?vN!7&!MDHCQHj-$dYh$81*bvq&O8t(z!X5%I3?G_lTjCdSxidHON!n zRSGmPRGGHTQzXx4!$?v_j%L-%QAw#B`5afGk~3;_Cr^W-)U`?LgC;ffS0~k6RXR3I zg_iYIqG4h+n%JUEXDZc6W|%6?@=&7D@rrbDFy9#;sFD`Xb+i2hn(tUjL(X)P_!wm@ziHI-dKz^lOe4vg)5u6>I<4A2jf$OmXu^OQ)M7c4ynCimLg#cEtv{XG zzf7YuY11fD%-MHnDh)f>PL@HjWI1yb1*q%O_ltUD*J4QhuZ*CDn@5r;#hfPWu%y9; zcGR%bnP@X3m!-~B&AFlQJr^3MbMfy1XV#BgJbjsq@sDyb!}G72As-`cMRi$Ay@)DhJuxoOeI7 zar0$1F6_#NWk4?eX6GSlRv}`K7vjUMLiF}6f?7%;MhJQ6vnCUk9_i41mx7c-DcBRv zJ6?Gbyz7$ii+ic6ngsNjm;hDIjtvVF@nK&gn z>!adfCW^xyvpB4L8;h8`v5;1a!{C4fXz{+e&Lsuk%Tf_?Cl$;o1#|L}U>+QgU4gOi zZHs|^eKbC`M`6551ingyL#8nd(RE>XawQad9|S|9Fc9_T{t#Ncam{%=oD#f{66uZN zra&CEiA42Fp6g5!V8oxRZxTGqw#6ZNO&sQPzOMhp*=v!2ot)9I!t0Azy5sXW+f%vQ+fN$6Q;rh@ISH}5b)jn^$_%I&-#(HAqGdHZb zNLV;uKzX(Zd54@KH_`!T3~X^>q#dlk+CWn@65%N$@&3gqh?-5Iz26k}Zlhtpd^Dyx zn_*`tcmBram^{=Hi%*%suXH4eUzlR-EMxpM=6iv;KKBvYxMQe+@LehxrKEuGn}*<2 z_aMxd>W{@KeetvL4NH=Iz)BWhXYO97+3XdYSf|nwR%AP!l_)i`JpWAQb1j&uxVf;; z5~i%=y*g7e8qC&c|0B14AL+=FcjQ%lnjB`&A}Ll#)kP6>%_x`}egsgdvJYu5cc)Wt zK>IdXQ~g>y`uo6+^!3c?v6~i+8Y4$L*7qe_*UN&N!E~YeoJcs{Z7(RK3W8ipy3io8 zNT?Y)kmOeB(~JrO^0&33@+V?y+fYWe1Is99k;Gz=zv zkqqfikflQTp|r+bk-6Hj;Yg?;^Cw%H=48#I?=#`3FN(E0%=NjQ=iStV@iEg$u6i0BI5&-Sw@s&)qo>lorfKwJQ4ig{H-pAro=K)h_#W_UI{oE5OOu*T zhia#jPf8E{*vk2Ka1!0@mqv%OZK!X)CC#`uib9)AY5&)ebnLYSU2n9ZSyP?KDBOd_ zSO$<+jyD-z$VK|)T;vtx;r@_3WZcWe<&U|r=FF0R!jIcLn_bOCGk;&7_s&k$d<4JB z#lv?wkQkba-9K{h^;r%&UUJti%7yE!Jgiw&02ie~sC>!8@Fh8L)ycuLC)t=kF&ndF zv!Uji1qJT00vVY zp3mda=$g#)MlvELQ*n;>#!G!uz~YjSYnFf(lO)(KOTuZM|1=ZgVf`}>X{+L3-5$rc zh&V(@#esFlLG@i6`k&?A?t3yq?9-6+Dh;A%X&4lg4%O-;9Bqupl!37@ITH=79g#3S z8qT|HI6SU|;b(jphAa$4+>#Kel?CI+?*Q0(`r(ba4|a|5My7-}=06F^g+N0Z(Nx&9+FC4kl5gk)^6Z=AHv9J5wzW1 zku%O2qfa>?#?v0g*|vzDZHX5WX3*~9KJ2sw+*3#6gsB-Oe=|qq2y^V%Z;qW?%}~G0 z43!DynDTZsY-&s~+t3v1{>G>nFdV;C zXfl);O}-CDk^D(j+V)$L9v`|ORG6+620a=pIMjU><)}Oq88!V8O$;{_ROYP~Zhn3t z$nBG)IeoOq+Sq~OZi(r8-$Ls1p^$oi<&AIVU4 z<`6oZB13BYG9yx+G>lcqaIOj^9#f?BVnv#{co;R$97+L9j^?=!rSTg$!~BPmq0%ra z`z1#|l@-Z$lrps#sM6eORm!`lN>Sa))Gwg|9Ge~*uP}q;?)1=q zo-=63)*edQIfabc%gJhCI8BK1pbw59`%SL&?x&cxjH;lntJQQjsDUEI4dk`2iTaB2 zaNu(RKH3!GZF2!Syz^is%l$v+RR`~v_xa`J&OH3#Y>Ln>LidJ3tadEGCx?6ty~uN! zWg)zN7C`q-9=gXBVEfGiI1Vj@Rc-+$>*phLbRO=i!4mW*Dn6pX*dkKf5i;Q36uJ{5yEa+ftD z9h09XwS6ht2XEAVn9D`zh)bV5O zBYxb8LCfA4cyjkNgTMBz_;uO;#X$E+45T;5VEG!(znw8y!#$J#hIouMNaT!8!LQg9 zta_dTyiLNYJDi{C@o=BQJ0;I_hY!R<&YtJS{t2kz>}VStkBYx>cxM_1=VNh@-4l-= zyc_T4TZCSF3RK+Ecn3_yt+h#*u__T#E}VP(K3{B%LSN@dd>tNvt*gTE`%f6u7l)x+ zD-<*9LQu9l2)UaAFjC47DyMxgbA>m8{|msf!=dQ^H39=Y#IR_HLkT~JJJWgA~$`#bZ3rj0Gl~V>iVZl`KZ6k{C(eF_?EE8o^zW*xL{e*C}CWoEeJ42ZNEU6@&w4 z{jvC=KOQ#vBZd39^#LjpnfXZjK~5OI)?Hz|0cv)TUaZc#b(TzL=rs$w-XTHi6F> zW31UU0?Ii?__;+7_s3~tm8K?wy;LCqIo=UukXJMipYQ%*^EAJ+jPvi=r=j=Qi8<$( z%gF<5$;CBnM{WQHiMi|<$PycWlc(EbiV3&?MnJj0 zOuox@QR#t2^vklI)>L}a933}`%XFfyU`LX^J%SWB>d_-*9Xg+7K&M4I- zlg7!>uD@Rezrs1f`CcF4($mqx!#-a{KT7Y5RPH|#t-kn7v~)>|pwWFp81FNPo)+uS z+mGgSJu;ORb!F4}$b52MT0pmj_j%S^Es`=B%cgO}hHPwsamnl+~*$`U0Q<_{| zWGFs&7~MOqOc(4q$L=fA*NIBx)kldkG=}kRCr1*p!)VY?ISN@gl$;gh==&>KnxZF9 zs;)}Z(5Xr#8&#;uSc&|;D3d?b=xLZ5?Uz=d$G4TKub&!iex^z>@%pr6sUb<`n$RBu z3(~&iO*6b=$oN(qi7pjU(YJ}zuwyD+u$xQ^6(>_gVmCc|IE6;9oJO0DPNRc7Yo%|T zMn5&DQ@z7w zNNO?C?v-FeeJKph%i*V40U7xUIOdlkN3jq~-{&Ee?~=dvX5;hBEPT3{fw$Ar@pWk$ zMr5YpYr?#MgbPm^Q}L-T$05L2FiLr$r zpZ{?`^dSb%KF8qOP%$)B{`WUf#L#&kgWp$UaFb`ZlSld6yq7+|9fPk5}VJze_6Yy#h-&L1#pC(Ge)1~pqE>A$m(gduy%X1I!aSM4)8vZ;E z-nY0f8yFAum2s%N8iy?2feQ}Dq5GB?k;lbw=jSkgYz(yeL}O@D6#BVFV%z)(%zGYz z$og=!RfOQd{2=@s9{}BHe)uuj7gwcyk#`^jRRhCOt{sKHS7LDWWGq~(;$Z(W4pHOd z@kO08Y=0a|&&FYm0cR_JT%8NJlZ)e7bbU02KZwH5hzPK|VbFUS3eQ!+NV5yV!c&1r zeC`Jc-e<3m^279cU)X2+;CsP1oS6y4D-%w9b;W@hE+}qy#_NxcXf?6J*cW4P`H}@x zGt6Od%M4*#tl)OR3SM4gu&C4uPt>hoQp*`O%@T%YR@gV)3hLSX<0CC_JID;COHD9i zn-PMy7-EKk0YdF`a4S*^FLtU!rbq>U=MRIstTeVS=!f_EeNgNBkqP}?G6(fLOh)1i zTQ_JIgJ?aQZ`H+)T(4mH$CFrqlOKyQv1K3Sby?es!7Oh6Pg=kJ1r0lOllBcgNs~p( zC}U_hrMqU*zI<1zf9gmBAJ|Y{^hj!xH={j&4XMRLg~WO4)LyJcmQq@@)>Da=50sst-j^X5SWdU3(%LFwsz`*>PRCKcX*1uN2YLuklni zsgQ(01!O(7h%Bd)^JUtnj z{Y;ilc@3ov4nwJAjV#&c$Wq#iVKiJrk(_$v>GuFt>OWGQY&nGxV!yl|mbNU7JgJ6Q{Gnrh=r_y=jX>|C%sdSU`>}b+7I+8e@ z#+LVx%Ejp<-){zef6_zolY8jW))_Q1aTZnfnN2ij26g!PNuEi}TTfLi9p(8Uu`6!NzMA9ht@=&3Te zj40s_qkwPU1z1>B0HyjOl(-k;tw{+!c$7k2sSMZma-N+pMPf}ce0~%og@5e7A?46N zR)N{0YtS;V4(bnTp=n-=58o14mPD{jdP2ThLl5qHC5{CYlj72+B@v|u%^+Gy!cyo_7 zIvG26#3TGpEVho0MP!N?uNA~_8!bjZ&a)vOV{n=?DneO|(`sUpIrn_;qF~b+0qZ{DSidAd| z;5L6gGUB4KYIrnqx}uOfA__h3QHY)s0dMnge47;tr)9xN*%^qAk^acG^hLL?H?;D{ zV@BFI>_6*)IlW`iFN=`1L4;{>t`MwTur1jMo4p;eBgqzzIGd)nTmIj_5qAkO!-_BF zSZiX*cLXcAhL1r4Y%w9l8h6#kz_G^)Z%>Uu=x!@W{IW#Pb2D6cJrc`i89~)lA0~@+ zvF)1{z8+9V)L9jHJyb-d-7v^o4nm}h1RhrWVFs~pSXabDHvi8x#vbis(LYu(If+H= z>)%?|+CQD?c!sd|rYDSg%ienR9F=(OholW$>xS0aiHjvwjMmpLOLI*5G^lZNZO*Atn$Lzrr z|3r$u#z~XSJ1Oe^C_~e<6{+KwCT);Xp=m!ZVwbbGuk_2YcJCMid+?kba+D$(;mf!K52-A=8f{u;X|vhQ9hN?K0(YCR;? zcNtB*IiF@Nn@T;B)2LQ7mDDFqrSVzZ_kWm1aeSwATtAIE?50!0wdo}Dw1-aaor>lWpeEN6R_%&wV!O?ViOq&RJArH;amo%_RTw znG~rwi?*pvqEoZmX!)BenmHn!^!jJg=ue3>>R3H3yH$yo2P&b~w;b{POJI~xgg$Ev zu~N1e;Y&+keyIfA5~Zj+Sc>F_-5p+rKfWv=-wbzckVjV;Sd{@(3}kIO9HWUR~kry1{)}96xTSiD6jJyH*A7 zqsd~Vj29z-^KCQd-jZQr+~iyv^gITJ=VB0fD+XtHAHBriUo}~b3cfd@haWe1raB|W z_oZTZujQP0E#`SQ4*xQfp*=han;R4GGd3Q6{JN65IIJEPhguoVtZlI<8ORyv6$eF8 z0upqSkjHtLyqLSIX72u?*lZ3-sAwusv9U<_nnqYGOlE~2bzNn|` zx~QY-iOBcs0HKWg|C_(=3l&ELD5x=pK1Joz=|_e1C%cGVJgT75qg5nWH&6;1XjW(= z37L&Fb#4RMn>W#untJknQcd@L&4@LrkekXVa?CZOd4&q3WIu*FJ1ptTH(hEyG>kIG z4Wl7^8#tdQLub0A=%A}K?V2Y|<9ABYqJGk(?<+|<0|wLLty0wgjTHGlm8AX`d6v^x zqRZ#?=)%rmdcik|u9$3U+PRMUyuMC_B@g(0U!b*y2e_kJMJE-O^NnN)T}xO*kyGZ= z$D4Dg=GYwCwQe?j*O^U>XDy90vnYAtEV^Gmi$pcED5Po@&7C@n{=S<fk=~9g4`D@(LQuN(WhKW68Fx*xKyMLuP z5Lt%LbIK4FU5bqDC1|}|g6;p6LT*U~Hq};v+^eDfy9WDB>e2e99*6GKBYkiKqEqXk zT~vo>w`%d=L^c1uT7lP_N|4r2h+n?hxOP4jN90oQqA>|Q+Wd7~kca@zt>ar0Ffus- zzugnikADYWEhIp3b0S=iBwk0-$^EE(ByV!ZpybD3)_ z!mq_*+c=(YcpkgOdA87)_f1RQ-$;z9QJifhJhxSHcFhx`=`jDCZ9JRtoxmoKcgrisMRa~7lbVGO45>mJy0H}@?DoxIE5zZ8Qb+&z7oE9UN8 zjGT`N=rl;ek(r68?~?>01HM&+#KR~g4o`T;v+WZP078C&e=l)nf&mSlM1mM)yAY69| z#fp+}gglKvO`k}N;hD?md>j^t<6)r~hsg!eh`SXD|Ed@qQjfy9M-kXFCIYsn!y$P! z9BY<`<1*h#r)z{lwKNc=^L#P-_jp{39|s)=4@@Z@i)&pBsuM-Xy5kJVBThJ&?u7Xd z9C1j=5r3}O;`e83oUI;%=uK9fbC$3gX%6`z+>LEBM^C>oJm*^DWw9-MpW0)jydyf& z?IB}ejf!Jt2=_L}M{RRFmKw$T!AMB&;T<#95R%Vy@M44(URSDOLAE>=Mhr#3L}|1J z48V)*KP)ie4Xd}l&BA)mGl@%E*@MT6*^~ZLna0a{7QQHpEiVdY2?5Sb9AnHjPf=m_ z^d*@7?)&7NbeN1*ZlI&;OKH#Ci8N+h5)D7$MekpYA@v4B^6)dGAwJfWXKF=ftn^8B zk1F+^A4qqeeikx{UkfK6-xIFq&lQ%Z+X_|36a~kTQbPQMv!eM$XGQVJ=R```4@E0B z+!6&VnFtB*BL$&MM0;XV==+jN^7&RsJ&jrPA-t5F`D^r7W<6cJRYL>bx6{fU4didy zNZsm0fQ!_z{Q@OlJwxA*9j2qS zoz9hQAdMlLXlLKG^r?IuSzcRDPd99%ak@KcOxiAb>Aj7_8@JG;VH;@a)+J=Iau&^R zoKI;V=8&f0T#{cshkUNgqgnUn(W{2JROvj2ylQ5X_`_^kF>p4m`P51qy{oCI36{F$@l5tVcb38(ZW1=kNyHDQM5H}Pz(k&7WCIc) zt&sqQ*YUXjmv;%4SvE(EN4&r7+RG2aSV&98;@3VgW>t&PKSPXF zSz;t4advU0Im(D}jC-||cYH5c$G_&USX`5hg(r7(eU0PbSRrR zrt^=3Zv$>D0afW@4CFiE(nksSGCdL3f;giG@Ga4ZXUh59%MakJJ{b?YHQZ&rj>d-l z(L5W)VQE(qrWz;V)bK>~35bKk^GJMGh`{LyAt*l+fR?BLEVc>60mV=xbaIaE4S{!8 zC>GBQgM3E>va+IZV!ap>QsXe6=d;JdqH%tv7&7*8I9|ga*XS5rFX3_oD+Lo#F@O8$NiTG9C-wdt%3Rccjz-n?xAb&jq?I&M;o!h`0aju`Agg zLaQAlZ0w=4#s-^5jKQV77O)#?fvz5NNH?zK=X|`~Uu!W(C17_ZG#I=+5H~<^m zzBCG(UzlLTB2z>!Hii0ZV{{BMg8EQHtP0kJ$Xo+YTU8*hDUUDz4Mud&An5IpfNtzB zHdpoyb56R$j5Zu+r(SGgp0gJ+m9WW7c|`*oHz=3+^b2P~ku!@QZp!wIRb)Rh{*lFw z+vMeXkajLuLEptw$p1qHH8lFs=l{l#$xCgjn5|D^=U7nf)e$tgK#vx=snYrKU&4y5 zlJv>yt`L`3B`7*r3RbrAf`a^A(O{iDqV7#QMUJx%i2|cei^geRY>RZsaO*wmwYjZ@nOw>Nn&U@Rn{Td?C-Ck970SZ(3f| zhZUWDPC;6CNLBtnl6!KM8eZL_56>UaaHV@RMPWNx%F9O}KXo>cFy zq;;l?NnddR?NeJwH~-Ej>4Qs2w{9*K_spT&qvw+Qp-$S^*hKzInyKyUbXqxJK6US( zMUNkJlH7-dG<4rC8q-??{pvalZLPyfjavK)tww*}YW&+%4aXf-XxLhbPdOEs5yqVu zXPL|Aa(I_j!tf!_SHV?ybh-w%cj~ZxXFcqt8gb`SBWgODxQA-OH18(heIstZXn=HD z1CI5n$2gxFRGC)bt922+^9@y|D+}{~r{i2E@0wQ=5onZ%XZZ;jdORL0kH%w6UObkj z#lviNJf@zBN712pa}-n`MM9@G3LAnG;4v-<7o?JL=2rq5KE^^ZI2M=Y#X=!mjD0JkG1N2~Hj84g zsGUF7b;;;fPeRk$L|n9vher|bnaSaJI3p0xp9P?8eE>{PgyYesNMuGuASERXqd$k> zU0NtuTqJ@u#F!Bhi}YN+1GvOsszn^`K8eMS-Q3xk$6)~H^E{7OjP8y>#gs^%V(G5Y*B0uuX-(9~pvw%dk?>oUN!nYviINgcQPE2H9%92$aUFlOICT)5a5T@v5f zCF7Uu$AH@`Z}~|UcWXT}*u9V)cs-dpYc;X%syx>ACxZRF@5<)5o3a*FIW}VPHyR&) ziChnCqYI}NQ$ywiaw^EAQ+}>AOr%1EAC$@WwkkzmRHq>~hEl|GdD?hEhI(e675p`x z3R$-+h1}kug6o%4q9=CCMR5x2MR87>L?i$17QL)IDB804u*gdPglO}Pi=w2SXQH{2 zYlYX--Dvs&zQO&NO@l|(la5Xn4V*KMOiBXi)Y2f@u^^RLb^$f4tEQgMO*Ag3m9BZ! z(O5UW1MI1z>#qEB?pM=ePj9jhFs5r?%;=KNC^}*|l75<))3hQpYMW+GYDzX_zr=x> zFE~&~>u`G7ZbU;q8B=5K2r^U{N!Q1j(EO=pRHkS`w_h94&)37rY_<`V-$_F}$Z*SY`mM2qLbk1=Q=j(JxTdq@ zzH~piIPayFEjws)#%^lP*+!3qMYN)GAu0A*K&OWAeC9osCMHgx*Xa{U_Rt(UbAB$Z z;TwUlY8HvVY$1iKXKC~6dYl^7fK6xXamKzL`6YF5TUU#{uWN8}R1GG)ufqMi+^2f=3t6-HsW_?;uPW?K|I!M=g0hbcv;0m_|6@aOaiv*a5mNbj={q8SZv_^Yv<=UJX{oub9?yhdH?L;Tj$9Q+?QPy z;~{5ZD(|g&x3~-A{Hox$>E{rOv*$PutmE+ZXdKd}$3yyY9HtJ9=UyoR>nsxxxIF=z z;$qQ$F%ly_hx1$)fx0U0y6(i_RBtS-1kPs9cx>Vs?(8fvR37l|#~JqdZWQE|V~`OR zk5`6CFdmSExQIl!RmX!$)|q9W15&-G6KSkx?x!)wkj^LO!h9-e^PmhlMK7>l&Z7~E0ky9Uo|MU3;-EF3Q?f zZA}sU%os6FMi~3Y5O4bq$KFOme14(_)xjF*>Q=_dX$tUhltbAJX>3dDk5vu7+5WEA z?4ZJ37Sec*ofy889coy_s!mO279mZ{>~%iV`5wq#Om<@>bBx&WoT1F*&}UNpd5)g= zY#@K-nKYrcfz~AjkwUK(H7To*n))~4&g$=ikMcJ`(QhDiruCfOpSn_CTqhu6{O)Os2_rh;}Bx|8N>Px?`$PcefnNWIF4Qz#Zv;$eXD0AlibKwnqxSL zmMBlAi+KxZYTFVzW^tMp9s5C_H3l%*Es|`=m4R%}c^P(dzBY^hr_W|Q&}Evx^w{ev z17D)Qg`*1$F&YVy0 zmo6g@VK;dNU8EDnjmU~`#PyU047yN{J5lv0dsv4-8FlbmQj5Ug8o2q?BIZF2;-=SN z{KRUET2YP8cQsIJs)N+&Is`7S$D2NlNa306b7B*|EpI}HVH1W{HQ{nj6BO4s;Yf5N zI>PGl?RN!OVId+ja?!tE4t_;tWB%hTIIqdVlF6C48Iz1FFXLe?&AmVOWcm(?s2iJt zSyE}ZFoW*{8xqm!m4L#@NvIx>f<6{0m?%oapnvJurkaV5Thnp)Y8p1bPK0wR?|tQB zX!Cy8z%!4prxp&a+wDtr&#u7GuuPSoFM!=6Oqu#e7rzax)6uoMn2g(RgAQ zhYMp8q0e*N?^is3UX4Z8m1soNMf~62I+4~_1A{GVdzEPlz2uN0j zVh0PtAq#&DUg?9vDsMcPI1YJB$HL_R!$LQLw7^5!uUjCb%K}<)R=D%U2JSEIkQ!o-LIVe6M%zPguMIZMvqJno zGZ^k2iLnMIP&sXcGyO(jmxD3No*VM6X@Gr)bda=A9giH9vBO^uIYH70Up4?v#{Zb< z#!t-I`Z;?YafvP1dWa!%85_M|BHOyIo)tCbvhtmQEV9O#HAWh+Y34(iTGvM!HvJr( z`LmXMd?!-ujdWW2$dR05wCQpFP{IjmD!=+g$kM(jD7SP8_2*KBirOrpKxUNCM|dl0 zGMXuJKiMNHSTb8w(79Zc;J-@rYu0L!pX@%-V$(e$`-A&LtB$-A85b@S?MU?y3K~pk zTE%?w{j-{`OU$C%59+9X&BhM>-RJN#^-0v3BP3H=7*Quf#sH0QE zD(L+9S~BieK}X`sXy)n?x-L{xpJk1t^tqO%Rwt8Ik{$UhHK()B%xGzoISr8;O=C=* z$oaJc?Oo(RvbB!%VWT0nIT=ymY!f;fKZ2UZhf&Mj4$=)QrXOtyq)AESeQW|vH|wHa zw@Kui_~Z8Te4o>pEXEJGP}caEMtN?6SkSKqo+o(0i~na?wm1f z$|h?zC&+>Y{vE;k-PdJ(ChD^T_4=&OOf9xmO_k50vTS_&K-PI$l10}HWF0RDFt`2v zm_tAx7QEyy-TwEPTK+zwTdEhyMrS)+I=+Tls#a4??i7;VH=i7iE+S#lLOSZdly}Xg z(7|{p??#&KIHsS>LW-l+- zV}C~t&YmyFp~`&hoSF-V{W+*I$ictBJXrk8h4$raEOtvpj#nbi{z-!L++v^VuEFKet#+ z)Q*KF?_h6l#-hMBp7SXIG1n9D`cNX?K1qPzmIPR@NrX5x33Z|r9G{$mIo#FjUf_&N z5#!L~NTi4(uw!rpyFiPqKE8bW>c(F~*^D!?A9G0j8Mf!PQp_q6NyhH9{Vq<}wHml!R)a1Xk4k zWJO!wvPWtcS@gNx%%F4$yR>^UYYJ~*deM0-e^4|tizKFhZa6CskYx|wyr=Tylhka# zj0}?|kmQ$WIz8Kt5<+!p;A?4;50M}rPbr$*zYjHRxF95ovjy!DR>HP>pGBcZCx|R> zcZs?~W{I|qoF!_QJ4aM}c#+6{@p94J)@`CGrMpF!ghQg-%g04ax2+cKo^nWdI<|oV zM$M<5KmW@()9BcpW(qt~O$rU+qHWpC&?E+dQRY{9>my`H> zIepcxAc+Z8GY>q9x=S`#UrY9e)=T6#OznTAd_qigrgDS3@Ky%5Yv z?x-DgH#kz<3cd-D1v2N{R2J-?g1|q@t7lv+AIYzbBAvXBYV`pGcEm z_K^3P*>qEN25r7Nhpa2M(3kq_wB*)H()5#NNe8u<-jEUO(RMQyY%zwd-DAi8+wR7~ zzkq44aAVTDo!Q4WJ0{cQ$d328Fui0K*167>xu39P`D;vBbAt(+W3I!pdWNwHEqpv?p}~HT~-)Oj|@3&Mu*1&0doIxr%&GtfOv~ zEoAR=kXZlwl#PP=ckK^HH`i7wVC@_>BVoS}K5#ULLL~+T=NxcIMYmd#(BmWrrE&r zOCRpdE8{VoZ>kYXQqU%mj>m`7VVsnK2l1I`d6Nm@bt*g`aYt{G1pClLEL@g=WeVK? z_m9FczA5%|i-6LeaHw7ggB|zsX0Jmr)H4)ux?%7;9*#YyBXHq&BzD}7!q>|&D3=f; zxI&C`8nGDlC>jFi*p==utQ{TW3Y4!cU{wCvFb-0+;_wQ2cj|D zDhfYGN8x@;Bp&ugqWmz=UyoxTrdZfM;C{?Sj25qGR4j}_Xk8S>jf%vUec||v5J-*k z$MrtGc(HLjVrO{b$SgPflwmlh#rMlv2IqxtP}gz8s}O=@sT+Q82ZpR=Q1~lAd$tID zVq9=T+8%F4k3r}fOB~6!gePa(+Le|_QM87{Qyb(?w#DOmTeKP5;`~-?Z1?5ND=^3B znwS1cPr4$KB@!NSv<^-&;D6sL;Z~Vd}6RsRWqfy3@oqCFHQx#nJ5P0I*}9O_^n&Jd0KOM3%+-N!D;C zP0_8Qw{anqs_sBP=NZ!NP4aZU?vL=c;h7Ni=Ck0V^Pix*!C#no?vLoNXo?7V&7y%l z^`cE}r$r6l+ePYcx<&uiw2L|(&lOeCT2a>PJtAc*Nuf-qL!|THD&fr>Mj68<(;>dm z&Dk}d3_IJ&Vqzly|F<63iPYsjZ2!>|1Ez&wMIb=S&B6tZ3mUE82S5 ziq=Mrp~3TO7?IgXhL*zE) zG|iw}w1385eqPORtZatFpeD$;H^Qi~0o|+t?=ATz*|!0Q!t1eoLmeD_>R@74hcq3| zq^)((;T-dPQ_u5#1167Xz-*reT$|K@xj*Z1I;I|XKh!}8uY-DZ9p-H2o5JxbWE`qM z$Gjq>_shd&`CLpHlaHqwML1wsge|LbVUd%AoE>SfE6>1?{48`Y%z=qx7H-&O;={fS ztoxaX;sIIcnUIMm=2@`6m4)4bJdVcSoh6A_{=OAAIpZ)*&NKO&4uLH9H^LPVWDg$qCHbFC^!ifZ{zXLTZ}1n z(df31f{c75GHfCsV;X@zyEDS^Thv4d-VDvr)kShl>MHdcjT*yBK`P7K7_q#&Gbh`=NDXdIR1+4BY84tW1v$g|`dWig_D zM&tc}Xl%}i!s_-I>^6@9lZt}z`AEEd70DfF6dqpSZcQo%?`31)5FQ0}zX;6z9*hT{ z{g5@t7i*@DgVaEGjEVqOz9H<0&}b1H~_It)VwNy6@?1YU0Z z$<%+mW~q@^*m}SHtm48V_Q`TGQ@+u}9_kgczDFY2&~(OL3>(SR@5-^+p6|#*w3qDX zOeNEha@tT9NPP#n(7@Hkl$kV)j34%+D-Y%iFHA2B&%e$Swge3o^k!ZbHMzBj(&Sr2 z3Qv!UhB)06eVEfB+VHksbl0O<!i+pJcFw1?=0f^0yoOfm zR?{w(VsiP%Ipt7913ik!KdPAQ=2y_MlTCDKY9mQaY2vqOraj|Z=uvSKZLLTpzcse> zm~Wic*R4p)cpR-s&!U?aZM4y*i(Hes$X{*}$!AQWsYfS~v+^W5Fm57sDNm*MI@4&h z(q!sZn?eOAr_kq~IrLX+2bFz&PB9w?vsoFMY}FZK{@mI!$5vMscX%wDA0NQ(=ti=S zuf>c+F-)^KiY3WJvZ6IH?Ebz~_E9IDIe$)Ov){+D1Go5f24So<+>Lep=ftf4nX&t) zN3geIEmnMPD65*@pVdo#rqHTKRI~XUP56)hcN(yRs-_>H`M(a5^Pj^s`@<=UnR=Br zcs`@JtRLhUCdC$Ox8Ou}GnP+ig88yW{2Sbe(U=!J!e= zkXlsje-|ZFpi0W#>q$+Z3u_b!7%*yJQN!YLlAf+7>(zGFhzen*1iss$3g!57}99vgLVCKju?23(n9Pi<=x1w>$APNKa zh9UA)0Or>D;NQS;C{G%ThR+Q17844!8MG%coI1x)5yzl*3$VE0hUdrJkn8J)>;DmM z4HMuz#1*^R9KpWZA$yP=mK$3k_?RV<4p<^B*a}@@Ydp5IhCW&I^BRM*LDsP49W#e- zovnjMBPqlPa*OqF&RYj#zG~r`sx}Tn4CBkIdNwq$g){S+?3n=Oea?~XYtU!S*9Nl@Tb@v^@eXQI znnBY8D(F#BB*}dnPT`X3G`3+N8EpI|JRZMM*fDU0FnixLL3NRwVC2*#S{>0Ss_OGz zWOl7gG-2{$(f4&FqPFgA(N)PD(ZlcCMHV)PMOAYZg*C}@L}wZ{2otWm)5w@P^!@ET zs`|sT(8d)#5 zbt+P9y5{TFHNDO7gOl{ z^~qEfHktaSb<-S~$uv7+F1>tyor2{DFn1kwHvPC2>r?H-wjB3n4^= z#$+-jmpm4`-OBoo&Ds}x?!cZ`| z=MF5z%7XPM=%}cgnH`NNsLF%;ZJ{Z)(I`)`;z&8nCaPvyQXu=IC0eeyKr5 zMKz+=mt%HPA(nMy=g4Fy%%*z-0AyWZsCsbc}U zyYn!nPaceWb8#Rd7ZV%uF{&dUdFS%cFFv1dmD#YBPKDgP1pGO{9r!WMu){Ghyb=S~ zSN!d7e5?D#d)VI?990m*vmybnCvg6yWgzES8p0nYpujB-6FhZ_|=n#Uo01k8*(x1NFiN#r;s!Tmuz_VI0W#Dr+9of?IA#CzY`2(-Nj#dymQyt^KZXD5S^`6(Eil0$KF zO*nexA~0r01ROs{Ao55kLR*92btVAO-2qTt8i4b21Mp{VC{`bdg7|7Q@;^jlof6Mu z^J1Xi^Z$Pa<=oF0%(?KtjW!+&KgPqOEDq0WqhVVg3gvqtaNQjVTi#`rvc#Bvl>0WP zXp9ntV`Wbu;%a=+k~Iz+hK>a@W$0fkpnbXk-#9}1PzLWRhIespn8@?lW*;{kwE;rD z30P(DX9^86aM$+Cy3t!iM!{qx!DuOaNme~!%BdjxwnMUMGve<9u5 zTPas%3h9l?rc3Vb)L5xQvs5H$z^a$Rf1P)P@)wJQz@{eQ$A@;|=^I7ies!K`;+`gv zXWna(|G_#@AMe$op2Gzq?SqA)6(bu&?!rw`Lg5Ed&+vgl(dS>Hhe2|b_c?@$x|UJz zgM}2LF@XxUHPg*^ZPbw2L0^Y4q}eY1c>t7B-P#Y!l5@Zls~Ujbt^knG{>w$(uUJ zN^1g%?7C>xx``CLa3a-oPNX;ECz6`OM0(OQky@=L(WQ`yq^B~Gk`<@W{o|7HWMG}9j7&0bF= zCh^#w9d{bdW;*FHr#;H7WP>D|bo~=mZg@Z~?f0m>-xIPie?qrp-_owKchv6ji^e}2 z$Tk}*vXig%*sE*CY>rVICXQ>xnRP8FFlaEQ;79(EY%~?5Vcm{Ij0ld$lnXIPIvIoD6FhJ5P0yM0 zEaNuk+JhL}ei8%8k1-hkG7%pg()f9#!QVX*UJ`K_s~CqZ<_TEumVh1m;$X(&q1=>+ zff^aGtjfoutOD2=3iAe z_2#@bN`u+-6g2fq#`CfS4Br%m-^(NM?NvAq)^l&RAQUG3LZGrb81)B(k>eAFuUEp6 zCm)W{_rlQMFbo@>24h`h0ODo_U{*%}vb+Ouqb&f(y?IA{8;;yp5%^vefwk!os9zt2 z)}=A{RuhBm8L?QuDFGRm;*rPmW>IG>?(^;$k`scyz9Bei5re>eVrb>YphiCm-w$%W z?hV1N2LU);=>yYu<6s&KWPK9g<|$yy5W@cTZWz0HEC&1@3-v}fj2;D~zT;Z}|Jaf& z0bdPWaLmC8k}38WVQYsceQe=%kMEa5#z5A@8W!JeP~SQRU)rp2&&Lw({VWmSVu6KQ zEs&RQj>D3p@OiK?dgS%+ty2?YF6hF1pFYmC8iK9YgF~_=?!H#W55*xkvRx9%DiY99 z|Hu~3yT#fLUSR?MPP6$74zo4;*RZl{vzc+x1H4d@`xwj463cPFKzd9>zRC9M&X=-81&+TM^v`fHOZ zXLtt971fcWXFa{#UPV9qS5Xd?(`=h^%Fu40>)#t_1vOF?8tL7$X1XHLMj2{dwBf`A zGI#4F3*|1V*w;n-W>2L4C%Y(n`b0|I+(oLPlPGS?L|VxmS@7b?)bw!@Jvum;tM6wlgva@fm3MJ!&bl-ZQ!G4+Z>wlY12Y3Zf1dzTZLK7`$!@sqY0eV|CQkCbTlogUon!@h3s$M)3?VzPsV zvXj?TnX!iTvqD=JSkLw02obOtoy{JMISyVhf(LOmS+ z*5T#nI()ywFR#>L`QCaYmp5SE$VM1n;mPA$Bj zRYS+S3g32?p-i_JHadCu9LzIOZ91fs)1k6I4G#)4aLXhOJ!{f1u{{$@c+MGBU5Jt+ z#rV9W5LzgLr(O}H`xW8w+yacbl>_Y~nHYOL1^x+%xUxSEaXgdco#Xz8A2DZQ`0fyc zAbyy0y?#m zA@w*1H**TGlIO|Ru6%?R=i~R?d=#(9#k%RaxVJnPtEzKxOe+sTm-0{-l8y1Ol6OxQ8VIAq0Z>d1K{G(@H!mV zRH9+Uv+1s8G2ZxsoLl6Hk=uTuyaQ~<*yF@RTP$8O2HGmtm_5S|&;HsWe4-7M_;&eS-2!{v%rHII443WA zpzA#v3L}k?*lvJ1wi>veqlt^}b%DpaNEoAo!ed(4_(=s56NZ7Tq!47*5B=@mvB==N zEbqb<_U!OQHhjtn_VeQ&w$5(_GY{xt{ZF>Dj^*V{%Po?Heg#{!&xNHtH)2-5-q7xZ zsWdJwk_K66(QVUyw5;^GF!AyYVQIfb0#|K9s@OzW^I2bL`7%(@o@XGq?ottY9;gT! z1&yM(nBAfbvV|gTl`;`ZC4{79rh@*>t0LE9T|(Mn7g}Z3MT2+FrTKf? zN#C-VZp7wNr%oLyOpYV3Zd+RDr9%KsK(fEO3k=EpeFR-!mPNL&5@_n11kwpjq0?UJ zboD-W|K$yI>_I)rzpkVG0d+LMzk$5|Yar2zM*2CYk;e9KqW67T$m?PUnd);7hAwg# z*-1ASb&`@#C!N30NohkS(h}#1bfRt|&E~8Ne$z$j2fJu7Cey+2$yDaZw+W|7BrNEr zzuA+?ba*#iEbgXtyBE>-hwte9EG-slXT_9m5WD8fJ=dghY^h}iJNYY~B{-xpFNrdy z`(GtMkr)g+&O4`}7-6%L@ZaKi%*%?!fJL#`b~zTi@5SPxUmR*y#6coC9vj~! z;Om(zq#OM2?-vweeM%wf#^&Ss`#c25SUxT6E5emas z!HAg`fQ6fUpnQEi;)jgG+ytOFfl%coK>2^a3uB=OJ^Kl>7c(51!El7}>)*Pf^SCSC zs=GoQ>H_ruXULzmNBRys?8~u(tCkJ=?-+xRi*2xas6F0qwu6$RJ=%xaK_q1Z1Hla1 zzfJJ0egxvjjlinQ!%^0$i#6|5@$ZN-YKAIv$Do9oIjRux%YKz|Nc<;_{d|{uV}b3}>EdO!qI*EVK5 zr|+WOr<$qqxC=eq^j4T1{ZMe9zE8NBzFPR#Fh!{OHC4!$SuI!^t`JJQPY7Eg?+AB4 zeh{KFmI!6PKZ6u6`J&Elrkk4tB`-v7+M(NL&DEg>OVM-Mr)Uo+_DN< zB3(nu3+w5?$wuzWT4>hBM*7vzK=GR!XveQc%1dvg(JLD$!>@t<%W0#pJcD_!nMg0< zy6E%!3AF3O1bVZxlgiz?==Y>9>T&O)fs$R+o6<$I=6BK8%5GBJJ&6j@P5B*@DbRT` zc{y~`=*!(CL99K~iU+Oxz*t}HRmi#_}6$>#A)^?qS6Q;bSv ztM?bO6%8eD$%ghhIHrRn$ZMVm8l)nk%QJ*2la z;K9)byl$?;mIu6N9xaFaxjxC=?g&8l>~dY%qN=S)n^EI_zj z5!Qy3;gh%we`Cv`E-u9-p#Z12b8o)Qo&Aaw$Ymr!BRdf}N4e8~7=yFdd8fP1_uqq7^@VI%)zn98Xb#Syoddm5{pCMW1+1ahjC}(Fr_pB zUaqMK;5_O&SpeHFg&1-&AA`5#Ba8dFrlWcAHO@of;aq$U&cl?A1?YTJ2-a4FWnYV+ z5nqT03-fVwW-j_o%EpDS85qzf9igSEP+yz`{TJ~V_CXB&u1FYvr zRl)`9e4VhY!2w-rc8EJ-3um6mMEz~?UeyNak8IEX znd2xV^bW@fZ9Qxqsg3b<>bRn$f_D`PsNN)pyh3@%y^_P+W*M|^mBy6SgHWwI`$Zb7M+@hU?H0=R z^`+D~+BEDU&s<3X^t&gBR$Kbg>YfOiwke5Bhlyzo@@U&4p1B^i(0y?u?W?V%n{`E0 zySJQ{M>Np9>UwJY-bh<28)?vb&a9exS|8p(MJF4mEVG`97T42{_l@*ve+P|8>Y|0& zJbOuW(bhK;XzcBYG;mfYdHw06|0E~UEkoWhwL2+}GfngQMDmrHMAr7*lvXpDUap%$ zoeL(@n!0X!5Zz4%#*;~0I*)egKcnZz71-J5BiX{$j%?v?Vn40Mv*kC(vJJa^n0HkO zn4N!Y$jXiUckCm6tnPUdF;-M5_Zh0g!PxNWPK&`nD&GM=Hpk) z`gG?pnF%TEab_gT8R*Rn-#fAZ)sZZvT7%Uc8OG*@%CW0&6xoi=n(VNrKAXEvm;KqN z&l=-LGb4Ei*1H32)KM|JUz);5E09FB z8qvR|5o#wIa3r`MhH>?{R926H-0hFv*#LFEfv&yZghTzBcz@%a)2sph$qkU$*@z=k z8)5vR5!Ry{v87KvCO6jLXHOZv4$H%t{u$Wdn~rw#bj+l5OsGr$zrUj-+{?hsH91(7 zUWg0J%5YSt9DlmX@cKt7tj`uBY<)fg%yO|iA_KRVCSmmVL`>_AN1B%y8}7t__|e4u z*o&Jn(EBe2E*Ck^_-i|mv&?Ku3|8rhF?bR8;?Kn}Qi;V}?^q=BY_@xJ9FFab!&Kb_ zWY0*3tU)Fg_2WG1=6+7K0M}RWyj9P+#(8#`@=$vt4=r7J2pd(1mJ@~W=6Un9Yzb^L zN)TFG1VrTX9F&U@ceC)uH4{53((yN`I)w`NL^oAVwYMo59L( zOdJ}8yZvI2`85V3I{D))kA<^GJU0C1{1)-&!ypEIdZH0?C>%C_gRo2@5GKxk$jbJG z{}CVb9q*65^1kpq=!xm0#v)FiaAP3%`BDPJ2V4eSDj&i7XjaoF1_YBuN+iQ%kuwkq_A{Uf25rL%K~@4 zVdu3Tup1edm}k&&man~sZ5X+M4W6=$;nF--t2B*;4C`bU9lBXh)kJnQw}wSt31htq zJL&$Fdb(2XOodW6g%_sBgu28Hg4v$A!k1}_1QW&eLSW`W;kn}tVYS>#;qkCJUF{pG=D{`XwTS0(Y`-%BDth^k<^ed5!?7dWTrJz7*UcV%(cBQ==Vy{ zsnCJseZZJD8931!qjB`6)ramUB$MNf2Kw5soywHkX`p=@`OR&mTP-|yIhOGHTu2_h zB{ZU`o}!=D(=bsZi7z(L`>Z6>v@nHwe3-6>WVUuXKVK&tPvl(%JQunXEe@hi{lE?9#Y2R`w>F9WY8^ zk-1Uqf`kuqyCz~+E?P00MZ?+e1!`<=qav$)q0H{u>9UCRMr?eeDSPH>8x}NSvTGCE`ZuAM z?+EYy*28Cf9UiZ&ge!OXADVOVtTF?K!TY3rI($RYF>*#a)~WD4pehpsxc5&!RF0o^ z6;NDNjwHJ>9CI$g`}>7BRGyEYGtVo(|(#&5%#<dl5zJ;9M14_R$Uv7?`;uq7#f21 zw*g4#@rTC`KUgLCL+^+`YL@$9#yvlH{PaVktv{wb^uvc2eyHE&hik|E5!fCK>0zPR z|Bt&i_h@XtAB|(iu81GR-I)PH!ElBrT7cOyplPfd3ceCD zzX&j`cSX+-C%iPXhxBMW%rmlswmxSU*&}X)JuJ-aaar09+p2BwFw6$?BKS72#0Djo zEOCFCDO`UWqGgjda7YycJ>-FNGPsjH7)yT+LD}#j2$mg;=O+CzLF*g)z5X3rDD{-R z6z;HRbe&nJpJykvPq3LgcClycRx|s`CG39pEVk*&9Cqs9Y-V?RA~V^Q&&HXLWMe`% zkYsf=wJsY?2}wtUKNV{P$;V5CC;fVb9Hs5TcZE~JBGbphp!API#j9UJ#h?C^J+L2D zO?D9WR~3sEOh^|+a zf3;_Ke><@!Eh2Vx;8><`ES!B3rLys_GFe?}63ds4V{1wi*#f>{{@x>I7X4ybG5?;@ z*5S{RMviBPZn(4YTbx*^q8ZaWsK@R;QDYySRN3X-I_%j9pr9$viu(Si^XG z=IPH^NSHTs5mH!&ULmvAu4a1`JJ^}9iKtBN#HIJ`Se4YqzfZQ}ONYQ(Y2jp&@wh$oqiFxO~;@scJ4_i4sn&c881 zjhH{L0g*h*^--_EsX=A1@y_EuB^zBu{phD}4#@pfD~9=4_9_?Qd?UdhBcsS-#x zRw89q1+t9FvD&y4?++KF=vxs!4$g%E_x-Y}aahJV(B~XKcz2BEUM%4ucSCPuu$*s| zTi(aO_Eik5IMYt@+fCy<>paM}fju#}`!@z#my7Z2wHT3k+_T+|!~2VI=v7FBhbR^9 z|7D;|J0B80`PfjJ4^eOd;`okwW@a8b!t&8DzW~dM3ekTezqBnx8TWa6EK0D;v=~=5 z6<~5Z|J*ADDC<**j8{c?8osGZ8vmm~kj_#~f1g=U%;JG*~|02eYO)+rX zAC5`Gf{@Z1fZzy!NG1BCbB;esRs6B-vmcJ!^@I6#Kdd_Bhf+_T&wl&CZM`3qM*G9l zCJ;A`gYf%HIOfSmB4=SFqCZ9d-@hxL)i)mU@(D1`Nx+K01n76f<7!a^0zL#`eqI1x zuJXZ@*}mAb)fe+py|GCck85AY;`Rp-rY>_q_Bm&~{^Ek?wIZCKM#y`?bC`k~4n(@4 zaF!eF_YpSb2+)}-g3%XO*q1v(F4_)F2Ao^1_PEvS2#aWbTywyhkq)^3(hmE59H3HQ z54-!exI4-cQCCb5{KpWtH|ZebfCfIMDZuOI5Jne)_ak=X-febw?Jee=a-O~MKg#l2_cGa%4Q%I%6>LEBY9@=NEPm!pmcP55WeiVX z5;vcb+2NJ+`B6P-pEsqaW`_jHZ_|ZIH|7XC^VbNkP9G3Va~}wDN?(N2))FKnNm0O$ zcY^Wb3Sr=?AkjD11ku9wB+<12@uCCQ6GVDxA)@=raiVC+c+n+sAE9iAk8oG|i17Qz zSK&sEG+8(grt9gdw0Wp0^?74NJr1hWGs}z&R;E(h%odW?Yo%u$oK?SDC^5W-p8PDN z!xF`mR#HiO8*1qG_BvWUww^xw)l#-&9mV(ZJod7l@^ACJ=H5WTTbiglVgfytXdAgtSiTxYj|PBf98JaVMQ$Fp+k~Od|Q!lj%sq z6v{i)Lo*bnQdiqVigcb#9q%SnLiG}ATltfYt&?YqO4QlmUD`~0gegn(F=MKhwrpa5 zf!PnSXTeJ?m}7tmlXD-%#-6og{V({iPX^KKO;q|S{L}lGgMY_h^^X{oyokZ62fRnV;~9)! z_rIT+`mc`|O+RDs<5LW7y^et*XId3!+V?A*d)%qHREcrxs~ANa;t>8I9t{SGklde) zROc+5d7p#ZLvpd>L@p*B$;GToxxkrR?rrj*@wx!MSp^v3QGkgL^YAbz9MIH!>jgGY!GIskkvU5qrh)(7hiA z$%Um9E!K$K?v#&#QD?y_&(Pk6Jz`_O2Z!)-uc1)v>$&C{a`vN0J72kNEzV| zMSFjkrupM%x=F@$L~8)FyfthTzni}9*;-np?FAbi^r46@i35&hM7z# zN_eO3I^~1Mx4qGD+#5d+jR%YJLP~`@r00uJGS~&43eM;q<$^^g2z!YkWgg&|2xO%L zG6uk;e8QVT0Vz907+@q|SCuw679L0Niu*Qkl^&nhtT9)^RTrJ><1iB~-X(0@XI=!gDe{z0GF zPth~>_UC<;n|XsxpM8zhW}jorpB!dqe(q%YY$F@Cd?P#YYc<<_YYE$6J)PNCG%?@$ z2==GsCgmioqBWXTq*^MBX3@mo-T zA1*{|mWTodM~XgdixU}Nix%ZO=8NQV;zYHdV@1xR<3**WyG2sZ1VL{{yO3x4N66|o zn8q%bq60@{$?TshO}L^>pZDpJu`17Kh6?oekQKGHq|^3GEu`kvO6xkBXr^fm`PLQC zV(Vghx}kzH=GRb%Y6Dft)>Ef_9o?B$PlmheNjbTJ1{|%YYi5l!Caj4zq%}~zxPdPG zuBZFuP2{txnW~zbsk5tvE`Dw&kD*-@c%y@+@||Y}WT6A9tGU&?jU28*eb zNi)t5*&azpY?b_BOGhN4nXoFSD6h|4!^))PZHc+Yq~`6>m?s;NkLSIH@&( zyc?0C+K7J_8ZdZa18f@_@V>MGnvWZi=-!NDmCbn3w;4IfjhHgD0f&0(Ar;9ze|iI! zaA%g4$o;=@9bAP&tU;3wz9Kji`x=Vs&e_f%~95RYV@uP(`pac_(m-|fXvHxlEuq!{@LY?Z`VH$;q^gT#2>PmBi6w$fjmWglWt`-g7>Tg8am7YECaaTquw5i)m^aq~$U z7T4t>FfbqexDR{Kl?z$3Jlr3jhbaYl*zaA0oErrgxi}A5YI)em`805PE;0iPAp52O zjysE>y}1yFj~3vsa{;246u>Dp9}3g*@Cdo69h!}GThrk&HVwz8rDC&6B8EpKAjmf! zHCtn0bTt}9UqgXg!I&x^4ATq#Sa!-EYp468Pq;t&4(7gJ!XHjNqbVi@z@7WExSSv; zeGbKYgAknQ^~LCXUr2`qqw_)-+)N{J>O?d?)W#zBXdEs%#zRjh9!rDc5ZB2!fluKG z+!+kVV4n9TdP9GL7q$=f!Z)LFxbw#Y1GNd$J)P07!U?)Y&e+#P@GfKU9R?(sa#wbf zp?4s|YYD=NCK2BiT(KuwgyID*=*Vz{+h!*ez2+5c9?y5jMEImG;NWZ*^qFV}^F!8n z+%*ObODypxWE2KU8)M$(%)SX%u zrxwIayRT4_;WG00nnq`2)aZxQe&Kc0QbG63dSPGuL1C25LqYcNFQH|@Kss^gv2daI zw@C73C|YtnMl`<9SmEgMY$0dURw1pI zyR~P7Nb2WcS`#&lUcObJNxw8n{_-%g{h>-)nzHn;uM+)^ai)Rhb@YC8E1lcXP9GB* zNGiX8+FlmW&sC*lp;${2_v=Vor=I#;s-vMhdB+sj5qn!tKbO?guFMANO>HFii4C+o zvVs0`ruoM<(&Eoeq!ran2KQTNQvWuR(V9R%p0<m2j!&vBbmVi*ea4^ zNg=ANW~er^e4x$J6P4NXIf`ua7(Ett(437ec49jM-B|cq55^L}6t%~)LqEa3uM^nZ z@vcm=&YmrtW62t%ELnSuIlJ+B1oM8O%SOJ^WLbx_*z*QG=K9Hy9iL#uK6N`Xd0T-! z)b(Up+97PqsCYK_cpgieQo}}DcCpOp8Ei+)V%F#A1k6n7;LfZK#baBsD6j=i&dnGc z+k^w9+=uBm;@bBH^t;}GFyBVZILdn^_xV>(H6dYl6Rgn$!~Ytg&2yXK+B)3-Rg1$@ z>)`*Z4u`_)VBJuOO{Yq6cYh%~x%1z2H47Vf{<<|h9X+xcknYXE-1!;kvp5S`QpHeM zUW!GlOR-~CF&z38Vb%0JT({1_f1G=l9ws88O^mxLVyyM&ek@OnixpzjWQk$S#F(fm z#x8AsIa-X5=3=ZGE=CJ~{}Vq}>Wh)V-*4iallvn^|CV^X4@*R`M>3w8r6R*90}*es zP!*L2MTdOk$>-tc(_FaT%7wlQ@5Sr#k=n_f{_k9rjLyX!p4GnI%*A^BTtu(U#n!2L zn8thQhMIib8rE&Y{|SNDj1btr^T$KY00eWMRd)H~0C)Xq+Ws&a;g22M{f~X>kC*R* zG38PyejA0L>}LR0KK91zW4;)BBnUOyp=jO_ff?4(FgD}t4iNL)%306z>z9aVENPCw znI9pzR~>-fFki@+jDz7GPYky7MDizhJPQ_(810Oa7S6bEk>@M!#9X%%wp?L2@s6;E zUnad2@c5SqQAb4x=9{L2mk4jWU10Czgweq+&~6s6J&KTifx#|?@O+gk=DOSAaH%z_ zUe0yO31kTgRU1Ne@)VsAgRr+;Hj z;^$0y)IDaRahc5=bdL3Te2fi#cz~5=?_sAjcC&pWwy-6YiXQ2{n&U>*3Jk~I3h%sR(%zXDvB25MmGw|-fx8kDT8Qu zzaeC{UXBj@l_l2zdHOy@iR2QMXm^qVNtVgdV0~E%U$04eHeqySX$zSpw@_hZ9W7f@ zOzxKo>8MmWZR)C~^~H5$_qUGra_4_1r-n2x)Y7H0I=a!no+?+@)4A8J6u!Na(iJN=k z`9-ohGHkH*F!plEP$sQEgbfTB&W`rjvOcq2*~D%zw@D(FcF~4aNZK&77Hify+?uKM zv0}CDW=zv=6#Jt*ioIQJ%I3Qnu$o#Oc4e^+8#Gy$^?zl^97dS1S;82W`pbn`Ep%g5 zzx>z+#~5aDDvdqPDrKAdH?ukIQ&@NNB39jh6?2_G0h<4{V{U0HD(za4;n|9x4lNil zstMNu8*#e55w;zTkZ^2*C*J`cQ4?N&YD6E~COH0T#J+!x&^XYDetR3BZ(NUyb8BE} zR}IaVRbXY6kQ-SB=c`57Af1mojT|IbXW;jyG@SXA4qfj|9NU_ODQ7s}JoC_ZPVxW! zJgwtt?)lvc@!KO0&oi?SdodOMPRV$DCl(WB`Tpo3#$L{}vs=XYb5e}5{rq*>B1XA| z7~4jOaf@%7z2iCG+<9KJ7UQy&7!myaffvM>ClQB8>jXSLnF8;~G%WjXfopM!_rav;8+gZJ$@_%SUTvplk38kB){pETGVPen;vDr9)474J#H zl`-*<7#M?!Cz0s=5e5w*44?i6!+%g9g7*jDscr!JUgj(-_J>Mg+&h zHwMG!cQDQy2BY#!016IxWAW?pNDmqhAB_OaO$$ZQzA)U~6Atl$NT?l+!qHDrc%{z$ z|Dy=3J{ZhpTPuJTJoDAp(Y#h;VYE3rzG}pc(0cWpN@z)C-vPkzu$p!Emq$xy3GUJ!6L; zp2=hz&GB~ANK_h_;L7FUDC*Ed??Nr?wb4T4M|C_cRKd9miui3Y4DU}U;l^Bfc#Ip2 z*>(SztRR^`6vc<@$HzeC*lTgQtm#Bm~tF;hhTdLe@C#9|?}c$Sc1_E#8( z0hIlPyRv(VbaRk0ZF-|d(|Xj%S)x7Dq=acv}$-A8PBStJ?{0S=-o!LyC+Z) z-w~{m+Q}uUgI->2Csp$fnl*C*MNe;|@8_DS&zok7eAP~?CUueG`H55&-%atdlPT@% zWJ>LvLR;_Dlgp!0lKk6B+s{oR#Tm1y$ZsLV=^v%C(TB;jeJhpi+eq1~_R-H@XX$It zO`7gbci*Ab2Q%CA!>X@sd#1OEQ3gStWu zOlMS}Ijt0{Tev@JF2JP^d640I-UX*D-Wk&nxgi~<*D|0NoCWdg94zDPh?`Z2qjn|e z*jtQICyOBN%EKbVEL84Gh0~!VxGTgVg?B`0-X-J0#i-oI^OZy_7F)z3iXUfgi(&65 z#;-Trjpd7x{)A_+dNJH$V^Q}n76Z96dT}`(r?}tmaZADDnY`1!NQ2(KT(~(GV!*>f zgnul+^5gmVGB+Pve-}XiOA%5^ORy%e7|RwHqhJcZ&BQ#syvW&An~P;$xtPt}+OVTJ zI5s~A>DzL!YHki9p5?$)k>|I*xe(pT#-Be~*rT0^gFJ_B<$Zd|hjb{GH@nOjE4MyF;Ks3t)!r^HkOtk~BZl^!$C-_5_?*Tph{r|YP zA85q=eQ*$zUj-uaoFA6|^hK?+H+=Sc;kuj`0#5kg?|}eBR)^qNMi{7+D7*>!2T6e7q6e;Dr{Iu}F9ZK$gLxuLvq8gulA3@S5d_zLEkCl`!P=b3@t| z2E#>!`p2AO7ewe~0+LU-0+ud#>n6f8&aD+40z#J)qIn*Z_)mmO16=WUsxy?|*kOIk z7?73_S*QvJ`e;7iBZ(!9eg_sxE$zJ4d$lDM4Rten9zu1#hha~fFOVPCfR zj4T^xc$j{bPbH<*ku+`1FxuSzfWYz>3U{=Z3Nu}H37h`AESNs660{x0ikdtoh=xl{ z5Z%)5C!`PTBb=YHMN}-T5UD(h7G2Pa7L9u!E6TrTEaYe>2_nBVq43RKVXpme;lsE7 zq;4ZiUPI++(Ls3{5}`(m3)Lv8U4w4RYmoU!RT{BFjv{sqp;dM2G^-c=2J`dgX>7KZ#}J5XeX0^4!Xp* z(1*6|^w+3^tn=C_bk+n)nc7aq@l7hcXbt^b{FYN;}X&!#M2)|lB_4ri*-daNi;la-dLvAeI;*dBHHc!A5NR;Zbb+PJ8APK)_~Q#OZIMW!1z}UxbmU_E59}%ly3sJm+?$Cvkv{v)Z$D{ z1%ii^Ak(H0r?_w0cPA4I7iB=Xe>x@`r-M8*QIVR3r~l<5gEK4AycoN>OVCnQf|-s* zxWs$eO|wj_X-k8;V=~N+$3cl_ss`@H7V>`im}jkZr^VPF9*gZKVj=r17WSNHz1*J} zosNTHMgm%h?{-#+7=0rNOKv72vpNwAXC>mwfnbFK1(c zZVryeXSVPkp(N(O{s>-j)Td>e@IUjy-McQAfd2IAP#036f_z)1T*Lb{lxR!wtLt z0=HE;?{*7lP!(`)lz@OL7hL%xAp5Hb*24wN93VhYd}SCOKtm60N~!N*0bEaQaav2lX-^#Dqnt^Hd-i{=&5ki;^|e_BOpy4586Uk&MUuDu>zLk+n#G-gvRxw|yb``PU@E^-3p zPj089_3hMqqn&2IX{WN+P4s0$6BWO&C!HfTWUN+6lFsd9I-!d)dM8rBnu&B{&LmRS zZ|A)+m_|%LM~x>Y{@GkE$?P4HcFosm5jxQ(<|QN=%}!5)+3jGPzSK zti@T4HGR=yKPT$2$cwsc%PBp!rrCh~7j3{aw+?5T>qatZPu@rUT$y|SaqRjGKlXY} z6nkiq#18h$Wm{Al*zF&kEbdx28~btw+pM^V)ugXr_9bn&!1qhr&^DBfX+_HSX0)tt z#96~89307ASyuz18|&dHS&t5tdi1WV$19ZvRNdgmfd;e{H9%)HcV(&#D7CJGT2T#1 ztpcaZO3`+_5W9F@(R!AS70&6Xo53@QWd@oQv!FdF7uKKiaBg5B0vwBR)v*NMMi#?E zGmmd|S)hsxcu!8li9bn*6ymVKBNj_}uk^JPL+Wn~;3E{_H5g%}vEPSFa>S=I_E?c(gLk9M zk*RD7#dG>_xUYq*eyYGeRY)vS#k-A4FfLR?l&l;!RmsCQR{`?63YhOW6a!QTqegZB z{+{~BlFoi&>2L0`pEj4-?2qSJ=&tiD&hP}Q&)C3@Tw2H`m36bN-Oa3QRyN~#kjX8R zWuvwqp?N9HdS4Ca)S8gJu{Fp?eW|q^Q zHCFU}y$MyT>QS_eB}Iqk&{LUiy8n0v8L7;obDBNml020jWKW^Fclc)Tq>CIoXH!nZ zEE?xLn+`o&K$~AJp?^Lb=vDbk`gm&qoA+6gojg346^2N$PRk)oeyJRrzHS&>;iAOc zj;OMQQCjTWX>E37nJ#lL;JGZ)fZg3Yf*rv~HY(nV?Q3yl!^^;0BE6Y-Y%p{98N}tiRxE2^aZ-Og#{<5bV zxZ`L*S$94D$kpNbJnq3p)?&`~IvkCv$NgTO!$vf~*{uPC^ctY9Rgb^5S9g7sdOmjro;bEI{b<SMBMz!gtaZ{s=y`k_rIRxJJ zlK6S4A685LV?DDzu}6KLvZBRTSajqiR&eSnGoN~#caOE~)7LqyTxv2?xZTVKw&yXn zX*?@=tiqxO?;yu(O=Le&j$}9d6}lbP3FEZdgmbp-!Wh|1AwSwo)Zu6;%2@MCEp}oU3s9=CL-3`#70V`GMv7tO=DM*uJ++Yfup+;9`C6ML% zdRo;|LG#OtDA1~q)YFT}pu3zZx~fRQrG^%DR?!OUDhg|;B>D78`Y@(}VivU1e(w8c z7`D=n$QBxB-%2O%a;AN1CDLuAx^o3I<5U4vb?4EJK}}>P(@udSJL$^84%+g(ffBy> z(Y5Q^WK^U{V{)` z!1IE7%s)=ko5Ho{Jv- z^)7R7c8TSp`fx4^O!BZJI3NCRb0Nz)KL11}ZWL$X&Gbxk)@Py7Iun;xrr~XNDmJZ8 zM9P>r;I!_GT>WQ={PwY4H zgw0A1n1*^F$(SD^52R$dqg8(_hAO+m@}DQlMtDN9#vL_7$HFLvVbMV+NPe@!&6~FT z``V3XTAYUafWTA6Vh$%QO9hMf7=pf z7e-^*>*4rk$h+ksHK-0!#M^|ScwsgeZbPK7V1^{7ZSD`RZG$l3fiyOU%D`~!5bS*- zjXUQCV9&I^cz^gObJY9J>hHf~5;JZxA?X^MwCxJhJAZ&3@>s)8hRk3^sohMrp^^1I z&0v>C`?53lWZB@rom9G~f@Y>$&?vmndlxLX?H9hFLGP@L)0mkE-cQ86|R^j3BQ^v1?}j?!WD<3 z!nv3i!pOIOgvpNwlAGlq`s}Pom-eVqT%#IQ_-N3Ezv`qoRE@+YinP&SFeS_%OzQ+S z>e5f3Yo?9#d2JQ#nOsaKYzd6T4@dET3}c!Ow z=_|1brOK?=Muo|})?p&E5$w653F{YQ!TPB>u^Z0?rs54&UrX%$R4=wZA(*|tE@pWT zQ(1qnY&KlJkok|SW^Lt7Y|8F-c1CLo`|L7@m3&^zthCp$q}wg{G_D2NcFj0S z7IHK)@gzSJ&!jT2$2b+&<|pH#QxaZ>@ywNygtuc7aCTQB3@4@Hdq6ra+2p`5JrBdU zj~7c8VEwj2G;Gepct_5(#4Mcukc9<@vXHQ)q^VMZ}tWENwyMj^hK72se00#v-qN6m*qY`C5WFCX5Eqw?UYlZP!| zbFo^;LxOicM%m>-NaK6OtbBZ}E5Nvq`3P~zMcd14{Ng)+Cf@+|)n%h$SSDm0({Zy8 z-!tu!(CZurwoi=y&Y!LisiH4=T?L$UL=57d@>VW8qTe$MV@%r5BsjzuB!fe_8vu)ydO4@GrkWD_J3g)z5Xz%ls|0znwRX=t2^vV!zK1==vnqcyoc30uVT}hX0VT2 zI$54{4HIj{vp@S7%Nje7?OA%1`gccAW$^`JY|B$_oPseD0CL;-AqJ1IGwplcmESLDclfE_FgI4H2Z{T$w+I_8Ao%Gg)mNZ<=t4}nn{8n`^--`ygW+Slhi3B zE?zEdv^XKGzxzeR+JO}{cViVsmHe+5}O zEIEYSK5J0?sU%XDZKT&5YRIsml)kShpk<1MBzdNk`hBaW{qEJY=WivAdRjpri_0kT zX&D(006##$ziTGXt8KI^pq+dlw$aFEO*E~wl|(;UXr2f6VEx*NX{ON9&hhl2Ig(zc zjU$Pau{70vRP9JV!ZHg0ETm59_>^+Z7h*-uxzgo-w&fUw} z4>Y6UTN4uRG-Cad2J9=TM~G7$BDdARCaeZ2@2jExt{N>nt8t3wvolNhp-=}czdB4y ztV6)9YJ}{m#DH^UxWc<)vwR`g;XE9DoehJVX=v6?$K1*cOw`W6x^Fr7?VgXgIk~vi zClB7!b8+`;HeLtsp>Ceya`{rCUF3Cn| zI`7&EnV9(?9TL3j{#H!GH{)cylZi!qVl=*sBa!hZ1Qiv*SnCvwr978;)$x3m9E5*Q zLonAY62*a$NM9F;{uxnt(ishJW$cBrJzm(~G!Cto z#=7YZml!Yw49Ng?SN%p z>~Q3a9peA9ftmLhxURCo6fZLz&oIHmRR%C}(Z)DqWnA=D!;MY$0Ts+eP3+<^q1*;`_AT{`piIU|aTA%Zb1GEPGL#HI%8>9=kDgsgrpYgxNjtrsYMaWbsknfq+80oPc@cd& zRZTGw)nqiIn*3Mrtuw8R&ZtyS`OOyEJfe+8Xtq$AQX9n>H1QtWM(fYCQQpjUTH%>Q zDQ0oxEayRAYaNOGHK)^2x^(K1BH4BgA>$kR)cm!Dnm)~-S+myD;Q`BO;eom2u%wCN zhlWxg{|pM?_tpEin*NI{pckIm6!$uxnwL!>y}t9w*nTCMYi^^OsN*y&^BP@0`<`|l z8o(5vNV0_&WtieMS+@1HI;$9{&7Rcgv!ZE6>_X~DW}GpSndKX?gU3g)5s|j+(l%nB zl!Do@U7>8NZwTw>7s$lpqj_gcXK(iwGsy#0%zRlrd#~8W?tSZII)|sSeFJARPyZ$C zSovD^;r0$TynQd*qu7jx?;6oh+<=-D^>~_7hb+}v*qPVh{IO~XE2?qxSrv{ws>0zv zHF%I-1N&LEIR3I0M%uMt22~iGS%K|Fr3iai2=~l9um?G4>X(5z{dq21m4yj~S?Ep9 z#p$E@(Bu9|{&)^nz0F4dWm$;f-7l#(6PFKU!Zs@l)j=6p{WSwmzwy_7RvHcsOT(=F zX_)Af44GGn_+*lbWaVrW@(pndcT=}tBwx%F3*8XRURrhkN)>> z!UfK!tLeoEEG~tjO&RhE%Ap`%iSVywSjGL=J&j^a`&v%3m*xUOXO?OS~X{=LJcg+djv6qP)ZtWqkMi zt~eeGg2tmG%?p3;xWhFXnDEpYe+`^)MaCX(Z*9;fwn5P{8$2(z#iPe|80qf_@fsIA z{OAm=zs@*j?gZIG4!Ee|fS4$I#NM;T4qqEo#8}|k03&=1)q!@bDvHhJv3Q6q-n|(N zFS9{7sMHskq94p)*?SiF`4c-j`Y#Jl`@!z+{m4#ay<+m=FWH9Zb8P12+bq@g6}vm+ z0aJf-fjzhCWxKo=vi8{v*!r3oOkzzPyY;=E?H=sU&fj)olUDtqkG(HRws{|2sh>s{ z`YDry;c-Fs+HT>);{`(Sz+pnph}EK_9xkF8-+qc@+;54($_EJE5<5iw7xjp|2N{aI z-k6E9f87>oSCEjiIzUKvauG7-cnTYqb_n6QbA)m>Pe?1=EnG^xCS)&vC78Ou6%?oc z6*}I^kc_?>d3tEjo(5$av`~$V_h^tJ@0F9<_0yrAtmVv_QU+`ec^UXPYWg7d6wcTdfqF#kb2L%`_vunci<` zqzzYEDM{2s`)7vJsne09JlvexHjSdwR>rhvkp|6=9Zb%1Y$&sD8}02_OZl7k(H8qn zbaP2BwK~tEntsjHJ~M_EiK9q4Cz_fC$F9ka zX372*YL_3q%WByQsbEu%95f_kV0I>V zMcrBGxRZs%_H4BAEVGFBJU6*qwC>909x5BZeX7=VKL5(W zx29~II+Tu}qp7g}nTA`FlAz<0g!fC+af0`?b_JfB;&NfdKbQIEAVDS@Ne{E&%6pvb zs61F+&qw9X0t9ouelID8=bIAPYn0)xTRDcSmgCaea!e~ILHqJz*v1t>=1mbg9g0vj zz5vra3XsW<^Qr}K|CSGZd`$N#ME@y8*uAd=($eMFeW3zDzsoUfZ5iCF z%5d;i8H{F^V2)8SqHh$!kAJ?ETppC{vv4zj_scEmXqb|UyK+gW?;8vI@F+}O5DuGr z!3f?LjPZ5B&@~Q5LjPc#o*IOYF2PtgFbol@{5F#!@uQq`Z*~lJpAtj&R4k&kV^QTE zhlHVV=x-**@^O*aJvsydk^V^1@xtog<6x!eg||<}p>XH_F*1OyRH{y98lHI9=-c*@nexS8ZVin*4-G3e`=y^ zg%Y}L%Sv1RPt z#wpD9VjCNEdOkZQy^USBS;7{L%VJecIlPZJv)a9K?1v!9u6=$&zHv8bo!v3AC^}B- zey*Uo)4QqPGau4!ekq(7bx1hby-RpyzF08W*(};xGDZ|V(LtoYdY|Zd*cp+3=nm1v zpi80^A-W>3yCXzHt+GTl-ou59%iV>zTY(2yd&(A`RgCr?FSEa4t+GICdg-ZLY(i#_aa%@tkeVk(}BUMRG$%v*m zrqW-ZCR(i2K>deR(uJ-vnkZXBI~EtyypCE*PN^W#sB+2~SV7gli)nvVF_{K5Q$j@> zJvrM-Q*YH!#kq2F>zhfRJCdpMYXJS39!$|cN0V7ULmIAROkJPUsb^V#D*LBJk+Mx> zbZI4NKHNwcw4Y`#-oZWoGU`|`l>&44c}T>QXq`6|82izi!Y~>!C64--w$RJ!1+?h! zW?B$`o4lrerQ5#$$l|L6`}0nM`B_P^Up2$n>D$WeNtOn)U8K!E@7H6F`KD}b#TZue z){*TS?_-Rm#cj7$E z=Nn;pY6f<{&cuNDOw2u(i5+=aID9@E&nD);ATI|UF*)ec&%wv(+0fgTjiOqfp;qMJ z)23WV|H#IJpLv+b8MXOdHbRm!k?}MIO>a{ne>V{`ha_WFOdhelEsR>r#vxP=Q{#N)#*_J|4)q|Ld;U76o5-X$#xup*`ii#>|b!M6c>^Ku*t zt;9i54P>mUP@Y?fm7*%F>RW-jyQRonQijgmrAW~$#N_T=e14jRqKr)J&*Gk}BpC;O z#KG7q8Ya)fF?>b{4vq~$ze~Y585@j97K|l3f-qAe7{9ZE@lz!P>fWIk^)DPBCq+YM zS}cAX6eHz`7$1L&!4Ae@@`E^dZ-|4ZlNi^O!Vq~j2-~iDBdyU3_hxxP{+<_H8@*wg z>4)@$P`q6phS3cnh?NP3>={4Y&+tQ^MSi%V6@bUX{88-ajqb@F`0UT%^U@iK*$(iW zYzHi~!Fx>`^m$~9ufF!Ud)yvra~!Zm8fbNkM*M9Ri>gPwT7aj@{Xbv%C>^sNk?G|6NC$e0YXi!h4A)uiLmhA3L!>u zrf@)JmY_R%qwr(s2|;%7ZQ;$JdqQo@Q(=-`UwUn#OuNFg$ZNMcUGq@lPF#ihSEx|X zQ3aabSBozGXGoqqQ%E(ZnH;>DXptPxW~r65i*w8~w}}4U%qN*$#bmL$oZM~8XiZl! z#m+CLeZH;qZ(SRiy=|rQTN^3(fCpLHjVF^C9;CH<92v~8B8g#2^zo!3y-xcjj9ej2 zJ|q{S4=w$kP!`^f$523nfCgjQ~zLPI;cDDig$J-Hc1&mA&oU<}WE zrBg`r>k?8qy^GFkUnJ@3r}We0JL&$DU<ElIGwDCPW1bCXmY&f}(J`BS zZ*OG#9#3SaUrlDg&OPjn_ADmnGM{zyE@6G^okEr^o6bFIA5nEp?FdslnH^mAJ!m%(TifXdNqulw1XlEh)!2<#N<*Eya1G z60CA9gx6G_gF-T4d?o{%er3S5JrlF@GLhK9H%5VHuaazxHqV9PsT?dmm4}aJdH9x) zi>3|Ph~>TT-ji(fJ)Mmvg=};^&w<369CWDV!nSWFqDG})>C$9O>`a23Q5q~c6XK(D zuqKyhxW-JB-Oa#U?)h8#=YYxOVA&1c2NepCWmtrPLB%keQ;fHsMOd?^6wNIaXn#r z^AVL;S66{Y4b>opT1cQ4_Wm{av%3Zt^QuuVUxAZWmH51}0zL0aF}$k?ZLf22EIb#6 z{5-EHr$M|k0g}sOaDE2&^%RPipF*(8D+I1vf)P7A7|O$gQPUEPoZMj4Zwp3`QwVO& z3B?uvD5RWF_@WqZU1KrFI36iJ32;?O!0Y{DobV6BjIY7iY!rz27@n_u z$78i0?~Px_r0KO((?YIKs%z z2HnY4*jHeIqKD>a)iuFh6Fuw@)L}YC0W*Kdz}cZcMp^%1ZPz}s=B+PTkJVlFV)g|l zbL${`|6&VUU$=ruu{kVbAUpU%j?o?`k6=6 zef=PH>|9NaYbMf^_G0o&iy`Yip46dZPC;k1XwtTUG@$f>@O;=tA)#ir;5~e=;83+l zcu^rMiYkd0wUxgU%`_S+NatpV+#Zh-{Ws50wDQ+m(Wr$aYz`(NRmx2m-r+2CnOFz| z9<&M?FBSfIKm*?$vuxX4r63T+y8RFk{* zVKm57p5%rok?mqdV#Bql^Nl76XCi2mdISCZ+ekKZ>u739B^5N4(58-3vewC`GPM%& zFQ}xi3(JVzDxsG@OKIKJRyua4m7b!Nc4ak__r45ToM%aIS9{V!PY*hruR_a?4##lUChSw~Xts2! zIh(35nzd<~GsE-d>`1pA^WW&mCf{{orppBOv|C`6i-^4#;l%Y{b8o-c$_^B zy1*8TuCu;+^>{q69z})qI9*zgt!L|SY;g@5c%M93S_ZG!QXKX#MQ~6VtY(zr(pBEA z{ws!-YY~hL3$e;7AD{l?U9uws=1Vj1tuhlo7G~nnpG=5?vhdWM_sOAr$Ft2xw=ZYR z;(S=&%0tn`T+Ca*yQfPQqIrH>J3RyXe8;@TU+>#~afYqWMAPDQEZ&n0ccWw|eNTc> zpLFy|$ikbLEcic6!;G4Aj8e;h!sBeLvB>4@%7^B@JTz7Fy;7qH`o{`U!1u>BzI>PL zUk2}arKlcIf)>AGSO*lN?}TD}zF3T#4SW;WREUmK1sGCaz}ZoN3xNd)&M(Bao295p zDaD|L<+whs5}l)~(En;Bj$~K!UR;M|$#pQ^Q-`z*HF&+a3YVW%LUU#%_P1AHvzYG; za|&^Qd$-Ds+0Y%44vX?c9M6wMxneXTb3?JUG8B_GhJai{cuow#o=v=8t_;S4jll>x z_`knV9)jm1Lt)eshPSnmc*t|tFNs+6<=rygFBa8rAt9b{GDx& zeam#3p0Ut9ci6#U7ucQ=huO_V0gD|B^X%jC^i zU9cwmd{~-oAM$}FXC5cr4GZaaR1>{aNucOb2Xb6xPG?`6kWaNH*^M1c+p4|_kDSg4 z&W{!ehEJyp`rZ441C57;DJOD81+&J9ZZ26YdU)o8=zXlIXzp_p(Yj@6qDO8vLR5f@ z@a4Xn;B-nP{9HOlc)!_9U@Mx1A14+H3gOd)Y0st$Crwrgw!01qXU?7y&Rn`GELFN8 zTyuOU3|TBgiG#VP3eljOQ#5EtjS>y|qDZ$FD^RbmI$57pCS_Y!8ijg#Hm;FY#?_Ov zVl_SYE2Ci(O6iA39^L#}K-+7INajl+9eZC$-=esa&u^xryUkP~+f1i2o2hGT3*FB3 zqaF)aTHI|*-6{R4V($Q2yjGLsZpPDrO$% zv=sbMNp`Z#nrq%esUE$|-bug@`$AlC0NPAX?qi1W- zGPnln9yRFytrD^q%klO@F{IOrV7MqB=XPXchfxZW4C1k+EgFlX!ZGekD2&&Hf^7;x zuWbmvI))%uJ_K1VA+Y0@H(vz9-#HY)Yr?RyEF2@GB2jWX2Fb6*FrFBTbrgpS@p1Ud zec6$laTrz`izCW02#gL#AI`;dcYSc`j}LS<`yxT#2hZz$F#Bo{PFaM(ae635cEn(5 zbpnQdOvJ^J@z@g;ida+RWxo*jaq;6S{4=#7#`?(pp^;NN2>`0sas`YbyPvbRO* zd>h0xjlu1m7WjF`97m1Lk(@KjvO?j*WkLn1!6)!LC1@$A;XT#0q?y*!$51 zY}4CFwpop_IpyZeX`BYTdQysQY*6xMehYQ9E_5_a{foQMB)-DQ%cGl7jOL z$=pJb_AZy8na5uXiE4+0Ys+T}AFlNXUi0?~u3l$_-ht0WCKxG-mz*fd+|Vd;JZvet z;5%Got0*DZAF~mZV@SBd_sgBqmcqVw#=;RZXQ9G&ns7XOuCQ`;hp^dVy6|-DazS(M z9>L}QLBYG~k}!AJf5N;EkA!oN2T;^YRhoQLnt@r@_4(wOTSA|QmQYt!5nVHFq8{$d zUb-|>OaW)v!dBYvJ^*JDZ;Fo=#o9ZPcJuNok3b$ZXDRI%Bz)P8q(VZ6l;u z#gbuc%0C0v$D<-v zs^Gzr8$8&JIxjX_*@tyI2D1HuA#BFR2xexH!1Qf0*pAo5ERLF4tL|`8q32y2+fb-)Dh|kC-ukJw9B-d!}7I&Wh@g zKCT932^DzywG@jb${^)iiXo-NINMYJ&&mSWuFHdw2G3Ljv+&b96G~E;=$EdW&%V0l_0eD%ta$v2OGAyv3>xf*{*)Ixt(Eq>0a!v}FaUVN@c z#efF*&#i|h@8#L5oNe1`F!^{j8X7B6aH13^`D66hQHT}6`FJGC!YgqSK7A8oe_}Y2 zHie->A`Dptq42p8g87{xIM5r6_%$Jz%{`W5P6!&iLh>|+9V^PaU&X2 zJH?Q_DaIMuSiJfo#?B97^wSe#mP#}Xs=~2FBMePX{88iPhv!=ZA)-KhX!65xbw3z7 z2Eprf2=vc{fgOv(w9!duj!8s>O*{(tE;?8(7V7U~&^$026k+#!v+qrRuKKO#D)hJDAON}rzR$l9Ak*@e{|t>M-$FH8aU~w zh8tIv;r(a`T$`n!xW6AZdU4l(?-P@G@|s<}{)pxNzR5IGE-9SuUMiGnz8A zM$%I?6B3WsqXj>eNKr?M%5QxYyqj(d{~DJI;mQ+*w8r^@P<2wclDc28NsttR@2iT! zzwZ~R%Z?K5&gYpd!B{kZ!zkh3iP1vK94BG$IB(&8g^4g_tBKI(g{z>oc82iRa+a{_ zL!A&kW3q5z#d6{6t?k0o;9bJD5f_EDJ=X*|n;U|K?N6b!LV*rF*Cg#44f4oRqowba zNsv?_@l|EI;H*Yh4;Yf7D4afBsw1_M8j8@ZBz3P!s%a>q<(5U{-=0NTD*5#3KrzYn zDItZTB6=2HPf@!X$ZJUp>BYCu(@`z7DbbQ@|Jsnfj09bM{9U-?{6`4b97%J(O(3TS zJ+$x5I{K^hh<=axO=Cp;S?cYdR5JWEed%+R<_7GgrA@sg_|BukrBmocQ5(HIH-WJAPXu`W_=_z+4G}D?8N#}tZ;!D^KG+bb>sOSc?0Z7Ay~n9JGMx$W@&G2 zSXzZ0lkjq8i586g+Tg|x>>JD4|BYk)|M;`s+6Z>QBbLovk<5OFXEM>vBBuVRn!Om( z&f5D+Wu16j3OM$3HXkG;rc^~_pQ1X92udsQ42`1?C9ndu&fnGWIa-4ho9hvC!I|EZs zWgzw@?}jfjP;HV4Bg=eLbeEwbv>H{4wOHD>8XG27!nmsf&aWy^kX8w$*A*zzsX*n? z5*)l)g!keiG^-ThXkq~#t>oT*T_&u$Q=w;-f-C(~xTj9WuJB|GPfo_)B`MetpNjfh z=~%fW2S0Rk;nkgkvC=ts!*bv^F9*L|a&fXh@0&4sct58A7FmU;Gc3XSOZAEDJEJXLZ0vPWrz|fAWIJdP5@e|8&z^N4GXNur$REX`5a`1Xn z7LH6!;@K@46cUb*t6{h~DhxZzL$TB#6ybYA&~+jNM>|5W>3s-Jy$V5Va~Q;b!razsJjsXo3YWjwpEOm4KWxnRg69gVgx^p!Rw`waQqks^X(y+@g@+LmIvbM|K1)O ziU(aGkbf2o?O}n?uL#7nUcMz9kHV^z2^iawh?`>*A^k1^4Wkp_`zsDEpJFkvzZhwM zBk`vw3}2r6LD$3^y`ApRdc&~rp)1<*Twu7~1#bp9Abp1o@=UDZyv`EGP0evN#1u+% zhGX7JJ*;rnM9EQQ#Kp+Np#Lz0U*}CAeE@Q=^nq92SLS}}HH#enm@PVbiy6GT%)V?n z#a_PM&vqGdSN64+#m}A1#>h=%t3yhdZc7|%**uQr7K~x(@meghy&pScdy@?3t)+{nJOIrndn)lk6isoogUA9GFkZ{0Sb^3D{YZ}wzC9J@)7 zxO-2Km|Gx7AG;|ke7;g7T@Ws^zGNxt@-`9mbu<-ujxiTHW%Y#Dp60^+HDiUmb!I|x zp{DRwa1kDy>Ec}L5hAKvg_AXtgxBUvh2YKWgxgkI1;ux#1fk-(koWhJK%UQpeIfEB zeVwzcNrOICYtZ(eDs(?vm3(uR=v|{K<*iYp?Y{*&{I8N0@ZGZfZw1}#sU-bfWz;aS zgl3IQrk0v`Iu>66^`v5In8)+k(k7Z`(@NsQZB!B%NTzQsDe8s-8BPBs-1hk* ztScKsxiRIG;53nXR?Mf6gC}WW&RcqDB+K*;3}a@$eo@Dz_jI@5A-P9fqytY5(l@r5 zO#3dQeS2n6ziqQ>2B|WhZ4ESiwUlrnJI^Y5R-V4>2)kO%Il2;>!kfhO>;eMCSe_jg_4)VrId$ zY@fJ+S*>Vh9sN6*?BpJHZ18IKch4T?)_a`wEIYwky3R1wz89I~fB!Md%lBDA=M(nU z`#H1fdBe=(J~Nf8^@!!KN7-`Tjr=R&e!mQgxg~h3S^}Gi#n`p15Z89)!)RtU&W_6l zVzM!mcgeZGGT`1P6Q*jJ7*?JI#ML=w+iC9mAD&O21EB!lwK%C#)BeUdRvG*p0Q>e%0`Pp28tJ@;HgCl+SVnb;b1CE zdAHoZEENNPro!Al9eJ0tQ93)9vnU7c*RoOeEE_{Iaximt4l43;(bbrbA9eZY^Qs8X zrj?>^Z8^N7%h6C$jwg@GaQs^-!b(b@*IJD5Y5XyMD8SON5*+6Hfbqaeyt`Nl50e_G zjjYG9S9MtPy$*%$^=LX=j|R>$?}zm;yi$j_TJFrg*I@Ay-pjQraml;_KZllM+umaQ zwkgD2-8|Ig)}@y9k4Yxt336^byk zFuWZb4hP;vdpaY~+cy%VbrFv*j(a{`)vYVpL0S2wws-oZ$+gdM*lW^PkGSh!%ptRAYe^xVE| z(Y6b;K6M2pIyKP9yROu*-i~IGp}=de@S}LPP#G~n zxU0WNa7s8XoL71$NPiTByAgLp{wr>YR5}es%66kfO{0xPdm3Uy*7>$V))`Gff6^$y zZH$-j+h~liG(}lBGIO+W6pcdZ^l3uduWq4X+hjp^_FQ4MRj<%;WP_llcubg*cU>^7 zJ1-nxc3pU>C{M@TwJ7>OReH)9`{kb+CHbn6)(a)tv0I*UPHNED`&M*(Ubn*Q&F?$sk`sed_j`1X+ee`{!6li0QK)0q6?DQsBI9OfZj&iu@_u!2i_*uv(+Oj3ND+4Y`c z`~1(d;mMcTLVnjW957uH}XcZ0@m1C$-hG{R0F=|Z_N_bv5 zk(7^R&bcV|%);tpnfSulbzO^ZmOnF)H!%|zD|0c#tPE+|HQ1M3i-t$F@Eco;HIdbD zUQmUR2P)w@s0xNvRZvc>##EVVm|Ij~+PN|$lounwu?VUg3$bfV0ru7AAhur?dP>sp zYjg?j*nL1&>bI!cdtWn z@J1+}%@4(v8KF3TArvo8gyYqka9l5nz+vY|9Pf$5x9c(RxERA7pcr}6WAJx*B!ct8 zpm{zRy7|FaJ2e<4eK;4F^IrNU5~t*%;Zq)kTf8soe~rL4#&ecK1ol~m;ZkrU3eLu0 ztz#0_R3srZKM{EW3CNI)$0w~=SWz@=y(4h#X$V>@{a|s+8?y|((7Vb5{dTzHPcJa* zKUWm5cEro?4sg?RK=)r;bblX%_wUV7Gu;@Yl?>onrj2#0)i86RBIKsYp<}HKj`a@0 zFaJJp-t>{ZZg|8@;xDr!+NW6k`Gf4{u^sH|jP*?K-ZIv8Xdb)mF^LWLZel-cGg%** zP!{McVls{?p3k9T``qnst_%jmu| zx9qNP-*1C3{cNX@HM3iYnX*|}QgKU|X7^M0Sm7evxUpLF&Gw?`u&$2i&7hGY{kM9e zH4}b{K07)Hr^e|CvrC2xD^(y|nmtC?9x5mJh3N=&SM!D5+mnR_|4kPDjGrvjznw0m z?OPB5x$#PoqwwiR_SCNyboEBG>&{NMq>YtQL?yCyuzdMC=ZCfcVn^#Nc zH7n`nSbzF17|`S>V-kf*(!eR-gz@t~3Qs-m3ElaY6y(xPK@*P91-qAIv09Q{cQIfY zeT>mTjU4M3V#bUvJF>sUzO1c(AnQ5o%Y3i8F?UBt78vNr{Es=Y@HftEr;{tQo4Tox{5E|H6v>84d| z*zPT?dh;H3P~`|Kx_pX-54gyZy{<6(_N(k@@ilhB^EyjAe}nl9yUi}l=iTyaHDo0! zAmv($BA&^L{E9Hwt`LVh^1%#qfPb0T`k4FqxJ>*)CNiF9U?lIE{+sgfw7CdZCY52h zP=(>L)mU=75>HY%6QU|GOH_{hedTb!S&7{5RoIeUgYCuDIM!APHmw}%b&HX=v;Z5p z8{fD)7jjQBac)~W_PM0t*tkr*8k-F*vuxyh=0Q~=5BI-iV^(}FhV|s3*(eXnBA&mD zIs4vZA+RbNO&7Bf#Cx!MV*zxWi*SZ}zLnRwd%w%iaZU+l#FydzyAtd`DNHXstkQv%kj~u5?^boA@-?7KZSaj9InG8p1~fUu0!C|7FX`ZmrRyqIx@-}9`(y@t zxuu=y{-|IlcO|j<3FFyQ4I6fHfeuT$EX9s|eoPk*>>~dS-IQaNKtFn{>B%E?A}uAd z*gBK~jt{1-8wXJH{ZE3-iSt53La(rLVVkhOex6V>`k2ta_nqKBSDI4icL;s1*NSEy zJ}&D1Y9I=qG+ZRRQc*P1^Qmat09(Q9*GQq_u7V&kv==1LS_vw*6@)4M)CH;OLBgiN z9YWcb$-?HMX~H&#nSy@DJfTswSIF<&BCI*TO^`G{C_H+!Sy*%Nm@sn8Lc! zKE?D(IfLqQD`?BU1nSZrMHg0SleD1%rM>?tcncqdB3>0 z-g$%Uqi)kF%ZF5TzTQMW%gggIyP@%j~DJ`lOCO5UW%95sI;4G-{qTZSLZDz({_{96y0PSns2d- z2X3-!ZMT?ob{!;ptKhhy6p3afFq%`0vK58cTAGKcH9YGK=A1a43C+?>j0()eaN;bh z%tkX610RaeGoJT1?uQ&cmf?(d85V?cmKrX~Gyz}18!uVO)aCpeK!b>?A->(phClw=^cS!Y>#kjw-1gASn zus5v)mRF0=kX?jj)*gnM=|i$k4P96X1` zqI+fxjE}^^cVIkpCdWhTek@e`$Kmm)SUm2EhVhvQ1bqs{;2jaj>JyCvYh$qJV=PX! z#G~9h4(<+duvixh$=R_`J06QZ7sVKLC>jeNMM1PT2Dk3Tz$Yjc@;~_J$;Louc@%U4 zBOx0dfsx6vkN8B?^j=JJobf6U-*={YTaO?SDa$A zP4}?Xt?StD`%BoF=GiPOWfB_^+{DyMO4xq`lUQnrC)2aFWDAC9u(CQSmT&i#R;M4O zzb!LKX?PS3%pOgVV`b=oq%6G-m7xh{QZ!S!A4z?9BCMEkSXef9rf^xgONjfiTG0J_ zMYs|mNr%43(-h;k!i)G-Y>I0xjj9gVA| z3E!&daZ?2`RzU5IHZ){>7A-!QO%DwGDNfFf4lPxtV>*fynKqEFUVkMl^nNS&u6ZCl za=I(@-uf<_nx{|cqnpTg#sPZX^oXo;`ms}Y2eXZH2eT)`WZ8sfd3LW=jotCoVB_R9 z*=7Y5Ceb*IeJmKv;xq;_#e%=I@7Y_rbm0v>P5VT7-d`#2;t%?9au6F8#J!oe5xc5o z%ck2qGgnTS>LolO$5k|e*^=llEPb*uMT zor80KUDy5UT^W>@R6;N0s_BX0EDD&hoc7$_NEa2hk&5IFQuEwPdzT%ekhJ5pSN%At zWSyh*q+8Va_8|=(_>wZdz9$dW_cXQO4FwdpQdHA(ateM<#uHm9MYjUc*NSobPCnMn z&co6{`LOoP!Q0qO{EJG(-{ut9?@EE9R|&H0%t{#bqJaSO(`Kwy+R=Jq2jL zSb$}(3NZV60aB#@|L+;k7P5-iqe57$FGgWV33P{+2>GQ1f7FYxc5@zBTQ*+oPKU;$ zRCwg3KyPIljDM$MLsuGBea}FCcP8?mWTXG{9E{zUjYqjTFc5mbIRl;`gp%DLKiZH*k z5cb0L#t((vVn8;YI}6WO=((4Vr{Pln3{)>k#Z&oI^z2T=q__l>`U+dDL=-Mpg<<~v zAh@3jf@5|dY}JFHr4xunRRK6-8HfktqhS~m4d;!~*s~-OiSq;oc}Kz6JsPhCmOWn@ zh5FS|xFs0|`@9gC@eo`u3k3BG#xeC!I3>*v~-BjSa%*LBUw~C>Rooff(Q7g*V^b(L8W6oF7g? zul*!U6*Ad=?t;~iouTLm=)U7P_0bsvnq2VFhu{$42o>YeFwL}p`-`C%b$JLZY=^+% z>_9wwppWE7YVh__z@;IQD02QyC+>Y9ozqY0>FjGX$E%47@9(C;MjMHJUPSk+XOQBY z60)3}OjU|uLYHx+^U*t#QDtu zGF(1Lnwu*2vc+n*n1%5HX8d9fv-!A;4Vl!$e$IT$W@Ib!@FY!sf3ynM`d7-fhi(#i zzv^(}&=rktH4!aw>k_SBF@$-29m4+9YO~4OgP6jCLCo`)28%7#U_Ga_*@n4cY>9mq zJ7_$e%}|@g3SP}+c76-kaZwdJe=VPhtY)xC-+8RcX#x8$WF|8&oz9v~7qQRhs+omc zE*q%+o;k<=VYh4LxNeaS|5|6o`Em>1nN-A&+%Dro%S-s?js&j%$drG37R5vMPT`Jo z?f5bkE&kD3g}*KR%mxpB%&gEwl$Y}t>o0OE_YeGo zq9i?hp+p;I=@R?apFGYEqT-? z(@rfZiVc(^lOs~3)vHJ^`)km?-~nV`8An3zoMg0D)4A- zF`S;|;oZ<&oc)lEtJkxj(wmN+v8lrOTj1A?6ch)g!CP=pb98dB^=lqnrsN}gc|P|4 zD}a-^z>Bj5Fc*4S)zJbB9$Evem2yQlpp=D?!MhuCJMKL@8hVte05L$$zg%3y67J7t`^Ri$r{!+4wt?O3-6wS} zXQjcPiD`0a+Rj2ZtQX0o4G=x8Fcr-$>L>a);e{w>vl>hJt;@nihAbhzKl^mnm`yBI zV0&&$u}JhewpeC4Q`z#3U3;O)eZvj-(j~+BsycT*FT0GpB$e<6 zXMg^pzY({p9l;%PM(}W1Gwv)e#}7_zXZzHyGuMwdnfY-He1_Fs=2f8{ceo2Ja* zK@R`%iki=S;B*-Z->Obpw{_@Zs3GMim{DW64eeAIONSjs(?oL{YW!hO?b{vbeU>eG zZnvbRmS%Ket1d0QAxqmz6ljO8B2{ltroXK!wC}bCNp3Nu+8z^{ILVB*eI7wF=WOVh zpFKUT9#8AkLBl7zk>_47x>XrWGN+@dE+dYl<`ocyRM5zqGihj&uJfh^qk16HYOPZ7anhppYOZfGY zLW*D0^QG@dV^BeJj%pH-3(-Vrr}^iDjsVKxhgdif2L=kp&=dD<1%5iI}3hoS(t)c^!&`j zs)|A^iYmgHnmo)GIQ8UNCTfE-(cF-MNdiwq`_p0iNXU4}nfUKo4kioP>eb3TIKL8j zqmqXmZ*uWjIuEW-b8%x?E`H1x9+&5$*g|-I8*^c~EeA61va$1i7Dmny^5EPIxV*`L zLvJRmJ+hG^+$U*o9=4n0AaH3mSVRVf4o?xXVKT;vr@(T!;QJ!75P2sHmN(Mj9w@j! z=LE!+Mq%%r2*e1R?{_4SaaaC~f&x6pV5sW#KfzY)G#PzQc=qZfGwyH>& z#YG{{Dhe{IW8jt?hq|sf#1Dzbk%$<`e27Hjr7$=?567dU!Pr(3jP6+>IR7pby{n>- zBW1wn$(x&~TXO{+JUWjGpG>2+pK%mEZwk4`+EK;BL9_$fWL_^#uCHEj zsfrW)wOl>_d^v~P3>eRIYgIUJf5-CdJK6EFZdTrTpB>2Gz!troC1jO(?C#UUOgHi+ z8@Ew~Pdu;A_iAf!i;Eh(V&Da4P`6Q(Sw2t{c}P{XQB7O)OXIA_B0`!OX=<}74K>za zti|SDlwh6DV&*!D?6LSA_UP|hwkAiP0(JSgX<~ei-%a+3-DTg)Uot0)Pi(-w9(Gb+ zj4Q58;8`A5xa<12TrEV55`9%E{Lmn35jUq$z0q_vXbfGju%q=y#?aAeW5{HYBiVl^ z+O*k;q~49Ca4B1g*f^3px=m?K$z2{mBVEmjs}Co) zypg1zJciP{9BGypXcTj!Dau|Hm>Nu0>td++Kq~#n$fL~%OKH^RnN)7EkQVVJR4TQW z>cY0r{pWirSo}C$y?dRSZd@gYvDfJSk((5#^MIbjzMvI`FUWoIOL95;nrfcDplIRW z3#?zz(1Wk&x9w{>(esM-s=lHBwwB|ESP^P&4wlWy#_5BZaIa0p-A5@f=u1UT zXck-zv*9`}8*M`cw{$NLCodJ?*N;Nz?G!qgMG+ov5&GQ60(8U|!un|;0-Xw>G&&D; z_PHoMlY_NI*^n{HMBSityqJ)Voza3Ht4f1jd>TT6(sAED1O1gV5M`8s3X5z6yW}DC zOdi%IMHXa+WMQ4)_KFv0;2{(8oR4r{*?jzS z$VdH!0z7(|htfp_5N*$c^6z}y7yMbdz&7ds{oMY99Q1l+qoOh!4SiWS9*_n7K?2Xl zr9sKpOtDNyU|p6nwBwLhh|3C|pXx<7;V%*^~y~ z_o*_}IGlBe#Qx1;xE>mYi2p*7{V4?D-XZ9`6^!ePf+2Z32-VwzQ0N{AIUx_d zIU9x>_7RvrFam$&A~C8g8onjM_PIF@22Js3Pl$)~ni#CQ7L7rgk@z?!9J|McqGDSp z-rf#HvP1;b45Hw)Q24rr5y%sA-DjcK)@+Z&m6k}@P!zUKiow9BIK2EGi^7%|B&-cb zPnI9PJ@J6)&?$HoG8yZ>Ov38;NvN7BIQ}axh&G&v_B#_0dCnQ%?Ky7DW>7NXcs>Xy zTjh+ejV>5JV7;1{ZL=FaQPfoEwP#%^curl z5Bji(^;ztbZZ%U`vy6pFFK01kdsy8IJzlP&%Wpo_;v=-R`Dr&bUbFHIyW+o?Jw9`a zIiJ$ti{53sne@(T=-_XCfHX2~rMqg%>K`OBj`FFG7wI>VdKQo}< zn1lL7{xDb{j ziV;7v0M0=|M!TMaCjtX|QZnGXAOkm^ro$s89kG$=*d{P7@^%{bZx9^QstiabXP`AW z0~2jB1&-um5ejhZLq1OIF2c<@3J~BnzGJsnGxSXcpncxne9BScL0B=go2{!NKdrXjLsn$D<;6Sr_3+|9s3U%*M!+ zENETN#7w75tXrP}clUHqXDaS}6uNbN3iO1{1;cjOP7gc9kAkeTIz>buTJl$ikcsm|Ca2|ORpVc0Mp zeSMA?1}Csd40`&&j|t8&`)>l0JzTKBc>=PpGDvwi;>`;?L^}?Li@_kgvoV0Xg(il7 z&_Ju43dD`%(bOSO?o*vcT0#3?bh&ZO8sk>q0b`Di`cA3UE`Y0C1$b5*!^j{)~TsL2E3wfVO* z8a#CVTlVwsbY^y3jh|Yq$IcJ%Zew!6J zSB|6Ea*i}J+mS2{$5a1PcGNh*nyy4yl9iJM?Ym`8ccVwqqJ%ND^5$4_&KXSwwsv%8 zwH;+{v!R_AZD@bk2zsS&O)-;3lfl*T7GK1;u>lkYNo=Wlm<&pH} ze6k-?O*NzDko4fi)V_Z?4USq%N*A}2YVZMy{&$9sW?Z2=LvGUV(|73O&wI4T;xTnE zdrlEU-_XNd?KE=iC%W;Xoor@)q|7%T>Avwt8n&dJu1tSV*BU?2=K&pLTwjXSx&MNnr~t_?wGX+dNd( z=RuAaAoybeehNNp`rJIo&&WaX)+`i?XJTlY;KhqG@Y*2*BS#93t0V*J0(WXs)3GE! z6BF_>p?y08Z*OK`U|beX2w$tmxd8sw#W=d81nYPi#@CcUq#^7IhIxn%%7e=2e3Y-r zg=cLJULMZD{jglj87FKOdAXROln*c6BDj?nLbavj*pK#T=-l4=35HR985u1Y7!)J6EW<0 zJfdgEW2Al}vYiqzM}B_8<_@mQ{xh|!h__^cR@ z*swU9{1}IWn`7|VJqlHOBd~f~I1>HCk(wQjM+d`^YY+j=!x4!88v%*y5qLf&5|3X; z;nAoV%yy5(itVw8Fpfd|#YlWB4Mxn+Kpaf)!=*V>;Wf+^j~+QA-vP+=CNvfj1SNw% zvqW&b;K&0yc$0OtLQ`rgY0!7F^&9Gu0htV;P-uWWL`HhEFnLT=AMk09f zP-LYVL$*;J9+I+nDkh68;iE+T5Axe_o&0|uqgz{c&;_RrbpGNhdZM|MW>77C5Y^Dl zOZoKLFqZshxRbU2XnN;0fZTkQ1;5tC_txI#78CdKeyNo_Mr;y)zFLFN+x?w&JiN`; zZM?_qbRM%OmNys;+{@C7XRx0#YuKgDH(BorW!^qtnO}OU!cU1*dEW$ezHGKO?~9S; z9p?Sn-5fQMNr$Xxw$o(MMw=Ru`>dCunrD)1X4@Z8rk5l;I6{NjR>?BOLtjP1YsA>j zyWd2nzg5`!%LCZ$KZBTq%V@UmUN$o=EMpQAYS|{+h0NV-3A^vNl-WIQVEQKNyy|X0 zKDu6$x31CPPX(53+a%9NA6~>df2wf(6oGNCH2MB+Z9Zp;Hs5Nk#WQ>#u?;gWG5wkk z>`urTwnX z%cElTa{63aN%?WJX!)7NBsXLQ?S8k0Hon?OcNgrWt9i%B+_sqvlP=Rh**j$6^^iK} zKBoG8FQ~lrH6?d^q?eIjY5v+Dw6*pdNf~~oLG~T=-Mf?4&HGBOA#Iep^#d)+`9drE zieNo44`*_+(KIRxGmoX=$fs0v{mjO=-aMQVxY1dei;W(+7+aBxM8#ZO)yl=e%v`86 z=Hk)1TtrLdq2ql4O8XV!>6v^e2|m4id={#`Gokoa@J^31P_!@u%cC-od?W)+g41gI zmW}Cxzn2)54WrRn2o`$ci`7E6U62LwZ#kF{QUuTT61-9?MZIq^stpQpc|ktvgw82C zTmY%g0xa5@55+*ib$!Z3SCio4?DDZk*e9zh@-RlVEZ>(8cOf@w85ZEhsRBH$ zE`rm*5+pR2;%adzKHe^dy0A$~PAY`&$U^wJ7DDG;0XiQFuW3mxJbq{5v3CXn-lXG_ zehQdbB1&o#pdfTUITnw8H)0SZ7mZ!(Bhb$(5@&>L^k#Yll8YnJPdNg!#3CUb7=fuz zglzOK3Z9kWSiU#}gDXO?^@FfydIn*oZxDh@!yrE=95ExK5cMY-GZ2e&*>T8?6Z+s6 z!Mmx&qog_xlhfm%c1*}>?r})ST zLdm7^@ZaWuv43o_ev37}%(X_Ow+)Pp?XYpZJ^J?>hkG%O5PLQri*`9->vx7T@f-{1 zj7Q)|2h`jj1=IV(aCU_;`aA1D>xvXALc6HuLnqn3dP9y@r|5ak0eaZ4k?dqvQ%m|% zIv7w(MNekZlxO9%Lv?{B@@r zAN5I*S8sjDjB`%2g2?MEX!ZkE8h?kWSRQ0&E$6WH8f#gN%ty91Nr@kcQs%>Sm3jIM zRX%RE2Je`w&FQ)xpLlK~%T1gllGTzI+05&8+C9rz^n2eO(VU;xMQxNRnjyL&`nN@! zjsB_3rayfr(z^Urlvw#ubmx#F+vKdlJg1tnEzRL<@217fZuWeZiFwTE)2SNgVWJe9w;B0Z0hq%eOZg`6Ep z8J9=Upc+g1TV+X0E{!C^J=V15njMu&i^yQ0Gd0*dQR1($bUc4NDT^kO=>~6_=ov!V zrZH5yCW$U|rcvFxZ2DtgNJ^QNRN^z8RORMT+^(fGGJg%dnYf<1Pj9E&sRt?W(s8m| z-9ojq9+9Nn3$oexk^+_AQqYzUw8H;0Es6U|GmiZvhqT|+r~8`%B0I_WKqn19|AX4a zzR~QTpQvui2b$jWg-R5Q@Xs?32Eu;XC+w7q?x&%`F%yX?`Djlsz})4+CQzS?z}j5Q zEEKxmc}XVzJ$5xiA}bvCXyXJgl!EQC2_L2k0JEj%j5b1H*ta2fWS z79-H80BVDT9y`AP=dB9iDr7Cyl?90Yk%yQG0*hzm!MHmgra~^9SDTOI6ZueR&WDt6 zpIzkzcu-h~k4=U6JiSQhlf^ijP=evZOK^NxF-~+A!MLFq5PaO4!$qhI6_~s#AAcrg zVeHcwE~V4_X+HKlkF%8WxX+rdYh+8;#LEk&se|fc^Y%XgrF*lI}yp&k*95u2jm5*&$v84+N|!x0)6j?c=WaFhLfU9<*3ygk8zL3V)&3T2*|ZT|J$Rmt7jBW#aKh;yDbhx*$cbrIIQb-#8K{q z@>LvPHVZj!4F_B6h$D-~z@@dUda!982cW-nrFuySkLF%}J-}+oNcFy%$}2;6ycphSTyvdUWoX9I1*rd0YE! zzU24;zHrV=Ub15hZ+`ig?HJg|G|CUM`47*qk3QF#W8w|Ab?;GD*KYxn*twV4W-0T2 z0RqE}lzG!`B_0~7!u!c-@Y@SC`O3dq-1godrnR|4G)q-Z6zV7~3N_UinOGHzX4m`> z)q30&r6;Tv>D9E0j?Oe-fy;DR8k1zZGJ8Z$&NA%E0%i7nf;DSeoX9?0Ucgqmu4KL~ zGuhGubu8iF5~lKfDf8`E%yNIvW*&w;Y)+C6?=I8hQhM6_>2x(ttMvGivKA(PX(JO` zx{8heSkEjcUlQ_IFT1Yxk2MYy=SPP~@U5r6GN}$l?)XU9FJ`FoMeW-B&?kL<^ra@R z@$ltKUd`f#x*Pa3?K6D+&Q=~9@tbQ+RG{o7{V02=4ed&qK!G+>scEDaJ>4>q9Q8)g ziEJy1F|;Bl?~%lwjij-wM^adbC?YwTThZ;?jfz79vXJ+ z7pa$YQ;Fd(%CrAL>z;n6G=CwtNq(YnRYl1Cl83fYIWX)^M^sD(dcS5NcS}C5ek;V( zrv(^sD-W9w=VC`gE}9qR;(^d9Z|CRY_)HZ0M}WhN5XMu4Lq3#~6V> z&4TaRTZStu%HXb70t1OcoJ%Tz_>}?-Ur>n37X^sSEf9KdK5Vb&W6raD^p`0@s&o<5 zZWlsDc<#~dML4QcjHd=ASlU{G3wKJOeN5?knmrjf8D{Dl453w*pgzAc}Gj)5-1RyiJ9){e!wFgt9kv&N(uqpS-9UITrYkhD>h#)n!M?& z(;U%zCz*C8+NQQZ^sP~r&Drr)bm96=(feOAYsi{|c`Qcx3EOr_mzRDsE%Ccu$~-GZ&TnQ(-irf^Rj&Zo3E^?T$VpRYR=v7jN)d7 z2K-dUXgrxIMedhdk}zQV%4^(Mg>$=k}v|gP}Aa(}Co^Os0|_ zUgY$73N=1)p_H+MCZDO3YkZY*$OG z^A^#|8_OtQek0AR-bD#xE>gSNW10~Ao)nk;?$j7jDDVr5m)ojBRKfTule{Qa3oI1Fd3mw=+mVPaE`kIrSdv%Uyj#|p4m;LKL@LNp3}@V;Il#>Ez4mXNFZ zY>P3bxCAvbN+2WTEr*jODC#JN_1qHZE-Xc0lyDpqFu z;{bs#N799?G&B(#3KKx}3DDPzg0pxOHe8Js@?JD@@}i(MDFUk+A_b= z?uH-?9TS2R`@_KIMPdd;W3R9;mI%4*mPRZZLt>!xFba=_ewcMH2Fv^ecb_h}uApdq z3XZ|=>PWb*6uN0~IMm9*@Nh#YZ1;zwH7x>19toHEhG9WW({IJRdyVrab`9?r5xP`(vHB+Tr#ap?@x9Xi4T!6DPc@)d=)pxx5;5A+|@Cbjmb`^K;NaiC#`}3!|Pnf~_U5sCBV2>>~v+uN>Z5+Ci z{VZL?8dh&*U2;SL2gx)cMMA4KDUYo%hetbN&Od_IZpB-+!{F{8-$rLYUsS<$HsD=N+%MX|H2$?Df=GTCoWN4&?;^j}U? z=j}|sV(wI-5kQyrM3L0D6dKc0NOt$iX#DOf%2u95Q_jz!_U`#~ab_*;zcr774eBVo zWFf`NFQJOXt7vfOM!I_N zh1LU^*mN)xmYq3pTbqx_ghHfF6u6U6gef=jVLvkmXCt%Wc_|m&S$S|?nu~MOa-g&` z2aANyj|J!BjliwRf-9RHT@JJIa%ikBhvB>uCM<%KwwQ;5u%LUg$n;@gixw2dsr z`M*V2^t~8`o~7^tXl#%?nk2bP$U#4L?S0T8d4(!cQsPDd_)BL z+=EcDA_z15LhwPjt&6~!FI%IacPt7Q4n<*$b2PTKMqy)3Bua(s6<895ZT`_nVUgJM zCjudsk=Q*l9P3QN@k2KpX8%I*=8dq2YK5RPHyF<cXD6(isE(W7t53QNX8v(FxJ|3>5Aw~@Gia|r664MgAO z{@6cM1NWuB(@dx9l%088*zb3e&i3_WR~s^30{g4i5U`6nf#U)060@PLa*(qD^UAMEjCendGX+qPqJBML!?*iY!tTS!J;* zi|cAKUtKD zBoC2R;PXq=xPgNvfA&X<8$Z?JPt-KH>`gU(^{P6*606PsE>-0^YZSTVCwVUO&XIRk zRdb~w)x0`l5ue-imYe^Sqc-`W^!S4<&G_s>8|J&x=?4?&zvyrp6p~E#m&3{BQU(>t zP9yVkGw54$C7oPYOm*kHXr}_F((l$}ym%Ca&b1OOOn)0WPCbcgPRP|m& z@w%?0GG!_;eIMGv!YTY#GEI4!PlFGalT}|OeZMk;iks(Bm%q>_dul0Mw2;g~7Se9N zMf5FX3GI_!PWc*Z=~VJ=TIG3;t~j;OkOQwM@W59Z-1wIq#l&FhAdbV~l9*{M55F1( z7|JVQkGnE%m?%OcL>9RoCdNyuXh1XjKm@}9uQ8A5lJ zfv_i3=L-8^KBD>;W2ko-TJ*~?@V{~dPoIXVLNDC3uTXI6h4{I*5HqG1;%0Io{;exS z(7Yma*_Wc$xE$NwOhcGK1sv8+L%GH@oE$I>B86!%KUj*xV~b!Vbn3g~i?LX}6illO zzFsAe7d&6Nki}j+&w=dg9IQ~xho`W6y!zkw#T^p86Sz@%@G(lIRc+AhGWOn2)HbZ z#J{BfZIt2owLBc_UxeaAVJK=6Lr}dw2(gQVj8zth+&h7|K0?T7cA;qA8H_IHU>pqy z!L_l$n6@zxdvk)&Z)Fhd4+bJx)*quDctNRpDpsnw<5HI^W)E;h?=lxWSPsPB9}guP zTf7}H5-09h;zP=4BxjGu)&>!t3}d(!!Ek(!2>N%Nuw$|lWGfudTWE_|M=PW`3`hTn zfv^Z30NULTO~@wT_+5Im=!?xy0&1 zTG_>`pV;}YGF)<&G7tT$&Yg~E@*_j_xtY@tu0CFuUu#t7^1^d8Jg&+6rl|2X(mFiz ztRBCXo6gsDR`Hb+XYwB}uXEQ8%5>s{5s6*3rucVb>CGJvx~UgJ^f{Pn?_^Tbj9hv- zHCnQS=3}Xot!diNT(@@QeL`}o%tA=AZ#lw-$qi!11oyB(wde@+R}fa z4&?lTh)h4L@FB%^CzXx6zdN<96CmRkKI zH7RM-I4j`NBt<+Pp^PVLD)>Q)cy&z%3kJ&K-3vLq6L#?O8fhU{^wE;{Vz62-4l}`7 zMTcd<`K(+R-(_b0(#En*e-O=r-y|+*HDPP z!t=6{FNSZwV(66#uQ@djpG$M0HY^wSg=77YTuhG6MNvvFEPQj(w=WM;6LXN?PjG%t znaCQOA^5vg9O+0#pJ6HtN2ef0Ckf9c#^ZZg6viP6!IPpe-8Kq4jH2)%Dhf*_V{qX{ zEZz#+fNy04Y=!(Z#VrVnZ9*W^EOf!la17FofYy|7y!#f09V^3e*)#%tXgHeVLNO#D z6nYw=*oH6+SQCa*TZHZ?a4J?g7(=9jU>hHV0L4J)91K7|{{STW2H@0Nfo&H9F>g#D z97O?;{OXT{YCoi(@I}aTKd4*#<6V+3ZvXH`%2po?zv+#<|2(jwZ8Dxc6?*0ZSKO&^ z#+aV*&}gv5oC6~ff58GWB_ohnJQ}t~9I@e_@V}(?+F|UT_MlIyPMhN6H+X7u&*e)vE9kO;kMJTwfmj=og6OO z@w{2Ie8>UOfb8cYkI_#>r>dGo6=$A_9?p0z+I+EF^kKXP>&hO%ROXCfryMyuxv-QC zS>DJTLhrNtkssNFF+bVvVe_;N z&l+m-s=em?R!lUnt*+wZ?p1Na&4;)}?Qed1pgM)G9860a?I?d8(f0LWlsYb!?uy6L zlH1V3$ z{z_tk%imYK%fBnxg$`m5mA zT*Q&}O#)TI#yGq#5070+apO}tR=*N*(79rWeiWmhasfgg=V3%s9%_XSxAbE%23HEX z=6yaM>*QhBk8IS)X2V)58!3X&~W z`*{J}4;R4Dr4VnQ72w;-e6+651s3FD#(-S>x|NIa@A=5>6xb#3Y{%bHl*g99>`@Vx zeJX^Wz^;=*jxt_Vh&g$M(C#WimUubl23No!suC-IRbo#>C1#(hK*Ev=xRg)B=i_C_ z_b-NrcoAOv6=JRMwSA8jLVs%^9J~r(BxJ*HTXWIbmJ7%6`B-rzA7lOreuH>069DF#l06H6)zMZ}Llw4Vurl|uyDo<$&NMFbk+BJeII3}Y6BVsu?7 z+$M$L%#skyGz~%V_F%N!3P!L~7?zF=h59goQ;P%f>_h-EssrFGAAp+Aeo%bpgLgZ< z&^XN#ZwkCIWwsyM4*22vB!4)~@yEStUv#+opwV3TxY-Ap&YnWY_raBuJ}A}j!n1Kx z;npz)x35k{`dBw4-kpeyy9_`7+G0$o65Tr@un~-9)M$Cv~Yi=I>clYQFBTP3sc(3?fo%IHe5|93QOqKr`fc9 zPbIw-PbYi#V4AqZmE4cnQET%ED&0MZ+T!&|EnJaSdG_&j2i|i<)ysU(k4=1P`a-^L zSUQ*ZX3g84^|8i;ugrh*4`%o3Bdg#4lA-nsla~9#HpofxL3`wR@eD=&>wpsXeWuKv z&#Cdp5^B8WnkIMrr^cVQsPj<`D*SG(D*srh#*6!@@$_XX+&@5$@AZ)2-6tlnz3F$I z3Oj#0m5lq~bmQkvC!2>IPAcnroQ!kSMDg0kojx1aiqh5Qi6(AcBN`NaP*i*SjVSJu zI2*lOnq`kxWKCV8SjFn)>`cic_BC9Ji*1+SWo|N@@|5{<4Q2jfu?lw=y5t;E;s54o z@;jC4eA7xzo;gT|cTLpeTMdo)HRWjT*jmYtJ5=-RnydWOxjw$>rz+id8BV>y<7mZM zPuhPvg!GQaQoMC4S;*y6zxrx=`=o|uWmnQ6w;A-(XeMnC{QTEB8N_S?X4a_$?NTVCO&6=kRcR&}Wz3@( zwFOibQb%9eB3j?Mn0Q$|rQBFSZD&_fpjiWDb~cd7%k`x4u918O>>zQKgXE=qoOZF> zq~ZROZsdKS8ov(OF?paE+FB(cKSLUk+a-nG zD2|sS#IZR`5>C<*2-_urTr0upzbiwrbp^_QO@ran5_rEX#(nETY!=+rkg@r=L*8%odzbF7mGD;O6&i>@3bkVoeT4*yKU$SUzH% z^YBM552vQ(?0o`T;mE`pM6 z5$Jj$B(w@4=Ogsr@1wj1ML+_d@>ashGHQDpYK|U|#Bra{{Y2BngL-4@@R{;YPt!Obwol6aF5^_L>6Q zb?*4#=YbugreG!$a#^VxX4g+bf+ zsB8U9sy$Xt-}}<3cT5Gc}EP0N#)?KI}6FaKm&(|)mErN5b3=ug&B_?~&({lm6L^|AG43j9N;3V&Iv z#22?K^T;2Hd@z7A!+NuZsobuDpJ1rTo+sV7T-AShWz0;;CgGBO8I-;mU zznzvV8;ULm?H6@!ZV}D?a#7?_EWw6FWV2Vdf3c@0Wcj@U1wJ`Qfgehf=V@mZ_?`1g zyz8wp4~P+-$7p5#eu}`hbPYZxQj?b~)Z#A|>GGU0cKp=qO0My|hWD`JJp098zDZG) zZeJTlfr|E&^J+4s_WDq|R5a;mrPKDu#WZPHCD}R`k%Ch;DYjP7!RMuPp*NP+#kv-)Ut08t)r#nCbo>uPg+hpLsn4h($yrhc@5o{TTjO|Hc_+g7HYTOPDKy)kfG~7 z3VnQ>QbL=lw)QG@ow!e3jcqh6uZ`vwf1&3Nzsbx;49DL~B3DcXvDc(v7ypNz$^W6p z!zA!3D zvalTrE^a_r9ty?_4qo8NPro7zD=LJn&@D$DFUIrHMS_DWguPu6B(sZAAY{o!g2VUR zUW}!uiZQ>r7-^r2arj*+t{y3b`wD@7!t*yYE<~2Kux;$lhl{XtB=UUh;6hg1Q-~cu zim$6LO;vXc>*2646*;6ph8_g&i|E5{)bz4oabzZ0v_ig7Z&$7y^~% zP&|(gLwLVXbR`9$pg9Pd`-7p;ConBE7+d>-FsCI5=W~PbQ#J@q4FboO1Y%1^AnFx@ zQ0y9nj_pDCCl-WnNBpoS))zllc;cq&RFuBM<`=p76lV10F)Jo`mf3 zNl={Uiu*4o;^j(Lh)tY?H_?;u%-0n*TU@dGyepQ!aY4X$VCYZ|rCJgG`8Yu7h8ga? z7kqxYF=iMWqVAk7Ry8->iS1E97GbMh>T#dgRqQdoR)p@d}1`ivm&gU)G;IoZ|#}Kx|bz&;~fxr-Z4OJd4 zYz5bJ<#~6C0$1Ll#OGxz^2mWVnE!ue>_P7^7E3!tDO$CnhibK=__YH>)0=ypj?SwQ z$vuq|9lL8Rx+yOqYV92?YIM*Qy{FTn_1k}mMy-}$R+4X6Ucf&#_`Ea^4^iOTm&o&k zSIWHYkuraCL5u4xRpML5tMHr;YWzU8CNEXj;*0KT@%Stqp0t7UW6|0C&Bj^$p65|M zMD8uuJ1S12#~IS|H6zGPoYAec2^82BO1%e?$!Ss^{a9H}xw6SrQ=USP?9*tsQ4~$r z9Y?zyN0EGqImLUJ(LpD33XKxH|1(=k>?V?V?@jAo2UGEscw&@EJr>1eHnoc09hgOL zWooI;b0NL&UQCgHmXX2ll{7G6HN6U3LoZ_2Qo#Oov^#Di&FaO`gMuFY5bZR&K{?|>bbGzuf z;xAh1^N&1c3VAJC0xLF(VP=^GT-QlrrK==1-4(}J4Jl#UEI{>NG-T{@!1refrj z1oX(nVNqQqGTS4O{xcG--H`~;kHXVBp<_N3yxH_Uh7u?y~72&X5=?k{b z2eG$=p1C&&dj|xeJlh`ytNd|iZ6Hh!2ji_*C=!Q;;)P)-qy~oI)V5&s3QV&s3PPq{ z5DxVO;i8b?vYbPaZyAQdv!P(p!PsRMh`=#^i0JT!&6KJ5c+vwyBs?%)${ja+CSlJv z7gVRapx?X+82Ne<+HWu{Q6CSc7)X&Jq-%3X zxs8LB?MT$k843pv6HGPg57UFXP&};x%W6fOERe>~l3wz*_)as7w6DkbXqXp2S zn-`Vn`89ERv-Kml`g@-n%AVzqA8g`s7B$@X>QsKO+K`9I{AHGj%I!gT6dSzZHslj{lHTm&rI(*r8T|VHKuop6Q zZnRaE*E~_??*w-i14X`ny*zIlslZpeD)Ldk75Mt)id;Nhkr%{^@j9~}7HIU3na$nA z(l_`r9sm9;cb+^m2wo+6c*s^HJ94DxT)3AgezL7-ShT8W)#&M>m9-~D$=-FWY40ai zQTdm(J(T6G^A)-46*<2ByfR;Jti%=et8m9LYCQFcHm}Rlmb@T+mX(RyVD!*0J3t3rbLfqitG&} zy}U5e3k#yBnhvyRq&Ycm97Lyr2hrH}A#`x2IiiHPYZWsWIRlxm|1}w{5S;-}fc?-+w_j+h3Ac&>M=Cd`HtV z-_xT@AIRz62U6;3r-~mP^zcX*HMjIqKk;6=vbc*<`+kwZjek_G{f{o67stV1c`RR& z53$n)!iHIlO>QMfPbfvJSt;7jmSIq6DZbYT@9`}q$b29$M95R_w*_Bsmj^@1T=*pj z?{DvXcnj|Qxo$4zX$u^im5<*NLO=bLk7}6$n5h$>}e zf`==_1KlD7Ru>~rV9-(h60A-rL2-En(hp37)Z{Xh6c=Nrqrl{XLezZ7N5;{7ygMXx zNufVy3(skV;QsWU77Grq7`r=*VJ0}WuxW+xf0U1@EEflq^I&={56|}e@Am~~<6Bw= zyxY@Yu9bpaG4Xg&E_BIlktkRfiMnf%aNQe;ML!~OuutIH???<)i$?y37_@(lLr_pG zTGvM7oQtqap7%xI5if|Ls$P$1jPj*Tsr`RlYOz|ohLHSOvR!h zQ?YHa2aa=hDH-UC#Kkf#cxK@pyA*EZnb-LEsd7c)TAAc@++Y=M(Tv zVIn#lfzFd0dv|kmcyVN0bHw}$c96bo4mxjw2kZJ{aG*Y_w`jtDjxv5NlEIPw-K4X# zjl3tc(#%J<=v(LoTAFv9CRlGLcbhe|yJZpC@1H?wiG`%}JC5#$_|W-X6Y1T_F?4>l znc!j!=x~)fDab02PpcF?AM=^-th>ieFQ4XOn%lYGu!X#3WfH%!b0mL0K$a^?{A0^f zf3ftzzu3;^@9bM|H~X#L!!n0U@&n#-JX2uB>Pt$zV6x!sF;AYadoItXhRO3`PD9}FJvpEBJ}sc@i$8)=?&BgHS- zMw|BTrsCrV$nNVgn((EWA}?Q}mawaoJm?-ZDLkc@iSNlq>myw<`$UcJKhvQ#A81eR zYYJ(3P1{z!p{aA;($@L!g!{D7db4bPl96b1?@MM3}(W07+iPsYGHzO7;7bBsQ9*LkakvNwmue|8f-0>c^l-NC)dr#6W#+EFuLjx8`vyq!VI=%oT&r9T6y69R`u2Rb8fJ0b9;2$w2Fu$2>G-!LZx=8S_u;utjCv&7bt!I<~e2=o07 z@aVi2E>Bj4MTR`wKZ(I{)n_`P^nmQ7E>X>`CR+1(ABjb7p}%G;$o|rNS~;egLYuQ_ zdRhoMU2-EOPX}_pKa7r<_os!^G{{UOOTlv`Y4_A_-t^@e9~yCirL(%5(-<=jEU3D^?{LV?j|GU%v@xPo--M_;I{*~gfLciQ$s?2-Rm3hZ7Rciu}#yGgwKWhIv@o6BW3*76Y5T0XRR zHn%=s%{$I5=SxoQ<1GuCxZ$QVynJgbPo*E+`RPCIsV+@F$179YT^(vZ)}KC47(%P( zkDyOi#t=PorVPypYSJ&J=6Tb~X<@f2w;JZ}(`z5XZ*G3x- z{2;UTZkp@fLj%8bQ^)S_q`J6`4Boz_lT+T3+~9Y#+wVO&J#M3Zi+l863)QXCE|gJF%3XNFclBB=t~hgah2 z!b<2apN7>d3ZPXebi;iDt5oxG$08p?m*!zim$3646+FJcfxCiNvOFSWyXysbs#XXy zry`Vk6yuoC-K<>8kbI^DGqs8!Gob{j5yeQnR|xCa0(8H~hlaqYiPy7X{4EE=W(%yM zO!%+O#9fPY*n6hq;>HZLi3xnu%Yv$A7Qzd%5IHXk?~Ag99-52HmVAh8iXdiD3h%md z+zyzAEo)10uv6%_@p*`Rmy1L1bKz?$_&S9=v|q}_qGiI)AnXgFqw+ASM(C2ia*f#zn!tF9v<<5-?Fe2@M02;5ao7J+q>bof?W+C#OP2dMY&EdEkQJ zz7}l>$E>C(bgqbj&evF6T@r(quo%1+7`9|X9QJ&O!-4iV1Rsfq`LsA}{16L`7t#1} zLD)xoB5>kk7;;pEtHs6)7}HEbnMo3y1nE$QQltq-MXGa z(w0(P_iRc`E+@bH$#l-gmr{*6>5Gk`j;4Vmx~D^y7RuBf{Fj@}_{OD|zvDv`ZgT^} z6Ws9Ea&C}Z&d;}c@&Aqv;Tx7I@aG>S_#ms_ENI3z_WkcKHmFpB?>!^V*Z=Hc3p#(Z zsi6|QUR;qM^i<}XBb504HOhQ|4f;`X7QQ+Y| z3jDBzGOx8%;fbfz`0GqHz9LwQKep54pM{;(ex&ems0RPDK!>mVZp4?rXWVyd4X>)2 z%ioNyhUh{CJ-SUs_+mTkhBM=T+AG~mIhr= z=}&*V2a}YO6@}azOUq&>Qsikr%8XB?aUOH&d*fo7Te+N;_HUq6v31mWzJX@FZlHIH zTdC{nPI`2FKRp}2pOQE3r<#p>DZFqWrJ5h7+9PME_SrcyZN5h4IW5%v^e&a&eoEfo zAJf!BZ|L`?Z)DW;kCr`1`z=#@xd<<}mH9{!zl!atEiNe6uy)kbww+Q`cK1L;V$ z(}sk0qS@`VeVE`|+dtCbiyfqY^%I%S|3Y=!zLV|QKQuUA64{}$a2Z&Jn{H)@kDeyH z52r!yTLl_cRibxY1@c8D(AtxSd3Ay#-+%V2$?1b^O_;p~SJEE4unixCy5GcSXDVlmoNO0i*=kkj54q2`#-@x1f!!8r#? zg}F#jD8S+RTx{5wg`aaWFh(IA!MSPBS(}O%r&96ePa4LkWnlZMOz7Up!XR&9@BEts zl`pxdkrndS_#zYtyG8zl61+WJh$w-R5+`z@s+)ri*Rya&=&6ZgvyoJqjbhmxyxW@t ztEe3G_GBaUcQzK>&4#;u4tDR)!Kpv_h|$l-tC{&YbuSOA2MOGKp9&xEWQ4gUKp`s* zsyNmv+>gm;tU@cdyk7Ay}& zOn?_Qx_H2<-V?pIgArXB3A+u^C_Wm4s_Iy5RFB1og)wkA8H@PDIJo|egVCsXJUA0C z7^FCOIK?98L^SrwN8@W!1ZJs+;m6lt{Jt58L-YNx`MnSJP4NYe`@`JKAE!Ee1s;0C ze4Hm7SGb~bx+~I`Pek9i33%|y2@)g5ptgGy3X4Z!$db`m;pBkSUI*+7bwd0`5k?;$ zk3LOD3@RCm=sR|x|7>u>${OR6hU35v6Bwx&BB)qfaBC_UDldm!PU5Jl`c6sv-jki% zL;CUJBn_6^K`;NTrZbj{DfQt@y4SCeY`SBp@9z}KJ3NlAtu&{|%>&2{8uTSxmQ1F8 z<2>di|5G>-4GcP-P+WoLA=crmDQQ$wL=?Vhc}PVI_7AEVtE*eY$DK22ZkPlK-sOs=4~CKKG@laP9%o^guCDV}-fM zqA^Bfm9Uq|70K~Y+KT+iJ0)Idq{PK?)VT9bP2N3Si?0+|Cb3?PyY;Gbzl~ZvCrO8o z-(|$7_c!MkmKXA$U*__So9FS%lZ$v?)<_m*N7V_I|JNRegi~N4YBR)0r zGY=mlOQ!HXLKB%@|VK4zn+AjpB-;@#<+bP%1`I(+{L@u$v^EB|u}Pv3s%%=2S|dabh1CZt11_C;rl!+g*eK zU&&-lC&?yvkW0u%%Kgz!nu|YDp3Nt^>hhT;zxqOd_;+%W?xN(G-DIZIM|__otYTzg z|F0OL=_Tk0Duc`Pa{MT(Kz7YE6rU-9#DzRWSLb18Wgb@5<_W$f55x3@J>XqFY_=9- zPgV)Gek{R{NP$te1V1&Z9M)kKIJ$oto)nZpLv0$i4w{DK|4L!Fz8J5=N>H$`7~kBA zG0i$33m@cSQg04Sii*(gR*2ilx!C_N8)h+?ko_+Mivu!nEiDzH$tjR&O@Ufc8geIP z;E!xJM8ej2?PfOGh5P5IiV9yxA?Yjz_q~Pp^+{;0ia63Cg!NKiGN6mtCgg0iO zzreJtUD;4A%*Kx@ArCIiMygRB_6Vng~y*#p`o0N z2{RKAye9@Og4-XpBLY5CBJiL*0(%BTVq;nqwBuqByD=6M^5e0&S=cvkCSdl-B)rK^ zLfMX3>^~EQb7Fz;6LOi}_bIqyFa?q0f-t=z3OAO;AW}6JheyT1Tp|vQ&tsu-Jr=Wq zW+b-tQ{JbSVJPu3dg%fz~agf$W0oEO*8r<&`1aI846JE zm4NYu@AT2}E$R3?q6rS?=P8bXN0Z^Xp>(j$ zfb0$`QpFMp+7j`Zht7P!&HkL_j^^8VMb=v0?LME^wIuLnMQeWGqaN2$m*M+N#W?N! z$*MBNxY<`JzVNXOpZDP(OFkgR@mP|t%97=`Un%g-w#xhkSLJg0s(faDHQrmL%pXxX9d37T!vfc%JGA%Wck<03VdCa4F6p( z!+#{n@E}*AbDAk|nf_utTJkNcs<^^hwYIW%a=C0o8)vs(TCoOyJ2sE(n76Sk+py!f zNc-kck#CW$$oh1H=zZIJQOsjGetfqgPZT=sL~ms-^+}cc_h@jLO`3dHfI8puMV0Fa z9?$E!2A8YSDNWPcEOuhm|Dq-%p0}1=ZvE(ZRX=@ZyLwvT7%$Dk(CusuRsOpcTP?X#44 z?gCA0x<)0J?@{;f7c|+sjV@chr1c|Slj6E|3Nh}bV@wiX#>pW6x(o_Rr0~8=46~<8 zAo8?0^n(7@jSr=e_DdEc))t_g72?LoLWB({!n4LA>@z7s?Y{zOAIQV)1$j7?pNESvdDuKV z53x@RaB@vCc8Zsw?P(c8W6QDDaT?SD%hBdphM$@h*dkeh&sWRfSX_>+k!4skxD?~F zigD?y&@YRNFz;(2j=adhloL4^a6TUv(~D3TAZ(QPQc-T1jujs=puQ^;FC{Xuy)6@ugsxh&EeEShg$?#s4)zN>Y>$xB=EP>> z&6aGO>dU~5J*n6;Itgns<8UJ~631G?QSK0d_K*mSIuL;k?;=rpB^C~5N!YEI4E-%h zu-cIb?}j9lcP8NM;CMKA3!HlphUh>)tg4&}yPuQcJ9G;EZVtd?iwHy}$H0G43_@aJ zVOtl23$J2uc~~rBy<=hjI|jiHvDh*s4(~6o_hPup@$pF$YpUdEx74V z`$sxc>Yq`x@P;uhJETo({>stk`Ca^kW*cv@yU883PH-2^Mh;fX#}-v`N7)E|%4j%O zP!T%eJQY5pQI2ok^ zB@Y?IB3F!JFBXnqHrIMYENGwThM%_RP1jOUM$0`>!o=Hbuc{JHX;tQjgsk{GSA~B& ztHCpls`I1U)wtatP2ToZjW;e-=f|}Khju`dpIM~Kdp9#4Haw7*AL-BKGEMmE;~{*F z)hs@#-+G?WdzLf1HXbM=PCD9(bmfl@X;heyl%*Aw)s3a0(_P8Fp9k$9A5Ld}=h6A1 z8mbsOm-6S<(frKCv?g>JN$Ibq+ZtPFob^768F`fK^bb?r(4#aZrJ2mF&QP@SY1%UD z1g+^fO0L(NXv5iNx~_kb`s@FVl3lxK*}z^3y3kDl ziM{k+ff(Kxh@o+jICSPnVdyg%d`^&qt>C&|*c3pQ6~OI40qph_Kv6gbP(GRq^3al; z2S?%1bjgECP#$^$iy$_#1nu!<*eCSI8jlJnIaQ#{pcGZtOYnPQ8P@(NgWQq|%y?A+ zKl?It{U>apEyY;xQiQ=93ZUYj2ld@KSgDbVJ2&&ucDVqt^#Uj7Wn1Y9soMDmwJD98(5eL)hQ8m8fApU^?iry>%LR}!*?Tz94;1r^IuaLPCpyA0BBTj;P7!!lrFlZl~kv#`B53yY(&;9;JH zA8#_y6_klA;kw6@(okQPj6(}zFp!6XjSj~orErK2hQsPcB;EoLPoEwAMFX71F5(IfoAGFP%f-}F}aN&o*wAcU)pBVwuZ)F)7=<+``^z{Y>2`88!`B#7mfSrk!WrULHX8TsJRD&b_Ao} z48fDV_eakif6NzlQAH0wJoxQ{&w<`Jzt{^e?c9M=lVNK;6%OCr;haAiR*M0{I)-WM z1Rr+I4zI*UAktzeWY$^W;sSG+nDxiT-3G`z-4FjwAB^r83pDMrM5e+>SbB}X^o(H` zdwCGPPZ6%apoiXk9mF0`McI5Am?;0E)|GE*gVi0f9)6bMlN;%~^7|f^CaLqh9Cf}>a8<_IYJA-dRc`xHmFIUUagA+? z+}ea;3wZ)Oi}?P5P)tz)-N zEoC1ivf1QsT1@YlG5bBio-O#Q%eoF+7pbbx6d7$#6|Eb6Q#4TffhY}fe8mA(UTdbt z&zLLn=t!aC=4XnM4 zq$L$}dEOifc3(dOOD9N1{tTTD`;P{ly-eZdcgS_)Yr&7UQ-#kbI%@ZktZP1!$?{%$7bJl$ zHEAq(C5^gg(zui;^ujw5IQ~out)Hb}_*NXdzy6~N>%U~!uZPl={*uO_UeZty$0C7q zTZ<*}=9diWKPy6dq#|~5MKq?#<7|%{Tr%ZwBd!R&bwY1!D!`-v3a~Sz0EKVz@gp}6 z9-)HI@)7nwr#!qKk_V|jxiE1kLX|}cR(6y?WpFvJH&h_hyaMlq$IxjiLMoNu#gbB3 zSCyk>V>!%5m%#f)A-sjheLS)NokE{0nVkds&pEIaa*#xEJ~D;JHQ$$wS5DbjR+f!3 zdYKqCHXU(&spx)?jK8B&5LK3h52F)sbw@njX~$#u&^YY`BgF(P(MmvLMk3sdQZVCbDn74I!_eqd{9K-b zf)%OoJe&qas|-kc3M`$RfxlPMu+%mQOMN17WoZ}!Z->EuS~vzLL?bpj9%tN=k@`Ii zqs-Fbke`NMW7ELaCgHZ4z_P|f40;!bWqyJSYYD--wf@*x;Em(Q-EpXC5{_J&f=fC9 znDsFPj;|vSpdJk|hbTn9h=g@|6#5+%I%#|qx}~BKH7FXjqG$|Bi~zq80{zoLc=Iv< z73Tx6YnC5;#`q!K+aH(4`$P7SA4;1RB#VZv0remM!7a$PYta0*%)-4Sd& z8BL875SHSI#jnOe!`U7qA6VkF+#tAB8$s4b7afzeVWp#qwo*;(OgF^77E=tGV~!&p z!_hoyIQ}FKh1%SKn7Kp`G)o(M+%<%Zs0gblDIC7nP5Gia zUSq>lUlsKR1)x(;cf3xUof0_NJ9;R`;izVC@=XoP#_$E^Y z{`;*8Z{MKK`Fst2c8nT#%uwP>$`$yK&x*X%P=$XSqt1UTYw}~&8vMA6I(L@Q;P*U) z&tIr>$!V(m$1Fwep&-v?f@Qf;gU~B&WO;kLEH{}T$Ahb-ct}SV+jQtH8z=dcg`Pjh z4(c9Y2Msr{y{FeOXT=7#NhOTU4iRHZ3`eukF=otW%2$!o%>$yqN;T2l%sHZmj~0nW z{&$@%+o8r=E7bUs3L*D-s`1bRLhl`+&JUkf<;r`N`14u??rNmM_uHuQ8$UETYuD!L zDLVX&e*nL=Y#BGK+s|{xU*+<*TKTV;(v-f|ntn8nrS6f8roM5eib_s*eO>5=sXHBV z@+6Zp{uFB;O{WqIsl#Cg-I=z8Dniy%&b+?iF!JTC7zlY)?_mbnV z{S>uxKLy7gAhCT1$j$#SDRrJC)7oaro_>KIjBcTZeUE5o(g(WX^O-tNeWA`fA8EDC zH<~qC62q*d5qMV$2kImdK3E!Y-=*w7Hql6Wq^61eiMevF;I2#rr{cHhZ z#0${ZnFpVQJY4b4L+_+KbPmtMBE3Aw3%lVW=UnVMn-BNsA}lv8MxI0oHp&!1V^jg+ zH{@gM&I0(P6hq;7DO?W~!fIwN_MFYZ#;sY{yE_xV{$;^&yU^b{GokrA3q~ih@kMa) zpF%QmQ|OBhpEF>&I2{d>1xIy188%CkVVsE#b&`7=|IH(b%e< zh^V6}*yfam`-N%v(vXHF!d5Y6NGgt*Cgb+y1Podn4?WpfTqub|>|KF7i~Z0q#tWAw zyQB5`M8q$fjF6??h+Ywh&kdn4{VwD_!w4)2h=ALF5$JgpflDhQG3;C<(x(e<>_IqI z?+JmPV+h9H3_!MDAiT%;BgMfF+`u2NPWWL&o-cf-`{LIrA6)q8joe>8C=L>sHqIR< z=DXnC$w|<$n2hVgC&AMMNIdF@?d!&2#JjPuo@D{O{YGHrdV-tO#Cr`@XjLiWQ=JNi zT+l}4mVr1aHWZcC!;tK12E~}cI5^N4JN5P8zexkZ2`bPil0(B=3E>>}l_WY|(7*J{ zH-CO({t&9vtl&6-@(Pm$laDaMT!eP=JKezW%3U2O2;uWXNA zFY`^7;yn_w++m0!kJ_ieRllqA>NsH!l-1yyid1=@sxsd$c(LoNRd`3FDv!UY&L>aM z-~-;Ob5o&jM*k3c<~B8Mwq2QD>`>q{Yvg#zNLl_;Muuaz953{d}kUc6G3Pn1yR9?RwV!&6ec zZjlrpa!QF;yjJ3e`%mRH=XUZ%AFpt!yZ88ojt;Irp_d0vGNxViH{ zm!IjsouBC9o(>w__?vpKNW!U48Ws=5VZ2xj+OiTb36sLGd6I~zmBO;Ol29{{#FGES z@g_k6+*t~4nKC%|Llz5P%i&DA0%B|wpto2NE$PZQbWsIQnHnx#P(gQ`5}N%LP<)^e zC8?$8cv%38Rr%PkL*SU;`9;xrI5Amp|8{w>pgg2E3tXF)2YQo(rcv1#FF3R|55co< z%0uItT%3{5!#AHSL|x5??5G@XCg*f$YE>JP?Da8ZXqYB%u2?USII(7NrGBO0`jU7aC&tLd==7g?n^Rq zBa^UrejK(6T`bBh1})+-2(^krr$IEd_C+DmB@*pUk?6V-iNH&d$ZCy5#N;TS&~E9?bVBC;VSWVU$cc%;4#!ntKZh<+W6367yy zF<)?F6BE(+FB$P;Q*qEf4W`2D3cZqw-v^S>B9VlxjR|-x9*@UoV)6BK6waRvMf7NY z1kUwA^EMBBk#NOq)rmMa(+x(Rp7{3IAD-)iFkB-9Bgcorr6m;aGQ!|CF#?`S5$GQm zf#Lq)m@6Ly<@bR&oEU)ZAA)csD*$uU{jg5a55}#&kT~Rv!XLg^BJ@j{`+l%pRxqfjF6%M!#a6i z@)K_G`U*cW^Ej_?-o=Nvt>w&eAx{V{;N`yqxbxD%+;fcJxu(nVQ?h-mBH}00lkZ_Z z1;3chmETPDY7e`cD#fP-%W;*Nid-#9l^=Yl&eNi`xXfw|o~^6KH%(R++`0(srP=`yXgQ$OAEbH@+cj+c9e|IpQ2&I|D&TmSLpoY>om{# z9`#1FQonz1XveV+bm`-JI=SpK-LUyfA)OMK32`+3>Z39J#Zj6eiQ;XNnDrbr z+oA+{M@H9#2Wr@fMPeMa*5*~KP;^6scIR zOFtEv5~=uAnT+x$i5U7f9+^(D7`Zr7;7|l&W=G)AVZq7YjKEs`|NY$PNZibi#QxQh zSTR>%(7s3*W<(*qAsQPs;_zx^94dkm@b_O5Mhp-bT9pQu^bBltPQ&5VA&{>PfT?hJ z&kw|I`Cv?aA0~9Nc-W3g#w+3VT=LopSPwW%9<8ThaJ(cM zJ;x(akrRT!e}sIb?}-FUH@H|%MAJeS*eguMWCb7OX!t=cH~=?F0%0~O5ZOw>nC%<_ z7v)fRzYIa`-(U>R4Md^PQCo)vplDeD9$5rpTt7bq+xVfo!xzWj_`z*g01iC&$5=yu zSo;Owu(iNQcVA2|@kGQ>S6t~Q__4|fc&Io5d(SZVKXF3Mbw@Pyjm7->(Wt*=3D#zU z#4>e!exQU+B!_4(8I(uMf#>O>^qvv49Zm2u(FB`58)NxGBkVn>g`y@^bVkb}?V32I zU+AIG<~C{?^^&%`-=$3j2g!ceT5A3!>r{K|4wyi&%DN-SY zL%rN0;w_)v-opKw&+q~I`}nYj8~B{8dVc9c6*t)s#$Tk`@@wz4`Lj}?@6|}~nW5dR z@Ah~0>){_(JNh@fAMlsi>WTAjp;BDWN1kUXD08z}YCNMvou^xB^W{<+T>PaPk2$8s zP3EZau@6=Fo=|0O@KT*W?NH-af2;7-R;qlz(B=M|SLS23D)FjJ6)t&4iGMsU&)*76 zo6#h}mug9KQFRv!xcGv7e|?W>yI*I~gHEwd&&@3Ta4lP!SIxY2%GsVzGubKaTJ}6C zf-Oks6-9p0WvT z^{C%0(&QieIYWVmj2p`5e6r-GJrlUyWMBScQUHHBa4moL@gmoI{gyYal%W34R49If z0iBB&N3)n8DeQ=%%`=mzJ0p{}%jD6{_c^4lo<%RtW{{O#p(YX7Sv|nN-nYh-G z!`J2H!q(B)$D2rM`ZlV2zLUNv?j@h!`^i!J5KW6bLW$PL>G6^#lI?ej#J>DTmlUti z+ZQ)zd)00Fs{DZ5M?a-Ud9Uce$=B30^&LI>&_SkBJ@ji?AIbRrrR6*NXt=x-s6`5i zqhxVgB#(U&^0;wJ9zjp#V6sIHdKC(&bx}mpBL$RnD`0$>GPcfA!9^(*I1B8GmQ%%& zbPYVs*FferHOy#LL*pw|4A9O;Q)M1jXXhc%JrBjHd5Ch$hn#W&q6g(;_lP{~%_x8Y z6(HwpK6<9-qVr}3R5CJ<=9hssxeQGFoQ~LM0=pX0Ay<@++ac+gZkvv_|I*-Clm?v( zsaU*Ga9%Mfm~b!&_K$>qmXe4~*AhTq;?dy{j|q~oc+nXxc;grx6gaSVZ47o-#i5W# zVPIhdWc*_A?s`1t6eZ%+iUhNpnB+hWTNXn;&!z`NMyD0G==P z$J+Bk9@7ti3i-ilkq>6IdSXQ56qrp0%*`0Y{0JWhbG%sTh*VuiO!OX$6H`VDJ$)23 z+AMJ9njuzcs3CQb0yYWTd61tJ?oF4$fD_tS?Q96^eFM;R#Rxl38KUsDK76;U!eg`| z##Tt-U2qTOJG`Zgb=T=d!wHJX+DCKNZlH=43#oQ;4Ow=jl8s&{orv-vPn)q+1!Fp2 zp-P?y|8Wns4lYu9$dzR;@v2$J_`KVDc;&JJ;s&W%=HSX!H&SwifG+jf5Ukz8_rAHOH#8+khTj-f5 zz9{lr9*TVHGZ}tBV9a2N7p(K`eKziR3sa~)$D*x{uz+_fSlZAMX0kAx{rMin6oazZ z;fH0c?_2=u8g9f&HuYm&A}h9B<%Oti$9|Df$~Py!%bP^*AG<|mu}j!;_L8ali}O0+ z>#od};02$h`P9iSyiYTRPuftyFU^bRSA0^rb>2*#XIIb5CT-%)ACK^i^ftbJyCfMo zD3e{U5jA&>qWWfM8Y~__i~XW#-S-rFcp!^9M&yvjgM9j}QcNBiWwdTp71>G6qFpEE zlezt3dK<8sN``HqqdNp&_I4-D7`vCOhaDhuo5Pe?dX(NJ9;YI;Q{-WKmahE$kM3@~ zNNyF^$S(30ofvkH9REF_O@|&*%7RuhFnvW^XTGP#g3q*%{iHwVdZ=YpAGK?Vp;KNO z(E_hjN2*{)pDNl*G_n1*CVB>F!f3oEHXKsN{84I1OH#%9A!@jtr-m!T)X~zW2Dfi& zXzizg1}AOY{-OotTy50YYh(9DEmS%b;KuPh%oA9)+%gY!;dvM=WoruIp z-w1q;j)4D%N##M<@-Uy#vh9w z`Qzx|06f0nhkLJlaIwrAIxoF2QNj}=6+K`}E=UzRd-(X}5M)b-JS zv<6ZR3)w$F@UzapNM=AAP4a4?A->zGX2Cj|Y*)c`3vd$Nr`9QlwNmO~?2}}j zgjc(yR1^ockhdsm9j93{&qr73WkC}n;!MvZ$< z(csgWHt&B|o$uSB%Fi8C=2L~P*u6`EOJ*wZMrAeL@0A+A*{aTEd^C8(UN!#iKP7(q zjyym9QJxR}E6p!EDDY?_Iqqm8!DYlgv4>GlSn%X$?99_97T|l5b(id6C7z2}U`Z-# zl=EU9lczGJV`1#b(Ag|;g)@_T@=0`iq!Lqmpvaia4$&t*M${ZI+v$tlEK%o{@1nn3 zXEF_!Pwdlyzf8&M9(%Dzjvt>lg@#f2$cx>kyUOTmshmG067tcM& z>pnH}{j;9(+rz%|%tl#?8rY9&4DD&lP!O*PrDW$YYWGc~JL59Rc78r>^PNUB8>^{d z_Du3vIG0-O7gE@{CG>vD3VJboEv;f(c+KFnPWfN^^86E3eS(8Jvh z9UT9r0Xs!axK7XnU#Nv}8(k=T)rGs3KKcz7ZgWu&7?uy6);ug+mIqt=JWQ3yL*~Rh zSZU_rkVYQ#{^cT8;N9bx0!VDg#ly>jQ`($?wF1w!=4D{dHQ~QcW#FMw2HXy(W6g?m zs7*_UmB2Jd{S5r5%EF`VnaC6V`_{!&%s!ip{cjWT+b|L5y%MluWjsnh#G%JB77rgq zW1K-W>>Z=d)6Ch#K!CkF(hm< zL8%bIeG7eZy+3^Z`D4MH01W8}!UE?=m|5<7gc{@zrGXH7)qEoay-;@EFG2@x@q zk)h^+mhWEpv&|p5M}tsbF1((Np_u3t4D%a-*xBp{zmu;a$(sVI>Pw(E z<9z6YE9mc2OS<;2AARdlqT^fsakoQnx!J{Y+#~t`e{pjg*S@%&cV}$pnQOQ4m3JF> zWOEJAulMJRUJc|jqh)yE3^88y;}`3E-^m8={J>0qb+Ic3VtjX^1Q+Q^@fsglzH+4k z*DX`x`~1{+@C^+7U5EX(nqof5V#MKQP&1ZLT0^!_S4z6;#2SR_yWRR%=U?8)k=3;8O9&?kzb>y%8d-{exCTPfYk ztEQ+0b4WCH0nItIgqo+Uq8XCwXwb+_v|W1(y)M~KV|4b=-S_(`JohL`Dl}31lT-91 z=?o2?{~x(I-J)Wxdvw(P9(mk)NS~iQq`@f<=*yo6WVQAY9l~=ubMY+=2>3(}{k!N% zju;01l*R9}%Fx=ShSO6tP^qYc&TrbV5qKs3R|m~;dYG$k07ol*Of59P=Pw30qN|7h zzUxA9t^o}H>LFWTS759*<}K603`=c9HtFJ0u|ATm4KRA30UmGfhnAFl=xgMo>|-7# z=L+6zX&&y)%)?U0JY1K{gKteP0@aA9AK#Do)(I6E{Pe_n-R=cZ6BIT#G<@j(cEAmo?7e#rdohgu=ySPCx5 zR6YQMPX@w1JP^*30qFOiKbo`r;T95rcUi#*QjbA|O&pR+;xSGu5x4IqAZ$bu_68*3 z=;9QJ(o?XvFc~-W1h=;{9!xqBdLx9_(;5xW&yjF;i-h@~U^IpJpy}jf+=!frPcqJE z8Oq^*+64yBr$BVp8v|4V;l3{jBXmR2HX;oBZiS+sC-%Kkeum-&KFZram)ifr>EfO^2s>aXIY&RY7ge?@CWvqJ{5y}oaDRA;HZea78uYCEMk39O8kJf$1#b>|ca&J0#XWL(49U@+;Ow;{LX zE>!b5fR4As(w)1R^!alMEt*tCm%L`ukNFFU=dPq(N7vKgOS?(&(qZZ~Jx#S^PthYf zK@-f5&{BaSPn#P55gdipW7Pku(9mpmuO_s{9%n&(uP z^PJ@7zoe>)57gK9lj>)RL3V~DVy$HGN?IAo%hZs2Rs-!nw4tJ^2d!v*w0_Zp$}oL6 z>^FeinSPLZq7VC5`q=cJ9(oM)Fvms@uFv&RW7ZG$_Y9z*qL1TwdayFmgU)#!<5SL=RvA}F2Y$h zmY&Ume@Hg`?`B~1h;+0Fz0|TuaBg84@IRZ5m4b^3eGrESuVY~u9)*CKa6H=)24m?k zY}y|RncPrJr%-r^g+ljs2wc~O;9zhF1{4S3ep4W}9}$?OD&(%ULFm31h|RtMa21%< zb;S?v+x&#RQs`;d1eSgALv^noa&`RiGv6QE1^;9^${*v-`s4YM02JH{M$NEDj2{q< zX7yOyX^MlQL;_AGC!kFw5x!l?=s!6XLG>wUw-8CIVBX6 ztb$=O-w$0MePAc;3k@@W{Fvp39VWcw9erNJyiHh9@Avtq$-2r(SOOL@F#t#`Akdqy(6pimvrXxZCWzu z9PLcqN^_FdQ1p?-wDIK(nj%+1g=Q(FbTE|em3UB{v=cS@S(0^sBU*PwmDWCzpzE94 z_=#6{c>TN6T;}plKDcWg-}IuMPt3085Be7Ht%qv3#iD3FckURzz{HsEjo0BRmz4R1 zP-!l^;uq6B^oxbWe`fhXl6>hWNuHA;!#CX(a+l!vm(;8CpTpJpeq&7@7OKq)muYZ^ zGpc;udo})gsXRCA6+HV|89vNUk+1Ei!k@G$@eyGPT*6qMyE1wHSYL2v7ZrKR4{;v& z^B$X3bC~7Y?qgrqZeco?m$F_15wkBF%zEUHuzmY>v4gojEMD|mWZ*2uGJBguttW~^ zmue$KK_hpFO2giZcJKcyDxCIK^sY}yRD4fI6z-)dYB=~&RQ#Zv{c60!?teAmqNnEk z@PpypXrnzp@aG`^xm%p>A5|uce@YZMK#8U*NKr?a1X(zV(ZaFP^z)Jg^{rE*q6R&B zd2SG$x;csx4~l4j=2W_SErKjArqQjhg%o(AnzlZiO@USmXr9+5YD+&%zidy@F#n4b zG~_C+sJupT71v22^(MWlZJ~#^FOqlgd2+aQk&dQZBG>Ti6uIUWt+cpL)_0#$f9scI zxA-kN9%-j}O&`g8>}Qg2`cC87FPb>AkM^0#p#7XIQeEW13{+6ps{xHx9ZdGt$A#Jb z;H_o|H|u`bKdT=UdJS=5n<4HlGeCrpvp#g_VX8@ zq!2qV7C`hM59O)3klLDy-jlgFF{lvpB#Iz@st`xAijen2@M(vNa9p|=uKuNv(I`PP zuYiwy8Lo^e!q~+HXjzeuukQJ|t1_%l5N`^yq>tvwEA zeL_#$B6xZpiK6r2=;#T@_V5VkZ4Ae$vtgL>FBE|vLLd_!isGXJ%MJ#@abo~J%lgCP zfFDfa{qR+AO8blaFd{=Z!i63-T{xBrPJD$Qv|jlk^06Q0-15WacYa97@JEs0t1`1f zASL8F$8(X8_#1^)XJTOeG7j(465#ze0e|Ksqc$f6ZzEG+a4i|;1Cz05dLq_43J!2? z95yA#;J&aEDEtaXu8_03NiS^B z@W93$lVM=xhAlZ0(EAdQi)4r_9*d6iR#@6?j^pm8P`Wk*-YbXTVAv4+=^cWBGX~?i z%RpGR^n?2Vb!@q+jNLZ!aFP~wroujQ`1ylO`hTG>j-P3F-FM3P`i&;2f1+BMc5*9! zO;_U{QAN}Z8hhjd-QBRCV$wHLU*#%V|6~CT7&@KaeJG+jnPf7)6GV!66REV`juOWX zrh#%gq%}y9B6P&a|JHl%yZRnCJJQVSWw!HZA(zdZy^K32&EaQ8RPoNtRGz%hk9(Z6 z<7*U6__4x%+^A85m#Hao+Y|C!-Q+tHiMO$1{_@;^yad;|EyY!o6?yIdHp%`Pe6*@I z@3qt6+H16V=n+jWZl}dBELY{%P5<}%<)ry5Q)xc;kvvbjuf(rvDRJvx3fxQRptFkP z_@s3*d|j?M-{1L=Nk;Ew!+aMr*^!|vQ$LCg)Kg;(Kc_G+tCy_j?k~2h;WV4Ec0N;m z7Q_l$?b)kBGq!rsBv$I9#Ex~`7v)qx78P7?70r~AWcx>s6}^uhBDy|QPn0JALKJcM z6uW0H!!OJ+;3pV~%? zl1@_TtP7-Rcb%r3zD1U=@6jvvnEv{t<>q`sS>fa@ehYu)f zOe?iVzNNcYzfkYVE}D5k9AfFxVBOMi)Rn~3(7zO%ErttsWKh_t07rEtNLZ^vJ5(Fj z%=965uOE`S4PiOf5Pkdm;nHS9C}{M@poNCe*lK{g2-Q(imp&JLIPmu_GBKZ0TVff}7ieCl6 zC>I6d_!xi4zYw;(1%5c50j*<8>HUd3$VFe~CECPN!nxfFe-C<(mAokq$rXuFbE%*WyFfb@&+%4K99PgX>IHpqE;`o9$oTHPw@9X3#O<(Glz z_9rcon96V_Gv*vCme=J=J{t1g!$bI#ItyNQE{JC*Y~_1zN>S@=Luy<(l&-~^k@(|5 z^w4t{Jxws7j>>*?JKL0I$W+($kzgGOMnlmD6TZjQ0YPU9*Ia z6t5zKp7r!;(GIfs)Z0?3HsUU7W5W(@usUsoZq&ws>s3%FtH#z- z)zDg7g+RFqyt!Qlh2jc4Hm!tyLIvXU%kgeT8SKnTQT3%9<{QgV7BUSFM^?Zxr~)op zl_+~$iECArFmSCz=BY~bSyw`IwGzwHDv%#j0kbEi&~qq4>h63L=oP^)sR&t~#W0gB z#@g^g6i>*KtCo7hwg=7XGXA)O#;#7=a24oKltqMgLkeUrl`sA+sQt7A?}aeyZvGHI2=nx3y-}z z9BFMK=x-kY{ak-MobHSBp;OTwJq4+6Cd1#{6^a9#VQw!%kMvmlI5P(6FRanGZ#X+@$Qdi{v@$6#cX~M46Yi)4Ol$=y&Zh64}*KJF-wTr3vBsl#`@P#E1wM7lakXD!Tt>2sS=4-HJnuLA zc1)bx3%zl{YgyhqM}f;1De>Xn%DiHgDi8ml$@dOY=f7Rl`5``&%x4?Sj2UH@vsZan znbSCV-t|C%SKj!?q{|<$3y1cwQH}Fh#rjmXl_FT1b37{>l+2EfNMpM#h^+}#WtYC> zi|DDAXluQ^sC16Bs4Mn@sLuK*ixys!iL3!v$u;C-p84?|S~K{8gw@>flNya#GmMmT zM$rTfORD-coW>PeQ}2Frs>~Wm9-j6z)5@8o4n&Z~?kuvLRYgN+9{nhuPm>ofpoL58 zX~&HfWU`@w0&i|68QM;LE_*0Z{4|A+yFqSCA5eqc16uOm6Pk49IbFKmN>&@6P+9&% zTATQkHb}jqQybdoqRKb=rQJ)dJ0u|cPznzBWT7!u9$!KgasRv$=6_K_Nk1jH%gZ3p zR|eArzCFC5jM_nJ_+hLC-5`B*bqnrGX#k8i4ZzT?{ScVn9~LVH;JAemrb_n5rbs=+ z?$X7tpSmboV}Pr3jF2?h7&RRxDDoeSfB+MGPBzBiXhSFu*2U5F+K^qPjh2Plkee+q zjjEB*T8$YSt5I;I5(}d%u=<0*pSx8!%Bs<9Tm|hBmGF5}0XfA=6pAY0AYO@bAW?It6kLyG^$2teHE4{R^iy}3RsPshBGZC*f^*NGQSJZx~dTV zhYGPeu@L)j7odNousyuYMZ07+p0#D5&m(6pjpo?dM>e-V}@{w*+T)C=l1O1Mu>kALPUXamgVVL-d2N z*j(tBy*?N!u%hOJkH8Xt{C+BAx{V>wxE+cuW5RK8b_Dz*qtF~44Ud9o3>LcXpHtB| zBJ^IFJ<(XdNx1xa6mC3-L|0fO@~=eT+=NIRu8G9BiYTazjlvV57Z3OtjAH>lP->fs zqFEC#u@`7RCc-!k5z4d(Po!KSztbHi!#xq~>w{hcUsO2wVuz6rW?KD05e3;G*+C#>Z8Y#7C4IR~3 zO0$N}C7Jov)UKIN8)n6kWr8>9EhMsOwx+s^2bl!jp3X24dZOO3isYD&F!D|v4_P! z*n68#>~Q!`c4$-&^LCfz7Fr5Caf<>Ew^!t@ca?b*RJnh%2LH1|gWr6v#y3n-;cjAz zJouX|*R_@6u^(i(#A-#ZzC@mz2mNAkwHMjx4SU(QdyCoQf=t%0W-@czs>TwA+A~+% zQdTSa#x}bt@{n7K-1nzEAF)h~A07If`TX3=vK{BMpz+D9pe~qs4+~<2-@;hSie$E3 zYb+yOeYX3Oy-1-?O60cdi_>P4b555c4vJh%b}hXq&}v`0Z&N9 z@(G2=JRzt*C5?SAsKTX#{}S`X+(b<7JVaDG#wfiilmW1f^#xxN}n#v&~g; z%Si$LV`O0vE|0Pk%AgoktP4@cjZAF}YBs>B#QxCMF@`t|#3BvkmQDGDG92kmK z+94Re`khcxk)5U zVj|(OD+>9uqHtP23WtwJV8^5g=+uUzLN^>`Kf(}xM{s&W1(v;vfcd%zWV(mKWk(<$ zZuElRA$Qc@nt;rmz`schx80m@)R@q`+XbmNC&5^03Y^z@B2mg4KfZdQ%iI&r7kuHm zA`tISL?Amg5)a3P<3Lm>zAXsE#R8!lkN1VsE-%;*_JHjYH#|7u3c26T@HZ8qz;zsC zr;bNqiv#MW+hNUCYkc#x#GFIs2(%iC^YaJ8H_jM;V){d3oB>`<)*ao_`OFndKpSfM>1ZL0B`510y_ydg$6j(*_f zKOS)lqpN(3_DMdpVmDtmbS0m4V?MWZspN4Nqxm;YH}1M|0?+!wc=rrD9{gL2H#2db zEZfV19lx^NLmkZR{%JV{3Wxh&Lnb&<(;~q?nw;O5j8M{=u>2-Bp zTA{}0H7N1053;;ZVB$F~8NN#`XP)a1Ftyw|wqZvqYwU7lFGk0*HX~1F z_;4@_%U#L7d&=H^zv4UD!`FeOBRc2% zn4%6prbFUS$X_I|&90TMCchIlpPyuT;V)gBC55_tX_U;Ahi{-FPVG?!d#ZvDW~vBq z(m=#>71ZY|L03u+O-utK_z9+X%1Luu9k41Z&U_OII5TC9cI zAU%AP(Z}Oy+OYA~#u$?tXulRX^|BH&Pb#29m3XUFjkhV&;WA}9mYk@8#^!45{Za*Y z<7&+DsD_-V26IenaOFoewENG1)0*j!m^K|_Pfds2wHg?xRKrNfbKa>HxGz=)vxp-2 zUoFI{?S)u4vk+TC3vpK9nq6Wsh72kZwzgty4iS#Ug^1A2!=rJ(`n z-s*>?WkDzu9Fv%NAX;wu2nGPS`_cMgKH;Lak1 z9pF{f|9&1%B>F~1W5mcvNYzB(#+h*F{|&?A#xP7=C-ht4cy%QVTLqqdE{(v3^I^DN z7=om?0a%mfjnk@=Km`-9a2v3qfnm^(@kpoGLD7U)%y)*O)=JcnGwH4hAhSfyWXf7(LZP z;%*H*dL@g`zs1lz={JSX?xdzkAE;)?OL{H+kTx7>p|hthlH}pj)M9>&t{gf*%MEu@ z>Do@H5jD`O^$#K47^Dzc4_Rr`fCXY+ZFu zy{dehkj>m0)cILwC9dbCz>m(A;c{itymp)%-w`9m>&{(cb$iyZ2CD@2eWpJ%85z%3 z4c^NVHp%e2k@9^1Vr6c>NuEDYR^l!qc~0rVeMk2&gMas#OZhI=pgfQL^i5!yQd8Nj zP;W+Kg4l-*ZtQ}k9&`OQh|QDPBw}V=PPc8oI-SVxb=q6fDVjNEIqRIgid|hL&tLp! z&WCDb@y^pbc+OxMTK?IHZVb1i{ab7)EqN>{r8v=+j)~OqCX)UelTOX6v+3@yX{0!C zI#s()r~Q|f(Vo#8DWGOMJsY^6tQQ=ixKk%cC;JQ;_q$C0+25w1)JL@a=0m#m>ORRI zzDvHwk7%d=V_G)k34Px8gf7i&6-f7ze5yXuBIO>MpDvEKhhfS-yI z?#@=l*G^TK-4S-99u?gFp@3Jm^61>Cf`N$|Fm)6*#zGxPck9AiRbbKA{@DI-AhrgY zKwRjKPVW8CSgZ$6aXq|l7dSRn9}O$@ps%fupEY{ewnG>CKXsrkr-jthTF|l4L8P`e zoDORu_@gGo+jStbL=Pe9+Ay7{jmOWb;47+vYHB6A&Q&1oze>EnS&fc}>F8QJ9k<$R zg!8nJ&n{F$VsbV9N>0aR(R64DoSSJq9fk@s@Z#`v@GaAEw5bNIoz*BGRtc+hLLPfs zf)~C;=;&Vrv#*7Cu~*nc&lh6({$kvgC=>d22?__4V9=T(*tQhn{^op~5;oGP>>N~S zW}`ed8=IGB;bB-NE*(op`GHit(Mc8F->E_dOGbcn3c|~T%j8la6Pk?R^aSwcSa`a` z;A~AOCJ1}L@-jbc^zg<09A9V&S;w~02jR!P(WCDJv){h3DfPpOr~a@s4MaDBaAI~K z?mY3wfTaO29~p#|4@02vG#rO5qtJIu$YPPg#wihw?s;Ljnj z8*K)_&R8E)z11NVCykXpV)%IN7ukk>A=||tDCWm=TK4Te4RyFd3KGpUXa5muXxvZd z^>>o{pUt%5!g|_axl-8w7g9>oJaY4^rg`>-lz1$T`s@N|O8g|6G0K57FIdp6u7Tvf zRh>E>$&hmMFW$WCEq}Y`K7XlynVZ?4%%3 zXP?R6%?Rbo;_djJ`3k(n@I6~@`jRy*Zed4LUNDs>FWJb~GThr;i6`dj@ZooixR;y} zKUvkE`zC4dYl*7-_DD_cbVQvW3sd5Y?Bx02$@2WWh7{kVE5%p5|G~^0kF)%n)0q~$ z*hR5Qmc6TyDR{qTY1Y!*m&x-p*^2!7e~SF{AVq$uU4gsZli{;p3AxPlIZLlQ%qpHQ zU`GxovY&>NnN-Up)^gF4Q8{N*{k7TAZx(D`lQx@__{Zt+Xc>|J9A(jm_D7;QVT;)9 zDT~?p2kY4&)q#BU##+9q@*2N(Tap@F2hghN=422tj)LD3oqNpboT3k{HHf1R!Nv68 z%yinVI)~OCTS)qQ=hIvN#Uy3Ak!t2_Bmd3&=!evCGHGZczqn@FI{6ZvopzU!);_0z zaWBbm%_}l6e@SgG9#hk@C)BC@lrBrRQeV?ciaqg`8eetN+FiY5mMDdU!Lq2bki@K7 zDLmk^So}ulnO{}W@m3uM|EZ!nSp``i<%KSxfM^|cm`~HhqBLFX$Q3f#FkzD%Y>2Eb zBXkMbtm(87B!?Mce6t?j8S24BLk~^wbg@T4AFqVlrtUSstylwuoYBMR8@ixT+Hf<} z!i!CssC3o@OVGs6QcWCc*2LDgn(zzM!u=mw@Yz_2{5=(j;uW~Gw*r2>m6)nggL%z0 zP*JKuySKo#=hcYh)u+c9V5Kb@a+P(-(^+oKY0A!90#*D5|ypj$_`n@nL3Jt?BpD;)Wne0eH82T#1FmYBG zcK3zi-iT1VTr4=dT|v+s5Qv$3eBn6I3&RX2BVE!J*KTmkTI2+haKxjg@i>^zA!*@) zv9)d}U+xa$7gN#K-wR*1c_aHv0NPdsqj*mc)`SJ(wv<1N&-q~T9xpT-dg9r8cifP2 z$CYOjv2l$vT+ch9SWC!b`<&2k(Rg&69tV@Z_6WDOMPth-801=G-I`I5sI$cCQxhF@3a4g6bhK$=Xp%e}1XmWgq>omt>Y`IYpXv3LHe&0Z(|Pe*RQUE1DTg)F z`pIVmAAN#$K0ZKmWOvYorp>e=U@hezSx&MMb#&Wh25pxur5jag^i(mNRyTRlT?Yqx z@_QIn=4#UVv9k1Kk2oc){lGIE?{TWXz{Q`R5$8#+d z`R=(&++w9Ne{7@5bM9#Ha&c9Dbf6mdJg3TA{N=gPNI9M*C(D;j7vl;~{xSJ+t!%`K z^=#+P4Ce8)jIGiJH>z*xy_SAene!_9Ca;QH=GIO#UGKLI3*;8XTky+&=atv~#i%UYt zEVqy*zMe}TW9#X2{xY(aTu!zzE2!m4Jv~rtpj5{lBtG~c>1LgzOxOP?RqHC93b{_( zpFW`HQ(n?v?KgBXwVfVrX``c$Uz7jor<7{cN_5}_Xo3Y#W*fIon*2Z4Opb7CMfnV~)VG!Mjtj{ZA?e&P~DO+$0oVNrvax zWJG>Uzy@ItxD%Fu&HizCy(}6h%Ojz?E*Ovfd~xc$kC5wpu>FuXq%{2DFBb^Q3;y`F zC_vz3AWqy5!anr?gs1wT-#{No5BGtKt+0cZ1fo-TjF`tEnDZ?J(SncDO$jeWc+)-^%^faKoF;P}LR6OWkld%N^GLd0^U*sj$4|hna(e5Gx9VbE!WZM+olB$s5=DJcZusfo0^5 z^UMuphbQ8xwllPrh_JIq1erz=PFp#_XpSRp8av>sz8%(PSmTC{Ek-EWV&r`r?Cn1a z-o7J{FvA>M#|^`>9fR@WktT|EDd1y)49-rKKzw@-ovZjt1>S9RHvA=(r#&L019xcQ z#T)eE&t=+z^Az#@6pi0|n7$s`OIj}5>9G4oVg_p{X896IX`4rNSEkeaBgN!ykx03> zeW-0Tk$c)mihpH5zC$GFl$#Jc* zvV3x3H`A$p%BruPV688gF)A)(TASV3RW&;{^7L>vd*l#i6R*WQa)z1`gW$7hPL0N zzen#=mEHr|wc{DpA9_cTe?L(AoLZy=A4wJXh^xS&Lm8jeC_(L&0?J3LLnBiQ z{bKd-b+#dL&KTj{?*UNaCTNd0#!jOF=sm0tJ%4@cs4 zkupXPk7M*;H9!yh?`a7P(t?_$26kW6z^O7#tS{8W;Kf>S_)iZG{q>TR_Pnfs+L}rn7WPM}%jMXkSAp&4Dv&&?684)bF)LQsFS9E#W=<7yUQ}V; z$|_t)7979OQ&;brh82lr*pXR=ZrKw2=T(B`XG^i!y9`zt)6jf!8hX!HAY7snN?jG` zTp~Q~<1%Dsu4RXeQp?O2?#;={S8Q6^93> zz^E|^r!FR7(8vT>md8V3c^oEL$0Pho9KP<3frMBzF3*faAJ(6JAUnbv<70e;y&w>ga{{scQxG1X48^!z zVbC=WL#(h-P8b`8imWgk{1t|N7bB3L5{~`@!qIRe1bRb5u%{{rGw%7~q>2~rTDarr zy9vmcF2d@O4p1;~g85W}#1t1HFS_FJMK`pjO~$0@lkx17C$`=6!EFUUe4ge5RW%>1 zyWk1)#~v`PoC4zzH)zMXLi&^o8uXoUHnFdKl_71_Q@65q3ryt4GMgKTH}@m0}pZxtmg5ztP59 zAIS`FXwcT@;Ni0t3;L~dikk?&v<~}P5$lNX@2wS4qh{2 zGjGvb!Gmm<^7@(eJSeS!pWd~R-(0nw>j;}bljITZ`(z70EV+~~pOeb3j2Xgjy=r44 zZk%I#u03EwDxR{7_y4hIbp^gjO_BdAQsnDKsB@EV%6!)pWu7RwE>lHSe)ys?*S;yu z7cUj#O)5WGa%3C(-28?;4rpe}HZ5R%l36U)dOkZ7e}%a@e_>m=6i*J9=Prxo`8Oe- z{hBAs<44Qzhu@_6WoFqSK{0sd#@r|}l|3X$o*WrvkMoO+1&?Mb+GXkVqPU{ws}g zk``TbZPkV2Z#~S)*2Yrx)MxpI+44yQ^Am)7}+CN1Cei3+mC2H8x)Fuh6m9X=Vshdppbcuv{_ zJRuk03Gd0CxO!&_HpRQ6N7oI#X2N}%Cm_<@84~>oX=WVFMI8O>MQC>#kBAcvSdnCp z^bK}s95fp1N=D;=g#8mc&E( zzhrl)mvS|`X`SkK8YuIbW*M|o>ZiAK?rbZy%iX8@v#!(Ez-BVvevERv4^ryyo#cG6 zk-{&prkh^%RO4Dp+MP3~u&S8W1|<+Z@S`tdoT+r#C>prdm{wb=)84}pG;8T=KDz51 z|Fz^8-#2V8uj*>#)8202e_Yn{*yS6!(~|YPXVxaZ||mWa~E=%^L%;!OkR=OMyT-9H7fkYJ7s?GjS|m) zs?1j(Rpg$wM490Jb>z9qS{XhiQ=0b(yMgU3Nj~S#S9beSE9;AIW|*^_iH%HV@lzey>wbfo zT$dqhxT4CwjaOjvQiiaYc>~z;q}d|XL5d=`%l$+b#tjhdsGP+{5AV;zvv+gauSugM zN6}cHP+F*yMY6LC$n;|_ts7cMLx<+j-S_FVeQps|yVua{+WGWsZXJ~rEGDnWdeRqP zMJ74xDYAA8m8R^X-64m_%%Yjz4!c7qf4roDzMsgs`4ip!)JdFuCpY}0U1Pt~sV_gN zD&ZR~6z?RN>hEOs?+^7xies62AMH%)rc*Y*Ng}0(tXjmd^oR_auF0dKP64)RYOsk_ z!+B#3$nI4KK^Z+Ia=722fNU2v9Qh*TrhNubZZv|;qXDRXI}m|?4ROWO5S{S?-F>!l2BEOa6 zyTddXubzhTs0#GoQVCX6g*gRP7@}VVJ+CUf8C!`N`zjFmY#Ig*E5}~9QdoMILUgkP z(k&&J_^urBuPR_2R0-X(N;HU9Vx>g|Hl>w8-L)7am5UG`Q-H&rd2slg3zfPYSh!^4 z^vX;GCuAV0Dh*xRQ!r;xG9(r!VC(QWc$LKe#G|0ID+)VjM`GK~a5#Jq!z;58 z$n*waqL8;d5Bp(HjvsQj3tjD*7qriN!SsX|?#Ox}^|&WaU-m@lCr|8O;)TpDeu6Iy zKz*zqtXTk#j`GL3I>BGH2jc3_AUH_`PD_UV;vvyt&!I!p5Wb5XZWI9 zN4eVE%{=x?34glOoc9I1W-I(Jv+gY|%x3Q$Rq~*w*3=H9F=4~S;|c2fhv2msa16G{9sY=*~ucQ-yx#C88ey4-kI05 z9pN3BdSvy|foxSGXcNz*TPpc0eO2 z@b|Nd9(Apy73z(2C3g=gh#et|k0(hi{T#i0{hTPHlUB<9q)!Kb(9~^T$>H}mn)vkx zS&09jP3AwyBkViXY5t%c+q&qAaW5S_Ck9P7aol3putHM5PSHbTm8u;)?AKL~R;dG5LN)H<$xz`Yf zCijDau>lezbs%v-8|N2mL-VgTVuc)6vq}e#VukH;f;Nf&!)39q;B`&Y6z*XV#gZ`Gnenk=P<`<%UX#pyp z<_Y^mu8>7@Ft$1iT~jh~yhU(jmr|gzHwhoMCqhFM2mi=eoZKIU_zjVW9~cSC{0N*F z6^@S|LhwTH{`YtwOaun)P!2$3yFXg%{c-nzFRW|5@g?3Hy=#4-66yn~c26iC^~9Hr zo=B1RhWAi^T=^6zcsL=CMF*m8bRcT<0tN3Ch>U-MNIVsYL&1T#mJ^5w1mdMtAPk)X zFk`PjbZ!Z~aEBL^$9dva?Ns4Bu4Y(2emt(taDwp=C;U6) z2+2QVF{IKS)vrclWsMEa)LWyC+rsdrHDcAQ;T|~(LshIm+lOP+Zd05XZG_PWG~qcz z1=VWuI29!ax5sj@&6dTpq0;!zLlV=@h{5>eZ~7kcjlSu1&_azj)O!6XJ#81x^PjF# z`lU1U=;~2Qj@(BU3%8N`xpm}lbQ%3yP)og+YbZ9PfR?Y1r56Ld$>%JiN?9vf%LmYd zaT;_jUz(aXf8cATUFV5aP24+bAJ^{K!%ICj^S?2x_+p(EeEylGJT7rHZ z;~xE$`G_P{-e2g0mzFAXlW-;8&1AXQ<$tWIQ()PYx9r@ydkky-V-rV=VJkZKvG4sZ zvxn>_GjRCJX68xrJt?wWW~w}YAvm)7Tp51FREh^)li&~ci}OWd-b@r5oYeT zjP-;jvpb*d+2|`qY_6jsdlf9sy!**8r4n^EFjbY!I`UOC?!2|gM?P01nSVi4s6C6_ zUE#npX6)hX^*{4_YW?YE>_m!ni=pGeS#;)5A?@8-LCbH>B12)kUi|r(tyNe8D4^n*oaf&uQOWUSgr5pY4(%nt3X~x0N)U@Y2NuB&oyVm}s z5x(E4Qu{kKxc#6(u0JUM?oXO!-9=KnevxTt551@tgXc?e999#9=C81)&4kqxM-1yP>S6yrN}QXMIn{p{o`_|{3t`} z+-cDBuS9%Z4Ro?((yGIol=ABsv1aGPsf^~=~#YoI@V%3QeV}ed}R%eR90ii zK4CAYn1*3nge~AxDRhN=He5KiW)%rttO%c`l;B`)3EDf0VfD8d&2NOQ^L8O-JSf2I z-}x}wn}@M7IT#^uEOKrpu3XGO*xOW0IG2nEHxe<|C=SDWV&Ih)i@(2OP;ME8t&buQ zEER!<|~1Vp1v5R z>WjXwK5$;(iA$ry?hme`V zaoCh&$c6DRoH!okf5)S$lHv14j?kTR9Fg z4)$>A9*wGZHi#Z)gQg}c{8u&-?C~f>`&lEYZWJ8T1xI$=5-x8n@NUX5c*G6Fr%b`S zmTST!Q3XGLD4~@pqlJ|4Jw*X>rE)MmEsd>;lDL1UkHV_}PC&80e$f}TFQhWz9hsbZ zP7=TGQEKTmsz3f8waT2JJc<1@W9|+bbbdWOwOvk&PSuj(+#1?Hu7K{2jiEIUr;_a= z5h-+7(!=dW)P6*RW}la)>mFZt#fjTI_VXF8qv2|paXD*32AgzoJS%DH#~hMnnd>fbCOu7tJuX&brWZBX(}zDqo(C^GU7*KK zb)8I9@?aX<`SAjq5bVR3&7Q?KM(^NrguVBauOAJ#mPDynis{?h8XC8AF4>gVQuoFM zq}EeQEAtjpu0cJGySJIvkKRrOyLQvWNk{2Z=vk_KbCni_-=|F>Voepp5pid{h zkd@O{+Q0Y*g~xS~^t5ilmHniWdtG$C{15f4{6{y|{i6z-f23F~feB|N5%otB&SQnV z9wUYsm&Bk`AcZ@sauB^#fRnM{#y%*b{kJ?WoDQ4GyrWH4`+GJXW> zpm?Ssqz)Nk!|i_f{I4H8uJp&G)c$zL}EGCyf4G+ z!4)_rbj0Y(73fo{g52wBlrNqE-6=D$W9Up6Je-bo0=tH|%tXENOn8r)iJbQ{P#P}W zCS^LthfRmdM&UlWmH6aR0sGJ8;4bB`PAvkQj&C;Z@b{mJ*L_p4pxO=PQzl~GR43g3Jq8<6ZSgAI z4i2gIFijkXFm(}@J2MiEBV*9_!U0iE4v2j{ z7LTvnLtNJ$nv2Kan$2j4{j|mklTo-k+!Dd^78v@_9DVg8P-ACmBdpx2hkHl0(K|^KwaJ>ew^suP&Zwbns47IlzrRaY6`)WphdnwnFx8iU z*xkQGOMlW0^-nZm-z##{e@NkXuF){r|0rLhiPl~^K&2u8JaenW?1N=R|d7mh@_a0S!q|r&cd%^0xWNKX%>bqf^fEpY=z$nbamedj1-o z;lF~5XV-Icml{4J*_ZEZ^Wt&$%eh<1eBLj13128t$8T@U;ID2Pa(D9%)-vxha|pe` z3~R2lOHEgq$nr0{?JLVyyUOz!Lln8Iwh}k_Pl3Pik>HmHN%Q72()_fF4DXTcVY@xv zvxaxiSgh|I_DX3D%SjJldTY+IzP+zmoRl~ptSP}A8zuO)Pbz%XIeD(LMV2>RmFC*Y zQoPYqn&XiaSB?D3+TXlo?e|Wzk#|-z|Ab6tet0y~T%*perAo1tqa>KyOF1@csvf&> z)sRL1C&_-9pKywvVlSGsW3$L+T@I^=;dojMvcV+k!#T}V=o>qyeIo(`U9pgp#mY3TcXw64C1uI;}_d&%`=}- z>8h8MxbZX9Z~aWC1`9lE`bO&}bW!i;9@5GGLmfFiRCZJhMt}a%*}LKh-Xw_`krIgR zkif({l1M!uh1zs!oLMa;WPC|%_$3LqCz9|lkc7*0X~+nyYuWjaj>UG-s`)?3A-9{J zboWyKGg7egQi5NU7F?$a{4qAfQGr9jXACeRHGhx1eQ&`YJ{D4 zjUfBa0JiD+xX_{ssZLdVc2I|Zso>wzw4w9R5Es1$VO{M|bUhx1+}Xo1L&^dxGlt{; z0Z@?apm>}VnRi@`e;>x1r#-Z@X-c~!r1SlLF4;+Y?;%u3(%zE2vRC%rduJ0rBV<=d zs6{rLV@>U zti4+Z`L06nhlTjKYcY~m7vYKg5{y|^g!qERc$QoUnInskbbApFZZ5#R_*@LO%faEa zENl$T#HNesc(5S_Cd~^leqAyK_#{I)W+99e(=h#TDzwj~K;qg$?3%X_@^h16@_IhL z&z*y#%i>X+5QhshXXCN%EU2c>z{?lWsJIh_X}M82p&0=^=PY4Yg)D+6(*1pdrzCw`%k{MLM-8!m6D zw)8)my!1Bh48KY%ZO+i8k%y`G$sW>8+)QO}S5mY}Avw6F()@mrbX8_LRWdQ1b+@3< zv_VuHqDIemOH=EMAG|X85m%abgFkz5oIkTI;r*4@@x2A>__(A6+$wl1zx#C*|8jpM zH;A$24a@xb>&1Rts%sR_`#Xe-%H??2zD9Ph=PkQ+?;5-D=oV{Ub(if>|IUI2OK|mb z3f$wB5?5a! o=Z6mUGFMkwzV((gzttwee^<0Hd5I5f)twVelW$}vjwG|J{`=T& z$1m(^!yk4xONMX#EaH0;<@l^OBEBX;j$5yn=c7aAxcoy|zHF2X?>}9J=RNCYyEoLZ zw@J6yjy^lt&ZhaSc7h!ppRQ>}=NJI-1E1?8lB?e9qje-1+XQJNd@87d*RQ6ulTXgG>+R(2ZZqXiCr;8fd?s zD)ttWndKVtQe8{-^_ywqzCHB%%`tkk;{sJ~xl}i{U+{5CwsY z5G7uOgb(@1|B{20<(X)*%*6PTOzilafzI{m_<3X@+>R~4#8b&Q9VC2Rp8~Va6f|B< zLCJy?=*ccb{`Cbg-I4@Fop~@!jE7T59Ns6)Mt_G{c<~|@M|)#2YC;TZbt3UKI22xP zA;>=(h`KI6+>-D|-xNQTtqj0`kU(@)27+z{!t1;rnilw>c~cPPUkpa{jbQBl5{$Eh zL(%OYio~E0EPLdSufM(VH{KJM?z-aOe^XHI;RKgjJ#Fx)V;GhinWC`B6pl3}5brg{$-c%|8D)ggVk7)jF~UrFBV6h>Lt?rO zzR$6R_?!*0Us_=3is2YFcNp9YjnO^M5ZTNS9xaBrxZeQ10}PP8bSPv(hGN{-p?K~# z6mvX>qS9pu@}maf_QiqFn==4IuJ%J^zCLUZ>R?Z}HYUkwB5a5{Y>HKIvPcnbR&vO6 zl0u7j2X$>|q?ZL>DCOiE`epcpejT|*i*zs19letztG=IRjMzys(i^EPeihl(6;gm{ z3U!4=(fxPR>7+8FZ_O6;-EI&awpXE;yHfP9-w$p(@F5?+7ek+)f1u7UYfJE1d+L}j|H#g*e9zpc z++w9OZm=|kmyBsja;c>%{OUGke(b9P@6MLzrj@O%a-|IaP$tc_y(IVm%b(0N{~;^y zUCn|IE@U_RZDA6BpR?l=TiLB+9qi*gNj^eWhF|$3;>G#0{MImeE<01i8?8m$PROy| zP#IpeN}3NF-oef|Rx{`Ft1R)&2Id_d%N9FXv)3P$newm>@xV_~Y{MmWw)>?H8&EQk zeHt#oc7ML)yuC(ST)JeWxHxbI!}maD9Xpy$xUh+3wHok3{l&cE(GBk4(Zy@djG^O> zskCh25;_>ahJM^%OGP#t=uN-%RBXDA`rOz^{crB0n$Tm^cik~cTX2DNoNv>Sq0dMm z?=_j}e5S(_zfsxj-!wh8jlK=)q_f>!)b+cU>@%b=`K~NRZWm$R16hb_M0mMY0a5*A zQCA>?H!EZiKSu`D^F=6Ht$@vuDv($wkH@x>&{6+KD{?yM@`t}vPc1ZeZwqz$bkOqc zQkdi-heP*Oko>g|R{LsWn}!x1axMI~Ra;;nZJgbs4O!vrFjpIm+jP+>*AEK8gHXR= z5Ox&zL(d)Ic5P)Su2q8HQ&l_}qk*T*nn<$ki;9)~F;sd0>XQb+wZ8#=n;7G_>kw4t zWZ=oXOstK_#dYsIOv=i~L{@;Hsf8&0U5L%ri;=3h7-^G=V6CwPdrg<3sCEfrX$hM5 z6=6?BAzCjk!lc~=xMolQuSlv} z4o(V2%KIRE4GTqojZl1NVQAG3#o;FbSd!z1%QL*NWr90W-@BsIbqcn;ngU-PH{?8X zL-(O+FdRPxgS;l<#K!T$wPXVFnCOUqM~^UVkE}l=%lgOf7=mT zzS<$n!wPdgSm1u9Iau^?%=$A7liN%&Sl<*LWCE8j#)xeJQ+!`!j)__pm@slUX1_5J_;M&-{u&JX!hvWn=!bXx^bu#Jiv~|^+&ZCw z!rf{Z9HtDbC-Ue>lSY7RC&|zINp-HD$>jcP`t!Jqw&<18#iQrQChjPS8}`tNGh1k` zaWT#3E9jKWBB~sufewTEPD{i8!FM%tKA$KU-{Unx4BRCDgHEZ zFE3xTnqP5Q#BYyw;mg`MFa9{1d-*%^CC%1+ZpLu#5^Tu-9@FCmr_{Nml8CGAZDDPT zf3RNTuk7jkPi(aHRaOvwm6^$YX1&QGZZ=(szx$`aWh~`+#U&B9c>9aZ2$kgnM#}P| zGbQ+jfAy@QNa z)}7-4H{bB+l?wFpn+dh`T|j1KMO0?Din7#;DL!mHDVwb$3%&Jpa^g0s6z`!t{i77N z>kO$!T_O1ccWL;-=M=N2nlAf(Ckdw}S|za9a!U#9fA^06?g;&qNH_G7-sI6Y%Zccs$7%k1@S2h%z0I`L^SccXT3N_MZgNr75snFbPxUx*)Ms zjF{zOEEBSXsT|gsBl6FBxL+Awy)IF~qMzLud^%gqx2c#=sCyq78A>&=9@f4X{+!5SIjYxe#)t&b!ZPgW8y5S_T zmi?5ze+NxmwtjZ-tslh?S0tV2PVQR& znx{Ov&6gfJ&TT($=kDY3`P>H!`Q;hJYdslPH5z3jB>ty-yL}`98NS5bK6>bZ6{F zN;Th0>!VK6nbr$*tKuff$oxk;+CI=B`){Ov{SP%PT>p+sF1?nCgw>=jo?KyCsn1#d3({Q#R4Kp{U{ud-7@6k zY<%c53+2~mVBF{!$V`jI@(s~we-nxD4I!BP&=0xkzVJEhi^l`}(3LPvdM9CW~ zW_n^{k_VKgOh$p{L=4Ox5AReL=v;PzkF^Ud?OYH%zy(L8T=3wL3rZx$m$=bb8R>{X2YdWHYlSZ_%yGTh4DDmha9%tNvsanoX|V}p`kBCD zsWGIgj3D>U5MJSiI2U4w4=W8ZTEPgXmkPNRVTf({1~~B80FwU+zuZHIYR6A2n5`)M&&nikedl|whcw$ z#6i$-8i1pR`{9LTUwj>_hsl36;NPQ)_{oZR-6oBhZ~xMzLEoq(@;&K9JtOHM_h|RL zi}Xw47=7%whxT`Dp@Oe#C{1|u6gYFLHuP5np_m!%= zv|g4Mo|WJW(*Lo%xvgxV*)Qg!Citx_uUITEWv!Cunc@8p%(g^9$U#MJKTm;w(H8OZ z!$o|XeGQ9@{l`A($nfv~)v~=~zcHzW@yz;YF*~e&p7k!OWmmU%u*N4ntltD_zB)vX zkK8QcPZJcl-ykL4XT1W?_%7n15;9zMhzxh|li_AUzGW)Q@D2OgnBUg7%y8#PCYxKx zDo>7KX-jq4wApH`Rau`Mtr^6uTTPhXo1v`nO^LYEO-Vd==m2r-7<=)MUpY)1;#kh!LWwSG!)f2V|{GnA3DT^ah(s<6DM0Q-DdSWoyv z5{b3ct@WAqto}simul!lcoQux?kq zi9=c1n5LkEkM)O?+?Dz|gU3Sl2C&JGt_BKS>1*MauC1Pw*Z= zs<^pR6~%{Dk$zqke_yG>vrIS#Xk+r4R8;(zioNqQV6{0DbNl9?bbT(|w&p?iZ$3n} zi!i!=5lSlsMyp$l3auj4PbtEG=N6;Ev=E7Q1-Rpxhu_0!M^Vt6zY@D;D6n-vZ2Bv=Gx%645Yk4n8Wzqu4$UaErt8tl9WF zXBPGc&ctxFSZFjv!KxwxPws@{Yfl))#swiv(jN)0{h+bP7n61T;62_C2k!-9=C(i> zdk4X?H3*$ELNV-BFoK5oBlBkf(maAucrFMMTySXL0x+uC4`FA#ap4wAeMGP zuYn8tTo{K4%W-(@O$d1?MsU3o8V5R}gzS-!Z-pD0mXMq=97nay;n#mSQl1XOgg8^A zjWa>aO=Bqb8bM0vh)IKt5H-LE>naRUU2TZ8=Z0t%`0U?eLl_B%-%Uf@d@uCQu_jP1 z9*&}+7I-ko5+6odVKrG}U9=5k@@xdJV27YRBjL1d6jTdFVt?`o^c`%E58N8|0v`_f zXaXt0&28H_7-vQf#PalkI7I#M^|TK9I;%skT>;w`$-tqjiPG9?=#%O)F-J2t{y#Frh?z@SgwVL$2mQnkpMKpM8I>il%BfS%WRusql56>)=6(){y33H~xynr~X%!<@c;WNA@X zm|BSP3 za%vFYy(OG0#_Zzf-#_3okACx4z48?HqaVd(j-YI}apW|?ljM?uXaQnKx_d6Qb>-4! zw`Ej(yO>(+x6}1q`-n#$ql30rNVoJp$)kdL&Fd+{vV*pS{-ZhfrO?nLiH2iR&^jfH z+44%Tc%g)YtCZ1ig)-h*DWTk7358cxa8_WcKjn%zQ>h4pZ_2p8L>a9oRH0F?guN@I z@hPQ+ZvL*Oxj9vI`(+g^Szb-HnY9#tuZc|Oc2b{s3G6y4jpFqp%sH%pH9wV+Dyxn+ z=mYZ?0;8#DVPcs&t~e`VP^&7Q`KzF6l@eMt)ev+=4KlJSxN}nulRD(kO3FwsRYBrf zH6#l>wm|S_TYsqHvXG6(KMF4Gn<{eL)bQ?28p1LdhzYxl2lCiUNKJMO|ht&(?kft~b&dX!b zW)O>vnrMWqi$-)&6apuO;nw>Q$Q}&Bu_wN`*6V}rr@qJw_r;GEADna(Iwt&3u_pk& z(*j}tG7v$+R=I6w2n;@iVC5q}sFVew#5V{9-GOks5QrOp{gGtpi#rkCcxma0ZxcP> zVC;^d-EP8rJq?3zPsP%&lc8rh5j7jeqg2SSGL7-@$#+43!FVJ-ae=DX1y6>%;Fq%t zW>yJVR^x)9A6yV-Gy#<#CLrwN1UUXT4xx-;YJe>i&RXKWwIz;CvqV~dYy4ei0sT+r zX!$7YkmhDsV?GS(!%gu#+yr#d2r^zqXoC@~-Hfox-w4r*j8R-=iWP$YO0qUZlaNKP z+=P5|u)_3AE1Z5{iKG6OShB#fStPr;TZJbGNKR8mn&dr(m#5!<2UiFTCxDe+y<1ScI z#jyc&^u8(;{q5l;3#<7P^T&K$(+a=IvL5bdqc3e{V=g>lk-voZ&m9`%__4)`ym+1*ehVJH4*Gvl#C*@b`s?C=&Xb}d^1BibiS2&~CFhI(T3{-PX^h zl&Pyo{^Abm$~!;Jb4kR&UIQ^7uul2rtA!%R+oPx(IK76yV>Pd`w=Jhx5fbSYMlgFrJ2a zbt!OPnS#Eb(%^kF16mW);rlNQBR{1hV2E%Z=M0Q{o{qJv(x8=_iZk-bC|jF|_N@sx zHGMWxWM@L}Rty#kebV4Z1ZqP=aXdB%9=`st6ndcAHeZ}b5PD^dFOr`7V&xJaygToM zB3(cD_YZ*1(g0W}1;S}(kkIvlvGY;sf z!P^}W_S0q7DEVcB3~f88N7>@v0c&I}u);wXGuTKO!{hcqOpDjT^$7|{um49%Q<`Xt z%Qq@1eNDv+%V_R~YgEHe(Q~W4q`Gr6-I%+Y298}qsy)q zyC!yccRTZ$+`(#nrFdAiJb(FIjt?Ct$3H)l;g9u1JnW1t4_z(Kw>Zdi`-L*RwM~l8 zo~FRdZprg!r=|Grr|+3vHe-#NVgomq^83@j^0hwFu~UDkrWjsckjdeXwCX!;V9O4VDFY3$83 z%IwIY+m{!U&8zJsv)~BjJU>hAJ=dsn&UNaWbDO$XJ)ucM>ZoOF2PsUIMBOuCht!ux z`#2di%SuD>krbRs_}&r`{>@iHGKAh&riMei)zL3X9UTi*v2KYHT1Lu4^|u^8g(<>f zsLT+Np^nNm!u55Y20H)KK-zRI3=GqRtceD0R%qY^ONGMK6eKmJAVf12 z2~$&nTd8j5*}x2J9+!r?+AQ1{m4lNDv$6C`Cf@GJz`*7VVUNhdkJ2oZX=fr#xSveYLSz)o z$DK`cF`_CSAJt}~=XfkyU&r91VYJ|}!toJiu_fM^cETIU!@Qw2#2u9DkPv2ZwGC0q-K<8!Mi zG{;%N{`v@%PaOeA6+29RWQ&)AlYf533MZ~xLOaz0Z1!-(?lMMjoe_AA5h~sYd=_Dh z{X30O+GGqvFB7QGF~{1|!%>ho99{{+xpNpC{|9$kBfb6F!`ws zf|d)NQ{clTJ+`ncw8PPZ_E`691djF{0VZiD@Tv_urLAzhY#4&p7(jh)f3zwn;ZcDE zB6EIGC%#fZOa<+zc|gt@SLw0#ak}KVi;lQ&q}7X8(TFQW^kjY>9j#BHhlVq0LS`WS z8$5-sYck5-XGi^?8_{HCeTu!KKrd&t^Uo3=`K)!fxsm)nu9;iJZP)nnYYUC|!PzD} zdGi2XI7o+|OjP012g>uxJZV05wy+&~%JP&!vizQVFT1AshiOf1VyDADur=?>nT^H` z_V(39_NM(hGadGp9W<+F;steV<)%iKzv?HmkABM17v5)O71!8Z^>b{5;yxzz`WbTK zgVwI%=IR2m#QPP@>#YLsvN^=BSC(?MKlgc?yE=UzJA_1IZK&?}c-^25xL{!@z7EZLH=^sdRGBYFRGw1Kn>bzYVfvD!zmqgWWQC0^<+8h zJTCaI`y%k`ig=={g6jS%`1?Wz3P#OzC!?C2o>!4da}^yuUPX!bs;F|)7Yca)haL%! ztyNY=XtO#U;#Nk=W5d8Gy~d3lUh6T$APEc|B4Vw#@_&6{Nq z@BZV6bQEuvFB*b0NQa`e|d3nhy4xYQvjrBK4OJUQr4nvQyA? zCk5ZMQn4#h@LdV12tARCw1sJyH6R1rEC)}^bCG*H4;l*jh&`K!eqC8OeLV~REy%*| z*O^%UGY9edIWShu#Kw#aIOgZ#vS}WE_~&A`PBx6*X5#La42afeVpeYk8ZV|JX>Tf; zwUd$lVm=-mNkmL!0^Yx!jl&5u(bYZ!eWYffIxrH0Z-wHCb}+7c`XM&M4*?;*=&LSx z`prJb3G{){dmk7``Qq#YUsQJcVfkNQIGXw6#D0I=*&;C6WG{@f@j_#Z7w%+wp<}l< zDpz|U?vE#&UwNYbg(q~gJkdJJ6E!6sNR;$|iEIhZwNK_mVSWV!y!V-I27;b|oPiuS*9}ewM6X-V&gp-FX zG8XpGMEXT#USDYZ_6mBL|9~V{U#9r_qvW%>gnX85q^aLm(1g}Py04x~roWO%;$tjz zoC=_%Yg6gRH%7}t?dhA8Aw?T%k;i;l^3ZMO`j;wr%>9eJK4UWv95$0XpLFI+xdA_X zVjw@Qs?8r;sPN7zMgG@9jxSs-$BPx^c%8W{cfK#n=d)hs^{<_!88oxNAyw?&*r)8& ztFuweXV z9l2D@#R)SXiHkc#?B!$`RyJ9W>AaL>U8z)GY8U+`|RI%S#$c29DIO?s8Utg54DPI*`N`2ryO$`U9s^aBh6?`16ge%c< zNGp-Wh-=b_5J^M(tsKUhDxz(IG?uylqA9IacHK(d5?oJh*e_cfx8>`5z=?|TL zB@2%oO1N}S9m{hxF?We3>JDk4>_086U9W-HU45`e=$@sw74T`EoX}%LIJ-|4(YIw# zF-Hb*=VUN=vj|_;%0u^=3Z!qUzj874k+3Js%|`CKOh_L~$4aX-I9^!@ zP2UAD{ck?@ixcr%cOLG5Zpx-uNu=SzCW!Y!|l8UzOgNF~|oW_W2;G!W-LM zz3{Qx9rvzH$DG&Guzt)m6v?^4v2i+@m$-vjdm`bG@LZQYanW-+%7#xxTiZCSx?qdf zgDo*QWH?SPFu|F66I^$(!;|MD5CMCHzqZAJ|7>8$Y;e=V4CU%(xLst1jdLt8X|*{v zyf;Uw;I;}*4aZHv@!LsQAm+w!d~z^@UW*C-$(o>Xu`yoXGs3P^BTUIQ!n+(Jgby=D zxsL?~%34CPuO%E0St2>X8tS#yxYN%T6_@Q%H`gAUHrb;++YWb=tWoD>j#rh!4zbYy z%XexaRz?apH@4G%HTAUdi*Ws=7vwhh9)+}Bq{^8`sc_~lGQYcl_G_*rk?Ud_JU5@N zugapPG4p6=bBN%9r&I7)qSd;#w5Mz^HJYi@pi&8vnpDO6J}c!q=6m_Tj(lGCb2?A# zW5I`=>(AGFXmjV~iu}cBS-$a*3@=NT;m?!=K5~=gNvmY|`(|lwnkU8A{_9|`RyMMp zitp^8dj-3BLCC4@E9~Iy%WS&VO?L3&b5=N_nsq(-$g1=oF`1rQOrqx!qi@&Q`s>G8 zf=x4V2@xpuQ0-y!Uv=cQ!$u@Q27 z$N~|+utkois3`E3#d17%rwl*0Qed)2!nsb059nFKvPy1?Z{0X39%R!aZZXqjtx;FR zFUCWB>e*27)aeLQ4b$Rri`=+Y_|g!17D_&WrDj}+OH2L8*5_gAZ;Y?(?ycJ4vxRogyKC7 z94}Xe$r7QLcF4lvkTiPtN@9MpB%b~!4VmU%(zxG5F{ahzQC&sG`>IH8eHBFyt)_YJ zs;K(MSGsUZ0#-_jm{6ex#Q`dqI!PU4M15e^w-5T)D5EY)5eb5;o2smc(oJ$mStG*n zt0H{=A`86+X_R=$pxs9Xfv@C*ZmEhdf)CsIT@!2e>S9A5J#4Sj!TE1mh#IMhT{|@3 z8mEPhfvMQsoq{;Q+3!kE#m6tH@Vb|dY9V(#a?_9~m4^K$X=oAnE9#$+WBYTV{4^IP z1=*1Nk%ipP*$980i_8&1&JD|knqLN*PvpWhJ`X`gd3ckWhfx}NSamE1qb6oUc77(F zRi+|%UoyHw=VO@TJV-jtg=I_vj9O-4W={;-Ya)>`KOD}cVK}}z6j~IBIdy)R_|zY( zuLt8pMF2+J_Qm@;ABc2(5V^)1*5kdw26w@LJ;seG^|43yyB0 zl`kI4dt+m?2Xu#cU^{b1#%MR>#7u|8S$7yu^~6VSZwwH6aFUR*8$G5Y<=$k-syg9A zn=LlCvpBDIHV~+O%hh0q)e3g)A7xgWWal{I1 zZ(Bp+gAL|t+Cb}?4QhnG*!PY#ZqKpC5_fA18ZPXXndXr3HOBSlMmQI1gtSm2+?i~I zp>0Ok_reT!hM42@Y;*jGw!qfEmUuGE8n?1+aBQ6HNS)xJMA7020 zzw0w10u%dah! z<>$O)I6Eiek2lHjskh~L+bR)1dRvAoZ$TsKDuBZYEDK4Unyyf&YgApYA@mmZyPhemZ!Tr4P^AzHm&} zgWoe9$T;cX^gT_K9#DhqC?(Wyl*Pev2^bvhq2qEL6qe9LS8N-|`D`^+3mvmg;IgG} zt7wm0HMRY#qOa1`bk*`R`E^Obe!Vn|#S&<1k_3M)!m@>mC=fPNb8}TBsHnn5OBwh6 z$>Z?>c|0hW$9`oIoKp|LtBX&oQu*h?pPY~7pEfjny^7WPJ?Aw22^FUFmhuiM)XZb*RV9i<)=eoR0fnS zbMV_c2bK0Y*j<-{3ioW>6?p9Mscd0OO~?7QnRp^>0i#uN@oY~n61ww1`uPZu%fqe3 z*~kybME`wh80(vYW0R6Gy&(~bhVu~iW)Ad5#^Zf*EY5dDVd%sN^dA+96H9{-Q{e|K zeLr|v1R(2i5cUXpv%aq%lAC-Xanc7~f;ZdZ;|W9Icy- zajJR%YK85*uY)|+EtkTrgY9%A{wKY>{h3ZZc}@3UJ|>+TH)$0+PaAZOP|4^LvYx$> z(z92Rm)=t9BfpsLFUh3zp4n7_s0WQL)W_{IKf_z>CW ze4Viht?KVUl2Zd|%)kT+`o=W4odTuf!RHj#z*ZqgrhP3WC3sb%I5 z(v)Z>4gD7C_qc_ab}KbI|DY*PzR~>HUo>NJH=W)g!Y)lE1h}a{VTL+dO*HXKLkH)& z^l>?|9~2Y&<5RsJ#PfBqR#69b{#wX8rH)PCm9T2K9P}SZK>cnj#d_3{%#%-~aJ7oA zmsHWqmsOOeCFGZXHARfArqj(ebh7s|ZNB@J?8f{g??K&kE9D=h?w7;x@k;n$tco8e zH1Nh#$e?|y2xwM9$9n}h7|7$+Pf5XLOG0Lb6v9oUk((vLmP%!uKBR`}UK;4q)x!Iy zTF_dqiM3UVXc#PsK{1`g*Y#3^ZU!z3*>y5J4cjiJqV7*B%iiHBnj9Gwx(etrr`&{@G&cUpo30SKe2Z@SUwBL=!sn7^0 zqWKW2Vp%KTq?5{WNdLP7pl) z4PS(u@rSzL&@vbJVALpI9KPd+4^slLlm+7N#$c?w6N=kw!{J^ZhEfUz6^3AHZ7{-4 zgrLzm1UAP5VchPA@E?A{j^hi}vp#qh?}s>bFLciGM1$bg=H_@n(zF6P)^9W5nnfLH3^^j-(r5RjMi4ubUySz!Gxl zw#X%0%uutz>Rek)o@kFfi$(xhK&HQ-u3{vz28}|F`$)LDIKbw$;Mg=rV70ab4yfDW zgM$U=<}f@|GQr?{BOEd`LeN)3WaSv5UyT7o%MB1e)c`G0hNu?KPyG!bb-&gMzET1Qul%JA$#qnFpo;XYE2vQRG098aq}n;> zs7LuI?M^D8g@zkwQ11#VPg+d+n{vpS&!wi?5DNI{N++V7C}WW+#VBgiopcG>;_!tJ zo_e1*Pd~;#TwcjbYJ&K1EfNnT+g;_s7X`4DSa z-f1M_%us=!z9ka;yDaw`)X8>N{$U!^8`-G!b!<=EM>fX2oHg_-WsloWu_3?rGd|@g z+xXuB_D^dc`?%~Jd%9>BOLIEQq#n02$INc#ATQ06%!Pf@^dCDT`2O=@BHpMb%ggS` z@D~D`HSZVk{1wvN+Fgd5u9D?)zH^#F8y~F6 zCx=epP8XK)Eb9mS-*Y9(T4Ya6gWaiX!9uE&&Z1=wOKG)MFWXwxNms9B!dR^S^Z2ww=y4wUV6TFWRp7hg?7PkiFoetR^a8(q(0o zt<}V)gW53a6pjPB!e^i_ieL4I_U{33=+VQ9WNi$eu8CDfeNdpQjLoAIU|24KR&59M zDf&*WN2;jhL={yFOqMRN+2`PDvUmSX0|$MjADNAGd9~1Ox7X6=@*jdNhzJ_(b|OZz)Zol2%J*pm1S2J`3IQpy09A?M_2;UOMjV z%YggU3>4hSz*Ef(40@Y}-jq~)JH8N6*HVypKOM58GBH%Ry_aPoVnYgwwl2hgOX)D$ znt{nHGErfWjUy5{ST-vgvxR(|5F&8jn1zU*z5w#N^AXGi9&4F{-_~)M`fUdGD@8*u zBofBk!?5&dFm`YBN9Q+RL}~g#l<13wzP?Bq=8HxrUl?EVLGm9Txc&3N!Ezr+-1C84 zvkxl0ebE!<3mqpvsG0boAj22FhkfvFpf7A@`a#Yr0KUS{-az3f@{L5p&j=*^4a0=s zP%McK#ng`>@P8YEQ^$glcrXz6w)zP!&kt3ne2{y`8$lC&FizDAeNDYEwA%w-e?8!E z)Du$eE{{gGEYP{{XwSYf4s#a;6FkS&c58@j1yUlUo@eIwh~A86Uw7v!-0 z0U12GPRqWZp^NngX;#;E>i>ByB~2)zt+~0RFn2B;9TQGrF>dsJ;aD1$Kb)!~`_j2y zIcg24=l=U2arwa)_&|wWJox$|-sC)iPwYQ}kB{xg!{ar1hnF({K2VB(o8H3?+4QpO z{iOJ<1WB$TCB+r}q#Eb$Mg~Lm^x{mN)kLILy{~0f5+4i@xMt5{F;Iyf9NE`2gXbAl-2F* z->YA&yrGkwTG__VKEEW^*7J3K8@Cwj zTN<~N70_6$0=ebd7+a%fb12E2^KYskxz=bPHh+nFLGuBG@ zoS=Zreu|hEEQ`!hb@X*d6)CQ*qK=j-O7X3xi{+n4qTw^?^sA+(FPmxigzr83FYQm>w(z8DV=prY((U zDZONNSpqH#Wbq+Y9$IQj@RLx4!!!~64O=KYy^^X2y`bBvkLl0yyL2~O=#3xK@#Ik& zO4HLZxhn&smSo^?Ne1jTWWeQT20F{rh3zp7{o7L!KPVMLr4~SIUb2u?srV9|iXQ=~ zxUp&hw4Wv+$0G&3k5i#hnud9`>Bta%{`rGxIP9N-VTKE_@!ovorOgvKXbyH|#p7M{ zY)FrcMNU*SWE#Tp(=8Nv7lTk5?2pHS-`~K5Oj_uN(>{JM7kvD~EML4mE98rZA6_2z z!~Rr%?C=W0+}1!`y6%trBmAMODcpC1A2v_)N5&(6D9;VR5sN@P@e9P4TS1uoD-=|8w|kjMRSCIKOBl)(;(tBVG>37;lH-UD;az6n?jZqjKuUcqrfMQ#O`*1-)!vR5HIYdHfGRyKMaL?OoXk%2+Btc zv02*?*EblTvtTH+1`fe?-9gY<+#iWa`dI&22WtjtA>gz+His&MjOF0+SQ0Onwv*$; zMw%W_L#b(Rsq)wp3hQ&1WF)W9=}jl7I(Q#FU$mL#ny#cJOAE;O*#dG;kD?`;-N}77 zqn;=m8rN?K#obh+efzt(@{?CQ=E@~bVMlm%$u{n#UceK>C-Rl~=G>}6m$xlf<%@(a zSy0%^9@Mw7D5YMuRZfb(HIwC;>ikN8RR&`*+lX@Ugr8!pSY43XqBM)tA~yCnE@W1)LaQs6HR z%kqEO68uC%7n{7Nl@% zw}2YNnY1x&5uNp0L-ST_r(?Fe>9p!$y0!Z#dFh{_-D}HfOVL-VAN`XO5C5cR-p%yM zq@B9H_R{`}PEs1&K{f$BBz0N_4wgzt_tC;j4}EC#)kWk7EhG-sLF|yeU~7fXM7cgj zPE*H+Ckjxul80!u2;E&m*Q-{*6g33|ENQ2#4^=cs*e;hhR8iK`YWnYb4cX*>p$l=} zs8P0s@_%(vxx6%-_evwjPZoE+$sws%24zo0IKEE~CpXHYuu2|MeN^DvM-8-28SJ<` z=B<*#zyXqi|B=8|^$v1e)kgU@I!MDz5<_&Pk?>3sr86b4UZ#P}5``?gS4Li|ZqjJG zE0is;m4Z(O4weZFHYo$YO)^pbCWYwA`Sz&YHyW*189rw)EOZ!)vN@wM;qyp{W9(4kXoYEwW*BTf z9QzNN;<(T~T|two7v)XU)h2Gs##0*YnC$f6_bi7V}7GbS)W7aShd3`R<3r1jmf&oj<0*kyc8PP z`o$95-avw%E$Lx3vwNB9xh{6x>K~J=m*mcCC3s{_FRLEk#vZTkU@z3W*|Swre8T}L z-uhaRxBQXe3gdfN$fqv0!>p5C6LRmEu(K);xXga9zU@pdE1c6_>5Io{ofpTMtP`8K zIYVJrI(e=ljVx~{dkz~SpIbWY~F0Rk(+DZ z=dn#HbU$M>9q|jLN49h6TYWx7WUioTXSb2B<38fskJ9Ue)1M9+-{bM%u^ZI zeK#Ek|D_-!D-jM~<|206Tukpf4>u*}!nq;=((!Ykl^BPUs&B{umC42O($?@^HhcAS`;mV zI8+meR;gfY9TkqfauJA-jzDQvII4o({!j2%gFbIC`Oq3Va4R=TFYj<=DTdC)CFPyCK#@ZF$Sh3m@G2cBg zF?c%Eom_ECVH}c&kHXAS3&iv@!Hnfb*w$=-%OXS6XBc9#&;_$9hGX6*3n-{tVPm%y zwvVz!eXAp;DT(o1gX8D{j%R@!UH_aA_H-0twvIymR%hJZ%2EAPjLiYgD7)x{D;|z0 zb9O-N1{-KCvxNOb3z#+A;G%{tB7-c^tYZonH6u`l&@0ak#IK@$u=UbI-VIIY7OP{@ zDiw%A6|wz~EVlYdV5?6n^*9LE`Qe}GlGPj1ReMU->h~z&^ktGzJVD-y`>AQg7CMo= znyNn*(2nJabp1^TjiD)&xpXuQb~U9_b=tJ{hKP zH`w8z*NheZW|fA2nPo@^>pj)Z$`|)AW5af42}xen@{f(TlH?o0dRXOfDQ>Iqk7<6C z;FWQbJVsWU-*%ScRWtswnSGns)PK#a+)0A>TPw%2vSoPMU{AK;x4HP$b2VqBM@3?Z zODn|3zU~um3n>&kmTYmhTA=J~@MDMg!HRsQqHu;yu2tcg?}qX5lPB;YZeng5E#{9i zr|^Nt7IM>XoB7Pl_uRA9hLX*^DPv6>b&KYZW8o4yD_2Z=XKbfOD-Y6QoTAvBmnbdh z8kwHBM?F%{sbt0nN@rEn8q`YnQ#)y7buTSF) zE%j$KbaNREJbr_Mwx6e};it&)-U;e+Ckv}SWntTpY-}5z2?_Ts@XBl~+n0q|rCE?{ z$-*Q3EST$MVnS;=m{ls=bc8IsH3ur-HJfsf1wCi zBQVyfV08Qpft$d4M=e6(@E{aYcSF!KBos!yp?KFBhQPO>kjV~#pJE6e$%SB#VIW># z6xdJjax)hA;Bkkyz}LPQrsj*B246UN1t6i>A7+Z)Xgxa}|EYVTYqKX}-97PeiWltb zJW*8Qg|Z*QHjwKHrw~_ctC;|wGft?9w?V>pW6b(sh@-m6$7F3WcHVTx zls9A0zT6Q@UfAQKxjmjubh-*|NhxAYlq|*yS!OQTPMVJzDYB!6N_5{-_K|Yhd+h;zU3iTOtkazKM%h-fuOa2ka@2F?Y4eyZ6FO$?)LZP8&qX$r zuCdbiTP$etZAPW9SddaPJ7&_tK0N%xR=#d!YvsDxp{Ly}&-pLwr`N`|eCT2y4s^47 z5kjvF`N!U#kmSAZdYM{Z34Xn*oBfybi z0fQW|pHr;3RC|v2)r$AdFUFm7UNbt_dDfd5;^kTTEa=5*c5&?pKFiyc#~cgcpI#*K zcAYft*Dbd`&Uk=~5cs*B&)TM*g)9CVmNJ_Gcq$44Dr=8#=98OTTBd61&n#iA9~H`nQXIPm#or z201+MQiY?CW3%iuV5-&!r4#y~MpX?D*D534O&MEbrBs}tZX^yCe9+`@e+$06V zi9gA8$S11XSVfKBt7w(LWjmf#lj*VFbk3ub_9b*shlT`<`pe>cwhUqwWD)sV1ozqU za4uEC?J=q_`JjqMUqy^wB|L|wB(%yqg)Ov&K4$%)7KiU7G5!+`O@2@3*L|SF7v7S| z&R0}#`;NZ;d`Qn`-z1qCm+9=vGt_?P7(Ez&gc3&_rfXV)pFf<1#e$=c7j}T-WtmWQ z%Eq%#!Z9cZQ?+t%<$~buS7zb1T?Y2+r((2KB6b8Npi5yccGb?oYQ@=za*T%DqA2u! ziiB5NB<>xLL_uyOMi)hb?nhv0X*hgEf}64kgN|Y-&TkHZ#hGB7J{N=wYC-s@ABa6m z0}x>mfLetB$VmjCpHTqRy903RZxHrKhhn>Y75}NeSWDzAg%juSX+iel*71 zi-KrnB$f)Dv29%_)GI^bBMwEy&QN%d3WLP%5Y!1g_uDfVygV2eZG*5jD-cfZfmkNE zv@*j0C?4{|tIfXnGTIll9|O=79EAS{1mRY00LH8hK;9t1pDpu6v5^z60)`MF`L?L?$K8x4;i78sLhg2?rTNIzhJERg|H z4jbTlo-v%gEKyf!jZJrLAf;}P;6084BRb)gpBUHO7|Iejv`#aOd?Lp8!Q;@YGakvl z99M@jjNc~4YtOMb7Bw0{TSg*L(-9-*jm83hC-`SN!`;dW)=S3-`8yh0kBz`eQwv-O zHiOjxBYfC45EZ}m@x@ac2hIE7*)|nmlp>0Di(q|S3Qh$*6p;Lf!o7Y_|GiZtUH^*e zUYF5M?HjcF1@& zDmQSo!H>CHpF{jicM-1`K8JIK$=oO1j=Nmx$7|N8@yQvo{C!g|E5G`ejSBwD#zl6r zc@HJ{?L#7d>zy2r2omw~y)yi;izI(FvYnkB@Qc-#)iIsmDi$>TCHrXhlo^{mU|UOW zv*EAru*pZRvb~2cun4`IY{If-V9Z_0j#y9(`?MLrPkh;rDiS?MOE( zDr;knYb5!SSIx}gbvM)R>|*5^5~ZyU&p9apfhedn1) z$0)8O@Ylctsr+ViHm`Qe<&*9e@Fy>m`C-3YzFB4kzw5G&f7!O5>)8#Yl}(&3czF^; zezg8!2z9=lO?!{7AivTBG%ocV-EqD}-{uJ$^Su9Ps?k$Q8vC4yp9f)3E$)%$2#Y6dn; z&PHB*HU_7p;m7-QY}d$!{^lI4pj`ahkb{c>Irwarjj0*JPH-dzN1W#&adteUm&d{R zS1f|JMWc~LLu!8%^qV4)mKKS`VUch)h(yco2*gSWJQf%Z9lLNew1=VQLKq%fh9Tlk zDDnknN)~)pu67VEeh);rY9RI%2jE3^08Zuv;HzE`vciLL%_S6qdB)?$Fw~t6hn7(U zqTWRy)jASac1Gau+6Y{k8v!l7a1=uDUzVW)hXiBNpCEKD7CL7_5H<)K!F9tRgfs*~ z_D&%7#s)&$DiD2t1fawq5L;#j;PnN6yvy{1^@l*Lyb+A4La%)zJigN+7`E^I5S!|Q zdui@495M}#Ev~qzG!0Arr(;m6FLs{x#gA-16x;d2v(N|AF1caru&G$HLyS`g?67X6 z5iGtMplqB028RkgFU$zvewbo-yU+#mZE^XHJw{hLqON%?T=T^+{p^fY+XN>TNYFgy z42MT!ad;HraMd^nY5-@Ji{ULWpWDw-*t2*fij+s<$6hBKsU8F4HBJ~PxVGbpV<2BQ z3M=0U+d-oZu9{lorMo%iB$^_!uMw_w4uYzMuCPsMV9F&`$P7@zp>gu4@|S@@odhIp z+v(_kKk3EsFLb)Og09s)rs`!k>2$?uIy(LU8MJJpoBP*LWB6hk;E_r?Yoe)M!kxOF zI8m6_Fe)FZO>MiRD9GzOuetb^7kxd%Rcu!B&W3RQxWJJwTQ!`^AJFA5diwBPKjpY> zkt8>^{L2&{wXo5r+Sq}mz05#Qf;+_u9dEBJZ=Wmp`4`=6sc91{{$9^M7yn@I*1c!X zg&a$ZyUQ$H?y`Fy9k&gu{XsttbU0P zyF{=O*R_m2YG+25E&1MQ^Lc@K7GIr_!=sBA^HqPAa{HRayl~D^KF@hCU+jC1(~=wf z!J!v?oU;_|Up;{KW==MfOFl)F^7B>t;>VYgR*B>MQBIY9&oC{79A4>#0rs8ZCmj znrYd|20ETlOH#k9=uE&n5-qKuS8mTpspkPb&bUoq#$Tg~DVNCf+8Me#{x}_3cZBNB zAE1Q^`{?YMRNTLmfu(MlxUw%5msB$lyFLr{7C8_pDMdE5m+zmg|j0f@cCaj zJ_tQ9sWJkGo5Rsh=y`SSq1eKLA+yMMdFLE9TYfBV2+$qyF`eKFY97gg#$D7@!} z&+U`2ch6W1=xdL0&c?_dZh(mwhr&F0D87~&!e)vI)V~bJ_f}izyEx&l5yPqn9KT<4 z^nb(=Cdt6c$D$)=3{GqtgSDXyIq3|}*&KO|P6!+{8qcg8Q6%Ar^97?&pgkHnW}`80 zo&%JH{CZw90&gzcp>UkgaciwmnrMXweT3|kH%Gd`Fl;$C1b1xv;YS}`q^nA%<2ddQf%fSdutkPP(sjDEGY-y{`GhC-g7lO%G1-+xaW_hGQYT;>2)%`uh;> z?xMxpE~xPOqayxKrJGIL{D(R8|HWk2{9#VZTUow#C%Y%)itXiYwl++H7m5BciIc)@ z%Jrv&(1cnwNgLXmw)waa#=mg8}y5vm6PC?r~YBpH=9|_o(>k_EyZiK+gX@o8&fy` z%hp)_VuLf9Si9i!+TTj?=6WeU|34{yQLCQq5829&#QhVWbk}s=K54Ua@Iq_n$j#%N z&z9U2|NGJ<{{Ctx^NCx{E=j#*J4)nvxZW^6%LmF0e@~;2RZ)rY54wM$iDs&FQp^b{XsC$bB%Ec;r18B>5vOyt(6~Ym zKG*fo`C1JpwyNX$Y8A}TQ^u(@!<(yiGldB@b{MR&n+<%l;a+|LI zxR*tuw!L3 zg0iDAK_>>QYonp49*wpG(Qu|1jGq^c4?GGI_K_$)8HSRpq3GTgf-zQt+Y+4EH^Fsz zsfQtJQ8-2!gky$pI7T~1;GuscPTEBwpb;%)&ik7u9_-18u*e@jEjT zl?%eLVOThxJPpV5so^mE6o#>KLf8BrhW2+M*taAE;`PBecq;^-h5jfbxVi+v(_NbG zkA@rmAjLrZt__5BTL1z~1CVmsA7_5~p!bCb)GtiKzZxOS-nc^btSeULPRF{pUPu+# z%zlConuhs6%fTI!xhqcY9|y-dW3cX~J*I>iW7+JX2$vg*^q8U02{k~-Pb1j7St9cJ zNbGyUQ1TY|GGiQU4gvn51m~S%)D%18@WRp1vLB6+kH%ni>=-OMGa5Vf9U&@rfaM$q z%pE)md;d6qp4vm##~yEc?C>GP4w~m}QDAF>X~MR7VvQ9ZKeI&r$KgmmXom9tgdO0* zV2qR+zjp8RE^DWAPXmpiTN!~Mb)c}=4<-~CwF1{XCjVn0~yq9#_lt&`14>t*u7CgASa z!)ADQGoSW07XPb@6&(M~ZhBO+mCGKnjs5O1O}lrj@!?zcVCr*rN3)z^_BG~Lc7;t| zS;4jxd}o&Pf3eY}U)f!^KTOxNo4pEaWTzS$S@XVH_Q$`0eLvjEqPMlNu(R#VBDI?( zeraNrBfFVPbPw~F>16&#TUhn^4t6U@g3pxy$NGGg;Nk<){9MI*c6E*hGuXUa{JG1@ z`F`MT=T&J>ovZ#-iC-Hkvc%k>tnv0zHuu7AmSWw*-kGTIiUEE2&?F1)cdnR6I_}_m z6SwlRm3#Q?u}8VC=|!$!^_>5GBoND}O zKt(DUU)e%04qu=Z!|sxJKn2;DR#ElETAF0`jb>HU(aMNssy*FHzp|uov`N^gTcqK? zRbXyS5xl?lLBoGKc(XzqTdGxs&z?H=m}w$TU)VQ4DI?;C4Em`^AWx>3bXNVLur<~6 zmVczh$uDSA&1-Vn_KNzk@1(6#MQt{6%y$*E~F&HW&QN95ksVAn8;bEb3-s!}eJyl%I*Y17>3LnVDFi zIuoj&XJE(9Sd1Pq6Ppgi;%QPWmX3+V-l$keD8?fB-VEGZGXtV)Gw^mtG%k)1cxzS^ zoSsIZx^FZzY$7546^Kcv=J;PR(_cLz!RNpI{FA1CYj-i zyydQN`8*9C7H$|5>4EJf?%3V#hWP%yEVbyXOVq`4r);ImMvS#!P}_8G*g4WQ0m9G z_hGy>aSfj^_ArB__G%&ZJ)P9HQvweb z$YAaS5guF1AxNo*PR909$3ZEKtnCBqbS2F9R7T4ZP3*p@iSgSt@Z_f|`ew=CiF^-j zecVcG=YOS{0xN9{{7B#0p3%kAFX$G3M!Kc1>GG8C)OnP$ z?WToWL~!V%1i3+K$oJR4-rLG}Jwpy@Q{^yhxCoc!rE%b`*+0I#Ly&#-7&%&F-Gg0?xChqFa z#9`kVkTQ?KiL@DT*&mDXr-kFy40!C0#;{dUxEC0S>RsU|@DE4z`cO0rM^$tfrZt6w zZbqVYa}=a}qH(4(8sP%3wf&C3?gJ6H?;Rm#@iP=9 zSz$Qc5(@Fw5R?c$ZkEt<&DVuMvNI5A>w@7F7J}>?ferhIB5gq!dcuP6<#zze&I(SS z+|fAA6_LTN*k|L4tnX8yv2iM%ESLgg=gBBdn~0s^6VQ9Z1y0|c@z`}V_8hmu<#9uW z*Kr7*ZXbd;KLc3gm|%RX9lWJS!G3`g%mlwLy_ntZ=g$GS5xPJ)nwJUm_Cos6<(G3q!ATOD=qwJdnl*f zqleKxV_jmKB}g~;4KEKo$)^rn#cz)FxX`2-ikljv76tRv(F3maqd;N;>R6!D6oc2+w+TQrv70cnp)U{rY`1tOoC6M7WO^j zC);8CN^N<#HhR#+dA0*`2tA8wMe=jSu?_xG% zde|Vl9%jF-hb`|eWZpCMSo*7H;zugU;>6en=bWs^VmH2(b$JxCVTX#@_H|bn%GG#g zRv&(Gwmxs2L40v+5|l=t>OvNw80^6#Hg?xYuV=RysQwCp0Wlr(k? zlfX6;N&HTigzlqmx*a2lpL#-%93zh<%LFgCK^`iHMQC^>!a5l_Xipbm(*0g45&Efr z{T~YQ{Yiew-|6VrFO(x)O#y%3lAc*PbxkNE9nageE$uqhmR=^wdFMzZeUjc?Iz$tb z_K;3h5}sQoqt+q`1MQL^wLJ+BJ|*GVnW!f~fM3{eBZaU>@kK7+y`KPM97IwIjQ zDH552tLr~D49gos(PS#zt`v?I6XE{ap?K&P0{^8!7cXI<9*&BT zFnpUDg7G)~u=0Tyd}`eg|8OdjhInAUnkVX>yFvD#EBvlZ#jLYauoQv@Ee>g9RclTj0)03(Q?&4TVlSEVv?c$}EA^XashD zut)iQJ3K#OgY9F6LsHHNqFFi!@YTdYoj&L}CPMF1NfeFIz_Ii`$coZHRk$WHx!^B` zN>;)Bg}QhOaqom@hfClt~j6w#L0BI;Y5PX`?e=uBn~v2V$AIw_7? z`$f~|3%+#cB+-<)gD5CgiLy+8@Z(+Ax$(Kp{L1509<_WNZwayG&hzxRc!w$vga;m!E9rhR@9T@f#LZ_mQ2DZed|#y4d1Ty==$tZsw%c#ng}fW%pZtvUdGC<~8LL ztMR?Z8ZX{sV_%muQO+mUV(^2Viu%L`7u;i|Mc0|(kP5ax`#00Q)54}0{$|hjwz7|` zn-w3Ez(?i0A$u>HKorJ#KH`&EKTU( zLj&5p%!)!no#{xND_N2sm0gIS@9_z=d1nS~`c^=L7p)|RPkX7$?JS*te~aobm6OJw zkM#864;po-k>ZbZ(|J!R(0?M-JeR}GYjU_TN`$4$n`!<1dV0|J7j0P8LJf^=bZMhB z*k?H`E>lF~dkKW;w$uJ$Z4_Mji*y>_(=-3)l;QW3JXb%Yz|Z%o;nYKlaCk{6gI-aR z%3Ct?|3bPh4V0$TLtT9&U>7QdMgu9hIyO;=e-rsXZldGUT1ip$pTKt_1lWmi{JRX) zq(zuARuanlyGdEOoyHz(q4M{Qv~t}ql6l-f2}MPQiaYYhNeqD(1Z7I0flZtoQ8MtMWiBSpZn7uq5 zt>&qi7@h+8hJ{FMOvdS($;gDlGQCL!71&uIMbP62y{_FUc4@_Z@* zrNwd>{Xrfk>atjE-a=<3ZV;V1O%EmZQL5}lYV%w|&BL-tN+XHZ)i0o1x(jGtc>*m7 zj-#l*@f6q*MXrwn>3p>>y}9T@4H!luJvEZN{ExS!zvD>-XZYwREBV$p@qDR-8}FNJ z$JY+i<@2gk`N|g}9>1xBt=;jB`7VFY4&Qmjk`t@gnr(ksn_MSrvhHGocXhHoPup3{ zfj?~0qDD3_s-Ee#zhTK+O4(40+pIMB74ul}i8=rH$dpGsVF!I~up5i-u`~Uv*qe(D z?6z?;gW&bYIJL3o0{?wz?qux_t!&T8CKfQJfq74`w zD^9esx!)vss)YpK;@``n1ST9WRsUa}^ZOAnsM?WL3XUf&|#o^zMiG|5nDqAFFp zXp*H?KU#X6`4nEq`sT> zlgE!!TQRUS%zE2(13!W@9N(xzDC6Vw*7KSwnpeZUyXcOVL_Fs~i{)1lZ z{6<~@b+jbCksg;eQr|C~u7TOcbZi9o%-tj zBGY4?^s`I?oAM^r)47+qBY^FAX&6MkO5_ z^@9G_(RoMZ`2T;LB&DQ`BuPeuh?I(Szu&KCvPFmzlC<|OY2aha%FZSsB#DMfQB*<_ z(oz~)+EV0qeSd#k=hRjAICbBT>-l^;&(;*URNJ7BXf2tR%Y!;b27?&$pOfr$@2Fm0qe z;?KC^fW9m4&lmDro-2Y>JcLca3l4Ga&>Q83<(AHlZmau&e zU2cxIHXATFYb{)Ftb&EZasFEA zQYL`y0TI-44RFLm7a3-=k^N5-lbtki_~Q(0iqOE6;TpKGX$C$<&cMw58gRcn1^t56 z5M4bH3;pCU!@VD#_V!SZeHV=jZzE@qHrltXjXox}lJ2J#nr+oY)(>0hTvght)Bo{1WDLt%&*mDP{iWy4dez!A+_5vW~Ycb&`M*`jhSqiQz849`+)_iy=TXK5?NqX0_)oLhB^MKVX=*k%u%<4U7XO5-@Mt6 zH=XHZrxpIPl#E`cr`pa=OBuVU;KUG0(%l*{ZSK z?EK$eRx+`hjg4+)*OoQ02R*EjtxR=ihiTyQxjF`o=Uv zVlfT9w31S7&FF{cc4CKvNZV{Ly{kMyvN%c|2P4S%>{)UzyG#`mZ;^(u>Ag6RLf$Xm zki>;DYD=smv5Vc*oFxY5RB>3n6vwgdLFgAh1d4icm@>IPCdW5X-sNhF9bHW`C2Qz_ z!Z#tG6;V_82U6bpo+h|`rDB@`db%NxnswjPY0X!3oo7;y!*f!+nMrkLUQt)vN773v zqWdP{*tBnFR~feL4HzQlxfjL3bTKba#al}YL(JF)Auw+Jcs&< zvgl!B3Z?ygNK5kW&~3{o$|w!MlWT#{pJLF1h# zv}buC>W&9~rhDK)n>*^W-LPbk8+?YiqcFe?vs^teYor%W3;D2Vt0!(Gc?$1IH~f}% zfa#8C#kLC>#|B^gw;?NKD{8c?5bC@I$9guSH)<2Q)i*=B$r2HJE%3z0 z0*j|xVAnl!6zQ2`0X0C%zf9%^gc@(a-q`i9-LM{)hOS4W*Loy-2rSIqg39nM_}O3y zAD4}2thW?e)MiBg+zgkm8=DCLI z_s0?{wfjPOhbpM%c@=FwUqGC!y{lrJ7G%SXpZ@`Gvp_~Pa!<|^}*-K_k`BI1geq)|6pTH4F{ zBD$I7nr;>o{EIccZ(~2sHL{d_WdhT(n8oyTW~7+MoaDc-D$Dn5j%g+<(T!*G6w=t- z5oK)6yH2+1VGr9Ya4^rKo2`)k%XWO~U=gXm+1%1!OhTra9o}2PzDm}yvfm}ldh1U% zN~)K2#kI3_{rdC45BqUm+{Ffl_Oaf{|Cs1TFAJOgi^V{k&yY%F=V}IUECcL!LlbiqY;eRIj@?srt9z4^F4>b(u?rT2r z=k^lRvsH~8-Y=rS6nnav=SqqpLDYTMmqu@JrR*KPG{hy0#yK6M*eO@(&AVIVntGWU zk4MuyuUn-1Ifm?_AJVq;MDo4=nzB2JDEH@g@{RjVWy%AP_eDZ@JxXGe`yh;!mcfF} za(M3~1jo6li_2dr z+`NqXYki|au^KuY*+4}%TWQ{kPBK04n^roC;j^(Aa*qC^dg`O*fF3dx7}j{WlZF*^ zl9GKJ#eDrqYqaZW;r$ZIzxSRFCBC77f3xXH$YZMSx=oK-u9L_0AndUW#xfy$4K)Zw zcxEV02psEE*bU3ZUAW=38}pa#0n^@vA8lbczBd#yCxhS|>5s7?eu&Zx!1dL>kbmoi z_)0Gree}f}9bYVt@xtL}UWl0N1=BQvZ>gSm;^YBw!LQXBxuY?{4HYSFs8n}H+(%DX zKkz}Ih99~jeZltnqPEHlN_MVr7CFOH-wDzB4!FncAsJ+c4rM!3&#*=DBwHLSw81KI z8)#WtgLhiv;1?@2N^Zs@p?lWlZA4c1M#Ps};B3A*hO};gVfqFbU0jdE5;GihH^YFn zW>~t<97}C2@u+4!@`cP+w8b1s&E{|ov4Cvw2J|JG!{n$rVyCW!)z_s68nh6ml?&k* zV~mFzmSJn>a%f8}M_u(|ID~V|F`WE*tvZG|iNKNNK;Srz^kwr=E~kU^ZF691IR|SV>)_ZL z9jtGjgO6>q5$LN4H+2ojE>p)Hn~9K+9}P9}p~z8_fO*&-di}PQ4sNZZM`2&-ky!z0 zKFFgd1+OV=bS~YDeMQ~Hximf{kMyselx|RUF@l_EqX{bG0m*cY|F@0c5-|=8~Qn$T^&-u=1j?D znQ3uM)+mXkpMS%uCpNRf&z&seST9R*>S8A*H89Vj7B=E;Ckq$rX8&$=vDV?O%(T0i zZEvY#c|{fM_{j>kKB|pf)9z*`+Ws=*=)X+Xs+TP^{>ze+dYSvIhoNzExgk7K+u(Y0gL&jae z`EvU#51yYI$}48q@brRVWFeLBgv zIYqbDM3dr`$26k)DJ|B|AcvAC^iw;I?%jMssSPP)B$r7B$S3WU)pRbrn-UI+!K$Am zR$iBceS#D|Uy_0AVmWjPU9)HVZ<2mqPy5$b(%Da6DS!M2Iy>VvrD~;5Ow3y*c%bB4fUjRAH^KNj8)GjSgB@*G0-c-E{xxUs`mpKgOLEHiMKt z+M(4)u`h&8b9ona-S|x<(|%LJtTvjzxQPbNuOzL!QktS$K=mu%P|<@VS`~7O`1Q*q z`#T7&D?=b55hl3vFvwjE#nc~RP?Fz`y|Z^COJz5zpYDRY!!9`83Pb0YP-NKcM4U?? zuJ`(3`e{GhjuSkat~Yj#^v1X8e#jR#(Sygmv3rac3=};frR9k;Gdy76;f@XK+;L)o zJN^uGhs+IkutXme6!@WUQ~-!Q9y!*52;;Zs>|D4_uIS-UX|LZNYMj z6BfL3#K6A}IDNS>2X{@W3lz73o9+TiX~8|+NnilOEaUq$-wZRz^j{Ct2W#TUnC9Q z8bM>8AEI$bc9HyEU)rKzLka<_2+N4t@95Hoqtod^$QX+4KZNETA3!$kEquCWHP=ge z%TLiuzUzGw|J#0zN6$OR%`dxhTN5kZwqQM1S~-))OUv=RwVh0P!w_z8W+0Cr)x&xv zYuTzBl}zSzG1DJdz@*YXu*uD>>~P{QR`jo%t=-qfLZ`N|V5^_3<3$6bXCIl8*fTab zKbiRlXRwJwUop?ykJ$yM1ok@cCEGo?oO#S^Vd61eEc?MPRyd@co%3vBa~(R_{LbI( zuw^G3B0L94lV5D5N(U4F*vJ$N%GoT}A8cf68}l14?->(S%$a2m*bNS zIm9D_$+-ZyFv<@F*MUWk#6+MqWsV|)PC_jsV&M998?-<+ozMS^h@%* zmP_GtzR<#b)igr%n?hnFkn~9s?}RL7bxaBiG-V-mR0bC%`h-oiiwsX!k@&kW)E4@l zwpC?OMdve0KbA~&`;(}yFo_1}CDWmwNu>NLnF^zxQ>J1nHJ3c2bq)D6|N93@>H0to zIqzwPat`@)z9Osa9Qy3}mNLpeQUCE}#B8c)k68`5zpSM-ZyKmtxV~-{`$gHEe`vtY zKeVQ>AB@t(&~Zr&KN1GO=EXoXD2l<>zMC{(b<*4XUu4+XMB!ty{jP_b-tm)oxPggGiE13PMa#FziD^VdfT!H$y`awKNRcsbTO|+KtA0yU=oZ7tBM# zVD~;0N+lr}JA5bFx&pB^FaU9?{xFL1#tbuW@U7msq~Hw;!TG=V?uD=nFZ6nOBIuSU z#=rK<@kZo9Kn_|2*)h)dOp7eK2#DA4(7UquV0@JtqQ@zb612RtLcTs2^Tz_JqST zcTCZDL$8k;lm-a5+a~O$LZAU?S zYB_h0)FYy3s`EvZygoviL-x`8ik)Ot<(hFp8fu4N{$_?8IvcV`m&mhz5O&S_v`O`R;Zx`W+4 z*v&F)+nMzHW|lUhgWder$=ovg@v$#|vpGZBSfObbtMzMRe>AJvu6?y^``%U-)6mTp zYxb}+`8}-Bx0S6O(aI(%erC6>*Rn&qBG^rXiEK)2wa8TNrAX(Nz9@b73X#*;*&=T_ zbG9=onyqDv*{hvbncSOhwyS#}pYU%WPdYuAD?FCx)31);s%j&7)xwc{SyDEiG-niD zv{k2lOBc}hQj*{xUQ`GOx1)3Rnl|ql)q|Dq{(%h3q4|;Ma^g{uK zZ!MzY%wn3br+`8oa_G0fmpw)w=*IXW`h2#ORz@~bn6lua)=DDFT?&!o2I0ovLCDh; zhmBDmdAWDdU)6eH3;jY$O?mY1R|X~2C6m$1B+|T=L?NS+$xb_&+*c=)*WP5B$x|pn z>=`v5dPd3KX;gFM1?~HjL6tdKWV5g8F2*U4ub8&!Fant5)F&`q29HNY>u{( z+2uO=)mBNCj^(svQ!(8c@`g@`UQ+F~`_x`>gy7jAzF02hi`-f-$j$b|SY0o{>3hLG%nNs;ypZqchqYb)NPX&$oF;#0 zR|eqk^gt~8F91sgmev3A!W~UdWIXUd{Zdc-5ZJi4(F;2-dV%tUoY(D+-JR}&>vlt5 zf(v>tJ3_VC1{!iU=nL8ghjBJ=tl5s3WLp?)x5b=v8@!mh4JAXY5Z2EMrg590WVjje zz8fJ;X7DsK1qnX?jhhKZ$eX}3!vs~iE1{LX9Gk~1#r5sRP#y)e)rp`!X#pJH>*3Dq zxzN6OzN>KPV5uQSp3QwH}k7EXC+c6KS zuNUBcxFH&14Ke$@2+G4b3J)<%GciE;96by@s*Nw(wJ|MJ55pSgmOZ zD(gZfK?~QaG+};30|wgaIJ0dcVm-&8EmaC5XY_}Q;QF6^uBW^+<#bi!6X^(!A??fy z8WosA*G47K`JE4G<;Dl}R3e7DjH1cy+*R5Wc!4eqKSev%)EzOz~MWJ9WK|?T)NqJ5Il0?N(2i!Jr4M&h#ORzkiv%>-Uzq z?Rml0nU%2mvCZuJ=nnQ)@MVSdznI&fPBtO*Co?t|_Jfe$EWN&iwHy{U%eQUpy39{j zk=V|fo;R|i+iO{i*-uv1+R1hw?_?{Ux3l2OEo|J#Hr6*hi^xh$xOM+GE{zub~V2dUHiO{jZmp%@or*#?4u&K>(N=} zBN@d`IrQgu%M|#rw@10*#uA=Ab1(&698KQuSJH&l!L;GvA)0sMBn6(mKZV?WWMj#8RaR6(^pmBika(}Tz&S~L3#i8Xv7J*N_ZFO~F4 zuU%l<06b6@N1uckqKf)R^q`FvrVIPe=r#(U-b8tlC3JWDYbrXDN()9LQ>bYYjVn*0 z>(7(OdwDX|{+CRvA0(6Gv=mytCxy;DNuj)=XC$#bm9qTOsNug1N+e-l^vI?B^&e>3 zg0Hk^N+qeMG}8E6J(S+nNd|L!C{9HT@`uH7&}krUa|w)IA_?&klCac}K;JV7#P1yh zF@a-Q9|sBV|cH^RMq;X2$8hsFqBFL#IBA`gKJUKsI7V49;h z;T+B#T?X;! z#W??bA&$N!1m0PIp;PDK$BenSC@`!jdJbBHHSyGQGDa*Mjh3Gyu_9nN=3gHHJL}PC z-=YpVvpIN>r;k|mdEf>5P#SEA<`E)Nm?ZSk*{==ZBQ3&2hxyQ`(}j<+mf#zwBl+tT7;m3|^Zmx4=JX&ejqHbq=Q=3I zte&K@ztMwDU#Q&f9VHvRqEG9eQ;=yg{T=q0EVeu#NBg^^P;!$>8ZOg-=Cy8H`S`)Bc~`XwKdUs4tLaSQ*%2f7c{_Q&rel63SS;{^g%w)x>UzxIXJDaq=i`}SdW8pJ@vJdk0EYS4}Q}~|4ZdAvy zbK)^f{nQ9-Xk@0A?d;6e4px-i$u@U(vKP-0)iu0G^}nKjW}#gM_d1+ z(7negKkqy>@3~AF6ix+z^t$}_4- zS8!4p8FiHISV?{z)x@Zgo<3`*-;?{{@T>v&D!eXC{i{hs{~Jy2UrQ3s)wFhL3EkWC zk~}UZljPteT6#K(rVA|Vu1zBA0m<}3I+-5tOQy-C$#j2R3OzX}ux#Zs`c0|i{UD8Y zwx*Nu`ImGs<1Kx-@|jw4%W3Jy8nS%SPJSIfo3yvrQb$<@1^0fTQ?EYKykR+H z=6#QKj-cAiorwLh6Vdj;IM5f2aVK}8Wk4`CEepXbkP z#~tp6oFjfHboIxFyZ*?{@`GNW4K5+QykBO%OU=tsRQ?qvV%LU){oS_$CkC;>2F>SLg z{)!x+(Bpvn^BgcB-wr3YZO69d+aT_4jb$&k;(e+Wt{4bM&n6sFw}5fOI#ldmgT4c+ zu`1UD5{4_W%5XW7b(Z4Ar^T4~-5BZ71eN9m7&1~H^jRBjV%k`1qJhv)N;u&^5~hwL zFm=Ijp{L7Xz^>sCy&Z$g$y0G!TN9gF=U}CszOW0-gJHf1e>VsnGnGTQRHN&mA*^p3 zz*nS;$-8yn7OR6ynRz(qXMmZ}BABmc&=R_6(p*DWE;2y+ow@jRb~fH^n1Q7ZYH)op z4j1>yLvNb|#J~KbSI(Vu|4$Ps^{=LrilyYBTS)PO>$j76Ns(Qt6deA9KK+cP&24wd z(d`yB54-;V*EolL5#(HVh&JE*k9I`upm!ggXgaf?y-$qkWUn^Gw@o0;!J{bY-Vid$ z?&B+p%lWy2SKM-E65n~}9?!phk^5~6=SuhY^G3s6+(yiUUs74kAO14tJKyMYXUA#$ z-Ni|KnVK?}^d81dV&u5dZFyc8EYCxCO7Tm1;@mo_ht(YZ%`Q&uVFm4NtXJ(P+c3J3 zrBqb2y^B7wMIKo!q~bnvT783^cE7}ISC+G3Zh35yS}_}IQpH*=n^-_)J8K)=#a=z{ zVnd&Hu-y~dS-NvOTdMqv1@G)+-`2LXd7?HZv$T!nceFA8VIA!E+d5YNvVmFIx3Trt zf3a1)LN1hTV9iS&F}=w<*~cyg=JqdHWS>+mN|zBCev;mAxP0P4!yPM9MV3{`?8oWX ztTUk>U%yG5d-H*O&698J!S6L};lNhW3-LD5sjMq36N-aTo^n1z|db7KN z49{1S`;IEQ{imL;fBQjS8>*?=q=6*QwvpGtPFisPCmovBPWMWC=%CL(DwAp;)%s#8 zGW|%qbUu>a&H`HQo=rE(l4!%zM4D-lMCs3xXuMo9rF0}w*XJZ!ayFU5!;@*}yJVU% zJcZ=9rjY-&XJkJ+mE82x=!97&QRZtpS5iPzVk6CrXeJH8@7vt(rCUw?@w!$5 z8%_?!_j)OWr%U7I0$F^i8jje+;c#9ikN%^EqFOlr{r6xbK@v1y0>jrxVBw1X$Ww2i z{5@Z&=*(L>eItifUV2Qawb#kt@H~l^oFt>4C#c>s5X}lf$XF7Dl955UwkHTxxEnaBLww+O z%meQd+@K=ojquP&^zk6mn6+uxG3a>YLRu%61kkLgu2VTn~Cz^kLaw z1lKCTk8R~B+Q~5SuRcsZ>%+O;0CVCOAa|-EPI`*4xR&Ekw;>dtEI@d#fshOJ5L%!E zCx3N_n@_>1FOxBCiz=pG9}f59(kRa!2-i3Lkk{BvN^e>yLZ^n-DwI><%OcX2&L_n? zS!DAnjRqPgl8r??=`D?=>arM0_!LEU&R0m$?kst3J4&Sm`^X`3H}yJt)2dUO$?sTwASwlC{EY~m!-hY}3C9l!s^6T_Z>n;tm zi>JqjQs~CQ3^J=KBn7K7GFPjhGV^M>a`6W}4Q-+WN6Kj4oGQ9B@F#K2X7Yd9O$$o@ z(d4ziNk+bvHh7g#*p7G9rIbsmg}F36?>SXod`ff6p3+kNWQuM|rp+eF)Yq3p1B8C5 zX_QRLLN=?9Nv8Kj$#iaD3gt(nkm|E%H1~Ka?N5D9#on*zm-svCKJbZpZj{rq_68Cy zX{DZ)9-214KY}a8AvQt^+ahHUa7G4AuZMu`l1F!^0=}Odg<8SI#C{rq$;m@-X~+MJz25$RF1M>&!X_eM~u$q5P{d7Ny$eDPhu zAByGy@K6oJ1eG25y&w=(B>^}+V+U@e1VB|Y5XbEOFf!2_egC{*HNzW`CEhR}>J2{` zFZ>zliMh!h*fmh-ndBktbZ*Gxa+MDmZ?>a`xohdaJw(x;XOa|Cr4t8-QKH5mT9noOFxa* zPmSg0hdkmJhCkqWOK$UHdoOc)l~Y2WI>p^r@8kVsgZaWyZoFW*1Hbgrg8%$7i!0oe zu+|_y&oSuL4xP48N^d0 z26D{>Wo%Ee0ym6Y&p&Ps;jX6Pd`stXJ}~MupKO-GbH0@GfA_>GUuHB}*v=rWQHv-( z+K&>vj*;B*OLXw-b!uLDpFHj)k(qKjmG-2Oie)AxZ7d)k>k`VWC@0ksHDr%!GF?_p zj!Eyy`ST|dqdNMQ_?sqc_0p`a-&8lcn))w!OZsE7C@>|B()T^4{~TlKLf`{(D@-8c zkQAEV`J7&@eoo5{CR1=h5}9fy)2(sIl%tkHHltF=Y-bA1Hh)Hmx*7B``UMRg|C)?X zzop9lU#Q2jl4PX5ljwduNnULygI#?Tr!@crBPB7aXRwfCq#=7l1|_vaG1he$f&_-u zCMjV?pc1mX6)?_o7j zrzP7X=+&d+^nAfFIx@-~UxoanHO~i^|JU)R?!@$uJ23Ws0513kLhgAWPHzjqUOhjk z3*LRzK~F4G@W$m(FLggZKwT=6->2^DJ{@V>zwD|PH4FJ=er zw{~a=w#V6Tb}0VO7Ml4sD0yd%Y#S>~nY{&OksGmYy#*AcEKq*S9LCSh@o&%u)aja` z!*(6Yvesa!rzteHo5H}-6iXJbhWC3Dn3*j>(o-W;E)c=FV+!P_t3X;+0Zry|NZL3A z5u0T2Cr=jkx}$N)P8C}c)zB|`8r(}}!fB)?HjJBtk-zkD@xeT-{Hlx3Y(7@5pO1}u z<{?K_A1hz!A?UL%G{bc9!$uEhMDyYQYXMAzENHZRHlDamMVtC0M0BfQdx#RO7LNoC zmB+lF(nwuC2w6A8Ao~7?PJHX2eT|LOkY7a_@x^2vCT!@1d8EXJ{oF5$>X&BH{_$ya zASQ|8XC_d!&OQ2iNx?<{SBiBoDMp`u9o zzOtl$asZ`kmGiP|pSZg22mY?Tkl)sR!}qPs;@M%D+}q?iH`I;eK_?#aHj6u4H8_e# zY`wxC7M$awBaiVQ`j4w_vF2JnoI56tV3$^9CClSk^xoHSFi< zUo3UL82_?HocCFX^EZ=!GC3N<-+YMX%Q8y&mGx!Z`ei~(6pN{WLr|X(Y6p6>w zc_fA=u8$^zM=>;F#uGZWH-la%<39^vN46M&!o~yN~4V7=@c;N z6`k_f(Z=$DF9kf8$8TvmKL)RBcbUl;7uxx2O zD3O6t{7~FkG7`JDDq>Qr66(v9@Ij=A$}b8qHkC)4NCrD{rO~%Q8i{+wP?XzFD%It5 zXKFf?Da2Cr?&~yp=Xtt0ErOQlAEzG?M@eI(6PC5PA?=zU6j}nYAu9+E40fPOF8~sM z0LvCJU3iXcf|;Q zS1fqphWvC7^p?A!=%pLx3Fo{5T~YSa3Ej3%D1POHW5R82t9l^kq6gx4dZH%49SS4e zQ4r#ZT>@V_*7;)J0DsK+B^*Zrgs$y}QHj1d8sGz~Ti%$s-5b_IR{Su@4U%Clcy-PR zas3=IX_Y;0Zn1^;`0dyevmL5~ZDHtW3yZPaG4s?m1b15@aK>hc>uki=0T%di#~f2e zn?v=-27KILj{3P9ko|i-0$YSVpl&VZDz3qsrE!% z?9#CaJ)r>Ei$l?IUIv?m-@~}^Lvh`76keQE#Z_l@7)+WD`%4-~P0&E*J`L2gXbRqM z4jiBA!pm9@IdXcqUZ{&-KXh^9kv25CWL8b%{=jmmQMOW zrqABd4f8iN*7P;C-F!jQyV7WImEe1~$ITK0ojWi#eag zlGW1LD*X(WHmH&vKUl`v_J3e4FQ2fOfwx$J`Xe?aF^!E8%VUipZ&_{q8RGV1e_GV^tw~=kwR?mLaH?UT_CN^qlBV$TG zSj&kD<`MInoy>m1o@RQn+FdoGAsSVp(!gSo)6Qyv5dW&=A010lqaRA?=*2NxPQ{-pXUFTPGYk0R&fBH3EiZ+?a zlKtv-KG638H#>cSYiD2JDj#Bb&~9 z?UNJfqHHQ%evm<1E~inp!hO2eewuc7U89SFN3;KxO83>1$+|wC8U`oR{$)9&`o54{ z&%L3jkf#*k7)Pr#?@_2x4DGSFMfW}KQtP02igHMx{3*|A+jPOJ`@bUl=b6O#Gm15T zM(%Yf^f^A05(O_7@iU7IHH)bEW-)m=l#*vz866u^LB>;RXzte*+VHWB=09zx>GX@d z)p{t(v_G)J!K4nvd5r* z954P!V{NR^H&vuik=sMjb1H~=r%}1Ubg{PUH1gGXdMzJ8lUt6_i3vw(({o37U3JBz zAwq9l=#Tj<5WYe_E4u2B_$&U1sPe~-xxQHZ+#B=zd87YmPkeQ7hnb5j?DJf(G{FT) z_XLK_bA@593)V`xLQ38hRf(=}uXl%LkOyLq3T!iRg|n0k685-4w$mAd6rC_^oD)9i zIw5|N3*@f3LfXR(PoD@~F-q7Z)jjd4%?rQUym8OVA2lli@gmc<&WqjZ< zzz4Tq_#j`{0e-}JqVt41++VvwvegA=7CRy7f*ssbZIH0V8g~{%VpeAdMbjb~<+P5BKLe^uKq#2A=%`j=vT6ia!Lfm^5&g(Bj zaL5u&Z(4}pxxjEqJ>fnk;_w3{{B0W!eQ7jqk%#soB?RxCfMf+VB(9!< zz}M=~PFBa}MiQO&6i5^I_Mt08{Vj z3yhtQDQ>zracMRh-cE#7`#9KJE5rJQB65Vs?inM8BZV@UA36vsW5qE^wI6P^cT%WU zGdalB(7&K^8ga0gly`ihp&B2_CaQqGO?gM#ee#4cBa5Q4o>809W7;SiL%qYU(v?f+ zNPFyYTD5aOS)2`~U#A`E*qOETcAqh=-#C{F@2OF{<2ZWlKZdjqjG|fIa^x8&Ma!m( zQ>?JdJ$>8DV-ow3l6gO}=={lDY-_l!>NozUvWnO2FXMZDedT{X6!4@M8T|dK$DHd# z@K%igEeWES^|neDK0hQ4xU|EQXs6d2{Zubz$F^MgtI{a_?&V7qqKvEIKm z>||LTo3px}_5G-0uKmBW5qrL{yC%=ssL{SG?rfgO_*;U=E2UJl(>q-hJK>|~UU#x6 zU2U>q^qX~t(OI_)i@$tijxj%k$FE{GmwMRf9x>h^c(TN)l00OwI9JzcXVIr!_%-)u z{N9O99&9?0MyAS;zn?5sos^}B!q42RDUzQvIL%YmpW#YRV|bZTHaFc=&i6S;lER}| zl#{%U)@z25dCq^No_~@axjZEMz&J{Kmq4Rt-KFkLVRT7)Hz{@Pr<-Nx$n;JWNtWH9 zSv8B|v%Yz^m9X-9Mx>4>uEJiVtO86`A4v6QxL{YGqMHI=^nLF2R< zN$yq)X@2RVF-n5p7~ec@Pb5$|?hi>!s-!5v6W;p#kW_+i(lFZ#lx2E~ z7EL%oR=bXp8+SyRq6@l(o~U%l2cgQoD0}9I^UnTQXyp&t^M2TMNAOkCy>ZgW69t~` z*zr%uUL|gr5$A&J|6HI_;DRp+&hWqB4DFfDxaa7GnNmV;ed3NZ9XF`dIU{J+R{nOxwGKxpNH}4rsuR?lU2#Y_)-QHN(s~yh+U^Fiv7T^y=7qHj0#NuU0OuqE z5ifk4Eo=klM*5?0z8~gk2)pVze_XfqLrt?c_L+M@n%uD=M96*uUl#{C;+T-lR*v3= zsIgYU&uA;wT5iRBZ)>D{u!7&BEhta3MD2M?JhZjM>rWO)Ja2|&25ZqOxH7|IrZ{|R zEqYd)qUgH`F3wyIDK%r9I80EmXZU<(0aOGA_1LK4%IcB0dweJqbcUd3tu!J}NJ3?k z9QMo{i+2BsxN>bGFk})oXpe_S@CIZg&+09@ZUo@D0vUT z_P#+_8YT{7=YCir{)_5dn(38REq(Y`LDytU=y~lII#B(YHtzUBd3v9zsIHI{SG}d} zCK;5unlN>$R(#LZ=FYpGtvwUsK zX+G=MB_3@b%QqcN;|j&aJaSnB7d`6XVPA)lf81npR@R|azjSHk>u~zK{20AXGo??b zH&Mbh7aBGuh^#m7B~OIYdihgi_xLKEt-MPalONI2X-V|A^A1f&yhu^}JT2D0N#a9p z)4Z$)bR;H${*6l@F~c}|r1*qZT+O2I;jbwn;uRUhWl&PnGy2+|O4=7PY0a4|YM=Fq zqF+{$@BMOG9q^rePSsHAg(hm5*h)htbkggVKcsi6AI?Se7xL8roXr{l>!N{3T_bdr zxS<#`b{OI!_#@-+;N{y-M&smw=a*uw}bHxC;XV;2AjDaVEqJMX?Wsr zo)>fl*I#Vu1FG}EPCaiNe(8a-LvFAWSYvfX$Wi0nkQC*Dx5mz}(|5wuOh^3Ha6(Uk z6KYbNF{Zy89?f#Wgq=>pR_KJA3!QPW!wE;Y6H;$DqWGDkkO3Vr-p>Wjx`OYja>mwQ zPM9PmFmahH)ZBd$bI>0Wul$iM7J$z$190Q5Kc2o3&OP?S@=l=_D+a)9hc8Apc;P?c z`A7G6N5(=|tSolIxb+T@m~V^l+-)fOxCQ@NZh`!l&A7gFGn&_IMoEe#ZXLG7__vnO zo3k007H!1P7wZtIw+43xnBc{;73jLP3SPa-pf_+CG|wC1{u7Smy+TepFdMn6W@1vV zDx7^4(4!`Y(;H=Qc$^GQ<_w0$ph1{-VlXUrjKExFB`Bwjg{-#<&Q4Lmet~Cqj;q15 zQyu-LsblRQb-bvb2Bog4@a>oir<47EuJ{&SvWH&LOR1GiaOEGB0o zeD_9v)kl}#zbV64%Kl&vPTpf7$+y^}T?bkG=@>R=_9K>c;|1#uc+YfCePZH{#ca9% zSLQwF8yjd)#~KwIS)N`E`}gx3yYb~abNy7ovi4Q8;Z8N|=J^Vy7XO9$))ui1?+RGt zz}GD4V+M1Jd%|`kcZuGQJ0yDdE?cx_N}ecU?)w$FBOp zpIXS%%)XKID^reCwhp3H!`nP4_bmT5`V5b1jpVc>nqN5<%XL4b@XTv(`TW_{+(@&H zTb~o7t~-)6=(apL2Mr_T%j4<0yauKBTTU5^HqazHd(uA|N*x0a&{>_M6g~J7^@ZM} z6rCqj5Ob3T$DO3QzsG1q=`m`Jzed9z#ggR1CzLAwjP3|K=56zb6m!nGB z{V~;19OA_Tkh4l07Df_~Rvv_dTpD=-zt+|b#}T#R_`7EqdgDeSDO3p^SxVTSG8#Qk zl+l0p7~I!UM4!AIDr1D#a(62wG*(iZ>^n;DpGH%z#Lzjft0Y@;o(!$dQp31Y^iuaI zx#n0SiET&FK}V>aaKYr+g0~m;#)azcka+KkzBO)GCvr!vnBcRfy5svPH)u(^LgK44 zdOx~gZ@mj@?m43Op98K5eRU>x#JkmwLN*aNrRNMwcPA)qcSN71Bf4{hb6w6jy~zni z!skhs9P!%C5o0bnA=u6tx5yb~ z3r5}AgwTT^L(9sRtD?VUYqHQ$}Eg!*G~d%b{oU5KJB*g<~5B zpsW91I(@B`mIu{QhGV60{Vt{K4PR*H-y+hz`jP%D|3J5eZ8KUmm(o^d((<%qDmeF; z(!63xdhbnIfAaz*=pCZXFGA?(UQaqY+M06H&B-cvCC%wHp_^VNWSg**3} z9fQdr^$*WCuHpv4Z@Bm8>->D&E}oRVn9u$(g4a%KV!_K_FeCkatmwsYW||evBnuN+ zkz@{=bmjw-IbOsHW*0O0qhHzC+)5@PTno*|K3(EHL3K`@6P~ zt+Xm&2d)*em!Sph)y$VHOeLMUZ+XU~qNcNy^aCPNj2B&#$`sv}jS|(wCx}`oMWi0p zZMeT&L!@}cNAzzAV@47!?5fhvR+T;1I6KULhS|L|D{D~*utK^9XTDbD6KincrlFmPvNgvIY2wr|QoqV{NDl>Od z^qLUToOOVTcVD6>o!6-9S`6)Za*i^-AEeKxcF~_%M<{;Jb*f2=qv=PJ$>Cudt$i0S zc(8kPBO#6oj58_n);rp=JCAaYW|F;e3i(e@A%|lrWN`Qm6&8IU-|Qk%np;SRRZ6M- z^mj75(n!}`I%)9zK6;%Y4ms7qNUoKJv4#}J4wr=QFiF&zNuy+rJo*KUgwOPmST$uh zvbT&x=~o58Gbuo2f#CfYj)DFJB~(HIa+Y#%UoZs6zV*?Z{BQK{at?hvo=h`u-=)cP Zl?L8DN6U7cqIB1zR9 RasterMeta: + source = nodata_source or ("absent" if nodata is None else "tag") + return RasterMeta( + x_min=x_min, + y_max=y_max, + delta_x=dx, + delta_y=dy, + cols=cols, + rows=rows, + epsg=epsg, + nodata=nodata, + nodata_source=source, + pixel_is_area=area, + vertical_unit_assumed=vertical_unit_assumed, + ) + + +def values(rows: int, cols: int, dtype: Any = np.float32) -> np.ndarray: + """Node (r, c) holds `100 r + c + 0.5`: distinct, and exact in float32.""" + r, c = np.indices((rows, cols)) + return np.asarray(100 * r + c + 0.5).astype(dtype) + + +def whole(rows: int = 9, cols: int = 13, **kwargs: Any) -> DemTile: + """One tile to cut up. Default 9 x 13, point-registered, dx 10, dy 5.""" + dtype = kwargs.pop("dtype", np.float32) + array = kwargs.pop("array", None) + grid = values(rows, cols, dtype) if array is None else array + return DemTile(meta=meta(rows=rows, cols=cols, **kwargs), array=grid) + + +def piece(source: DemTile, r0: int, r1: int, c0: int, c1: int, **changes: Any) -> DemTile: + """`source[r0:r1, c0:c1]` as its own tile, placed where a producer would put it. + + `x_min` is computed as `x_min + c0 * dx`, the way a producer's tie point + would give it; `changes` override `RasterMeta` fields (to plant a defect), + and `array=` replaces the values. + """ + m = source.meta + array = changes.pop("array", None) + fields = m.model_dump() + fields.update( + x_min=m.x_min + c0 * m.delta_x, + y_max=m.y_max - r0 * m.delta_y, + rows=r1 - r0, + cols=c1 - c0, + ) + fields.update(changes) + grid = source.array[r0:r1, c0:c1] if array is None else array + return DemTile(meta=RasterMeta(**fields), array=grid) + + +def quadrants(source: DemTile, row_cut: int, col_cut: int, overlap: int) -> dict[str, DemTile]: + """Four tiles, named by compass, sharing `overlap` node lines at each cut. + + `overlap` 0 is area-registered neighbours that abut (disjoint node sets), + 1 is point-registered neighbours sharing a row and a column, more is a + true overlap. North and west tiles extend `overlap` lines past the cut. + """ + rows, cols = source.array.shape + north, south = (0, row_cut + overlap), (row_cut, rows) + west, east = (0, col_cut + overlap), (col_cut, cols) + return { + "nw.tif": piece(source, *north, *west), + "ne.tif": piece(source, *north, *east), + "sw.tif": piece(source, *south, *west), + "se.tif": piece(source, *south, *east), + } + + +def blocks( + source: DemTile, rows: int, cols: int, skip: Iterable[tuple[int, int]] = () +) -> dict[str, DemTile]: + """`source` cut into abutting `rows x cols`-node blocks, named `b.tif`. + + Blocks at the (block row, block column) pairs in `skip` are left out, to + make a hole in the tile set. + """ + out: dict[str, DemTile] = {} + omitted = set(skip) + n_rows, n_cols = source.array.shape + for i in range(n_rows // rows): + for j in range(n_cols // cols): + if (i, j) not in omitted: + out[f"b{i}{j}.tif"] = piece( + source, i * rows, (i + 1) * rows, j * cols, (j + 1) * cols + ) + return out + + +class Loads: + """A `load` for `assemble`: a dict lookup that records every call.""" + + def __init__(self, tiles: Mapping[str, DemTile]) -> None: + self.tiles = dict(tiles) + self.calls: list[str] = [] + + def __call__(self, name: str) -> DemTile: + self.calls.append(name) + return self.tiles[name] + + +def never(name: str) -> DemTile: + """A `load` that must not be reached (I6: header refusals fire before pixels).""" + raise AssertionError(f"load({name!r}) was called") + + +def footprints(footprint: Callable[..., Any], tiles: Mapping[str, DemTile]) -> list[Any]: + """`TileFootprint`s for `tiles`, built with the class the test fixture imported.""" + return [footprint(name=name, meta=tile.meta) for name, tile in tiles.items()] + + +def same_array(a: np.ndarray, b: np.ndarray) -> bool: + """Equal bit for bit: dtype, shape and bytes (so NaN matches NaN, -0 != 0).""" + return a.dtype == b.dtype and a.shape == b.shape and a.tobytes() == b.tobytes() diff --git a/tests/python/test_cli_mesh_mosaic.py b/tests/python/test_cli_mesh_mosaic.py new file mode 100644 index 00000000..14e76c58 --- /dev/null +++ b/tests/python/test_cli_mesh_mosaic.py @@ -0,0 +1,369 @@ +"""`rasputin mesh --dem DIR | --dem F... [--bbox ...]`: increment 15a through the CLI. + +`docs/increments/15-dem-mosaic.md` R6 and R11, and the parked design's C1-C6 +and T-real, carried. The oracle for equivalence is the single-file path, which +the existing `test_cli_mesh_dem.py` pins and this increment must not move +(I7): a grid cut into tiles must mesh exactly like the grid as one file. + +HOW THIS FILE GOES RED. It imports nothing new: until 15a lands, `--dem DIR` +is refused as "not a file" and `--bbox` is "No such option", so every test here +fails on its assertions, not at collection. + +Micro-TIFFs are written to `tmp_path`. Two real extracts of Ola's DTM10 +archive are read from `tests/fixtures/dtm10/` (see `extract.py` there), and +T-real cuts the committed benchmark tile into quadrants (`needs_codecs`). +""" + +from __future__ import annotations + +import io +from pathlib import Path + +import numpy as np +import pytest +import tifffile +from typer.testing import CliRunner + +from geotiff_fixtures import ( + EPSG_UTM33, + GT_RASTER_TYPE, + KARTVERKET, + METRE, + PIXEL_IS_AREA, + PROJECTED_CS_TYPE, + VERTICAL_UNITS, + micro_tiff, + needs_codecs, + with_keys, +) +from mosaic_fixtures import piece, quadrants, whole +from test_cli_mesh import plain +from tin_engine.cli import app +from tin_engine.io.models import DemTile +from vtkread import VtkFile, read_vtk + +runner = CliRunner(env={"NO_COLOR": "1", "TERM": "dumb"}) +USAGE = 2 +DTM10 = Path(__file__).resolve().parents[1] / "fixtures" / "dtm10" + + +def invoke(*args: str) -> tuple[int, str]: + result = runner.invoke(app, ["mesh", *args]) + return result.exit_code, plain(result.output) + + +def terrain(rows: int, cols: int) -> np.ndarray: + """Not a plane, so `--tolerance` has work to do; exact in float32.""" + r, c = np.indices((rows, cols)) + return np.asarray(((7 * r + 3 * c) % 11) * 1.5 + 0.25 * r * c).astype(np.float32) + + +def tiff_of(tile: DemTile, compression: str | None = None) -> io.BytesIO: + """`tile` as a GeoTIFF with the registration its meta says.""" + m = tile.meta + half = 0.5 if m.pixel_is_area else 0.0 + keys = with_keys({GT_RASTER_TYPE: PIXEL_IS_AREA}) if m.pixel_is_area else None + return micro_tiff( + np.asarray(tile.array), + tiepoint=(0.0, 0.0, 0.0, m.x_min - half * m.delta_x, m.y_max + half * m.delta_y, 0.0), + scale=(m.delta_x, m.delta_y, 0.0), + geokeys=keys, + nodata=None if m.nodata is None else f"{m.nodata:g}", + compression=compression, + ) + + +def write_tiles(directory: Path, tiles: dict[str, DemTile]) -> list[Path]: + directory.mkdir(parents=True, exist_ok=True) + for name, tile in tiles.items(): + (directory / name).write_bytes(tiff_of(tile).getvalue()) + return sorted(directory.iterdir()) + + +def write_one(path: Path, tile: DemTile) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(tiff_of(tile).getvalue()) + return path + + +def run_vtk(out: Path, *args: str) -> VtkFile: + code, output = invoke(*args, "--out", str(out)) + assert code == 0, output + return read_vtk(out.read_bytes()) + + +def field(vtk: VtkFile, name: str) -> str: + (value,) = vtk.field_data[name].values + return str(value) + + +def same_mesh(a: VtkFile, b: VtkFile) -> None: + assert np.array_equal(a.points, b.points) + assert len(a.polygons) == len(b.polygons) + assert all(np.array_equal(p, q) for p, q in zip(a.polygons, b.polygons, strict=True)) + assert len(a.lines) == len(b.lines) + assert all(np.array_equal(p, q) for p, q in zip(a.lines, b.lines, strict=True)) + assert a.scalars.keys() == b.scalars.keys() + for name in a.scalars: + assert np.array_equal( + np.asarray(a.scalars[name].values), np.asarray(b.scalars[name].values) + ), name + + +@pytest.fixture +def source() -> DemTile: + return whole(9, 13, array=terrain(9, 13)) + + +@pytest.fixture +def mosaic_dir(tmp_path: Path, source: DemTile) -> Path: + write_tiles(tmp_path / "tiles", quadrants(source, row_cut=4, col_cut=6, overlap=1)) + return tmp_path / "tiles" + + +class TestC1Fields: + """C1 and R11: what a mosaic's `.vtk` records, and the stderr line.""" + + def test_a_directory_writes_the_mosaic_fields(self, tmp_path: Path, mosaic_dir: Path) -> None: + code, output = invoke("--dem", str(mosaic_dir), "--out", str(tmp_path / "m.vtk")) + assert code == 0, output + vtk = read_vtk((tmp_path / "m.vtk").read_bytes()) + assert field(vtk, "crs") == "EPSG:25833" + assert field(vtk, "elevation_source").startswith("mosaic of 4 tiles, 9 x 13 nodes; ") + assert field(vtk, "dem_tiles") == "ne.tif; nw.tif; se.tif; sw.tif" + assert "mosaic of 4 tiles, 9 x 13 nodes" in output + + def test_one_file_records_no_tile_list(self, tmp_path: Path, source: DemTile) -> None: + vtk = run_vtk(tmp_path / "one.vtk", "--dem", str(write_one(tmp_path / "one.tif", source))) + assert "dem_tiles" not in vtk.field_data + assert not field(vtk, "elevation_source").startswith("mosaic") + + +class TestC2Equivalence: + """C2 (T-equiv) and I5, I7: a tile split into four meshes exactly like the tile.""" + + @pytest.mark.parametrize( + ("area", "overlap", "shape"), + [(False, 1, (9, 13)), (True, 0, (8, 12)), (True, 3, (8, 12))], + ids=["point-shared-line", "area-abutting", "area-overlap-3"], + ) + @pytest.mark.parametrize("extra", [(), ("--tolerance", "0.5")], ids=["stride", "tolerance"]) + def test_split_meshes_like_whole( + self, + tmp_path: Path, + area: bool, + overlap: int, + shape: tuple[int, int], + extra: tuple[str, ...], + ) -> None: + grid = whole(*shape, array=terrain(*shape), area=area) + single = write_one(tmp_path / "whole.tif", grid) + tiles = write_tiles( + tmp_path / "split", quadrants(grid, row_cut=4, col_cut=6, overlap=overlap) + ) + assert len(tiles) == 4 + expected = run_vtk(tmp_path / "whole.vtk", "--dem", str(single), *extra) + got = run_vtk(tmp_path / "split.vtk", "--dem", str(tmp_path / "split"), *extra) + same_mesh(got, expected) + + +class TestC3RepeatedDem: + """C3: `--dem a --dem b ...` writes the same mesh as `--dem DIR`.""" + + def test_files_equal_directory(self, tmp_path: Path, mosaic_dir: Path) -> None: + files = [str(p) for p in sorted(mosaic_dir.glob("*.tif"), reverse=True)] + by_dir = run_vtk(tmp_path / "dir.vtk", "--dem", str(mosaic_dir)) + by_files = run_vtk(tmp_path / "files.vtk", *[arg for f in files for arg in ("--dem", f)]) + same_mesh(by_files, by_dir) + assert field(by_files, "dem_tiles") == field(by_dir, "dem_tiles") + + +class TestC4Refusals: + """C4: mosaic refusals are usage errors naming the files; nothing is written.""" + + def refused(self, tmp_path: Path, *args: str, says: tuple[str, ...]) -> None: + out = tmp_path / "x.vtk" + code, output = invoke(*args, "--out", str(out)) + assert code == USAGE, output + assert "No such option" not in output, output + assert "Traceback" not in output + for word in says: + assert word in output, f"{word!r} not in {output!r}" + assert not out.exists() + + def test_mixed_crs(self, tmp_path: Path, source: DemTile) -> None: + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + directory = tmp_path / "mixed" + write_tiles(directory, tiles) + ne = tiles["ne.tif"] + m = ne.meta + stream = micro_tiff( + np.asarray(ne.array), + tiepoint=(0.0, 0.0, 0.0, m.x_min, m.y_max, 0.0), + scale=(m.delta_x, m.delta_y, 0.0), + geokeys=with_keys({PROJECTED_CS_TYPE: 25832}), + ) + (directory / "ne.tif").write_bytes(stream.getvalue()) + self.refused(tmp_path, "--dem", str(directory), says=("ne.tif", "25832", str(EPSG_UTM33))) + + def test_half_a_cell_off(self, tmp_path: Path, source: DemTile) -> None: + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + tiles["se.tif"] = piece(source, 4, 9, 6, 13, x_min=tiles["se.tif"].meta.x_min + 5.0) + write_tiles(tmp_path / "shifted", tiles) + self.refused(tmp_path, "--dem", str(tmp_path / "shifted"), says=("se.tif", "0.5 cell")) + + def test_an_overlap_disagreement(self, tmp_path: Path, source: DemTile) -> None: + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + changed = np.array(tiles["ne.tif"].array) + changed[2, 0] += 4.0 # on the shared column + tiles["ne.tif"] = piece(source, 0, 5, 6, 13, array=changed) + write_tiles(tmp_path / "disagree", tiles) + self.refused(tmp_path, "--dem", str(tmp_path / "disagree"), says=("ne.tif", "nw.tif", "4")) + + def test_a_hole_in_the_tile_set(self, tmp_path: Path) -> None: + from mosaic_fixtures import blocks + + tiles = blocks(whole(6, 8, array=terrain(6, 8), area=True), 3, 4, skip={(1, 1)}) + write_tiles(tmp_path / "holed", tiles) + self.refused(tmp_path, "--dem", str(tmp_path / "holed"), says=("500040",)) + + def test_an_empty_directory_names_it(self, tmp_path: Path) -> None: + (tmp_path / "empty_dir").mkdir() + self.refused(tmp_path, "--dem", str(tmp_path / "empty_dir"), says=("empty_dir",)) + + +class TestC5Usage: + """C5 and R11: `--dem` forms, and `--bbox`.""" + + refused = TestC4Refusals.refused + + def test_a_directory_with_files(self, tmp_path: Path, mosaic_dir: Path) -> None: + self.refused( + tmp_path, "--dem", str(mosaic_dir), "--dem", str(mosaic_dir / "nw.tif"), says=("--dem",) + ) + + def test_two_directories(self, tmp_path: Path, mosaic_dir: Path) -> None: + other = tmp_path / "other" + write_tiles(other, {"a.tif": whole(3, 4, array=terrain(3, 4))}) + self.refused(tmp_path, "--dem", str(mosaic_dir), "--dem", str(other), says=("--dem",)) + + def test_bbox_without_dem(self, tmp_path: Path) -> None: + self.refused(tmp_path, "catchment", "--bbox", "0", "0", "1", "1", says=("--bbox",)) + + @pytest.mark.parametrize( + "box", [("1", "0", "0", "1"), ("0", "0", "nan", "1")], ids=["inverted", "nan"] + ) # fmt: skip + def test_an_invalid_box(self, tmp_path: Path, mosaic_dir: Path, box: tuple[str, ...]) -> None: + self.refused(tmp_path, "--dem", str(mosaic_dir), "--bbox", *box, says=("--bbox",)) + + def test_bbox_restricts_the_mesh_to_the_snapped_window( + self, tmp_path: Path, mosaic_dir: Path + ) -> None: + # cols floor(1.2)=1 .. ceil(8.3)=9 (x 500010..500090); rows floor(0.8)=0 .. ceil(6.6)=7 + vtk = run_vtk( + tmp_path / "box.vtk", + "--dem", + str(mosaic_dir), + "--bbox", + "500012", + "6599967", + "500083", + "6599996", + ) + xs, ys = vtk.points[:, 0], vtk.points[:, 1] + assert (xs.min(), xs.max()) == (500010.0, 500090.0) + assert (ys.min(), ys.max()) == (6599965.0, 6600000.0) + assert field(vtk, "elevation_source").startswith("mosaic of 4 tiles, 8 x 9 nodes; ") + + def test_bbox_on_one_file(self, tmp_path: Path, source: DemTile) -> None: + """Q2: `--bbox` applies to a single file too; it is then a window of it.""" + single = write_one(tmp_path / "one.tif", source) + vtk = run_vtk( + tmp_path / "box.vtk", + "--dem", + str(single), + "--bbox", + "500012", + "6599967", + "500083", + "6599996", + ) + assert (vtk.points[:, 0].min(), vtk.points[:, 0].max()) == (500010.0, 500090.0) + + +class TestC6NonAscii: + """C6: a non-ASCII tile name is recorded escaped, and the file is written.""" + + def test_the_name_is_backslash_escaped(self, tmp_path: Path, source: DemTile) -> None: + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + tiles["Ålesund.tif"] = tiles.pop("nw.tif") + write_tiles(tmp_path / "names", tiles) + listed = next( + p.name for p in (tmp_path / "names").iterdir() if p.name.endswith("lesund.tif") + ) + vtk = run_vtk(tmp_path / "n.vtk", "--dem", str(tmp_path / "names")) + recorded = field(vtk, "dem_tiles") + assert recorded.isascii() + assert listed.encode("ascii", "backslashreplace").decode("ascii") in recorded + + +class TestRealDtm10: + """The 15a acceptance on real extracts of one release (N5): Q1 and Q5.""" + + def test_the_real_seam_meshes(self, tmp_path: Path) -> None: + vtk = run_vtk(tmp_path / "seam.vtk", "--dem", str(DTM10 / "seam"), "--tolerance", "1") + assert field(vtk, "dem_tiles") == "6400_1_10m_z33.tif; 6400_4_10m_z33.tif" + assert field(vtk, "elevation_source").startswith("mosaic of 2 tiles, 256 x 563 nodes; ") + assert np.isfinite(vtk.points).all() + + def test_the_two_lattices_are_refused_naming_both(self, tmp_path: Path) -> None: + out = tmp_path / "x.vtk" + code, output = invoke("--dem", str(DTM10 / "lattices"), "--out", str(out)) + assert code == USAGE, output + for token in ("7707_1_10m_z33.tif", "7707_2_10m_z33.tif", "0.5 cell"): + assert token in output, f"{token!r} not in {output!r}" + assert not out.exists() + + def test_a_box_inside_one_lattice_meshes(self, tmp_path: Path) -> None: + vtk = run_vtk( + tmp_path / "box.vtk", + "--dem", str(DTM10 / "lattices"), + "--bbox", "769800", "7749350", "770000", "7749700", + "--tolerance", "1", + ) # fmt: skip + # One tile selected out of a directory: the file used is on record + # (the label is the directory's), but it is not called a mosaic. + assert field(vtk, "dem_tiles") == "7707_2_10m_z33.tif" + assert not field(vtk, "elevation_source").startswith("mosaic") + + +@needs_codecs +def test_t_real_the_benchmark_tile_in_quadrants_meshes_like_the_tile(tmp_path: Path) -> None: + """T-real: the parked design's, stated honestly. It proves the stitch on + real data, real NoData padding and real size. It does not prove that two + real neighbours agree; `TestRealDtm10` and `test_dem_input.py` do that.""" + with tifffile.TiffFile(KARTVERKET) as tif: + page = tif.pages.first + tie, scale, array = page.tags[33922].value, page.tags[33550].value, page.asarray() + rows, cols = array.shape + row_cut, col_cut = rows // 2, cols // 2 + directory = tmp_path / "quadrants" + directory.mkdir() + for name, (r0, r1, c0, c1) in { + "nw.tif": (0, row_cut + 1, 0, col_cut + 1), + "ne.tif": (0, row_cut + 1, col_cut, cols), + "sw.tif": (row_cut, rows, 0, col_cut + 1), + "se.tif": (row_cut, rows, col_cut, cols), + }.items(): + stream = micro_tiff( + np.ascontiguousarray(array[r0:r1, c0:c1]), + tiepoint=(0.0, 0.0, 0.0, tie[3] + c0 * scale[0], tie[4] - r0 * scale[1], 0.0), + scale=tuple(scale), + geokeys=with_keys({GT_RASTER_TYPE: PIXEL_IS_AREA, VERTICAL_UNITS: METRE}), + nodata="-32767", + compression="deflate", + ) + (directory / name).write_bytes(stream.getvalue()) + expected = run_vtk(tmp_path / "tile.vtk", "--dem", str(KARTVERKET), "--tolerance", "1") + got = run_vtk(tmp_path / "split.vtk", "--dem", str(directory), "--tolerance", "1") + same_mesh(got, expected) diff --git a/tests/python/test_dem_input.py b/tests/python/test_dem_input.py new file mode 100644 index 00000000..d60b3c2d --- /dev/null +++ b/tests/python/test_dem_input.py @@ -0,0 +1,254 @@ +"""`tin_engine.dem_input`: `DemRequest` in, `open_dem` out (increment 15a, R1, R11). + +The declarative front of `docs/increments/15-dem-mosaic.md`: the CLI parses +flags into a `DemRequest` and calls `open_dem`, and a GUI or API worker builds +the same request without Typer. This file tests it through files, synthetic +and real: + +- micro-TIFF mosaics written to `tmp_path`; +- two real extracts of Ola's DTM10 archive (2022-09-24 release), cut by + `tests/fixtures/dtm10/extract.py`: `seam/` (6400_4 | 6400_1, a 51-column + overlap that agrees bit for bit, N3) and `lattices/` (7707_1, half a cell + east-west off the main lattice, over its aligned neighbour 7707_2, N2). + +HOW THIS FILE GOES RED: `tin_engine.dem_input` and `tin_engine.mosaic` are +imported in fixtures; each test fails on its own with `ModuleNotFoundError`. +""" + +from __future__ import annotations + +import importlib +import io +from pathlib import Path +from types import ModuleType +from typing import Any + +import numpy as np +import pytest +import tifffile + +from geotiff_fixtures import ( + GT_RASTER_TYPE, + PIXEL_IS_AREA, + micro_tiff, + truncated_strip, + with_keys, +) +from mosaic_fixtures import DX, X0, Y0, quadrants, same_array, whole +from tin_engine.io.geotiff import decode_dem +from tin_engine.io.models import DemTile + +DTM10 = Path(__file__).resolve().parents[1] / "fixtures" / "dtm10" +SEAM = DTM10 / "seam" +LATTICES = DTM10 / "lattices" +WEST, EAST = SEAM / "6400_4_10m_z33.tif", SEAM / "6400_1_10m_z33.tif" +ODD, MAIN = LATTICES / "7707_1_10m_z33.tif", LATTICES / "7707_2_10m_z33.tif" + + +@pytest.fixture(scope="module") +def di() -> ModuleType: + return importlib.import_module("tin_engine.dem_input") + + +@pytest.fixture(scope="module") +def mz() -> ModuleType: + return importlib.import_module("tin_engine.mosaic") + + +def tiff_of(tile: DemTile, **kwargs: Any) -> io.BytesIO: + """A point-registered micro-TIFF of `tile`: tie point at its first node.""" + m = tile.meta + return micro_tiff( + np.asarray(tile.array), + tiepoint=(0.0, 0.0, 0.0, m.x_min, m.y_max, 0.0), + scale=(m.delta_x, m.delta_y, 0.0), + **kwargs, + ) + + +def write_tiles(directory: Path, tiles: dict[str, DemTile]) -> list[Path]: + directory.mkdir(parents=True, exist_ok=True) + paths = [] + for name, tile in tiles.items(): + path = directory / name + path.write_bytes(tiff_of(tile).getvalue()) + paths.append(path) + return paths + + +def decoded(path: Path) -> DemTile: + with path.open("rb") as stream: + return decode_dem(stream) + + +def request( + di: ModuleType, + *sources: Path, + box: tuple[float, ...] | None = None, + nodata: float | None = None, +) -> Any: + bounds = None + if box is not None: + mz = importlib.import_module("tin_engine.mosaic") + bounds = mz.Bounds(x_min=box[0], y_min=box[1], x_max=box[2], y_max=box[3]) + return di.DemRequest(sources=tuple(sources), bounds=bounds, nodata=nodata) + + +@pytest.fixture +def source() -> DemTile: + return whole() # 9 x 13, point-registered + + +@pytest.fixture +def mosaic_dir(tmp_path: Path, source: DemTile) -> Path: + write_tiles(tmp_path / "tiles", quadrants(source, row_cut=4, col_cut=6, overlap=1)) + return tmp_path / "tiles" + + +class TestOpenDem: + def test_one_file_is_the_file(self, di: ModuleType, tmp_path: Path) -> None: + path = tmp_path / "one.tif" + path.write_bytes(micro_tiff().getvalue()) + opened = di.open_dem(request(di, path)) + expected = decoded(path) + assert opened.tile.meta == expected.meta + assert same_array(opened.tile.array, expected.array) + assert [p.name for p in opened.plan.tiles] == ["one.tif"] + assert opened.label == "one" + + def test_a_directory_is_stitched( + self, di: ModuleType, mosaic_dir: Path, source: DemTile + ) -> None: + opened = di.open_dem(request(di, mosaic_dir)) + assert opened.tile.meta == source.meta + assert same_array(opened.tile.array, source.array) + assert [p.name for p in opened.plan.tiles] == ["ne.tif", "nw.tif", "se.tif", "sw.tif"] + assert opened.label == "tiles" + + def test_explicit_files_equal_the_directory(self, di: ModuleType, mosaic_dir: Path) -> None: + files = sorted(mosaic_dir.glob("*.tif"), reverse=True) + by_files = di.open_dem(request(di, *files)) + by_dir = di.open_dem(request(di, mosaic_dir)) + assert by_files.tile.meta == by_dir.tile.meta + assert same_array(by_files.tile.array, by_dir.tile.array) + assert by_files.label == files[0].stem + + def test_bounds_cut_the_window(self, di: ModuleType, mosaic_dir: Path, source: DemTile) -> None: + opened = di.open_dem( + request(di, mosaic_dir, box=(500012.0, 6599987.0, 500033.0, 6599996.0)) + ) + assert (opened.tile.meta.x_min, opened.tile.meta.y_max) == (X0 + DX, Y0) + assert same_array(opened.tile.array, source.array[0:4, 1:5]) + + def test_a_caller_nodata_applies_to_every_tile(self, di: ModuleType, mosaic_dir: Path) -> None: + opened = di.open_dem(request(di, mosaic_dir, nodata=-9999.0)) + assert (opened.tile.meta.nodata, opened.tile.meta.nodata_source) == (-9999.0, "caller") + + def test_the_cap_refuses_before_any_pixel_is_read( + self, di: ModuleType, mz: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + """M5 through a file: its strip is cut, so a load would raise + `GeoTiffError`; the refusal must be the cap's `MosaicError` instead.""" + path = tmp_path / "cut.tif" + path.write_bytes(truncated_strip().getvalue()) + monkeypatch.setattr(mz, "physical_memory", lambda: 8) # cap 4 bytes: 3 x 4 nodes exceed it + with pytest.raises(mz.MosaicError, match="--bbox"): + di.open_dem(request(di, path)) + + +class TestDemRequest: + """R11: exactly one directory, or one or more files.""" + + def test_a_directory_with_files_is_refused(self, di: ModuleType, mosaic_dir: Path) -> None: + with pytest.raises(ValueError, match="director"): + di.open_dem(request(di, mosaic_dir, mosaic_dir / "nw.tif")) + + def test_two_directories_are_refused( + self, di: ModuleType, mosaic_dir: Path, tmp_path: Path + ) -> None: + other = tmp_path / "other" + other.mkdir() + with pytest.raises(ValueError, match="director"): + di.open_dem(request(di, mosaic_dir, other)) + + def test_no_source_is_refused(self, di: ModuleType) -> None: + with pytest.raises(ValueError): + di.open_dem(request(di)) + + def test_it_is_frozen(self, di: ModuleType, mosaic_dir: Path) -> None: + req = request(di, mosaic_dir) + with pytest.raises(ValueError): + req.nodata = 1.0 + + +def _shifted_copy(path: Path, target: Path, metres: float) -> Path: + """`path` rewritten with its tie point `metres` further east; same values.""" + with tifffile.TiffFile(path) as tif: + page = tif.pages.first + tie, scale, array = page.tags[33922].value, page.tags[33550].value, page.asarray() + target.parent.mkdir(parents=True, exist_ok=True) + stream = micro_tiff( + array, + tiepoint=(0.0, 0.0, 0.0, tie[3] + metres, tie[4], 0.0), + scale=tuple(scale), + geokeys=with_keys({GT_RASTER_TYPE: PIXEL_IS_AREA}), + nodata="-32767", + compression="deflate", + ) + target.write_bytes(stream.getvalue()) + return target + + +class TestRealDtm10: + """Q1 and Q5 on real data, one release (N5).""" + + def test_the_extracts_are_where_the_design_says(self) -> None: + west, east = decoded(WEST), decoded(EAST) + assert (west.meta.epsg, west.meta.pixel_is_area, west.meta.nodata) == ( + 25833, + True, + -32767.0, + ) + assert east.meta.x_min == west.meta.x_min + (307 - 51) * 10.0 # a 51-column overlap + assert np.array_equal(west.array[:, -51:], east.array[:, :51]) + odd, main = decoded(ODD), decoded(MAIN) + assert (odd.meta.x_min - main.meta.x_min) / 10.0 == -0.5 # N2: 5 m east-west + + def test_q1_the_agreeing_seam_is_accepted(self, di: ModuleType) -> None: + west, east = decoded(WEST), decoded(EAST) + opened = di.open_dem(request(di, SEAM)) + assert (opened.tile.meta.rows, opened.tile.meta.cols) == (256, 307 + 307 - 51) + assert opened.tile.meta.x_min == west.meta.x_min + assert same_array(opened.tile.array[:, :307], west.array) + assert same_array(opened.tile.array[:, 256:], east.array) + + def test_q1_the_seam_shifted_by_one_cell_is_refused( + self, di: ModuleType, mz: ModuleType, tmp_path: Path + ) -> None: + """The control (N3): one column off, the real overlap disagrees.""" + (tmp_path / "shifted").mkdir() + (tmp_path / "shifted" / WEST.name).write_bytes(WEST.read_bytes()) + _shifted_copy(EAST, tmp_path / "shifted" / EAST.name, 10.0) + with pytest.raises(mz.MosaicError) as info: + di.open_dem(request(di, tmp_path / "shifted")) + message = str(info.value) + assert WEST.name in message and EAST.name in message + + def test_q5_the_two_lattices_together_are_refused_naming_both( + self, di: ModuleType, mz: ModuleType + ) -> None: + with pytest.raises(mz.MosaicError) as info: + di.open_dem(request(di, LATTICES)) + message = str(info.value) + for token in (ODD.name, MAIN.name, "0.5 cell", "east-west"): + assert token in message, f"{token!r} not in {message!r}" + + @pytest.mark.parametrize("path", [ODD, MAIN], ids=["7707_1", "7707_2"]) + def test_q5_each_lattice_alone_opens(self, di: ModuleType, path: Path) -> None: + opened = di.open_dem(request(di, path)) + assert opened.tile.meta == decoded(path).meta + + def test_q5_a_request_inside_one_lattice_opens_with_both_listed(self, di: ModuleType) -> None: + """South of 7707_1's last row (y 7749740), only 7707_2 is selected.""" + opened = di.open_dem(request(di, LATTICES, box=(769800.0, 7749350.0, 770000.0, 7749700.0))) + assert [p.name for p in opened.plan.tiles] == [MAIN.name] diff --git a/tests/python/test_io_read_meta.py b/tests/python/test_io_read_meta.py new file mode 100644 index 00000000..d3042ab4 --- /dev/null +++ b/tests/python/test_io_read_meta.py @@ -0,0 +1,120 @@ +"""`tin_engine.io.geotiff.read_meta`: the header phase alone (increment 15a, R3). + +G1 and G2 of the parked design, carried by `docs/increments/15-dem-mosaic.md`: +`read_meta` is `decode_dem` stopped before the pixels. It must agree with +`decode_dem(...).meta` on every valid file, refuse every header defect with +the same message, and **not** read the pixels, so a file whose strip is +damaged still gives its header. + +HOW THIS FILE GOES RED: `read_meta` is looked up in a fixture, so each test +fails on its own with `AttributeError` while it is missing. +""" + +from __future__ import annotations + +import io +from collections.abc import Callable +from typing import Any + +import numpy as np +import pytest + +from geotiff_fixtures import ( + GT_RASTER_TYPE, + HAS_CODECS, + KARTVERKET, + METRE, + PIXEL_IS_AREA, + REFUSALS, + VERTICAL_UNITS, + Refusal, + corrupt_deflate, + elevations, + micro_tiff, + needs_codecs, + truncated_strip, + with_keys, +) +from tin_engine.io import geotiff +from tin_engine.io.models import GeoTiffError + +VALID: dict[str, Callable[[], io.BytesIO]] = { + "baseline": micro_tiff, + "area-registered": lambda: micro_tiff(geokeys=with_keys({GT_RASTER_TYPE: PIXEL_IS_AREA})), + "nodata-tag": lambda: micro_tiff(nodata="-32767"), + "vertical-metres": lambda: micro_tiff(geokeys=with_keys({VERTICAL_UNITS: METRE})), + "int16-promoted": lambda: micro_tiff(elevations(np.int16)), + "float64": lambda: micro_tiff(elevations(np.float64)), + "deflate": lambda: micro_tiff(compression="deflate"), + "larger": lambda: micro_tiff(elevations(rows=7, cols=9)), +} + + +@pytest.fixture +def read_meta() -> Callable[..., Any]: + return geotiff.read_meta # type: ignore[attr-defined, no-any-return] + + +@pytest.mark.parametrize("build", VALID.values(), ids=VALID.keys()) +def test_g1_read_meta_equals_the_decoded_meta( + read_meta: Callable[..., Any], build: Callable[[], io.BytesIO] +) -> None: + assert read_meta(build()) == geotiff.decode_dem(build()).meta + + +def test_g1_with_a_caller_nodata(read_meta: Callable[..., Any]) -> None: + assert ( + read_meta(micro_tiff(), nodata=-9999.0) + == geotiff.decode_dem(micro_tiff(), nodata=-9999.0).meta + ) + + +@needs_codecs +def test_g1_on_the_real_tile(read_meta: Callable[..., Any]) -> None: + with KARTVERKET.open("rb") as stream: + meta = read_meta(stream) + with KARTVERKET.open("rb") as stream: + assert meta == geotiff.decode_dem(stream).meta + + +def _header_refusals() -> list[Any]: + # With the codecs extra present, the LZW-relabelled file is not refused by + # its header: it fails later, decoding pixels, which read_meta never does. + return [ + pytest.param( + r, id=r.name, marks=pytest.mark.skipif(HAS_CODECS, reason="LZW decodes with the extra") + ) + if r.name == "refuses_missing_codec" + else pytest.param(r, id=r.name) + for r in REFUSALS + ] + + +@pytest.mark.parametrize("refusal", _header_refusals()) +def test_g2_every_header_refusal_fires_with_the_same_message( + read_meta: Callable[..., Any], refusal: Refusal +) -> None: + with pytest.raises(GeoTiffError) as from_decode: + geotiff.decode_dem(refusal.build(), **refusal.decode_kwargs) + with pytest.raises(GeoTiffError) as from_header: + read_meta(refusal.build(), **refusal.decode_kwargs) + assert str(from_header.value) == str(from_decode.value) + + +def test_a_bool_nodata_is_a_type_error(read_meta: Callable[..., Any]) -> None: + with pytest.raises(TypeError): + read_meta(micro_tiff(), nodata=True) + + +@pytest.mark.parametrize( + "build", [truncated_strip, corrupt_deflate], ids=["truncated-strip", "corrupt-deflate"] +) +def test_the_pixels_are_not_read( + read_meta: Callable[..., Any], build: Callable[[], io.BytesIO] +) -> None: + """The fixture's header is the baseline's; only its strip is damaged.""" + with pytest.raises(GeoTiffError, match="pixel data"): + geotiff.decode_dem(build()) + meta = read_meta(build()) + assert (meta.rows, meta.cols) == (3, 4) + assert meta == geotiff.decode_dem(micro_tiff(compression="deflate")).meta diff --git a/tests/python/test_io_repository.py b/tests/python/test_io_repository.py new file mode 100644 index 00000000..045d3e9d --- /dev/null +++ b/tests/python/test_io_repository.py @@ -0,0 +1,289 @@ +"""`tin_engine.io.repository`: the directory-backed DEM repository (increment 15a). + +`docs/increments/15-dem-mosaic.md` R1-R3 and Ola's Q3 ruling: the repository +lives in `io/repository.py`, **the one module in `io/` that opens files**, +read-only. Tests F1-F4 are the parked design's, carried; the Q3 guard and +the laziness tests are new. Files are micro-TIFFs under `tmp_path`. + +HOW THIS FILE GOES RED: the module is imported in a fixture, so each test +fails on its own with `ModuleNotFoundError` and collection is unaffected. The +Q3 guard's own scanner is tested on planted source, so it is shown able to +fail before it is trusted on the package. +""" + +from __future__ import annotations + +import ast +import importlib +import io +from pathlib import Path +from types import ModuleType + +import numpy as np +import pytest + +from geotiff_fixtures import ( + DELTA_X, + TIE_X, + TIE_Y, + elevations, + micro_tiff, + truncated_strip, +) +from tin_engine.io.geotiff import decode_dem +from tin_engine.io.models import GeoTiffError + +IO = Path(__file__).resolve().parents[2] / "src_python" / "tin_engine" / "io" + + +@pytest.fixture(scope="module") +def repo() -> ModuleType: + return importlib.import_module("tin_engine.io.repository") + + +def write(path: Path, stream: io.BytesIO | None = None) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes((stream or micro_tiff()).getvalue()) + return path + + +def shifted(columns: int) -> io.BytesIO: + """The baseline micro-TIFF moved `columns` nodes east, so names map to places.""" + return micro_tiff(tiepoint=(0.0, 0.0, 0.0, TIE_X + columns * DELTA_X, TIE_Y, 0.0)) + + +def decoded(path: Path) -> object: + with path.open("rb") as stream: + return decode_dem(stream) + + +class TestF1Listing: + """F1: `from_directory` lists `*.tif` and `*.tiff`, any case, not recursively; sorted.""" + + def test_lists_tif_and_tiff_only_sorted(self, repo: ModuleType, tmp_path: Path) -> None: + write(tmp_path / "b.TIFF", shifted(3)) + write(tmp_path / "a.tif", shifted(0)) + write(tmp_path / "c.Tif", shifted(6)) + (tmp_path / "a.tfw").write_text("10\n0\n0\n-10\n0\n0\n") + (tmp_path / "a.tif.aux.xml").write_text("") + (tmp_path / "notes.txt").write_text("not a tile") + write(tmp_path / "sub" / "d.tif", shifted(9)) + names = [f.name for f in repo.TiffDemRepository.from_directory(tmp_path).footprints()] + assert names == ["a.tif", "b.TIFF", "c.Tif"] + + def test_a_footprint_is_the_files_name_and_header( + self, repo: ModuleType, tmp_path: Path + ) -> None: + path = write(tmp_path / "7908_3_10m_z33.tif", shifted(0)) + (footprint,) = repo.TiffDemRepository.from_directory(tmp_path).footprints() + assert footprint.name == "7908_3_10m_z33.tif" + assert footprint.meta == decoded(path).meta # type: ignore[attr-defined] + + def test_an_empty_directory_is_refused_naming_it( + self, repo: ModuleType, tmp_path: Path + ) -> None: + empty = tmp_path / "no_tiles_here" + empty.mkdir() + (empty / "readme.txt").write_text("x") + with pytest.raises(ValueError, match="no_tiles_here"): + repo.TiffDemRepository.from_directory(empty).footprints() + + def test_a_subdirectory_named_like_a_tile_is_not_a_tile( + self, repo: ModuleType, tmp_path: Path + ) -> None: + write(tmp_path / "a.tif") + (tmp_path / "trap.tif").mkdir() + names = [f.name for f in repo.TiffDemRepository.from_directory(tmp_path).footprints()] + assert names == ["a.tif"] + + +class TestF2HeaderOnly: + """F2 and I6: footprints read headers only, and read them once, lazily.""" + + def test_a_file_with_truncated_pixels_lists_and_refuses_on_load( + self, repo: ModuleType, tmp_path: Path + ) -> None: + """With the pixel read in the footprint path, listing would raise here.""" + write(tmp_path / "cut.tif", truncated_strip()) + repository = repo.TiffDemRepository.from_directory(tmp_path) + (footprint,) = repository.footprints() + assert (footprint.meta.rows, footprint.meta.cols) == (3, 4) + with pytest.raises(GeoTiffError, match="pixel data"): + repository.load("cut.tif") + + def test_construction_reads_nothing(self, repo: ModuleType, tmp_path: Path) -> None: + write(tmp_path / "junk.tif", io.BytesIO(b"not a tiff")) + repository = repo.TiffDemRepository.from_directory(tmp_path) # no raise yet + with pytest.raises(GeoTiffError): + repository.footprints() + + def test_footprints_are_cached(self, repo: ModuleType, tmp_path: Path) -> None: + path = write(tmp_path / "a.tif") + repository = repo.TiffDemRepository.from_directory(tmp_path) + first = repository.footprints() + path.unlink() # a second read of the header would now fail + assert repository.footprints() == first + + def test_load_decodes_the_whole_tile(self, repo: ModuleType, tmp_path: Path) -> None: + path = write(tmp_path / "a.tif", micro_tiff(elevations(rows=5, cols=7))) + tile = repo.TiffDemRepository.from_directory(tmp_path).load("a.tif") + expected = decoded(path) + assert tile.meta == expected.meta # type: ignore[attr-defined] + assert np.array_equal(tile.array, expected.array) # type: ignore[attr-defined] + + def test_load_of_an_unknown_name(self, repo: ModuleType, tmp_path: Path) -> None: + write(tmp_path / "a.tif") + with pytest.raises(KeyError): + repo.TiffDemRepository.from_directory(tmp_path).load("b.tif") + + def test_a_caller_nodata_reaches_both_phases(self, repo: ModuleType, tmp_path: Path) -> None: + """`DemRequest.nodata` (R1) is the caller asserting a sentinel for every tile.""" + write(tmp_path / "a.tif") + repository = repo.TiffDemRepository.from_directory(tmp_path, nodata=-9999.0) + (footprint,) = repository.footprints() + assert (footprint.meta.nodata, footprint.meta.nodata_source) == (-9999.0, "caller") + assert repository.load("a.tif").meta == footprint.meta + + +class TestF3Refusals: + """F3: a listed file that fails `read_meta` refuses the listing, naming the file.""" + + def test_a_tif_that_is_not_a_geotiff(self, repo: ModuleType, tmp_path: Path) -> None: + write(tmp_path / "a.tif") + write(tmp_path / "stray.tif", micro_tiff(geokeys={})) + with pytest.raises(GeoTiffError, match=r"stray\.tif"): + repo.TiffDemRepository.from_directory(tmp_path).footprints() + + +class TestF4ExplicitPaths: + """F4: explicit paths, resolved and deduplicated, list like the directory.""" + + def test_duplicates_collapse_and_the_listing_matches( + self, repo: ModuleType, tmp_path: Path, monkeypatch: pytest.MonkeyPatch + ) -> None: + a = write(tmp_path / "a.tif", shifted(0)) + b = write(tmp_path / "b.tif", shifted(3)) + monkeypatch.chdir(tmp_path) + explicit = repo.TiffDemRepository([b, a, Path("a.tif"), tmp_path / "." / "b.tif"]) + listed = repo.TiffDemRepository.from_directory(tmp_path) + assert explicit.footprints() == listed.footprints() + assert [f.name for f in explicit.footprints()] == ["a.tif", "b.tif"] + + def test_two_files_with_one_name_are_refused(self, repo: ModuleType, tmp_path: Path) -> None: + """A footprint's name is the file name, so it must be unique.""" + one = write(tmp_path / "x" / "t.tif") + two = write(tmp_path / "y" / "t.tif", shifted(3)) + with pytest.raises(ValueError, match=r"\bt\.tif"): + repo.TiffDemRepository([one, two]).footprints() + + def test_it_satisfies_the_protocol(self, repo: ModuleType, tmp_path: Path) -> None: + write(tmp_path / "a.tif") + repository: object = repo.TiffDemRepository.from_directory(tmp_path) + assert callable(getattr(repository, "footprints", None)) + assert callable(getattr(repository, "load", None)) + assert set(repo.DemRepository.__dict__) >= {"footprints", "load"} + + +# --------------------------------------------------------------------------- +# Q3: io/repository.py is the one module in io/ that opens files +# --------------------------------------------------------------------------- + +#: Calls that reach the filesystem. A call by attribute is matched by name +#: alone, so `path.open()` and `tifffile.imread()` are both found. +OPENERS = frozenset( + { + "open", + "read_bytes", + "read_text", + "write_bytes", + "write_text", + "imread", + "imwrite", + "memmap", + "fromfile", + } +) + + +def file_openers(source: str) -> list[str]: + """Every call in `source` that opens a file, as `name:line`.""" + found = [] + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Call): + func = node.func + name = ( + func.id + if isinstance(func, ast.Name) + else func.attr + if isinstance(func, ast.Attribute) + else "" + ) + if name in OPENERS: + found.append(f"{name}:{node.lineno}") + return found + + +def open_modes(source: str) -> list[str]: + """The mode of every `open` call in `source` ("r" when none is given).""" + modes = [] + for node in ast.walk(ast.parse(source)): + if isinstance(node, ast.Call): + func = node.func + name = ( + func.id + if isinstance(func, ast.Name) + else func.attr + if isinstance(func, ast.Attribute) + else "" + ) + if name == "open": + positional = node.args[1:] if isinstance(func, ast.Name) else node.args + mode = ( + positional[0] + if positional + else next((k.value for k in node.keywords if k.arg == "mode"), None) + ) + modes.append( + mode.value if isinstance(mode, ast.Constant) else "r" if mode is None else "?" + ) + return modes + + +class TestQ3OneModuleOpensFiles: + @pytest.mark.parametrize( + "planted", + [ + "open(p)", + "with path.open('rb') as f: pass", + "Path(p).read_bytes()", + "tifffile.imread(p)", + "np.memmap(p)", + ], + ) + def test_the_scanner_finds_a_planted_opener(self, planted: str) -> None: + assert file_openers(planted), planted + + def test_the_scanner_ignores_streams_and_prose(self) -> None: + clean = '"""Nothing here opens a file."""\ntif = tifffile.TiffFile(stream)\nstream.read()\n' + assert file_openers(clean) == [] + + def test_no_other_io_module_opens_a_file(self) -> None: + offenders = { + path.name: found + for path in sorted(IO.glob("*.py")) + if path.name != "repository.py" + and (found := file_openers(path.read_text(encoding="utf-8"))) + } + assert offenders == {} + + def test_the_repository_opens_files_read_only(self) -> None: + source = (IO / "repository.py").read_text(encoding="utf-8") + assert file_openers(source), "the repository must be the module that opens the tiles" + modes = open_modes(source) + assert modes and all(mode == "rb" for mode in modes), modes + + def test_the_package_docstring_says_so(self) -> None: + """Q3 (a): `io/`'s rule narrowed, in the package's own docstring.""" + doc = ast.get_docstring(ast.parse((IO / "__init__.py").read_text(encoding="utf-8"))) or "" + assert "repository.py" in doc + assert "Nothing here opens a file." not in doc diff --git a/tests/python/test_mosaic.py b/tests/python/test_mosaic.py new file mode 100644 index 00000000..e2f70218 --- /dev/null +++ b/tests/python/test_mosaic.py @@ -0,0 +1,901 @@ +"""`tin_engine.mosaic`: planning and assembly of a multi-tile DEM (increment 15a). + +The design is `docs/increments/15-dem-mosaic.md` (R1, R4, R5, R7; invariants +I1-I7); names not fixed there are pinned in its "Pinned by the red suite (15a)" +subsection. Test ids follow its "Tests for @tester" list, which carries the +parked design's M1-M12 (`git show f8cedbe:docs/increments/15-dem-repository.md`) +with 15a's changes: M2 is lattice grouping, M5 the cap from physical memory, +and M13-M15 are new. + +This is 15a's **invariant-critical** suite. Its tests were run against a +scratch implementation and against deliberate mutants of it (a wrong lattice +alignment check, an off-by-one in stitching offsets, an overlap check that +accepts a disagreement, a coverage check that misses a gap); the handback of +the red commit lists which test killed which. + +HOW THIS FILE GOES RED. `tin_engine.mosaic` and `tin_engine.io.repository` +are imported in module-scoped fixtures, as `test_bench.py` loads its tool, so +while they are missing each test fails on its own with `ModuleNotFoundError` +and the rest of `tests/python` still collects and runs. + +Tiles are built from `RasterMeta` and `DemTile` directly (`mosaic_fixtures`), +and the repository is a dict: nothing here touches a file. +""" + +from __future__ import annotations + +import importlib +import itertools +import math +import re +from pathlib import Path +from types import ModuleType +from typing import Any + +import numpy as np +import pytest +import shapely + +from mosaic_fixtures import ( + DX, + DY, + SENTINEL, + X0, + Y0, + Loads, + blocks, + footprints, + meta, + piece, + quadrants, + same_array, + values, + whole, +) +from tin_engine.io.models import DemTile + +SRC = Path(__file__).resolve().parents[2] / "src_python" / "tin_engine" + + +@pytest.fixture(scope="module") +def mz() -> ModuleType: + return importlib.import_module("tin_engine.mosaic") + + +@pytest.fixture(scope="module") +def footprint() -> Any: + return importlib.import_module("tin_engine.io.repository").TileFootprint + + +@pytest.fixture +def plan(mz: ModuleType, footprint: Any) -> Any: + """`plan(tiles, box=None, needed=None)`: plan a dict of tiles; `box` is a 4-tuple.""" + + def run( + tiles: dict[str, DemTile], box: tuple[float, ...] | None = None, needed: Any = None + ) -> Any: + bounds = None if box is None else bounds_of(mz, box) + return mz.plan_mosaic(footprints(footprint, tiles), bounds, needed) + + return run + + +@pytest.fixture +def build(mz: ModuleType, plan: Any) -> Any: + """`build(tiles, box=None, needed=None) -> (Mosaic, Loads)`: plan, then assemble.""" + + def run( + tiles: dict[str, DemTile], box: tuple[float, ...] | None = None, needed: Any = None + ) -> Any: + loads = Loads(tiles) + return mz.assemble(plan(tiles, box, needed), loads), loads + + return run + + +@pytest.fixture +def refused(mz: ModuleType) -> Any: + """`refused(call, *tokens)`: `call()` raises `MosaicError` naming each token.""" + + def check(call: Any, *tokens: str) -> str: + with pytest.raises(mz.MosaicError) as info: + call() + message = str(info.value) + for token in tokens: + assert token.lower() in message.lower(), f"{token!r} not named in {message!r}" + return message + + return check + + +def bounds_of(mz: ModuleType, box: tuple[float, ...]) -> Any: + x_min, y_min, x_max, y_max = box + return mz.Bounds(x_min=x_min, y_min=y_min, x_max=x_max, y_max=y_max) + + +def window(mz: ModuleType, row0: int, col0: int, rows: int, cols: int) -> Any: + return mz.IndexWindow(row0=row0, col0=col0, rows=rows, cols=cols) + + +def names_number(message: str, number: int) -> None: + """`number` appears as a whole number, not inside another (`3` is not in `317`).""" + assert re.search(rf"(? tuple[int, int]: + """The global index of tile `name`'s first node, as the plan places it.""" + (p,) = [p for p in plan.tiles if p.name == name] + return ( + plan.window.row0 + p.canvas.row0 - p.source.row0, + plan.window.col0 + p.canvas.col0 - p.source.col0, + ) + + +# --------------------------------------------------------------------------- +# The module's surface +# --------------------------------------------------------------------------- + + +def test_mosaic_error_is_a_value_error(mz: ModuleType) -> None: + """R11: refusals become `typer.BadParameter` like `GeoTiffError`, one type.""" + assert issubclass(mz.MosaicError, ValueError) + + +def test_align_tolerance_is_a_millionth_of_a_cell(mz: ModuleType) -> None: + assert mz.ALIGN_TOLERANCE == 1e-6 + + +def test_an_empty_repository_is_refused(mz: ModuleType, refused: Any) -> None: + refused(lambda: mz.plan_mosaic([], None, None)) + + +class TestBounds: + """R6: `Bounds` is four finite floats with `x_min < x_max`, `y_min < y_max`.""" + + def test_a_valid_box(self, mz: ModuleType) -> None: + box = bounds_of(mz, (1.0, 2.0, 3.0, 4.0)) + assert (box.x_min, box.y_min, box.x_max, box.y_max) == (1.0, 2.0, 3.0, 4.0) + + @pytest.mark.parametrize( + "box", + [ + (math.nan, 0.0, 1.0, 1.0), + (0.0, 0.0, math.inf, 1.0), + (0.0, -math.inf, 1.0, 1.0), + (1.0, 0.0, 0.0, 1.0), # inverted in x + (0.0, 1.0, 1.0, 0.0), # inverted in y + (0.0, 0.0, 0.0, 1.0), # zero width + (0.0, 1.0, 1.0, 1.0), # zero height + ], + ids=["nan", "inf", "-inf", "inverted-x", "inverted-y", "zero-width", "zero-height"], + ) + def test_refused(self, mz: ModuleType, box: tuple[float, ...]) -> None: + with pytest.raises(ValueError): + bounds_of(mz, box) + + +# --------------------------------------------------------------------------- +# Planning (no pixels) +# --------------------------------------------------------------------------- + + +class TestM1Layout: + """M1: canvas shape, origin and each tile's two windows.""" + + def test_point_registered_2x2_sharing_a_row_and_a_column( + self, mz: ModuleType, plan: Any + ) -> None: + source = whole() # 9 x 13 + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + result = plan(tiles) + assert result.meta == source.meta + assert result.window == window(mz, 0, 0, 9, 13) + assert [p.name for p in result.tiles] == ["ne.tif", "nw.tif", "se.tif", "sw.tif"] + placed = {p.name: p for p in result.tiles} + expected = {"nw.tif": (0, 0), "ne.tif": (0, 6), "sw.tif": (4, 0), "se.tif": (4, 6)} + for name, (row0, col0) in expected.items(): + assert placed[name].canvas == window(mz, row0, col0, 5, 7), name + assert placed[name].source == window(mz, 0, 0, 5, 7), name + assert placed[name].meta == tiles[name].meta, name + + def test_area_registered_2x2_abutting(self, mz: ModuleType, plan: Any) -> None: + """Disjoint node sets one spacing apart: the canvas is contiguous and + nothing between them is a gap (the coverage check must not see one).""" + source = whole(rows=8, cols=12, area=True) + result = plan(quadrants(source, row_cut=4, col_cut=6, overlap=0)) + assert result.meta == source.meta + placed = {p.name: p.canvas for p in result.tiles} + assert placed == { + "nw.tif": window(mz, 0, 0, 4, 6), + "ne.tif": window(mz, 0, 6, 4, 6), + "sw.tif": window(mz, 4, 0, 4, 6), + "se.tif": window(mz, 4, 6, 4, 6), + } + + def test_one_tile_plans_as_itself(self, mz: ModuleType, plan: Any) -> None: + source = whole() + result = plan({"only.tif": source}) + assert result.meta == source.meta + assert result.reference == (X0, Y0) + assert result.window == window(mz, 0, 0, 9, 13) + + +class TestM2LatticeGrouping: + """M2, changed for 15a (R4 points 1 and 3, Q5): lattices, not one lattice. + + The main lattice is the 9 x 13 point grid cut in four. `odd.tif` lies + south-east of it, half a cell east-west off (N2's case), overlapping `se`. + """ + + @staticmethod + def repository(**odd_changes: Any) -> dict[str, DemTile]: + tiles = quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + fields: dict[str, Any] = {"x_min": X0 + 10.5 * DX, "y_max": Y0 - 6 * DY} + fields.update(odd_changes) + tiles["odd.tif"] = whole(rows=5, cols=7, **fields) + return tiles + + def test_a_request_inside_the_main_lattice_plans(self, plan: Any) -> None: + result = plan(self.repository(), (500012.0, 6599987.0, 500033.0, 6599996.0)) + assert [p.name for p in result.tiles] == ["nw.tif"] + assert result.reference == (X0, Y0) + + def test_a_request_inside_the_odd_lattice_plans_on_that_lattice( + self, mz: ModuleType, plan: Any + ) -> None: + result = plan(self.repository(), (500130.0, 6599952.0, 500160.0, 6599958.0)) + assert [p.name for p in result.tiles] == ["odd.tif"] + assert result.reference == (X0 + 10.5 * DX, Y0 - 6 * DY) + assert result.meta.x_min == 500125.0 # odd lattice: 500105 + 2 * 10 + assert result.window == window(mz, 2, 2, 3, 5) + + def test_a_request_straddling_both_is_refused_naming_one_of_each( + self, plan: Any, refused: Any + ) -> None: + tiles = self.repository() + refused( + lambda: plan(tiles, (500100.0, 6599955.0, 500130.0, 6599965.0)), + "se.tif", "odd.tif", "0.5 cell", "east-west", + ) # fmt: skip + + def test_no_bounds_selects_both_lattices_and_is_refused(self, plan: Any, refused: Any) -> None: + tiles = self.repository() + refused(lambda: plan(tiles), "odd.tif", "0.5 cell") + + def test_a_half_cell_north_south_is_named_so(self, plan: Any, refused: Any) -> None: + tiles = self.repository(x_min=X0 + 11 * DX, y_max=Y0 - 6.5 * DY) + refused( + lambda: plan(tiles, (500100.0, 6599955.0, 500130.0, 6599965.0)), + "odd.tif", + "0.5 cell", + "north-south", + ) + + def test_a_thousandth_of_a_cell_is_another_lattice(self, plan: Any, refused: Any) -> None: + tiles = self.repository(x_min=X0 + (11 + 1e-3) * DX) + refused( + lambda: plan(tiles, (500100.0, 6599955.0, 500130.0, 6599965.0)), + "se.tif", + "odd.tif", + "cell", + ) + + @pytest.mark.parametrize( + ("change", "tokens"), + [ + ({"epsg": 25832}, ("25833", "25832")), + ({"epsg": 32633}, ("25833", "32633")), + ({"dx": 12.5}, ("spacing", "12.5")), + ({"dy": 7.5}, ("spacing", "7.5")), + ({"area": True}, ("registration",)), + ({"nodata": -9999.0, "nodata_source": "tag"}, ("nodata", "-9999", "none")), + ], + ids=["crs-25832", "crs-32633", "spacing-dx", "spacing-dy-alone", "registration", "nodata"], + ) + def test_other_differences_are_named_as_such( + self, plan: Any, refused: Any, change: dict[str, Any], tokens: tuple[str, ...] + ) -> None: + """The parked `refuses_mixed_crs_mosaic`, `_spacing`, `_registration`, `_nodata`. + + `ne.tif` differs from `nw.tif` in exactly one field and is otherwise + where `quadrants` puts it, so only that field can be the reason. + """ + source = whole() + tiles = { + "nw.tif": piece(source, 0, 5, 0, 7), + "ne.tif": piece(source, 0, 5, 6, 13, **_meta_change(change)), + } + refused(lambda: plan(tiles), "nw.tif", "ne.tif", *tokens) + + def test_mixed_nodata_sentinels_are_refused_q4(self, plan: Any, refused: Any) -> None: + """Q4 (a): two sentinels, both named.""" + source = whole(nodata=SENTINEL) + tiles = { + "nw.tif": piece(source, 0, 5, 0, 7), + "ne.tif": piece(source, 0, 5, 6, 13, nodata=-9999.0), + } + refused(lambda: plan(tiles), "nw.tif", "ne.tif", "-32767", "-9999") + + def test_a_different_crs_outside_the_request_is_not_a_refusal(self, plan: Any) -> None: + """R4 changed the parked rule: only the *selected* tiles share a lattice.""" + tiles = quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + tiles["far.tif"] = whole(rows=3, cols=3, x_min=X0 + 1000 * DX, epsg=25832) + result = plan(tiles, (500012.0, 6599987.0, 500033.0, 6599996.0)) + assert [p.name for p in result.tiles] == ["nw.tif"] + + +def _meta_change(change: dict[str, Any]) -> dict[str, Any]: + """`piece` takes `RasterMeta` field names.""" + names = {"dx": "delta_x", "dy": "delta_y", "area": "pixel_is_area"} + return {names.get(key, key): value for key, value in change.items()} + + +class TestM3AlignmentTolerance: + """M3 and R4 point 1: offsets within `ALIGN_TOLERANCE` cells of an integer.""" + + def test_a_decimetre_grid_with_float_noise_plans_on_integer_windows( + self, mz: ModuleType, plan: Any + ) -> None: + source = whole(x_min=600_000.0, y_max=7_000_000.0, dx=0.1, dy=0.1) + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + # A producer that accumulates its tie point: 6 steps of 0.1 is not 0.6. + accumulated = 600_000.0 + for _ in range(6): + accumulated += 0.1 + tiles["ne.tif"] = piece(source, 0, 5, 6, 13, x_min=accumulated) + assert (accumulated - 600_000.0) / 0.1 != 6 # the noise is there (1.4e-9 cells) + result = plan(tiles) + placed = {p.name: p.canvas for p in result.tiles} + assert placed["ne.tif"] == window(mz, 0, 6, 5, 7) + assert placed["se.tif"] == window(mz, 4, 6, 5, 7) + assert result.meta.x_min == 600_000.0 + + @pytest.mark.parametrize(("noise", "accepted"), [(5e-7, True), (2e-6, False)]) + def test_the_tolerance_boundary( + self, plan: Any, noise: float, accepted: bool, refused: Any + ) -> None: + source = whole() + tiles = { + "nw.tif": piece(source, 0, 5, 0, 7), + "ne.tif": piece(source, 0, 5, 6, 13, x_min=X0 + (6 + noise) * DX), + } + if accepted: + assert len(plan(tiles).tiles) == 2 + else: + refused(lambda: plan(tiles), "nw.tif", "ne.tif") + + +class TestM4Bounds: + """M4 and R4 point 4: the box snapped outward to the lattice, clamped.""" + + @pytest.fixture + def tiles(self) -> dict[str, DemTile]: + return quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + + @pytest.mark.parametrize( + ("box", "expected"), + [ + # cols floor(1.2)=1 .. ceil(3.3)=4; rows floor(0.8)=0 .. ceil(2.6)=3 + ((500012.0, 6599987.0, 500033.0, 6599996.0), (0, 1, 4, 4)), + # inside one cell: 2 x 2 nodes + ((500012.0, 6599991.0, 500018.0, 6599994.0), (1, 1, 2, 2)), + # on nodes exactly: floor and ceil of integers add nothing + ((500010.0, 6599985.0, 500030.0, 6599995.0), (1, 1, 3, 3)), + # past the west and north edges: clamped + ((499000.0, 6599991.0, 500025.0, 6600500.0), (0, 0, 3, 4)), + # at the east edge, two columns remain + ((500115.0, 6599981.0, 500200.0, 6599984.0), (3, 11, 2, 2)), + ], + ids=["outward", "inside-one-cell", "on-nodes", "clamped", "east-edge"], + ) + def test_the_window( + self, + mz: ModuleType, + build: Any, + tiles: dict[str, DemTile], + box: tuple[float, ...], + expected: tuple[int, ...], + ) -> None: + row0, col0, rows, cols = expected + result, _ = build(tiles, box) + assert result.plan.window == window(mz, *expected) + assert result.plan.meta.x_min == X0 + col0 * DX + assert result.plan.meta.y_max == Y0 - row0 * DY + assert (result.plan.meta.rows, result.plan.meta.cols) == (rows, cols) + assert same_array(result.tile.array, whole().array[row0 : row0 + rows, col0 : col0 + cols]) + + def test_a_tile_touching_the_box_on_one_node_line_contributes_it( + self, mz: ModuleType, build: Any + ) -> None: + source = whole(rows=8, cols=12, area=True) + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=0) + # x_max lands exactly on ne's first column (500060); y on the top rows. + result, loads = build(tiles, (500012.0, 6599991.0, 500060.0, 6599999.0)) + placed = {p.name: p for p in result.plan.tiles} + assert set(placed) == {"nw.tif", "ne.tif"} + assert placed["ne.tif"].source == window(mz, 0, 0, 3, 1) + assert placed["ne.tif"].canvas == window(mz, 0, 5, 3, 1) + assert same_array(result.tile.array, source.array[0:3, 1:7]) + assert sorted(loads.calls) == ["ne.tif", "nw.tif"] + + def test_a_box_meeting_no_tile_is_refused( + self, plan: Any, refused: Any, tiles: dict[str, DemTile] + ) -> None: + refused(lambda: plan(tiles, (400000.0, 6000000.0, 400100.0, 6000100.0))) + + def test_a_box_leaving_one_column_at_the_edge_is_refused( + self, plan: Any, refused: Any, tiles: dict[str, DemTile] + ) -> None: + refused(lambda: plan(tiles, (500120.0, 6599980.0, 500200.0, 6599990.0))) + + +class TestM5MemoryCap: + """M5, changed for 15a (R7): the cap is half of physical memory, from a function.""" + + NODES = 9 * 13 + + def test_physical_memory_is_the_machines(self, mz: ModuleType) -> None: + import os + + pages = os.sysconf("SC_PHYS_PAGES") * os.sysconf("SC_PAGE_SIZE") + assert mz.physical_memory() == pages + + @pytest.mark.parametrize( + ("memory", "accepted"), [(2 * NODES * 4, True), (2 * NODES * 4 - 1, False)] + ) + def test_the_boundary_at_four_bytes_a_node( + self, + mz: ModuleType, + plan: Any, + refused: Any, + monkeypatch: pytest.MonkeyPatch, + memory: int, + accepted: bool, + ) -> None: + monkeypatch.setattr(mz, "physical_memory", lambda: memory) + tiles = quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + if accepted: + assert plan(tiles).meta.rows == 9 + else: + refused(lambda: plan(tiles), "--bbox") + + def test_the_cap_is_on_the_window_not_the_union( + self, mz: ModuleType, plan: Any, monkeypatch: pytest.MonkeyPatch + ) -> None: + monkeypatch.setattr(mz, "physical_memory", lambda: 2 * 16 * 4) + tiles = quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + assert plan(tiles, (500012.0, 6599987.0, 500033.0, 6599996.0)).meta.cols == 4 + + +class TestM6PlanOrderIndependence: + """M6 and I2: every permutation of the footprints gives the same plan.""" + + def test_every_permutation(self, mz: ModuleType, footprint: Any) -> None: + tiles = quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + prints = footprints(footprint, tiles) + first = mz.plan_mosaic(prints, None, None) + for order in itertools.permutations(prints): + assert mz.plan_mosaic(list(order), None, None) == first + + +class TestM13ReferenceNode: + """M13 and R4 point 2: global indices count from the lattice's reference node. + + Spacing 0.3 x 0.7 and an origin picked (by search) so that stepping from + `se`'s own origin, `(XR + 6 dx) + dx`, differs in the last bit from + `XR + 7 dx`: a formula other than `X_ref + c0 * dx` fails bit-for-bit. + """ + + XR, YR, SX, SY = 3448.3132, 515.2302, 0.3, 0.7 # searched: se's own origin + 1 cell differs + + @pytest.fixture + def tiles(self) -> dict[str, DemTile]: + source = whole(x_min=self.XR, y_max=self.YR, dx=self.SX, dy=self.SY) + return quadrants(source, row_cut=4, col_cut=6, overlap=1) + + def box(self, c0: float, r0: float, c1: float, r1: float) -> tuple[float, ...]: + return ( + self.XR + c0 * self.SX, + self.YR - r1 * self.SY, + self.XR + c1 * self.SX, + self.YR - r0 * self.SY, + ) + + def test_the_reference_is_the_north_west_node_of_the_whole_lattice( + self, plan: Any, tiles: dict[str, DemTile] + ) -> None: + only_se = plan(tiles, self.box(7.5, 5.5, 10.2, 7.3)) + assert [p.name for p in only_se.tiles] == ["se.tif"] + assert only_se.reference == (self.XR, self.YR) + + @pytest.mark.parametrize( + "box", [None, (7.5, 5.5, 10.2, 7.3), (0.5, 0.5, 12.0, 8.0), (5.5, 3.5, 6.5, 4.5)], + ids=["no-bounds", "se-alone", "most", "the-shared-corner"], + ) # fmt: skip + def test_the_origin_is_reference_plus_index_times_spacing_bit_for_bit( + self, plan: Any, tiles: dict[str, DemTile], box: tuple[float, ...] | None + ) -> None: + result = plan(tiles, None if box is None else self.box(*box)) + assert result.meta.x_min == result.reference[0] + result.window.col0 * self.SX + assert result.meta.y_max == result.reference[1] - result.window.row0 * self.SY + + def test_a_tiles_global_index_does_not_depend_on_the_request( + self, plan: Any, tiles: dict[str, DemTile] + ) -> None: + requests = [ + None, + self.box(7.5, 5.5, 10.2, 7.3), + self.box(0.5, 0.5, 12.0, 8.0), + self.box(5.5, 3.5, 6.5, 4.5), + ] + for box in requests: + assert global_origin(plan(tiles, box), "se.tif") == (4, 6), box + + def test_the_mosaic_meta_takes_the_combined_provenance(self, plan: Any) -> None: + """R4: `nodata_source` from the first tile by name; `vertical_unit_assumed` if any.""" + source = whole(nodata=SENTINEL, vertical_unit_assumed=False) + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + tiles["ne.tif"] = piece(source, 0, 5, 6, 13, nodata_source="caller") + tiles["sw.tif"] = piece(source, 4, 9, 0, 7, vertical_unit_assumed=True) + result = plan(tiles) + assert result.meta.nodata == SENTINEL + assert result.meta.nodata_source == "caller" + assert result.meta.vertical_unit_assumed is True + + +class TestM14Coverage: + """M14 and R4 point 5 (Ola's ruling in 18 R6): a needed node no tile covers. + + Area-registered 3 x 4-node blocks of a 6 x 8 grid, with block (1, 1) + missing: its nodes are x 500040..500070, y 6599975..6599985. + """ + + @pytest.fixture + def source(self) -> DemTile: + return whole(rows=6, cols=8, area=True) + + @pytest.fixture + def holed(self, source: DemTile) -> dict[str, DemTile]: + return blocks(source, 3, 4, skip={(1, 1)}) + + def test_a_hole_inside_the_window_is_refused_naming_it( + self, plan: Any, refused: Any, holed: dict[str, DemTile] + ) -> None: + refused(lambda: plan(holed), "500040", "500070", "6599975", "6599985") + + def test_a_hole_in_the_middle_is_refused(self, plan: Any, refused: Any) -> None: + """No corner of the window is uncovered: a check of the corners, or of + the union's bounding box, passes this; only a check of every node fails it.""" + tiles = blocks(whole(rows=9, cols=12, area=True), 3, 4, skip={(1, 1)}) + refused(lambda: plan(tiles), "500040", "500070") + + def test_a_request_away_from_the_hole_plans(self, plan: Any, holed: dict[str, DemTile]) -> None: + result = plan(holed, (500001.0, 6599991.0, 500069.0, 6599999.0)) + assert {p.name for p in result.tiles} == {"b00.tif", "b01.tif"} + + def test_a_request_reaching_one_hole_node_is_refused( + self, plan: Any, refused: Any, holed: dict[str, DemTile] + ) -> None: + # rows 0..3 (y 6600000..6599985), cols 0..4 (x 500000..500040): node + # (500040, 6599985) is the hole's north-west node. + refused( + lambda: plan(holed, (500000.0, 6599985.0, 500040.0, 6600000.0)), "500040", "6599985" + ) + + def test_the_hole_outside_the_needed_region_is_nan_filler( + self, build: Any, source: DemTile, holed: dict[str, DemTile] + ) -> None: + needed = shapely.box(500000.0, 6599990.0, 500070.0, 6600000.0).union( + shapely.box(500000.0, 6599975.0, 500030.0, 6599990.0) + ) + result, _ = build(holed, None, needed) + array = result.tile.array + assert np.isnan(array[3:, 4:]).all() + mask = np.ones(array.shape, dtype=bool) + mask[3:, 4:] = False + assert np.array_equal(array[mask], source.array[mask]) + + def test_a_needed_region_touching_one_hole_node_is_refused( + self, plan: Any, refused: Any, holed: dict[str, DemTile] + ) -> None: + needed = shapely.box(500000.0, 6599990.0, 500070.0, 6600000.0).union( + shapely.box(500000.0, 6599975.0, 500040.0, 6599990.0) + ) # reaches x 500040: the hole's west column, on the boundary + refused(lambda: plan(holed, None, needed), "500040") + + +# --------------------------------------------------------------------------- +# Assembly (pixels, no files) +# --------------------------------------------------------------------------- + + +class TestM7Values: + """M7: values at the right nodes; the result's array contract.""" + + def test_values_land_at_their_nodes(self, build: Any) -> None: + source = whole() + result, _ = build(quadrants(source, row_cut=4, col_cut=6, overlap=1)) + assert result.tile.meta == source.meta + assert same_array(result.tile.array, source.array) + + def test_the_array_is_read_only_and_c_contiguous(self, build: Any) -> None: + result, _ = build(quadrants(whole(), row_cut=4, col_cut=6, overlap=1)) + array = result.tile.array + assert not array.flags.writeable + assert array.flags.c_contiguous + with pytest.raises(ValueError): + array[0, 0] = 1.0 + + def test_float64_when_any_tile_is(self, build: Any) -> None: + source = whole() + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + tiles["se.tif"] = piece( + source, 4, 9, 6, 13, array=source.array[4:9, 6:13].astype(np.float64) + ) + result, _ = build(tiles) + assert result.tile.array.dtype == np.float64 + assert same_array(result.tile.array, source.array.astype(np.float64)) + + def test_float32_stays_float32(self, build: Any) -> None: + result, _ = build(quadrants(whole(), row_cut=4, col_cut=6, overlap=1)) + assert result.tile.array.dtype == np.float32 + + +class TestM8Overlaps: + """M8, R5 and I3: valid beats NoData; disagreement refuses (Q1). + + `w.tif` and `e.tif` cut from a 4 x 9 grid, sharing `overlap` columns. + """ + + ROWS, COLS = 4, 9 + + @staticmethod + def pair(overlap: int, nodata: float | None = None) -> tuple[DemTile, dict[str, np.ndarray]]: + source = whole(rows=TestM8Overlaps.ROWS, cols=TestM8Overlaps.COLS, nodata=nodata) + cut = 4 + return source, { + "w.tif": source.array[:, : cut + overlap].copy(), + "e.tif": source.array[:, cut:].copy(), + } + + @staticmethod + def tiles(source: DemTile, arrays: dict[str, np.ndarray]) -> dict[str, DemTile]: + w, e = arrays["w.tif"], arrays["e.tif"] + return { + "w.tif": piece(source, 0, w.shape[0], 0, w.shape[1], array=w), + "e.tif": piece(source, 0, e.shape[0], 4, 4 + e.shape[1], array=e), + } + + @pytest.mark.parametrize("overlap", [1, 3]) + @pytest.mark.parametrize("holder", ["w.tif", "e.tif"]) + def test_valid_beats_nan_in_either_tile(self, build: Any, overlap: int, holder: str) -> None: + source, arrays = self.pair(overlap) + column = 4 if holder == "e.tif" else 4 + overlap - 1 + local = column - (4 if holder == "e.tif" else 0) + arrays[holder][1, local] = np.nan + result, _ = build(self.tiles(source, arrays)) + assert same_array(result.tile.array, source.array) + + @pytest.mark.parametrize("holder", ["w.tif", "e.tif"]) + def test_valid_beats_the_sentinel_in_either_tile(self, build: Any, holder: str) -> None: + source, arrays = self.pair(3, nodata=SENTINEL) + arrays[holder][2, 5 if holder == "w.tif" else 1] = SENTINEL # canvas column 5 + result, _ = build(self.tiles(source, arrays)) + assert same_array(result.tile.array, source.array) + + def test_both_nan_stays_nan_and_both_sentinel_stays_sentinel(self, build: Any) -> None: + source, arrays = self.pair(3, nodata=SENTINEL) + arrays["w.tif"][0, 5] = arrays["e.tif"][0, 1] = np.nan + arrays["w.tif"][3, 6] = arrays["e.tif"][3, 2] = SENTINEL + result, _ = build(self.tiles(source, arrays)) + array = result.tile.array + assert np.isnan(array[0, 5]) + assert array[3, 6] == SENTINEL + expected = source.array.copy() + expected[0, 5], expected[3, 6] = np.nan, SENTINEL + assert same_array(array, expected) + + def test_nan_against_the_sentinel_is_nodata_in_either_order( + self, mz: ModuleType, plan: Any + ) -> None: + """Which NoData wins is not ruled; that it is NoData, and the same in + every order, is (I2, I3).""" + source, arrays = self.pair(3, nodata=SENTINEL) + arrays["w.tif"][1, 4] = np.nan + arrays["e.tif"][1, 0] = SENTINEL + tiles = self.tiles(source, arrays) + planned = plan(tiles) + results = [ + mz.assemble(planned.model_copy(update={"tiles": order}), Loads(tiles)).tile.array + for order in itertools.permutations(planned.tiles) + ] + value = results[0][1, 4] + assert np.isnan(value) or value == SENTINEL + assert all(same_array(r, results[0]) for r in results) + + def test_equal_overlaps_are_accepted(self, build: Any) -> None: + source, arrays = self.pair(3) + result, _ = build(self.tiles(source, arrays)) + assert same_array(result.tile.array, source.array) + + @pytest.mark.parametrize("order", ["as-named", "reversed"]) + def test_a_disagreement_is_refused_naming_both_the_count_and_the_largest( + self, mz: ModuleType, plan: Any, refused: Any, order: str + ) -> None: + source, arrays = self.pair(3) + arrays["e.tif"][0, 1] += 0.5 + arrays["e.tif"][2, 1] += 2.5 # the middle column of the overlap + arrays["e.tif"][3, 2] -= 1.0 + arrays["w.tif"][1, 4] = np.nan # a NoData node in the overlap is not counted + tiles = self.tiles(source, arrays) + planned = plan(tiles) + if order == "reversed": + planned = planned.model_copy(update={"tiles": tuple(reversed(planned.tiles))}) + message = refused(lambda: mz.assemble(planned, Loads(tiles)), "w.tif", "e.tif", "2.5") + names_number(message, 3) + + def test_one_ulp_is_a_disagreement(self, build: Any, refused: Any) -> None: + """`==`, not `isclose`: DTM10's and ANADEM's overlaps agree bit for bit (N3, B4).""" + source, arrays = self.pair(1) + value = arrays["e.tif"][2, 0] + arrays["e.tif"][2, 0] = np.nextafter(value, np.float32(np.inf), dtype=np.float32) + message = refused(lambda: build(self.tiles(source, arrays)), "w.tif", "e.tif") + names_number(message, 1) + + def test_a_disagreement_against_a_sentinel_tile_is_still_one( + self, build: Any, refused: Any + ) -> None: + """The sentinel is NoData only where a node holds it; elsewhere a value is a value.""" + source, arrays = self.pair(3, nodata=SENTINEL) + arrays["w.tif"][0, 4] = SENTINEL + arrays["w.tif"][0, 5] += 7.0 + message = refused(lambda: build(self.tiles(source, arrays)), "w.tif", "e.tif", "7") + names_number(message, 1) + + +class TestM9SplitAndRestitch: + """M9 and I5: a grid cut into tiles reassembles to itself, meta and array.""" + + @pytest.mark.parametrize( + ("area", "overlap", "shape"), + [(False, 1, (9, 13)), (True, 0, (8, 12)), (True, 3, (8, 12)), (False, 3, (9, 13))], + ids=["point-shared-line", "area-abutting", "area-overlap-3", "point-overlap-3"], + ) + def test_quadrants(self, build: Any, area: bool, overlap: int, shape: tuple[int, int]) -> None: + source = whole(*shape, area=area) + result, _ = build(quadrants(source, row_cut=4, col_cut=6, overlap=overlap)) + assert result.tile.meta == source.meta + assert same_array(result.tile.array, source.array) + + def test_with_nodata_inside_and_across_the_seams(self, build: Any) -> None: + grid = values(9, 13) + grid[0, 0] = grid[4, 6] = grid[5, 7] = grid[8, 12] = SENTINEL # (4, 6) is in all four + grid[4, 2] = grid[6, 6] = grid[2, 9] = np.nan # on the seams + source = whole(9, 13, array=grid, nodata=SENTINEL) + result, _ = build(quadrants(source, row_cut=4, col_cut=6, overlap=3)) + assert result.tile.meta == source.meta + assert same_array(result.tile.array, source.array) + + def test_a_three_by_three_block_split(self, build: Any) -> None: + source = whole(9, 12, area=True) + result, _ = build(blocks(source, 3, 4)) + assert result.tile.meta == source.meta + assert same_array(result.tile.array, source.array) + + +class TestM10AssemblyOrderIndependence: + """M10 and I2: the assembled array does not depend on tile order.""" + + def test_every_order_of_the_plans_tiles(self, mz: ModuleType, plan: Any) -> None: + grid = values(9, 13) + grid[5, 7] = np.nan + grid[4, 6] = SENTINEL + source = whole(9, 13, array=grid, nodata=SENTINEL) + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=3) + planned = plan(tiles) + for order in itertools.permutations(planned.tiles): + result = mz.assemble(planned.model_copy(update={"tiles": order}), Loads(tiles)) + assert same_array(result.tile.array, source.array), [p.name for p in order] + + def test_tiles_are_loaded_once_each_in_plan_order(self, build: Any) -> None: + result, loads = build(quadrants(whole(), row_cut=4, col_cut=6, overlap=1)) + assert loads.calls == [p.name for p in result.plan.tiles] == sorted(loads.calls) + + +class TestM11SingleTile: + """M11 and I7: one tile, no bounds: the loaded tile itself, no canvas.""" + + def test_the_loaded_tile_is_returned_as_is(self, build: Any) -> None: + source = whole() + result, loads = build({"only.tif": source}) + assert result.tile is loads.tiles["only.tif"] + + def test_with_bounds_it_is_a_window(self, build: Any) -> None: + source = whole() + result, _ = build({"only.tif": source}, (500012.0, 6599987.0, 500033.0, 6599996.0)) + assert same_array(result.tile.array, source.array[0:4, 1:5]) + + +class TestM12Loads: + """M12: only the selected tiles are loaded; a changed tile is refused.""" + + def test_only_selected_tiles_are_loaded(self, build: Any) -> None: + _, loads = build( + quadrants(whole(), row_cut=4, col_cut=6, overlap=1), + (500012.0, 6599987.0, 500033.0, 6599996.0), + ) + assert loads.calls == ["nw.tif"] + + def test_a_tile_changed_since_it_was_listed( + self, mz: ModuleType, plan: Any, refused: Any + ) -> None: + source = whole() + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + planned = plan(tiles) + moved = dict(tiles) + moved["nw.tif"] = piece(source, 0, 5, 0, 7, y_max=Y0 + DY) + refused(lambda: mz.assemble(planned, Loads(moved)), "nw.tif", "changed since it was listed") + + +class TestM15Adopt: + """M15 and R7: one canvas, handed to `DemTile` without a copy.""" + + def test_the_canvas_is_adopted_not_copied( + self, build: Any, monkeypatch: pytest.MonkeyPatch + ) -> None: + adopted: list[np.ndarray] = [] + original = DemTile._adopt + + def spy(meta: Any, array: np.ndarray) -> DemTile: + adopted.append(array) + return original(meta, array) + + monkeypatch.setattr(DemTile, "_adopt", staticmethod(spy)) + result, _ = build(quadrants(whole(), row_cut=4, col_cut=6, overlap=1)) + assert len(adopted) == 1 + assert np.shares_memory(adopted[0], result.tile.array) + assert not result.tile.array.flags.writeable + + def test_adopt_keeps_the_buffer_and_makes_it_read_only(self) -> None: + buffer = values(3, 4) + tile = DemTile._adopt(meta(rows=3, cols=4), buffer) + assert np.shares_memory(tile.array, buffer) + assert not tile.array.flags.writeable + assert not buffer.flags.writeable + assert tile.meta == meta(rows=3, cols=4) + + @pytest.mark.parametrize( + "array", + [ + values(4, 3), + values(3, 4, np.int32), + values(3, 4).reshape(1, 3, 4), + np.asfortranarray(values(3, 4)), + ], + ids=["shape", "dtype", "ndim", "fortran-order"], + ) + def test_adopt_runs_the_same_checks(self, array: np.ndarray) -> None: + with pytest.raises(ValueError): + DemTile._adopt(meta(rows=3, cols=4), array) + + def test_the_public_constructor_still_copies(self) -> None: + buffer = values(3, 4) + tile = DemTile(meta=meta(rows=3, cols=4), array=buffer) + assert not np.shares_memory(tile.array, buffer) + assert buffer.flags.writeable + + def test_adopt_is_called_only_from_the_mosaic(self) -> None: + """R7: "Its one caller is `assemble`". Grep the package for the name.""" + users = sorted( + str(path.relative_to(SRC)) + for path in SRC.rglob("*.py") + if "_adopt" in path.read_text(encoding="utf-8") + ) + assert users == ["io/models.py", "mosaic.py"] + + +def test_the_plan_never_loads(mz: ModuleType, footprint: Any) -> None: + """I6 as a type fact: `plan_mosaic` takes footprints, and no `load`.""" + import inspect + + assert list(inspect.signature(mz.plan_mosaic).parameters) == ["footprints", "bounds", "needed"] From f74dad15136a69cdee9df1a184307096b15f4502 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 22:45:08 +0200 Subject: [PATCH 07/28] 15: DTM10 fixture licence checked (CC BY 4.0, Geonorge) Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 8f6d592c..0343fa93 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -1005,7 +1005,8 @@ the stem of the first file as given. **Fixtures.** `tests/fixtures/dtm10/`, cut by `tests/fixtures/dtm10/extract.py` from Ola's archive (`DTM10_UTM33_20220924`), one release, © Kartverket, CC BY -4.0: +4.0 (checked by the main session, 2026-09-27, against Geonorge's metadata API +for dataset `dddbb667-1303-4ac5-8640-7ec04c0e3918`: "Åpne data", CC BY 4.0): - `seam/`: 6400_4 | 6400_1, rows 3072-3327, 307 columns each, with a 51-column overlap. It agrees bit for bit, and a one-column shift disagrees; the script From ff7cc8da5de7ee0f06707513b1348d302f96b19f Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 22:57:06 +0200 Subject: [PATCH 08/28] green: increment 15a (multi-tile DEM, one projected CRS) The production code for the red commit 2696bc2, per docs/increments/15-dem-mosaic.md (R1-R7, R11, Q1-Q5, and "Pinned by the red suite (15a)"). No test or fixture is touched; no C++. - tin_engine/mosaic.py: Bounds, IndexWindow, TilePlacement, MosaicPlan, Mosaic, MosaicError. plan_mosaic groups footprints into lattices (same EPSG, spacing, registration, NoData, integer offsets within 1e-6 cells), counts global indices from each lattice's north-west node, selects by index window, refuses a request on two lattices naming a tile of each and the offset (Q5), then the 2 x 2 minimum, the cap (4 bytes a node against half of physical_memory()), and per-node coverage. assemble merges tiles one at a time: valid beats NoData, sentinel beats NaN, two different valid values refuse naming both tiles, the count and the largest difference (Q1); one tile whose grid is the mosaic's is returned as loaded. - io/repository.py: TileFootprint, DemRepository, TiffDemRepository (the one io/ module that opens files, "rb" only; Q3). Headers read once, lazily. - io/geotiff.py: read_meta and decode_dem share _header; no duplication. - io/models.py: DemTile._adopt, the no-copy constructor for the canvas. - dem_input.py: DemRequest, DemInput, open_dem. - cli.py: --dem repeatable (one directory or files), --bbox with metavar BOX, refusals as usage errors, elevation_source prefix and stderr line for N >= 2, dem_tiles for a directory or several files. - Docstrings of io/__init__.py (Q3) and cli.py (no longer the only module with a path) corrected. Co-Authored-By: Claude Opus 5.5 --- src_python/tin_engine/cli.py | 109 +++++-- src_python/tin_engine/dem_input.py | 67 +++++ src_python/tin_engine/io/__init__.py | 11 +- src_python/tin_engine/io/geotiff.py | 73 +++-- src_python/tin_engine/io/models.py | 23 ++ src_python/tin_engine/io/repository.py | 101 +++++++ src_python/tin_engine/mosaic.py | 382 +++++++++++++++++++++++++ 7 files changed, 713 insertions(+), 53 deletions(-) create mode 100644 src_python/tin_engine/dem_input.py create mode 100644 src_python/tin_engine/io/repository.py create mode 100644 src_python/tin_engine/mosaic.py diff --git a/src_python/tin_engine/cli.py b/src_python/tin_engine/cli.py index 5e2afbb6..68e8341b 100644 --- a/src_python/tin_engine/cli.py +++ b/src_python/tin_engine/cli.py @@ -1,9 +1,11 @@ """Command-line interface for the rasputin terrain engine. -This module is the **single composition root**. It is the only Python module -that has a path, and joining the file system to the engine is its whole job -(``tin_engine.raster`` also imports ``_core``, to build the one core raster, per -``project_structure.md``): ``viz/`` is written against protocols and never +This module is the **single composition root**: joining the file system to the +engine is its whole job. Paths from the command line also reach +``tin_engine.domain``, ``tin_engine.dem_input`` and ``io/repository.py``, which +read the files they name (``tin_engine.raster`` also imports ``_core``, to +build the one core raster, per ``project_structure.md``): ``viz/`` is written +against protocols and never names a core type, while the core never sees a file, a path or a CRS. Everything that has to know both sides lives here. (``project_structure.md``'s rule that exactly one module constructs a core *raster* is about ``tin_engine.raster``.) @@ -52,6 +54,7 @@ import numpy as np import numpy.typing as npt import typer +from pydantic import ValidationError from tin_engine._core import ( ChainRole, @@ -65,14 +68,15 @@ sample, triangulate, ) +from tin_engine.dem_input import DemInput, DemRequest, open_dem from tin_engine.domain import DomainError, DomainPolygon, read_domain from tin_engine.elevation import Trimmed, trim from tin_engine.features import DEFAULT_VOCABULARY from tin_engine.grid_domain import default_stride, refine_start_stride, subsample -from tin_engine.io.geotiff import decode_dem -from tin_engine.io.models import GeoTiffError, RasterMeta +from tin_engine.io.models import DemTile, RasterMeta from tin_engine.io.ply import write_ply from tin_engine.io.vtk_legacy import write_vtk +from tin_engine.mosaic import Bounds from tin_engine.raster import to_core from tin_engine.stats import PhaseClock, Refinement, Report, Sizes, _exact, quality, render from tin_engine.viz.fixtures import GALLERY, Fixture @@ -526,8 +530,21 @@ def mesh( str | None, typer.Argument(help="Gallery fixture to write; or give --dem instead.") ] = None, dem: Annotated[ - Path | None, - typer.Option("--dem", help="A GeoTIFF DEM: mesh its extent with z sampled from it."), + list[Path] | None, + typer.Option( + "--dem", + help="A GeoTIFF DEM, several (repeat --dem), or one directory of tiles: mesh " + "its extent with z sampled from it.", + ), + ] = None, + bbox: Annotated[ + tuple[float, float, float, float] | None, + typer.Option( + "--bbox", + metavar="BOX", + help="With --dem, mesh only XMIN YMIN XMAX YMAX in the DEM's CRS, " + "snapped outward to its nodes.", + ), ] = None, stride: Annotated[ int | None, @@ -619,6 +636,8 @@ def mesh( the inside is meshed. Vertices where the DEM has no data are dropped with their triangles, and the count is reported. The file records the DEM's CRS, so ``--crs`` and ``--flat`` are refused. + ``--dem DIR`` or several ``--dem`` files are stitched into one grid first, + cut to ``--bbox`` if given (increment 15a, ``tin_engine.mosaic``). ``.vtk`` is one file for ParaView: triangles, constraint lines, their feature masks, one 0/1 array per feature that occurs, and the vocabulary @@ -643,13 +662,15 @@ def mesh( """ clock = PhaseClock() dem_run: _DemMesh | None = None - if (name is None) == (dem is None): + if (name is None) == (not dem): raise typer.BadParameter( "give a gallery fixture name or --dem PATH, exactly one of the two", param_hint="--dem", ) - if dem is None and (domain is not None or domain_crs is not None): + if not dem and (domain is not None or domain_crs is not None): raise typer.BadParameter("applies only with --dem", param_hint="--domain") + if not dem and bbox is not None: + raise typer.BadParameter("applies only with --dem", param_hint="--bbox") if domain is None and domain_crs is not None: raise typer.BadParameter("applies only with --domain", param_hint="--domain-crs") if name is not None and name not in GALLERY: @@ -663,7 +684,7 @@ def mesh( "a .vtk file already carries the constraint edges", param_hint="--out-edges" ) - if dem is not None: + if dem: if flat: raise typer.BadParameter("--dem samples z from the DEM", param_hint="--flat") if crs: @@ -698,9 +719,11 @@ def mesh( raise typer.BadParameter( "--no-constraint-feet needs --tolerance", param_hint="--no-constraint-feet" ) - label = dem.stem + opened = _open_dem(dem, bbox, clock) + label = opened.label dem_run = _dem_mesh( - dem, + opened.tile, + ", ".join(map(str, dem)), stride, delaunay, snap_spacing, @@ -713,8 +736,17 @@ def mesh( ) surface_mesh = dem_run.trimmed epsg, sentence, described = dem_run.epsg, dem_run.sentence, dem_run.described + names = [t.name for t in opened.plan.tiles] + if len(names) > 1: + mosaic = f"mosaic of {len(names)} tiles, {opened.tile.meta.rows} x " + mosaic += f"{opened.tile.meta.cols} nodes" + typer.echo(mosaic, err=True) + sentence = f"{mosaic}; {sentence}" fields = [("crs", f"EPSG:{epsg}"), ("elevation_source", sentence)] comments = [f"crs EPSG:{epsg}", f"elevation {sentence}"] + if len(dem) > 1 or dem[0].is_dir(): # R11: the files used, named + listed = "; ".join(names).encode("ascii", "backslashreplace").decode("ascii") + fields.append(("dem_tiles", listed)) if described: fields.append(("domain", described)) comments.append(f"domain {described}") @@ -883,6 +915,38 @@ def _fixture_mesh(name: str, delaunay: bool, spacing: float, clock: PhaseClock) ) +def _open_dem( + dem: list[Path], bbox: tuple[float, float, float, float] | None, clock: PhaseClock +) -> DemInput: + """``--dem`` and ``--bbox`` to one tile (increment 15a, R11); every refusal, + the reader's or the mosaic's, is a usage error in its own words.""" + try: + bounds = ( + None + if bbox is None + else Bounds(**dict(zip(("x_min", "y_min", "x_max", "y_max"), bbox, strict=True))) + ) + except ValidationError as exc: + raise typer.BadParameter(_words(exc), param_hint="--bbox") from exc + try: + with clock.phase("decode"): + return open_dem(DemRequest(sources=tuple(dem), bounds=bounds)) + except OSError as exc: + where = exc.filename or ", ".join(map(str, dem)) + raise typer.BadParameter( + f"cannot read {where}: {exc.strerror or exc}", param_hint="--dem" + ) from exc + except ValueError as exc: + raise typer.BadParameter(_words(exc), param_hint="--dem") from exc + + +def _words(exc: ValueError) -> str: + """A refusal's own words: Pydantic's messages without its wrapper.""" + if isinstance(exc, ValidationError): + return "; ".join(str(e["msg"]) for e in exc.errors()) + return str(exc) + + @dataclass(frozen=True, slots=True) class _DemMesh: """What ``_dem_mesh`` made: the mesh and its file fields, then what @@ -901,7 +965,8 @@ class _DemMesh: def _dem_mesh( - dem: Path, + tile: DemTile, + dem: str, stride: int | None, delaunay: bool, spacing: float, @@ -912,7 +977,7 @@ def _dem_mesh( min_angle: float = 0.0, feet: bool = False, ) -> _DemMesh: - """Decode, subsample, triangulate, sample or refine, and trim. + """Subsample, triangulate, sample or refine, and trim ``tile``. Without ``tolerance`` this is increment 12's R6: z sampled bilinearly at the stride grid. With it, increment 14's R9: the stride grid is the start @@ -922,19 +987,9 @@ def _dem_mesh( Returns the mesh, the ``elevation`` sentence for the file, the EPSG code, the ``domain`` field (empty without one), and the ``--stats`` inputs; ``clock`` gets R5's phases. - Every refusal is a usage error in the reader's or the engine's own words, - and no file is written. + ``dem`` names the source in messages. Every refusal is a usage error in the + engine's own words, and no file is written. """ - try: - with clock.phase("decode"), dem.open("rb") as stream: - tile = decode_dem(stream) - except OSError as exc: - raise typer.BadParameter( - f"cannot read {dem}: {exc.strerror or exc}", param_hint="--dem" - ) from exc - except GeoTiffError as exc: - raise typer.BadParameter(str(exc), param_hint="--dem") from exc - meta = tile.meta described = "" domain_vertices = domain_holes = None diff --git a/src_python/tin_engine/dem_input.py b/src_python/tin_engine/dem_input.py new file mode 100644 index 00000000..5ed83ee8 --- /dev/null +++ b/src_python/tin_engine/dem_input.py @@ -0,0 +1,67 @@ +"""The DEM a run meshes, from a declarative request (increment 15a, R1). + +`docs/increments/15-dem-mosaic.md` R1 and R11. A `DemRequest` names the +sources, the box and the caller's NoData; `open_dem` lists the tiles, plans the +mosaic and assembles it. `cli.py` parses flags into a request and calls +`open_dem`; a GUI backend or an API worker builds the same request without +Typer. Besides `io/repository.py`, this is the one module below `cli.py` +with paths, and it only hands them to the repository. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Self + +from pydantic import BaseModel, ConfigDict, model_validator + +from tin_engine.io.models import DemTile +from tin_engine.io.repository import TiffDemRepository +from tin_engine.mosaic import Bounds, MosaicPlan, assemble, plan_mosaic + + +class DemRequest(BaseModel): + """Exactly one directory of tiles, or one or more tile files (R11).""" + + model_config = ConfigDict(frozen=True) + + sources: tuple[Path, ...] + bounds: Bounds | None = None + nodata: float | None = None + + @model_validator(mode="after") + def _one_form(self) -> Self: + if not self.sources: + raise ValueError("no DEM source given") + directories = sum(p.is_dir() for p in self.sources) + if directories and len(self.sources) > 1: + raise ValueError( + f"give exactly one directory, or one or more files; got {len(self.sources)} " + f"sources of which {directories} are directories" + ) + return self + + +@dataclass(frozen=True, slots=True) +class DemInput: + """The tile to mesh, the plan it was assembled by, and a name for the run: + the directory's name, or the stem of the first file as given.""" + + tile: DemTile + plan: MosaicPlan + label: str + + +def open_dem(request: DemRequest) -> DemInput: + """List, plan and assemble. Every refusal is a `ValueError` (`GeoTiffError`, + `MosaicError`) or, for a file that cannot be opened, an `OSError`.""" + first = request.sources[0] + if first.is_dir(): + repository = TiffDemRepository.from_directory(first, nodata=request.nodata) + label = first.resolve().name + else: + repository = TiffDemRepository(request.sources, nodata=request.nodata) + label = first.stem + plan = plan_mosaic(repository.footprints(), request.bounds, None) + return DemInput(tile=assemble(plan, repository.load).tile, plan=plan, label=label) diff --git a/src_python/tin_engine/io/__init__.py b/src_python/tin_engine/io/__init__.py index 13213b6a..e5922d55 100644 --- a/src_python/tin_engine/io/__init__.py +++ b/src_python/tin_engine/io/__init__.py @@ -3,11 +3,14 @@ `project_structure.md` marked this package as the home of "all file decoding"; increment 10 makes it the first *encoding* module too, and widens that line. The rule the package keeps either way is the one that matters: `io/` knows -formats and knows nothing about `_core`, so everything in it is testable with -no compiled extension in the process and no filesystem. +formats and knows nothing about `_core`, so no module in it needs a compiled +extension in the process. -Nothing here opens a file. A path belongs to `cli.py`, which is the only module -that has one (`06-cdt-viewer.md`, "no file is written below `cli.py`"). +Files are opened in exactly one module, `repository.py`, and only for reading +(increment 15a, Ola's Q3 ruling): a DEM in many tiles has to be listed and +read tile by tile, below `cli.py`. Every other module here takes or returns +streams and bytes, so it is testable with no filesystem, and nothing in `io/` +writes a file (`06-cdt-viewer.md`, "no file is written below `cli.py`"). """ from __future__ import annotations diff --git a/src_python/tin_engine/io/geotiff.py b/src_python/tin_engine/io/geotiff.py index 23838e66..06b27059 100644 --- a/src_python/tin_engine/io/geotiff.py +++ b/src_python/tin_engine/io/geotiff.py @@ -45,6 +45,18 @@ NodataSource = Literal["tag", "caller", "absent"] +def read_meta(source: BinaryIO, *, nodata: float | None = None) -> RasterMeta: + """The header phase of `decode_dem` alone (increment 15a, R3): no pixel is read. + + Every refusal `decode_dem` makes from the header fires here with the same + message, because both run the same `_header`. A file whose pixels are + damaged still gives its `RasterMeta`; `decode_dem` then refuses it at the + "pixel data" stage. `source` is read but not closed. + """ + with _tiff(source, nodata) as tif: + return _header(tif, nodata)[0] + + def decode_dem(source: BinaryIO, *, nodata: float | None = None) -> DemTile: """Decode page 0 of the GeoTIFF in `source` into a `DemTile`. @@ -63,43 +75,60 @@ def decode_dem(source: BinaryIO, *, nodata: float | None = None) -> DemTile: while `DemTile` takes its read-only copy (§7). `source` is read but not closed; closing it is the caller's. """ + with _tiff(source, nodata) as tif: + meta, dtype = _header(tif, nodata) + with _stage("pixel data"): + array = tif.pages.first.asarray().astype(PROMOTION[dtype], copy=False) + return DemTile(meta=meta, array=array) + + +@contextmanager +def _tiff(source: BinaryIO, nodata: float | None) -> Iterator[tifffile.TiffFile]: + """The caller-argument check and the TIFF structure, shared by both phases.""" if isinstance(nodata, bool | np.bool_): raise TypeError(f"nodata= must be a number or None, not {type(nodata).__name__}") with _stage("TIFF structure"): tif = tifffile.TiffFile(source) with tif: - with _stage("TIFF structure"): - page, pages = tif.pages.first, tuple(tif.pages) # parses every IFD - _single_page(pages) - dtype = _check_page(page) - tie, scale = _georeferencing(page) - with _stage("GeoKey directory"): - geokeys: dict[str, Any] = tif.geotiff_metadata or {} - x_min, y_max, delta_x, delta_y, area = _placement(tie, scale, geokeys) - epsg = _projected_epsg(geokeys) - vertical = geokeys.get("VerticalUnitsGeoKey") - if vertical is not None and int(vertical) != METRE: - raise GeoTiffError( - f"VerticalUnitsGeoKey (4099) = {int(vertical)}; only metres ({METRE}) are read" - ) - sentinel, source_of = _nodata(page, dtype, nodata) - with _stage("pixel data"): - array = page.asarray().astype(PROMOTION[dtype], copy=False) - rows, cols = array.shape + yield tif + + +def _header(tif: tifffile.TiffFile, nodata: float | None) -> tuple[RasterMeta, np.dtype[Any]]: + """Everything before the pixels (R3): the §5 header refusals, then the meta. + + Returns the file dtype too, which `decode_dem` promotes by. `rows` and + `cols` come from ImageLength and ImageWidth, which `_check_page` has + bounded; `DemTile` checks them against the decoded array's shape. + """ + with _stage("TIFF structure"): + page, pages = tif.pages.first, tuple(tif.pages) # parses every IFD + _single_page(pages) + dtype = _check_page(page) + tie, scale = _georeferencing(page) + with _stage("GeoKey directory"): + geokeys: dict[str, Any] = tif.geotiff_metadata or {} + x_min, y_max, delta_x, delta_y, area = _placement(tie, scale, geokeys) + epsg = _projected_epsg(geokeys) + vertical = geokeys.get("VerticalUnitsGeoKey") + if vertical is not None and int(vertical) != METRE: + raise GeoTiffError( + f"VerticalUnitsGeoKey (4099) = {int(vertical)}; only metres ({METRE}) are read" + ) + sentinel, source_of = _nodata(page, dtype, nodata) meta = RasterMeta( x_min=x_min, y_max=y_max, delta_x=delta_x, delta_y=delta_y, - cols=cols, - rows=rows, + cols=int(page.imagewidth), + rows=int(page.imagelength), epsg=epsg, nodata=sentinel, nodata_source=source_of, pixel_is_area=area, vertical_unit_assumed=vertical is None, ) - return DemTile(meta=meta, array=array) + return meta, dtype @contextmanager @@ -320,4 +349,4 @@ def _same(a: float, b: float) -> bool: return a == b or (math.isnan(a) and math.isnan(b)) -__all__ = ["decode_dem"] +__all__ = ["decode_dem", "read_meta"] diff --git a/src_python/tin_engine/io/models.py b/src_python/tin_engine/io/models.py index 387dc8bd..0c6793e1 100644 --- a/src_python/tin_engine/io/models.py +++ b/src_python/tin_engine/io/models.py @@ -83,6 +83,29 @@ def _read_only_c_contiguous(cls, value: npt.NDArray[Any]) -> npt.NDArray[Any]: copy.flags.writeable = False return copy.view() + @classmethod + def _adopt(cls, meta: RasterMeta, array: npt.NDArray[Any]) -> DemTile: + """Take `array` as the tile's own, without the copy (increment 15a, R7). + + Private: its one caller is `mosaic.assemble`, on a canvas it allocated + and never hands out writable, so a mosaic peaks at one canvas, not two. + The same checks as the public constructor, plus C-contiguity, which + the constructor gets from its copy. The **passed** buffer is set + read-only, so the concurrency rule (§7) holds: nobody keeps a writable + reference. + """ + if array.ndim != 2 or array.dtype not in (np.float32, np.float64): + raise ValueError( + f"need a 2-D float32 or float64 array, got {array.dtype} {array.shape}" + ) + if not array.flags.c_contiguous or array.shape != (meta.rows, meta.cols): + raise ValueError( + f"need a C-contiguous array of shape {(meta.rows, meta.cols)}, " + f"got {array.shape}, C-contiguous {array.flags.c_contiguous}" + ) + array.flags.writeable = False + return cls.model_construct(meta=meta, array=array) + @model_validator(mode="after") def _shape_agrees(self) -> Self: if (self.meta.rows, self.meta.cols) != self.array.shape: diff --git a/src_python/tin_engine/io/repository.py b/src_python/tin_engine/io/repository.py new file mode 100644 index 00000000..a5c63bca --- /dev/null +++ b/src_python/tin_engine/io/repository.py @@ -0,0 +1,101 @@ +"""A DEM held as tiles on disk: headers first, pixels on request (increment 15a). + +`docs/increments/15-dem-mosaic.md` R1-R3 and Ola's Q3 ruling: this is the one +module in `io/` that opens files, and it opens them read-only (`"rb"`). It +only stores: which tiles there are (`footprints`, header-only) and one tile's +pixels (`load`). Choosing tiles and stitching them is `tin_engine.mosaic`'s, +the same grid arithmetic for any storage. +""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable +from pathlib import Path +from typing import Protocol + +from pydantic import BaseModel, ConfigDict + +from .geotiff import decode_dem, read_meta +from .models import DemTile, GeoTiffError, RasterMeta + +#: `from_directory` lists these suffixes, in any case (R2). +TILE_SUFFIXES = frozenset({".tif", ".tiff"}) + + +class TileFootprint(BaseModel): + """One tile as its header describes it: the file's name and its node grid.""" + + model_config = ConfigDict(frozen=True) + + name: str + meta: RasterMeta + + +class DemRepository(Protocol): + """Storage only (R1). Sync: `load` is blocking I/O, and an async caller + wraps it in `asyncio.to_thread`.""" + + def footprints(self) -> tuple[TileFootprint, ...]: + """Every tile's header, sorted by name.""" + ... + + def load(self, name: str) -> DemTile: + """The whole tile `name`, decoded. `KeyError` for a name not listed.""" + ... + + +class TiffDemRepository: + """GeoTIFF tiles given as paths. Construction reads no file. + + Paths are resolved and deduplicated, so two spellings of one file are one + tile. A footprint's name is the file name, so two files sharing one are + refused. `nodata` is the caller's sentinel for every tile (`decode_dem`). + """ + + def __init__(self, paths: Iterable[Path], *, nodata: float | None = None) -> None: + self._paths: dict[str, Path] = {} + for path in sorted({Path(p).resolve() for p in paths}, key=str): + if path.name in self._paths: + raise ValueError( + f"two tiles named {path.name}: {self._paths[path.name]} and {path}" + ) + self._paths[path.name] = path + self._nodata = nodata + self._footprints: tuple[TileFootprint, ...] | None = None + + @classmethod + def from_directory(cls, directory: Path, *, nodata: float | None = None) -> TiffDemRepository: + """The `*.tif` and `*.tiff` files directly in `directory`, not recursively. + + Side files (`.tfw`, `.aux.xml`) carry nothing the GeoTIFF tags do not. + """ + paths = [ + p for p in directory.iterdir() if p.suffix.lower() in TILE_SUFFIXES and p.is_file() + ] + if not paths: + raise ValueError(f"no .tif or .tiff file in the directory {directory}") + return cls(paths, nodata=nodata) + + def footprints(self) -> tuple[TileFootprint, ...]: + """Read every header once, on the first call, and keep them (R2).""" + if self._footprints is None: + self._footprints = tuple( + TileFootprint(name=name, meta=self._read(name, read_meta)) + for name in sorted(self._paths) + ) + return self._footprints + + def load(self, name: str) -> DemTile: + return self._read(name, decode_dem) + + def _read[T](self, name: str, reader: Callable[..., T]) -> T: + """Open tile `name` read-only and run `reader`; a refusal names the file.""" + path = self._paths[name] + with path.open("rb") as stream: + try: + return reader(stream, nodata=self._nodata) + except GeoTiffError as exc: + raise GeoTiffError(f"{path.name}: {exc}") from exc + + +__all__ = ["DemRepository", "TiffDemRepository", "TileFootprint"] diff --git a/src_python/tin_engine/mosaic.py b/src_python/tin_engine/mosaic.py new file mode 100644 index 00000000..7b231c0d --- /dev/null +++ b/src_python/tin_engine/mosaic.py @@ -0,0 +1,382 @@ +"""Many DEM tiles as one node grid: planning from headers, then assembly (increment 15a). + +`docs/increments/15-dem-mosaic.md` R1, R4, R5 and R7, with the two +corrections in its "Pinned by the red suite (15a)": tiles are selected by +index window, and coverage is checked per node. + +`plan_mosaic` is pure: footprints and a request in, a `MosaicPlan` out, and +every refusal a header can decide fires there, before any pixel (I6). +`assemble` reads pixels through a `load` callable and never sees a path. + +Positions are integers from here on. Each lattice has a reference node, its +north-west-most node over all its tiles, and a node's global index `(R, K)` +counts from it; the mosaic's first node is `X_ref + c0 * dx`, never a sum of +steps (I1, R12's global node identity). +""" + +from __future__ import annotations + +import math +import os +from collections.abc import Callable, Sequence +from dataclasses import dataclass +from typing import TYPE_CHECKING, Any, Self + +import numpy as np +import numpy.typing as npt +import shapely +from pydantic import BaseModel, ConfigDict, model_validator + +from tin_engine.io.models import DemTile, RasterMeta + +if TYPE_CHECKING: + from tin_engine.io.repository import TileFootprint + +#: Cells. Two tiles share a lattice when their node offsets are integers to +#: within this. Measured noise is at most 2.2e-11 cells (B3), and 0 for DTM10. +ALIGN_TOLERANCE = 1e-6 + +#: The cap counts float32: headers do not give the decoded dtype, so a float64 +#: mosaic is under-counted by 2x (pinned by the red suite). +BYTES_PER_NODE = 4 + + +class MosaicError(ValueError): + """A request the tiles cannot answer. A `ValueError`, like `GeoTiffError`.""" + + +def physical_memory() -> int: + """Bytes of physical memory, looked up at call time (macOS and Linux).""" + return os.sysconf("SC_PHYS_PAGES") * os.sysconf("SC_PAGE_SIZE") + + +class Bounds(BaseModel): + """A box in the DEM's CRS: finite, with `x_min < x_max` and `y_min < y_max`.""" + + model_config = ConfigDict(frozen=True) + + x_min: float + y_min: float + x_max: float + y_max: float + + @model_validator(mode="after") + def _a_box(self) -> Self: + corners = (self.x_min, self.y_min, self.x_max, self.y_max) + if not all(map(math.isfinite, corners)) or not ( + self.x_min < self.x_max and self.y_min < self.y_max + ): + raise ValueError(f"need finite x_min < x_max and y_min < y_max, got {corners}") + return self + + +class IndexWindow(BaseModel): + """`rows x cols` nodes starting at `(row0, col0)`.""" + + model_config = ConfigDict(frozen=True) + + row0: int + col0: int + rows: int + cols: int + + +class TilePlacement(BaseModel): + """One selected tile: `canvas` in canvas indices, `source` in the tile's own.""" + + model_config = ConfigDict(frozen=True) + + name: str + meta: RasterMeta + canvas: IndexWindow + source: IndexWindow + + +class MosaicPlan(BaseModel): + """What `assemble` will build. `window` is the mosaic's first node as a + global index, and its shape; `tiles` is sorted by name.""" + + model_config = ConfigDict(frozen=True) + + meta: RasterMeta + reference: tuple[float, float] + window: IndexWindow + tiles: tuple[TilePlacement, ...] + + +@dataclass(frozen=True, slots=True) +class Mosaic: + """The assembled tile, and the plan it came from, as provenance.""" + + tile: DemTile + plan: MosaicPlan + + +@dataclass(frozen=True, slots=True) +class _Placed: + """A footprint and its global index on its lattice.""" + + footprint: TileFootprint + row: int + col: int + + def meets(self, r0: int, c0: int, r1: int, c1: int) -> bool: + """Its node index range meets `[r0, r1] x [c0, c1]`, closed on both sides.""" + m = self.footprint.meta + return ( + self.row <= r1 and self.row + m.rows > r0 and self.col <= c1 and self.col + m.cols > c0 + ) + + +def plan_mosaic( + footprints: Sequence[TileFootprint], bounds: Bounds | None = None, needed: Any = None +) -> MosaicPlan: + """Select the tiles the request needs, on one lattice, or refuse (R4). + + `bounds` is the box in the DEM's CRS; without it, the lattice's union. + `needed` is a shapely geometry in the DEM's CRS, closed: a node on its + boundary is needed. Without it, every node of the window is. + """ + if not footprints: + raise MosaicError("no tiles to plan a mosaic from") + lattices: list[list[TileFootprint]] = [] + for footprint in sorted(footprints, key=lambda f: f.name): + group = next((g for g in lattices if _aligned(g[0].meta, footprint.meta)), None) + if group is None: + lattices.append([footprint]) + else: + group.append(footprint) + + chosen: list[tuple[tuple[float, float], tuple[int, int, int, int], list[_Placed]]] = [] + for group in lattices: + reference, placed = _placed(group) + window = _window(bounds, reference, group[0].meta, placed) + selected = [p for p in placed if window is not None and p.meets(*window)] + if window is not None and selected: + chosen.append((reference, window, selected)) + if not chosen: + raise MosaicError(f"the box {bounds} meets no tile") + if len(chosen) > 1: + raise MosaicError(_mixed(chosen[0][2][0].footprint, chosen[1][2][0].footprint)) + + (x_ref, y_ref), (r0, c0, r1, c1), selected = chosen[0] + rows, cols = r1 - r0 + 1, c1 - c0 + 1 + first = selected[0].footprint.meta + if rows < 2 or cols < 2: + raise MosaicError(f"the request is {rows} x {cols} nodes; a mesh needs at least 2 x 2") + cap = physical_memory() // 2 + if rows * cols * BYTES_PER_NODE > cap: + raise MosaicError( + f"the request is {rows} x {cols} nodes, {rows * cols * BYTES_PER_NODE} bytes at " + f"float32, over the cap of half the physical memory ({cap} bytes); " + "narrow it with --bbox" + ) + meta = RasterMeta( + x_min=x_ref + c0 * first.delta_x, + y_max=y_ref - r0 * first.delta_y, + delta_x=first.delta_x, + delta_y=first.delta_y, + cols=cols, + rows=rows, + epsg=first.epsg, + nodata=first.nodata, + nodata_source=first.nodata_source, + pixel_is_area=first.pixel_is_area, + vertical_unit_assumed=any(p.footprint.meta.vertical_unit_assumed for p in selected), + ) + tiles = tuple(_placement(p, r0, c0, r1, c1) for p in selected) + _check_coverage(meta, tiles, needed) + return MosaicPlan( + meta=meta, + reference=(x_ref, y_ref), + window=IndexWindow(row0=r0, col0=c0, rows=rows, cols=cols), + tiles=tiles, + ) + + +def assemble(plan: MosaicPlan, load: Callable[[str], DemTile]) -> Mosaic: + """Load the plan's tiles one at a time, in plan order, into one canvas (R5). + + One tile whose grid is the mosaic's is returned as loaded: no canvas, no + copy (I7). Otherwise the canvas is allocated once, NaN-filled, in the + first tile's dtype, and handed to `DemTile` without a copy (R7). Dtypes + are known only after a load, so a later float64 tile re-casts it once to + the tiles' result dtype; both archives are float32 throughout. + """ + if len(plan.tiles) == 1 and plan.tiles[0].meta == plan.meta: + return Mosaic(tile=_loaded(plan.tiles[0], load), plan=plan) + canvas: npt.NDArray[Any] | None = None + merged: list[TilePlacement] = [] + for placement in plan.tiles: + array = _loaded(placement, load).array + if canvas is None: + canvas = np.full((plan.meta.rows, plan.meta.cols), np.nan, dtype=array.dtype) + elif np.result_type(canvas.dtype, array.dtype) != canvas.dtype: + canvas = canvas.astype(np.result_type(canvas.dtype, array.dtype)) + _merge(canvas, placement, array, plan.meta.nodata, merged) + merged.append(placement) + if canvas is None: + raise MosaicError("the plan has no tiles") + return Mosaic(tile=DemTile._adopt(plan.meta, canvas), plan=plan) + + +def _aligned(a: RasterMeta, b: RasterMeta) -> bool: + """Same CRS, spacing, registration and NoData, and integer node offsets (R4.1).""" + return _key(a) == _key(b) and all(abs(v - round(v)) <= ALIGN_TOLERANCE for v in _cells(a, b)) + + +def _key(m: RasterMeta) -> tuple[int, float, float, bool, float | None]: + return m.epsg, m.delta_x, m.delta_y, m.pixel_is_area, m.nodata + + +def _cells(a: RasterMeta, b: RasterMeta) -> tuple[float, float]: + """`b`'s first node from `a`'s, in `a`'s cells east and south.""" + return (b.x_min - a.x_min) / a.delta_x, (a.y_max - b.y_max) / a.delta_y + + +def _placed(group: list[TileFootprint]) -> tuple[tuple[float, float], list[_Placed]]: + """The lattice's reference node (R4.2), and each tile's global index from it.""" + x_ref = min(f.meta.x_min for f in group) + y_ref = max(f.meta.y_max for f in group) + dx, dy = group[0].meta.delta_x, group[0].meta.delta_y + return (x_ref, y_ref), [ + _Placed(f, round((y_ref - f.meta.y_max) / dy), round((f.meta.x_min - x_ref) / dx)) + for f in group + ] + + +def _window( + bounds: Bounds | None, reference: tuple[float, float], m: RasterMeta, placed: list[_Placed] +) -> tuple[int, int, int, int] | None: + """`(r0, c0, r1, c1)`, inclusive: the box snapped outward, clamped to the + lattice's union (R4.4); None when the box misses the union.""" + last_row = max(p.row + p.footprint.meta.rows for p in placed) - 1 + last_col = max(p.col + p.footprint.meta.cols for p in placed) - 1 + if bounds is None: + return 0, 0, last_row, last_col + (x_ref, y_ref), dx, dy = reference, m.delta_x, m.delta_y + c0 = max(math.floor(_snap((bounds.x_min - x_ref) / dx)), 0) + c1 = min(math.ceil(_snap((bounds.x_max - x_ref) / dx)), last_col) + r0 = max(math.floor(_snap((y_ref - bounds.y_max) / dy)), 0) + r1 = min(math.ceil(_snap((y_ref - bounds.y_min) / dy)), last_row) + return (r0, c0, r1, c1) if r0 <= r1 and c0 <= c1 else None + + +def _snap(cells: float) -> float: + """A box edge within `ALIGN_TOLERANCE` of a node is on it, so float noise + in a spacing like 0.1 does not grow the window by a line.""" + nearest = round(cells) + return float(nearest) if abs(cells - nearest) <= ALIGN_TOLERANCE else cells + + +def _placement(p: _Placed, r0: int, c0: int, r1: int, c1: int) -> TilePlacement: + """Where tile `p` sits in the canvas, and which part of it is used.""" + m = p.footprint.meta + rs, re = max(r0, p.row), min(r1, p.row + m.rows - 1) + cs, ce = max(c0, p.col), min(c1, p.col + m.cols - 1) + rows, cols = re - rs + 1, ce - cs + 1 + return TilePlacement( + name=p.footprint.name, + meta=m, + canvas=IndexWindow(row0=rs - r0, col0=cs - c0, rows=rows, cols=cols), + source=IndexWindow(row0=rs - p.row, col0=cs - p.col, rows=rows, cols=cols), + ) + + +def _check_coverage(meta: RasterMeta, tiles: tuple[TilePlacement, ...], needed: Any) -> None: + """Refuse a needed node no selected tile covers (R4.5, per node).""" + uncovered = np.ones((meta.rows, meta.cols), dtype=bool) + for t in tiles: + c = t.canvas + uncovered[c.row0 : c.row0 + c.rows, c.col0 : c.col0 + c.cols] = False + rows, cols = np.nonzero(uncovered) + xs = meta.x_min + cols * meta.delta_x + ys = meta.y_max - rows * meta.delta_y + if needed is not None: + inside = shapely.intersects_xy(needed, xs, ys) + xs, ys = xs[inside], ys[inside] + if len(xs): + raise MosaicError( + f"{len(xs)} nodes the request needs are in no tile: x {_num(xs.min())} to " + f"{_num(xs.max())}, y {_num(ys.min())} to {_num(ys.max())} (EPSG:{meta.epsg})" + ) + + +def _loaded(placement: TilePlacement, load: Callable[[str], DemTile]) -> DemTile: + tile = load(placement.name) + if tile.meta != placement.meta: + raise MosaicError( + f"{placement.name} changed since it was listed: {tile.meta} is not {placement.meta}" + ) + return tile + + +def _merge( + canvas: npt.NDArray[Any], + placement: TilePlacement, + array: npt.NDArray[Any], + nodata: float | None, + merged: list[TilePlacement], +) -> None: + """R5's overlap rule, per node: valid beats NoData; two valid values must + be equal (`==`); NoData against NoData keeps the sentinel over NaN, in + every order.""" + c, s = placement.canvas, placement.source + region = canvas[c.row0 : c.row0 + c.rows, c.col0 : c.col0 + c.cols] + incoming = array[s.row0 : s.row0 + s.rows, s.col0 : s.col0 + s.cols] + old_nan, new_nan = np.isnan(region), np.isnan(incoming) + old_valid, new_valid = ~old_nan, ~new_nan + if nodata is not None: + old_valid &= region != nodata + new_valid &= incoming != nodata + clash = old_valid & new_valid & (region != incoming) + if clash.any(): + row, col = (int(v) for v in np.argwhere(clash)[0]) + other = next( + m.name + for m in merged + if m.canvas.row0 <= c.row0 + row < m.canvas.row0 + m.canvas.rows + and m.canvas.col0 <= c.col0 + col < m.canvas.col0 + m.canvas.cols + ) + largest = float(np.max(np.abs(region[clash].astype(np.float64) - incoming[clash]))) + raise MosaicError( + f"{other} and {placement.name} disagree at {int(clash.sum())} overlapping " + f"nodes, by up to {largest!r}" + ) + write = (old_nan & ~new_nan) | (~old_nan & ~old_valid & new_valid) + region[write] = incoming[write] + + +def _mixed(a: TileFootprint, b: TileFootprint) -> str: + """Why `b` is not on `a`'s lattice (R4.3, Q5), in the terms that differ.""" + ma, mb = a.meta, b.meta + if ma.epsg != mb.epsg: + why = f"is in EPSG:{mb.epsg}, not EPSG:{ma.epsg}" + elif (ma.delta_x, ma.delta_y) != (mb.delta_x, mb.delta_y): + why = f"has spacing {mb.delta_x!r} x {mb.delta_y!r}, not {ma.delta_x!r} x {ma.delta_y!r}" + elif ma.pixel_is_area != mb.pixel_is_area: + why = f"has registration {_registration(mb)}, not {_registration(ma)}" + elif ma.nodata != mb.nodata: + why = f"has nodata {mb.nodata}, not {ma.nodata}" + else: + offsets = [ + f"{abs(v - round(v)):.6g} cell ({abs(v - round(v)) * h:.6g} m) {axis}" + for v, h, axis in zip( + _cells(ma, mb), (ma.delta_x, ma.delta_y), ("east-west", "north-south"), strict=True + ) + if abs(v - round(v)) > ALIGN_TOLERANCE + ] + why = f"is {' and '.join(offsets)} off {a.name}'s lattice" + return ( + f"the request selects tiles on two lattices, {a.name} and {b.name}: {b.name} {why}; " + "tiles are not resampled onto one another, but a --bbox inside one lattice is meshed" + ) + + +def _registration(m: RasterMeta) -> str: + return "pixel-is-area" if m.pixel_is_area else "pixel-is-point" + + +def _num(value: float | np.floating[Any]) -> str: + """A coordinate written out, never in scientific notation.""" + return f"{value:f}".rstrip("0").rstrip(".") From 7a19dd48e72b790531ef53c6582f2a439cb5e5b4 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 22:57:32 +0200 Subject: [PATCH 09/28] 15a: re-point three citations the green commit shifted check_citations.py --base origin/master listed four citations into files ff7cc8d edits. Re-read as quotations: 12-dem-to-mesh.md's geotiff.py:169 (the codecs-extra advice) is now :198 and :307 (_representable) is now :336; 15-dem-mosaic.md R3's geotiff.py:70-87 (decode_dem before page.asarray()) is now _header itself, :96-131. io/models.py:82 still quotes the copy. Co-Authored-By: Claude Opus 5.5 --- docs/increments/12-dem-to-mesh.md | 4 ++-- docs/increments/15-dem-mosaic.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/increments/12-dem-to-mesh.md b/docs/increments/12-dem-to-mesh.md index 2d221c70..8b4820f0 100644 --- a/docs/increments/12-dem-to-mesh.md +++ b/docs/increments/12-dem-to-mesh.md @@ -68,7 +68,7 @@ fixture is decoded. triangles, 3.0 s. 6. **Without `imagecodecs`, `decode_dem` refuses the fixture** with a `GeoTiffError` that names Compression (259) = 5 and points at the `codecs` - extra (`src_python/tin_engine/io/geotiff.py:169`). Nothing new is needed + extra (`src_python/tin_engine/io/geotiff.py:198`). Nothing new is needed for the no-extra case beyond turning that error into a usage error. ## Rulings @@ -179,7 +179,7 @@ section), without changes: `keep_alive`. - The NoData value is converted to `T` exactly once. `decode_dem` already refuses a sentinel the cell type cannot hold - (`src_python/tin_engine/io/geotiff.py:307`), so the conversion is exact. + (`src_python/tin_engine/io/geotiff.py:336`), so the conversion is exact. - `sample(view, points)` takes a float64 `(N, 2)` array and returns `(z, valid)`. It releases the GIL. - `_core.pyi`: the class, the factory and `sample`. diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 0343fa93..bd4beb46 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -374,7 +374,7 @@ assemble(plan, load) -> Mosaic pixels, no files - [15a] Carried from the parked R3: `_header(tif, nodata) -> (RasterMeta, dtype)` holds everything `decode_dem` does before `page.asarray()` - (`io/geotiff.py:70-87`). `read_meta(source, *, nodata=None)` calls it and + (now `_header` itself, `io/geotiff.py:96-131`). `read_meta(source, *, nodata=None)` calls it and returns, and `decode_dem` calls it and then decodes. The header phase keeps the codec refusal. - [15d] `decode_dem(source, *, window=None)`: with an `IndexWindow`, decode From 2101d0623feae3e166418a8ac0f82f89dfe1580a Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 22:58:18 +0200 Subject: [PATCH 10/28] 15a: R6's refusal order as pinned; ROADMAP 15a row Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- docs/increments/15-dem-mosaic.md | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 4870ab41..8c9c9c44 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -43,7 +43,7 @@ increment that most needs a picture to check against | 20b | Minimum insertion distance from constraints: when refinement's worst node lies within ε = clamp(tol / slope, cell/100, cell/2) of a constraint segment, insert the foot on the segment (off-node, bilinear z) instead; a footed node still above tolerance is inserted after all, so the tolerance guarantee is unchanged. Removes the 0.0117° needle at 1 m (increment 20's C4) | shipped with 20 (#97) | `docs/increments/20b-min-insertion-distance.md` | | — | `tools/bench.py`: the 1 m benchmark and the thread-scaling sweep from one checked-in command, with power state, quality and commit recorded per run; the one-off scripts in `docs/benchmarks/2026-09-26/` are its specification. Rule 2's acceptance run needs it | shipped with branch `tools-bench`'s PR | `docs/benchmarks/bench-py.md` | | — | The serial phase: profile refine's serial insert-and-flip phase, then parallelise what the profile blames. Scaling tops out at about 2.0-2.2×. Profiled 2026-09-27: serial part about a third of 1-thread refine, mostly Lawson legalisation; the scan stops near 5× from load imbalance | profiled; designed as increment 21 (Ola's rulings 2026-09-27: L1 determinism, one path, at most 2 % more triangles). **21a shipped with branch `increment21a-quick-wins`'s PR**: dynamic scan blocks, active merge, reused flip stack; mesh bit-identical; on AC refine -10 to -15 % at 8 threads, ceiling 2.1x -> 2.3x (`docs/benchmarks/2026-09-27/21a-acceptance.md`). **21b shipped with branch `increment21b-lattice-incircle`'s PR**: an int64 lattice incircle answers 99.97-100 % of refine's incircle tests; mesh bit-identical; on battery refine -15 % at 8 threads, ceiling 2.3x -> 2.5x (`docs/benchmarks/2026-09-27/21b-acceptance.md`). 21c measurements next, then 21d | `docs/increments/21-parallel-refine.md`, `docs/benchmarks/2026-09-27/serial-profile/README.md` | -| 15 | A DEM in several tiles, and the domain in its own CRS: 15a Norway, many tiles in one projected CRS (Ola's 254 DTM10 UTM33 tiles; only the selected tiles must share a lattice, since 8 of them sit half a cell off); 15b the domain polygon reprojected into the DEM's CRS; 15c and 15d the São Francisco basin (geographic DEMs meshed in the DEM's own lattice frame, not resampled; window decoding and memory) | designed; 15a next (Ola, 2026-09-27: Norway first) | `docs/increments/15-dem-mosaic.md` | +| 15 | A DEM in several tiles, and the domain in its own CRS: 15a Norway, many tiles in one projected CRS (Ola's 254 DTM10 UTM33 tiles; only the selected tiles must share a lattice, since 8 of them sit half a cell off); 15b the domain polygon reprojected into the DEM's CRS; 15c and 15d the São Francisco basin (geographic DEMs meshed in the DEM's own lattice frame, not resampled; window decoding and memory) | **15a shipped with branch `increment15-dem-mosaic`'s PR**: `--dem DIR` or several files, `--bbox`, tiles selected and stitched on one lattice, Q1-Q5 refusals; a 2×2-tile box of Ola's archive meshes in 1.8 s. 15b next (Ola, 2026-09-27: Norway first) | `docs/increments/15-dem-mosaic.md` | | 16b | Interior polygons and polylines as constraints ("terrain polygons": lakes, land cover, roads, rivers): `--features PATH`, a GeoJSON `FeatureCollection`, each feature naming a vocabulary property; closed or open `Breakline`s, crossings noded, off-node vertices with bilinear z. **Its working example is real data** (Ola, 2026-09-27): CORINE Land Cover 2018 over the benchmark tile `7908_3_10m_z33.tif`. The source is Ola's local copy, `rasputin_data/corine_sql/.../U2018_CLC2018_V2020_20u1.gpkg` (8.2 GB, EPSG:3035, a sibling of this repository, which also holds the 254-tile DTM10 archive for gap 6). It reads without GDAL: sqlite3 over its R-tree, the GeoPackage blob header stripped, `shapely.wkb`, then `pyproj` to 25833, so CRS stops in Python as before. Probed 2026-09-27 in 0.2 s: 60 polygons in 8 classes (heath, bare rock, sparse vegetation, bogs, intertidal flats, water, sea, urban), 11 068 vertices clipped to the tile, median segment 54 m against 10 m cells. The EEA's public ArcGIS service (`image.discomap.eea.europa.eu`, `Corine/CLC2018_WM`) returns the same 11 068 clipped vertices and is the route for anyone without the file. What it forces on 16b's design: neighbouring polygons share their boundaries, so each shared edge arrives twice; the polygons run past the domain and must be clipped; the extract is committed as a fixture with the Copernicus attribution. Placed before 20c because 20c may split constraint segments and should be designed and measured on inputs that have interior ones | designed in 16's R6, no increment file yet; after the serial phase, before 20c | `docs/increments/16-domain-polygon.md` (R6) | | 20c | Soft quality criterion: a penalty that each Steiner node or constraint split must pay for in angle gained, instead of 20's hard 25°; applied at the start and during DEM refinement; may split constraint segments when that improves the mesh. Ola's rulings on 20's C1-C3 | to design after 16b (`@architect` measures cost against 20 first) | `docs/increments/20-start-quality.md` (Ola's rulings) | | — | Auto-catchment: the watershed upstream of a coordinate, computed from the DEM and handed to `--domain`, so a catchment no longer has to be supplied as a file (Ola, 2026-09-27: "not far into the future"). The textbook route is depression handling (Priority-Flood, Barnes, Lehman and Mulla 2014), D8 flow directions (O'Callaghan and Mark 1984) and accumulation, the pour point snapped to the strongest flow nearby, the upstream cells traced and their outline turned into a polygon; the literature check is `@architect`'s. Open for its design: whether it runs in the C++ core (a 10 m tile is 25 M cells); how a stair-stepped cell outline becomes a domain polygon, which meets input coarsening; and that a real catchment crosses tile edges, so it needs gap 6 (a DEM in several tiles) first. Legacy has nothing on it (`grep -rliE "watershed|flow.?acc|flow.?dir|pour.?point|catchment" legacy` returns no files) | to design; after gap 6, which it needs; placed after 20c, can move ahead of it on Ola's word | none yet | diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index bd4beb46..9eb79526 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -471,9 +471,13 @@ the DEM's CRS, as in the parked R6 (Q2). With a domain [15b], the domain's bounds in the DEM's CRS take `--bbox`'s place, and the two flags exclude each other (16 R1 already said so). -Without `--bbox` or `--domain`, `--dem DIR` on Ola's archive plans the 72 GiB -union box of the main lattice, and the cap refuses it (R7). That refusal is -correct, and its message points at `--bbox` and `--domain`. +Without `--bbox` or `--domain`, `--dem DIR` on Ola's archive selects every +tile, the half-cell ones included, so the mixed-lattice refusal (Q5) fires +before the memory cap (R7) is reached; `test_no_bounds_selects_both_lattices_ +and_is_refused` pins that order. Both refusals are correct, and the lattice +message ends "but a --bbox inside one lattice is meshed". On one lattice alone, +the 72 GiB union box would be refused by the cap, whose message points at +`--bbox` and `--domain`. ### R7. Memory From a5aabdc8c1d75dc99383ab8b8aa3f92d1c408d80 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 23:11:37 +0200 Subject: [PATCH 11/28] 15a review fixes: CI runs the two codecs suites; status, tile sizes, cap message, edge snap, project_structure; the basin note's spacing and scale errors corrected Co-Authored-By: Claude Opus 5.5 --- .github/workflows/main.yaml | 3 ++- docs/increments/15-dem-mosaic.md | 23 ++++++++++++++--------- docs/research/sao-francisco-basin.md | 13 +++++++------ project_structure.md | 5 +++++ 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index f1245ed6..26e89e62 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -192,7 +192,8 @@ jobs: - name: Tests with the codecs extra run: | python -m pip install -e ".[dev,codecs]" - pytest --no-cov tests/python/test_io_geotiff.py tests/python/test_geotiff_fixtures.py + pytest --no-cov tests/python/test_io_geotiff.py tests/python/test_geotiff_fixtures.py \ + tests/python/test_io_read_meta.py tests/python/test_cli_mesh_mosaic.py # Increment 13, ruling 10: read the .vtk output back with the readers # ParaView uses. The suite skips when vtk is absent, so it is run only diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 9eb79526..d5dc4646 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -1,9 +1,9 @@ # Increment 15 — a DEM in many tiles, inputs in their own CRS, and the computation frame -Status: **design, not yet reviewed by Ola.** Written by `@architect` before -`@tester`, per `docs/increments/README.md` step 1, on branch -`increment15-dem-mosaic`. Nothing here is implemented. The questions for Ola -are last. Each has a recommendation, and the rest of the file assumes it. +Status: **Q1-Q5 ruled by Ola; 15a implemented** on branch +`increment15-dem-mosaic` (red `2696bc2`, green `ff7cc8d`), in review. 15b-15d +are designed, not implemented; Q6-Q10 are open. Written by `@architect` before +`@tester`, per `docs/increments/README.md` step 1. ## Ruled by Ola @@ -100,8 +100,11 @@ They are measurement scripts, not production code, and nothing imports them. `.aux.xml` beside it), 19 GB. Every file passes `io/geotiff.py`'s own header checks (the probe calls them, so a refusal would be `decode_dem`'s): EPSG:25833, 10 m in both axes, area-registered, float32, LZW, tiled, NoData - −32767 from tag 42113. 244 are 5051 × 5051 nodes; the rest 5052 or 5053 on a - side, and two are smaller (2881 × 3521, 4103 × 3511). Header-only read of all + −32767 from tag 42113. 243 are 5051 × 5051 nodes; the rest are 5052 or 5053 + on a side, and four are short on one side (7305_3 2881 × 5051, 7405_1 + 5051 × 3521, 7405_2 5051 × 3511, 7507_4 4103 × 5052; corrected in review: + the probe's per-axis tallies had been paired into two tiles that do not + exist). Header-only read of all 254: 0.56 s wall. - **N2. 246 tiles share one lattice; 8 do not.** Taking the north-west-most node (x −100250, y 7950250) as reference, 246 tiles sit at integer node @@ -412,7 +415,9 @@ eight odd tiles. Here: `refuses_mixed_spacing`, `refuses_mixed_registration`, `refuses_mixed_nodata`). 4. **The window.** `c0 = floor((bx_min − X_ref) / dx)`, - `c1 = ceil((bx_max − X_ref) / dx)`, rows likewise, clamped to the lattice's + `c1 = ceil((bx_max − X_ref) / dx)`, rows likewise (an edge within 1e-6 cell + of a node is first snapped onto it, so float noise in a spacing like 0.1 m + cannot add a node line), clamped to the lattice's union. The mosaic's node grid is that index window, snapped outward, so it covers the box. From here on everything is integer index arithmetic. The mosaic's `x_min` is computed as `X_ref + c0 · dx`, and its `y_max` likewise. @@ -476,8 +481,8 @@ tile, the half-cell ones included, so the mixed-lattice refusal (Q5) fires before the memory cap (R7) is reached; `test_no_bounds_selects_both_lattices_ and_is_refused` pins that order. Both refusals are correct, and the lattice message ends "but a --bbox inside one lattice is meshed". On one lattice alone, -the 72 GiB union box would be refused by the cap, whose message points at -`--bbox` and `--domain`. +the 72 GiB union box would be refused by the cap, whose message says +"narrow it with --bbox". ### R7. Memory diff --git a/docs/research/sao-francisco-basin.md b/docs/research/sao-francisco-basin.md index 7b7ad9b1..7cea5708 100644 --- a/docs/research/sao-francisco-basin.md +++ b/docs/research/sao-francisco-basin.md @@ -25,7 +25,7 @@ marked *inferred* are extrapolations, not measurements. | DEM | spacing | what it is | licence | |---|---|---|---| -| **ANADEM** (ANA / UFRGS, 2024) | 30 m (1″) | Copernicus GLO-30 with vegetation bias removed for South America (Landsat-8, Sentinel-2, GEDI lidar). Mean bias 9.6 m (COPDEM) → 1.5 m; in forest 14.3 m → 0.4 m | free and open source per its authors; exact licence to be read from the repository | +| **ANADEM** (ANA / UFRGS, 2024) | 30 m (0.970″, measured from its headers in increment 15's B2; not Copernicus's 1″) | Copernicus GLO-30 with vegetation bias removed for South America (Landsat-8, Sentinel-2, GEDI lidar). Mean bias 9.6 m (COPDEM) → 1.5 m; in forest 14.3 m → 0.4 m | free and open source per its authors; exact licence to be read from the repository | | Copernicus GLO-30 | 30 m (1″) | a surface model (tree canopy and buildings included) | free (Copernicus licence) | | Copernicus GLO-90 | 90 m (3″) | the same at 3″ | free | | FABDEM | 30 m | Copernicus with forests and buildings removed (Bristol / Fathom) | CC BY-NC-SA 4.0: **non-commercial only** | @@ -91,11 +91,12 @@ In roughly the order the basin needs them: plus NumPy, chunked; no GDAL), or - mesh in a projected CRS while sampling the geographic grid. - **One CRS for the whole basin is a real choice.** *Computed:* UTM zone 23S - reaches a scale error of 1.07 % at the basin's east edge, and the Brazil - Polyconic (EPSG:5880) 4.6 %. A Lambert conformal conic fitted to the basin - (standard parallels 10°S and 18.5°S, central meridian 42°W) stays within - **0.46 %** over the whole bounding box. + **One CRS for the whole basin is a real choice.** *Computed* with pyproj + over the whole 7-21°S, 36-48°W box (corrected in review; the first figures + came from a few sample points): UTM zone 23S reaches a scale error of + 1.20 %, and the Brazil Polyconic (EPSG:5880) 4.89 %, both at 36°W 7°S. A + Lambert conformal conic fitted to the basin (standard parallels 10°S and + 18.5°S, central meridian 42°W) stays within **0.52 %**. 3. **21b's integer incircle does not apply to non-square cells.** QW2 needs `dx == dy`. On a geographic grid, or a projected grid resampled with non-square cells, refine falls back to the filtered kernel. That is correct diff --git a/project_structure.md b/project_structure.md index bfba4743..a9008825 100644 --- a/project_structure.md +++ b/project_structure.md @@ -83,6 +83,9 @@ src_python/tin_engine/ # public Python API (distribution name: rasputin) raster.py # the ONLY adapter from decoded data into _core grid_domain.py # DEM extent -> stride-subsampled nodes + outer ring; # pure numpy, never imports _core + mosaic.py # plan_mosaic / assemble: select, group by lattice, + # check overlaps and coverage, stitch; no files (15a) + dem_input.py # --dem/--bbox -> DemInput(tile, plan, label) (15a) domain.py # --domain: reads one polygon (GeoJSON or WKT), checks # CRS and extent; shapely + pyproj, never imports _core elevation.py # drops mesh vertices the DEM has no data for; @@ -110,6 +113,8 @@ src_python/tin_engine/ # public Python API (distribution name: rasputin) # ParaView; takes no path and opens nothing geotiff.py # TIFF container + GeoKey decoding -> DemTile models.py # Pydantic RasterMeta / DemTile + repository.py # TiffDemRepository: the ONE io/ module that opens + # files ("rb"); lists headers, loads tiles (15a) tests/ cpp/ # C++ tests (Catch2; unit/ and property/) From a88dda7465062dba63ff95b91a0d6ccf35f2b25e Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 23:14:11 +0200 Subject: [PATCH 12/28] 15a: Ola's reading of Q5 for overlapping lattices Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index d5dc4646..77600c20 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -27,6 +27,19 @@ are designed, not implemented; Q6-Q10 are open. Written by `@architect` before is refused; a request inside either lattice is meshed. Q6-Q10 (the basin) wait until after the Norwegian sub-increments. +- **2026-09-27, Q5 as read after review ("Yes", to the main session's + proposal):** @reviewer found that DTM10's 51-node overlaps make a box that + only reaches into a half-cell tile's overlap strip select that tile and be + refused as mixed-lattice, although the main lattice covers every node (the + design's own 15a acceptance box was refused; 98 of 576 20-km boxes around + the eight tiles). The rule is now, per lattice, whether **its own tiles + cover every node the request needs**: + - exactly one lattice covers it: mesh on that lattice, and drop the other + lattices' tiles from the plan; + - none covers it: refuse, as before (the Q5 message); + - several cover it (a box wholly inside an overlap strip): use the lattice + with **the most tiles in the repository**, ties broken by the name of its + first tile. Header-only and order-independent. ## Scope From 1e45970d9c2be90be22851e698005c43c8eb9a2d Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 23:26:42 +0200 Subject: [PATCH 13/28] 15a tests: lattice by coverage (Ola's Q5 reading), the disagreeing tile named, float64 cap, edge snap A test amendment after green (docs/increments/README.md, "Steps 2 and 3 are not strictly once each"): @reviewer's CHANGES REQUESTED on 15a, and Ola's reading of Q5 after review, land after ff7cc8d, so the tests that encode them come after it, in their own commit. No production code. - B1: per lattice, whether its own tiles cover every node the request needs; exactly one -> plan on it, the other lattices' tiles dropped; none -> the Q5 refusal; several -> most tiles in the repository, ties by first tile name, order-independent. Synthetic (test_mosaic.py), real extracts (test_dem_input.py, CI runs them), CLI, and the design's acceptance box on Ola's archive (headers only, skipped when the archive is absent). The bare two-lattice request is still refused: the "Pinned" section says why. - B2: the overlap refusal names the two tiles whose values disagree. - S2: TileFootprint.dtype from the header via PROMOTION; the cap counts np.result_type of the selected tiles; float64 boundary beside the float32 one. - S3: 0.1 m spacing, edges 0.3/0.7/0.9 add no node line (passes at HEAD; fails with _snap as identity). - S1: refusing a mostly uncovered window peaks below its float32 canvas (tracemalloc; 164 MB against 16 MB at HEAD). Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 57 +++++- tests/python/test_cli_mesh_mosaic.py | 19 ++ tests/python/test_dem_input.py | 23 +++ tests/python/test_io_repository.py | 68 +++++++ tests/python/test_mosaic.py | 257 +++++++++++++++++++++++++++ 5 files changed, 415 insertions(+), 9 deletions(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 77600c20..236eb147 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -956,10 +956,13 @@ failing M1's area-registered case. takes exactly those parameters. `needed` is a shapely geometry in the DEM's CRS, and it is closed: a node on its boundary is needed. Growing a domain by one cell is the caller's job (15b). -- **The cap** counts **4 bytes a node** (float32), because headers do not give - the decoded dtype, and refuses when `rows * cols * 4 > physical_memory() // - 2`. A float64 mosaic is therefore under-counted by 2×. The cap is on the - window, not on the lattice's union. +- **The cap** counts **the planned decoded dtype**: the itemsize of + `np.result_type` over the *selected* footprints' `dtype`, and refuses when + `rows * cols * itemsize > physical_memory() // 2`; the refusal names that + dtype (`float64`) and `--bbox`. A float64 tile outside the window does not + count. The cap is on the window, not on the lattice's union. (Amended after + review, S2; the red suite had pinned 4 bytes a node, which under-counted a + float64 mosaic by 2×.) - **The mosaic's `RasterMeta`**: `nodata_source` comes from the first selected tile by name, and `vertical_unit_assumed` is true if any selected tile's is. - **NoData against NoData:** two NaNs give NaN, and two sentinels give the @@ -973,12 +976,46 @@ failing M1's area-registered case. - a registration difference: `registration`; - a NoData difference: `nodata` and both values, with `None` for an absent sentinel; - - an overlap disagreement: both names, the count as a whole number, and the - largest difference; + - an overlap disagreement: both names — the two tiles whose *values* + disagree, not the first tile that merely covers the node (B2) — the count + as a whole number, and the largest difference; - uncovered nodes: the node bounding box of the uncovered nodes, with each coordinate written out, not in scientific notation; - a changed tile: `changed since it was listed`; - - the cap: `--bbox`. + - the cap: `--bbox`, and the planned dtype. + +**Amended after review (tests after green, S1-S3, B1, B2).** Pinned by the +test amendment that follows green `ff7cc8d`: + +- **Q5 as read after review (B1),** in `TestB1LatticeByCoverage` + (`test_mosaic.py`), the real-extract cases in `test_dem_input.py`'s + `TestRealDtm10`, one CLI case in `test_cli_mesh_mosaic.py`, and the + acceptance box against Ola's archive in `test_io_repository.py`'s + `TestB1RealArchive` (headers only; skipped where the archive is absent). When + one lattice's tiles are selected, nothing changes. When several are, **the + nodes a lattice must cover** are its nodes in the request's box, snapped + outward, clamped to the bounding box of every selected tile (any lattice) — + *not* to that lattice's own union, or a box running past it into the other + lattice's tiles would be silently cut short. Without a box, that is the + bounding box of every tile. So a bare `--dem DIR` over two lattices is still + refused as mixed-lattice (`test_no_bounds_selects_both_lattices_and_is_refused` + keeps its outcome), as are all the other two-lattice refusals in M2 and C4. + The chosen lattice's plan equals the plan of its tiles alone. The count is of + a lattice's tiles in the repository, not of those the box selects; "its first + tile" is its tile whose name sorts first. +- **The design's acceptance box selects nine tiles, not four.** With the + 51-node overlaps, the index-window selection (the first correction above) + also selects the main-lattice neighbours 7807_1, 7808_2, 7809_3, 7809_4 and + 7909_3, whose overlap strips reach into the 2 × 2 block. The archive test + asserts the four are in the plan, 7807_2 is not, and the window is + 10051 × 10051; the Acceptance's "`dem_tiles` listing four files" is not + pinned. Whether to drop tiles that only duplicate covered nodes is open. +- **The coverage refusal's memory (S1).** Refusing a mostly uncovered window + with no `needed` peaks, by tracemalloc, below the window's float32 canvas + (index and coordinate arrays of the uncovered nodes cost 8× it). The + `needed` path is not pinned. +- **The edge snap (S3)** has its test: at spacing 0.1, the box edges 0.3, 0.7 + and 0.9 add no node line. **`io/models.py`.** `DemTile._adopt(meta, array)` is a classmethod. It raises `ValueError` on a shape, dtype, ndim or non-C-contiguous mismatch, sets the @@ -988,8 +1025,10 @@ and `mosaic.py` contain the string `_adopt`. **`io/repository.py` and `io/geotiff.py`.** - `read_meta(source, *, nodata=None)`. -- `TileFootprint(name=, meta=)`, where `name` is the file's name - (`path.name`). +- `TileFootprint(name=, meta=, dtype=)`, where `name` is the file's name + (`path.name`) and `dtype` the numpy dtype the tile decodes to (a `np.dtype`, + default float32), filled by the repository from the header through + `geotiff.PROMOTION` (S2). - `TiffDemRepository(paths, *, nodata=None)` and `TiffDemRepository.from_directory(directory, *, nodata=None)`. Construction reads no file. diff --git a/tests/python/test_cli_mesh_mosaic.py b/tests/python/test_cli_mesh_mosaic.py index 14e76c58..fe00d7c7 100644 --- a/tests/python/test_cli_mesh_mosaic.py +++ b/tests/python/test_cli_mesh_mosaic.py @@ -275,6 +275,25 @@ def test_bbox_restricts_the_mesh_to_the_snapped_window( assert (ys.min(), ys.max()) == (6599965.0, 6600000.0) assert field(vtk, "elevation_source").startswith("mosaic of 4 tiles, 8 x 9 nodes; ") + def test_a_box_reaching_into_another_lattices_strip_meshes_on_the_covering_one( + self, tmp_path: Path, mosaic_dir: Path, source: DemTile + ) -> None: + """Ola's Q5 reading, end to end. `odd.tif` is half a cell east-west off + the quadrants and overlaps `se.tif`; the box selects both, but only the + main lattice covers it, so it meshes there and records `se.tif` alone.""" + odd = whole(5, 7, array=terrain(5, 7), x_min=500105.0, y_max=6599970.0) + write_tiles(mosaic_dir, {"odd.tif": odd}) + # cols floor(7.2)=7 .. ceil(11.2)=12; rows floor(5.2)=5 .. ceil(7.6)=8 + vtk = run_vtk( + tmp_path / "box.vtk", + "--dem", str(mosaic_dir), + "--bbox", "500072", "6599962", "500112", "6599974", + ) # fmt: skip + assert field(vtk, "dem_tiles") == "se.tif" + xs, ys = vtk.points[:, 0], vtk.points[:, 1] + assert (xs.min(), xs.max()) == (500070.0, 500120.0) + assert (ys.min(), ys.max()) == (6599960.0, 6599975.0) + def test_bbox_on_one_file(self, tmp_path: Path, source: DemTile) -> None: """Q2: `--bbox` applies to a single file too; it is then a window of it.""" single = write_one(tmp_path / "one.tif", source) diff --git a/tests/python/test_dem_input.py b/tests/python/test_dem_input.py index d60b3c2d..c38693f3 100644 --- a/tests/python/test_dem_input.py +++ b/tests/python/test_dem_input.py @@ -248,6 +248,29 @@ def test_q5_each_lattice_alone_opens(self, di: ModuleType, path: Path) -> None: opened = di.open_dem(request(di, path)) assert opened.tile.meta == decoded(path).meta + def test_q5_reading_a_box_on_7707_2_reaching_into_7707_1_opens_on_7707_2( + self, di: ModuleType + ) -> None: + """Ola's Q5 reading on real extracts. 7707_1 (y 7750690..7749740) lies + over 7707_2 (y 7750250..7749300); this box reaches 60 m into their + overlap strip, so it selects both, and only 7707_2 covers every node.""" + box = (769800.0, 7749350.0, 770000.0, 7749800.0) + opened = di.open_dem(request(di, LATTICES, box=box)) + assert [p.name for p in opened.plan.tiles] == [MAIN.name] + main = decoded(MAIN) + assert opened.tile.meta.x_min == 769800.0 + assert opened.tile.meta.y_max == 7749800.0 + # rows (7750250 - 7749800) / 10 = 45 .. 90, columns 5 .. 25 of 7707_2 + assert same_array(opened.tile.array, main.array[45:91, 5:26]) + + def test_q5_reading_a_box_inside_the_strip_takes_the_tie_by_name(self, di: ModuleType) -> None: + """Both lattices cover a box wholly inside the strip and hold one tile + each here, so the tie goes to the first tile's name: 7707_1.""" + box = (769800.0, 7749800.0, 770000.0, 7750200.0) + opened = di.open_dem(request(di, LATTICES, box=box)) + assert [p.name for p in opened.plan.tiles] == [ODD.name] + assert opened.tile.meta.x_min % 10.0 == 5.0 # on 7707_1's lattice + def test_q5_a_request_inside_one_lattice_opens_with_both_listed(self, di: ModuleType) -> None: """South of 7707_1's last row (y 7749740), only 7707_2 is selected.""" opened = di.open_dem(request(di, LATTICES, box=(769800.0, 7749350.0, 770000.0, 7749700.0))) diff --git a/tests/python/test_io_repository.py b/tests/python/test_io_repository.py index 045d3e9d..fde14aa5 100644 --- a/tests/python/test_io_repository.py +++ b/tests/python/test_io_repository.py @@ -18,6 +18,7 @@ import io from pathlib import Path from types import ModuleType +from typing import Any import numpy as np import pytest @@ -145,6 +146,73 @@ def test_a_caller_nodata_reaches_both_phases(self, repo: ModuleType, tmp_path: P assert repository.load("a.tif").meta == footprint.meta +class TestS2DecodedDtype: + """S2: a footprint carries the dtype its tile decodes to, from the header + through `PROMOTION`, so the cap can count the canvas the plan will build.""" + + @pytest.mark.parametrize( + ("stored", "decoded_as"), + [ + (np.int16, np.float32), + (np.uint8, np.float32), + (np.float32, np.float32), + (np.int32, np.float64), + (np.uint32, np.float64), + (np.float64, np.float64), + ], + ids=["int16", "uint8", "float32", "int32", "uint32", "float64"], + ) + def test_the_footprint_dtype_is_the_decoded_one( + self, repo: ModuleType, tmp_path: Path, stored: Any, decoded_as: Any + ) -> None: + path = write(tmp_path / "a.tif", micro_tiff(elevations().astype(stored))) + repository = repo.TiffDemRepository.from_directory(tmp_path) + (footprint,) = repository.footprints() + assert np.dtype(footprint.dtype) == decoded_as + assert np.dtype(footprint.dtype) == decoded(path).array.dtype # type: ignore[attr-defined] + + +ARCHIVE = Path(__file__).resolve().parents[3] / "rasputin_data" / "DTM10_UTM33_20220924" +needs_archive = pytest.mark.skipif( + not ARCHIVE.is_dir(), reason=f"Ola's DTM10 archive is not at {ARCHIVE}" +) + + +@needs_archive +class TestB1RealArchive: + """Ola's Q5 reading on the design's own 15a acceptance box, headers only. + + The 2 x 2 block 7908_3, 7908_2, 7808_4, 7808_1 is on the main lattice, and + the half-cell tile 7807_2's 51-node overlap reaches into its south-west + corner. Until the reading, the box was refused naming 7807_2. CI has no + archive; the synthetic equivalent is `test_mosaic.py::TestB1LatticeByCoverage` + and the real-extract one `test_dem_input.py::TestRealDtm10`. + """ + + BOX = (799750.0, 7849750.0, 900250.0, 7950250.0) + BLOCK = ("7908_3", "7908_2", "7808_4", "7808_1") + + def test_the_acceptance_box_plans_on_the_main_lattice(self, repo: ModuleType) -> None: + mz = importlib.import_module("tin_engine.mosaic") + prints = repo.TiffDemRepository.from_directory(ARCHIVE).footprints() + x_min, y_min, x_max, y_max = self.BOX + box = mz.Bounds(x_min=x_min, y_min=y_min, x_max=x_max, y_max=y_max) + result = mz.plan_mosaic(prints, box, None) + assert result.reference == (-100250.0, 7950250.0) # the main lattice's (N2) + assert (result.meta.rows, result.meta.cols) == (10051, 10051) + assert (result.meta.x_min, result.meta.y_max) == (799750.0, 7950250.0) + names = {p.name for p in result.tiles} + assert {f"{b}_10m_z33.tif" for b in self.BLOCK} <= names + assert "7807_2_10m_z33.tif" not in names + # Dropping the other lattice's tiles is the same as their absence. + odd = { # N2: the half-cell tiles are 5 m east-west off + f.name for f in prints if (f.meta.x_min - result.reference[0]) / f.meta.delta_x % 1 != 0 + } + assert "7807_2_10m_z33.tif" in odd + main = [f for f in prints if f.name not in odd] + assert result == mz.plan_mosaic(main, box, None) + + class TestF3Refusals: """F3: a listed file that fails `read_meta` refuses the listing, naming the file.""" diff --git a/tests/python/test_mosaic.py b/tests/python/test_mosaic.py index e2f70218..6ee23e28 100644 --- a/tests/python/test_mosaic.py +++ b/tests/python/test_mosaic.py @@ -259,6 +259,9 @@ def test_a_request_straddling_both_is_refused_naming_one_of_each( ) # fmt: skip def test_no_bounds_selects_both_lattices_and_is_refused(self, plan: Any, refused: Any) -> None: + """Unchanged by Ola's Q5 reading: without a box the request is every + tile, and neither lattice covers the other's nodes (odd.tif reaches + x 500165, past the main lattice's 500120), so none covers it (R6).""" tiles = self.repository() refused(lambda: plan(tiles), "odd.tif", "0.5 cell") @@ -324,6 +327,143 @@ def test_a_different_crs_outside_the_request_is_not_a_refusal(self, plan: Any) - assert [p.name for p in result.tiles] == ["nw.tif"] +class TestB1LatticeByCoverage: + """Ola's reading of Q5 after review ("Ruled by Ola", 2026-09-27): per + lattice, whether **its own tiles cover every node the request needs**. + + Exactly one covers: plan on it, and the other lattices' tiles are not in + the plan. None covers: the Q5 refusal (the straddle tests in M2). Several + cover (a box wholly inside an overlap strip): the lattice with the most + tiles in the repository, ties by the name of its first tile. + + The repository is M2's: the 9 x 13 main lattice in four, and `odd.tif` + (x 500105..500165, y 6599970..6599950) half a cell east-west off it. Their + overlap strip is x 500105..500120, y 6599970..6599960. + + The odd tile is also named `a_odd.tif`, which sorts before the main + lattice's `ne.tif`: an implementation that takes whichever lattice comes + first, whatever it covers, fails one of the two names in each case. + """ + + MAIN_REACHING_INTO_THE_STRIP = (500072.0, 6599962.0, 500112.0, 6599974.0) + ODD_REACHING_INTO_THE_STRIP = (500106.0, 6599952.0, 500150.0, 6599969.0) + INSIDE_THE_STRIP = (500106.0, 6599961.0, 500114.0, 6599969.0) + ODD_ORIGIN = (X0 + 10.5 * DX, Y0 - 6 * DY) + + @staticmethod + def main() -> dict[str, DemTile]: + return quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + + @staticmethod + def odd(name: str = "odd.tif", pieces: int = 1) -> dict[str, DemTile]: + """The odd lattice's 5 x 7 tile, whole, or in four named `-`.""" + x_min, y_max = TestB1LatticeByCoverage.ODD_ORIGIN + tile = whole(rows=5, cols=7, x_min=x_min, y_max=y_max) + if pieces == 1: + return {name: tile} + return { + f"{name}-{q}": t for q, t in quadrants(tile, row_cut=2, col_cut=3, overlap=1).items() + } + + @pytest.mark.parametrize("odd_name", ["odd.tif", "a_odd.tif"]) + def test_a_main_box_reaching_into_the_strip_plans_on_the_main_lattice( + self, mz: ModuleType, plan: Any, build: Any, odd_name: str + ) -> None: + """The reviewer's case: the box selects the odd tile, and today is + refused as mixed-lattice, but the main lattice covers every node.""" + main = self.main() + tiles = {**main, **self.odd(odd_name)} + result = plan(tiles, self.MAIN_REACHING_INTO_THE_STRIP) + assert [p.name for p in result.tiles] == ["se.tif"] + assert result.reference == (X0, Y0) + assert result.window == window(mz, 5, 7, 4, 6) + assert result == plan(main, self.MAIN_REACHING_INTO_THE_STRIP) + mosaic, loads = build(tiles, self.MAIN_REACHING_INTO_THE_STRIP) + assert loads.calls == ["se.tif"] + assert same_array(mosaic.tile.array, whole().array[5:9, 7:13]) + + @pytest.mark.parametrize("odd_name", ["odd.tif", "a_odd.tif"]) + def test_an_odd_box_reaching_into_the_strip_plans_on_the_odd_lattice( + self, mz: ModuleType, plan: Any, odd_name: str + ) -> None: + """The mirror case. The main lattice's tiles cover the part of the box + over them, but not x 500130..500150 or y 6599955..6599950, which the + odd tile holds: the box is not clamped to the main lattice's union.""" + odd = self.odd(odd_name) + result = plan({**self.main(), **odd}, self.ODD_REACHING_INTO_THE_STRIP) + assert [p.name for p in result.tiles] == [odd_name] + assert result.reference == self.ODD_ORIGIN + assert result.window == window(mz, 0, 0, 5, 6) + assert result == plan(odd, self.ODD_REACHING_INTO_THE_STRIP) + + def test_several_cover_the_most_tiles_in_the_repository_wins( + self, mz: ModuleType, plan: Any + ) -> None: + """Main 4 tiles, odd 1; each selects exactly one tile for this box, so + a count of *selected* tiles would tie.""" + main = self.main() + result = plan({**main, **self.odd()}, self.INSIDE_THE_STRIP) + assert [p.name for p in result.tiles] == ["se.tif"] + assert result.reference == (X0, Y0) + assert result == plan(main, self.INSIDE_THE_STRIP) + + def test_the_count_is_of_the_repository_not_of_the_selection( + self, footprint: Any, mz: ModuleType + ) -> None: + """Main 1 tile (se alone), odd 2 (one far away, never selected); both + select one. The odd names sort after `se.tif`, so a count of the + selection, tied and broken by name, would choose the main lattice.""" + x_min, y_max = self.ODD_ORIGIN + tiles = { + "se.tif": self.main()["se.tif"], + "x_odd.tif": self.odd()["odd.tif"], + "x_far.tif": whole(rows=3, cols=3, x_min=x_min + 100 * DX, y_max=y_max), + } + result = mz.plan_mosaic( + footprints(footprint, tiles), bounds_of(mz, self.INSIDE_THE_STRIP), None + ) + assert [p.name for p in result.tiles] == ["x_odd.tif"] + assert result.reference == (x_min, y_max) + + @pytest.mark.parametrize( + ("prefix", "winner"), + [("a", "odd"), ("o", "main")], + ids=["odd-first-name-sorts-first", "main-first-name-sorts-first"], + ) + def test_a_tie_goes_to_the_lattice_whose_first_tile_sorts_first( + self, plan: Any, prefix: str, winner: str + ) -> None: + """Four tiles each. Main's first tile is `ne.tif` (last `sw.tif`); the + odd lattice's first is `-ne.tif` (last `-sw.tif`). + With prefix `o`, main's first name sorts first but the odd lattice's + last name does: a tie broken by the last tile, or by the larger first + name, picks the wrong one in one of the two cases.""" + odd = self.odd(prefix, pieces=4) + result = plan({**self.main(), **odd}, self.INSIDE_THE_STRIP) + names = {p.name for p in result.tiles} + if winner == "odd": + assert names <= set(odd), names + assert result.reference == self.ODD_ORIGIN + else: + assert names == {"se.tif"} + assert result.reference == (X0, Y0) + + @pytest.mark.parametrize( + "odd_pieces", [1, 4], ids=["most-tiles", "tie-by-name"] + ) # fmt: skip + def test_the_choice_does_not_depend_on_footprint_order( + self, mz: ModuleType, footprint: Any, odd_pieces: int + ) -> None: + tiles = {**self.main(), **self.odd("a", pieces=odd_pieces)} + prints = footprints(footprint, tiles) + box = bounds_of(mz, self.INSIDE_THE_STRIP) + first = mz.plan_mosaic(prints, box, None) + for k in range(len(prints)): # every rotation, forwards and backwards + rotated = prints[k:] + prints[:k] + for order in (rotated, rotated[::-1]): + assert mz.plan_mosaic(order, box, None) == first + + def _meta_change(change: dict[str, Any]) -> dict[str, Any]: """`piece` takes `RasterMeta` field names.""" names = {"dx": "delta_x", "dy": "delta_y", "area": "pixel_is_area"} @@ -418,6 +558,22 @@ def test_a_tile_touching_the_box_on_one_node_line_contributes_it( assert same_array(result.tile.array, source.array[0:3, 1:7]) assert sorted(loads.calls) == ["ne.tif", "nw.tif"] + def test_float_noise_in_a_decimetre_spacing_does_not_add_a_node_line( + self, mz: ModuleType, plan: Any + ) -> None: + """S3, R4 point 4's snap. At spacing 0.1 from a reference at (0, 1): + 0.3 / 0.1 is 2.9999999999999996 (floor 2), (1 - 0.9) / 0.1 is + 0.9999999999999998 (floor 0) and (1 - 0.7) / 0.1 is 3.0000000000000004 + (ceil 4). Each edge is a node, so each would add a line without the snap.""" + assert (0.3 / 0.1, (1.0 - 0.9) / 0.1, (1.0 - 0.7) / 0.1) == ( + 2.9999999999999996, + 0.9999999999999998, + 3.0000000000000004, + ) # the noise is there + tiles = {"d.tif": whole(x_min=0.0, y_max=1.0, dx=0.1, dy=0.1)} + result = plan(tiles, (0.3, 0.7, 0.6, 0.9)) + assert result.window == window(mz, 1, 3, 3, 4) + def test_a_box_meeting_no_tile_is_refused( self, plan: Any, refused: Any, tiles: dict[str, DemTile] ) -> None: @@ -459,6 +615,57 @@ def test_the_boundary_at_four_bytes_a_node( else: refused(lambda: plan(tiles), "--bbox") + @pytest.mark.parametrize( + ("memory", "accepted"), [(2 * NODES * 8, True), (2 * NODES * 8 - 1, False)] + ) + def test_the_boundary_at_eight_bytes_a_node_when_a_selected_tile_decodes_to_float64( + self, + mz: ModuleType, + footprint: Any, + refused: Any, + monkeypatch: pytest.MonkeyPatch, + memory: int, + accepted: bool, + ) -> None: + """S2: the cap counts the planned decoded dtype, `np.result_type` of the + selected footprints' `dtype`. One float64 tile makes the canvas float64 + (R5), so the window costs 8 bytes a node, not 4.""" + monkeypatch.setattr(mz, "physical_memory", lambda: memory) + tiles = quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + prints = [ + footprint(name=name, meta=tile.meta, dtype=np.dtype(np.float64)) + if name == "se.tif" + else footprint(name=name, meta=tile.meta) + for name, tile in tiles.items() + ] + call = lambda: mz.plan_mosaic(prints, None, None) # noqa: E731 + if accepted: + assert call().meta.rows == 9 + else: + refused(call, "--bbox", "float64") + + def test_a_float64_tile_outside_the_window_does_not_count( + self, mz: ModuleType, footprint: Any, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Only the selected tiles decode, so only their dtypes size the canvas.""" + monkeypatch.setattr(mz, "physical_memory", lambda: 2 * 16 * 4) + tiles = quadrants(whole(), row_cut=4, col_cut=6, overlap=1) + prints = [ + footprint(name=name, meta=tile.meta, dtype=np.dtype(np.float64)) + if name == "se.tif" + else footprint(name=name, meta=tile.meta) + for name, tile in tiles.items() + ] + box = bounds_of(mz, (500012.0, 6599987.0, 500033.0, 6599996.0)) # 4 x 4, nw alone + assert [p.name for p in mz.plan_mosaic(prints, box, None).tiles] == ["nw.tif"] + + def test_a_footprints_dtype_defaults_to_float32(self, footprint: Any) -> None: + """S2: `TileFootprint` gains an optional decoded `dtype`, float32 unless given.""" + tile = whole() + assert np.dtype(footprint(name="a.tif", meta=tile.meta).dtype) == np.float32 + given = footprint(name="a.tif", meta=tile.meta, dtype=np.dtype(np.float64)) + assert np.dtype(given.dtype) == np.float64 + def test_the_cap_is_on_the_window_not_the_union( self, mz: ModuleType, plan: Any, monkeypatch: pytest.MonkeyPatch ) -> None: @@ -569,6 +776,32 @@ def test_a_hole_in_the_middle_is_refused(self, plan: Any, refused: Any) -> None: tiles = blocks(whole(rows=9, cols=12, area=True), 3, 4, skip={(1, 1)}) refused(lambda: plan(tiles), "500040", "500070") + def test_refusing_a_mostly_uncovered_window_costs_less_than_its_canvas( + self, plan: Any, refused: Any + ) -> None: + """S1: the check runs on windows the cap allows, up to half of physical + memory at 4 bytes a node, so what it allocates per node bounds what the + cap can promise. Two 2 x 2 tiles at opposite corners of a 2000 x 2000 + window: nearly every node is uncovered. Index and coordinate arrays of + the uncovered nodes cost 32 bytes a node, 8x the float32 canvas; the + refusal must peak below the canvas itself (16 MB here). tracemalloc + sees numpy's buffers, so this is a count of bytes, not a timing.""" + import tracemalloc + + n = 2000 + corners = { + "nw.tif": whole(rows=2, cols=2), + "se.tif": whole(rows=2, cols=2, x_min=X0 + (n - 2) * DX, y_max=Y0 - (n - 2) * DY), + } + canvas_bytes = n * n * 4 + tracemalloc.start() + try: + refused(lambda: plan(corners), f"{X0 + (n - 1) * DX:.0f}", f"{Y0 - (n - 1) * DY:.0f}") + _, peak = tracemalloc.get_traced_memory() + finally: + tracemalloc.stop() + assert peak < canvas_bytes, f"peak {peak} bytes for a {canvas_bytes}-byte canvas" + def test_a_request_away_from_the_hole_plans(self, plan: Any, holed: dict[str, DemTile]) -> None: result = plan(holed, (500001.0, 6599991.0, 500069.0, 6599999.0)) assert {p.name for p in result.tiles} == {"b00.tif", "b01.tif"} @@ -734,6 +967,30 @@ def test_a_disagreement_is_refused_naming_both_the_count_and_the_largest( message = refused(lambda: mz.assemble(planned, Loads(tiles)), "w.tif", "e.tif", "2.5") names_number(message, 3) + def test_the_refusal_names_the_tiles_whose_values_disagree( + self, mz: ModuleType, plan: Any, refused: Any + ) -> None: + """B2: three tiles on one 3 x 3 grid, equal everywhere but node (0, 0), + where `a` is NoData, `b` 1.0 and `c` 5.0. The disagreement is between + b and c; `a` covers the node too, but its value is not in the clash. + In every assembly order.""" + source = whole(rows=3, cols=3) + tiles = {} + for name, first in (("a.tif", np.nan), ("b.tif", 1.0), ("c.tif", 5.0)): + array = source.array.copy() + array[0, 0] = first + tiles[name] = piece(source, 0, 3, 0, 3, array=array) + planned = plan(tiles) + for order in itertools.permutations(planned.tiles): + message = refused( + lambda o=order: mz.assemble(planned.model_copy(update={"tiles": o}), Loads(tiles)), + "b.tif", + "c.tif", + "4", + ) + assert "a.tif" not in message, ([p.name for p in order], message) + names_number(message, 1) + def test_one_ulp_is_a_disagreement(self, build: Any, refused: Any) -> None: """`==`, not `isclose`: DTM10's and ANADEM's overlaps agree bit for bit (N3, B4).""" source, arrays = self.pair(1) From 540764a18921d0e3f079076e61c5af0f4249aaf1 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 23:27:34 +0200 Subject: [PATCH 14/28] 15a: the Acceptance line matches the index-window selection and Ola's Q5 reading Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 236eb147..3d3205c4 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -1121,8 +1121,12 @@ Deflate, 0.6 MB together. - **15a:** `rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20220924/ --bbox … --tolerance 1` on the 2 × 2 block writes a `.vtk` that ParaView opens, - with `dem_tiles` listing four files. The same box over one of N2's odd tiles - and a neighbour is refused, naming both. The quadrant split of the benchmark + with `dem_tiles` listing the four block tiles among the main-lattice tiles + the index window selects (nine: with 51-node overlaps it also takes 7807_1, + 7808_2, 7809_3, 7809_4 and 7909_3; corrected at the 15a test amendment), and + not 7807_2, whose lattice does not cover the box (Ola's Q5 reading). A box + that straddles one of N2's odd tiles and a neighbour so that neither lattice + covers it is refused, naming both. The quadrant split of the benchmark tile meshes identically to the tile. `--dem file.tif` output unchanged. - **15b:** a catchment polygon in EPSG:4326 over the archive meshes, with `domain_crs` and `domain_transform` recorded. From 77c5c4e5234914de9f0c67300fc739a6d9fea167 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 23:34:08 +0200 Subject: [PATCH 15/28] 15a: lattice chosen by coverage (Ola's Q5 reading), the disagreeing tile named, float64-aware cap, cheap coverage refusal Makes the 24 tests of 1e45970 pass; no test or fixture touched. - B1: when the request selects tiles on several lattices, each is asked whether its own tiles cover every node it needs: the box snapped outward on it, clamped to the bounding box of every selected tile on any lattice. One covers: plan on it alone (a re-plan of its tiles, so the plan equals theirs). None: the Q5 refusal. Several: most tiles in the repository, ties by the lattice's first tile name. Header-only, order-independent. - B2: the overlap refusal names the merged tile whose value the incoming one disagrees with, re-loading candidates on the error path. - S2: TileFootprint.dtype (default float32), filled by the repository via the new geotiff.read_header (read_meta plus PROMOTION of the file dtype). The cap counts np.result_type of the selected footprints; the plan's placements carry the dtype, so assemble allocates the canvas once in it, keeping load-time promotion for footprints that under-state it. The false "headers do not give the decoded dtype" comment and BYTES_PER_NODE go. - S1: the coverage refusal counts with count_nonzero and bounds with any(axis=...); per-node index arrays only on the `needed` path. Co-Authored-By: Claude Opus 5.5 --- src_python/tin_engine/io/geotiff.py | 13 +- src_python/tin_engine/io/repository.py | 16 +- src_python/tin_engine/mosaic.py | 219 ++++++++++++++++++------- 3 files changed, 178 insertions(+), 70 deletions(-) diff --git a/src_python/tin_engine/io/geotiff.py b/src_python/tin_engine/io/geotiff.py index 06b27059..490a3508 100644 --- a/src_python/tin_engine/io/geotiff.py +++ b/src_python/tin_engine/io/geotiff.py @@ -53,8 +53,17 @@ def read_meta(source: BinaryIO, *, nodata: float | None = None) -> RasterMeta: damaged still gives its `RasterMeta`; `decode_dem` then refuses it at the "pixel data" stage. `source` is read but not closed. """ + return read_header(source, nodata=nodata)[0] + + +def read_header( + source: BinaryIO, *, nodata: float | None = None +) -> tuple[RasterMeta, np.dtype[Any]]: + """`read_meta`, and the dtype `decode_dem` will return: the file's through + `PROMOTION` (15a S2, so a mosaic's cap can count the canvas it will build).""" with _tiff(source, nodata) as tif: - return _header(tif, nodata)[0] + meta, dtype = _header(tif, nodata) + return meta, PROMOTION[dtype] def decode_dem(source: BinaryIO, *, nodata: float | None = None) -> DemTile: @@ -349,4 +358,4 @@ def _same(a: float, b: float) -> bool: return a == b or (math.isnan(a) and math.isnan(b)) -__all__ = ["decode_dem", "read_meta"] +__all__ = ["decode_dem", "read_header", "read_meta"] diff --git a/src_python/tin_engine/io/repository.py b/src_python/tin_engine/io/repository.py index a5c63bca..ed0d87cb 100644 --- a/src_python/tin_engine/io/repository.py +++ b/src_python/tin_engine/io/repository.py @@ -11,11 +11,12 @@ from collections.abc import Callable, Iterable from pathlib import Path -from typing import Protocol +from typing import Any, Protocol +import numpy as np from pydantic import BaseModel, ConfigDict -from .geotiff import decode_dem, read_meta +from .geotiff import decode_dem, read_header from .models import DemTile, GeoTiffError, RasterMeta #: `from_directory` lists these suffixes, in any case (R2). @@ -23,12 +24,14 @@ class TileFootprint(BaseModel): - """One tile as its header describes it: the file's name and its node grid.""" + """One tile as its header describes it: the file's name, its node grid, and + the dtype it decodes to (float32 unless given; S2).""" - model_config = ConfigDict(frozen=True) + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) name: str meta: RasterMeta + dtype: np.dtype[Any] = np.dtype(np.float32) class DemRepository(Protocol): @@ -79,9 +82,10 @@ def from_directory(cls, directory: Path, *, nodata: float | None = None) -> Tiff def footprints(self) -> tuple[TileFootprint, ...]: """Read every header once, on the first call, and keep them (R2).""" if self._footprints is None: + headers = {name: self._read(name, read_header) for name in sorted(self._paths)} self._footprints = tuple( - TileFootprint(name=name, meta=self._read(name, read_meta)) - for name in sorted(self._paths) + TileFootprint(name=name, meta=meta, dtype=dtype) + for name, (meta, dtype) in headers.items() ) return self._footprints diff --git a/src_python/tin_engine/mosaic.py b/src_python/tin_engine/mosaic.py index 7b231c0d..5770ade5 100644 --- a/src_python/tin_engine/mosaic.py +++ b/src_python/tin_engine/mosaic.py @@ -36,10 +36,6 @@ #: within this. Measured noise is at most 2.2e-11 cells (B3), and 0 for DTM10. ALIGN_TOLERANCE = 1e-6 -#: The cap counts float32: headers do not give the decoded dtype, so a float64 -#: mosaic is under-counted by 2x (pinned by the red suite). -BYTES_PER_NODE = 4 - class MosaicError(ValueError): """A request the tiles cannot answer. A `ValueError`, like `GeoTiffError`.""" @@ -82,14 +78,16 @@ class IndexWindow(BaseModel): class TilePlacement(BaseModel): - """One selected tile: `canvas` in canvas indices, `source` in the tile's own.""" + """One selected tile: `canvas` in canvas indices, `source` in the tile's own, + and the dtype its footprint says it decodes to.""" - model_config = ConfigDict(frozen=True) + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) name: str meta: RasterMeta canvas: IndexWindow source: IndexWindow + dtype: np.dtype[Any] = np.dtype(np.float32) class MosaicPlan(BaseModel): @@ -112,6 +110,18 @@ class Mosaic: plan: MosaicPlan +@dataclass(frozen=True, slots=True) +class _Lattice: + """One lattice's tiles (sorted by name), its reference node, each tile's + global index, and the box's window on it with the tiles that meet it.""" + + group: list[TileFootprint] + reference: tuple[float, float] + placed: list[_Placed] + window: tuple[int, int, int, int] + selected: list[_Placed] + + @dataclass(frozen=True, slots=True) class _Placed: """A footprint and its global index on its lattice.""" @@ -136,56 +146,50 @@ def plan_mosaic( `bounds` is the box in the DEM's CRS; without it, the lattice's union. `needed` is a shapely geometry in the DEM's CRS, closed: a node on its boundary is needed. Without it, every node of the window is. + + When the request selects tiles on several lattices, the plan is on the one + whose own tiles cover every needed node (Ola's Q5 reading, `_covering`). """ if not footprints: raise MosaicError("no tiles to plan a mosaic from") - lattices: list[list[TileFootprint]] = [] + groups: list[list[TileFootprint]] = [] for footprint in sorted(footprints, key=lambda f: f.name): - group = next((g for g in lattices if _aligned(g[0].meta, footprint.meta)), None) + group = next((g for g in groups if _aligned(g[0].meta, footprint.meta)), None) if group is None: - lattices.append([footprint]) + groups.append([footprint]) else: group.append(footprint) - chosen: list[tuple[tuple[float, float], tuple[int, int, int, int], list[_Placed]]] = [] - for group in lattices: + chosen: list[_Lattice] = [] + for group in groups: reference, placed = _placed(group) window = _window(bounds, reference, group[0].meta, placed) selected = [p for p in placed if window is not None and p.meets(*window)] if window is not None and selected: - chosen.append((reference, window, selected)) + chosen.append(_Lattice(group, reference, placed, window, selected)) if not chosen: raise MosaicError(f"the box {bounds} meets no tile") if len(chosen) > 1: - raise MosaicError(_mixed(chosen[0][2][0].footprint, chosen[1][2][0].footprint)) + return plan_mosaic(_covering(chosen, bounds, needed), bounds, needed) - (x_ref, y_ref), (r0, c0, r1, c1), selected = chosen[0] + lattice = chosen[0] + (x_ref, y_ref), (r0, c0, r1, c1), selected = lattice.reference, lattice.window, lattice.selected rows, cols = r1 - r0 + 1, c1 - c0 + 1 - first = selected[0].footprint.meta if rows < 2 or cols < 2: raise MosaicError(f"the request is {rows} x {cols} nodes; a mesh needs at least 2 x 2") - cap = physical_memory() // 2 - if rows * cols * BYTES_PER_NODE > cap: + dtype = np.result_type(*(p.footprint.dtype for p in selected)) + cap, size = physical_memory() // 2, rows * cols * dtype.itemsize + if size > cap: raise MosaicError( - f"the request is {rows} x {cols} nodes, {rows * cols * BYTES_PER_NODE} bytes at " - f"float32, over the cap of half the physical memory ({cap} bytes); " - "narrow it with --bbox" + f"the request is {rows} x {cols} nodes, {size} bytes at {dtype}, over the cap " + f"of half the physical memory ({cap} bytes); narrow it with --bbox" ) - meta = RasterMeta( - x_min=x_ref + c0 * first.delta_x, - y_max=y_ref - r0 * first.delta_y, - delta_x=first.delta_x, - delta_y=first.delta_y, - cols=cols, - rows=rows, - epsg=first.epsg, - nodata=first.nodata, - nodata_source=first.nodata_source, - pixel_is_area=first.pixel_is_area, - vertical_unit_assumed=any(p.footprint.meta.vertical_unit_assumed for p in selected), - ) + vertical = any(p.footprint.meta.vertical_unit_assumed for p in selected) + meta = _grid(selected[0].footprint.meta, lattice.reference, lattice.window, vertical) tiles = tuple(_placement(p, r0, c0, r1, c1) for p in selected) - _check_coverage(meta, tiles, needed) + uncovered = _uncovered(meta, tiles, needed) + if uncovered: + raise MosaicError(uncovered) return MosaicPlan( meta=meta, reference=(x_ref, y_ref), @@ -199,24 +203,23 @@ def assemble(plan: MosaicPlan, load: Callable[[str], DemTile]) -> Mosaic: One tile whose grid is the mosaic's is returned as loaded: no canvas, no copy (I7). Otherwise the canvas is allocated once, NaN-filled, in the - first tile's dtype, and handed to `DemTile` without a copy (R7). Dtypes - are known only after a load, so a later float64 tile re-casts it once to - the tiles' result dtype; both archives are float32 throughout. + planned dtype (the placements' result dtype, from the headers), and handed + to `DemTile` without a copy (R7). A hand-built footprint may under-state + its dtype, so a loaded tile that promotes further re-casts the canvas. """ if len(plan.tiles) == 1 and plan.tiles[0].meta == plan.meta: return Mosaic(tile=_loaded(plan.tiles[0], load), plan=plan) - canvas: npt.NDArray[Any] | None = None + if not plan.tiles: + raise MosaicError("the plan has no tiles") + planned = np.result_type(*(t.dtype for t in plan.tiles)) + canvas: npt.NDArray[Any] = np.full((plan.meta.rows, plan.meta.cols), np.nan, dtype=planned) merged: list[TilePlacement] = [] for placement in plan.tiles: array = _loaded(placement, load).array - if canvas is None: - canvas = np.full((plan.meta.rows, plan.meta.cols), np.nan, dtype=array.dtype) - elif np.result_type(canvas.dtype, array.dtype) != canvas.dtype: + if np.result_type(canvas.dtype, array.dtype) != canvas.dtype: canvas = canvas.astype(np.result_type(canvas.dtype, array.dtype)) - _merge(canvas, placement, array, plan.meta.nodata, merged) + _merge(canvas, placement, array, plan.meta.nodata, merged, load) merged.append(placement) - if canvas is None: - raise MosaicError("the plan has no tiles") return Mosaic(tile=DemTile._adopt(plan.meta, canvas), plan=plan) @@ -252,16 +255,91 @@ def _window( lattice's union (R4.4); None when the box misses the union.""" last_row = max(p.row + p.footprint.meta.rows for p in placed) - 1 last_col = max(p.col + p.footprint.meta.cols for p in placed) - 1 + return _clamp(_snapped(bounds, reference, m), (0, 0, last_row, last_col)) + + +def _snapped( + bounds: Bounds | None, reference: tuple[float, float], m: RasterMeta +) -> tuple[int, int, int, int] | None: + """The box snapped outward to `m`'s lattice, unclamped; None for no box.""" if bounds is None: - return 0, 0, last_row, last_col + return None (x_ref, y_ref), dx, dy = reference, m.delta_x, m.delta_y - c0 = max(math.floor(_snap((bounds.x_min - x_ref) / dx)), 0) - c1 = min(math.ceil(_snap((bounds.x_max - x_ref) / dx)), last_col) - r0 = max(math.floor(_snap((y_ref - bounds.y_max) / dy)), 0) - r1 = min(math.ceil(_snap((y_ref - bounds.y_min) / dy)), last_row) + return ( + math.floor(_snap((y_ref - bounds.y_max) / dy)), + math.floor(_snap((bounds.x_min - x_ref) / dx)), + math.ceil(_snap((y_ref - bounds.y_min) / dy)), + math.ceil(_snap((bounds.x_max - x_ref) / dx)), + ) + + +def _clamp( + window: tuple[int, int, int, int] | None, limits: tuple[int, int, int, int] +) -> tuple[int, int, int, int] | None: + """`window` (None: unbounded) inside `limits`, or None when they miss.""" + r0, c0, r1, c1 = limits if window is None else window + r0, c0 = max(r0, limits[0]), max(c0, limits[1]) + r1, c1 = min(r1, limits[2]), min(c1, limits[3]) return (r0, c0, r1, c1) if r0 <= r1 and c0 <= c1 else None +def _covering(chosen: list[_Lattice], bounds: Bounds | None, needed: Any) -> list[TileFootprint]: + """Ola's Q5 reading: of the lattices the request selects tiles on, the one + whose own tiles cover every node it needs; several, the most tiles, ties by + the first tile's name; none, the Q5 refusal. + + A lattice needs its nodes in the box snapped outward, clamped to the + bounding box of every selected tile on any lattice, not to its own union, + or a box running past it into another lattice's tiles would be cut short. + """ + corners = [(p.footprint.meta, lattice) for lattice in chosen for p in lattice.selected] + x_lo = min(m.x_min for m, _ in corners) + x_hi = max(m.x_min + (m.cols - 1) * m.delta_x for m, _ in corners) + y_hi = max(m.y_max for m, _ in corners) + y_lo = min(m.y_max - (m.rows - 1) * m.delta_y for m, _ in corners) + covering = [] + for lattice in chosen: + (x_ref, y_ref), m = lattice.reference, lattice.group[0].meta + inside = ( + math.ceil(_snap((y_ref - y_hi) / m.delta_y)), + math.ceil(_snap((x_lo - x_ref) / m.delta_x)), + math.floor(_snap((y_ref - y_lo) / m.delta_y)), + math.floor(_snap((x_hi - x_ref) / m.delta_x)), + ) + window = _clamp(_snapped(bounds, lattice.reference, m), inside) + if window is None: + continue + tiles = tuple(_placement(p, *window) for p in lattice.placed if p.meets(*window)) + if not _uncovered(_grid(m, lattice.reference, window, False), tiles, needed): + covering.append(lattice) + if not covering: + raise MosaicError(_mixed(chosen[0].selected[0].footprint, chosen[1].selected[0].footprint)) + return min(covering, key=lambda c: (-len(c.group), c.group[0].name)).group + + +def _grid( + first: RasterMeta, + reference: tuple[float, float], + window: tuple[int, int, int, int], + vertical_unit_assumed: bool, +) -> RasterMeta: + """The node grid of `window` on `first`'s lattice.""" + r0, c0, r1, c1 = window + return RasterMeta( + x_min=reference[0] + c0 * first.delta_x, + y_max=reference[1] - r0 * first.delta_y, + delta_x=first.delta_x, + delta_y=first.delta_y, + cols=c1 - c0 + 1, + rows=r1 - r0 + 1, + epsg=first.epsg, + nodata=first.nodata, + nodata_source=first.nodata_source, + pixel_is_area=first.pixel_is_area, + vertical_unit_assumed=vertical_unit_assumed, + ) + + def _snap(cells: float) -> float: """A box edge within `ALIGN_TOLERANCE` of a node is on it, so float noise in a spacing like 0.1 does not grow the window by a line.""" @@ -280,26 +358,35 @@ def _placement(p: _Placed, r0: int, c0: int, r1: int, c1: int) -> TilePlacement: meta=m, canvas=IndexWindow(row0=rs - r0, col0=cs - c0, rows=rows, cols=cols), source=IndexWindow(row0=rs - p.row, col0=cs - p.col, rows=rows, cols=cols), + dtype=p.footprint.dtype, ) -def _check_coverage(meta: RasterMeta, tiles: tuple[TilePlacement, ...], needed: Any) -> None: - """Refuse a needed node no selected tile covers (R4.5, per node).""" +def _uncovered(meta: RasterMeta, tiles: tuple[TilePlacement, ...], needed: Any) -> str | None: + """Why a needed node no tile covers refuses the request (R4.5, per node), + or None. A mask, a count and per-axis reductions: no per-node index or + coordinate array unless `needed` must be tested node by node (S1).""" uncovered = np.ones((meta.rows, meta.cols), dtype=bool) for t in tiles: c = t.canvas uncovered[c.row0 : c.row0 + c.rows, c.col0 : c.col0 + c.cols] = False - rows, cols = np.nonzero(uncovered) - xs = meta.x_min + cols * meta.delta_x - ys = meta.y_max - rows * meta.delta_y - if needed is not None: - inside = shapely.intersects_xy(needed, xs, ys) - xs, ys = xs[inside], ys[inside] - if len(xs): - raise MosaicError( - f"{len(xs)} nodes the request needs are in no tile: x {_num(xs.min())} to " - f"{_num(xs.max())}, y {_num(ys.min())} to {_num(ys.max())} (EPSG:{meta.epsg})" + if needed is not None and uncovered.any(): + rows, cols = np.nonzero(uncovered) + outside = ~shapely.intersects_xy( + needed, meta.x_min + cols * meta.delta_x, meta.y_max - rows * meta.delta_y ) + uncovered[rows[outside], cols[outside]] = False + count = np.count_nonzero(uncovered) + if not count: + return None + rows_hit = np.flatnonzero(uncovered.any(axis=1)) + cols_hit = np.flatnonzero(uncovered.any(axis=0)) + x0, x1 = (meta.x_min + cols_hit[i] * meta.delta_x for i in (0, -1)) + y1, y0 = (meta.y_max - rows_hit[i] * meta.delta_y for i in (0, -1)) + return ( + f"{count} nodes the request needs are in no tile: x {_num(x0)} to {_num(x1)}, " + f"y {_num(y0)} to {_num(y1)} (EPSG:{meta.epsg})" + ) def _loaded(placement: TilePlacement, load: Callable[[str], DemTile]) -> DemTile: @@ -317,10 +404,12 @@ def _merge( array: npt.NDArray[Any], nodata: float | None, merged: list[TilePlacement], + load: Callable[[str], DemTile], ) -> None: """R5's overlap rule, per node: valid beats NoData; two valid values must be equal (`==`); NoData against NoData keeps the sentinel over NaN, in - every order.""" + every order. A refusal names the merged tile whose value the incoming one + disagrees with, re-loading candidates (B2): the request is refused anyway.""" c, s = placement.canvas, placement.source region = canvas[c.row0 : c.row0 + c.rows, c.col0 : c.col0 + c.cols] incoming = array[s.row0 : s.row0 + s.rows, s.col0 : s.col0 + s.cols] @@ -332,11 +421,17 @@ def _merge( clash = old_valid & new_valid & (region != incoming) if clash.any(): row, col = (int(v) for v in np.argwhere(clash)[0]) + held = region[row, col] other = next( m.name for m in merged if m.canvas.row0 <= c.row0 + row < m.canvas.row0 + m.canvas.rows and m.canvas.col0 <= c.col0 + col < m.canvas.col0 + m.canvas.cols + and _loaded(m, load).array[ + m.source.row0 + c.row0 + row - m.canvas.row0, + m.source.col0 + c.col0 + col - m.canvas.col0, + ] + == held ) largest = float(np.max(np.abs(region[clash].astype(np.float64) - incoming[clash]))) raise MosaicError( From 5e3514f321c9324a2b47e3deff779fb51a84fb15 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 23:34:20 +0200 Subject: [PATCH 16/28] 15a docs: re-point the geotiff.py citations shifted by read_header geotiff.py:198 -> 207 and :336 -> 345 (12-dem-to-mesh.md), :96-131 -> 105-140 (15-dem-mosaic.md), each re-read as a quotation; and read_meta now reaches _header through read_header. Co-Authored-By: Claude Opus 5.5 --- docs/increments/12-dem-to-mesh.md | 4 ++-- docs/increments/15-dem-mosaic.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/increments/12-dem-to-mesh.md b/docs/increments/12-dem-to-mesh.md index 8b4820f0..af0ea269 100644 --- a/docs/increments/12-dem-to-mesh.md +++ b/docs/increments/12-dem-to-mesh.md @@ -68,7 +68,7 @@ fixture is decoded. triangles, 3.0 s. 6. **Without `imagecodecs`, `decode_dem` refuses the fixture** with a `GeoTiffError` that names Compression (259) = 5 and points at the `codecs` - extra (`src_python/tin_engine/io/geotiff.py:198`). Nothing new is needed + extra (`src_python/tin_engine/io/geotiff.py:207`). Nothing new is needed for the no-extra case beyond turning that error into a usage error. ## Rulings @@ -179,7 +179,7 @@ section), without changes: `keep_alive`. - The NoData value is converted to `T` exactly once. `decode_dem` already refuses a sentinel the cell type cannot hold - (`src_python/tin_engine/io/geotiff.py:336`), so the conversion is exact. + (`src_python/tin_engine/io/geotiff.py:345`), so the conversion is exact. - `sample(view, points)` takes a float64 `(N, 2)` array and returns `(z, valid)`. It releases the GIL. - `_core.pyi`: the class, the factory and `sample`. diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 3d3205c4..57f1b59c 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -390,8 +390,8 @@ assemble(plan, load) -> Mosaic pixels, no files - [15a] Carried from the parked R3: `_header(tif, nodata) -> (RasterMeta, dtype)` holds everything `decode_dem` does before `page.asarray()` - (now `_header` itself, `io/geotiff.py:96-131`). `read_meta(source, *, nodata=None)` calls it and - returns, and `decode_dem` calls it and then decodes. The header phase keeps + (now `_header` itself, `io/geotiff.py:105-140`). `read_meta(source, *, nodata=None)` calls it (through + `read_header`, which also returns the decoded dtype, S2) and returns, and `decode_dem` calls it and then decodes. The header phase keeps the codec refusal. - [15d] `decode_dem(source, *, window=None)`: with an `IndexWindow`, decode only the TIFF blocks (or strips) that meet it, and return a `DemTile` of the From d34d79d82728a0ad299f6f270e08b838909189cf Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Sun, 27 Sep 2026 23:44:28 +0200 Subject: [PATCH 17/28] 15a: the ROADMAP row no longer claims the acceptance box meshes; measured LOC; a paragraph re-wrapped Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- docs/increments/15-dem-mosaic.md | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b74ffbad..c1fb1069 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -44,7 +44,7 @@ increment that most needs a picture to check against | — | `tools/bench.py`: the 1 m benchmark and the thread-scaling sweep from one checked-in command, with power state, quality and commit recorded per run; the one-off scripts in `docs/benchmarks/2026-09-26/` are its specification. Rule 2's acceptance run needs it | shipped with branch `tools-bench`'s PR | `docs/benchmarks/bench-py.md` | | — | The serial phase: profile refine's serial insert-and-flip phase, then parallelise what the profile blames. Scaling tops out at about 2.0-2.2×. Profiled 2026-09-27: serial part about a third of 1-thread refine, mostly Lawson legalisation; the scan stops near 5× from load imbalance | profiled; designed as increment 21 (Ola's rulings 2026-09-27: L1 determinism, one path, at most 2 % more triangles). **21a shipped with branch `increment21a-quick-wins`'s PR**: dynamic scan blocks, active merge, reused flip stack; mesh bit-identical; on AC refine -10 to -15 % at 8 threads, ceiling 2.1x -> 2.3x (`docs/benchmarks/2026-09-27/21a-acceptance.md`). **21b shipped with branch `increment21b-lattice-incircle`'s PR**: an int64 lattice incircle answers 99.97-100 % of refine's incircle tests; mesh bit-identical; on battery refine -15 % at 8 threads, ceiling 2.3x -> 2.5x (`docs/benchmarks/2026-09-27/21b-acceptance.md`). **21c measured** (`docs/benchmarks/2026-09-27/21c/README.md`): option C costs 6.5-10 % more triangles; A1 about 1 %; A0 is bit-identical (on these two inputs, not proven), and with evaluate-once is modelled at only 7-11 % faster refine at 8 threads and slower at 4. **21d is deferred** (Ola, 2026-09-27: "Review and push 21c, then basin-work"), behind the work in "Order of work" below; domain decomposition parallelises scan and split together | `docs/increments/21-parallel-refine.md`, `docs/benchmarks/2026-09-27/serial-profile/README.md` | | — | Release hardening: measure libc++'s `_LIBCPP_HARDENING_MODE_FAST` (and libstdc++'s assertions on the GCC leg), which bounds-check `std::vector`, `span` and the like in Release, on the 1 m benchmark; switch it on if the cost is small. Today an out-of-range index in shipped code the tests miss is undefined behaviour and crashes Python (Ola, 2026-09-27: "I'm surprised we don't have proper memory control"; CI's ASan/UBSan/TSan cover what the tests reach) | to measure (Ola, 2026-09-27); 21d, which it was placed after, is deferred behind the basin work | none yet | -| 15 | A DEM in several tiles, and the domain in its own CRS: 15a Norway, many tiles in one projected CRS (Ola's 254 DTM10 UTM33 tiles; only the selected tiles must share a lattice, since 8 of them sit half a cell off); 15b the domain polygon reprojected into the DEM's CRS; 15c and 15d the São Francisco basin (geographic DEMs meshed in the DEM's own lattice frame, not resampled; window decoding and memory) | **15a shipped with branch `increment15-dem-mosaic`'s PR**: `--dem DIR` or several files, `--bbox`, tiles selected and stitched on one lattice, Q1-Q5 refusals; a 2×2-tile box of Ola's archive meshes in 1.8 s. 15b next (Ola, 2026-09-27: Norway first) | `docs/increments/15-dem-mosaic.md` | +| 15 | A DEM in several tiles, and the domain in its own CRS: 15a Norway, many tiles in one projected CRS (Ola's 254 DTM10 UTM33 tiles; only the selected tiles must share a lattice, since 8 of them sit half a cell off); 15b the domain polygon reprojected into the DEM's CRS; 15c and 15d the São Francisco basin (geographic DEMs meshed in the DEM's own lattice frame, not resampled; window decoding and memory) | **15a shipped with branch `increment15-dem-mosaic`'s PR**: `--dem DIR` or several files, `--bbox`, tiles selected and stitched on one lattice, Q1-Q5 refusals; a box over tiles whose overlaps agree meshes (the design's acceptance box is refused: real DTM10 overlaps disagree by up to 25 m, which Ola's Q1 refuses; the overlap policy is open, 2026-09-27). 15b next (Ola, 2026-09-27: Norway first) | `docs/increments/15-dem-mosaic.md` | | 16b | Interior polygons and polylines as constraints ("terrain polygons": lakes, land cover, roads, rivers): `--features PATH`, a GeoJSON `FeatureCollection`, each feature naming a vocabulary property; closed or open `Breakline`s, crossings noded, off-node vertices with bilinear z. **Its working example is real data** (Ola, 2026-09-27): CORINE Land Cover 2018 over the benchmark tile `7908_3_10m_z33.tif`. The source is Ola's local copy, `rasputin_data/corine_sql/.../U2018_CLC2018_V2020_20u1.gpkg` (8.2 GB, EPSG:3035, a sibling of this repository, which also holds the 254-tile DTM10 archive for gap 6). It reads without GDAL: sqlite3 over its R-tree, the GeoPackage blob header stripped, `shapely.wkb`, then `pyproj` to 25833, so CRS stops in Python as before. Probed 2026-09-27 in 0.2 s: 60 polygons in 8 classes (heath, bare rock, sparse vegetation, bogs, intertidal flats, water, sea, urban), 11 068 vertices clipped to the tile, median segment 54 m against 10 m cells. The EEA's public ArcGIS service (`image.discomap.eea.europa.eu`, `Corine/CLC2018_WM`) returns the same 11 068 clipped vertices and is the route for anyone without the file. What it forces on 16b's design: neighbouring polygons share their boundaries, so each shared edge arrives twice; the polygons run past the domain and must be clipped; the extract is committed as a fixture with the Copernicus attribution. Placed before 20c because 20c may split constraint segments and should be designed and measured on inputs that have interior ones | designed in 16's R6, no increment file yet; after the serial phase, before 20c | `docs/increments/16-domain-polygon.md` (R6) | | 20c | Soft quality criterion: a penalty that each Steiner node or constraint split must pay for in angle gained, instead of 20's hard 25°; applied at the start and during DEM refinement; may split constraint segments when that improves the mesh. Ola's rulings on 20's C1-C3 | to design after 16b (`@architect` measures cost against 20 first) | `docs/increments/20-start-quality.md` (Ola's rulings) | | — | Auto-catchment: the watershed upstream of a coordinate, computed from the DEM and handed to `--domain`, so a catchment no longer has to be supplied as a file (Ola, 2026-09-27: "not far into the future"). The textbook route is depression handling (Priority-Flood, Barnes, Lehman and Mulla 2014), D8 flow directions (O'Callaghan and Mark 1984) and accumulation, the pour point snapped to the strongest flow nearby, the upstream cells traced and their outline turned into a polygon; the literature check is `@architect`'s. Open for its design: whether it runs in the C++ core (a 10 m tile is 25 M cells); how a stair-stepped cell outline becomes a domain polygon, which meets input coarsening; and that a real catchment crosses tile edges, so it needs gap 6 (a DEM in several tiles) first. Legacy has nothing on it (`grep -rliE "watershed|flow.?acc|flow.?dir|pour.?point|catchment" legacy` returns no files) | to design; after gap 6, which it needs; placed after 20c, can move ahead of it on Ola's word | none yet | diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 57f1b59c..8da34a37 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -390,9 +390,10 @@ assemble(plan, load) -> Mosaic pixels, no files - [15a] Carried from the parked R3: `_header(tif, nodata) -> (RasterMeta, dtype)` holds everything `decode_dem` does before `page.asarray()` - (now `_header` itself, `io/geotiff.py:105-140`). `read_meta(source, *, nodata=None)` calls it (through - `read_header`, which also returns the decoded dtype, S2) and returns, and `decode_dem` calls it and then decodes. The header phase keeps - the codec refusal. + (now `_header` itself, `io/geotiff.py:105-140`). `read_meta(source, *, + nodata=None)` calls it (through `read_header`, which also returns the + decoded dtype, S2) and returns, and `decode_dem` calls it and then decodes. + The header phase keeps the codec refusal. - [15d] `decode_dem(source, *, window=None)`: with an `IndexWindow`, decode only the TIFF blocks (or strips) that meet it, and return a `DemTile` of the window, with its own `x_min`/`y_max`. Both archives are tiled 512 × 512 (N1, @@ -813,6 +814,7 @@ order. 15c and 15d follow later. | | `dem_input.py`: `DemRequest`, `open_dem` | 35 | | | | `cli.py`: `--dem` list, `--bbox`, refusals, fields | 45 | | | | **15a total** | **375** | **520** | +| | *15a as built, measured at review (branch diff, CLAUDE.md §2 unit): 533, of which `mosaic.py` 325 against 200 estimated* | | | | **15b** | **Norway: the domain in its own CRS** | | | | | `crs.py`: `parse_crs`, `reprojector` | 30 | | | | `domain.py`: `crs: str`, `to_crs`, extent against coverage; `check_crs` removed | 40 | | From d0517f6957fa878927627f420a9467f1b8b83e5e Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 07:24:00 +0200 Subject: [PATCH 18/28] 15a: Ola's Q1 revised: disagreeing overlaps split down the middle and reported; N3 and R5 corrected Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 37 ++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 8da34a37..e222cf6e 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -27,6 +27,26 @@ are designed, not implemented; Q6-Q10 are open. Written by `@architect` before is refused; a request inside either lattice is meshed. Q6-Q10 (the basin) wait until after the Norwegian sub-increments. +- **2026-09-28, Q1 revised: overlaps that disagree are split down the middle + ("b", Ola).** Measured first: in 60 sampled neighbour pairs of Ola's + archive, the 21 made on the same export date agree bit for bit and 18 of the + 39 made on different dates disagree (mean difference about 0, median 0 on + flat ground, growing with slope, up to 30 m on cliffs). Kartverket's + metadata explains it: the current 10 m model is exported from NDH laser data + where it exists, supplemented by the 2013 contour-based DTM10 (±2-6 m), and + tiles were exported one by one as laser coverage grew. Not land uplift + (mm/yr) and not glaciers. The rule: + - each node of an overlap takes the value of the tile whose **interior it + lies deepest in** (the largest distance, in nodes, to that tile's own + nearest border), **ties by tile name**; NoData still loses to a valid + value; + - disagreement is **no longer refused**. The run **reports each disagreeing + seam** (the two tiles, the number of nodes that differ, the largest and + the median difference) in `--stats` and in a `dem_seams` file field, so a + 25 m jump cannot pass unnoticed; + - the result stays independent of tile order. + Ola will also download a fresh single-date set from hoydedata.no, whose + overlaps should agree. - **2026-09-27, Q5 as read after review ("Yes", to the main session's proposal):** @reviewer found that DTM10's 51-node overlaps make a box that only reaches into a half-cell tile's overlap strip select that tile and be @@ -83,7 +103,7 @@ What is carried across, and what changes: | R3: `read_meta` split out of `decode_dem` | carried (R3); window decoding added in 15d | an ANADEM tile is 2.5 GiB | | R4: every tile in the directory on one lattice, or refuse | **changed** (R4): only the **selected** tiles must share a lattice | 8 of Ola's 254 tiles are half a cell off (N2); the parked rule refuses the whole archive | | R4: canvas origin from the tiles as decoded | **changed** (R4): each lattice has a **reference node**, and every node has a global index | frame coordinates must not depend on the window, for domain decomposition (R12) | -| R5: valid beats NoData, disagreement refused, order-independent | carried (R5), now **with real data behind it** | DTM10 (N3) and ANADEM (B4) overlaps agree bit for bit | +| R5: valid beats NoData, disagreement split down the middle and reported (Q1 revised), order-independent | carried (R5), now **with real data behind it** | DTM10 (N3) and ANADEM (B4) overlaps agree bit for bit | | R5: gaps stay NaN | **changed** (R5): a node the request needs that no tile covers is refused | Ola's ruling, `18-row-span-scan.md` R6: "a missing tile inside the extent is a data error" | | R6: `--bbox` in the DEM's CRS, no transform | carried (R6); the domain is transformed in 15b (R9) | "inputs in their own CRS" | | R6: `GeoPolygon` lands with the clip | superseded: 16 shipped `DomainPolygon`; 15b gives it a transform | | @@ -126,7 +146,9 @@ They are measurement scripts, not production code, and nothing imports them. are at x = …745 instead of …750, and they are 5052 wide and mostly 5053 high. They look like a second production run. Under the parked R4 (whole directory on one lattice) `--dem DIR` would refuse the whole archive. -- **N3. Neighbours overlap by 51 nodes, and the overlaps agree.** A 5051-node +- **N3. Neighbours overlap by 51 nodes, and same-date overlaps agree.** + (Corrected 2026-09-28: tiles exported on different dates can disagree; see + "Ruled by Ola", Q1 revised.) A 5051-node tile is 50 km plus 510 m, so each edge neighbour shares 51 rows or columns (a few share 52 or 53; one pair 53). Over 4 neighbour pairs of the first tile, every overlapping cell is equal: 257 601 of 257 601 on each edge @@ -471,10 +493,10 @@ Carried from the parked R5 almost unchanged. - one value NoData (NaN or the sentinel), the other valid: **valid wins**; - both NoData: NoData; - both valid and equal (`==`): accepted; - - both valid and different: **refused** (`refuses_overlap_disagreement`), - naming both tiles, the number of disagreeing nodes and the largest - difference. That is the parked U1 (a), now Q1, with measurements: DTM10 - (N3) and ANADEM (B4) overlaps agree bit for bit where sampled. + - both valid and different: **the tile the node lies deepest in wins**, ties + by name, and the seam is reported (Q1 revised, 2026-09-28). Q1 first + refused this; real DTM10 overlaps from different export dates disagree, + so refusing blocked much of the archive. - The result does not depend on tile order (M10). - **One tile, no bounds: the loaded `DemTile` is returned as it is.** No canvas, no copy, so `--dem file.tif` stays bit-identical to today, memory @@ -1145,7 +1167,8 @@ Q1-Q5 are about Norway and are needed before 15a starts. Q6-Q10 are about the basin and can wait until after Norway. **Q1 (the parked U1). Two tiles give different valid values at the same node.** -Now measured: DTM10 overlaps agree bit for bit on 4 of 869 pairs (N3), and +Now measured: DTM10 overlaps agree bit for bit on 4 of 869 pairs (N3; but see +Q1 revised: different-date pairs disagree), and ANADEM's on 3 seams (B4). But the committed benchmark tile and the archive's tile of the same name differ at 910 706 nodes (N5), so two releases in one directory is a real case. From 0c97ac9df95f1024558a8a4b3067be129a1373d6 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 07:38:59 +0200 Subject: [PATCH 19/28] 15a tests: overlaps split down the middle and reported (Ola's Q1 revised) Test amendment after green ff7cc8d, per docs/increments/README.md: Ola revised Q1 on 2026-09-28. Real DTM10 overlaps exported on different dates disagree (18 of 39 sampled pairs), so refusing them blocked much of the archive. The new rule: each overlap node takes the value of the tile it lies deepest in (distance in nodes to that tile's own nearest border), ties to the name that sorts first, NoData still loses to a valid value; nothing is refused; each disagreeing pair is reported in Mosaic.seams / DemInput.seams, the dem_seams file field and a "DEM seams" --stats section. New: TestQ1DeepestInterior and TestQ1SeamReport (test_mosaic.py), TestQ1Seams (test_cli_mesh_mosaic.py), the real seam meshing like its stitched single file, and an oracle in mosaic_fixtures.py that places tiles by coordinates. Changed, because they pinned the old refusal: - TestM8Overlaps: the refusal test with count and largest now asserts the report (plus the median and the value each node takes); B2's "names the disagreeing tile" now asserts the report names b | c and not a; one ulp and the sentinel case are reported instead of refused. - TestC4Refusals.test_an_overlap_disagreement is now TestQ1Seams.test_an_overlap_disagreement_meshes_and_is_recorded. - test_dem_input: the real seam shifted by one cell is reported and split, not refused; the agreeing real seam has no seams. - test_cli_mesh_mosaic TestRealDtm10: the agreeing seam records "none". Names and formats pinned in 15-dem-mosaic.md, "Pinned by the red suite (15a)"; I3 and the disagreement-message bullet corrected. Red at this commit: 38 tests, all in the three amended files. Green against an uncommitted scratch implementation; test_mosaic.py killed 16 of 16 mutants of it. Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 54 +++- tests/python/mosaic_fixtures.py | 108 +++++++ tests/python/test_cli_mesh_mosaic.py | 147 ++++++++- tests/python/test_dem_input.py | 46 ++- tests/python/test_mosaic.py | 451 ++++++++++++++++++++++++--- 5 files changed, 747 insertions(+), 59 deletions(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index e222cf6e..9b4b1e64 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -752,8 +752,10 @@ lazily inside the parallel scan. or snapped. - **I2. Order independence.** `plan_mosaic` and `assemble` give the same result for every permutation of the footprints. -- **I3. Valid beats NoData; disagreement refuses.** No overlapping node is ever - silently chosen between two different valid values. +- **I3. Valid beats NoData; disagreement is decided and reported** (Q1 + revised, 2026-09-28; first "disagreement refuses"). No overlapping node is + ever chosen between two different valid values without the pair appearing + in the seam report. - **I4. Coverage.** Every node the request needs comes from a tile. NaN filler exists only outside the needed region. - **I5. Split and re-stitch is the identity.** A grid cut into tiles @@ -1000,9 +1002,10 @@ failing M1's area-registered case. - a registration difference: `registration`; - a NoData difference: `nodata` and both values, with `None` for an absent sentinel; - - an overlap disagreement: both names — the two tiles whose *values* - disagree, not the first tile that merely covers the node (B2) — the count - as a whole number, and the largest difference; + - an overlap disagreement: *no longer a refusal* (Q1 revised); what it + named — the two tiles whose *values* disagree, not a tile that merely + covers the node (B2), the count and the largest difference — is now the + seam report's (below); - uncovered nodes: the node bounding box of the uncovered nodes, with each coordinate written out, not in scientific notation; - a changed tile: `changed since it was listed`; @@ -1041,6 +1044,47 @@ test amendment that follows green `ff7cc8d`: - **The edge snap (S3)** has its test: at spacing 0.1, the box edges 0.3, 0.7 and 0.9 add no node line. +**Amended for Ola's Q1 revised (2026-09-28).** Pinned by the test amendment +that follows it (`15a tests: overlaps split down the middle and reported`): +`TestQ1DeepestInterior` and `TestQ1SeamReport` in `test_mosaic.py`, four +rewritten tests of its `TestM8Overlaps`, `TestQ1Seams` in +`test_cli_mesh_mosaic.py`, and the real-seam cases of `test_dem_input.py` and +`test_cli_mesh_mosaic.py`. All were run green against a scratch implementation +that was not committed; `test_mosaic.py` killed 16 of 16 mutants of it. + +- **Depth** of a node in a tile is `min(r, c, rows - 1 - r, cols - 1 - c)` in + the tile's own indices, `rows x cols` being the **whole tile's** `meta` — + not the window a `--bbox` uses of it, and not the mosaic's. 0 on the + border. Of the tiles holding a **valid** value at a node, the deepest + gives it; equal depths go to the tile whose name sorts **first** (Python + `str` order, as the plan's). NoData against NoData is unchanged (two NaNs + NaN, two sentinels the sentinel, NaN against the sentinel either, the same + in every order). So one shared line (point-registered neighbours) is all + ties, and a tile wholly inside a bigger one takes only the ties. +- **`Seam`** in `tin_engine.mosaic`, with `first`, `second` (names, + `first < second`), `nodes` (int), `largest` and `median` (float). + `Mosaic.seams` and `DemInput.seams` are tuples of them, sorted by + `(first, second)`, empty for one tile and when every overlap agrees. +- A seam is a **pair** of tiles. `nodes` counts the mosaic's nodes (inside the + window, never a node a `--bbox` leaves out) where both hold a valid value + and the two differ (`!=`, so one ulp counts). `largest` and `median` are of + `|a - b|` over those nodes only, in float64; the median of an even count is + the mean of the middle two (`np.median`). A node in three tiles counts once + in each disagreeing pair. Pairs that agree are not listed. +- **`dem_seams`** (file field) is recorded exactly when `dem_tiles` is: + `none` when no pair disagrees, else one entry per pair in `seams` order, + `; `-joined and escaped like `dem_tiles`: + ` | : nodes , max , median `, for + example `ne.tif | nw.tif: nodes 1, max 4, median 4`. One entry per + disagreeing pair, not per node, so it stays small. +- **`--stats`** has a `## DEM seams` section, between `## Sizes` and + `## Quality (plan view, x/y)`, only when some pair disagrees: a table + `| tile | tile | nodes | max | median |`, one row per entry, numbers + formatted as in `dem_seams`. +- Not pinned: a stderr line for a disagreeing seam, a cap on the number of + entries, and where the report is computed (the scratch kept each pair's + overlap strips, which needs no second load). + **`io/models.py`.** `DemTile._adopt(meta, array)` is a classmethod. It raises `ValueError` on a shape, dtype, ndim or non-C-contiguous mismatch, sets the **passed** buffer read-only, and keeps it without a copy. Only `io/models.py` diff --git a/tests/python/mosaic_fixtures.py b/tests/python/mosaic_fixtures.py index 8719c503..48d01818 100644 --- a/tests/python/mosaic_fixtures.py +++ b/tests/python/mosaic_fixtures.py @@ -155,3 +155,111 @@ def footprints(footprint: Callable[..., Any], tiles: Mapping[str, DemTile]) -> l def same_array(a: np.ndarray, b: np.ndarray) -> bool: """Equal bit for bit: dtype, shape and bytes (so NaN matches NaN, -0 != 0).""" return a.dtype == b.dtype and a.shape == b.shape and a.tobytes() == b.tobytes() + + +# --------------------------------------------------------------------------- +# Ola's Q1 revised (2026-09-28): the oracle for overlaps and seams +# --------------------------------------------------------------------------- + + +def own_depth(rows: int, cols: int) -> np.ndarray: + """Each node's distance, in nodes, to its own tile's nearest border. + + `min(r, c, rows - 1 - r, cols - 1 - c)`: 0 on the border. The tile's own + border is its whole grid (`meta.rows x meta.cols`), never the window a + request uses of it and never the mosaic's. + """ + r, c = np.indices((rows, cols)) + return np.minimum.reduce([r, c, rows - 1 - r, cols - 1 - c]) + + +def on_canvas( + tiles: Mapping[str, DemTile], grid: RasterMeta +) -> dict[str, tuple[np.ndarray, np.ndarray]]: + """Each tile's valid values (float64) and own depths on `grid`'s nodes. + + NaN and -1 where the tile does not cover the node or holds NoData there + (NaN, or its sentinel). Tiles are placed by their coordinates, so this + shares no index arithmetic with `plan_mosaic`. + """ + out: dict[str, tuple[np.ndarray, np.ndarray]] = {} + for name, tile in tiles.items(): + m = tile.meta + row0 = round((grid.y_max - m.y_max) / grid.delta_y) + col0 = round((m.x_min - grid.x_min) / grid.delta_x) + value = np.full((grid.rows, grid.cols), np.nan) + depth = np.full((grid.rows, grid.cols), -1) + array = np.asarray(tile.array, dtype=np.float64) + valid = ~np.isnan(array) + if m.nodata is not None: + valid &= array != m.nodata + tile_depth = own_depth(m.rows, m.cols) + for i, j in zip(*np.nonzero(valid), strict=True): + r, c = row0 + int(i), col0 + int(j) + if 0 <= r < grid.rows and 0 <= c < grid.cols: + value[r, c], depth[r, c] = array[i, j], tile_depth[i, j] + out[name] = (value, depth) + return out + + +def deepest_interior(tiles: Mapping[str, DemTile], grid: RasterMeta) -> np.ndarray: + """The Q1-revised mosaic, node by node: of the tiles holding a valid value, + the one the node lies deepest in; ties to the name that sorts first. + NaN where no tile holds a valid value.""" + placed = on_canvas(tiles, grid) + expected = np.full((grid.rows, grid.cols), np.nan) + for r in range(grid.rows): + for c in range(grid.cols): + held = [(-d[r, c], name) for name, (v, d) in placed.items() if d[r, c] >= 0] + if held: + expected[r, c] = placed[min(held)[1]][0][r, c] + return expected + + +def winners(tiles: Mapping[str, DemTile], grid: RasterMeta) -> list[list[str]]: + """The name of the tile each node's value is taken from, '' for none.""" + placed = on_canvas(tiles, grid) + return [ + [ + min(((-d[r, c], n) for n, (_, d) in placed.items() if d[r, c] >= 0), default=(0, ""))[1] + for c in range(grid.cols) + ] + for r in range(grid.rows) + ] + + +def seams_of( + tiles: Mapping[str, DemTile], grid: RasterMeta +) -> list[tuple[str, str, int, float, float]]: + """The seam report the rule asks for, pair by pair, sorted by name: + `(first, second, nodes, largest, median)` over the mosaic's nodes where + both tiles hold a valid value and the two differ; `|difference|` in + float64, the median of an even count the mean of the middle two. Pairs + that agree everywhere are left out.""" + placed = on_canvas(tiles, grid) + out = [] + names = sorted(placed) + for i, a in enumerate(names): + for b in names[i + 1 :]: + va, vb = placed[a][0], placed[b][0] + differ = ~np.isnan(va) & ~np.isnan(vb) & (va != vb) + if differ.any(): + gaps = np.abs(va[differ] - vb[differ]) + out.append((a, b, int(differ.sum()), float(gaps.max()), float(np.median(gaps)))) + return out + + +def seams(result: Any) -> list[tuple[str, str, int, float, float]]: + """A `Mosaic`'s (or `DemInput`'s) seam report as plain tuples.""" + return [(s.first, s.second, s.nodes, s.largest, s.median) for s in result.seams] + + +def shifted_by(tile: DemTile, offset: float) -> DemTile: + """`tile` with `offset` added to every value, NoData kept: a tile that + disagrees with its neighbours everywhere, by exactly `offset`.""" + array = np.asarray(tile.array).copy() + valid = ~np.isnan(array) + if tile.meta.nodata is not None: + valid &= array != tile.meta.nodata + array[valid] += np.asarray(offset, dtype=array.dtype) + return DemTile(meta=tile.meta, array=array) diff --git a/tests/python/test_cli_mesh_mosaic.py b/tests/python/test_cli_mesh_mosaic.py index fe00d7c7..a89e79bd 100644 --- a/tests/python/test_cli_mesh_mosaic.py +++ b/tests/python/test_cli_mesh_mosaic.py @@ -80,6 +80,13 @@ def write_tiles(directory: Path, tiles: dict[str, DemTile]) -> list[Path]: return sorted(directory.iterdir()) +def report(tmp_path: Path, *args: str) -> str: + """`--stats -`'s Markdown, read from stdout apart from stderr and unflattened.""" + result = runner.invoke(app, ["mesh", *args, "--out", str(tmp_path / "m.vtk"), "--stats", "-"]) + assert result.exit_code == 0, result.output + return result.stdout + + def write_one(path: Path, tile: DemTile) -> Path: path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(tiff_of(tile).getvalue()) @@ -212,14 +219,6 @@ def test_half_a_cell_off(self, tmp_path: Path, source: DemTile) -> None: write_tiles(tmp_path / "shifted", tiles) self.refused(tmp_path, "--dem", str(tmp_path / "shifted"), says=("se.tif", "0.5 cell")) - def test_an_overlap_disagreement(self, tmp_path: Path, source: DemTile) -> None: - tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) - changed = np.array(tiles["ne.tif"].array) - changed[2, 0] += 4.0 # on the shared column - tiles["ne.tif"] = piece(source, 0, 5, 6, 13, array=changed) - write_tiles(tmp_path / "disagree", tiles) - self.refused(tmp_path, "--dem", str(tmp_path / "disagree"), says=("ne.tif", "nw.tif", "4")) - def test_a_hole_in_the_tile_set(self, tmp_path: Path) -> None: from mosaic_fixtures import blocks @@ -232,6 +231,112 @@ def test_an_empty_directory_names_it(self, tmp_path: Path) -> None: self.refused(tmp_path, "--dem", str(tmp_path / "empty_dir"), says=("empty_dir",)) +class TestQ1Seams: + """Ola's Q1 revised (2026-09-28), through the CLI: a disagreeing overlap + meshes, and each disagreeing pair is recorded in the `dem_seams` field + and, with `--stats`, in a "DEM seams" section. + + `dem_seams` is recorded whenever `dem_tiles` is: `none` when every + overlapping node agrees, else one entry per disagreeing pair, sorted, + `; `-joined, escaped like `dem_tiles`: + ` | : nodes , max , median `. + """ + + @staticmethod + def disagreeing(tmp_path: Path, source: DemTile, name: str = "nw.tif") -> Path: + """The quadrants, `ne.tif` 4.0 higher at one node of the shared column + (global (2, 6), in `ne.tif` and the tile named `name` only). Was + `TestC4Refusals.test_an_overlap_disagreement`'s directory.""" + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) + tiles[name] = tiles.pop("nw.tif") + changed = np.array(tiles["ne.tif"].array) + changed[2, 0] += 4.0 + tiles["ne.tif"] = piece(source, 0, 5, 6, 13, array=changed) + write_tiles(tmp_path / "disagree", tiles) + return tmp_path / "disagree" + + def test_an_overlap_disagreement_meshes_and_is_recorded( + self, tmp_path: Path, source: DemTile + ) -> None: + """Was `TestC4Refusals.test_an_overlap_disagreement`, which expected a + refusal naming both tiles and the 4. The same names and number are now + in the record, and the mesh is written.""" + vtk = run_vtk(tmp_path / "m.vtk", "--dem", str(self.disagreeing(tmp_path, source))) + assert field(vtk, "dem_seams") == "ne.tif | nw.tif: nodes 1, max 4, median 4" + assert field(vtk, "dem_tiles") == "ne.tif; nw.tif; se.tif; sw.tif" + + def test_agreeing_tiles_record_none(self, tmp_path: Path, mosaic_dir: Path) -> None: + vtk = run_vtk(tmp_path / "m.vtk", "--dem", str(mosaic_dir)) + assert field(vtk, "dem_seams") == "none" + + def test_one_file_records_no_seams(self, tmp_path: Path, source: DemTile) -> None: + vtk = run_vtk(tmp_path / "one.vtk", "--dem", str(write_one(tmp_path / "one.tif", source))) + assert "dem_seams" not in vtk.field_data + + def test_several_pairs_are_sorted_and_joined(self, tmp_path: Path) -> None: + """Each quadrant 1000 times its rank higher than the grid: every + overlap disagrees, by a constant. Six pairs, sorted by name; counts are + the overlaps' node counts (overlap 1 on a 9 x 13 grid: 5, 5, 7, 7, and + the corner node in the two diagonal pairs).""" + grid = whole(9, 13, array=terrain(9, 13)) + tiles = quadrants(grid, row_cut=4, col_cut=6, overlap=1) + for rank, name in enumerate(sorted(tiles), start=1): + t = tiles[name] + tiles[name] = DemTile(meta=t.meta, array=np.asarray(t.array) + 1000 * rank) + write_tiles(tmp_path / "all", tiles) + vtk = run_vtk(tmp_path / "m.vtk", "--dem", str(tmp_path / "all")) + assert field(vtk, "dem_seams") == "; ".join( + [ + "ne.tif | nw.tif: nodes 5, max 1000, median 1000", + "ne.tif | se.tif: nodes 7, max 2000, median 2000", + "ne.tif | sw.tif: nodes 1, max 3000, median 3000", + "nw.tif | se.tif: nodes 1, max 1000, median 1000", + "nw.tif | sw.tif: nodes 7, max 2000, median 2000", + "se.tif | sw.tif: nodes 5, max 1000, median 1000", + ] + ) + + def test_a_non_ascii_name_is_escaped(self, tmp_path: Path, source: DemTile) -> None: + directory = self.disagreeing(tmp_path, source, name="Ålesund.tif") + listed = next(p.name for p in directory.iterdir() if p.name.endswith("lesund.tif")) + escaped = listed.encode("ascii", "backslashreplace").decode("ascii") + vtk = run_vtk(tmp_path / "m.vtk", "--dem", str(directory)) + recorded = field(vtk, "dem_seams") + assert recorded.isascii() + first, second = ( + n.encode("ascii", "backslashreplace").decode("ascii") + for n in sorted([listed, "ne.tif"]) + ) # the file system may store the name decomposed, which sorts first + assert recorded == f"{first} | {second}: nodes 1, max 4, median 4" + assert escaped in recorded + + def test_stats_has_a_seams_section_listing_only_disagreeing_pairs( + self, tmp_path: Path, source: DemTile + ) -> None: + """After "Sizes", before "Quality": one table row per disagreeing pair.""" + lines = report(tmp_path, "--dem", str(self.disagreeing(tmp_path, source))).splitlines() + at = lines.index("## DEM seams") + assert lines.index("## Sizes") < at < lines.index("## Quality (plan view, x/y)") + rows = [] + for line in lines[at + 1 :]: + if line.startswith("## "): + break + if line.startswith("|"): + rows.append([cell.strip() for cell in line.strip("|").split("|")]) + assert rows == [ + ["tile", "tile", "nodes", "max", "median"], + ["---", "---", "---", "---", "---"], + ["ne.tif", "nw.tif", "1", "4", "4"], + ] + + def test_stats_has_no_seams_section_when_every_overlap_agrees( + self, tmp_path: Path, mosaic_dir: Path + ) -> None: + lines = report(tmp_path, "--dem", str(mosaic_dir)).splitlines() + assert "## Sizes" in lines + assert "## DEM seams" not in lines + + class TestC5Usage: """C5 and R11: `--dem` forms, and `--bbox`.""" @@ -333,8 +438,34 @@ def test_the_real_seam_meshes(self, tmp_path: Path) -> None: vtk = run_vtk(tmp_path / "seam.vtk", "--dem", str(DTM10 / "seam"), "--tolerance", "1") assert field(vtk, "dem_tiles") == "6400_1_10m_z33.tif; 6400_4_10m_z33.tif" assert field(vtk, "elevation_source").startswith("mosaic of 2 tiles, 256 x 563 nodes; ") + assert field(vtk, "dem_seams") == "none" # Ola's Q1 revised: it agrees assert np.isfinite(vtk.points).all() + def test_the_agreeing_real_seam_meshes_like_its_stitched_tile(self, tmp_path: Path) -> None: + """Ola's Q1 revised changes nothing where the overlap agrees: the seam + meshes exactly like one file holding west's 307 columns and east's + last 256, with west's georeferencing (the mesh before the change).""" + west_path = DTM10 / "seam" / "6400_4_10m_z33.tif" + east_path = DTM10 / "seam" / "6400_1_10m_z33.tif" + with tifffile.TiffFile(west_path) as tif: + page = tif.pages.first + tie, scale, west = page.tags[33922].value, page.tags[33550].value, page.asarray() + east = tifffile.imread(east_path) + assert np.array_equal(west[:, -51:], east[:, :51]) + stitched = micro_tiff( + np.ascontiguousarray(np.hstack([west, east[:, 51:]])), + tiepoint=(0.0, 0.0, 0.0, tie[3], tie[4], 0.0), + scale=tuple(scale), + geokeys=with_keys({GT_RASTER_TYPE: PIXEL_IS_AREA}), + nodata="-32767", + compression="deflate", + ) + single = tmp_path / "stitched.tif" + single.write_bytes(stitched.getvalue()) + expected = run_vtk(tmp_path / "one.vtk", "--dem", str(single), "--tolerance", "1") + got = run_vtk(tmp_path / "seam.vtk", "--dem", str(DTM10 / "seam"), "--tolerance", "1") + same_mesh(got, expected) + def test_the_two_lattices_are_refused_naming_both(self, tmp_path: Path) -> None: out = tmp_path / "x.vtk" code, output = invoke("--dem", str(DTM10 / "lattices"), "--out", str(out)) diff --git a/tests/python/test_dem_input.py b/tests/python/test_dem_input.py index c38693f3..295ae607 100644 --- a/tests/python/test_dem_input.py +++ b/tests/python/test_dem_input.py @@ -34,7 +34,17 @@ truncated_strip, with_keys, ) -from mosaic_fixtures import DX, X0, Y0, quadrants, same_array, whole +from mosaic_fixtures import ( + DX, + X0, + Y0, + deepest_interior, + quadrants, + same_array, + seams, + seams_of, + whole, +) from tin_engine.io.geotiff import decode_dem from tin_engine.io.models import DemTile @@ -221,18 +231,38 @@ def test_q1_the_agreeing_seam_is_accepted(self, di: ModuleType) -> None: assert opened.tile.meta.x_min == west.meta.x_min assert same_array(opened.tile.array[:, :307], west.array) assert same_array(opened.tile.array[:, 256:], east.array) + assert opened.seams == () # Ola's Q1 revised: an agreeing seam is not reported - def test_q1_the_seam_shifted_by_one_cell_is_refused( - self, di: ModuleType, mz: ModuleType, tmp_path: Path + def test_q1_the_seam_shifted_by_one_cell_is_reported_and_split( + self, di: ModuleType, tmp_path: Path ) -> None: - """The control (N3): one column off, the real overlap disagrees.""" + """The control (N3), under Ola's Q1 revised: was + `test_q1_the_seam_shifted_by_one_cell_is_refused`. One column off, the + real overlap (now 50 columns) disagrees. It is no longer refused: the + seam is reported, naming the two tiles, and each node takes the value + of the tile it lies deepest in. Both are checked against the + node-by-node oracle in `mosaic_fixtures`, and the split is checked by + hand on the middle row, where column depth decides.""" (tmp_path / "shifted").mkdir() (tmp_path / "shifted" / WEST.name).write_bytes(WEST.read_bytes()) _shifted_copy(EAST, tmp_path / "shifted" / EAST.name, 10.0) - with pytest.raises(mz.MosaicError) as info: - di.open_dem(request(di, tmp_path / "shifted")) - message = str(info.value) - assert WEST.name in message and EAST.name in message + opened = di.open_dem(request(di, tmp_path / "shifted")) + grid = opened.tile.meta + assert (grid.rows, grid.cols) == (256, 307 + 307 - 50) + tiles = {WEST.name: decoded(WEST), EAST.name: decoded(tmp_path / "shifted" / EAST.name)} + expected = seams_of(tiles, grid) + assert [s[:2] for s in expected] == [(EAST.name, WEST.name)] # 6400_1 sorts first + assert expected[0][2] > 0 + assert seams(opened) == expected + assert same_array( + opened.tile.array, deepest_interior(tiles, grid).astype(opened.tile.array.dtype) + ) + # Row 128: west's columns 257-306 are 49..0 deep, east's 0..49. The 25 + # nearer the west tile's interior are west's, the other 25 east's. + west, east = tiles[WEST.name].array, tiles[EAST.name].array + row = opened.tile.array[128] + assert same_array(row[257:282], west[128, 257:282]) + assert same_array(row[282:307], east[128, 25:50]) def test_q5_the_two_lattices_together_are_refused_naming_both( self, di: ModuleType, mz: ModuleType diff --git a/tests/python/test_mosaic.py b/tests/python/test_mosaic.py index 6ee23e28..d02dce84 100644 --- a/tests/python/test_mosaic.py +++ b/tests/python/test_mosaic.py @@ -13,6 +13,12 @@ accepts a disagreement, a coverage check that misses a gap); the handback of the red commit lists which test killed which. +AMENDED FOR OLA'S Q1 REVISED (2026-09-28). Overlaps that disagree are no +longer refused: each node takes the value of the tile it lies deepest in, ties +by name, and each disagreeing pair is reported in `Mosaic.seams` +(`TestQ1DeepestInterior`, `TestQ1SeamReport`, and four tests of +`TestM8Overlaps` that pinned the refusal and now pin the report). + HOW THIS FILE GOES RED. `tin_engine.mosaic` and `tin_engine.io.repository` are imported in module-scoped fixtures, as `test_bench.py` loads its tool, so while they are missing each test fails on its own with `ModuleNotFoundError` @@ -30,7 +36,7 @@ import re from pathlib import Path from types import ModuleType -from typing import Any +from typing import Any, ClassVar import numpy as np import pytest @@ -44,11 +50,15 @@ Y0, Loads, blocks, + deepest_interior, footprints, meta, piece, quadrants, same_array, + seams, + seams_of, + shifted_by, values, whole, ) @@ -875,9 +885,20 @@ def test_float32_stays_float32(self, build: Any) -> None: class TestM8Overlaps: - """M8, R5 and I3: valid beats NoData; disagreement refuses (Q1). + """M8, R5 and I3: valid beats NoData; a disagreement is not refused but + decided by depth and reported (Ola's Q1 revised, 2026-09-28). `w.tif` and `e.tif` cut from a 4 x 9 grid, sharing `overlap` columns. + With overlap 3 the shared columns are 4, 5 and 6, and each node's own + depth (`mosaic_fixtures.own_depth`) is, in `w.tif` then `e.tif`: + + rows 0 and 3: every node is on both tiles' top or bottom border, + 0 against 0, a tie, so `e.tif` (it sorts first); + rows 1 and 2: column 4 is 1 against 0, `w.tif`; column 5 is 1 + against 1, `e.tif`; column 6 is 0 against 1, `e.tif`. + + The four tests below that pinned the old refusal (Q1 as first ruled) now + pin the report; each says what it kept. """ ROWS, COLS = 4, 9 @@ -951,29 +972,55 @@ def test_equal_overlaps_are_accepted(self, build: Any) -> None: result, _ = build(self.tiles(source, arrays)) assert same_array(result.tile.array, source.array) + @pytest.mark.parametrize( + ("west_wins", "expected"), + [(False, (3, 2.5, 1.0)), (True, (4, 4.0, 1.75))], + ids=["odd-count", "even-count"], + ) @pytest.mark.parametrize("order", ["as-named", "reversed"]) - def test_a_disagreement_is_refused_naming_both_the_count_and_the_largest( - self, mz: ModuleType, plan: Any, refused: Any, order: str + def test_a_disagreement_is_reported_with_the_count_the_largest_and_the_median( + self, + mz: ModuleType, + plan: Any, + order: str, + west_wins: bool, + expected: tuple[int, float, float], ) -> None: + """Was `..._is_refused_naming_both_the_count_and_the_largest`. Same + planted differences; the run is no longer refused. It keeps: both names, + the count of differing nodes only (a NoData node in the overlap is not + one, nor are the agreeing ones), the largest |difference|, in either + order. New: the median, of the differing nodes only, and the value each + node takes. The median of an even count is the mean of the middle two. + """ source, arrays = self.pair(3) - arrays["e.tif"][0, 1] += 0.5 - arrays["e.tif"][2, 1] += 2.5 # the middle column of the overlap - arrays["e.tif"][3, 2] -= 1.0 + arrays["e.tif"][0, 1] += 0.5 # row 0, column 5: a tie, e.tif's value + arrays["e.tif"][2, 1] += 2.5 # row 2, column 5, mid-overlap: 1 against 1, e.tif + arrays["e.tif"][3, 2] -= 1.0 # row 3, column 6: |difference| is 1 arrays["w.tif"][1, 4] = np.nan # a NoData node in the overlap is not counted + if west_wins: + arrays["e.tif"][2, 0] += 4.0 # row 2, column 4: w.tif is deeper, its value tiles = self.tiles(source, arrays) planned = plan(tiles) if order == "reversed": planned = planned.model_copy(update={"tiles": tuple(reversed(planned.tiles))}) - message = refused(lambda: mz.assemble(planned, Loads(tiles)), "w.tif", "e.tif", "2.5") - names_number(message, 3) - - def test_the_refusal_names_the_tiles_whose_values_disagree( - self, mz: ModuleType, plan: Any, refused: Any + result = mz.assemble(planned, Loads(tiles)) + assert seams(result) == [("e.tif", "w.tif", *expected)] + taken = source.array.copy() + taken[0, 5] += 0.5 + taken[2, 5] += 2.5 + taken[3, 6] -= 1.0 + assert same_array(result.tile.array, taken) + + def test_the_report_names_the_tiles_whose_values_disagree( + self, mz: ModuleType, plan: Any ) -> None: - """B2: three tiles on one 3 x 3 grid, equal everywhere but node (0, 0), - where `a` is NoData, `b` 1.0 and `c` 5.0. The disagreement is between - b and c; `a` covers the node too, but its value is not in the clash. - In every assembly order.""" + """B2, kept in the report (was `test_the_refusal_names_...`). Three + tiles on one 3 x 3 grid, equal everywhere but node (0, 0), where `a` is + NoData, `b` 1.0 and `c` 5.0. The seam is b | c; `a` covers the node + too, but its value is not in the disagreement, so no seam names it. + Node (0, 0) is on all three borders: a tie, and of the two valid + values `b.tif`'s, whose name sorts first. In every assembly order.""" source = whole(rows=3, cols=3) tiles = {} for name, first in (("a.tif", np.nan), ("b.tif", 1.0), ("c.tif", 5.0)): @@ -982,32 +1029,360 @@ def test_the_refusal_names_the_tiles_whose_values_disagree( tiles[name] = piece(source, 0, 3, 0, 3, array=array) planned = plan(tiles) for order in itertools.permutations(planned.tiles): - message = refused( - lambda o=order: mz.assemble(planned.model_copy(update={"tiles": o}), Loads(tiles)), - "b.tif", - "c.tif", - "4", - ) - assert "a.tif" not in message, ([p.name for p in order], message) - names_number(message, 1) - - def test_one_ulp_is_a_disagreement(self, build: Any, refused: Any) -> None: - """`==`, not `isclose`: DTM10's and ANADEM's overlaps agree bit for bit (N3, B4).""" + result = mz.assemble(planned.model_copy(update={"tiles": order}), Loads(tiles)) + names = [p.name for p in order] + assert seams(result) == [("b.tif", "c.tif", 1, 4.0, 4.0)], names + assert result.tile.array[0, 0] == 1.0, names + + def test_one_ulp_is_a_disagreement(self, build: Any) -> None: + """`==`, not `isclose`: DTM10's and ANADEM's overlaps agree bit for bit + on one export date (N3, B4), so one ulp is reported. Overlap 1: the + shared column is on both tiles' borders, a tie, `e.tif`'s value.""" source, arrays = self.pair(1) value = arrays["e.tif"][2, 0] - arrays["e.tif"][2, 0] = np.nextafter(value, np.float32(np.inf), dtype=np.float32) - message = refused(lambda: build(self.tiles(source, arrays)), "w.tif", "e.tif") - names_number(message, 1) + bumped = np.nextafter(value, np.float32(np.inf), dtype=np.float32) + arrays["e.tif"][2, 0] = bumped + result, _ = build(self.tiles(source, arrays)) + ulp = float(bumped) - float(value) + assert seams(result) == [("e.tif", "w.tif", 1, ulp, ulp)] + assert result.tile.array[2, 4] == bumped - def test_a_disagreement_against_a_sentinel_tile_is_still_one( - self, build: Any, refused: Any - ) -> None: - """The sentinel is NoData only where a node holds it; elsewhere a value is a value.""" + def test_a_disagreement_against_a_sentinel_tile_is_still_one(self, build: Any) -> None: + """The sentinel is NoData only where a node holds it; elsewhere a value + is a value, reported and decided by depth like any other.""" source, arrays = self.pair(3, nodata=SENTINEL) - arrays["w.tif"][0, 4] = SENTINEL - arrays["w.tif"][0, 5] += 7.0 - message = refused(lambda: build(self.tiles(source, arrays)), "w.tif", "e.tif", "7") - names_number(message, 1) + arrays["w.tif"][0, 4] = SENTINEL # NoData: e.tif's value, not counted + arrays["w.tif"][0, 5] += 7.0 # a tie on row 0: e.tif's value + arrays["w.tif"][1, 4] += 3.0 # w.tif is deeper: its value + result, _ = build(self.tiles(source, arrays)) + assert seams(result) == [("e.tif", "w.tif", 2, 7.0, 5.0)] + taken = source.array.copy() + taken[1, 4] += 3.0 + assert same_array(result.tile.array, taken) + + +def taken_from(result: Any, source: np.ndarray, offsets: dict[str, float]) -> list[str]: + """Row strings naming, node by node, the tile each value came from. + + Every tile is `source` plus its own offset, so the offset identifies it; + a tile is written as its name's first letter. + """ + letter = {offset: name[0] for name, offset in offsets.items()} + taken = np.asarray(result.tile.array, dtype=np.float64) - source + return ["".join(letter[float(v)] for v in row) for row in taken] + + +def offset_tiles(tiles: dict[str, DemTile]) -> tuple[dict[str, DemTile], dict[str, float]]: + """Each tile plus 1000 times its rank by name: every overlap disagrees.""" + offsets = {name: 1000.0 * (i + 1) for i, name in enumerate(sorted(tiles))} + return {name: shifted_by(t, offsets[name]) for name, t in tiles.items()}, offsets + + +class TestQ1DeepestInterior: + """Ola's Q1 revised (2026-09-28): each overlap node takes the value of the + tile whose interior it lies deepest in (the largest distance, in nodes, to + that tile's *own* nearest border); ties go to the name that sorts first; + NoData still loses to a valid value; the result is independent of tile + order. + + Every tile is the source plus its own offset, so every overlap disagrees + and the offset names the tile a value came from. The expected maps are + written out by hand, and the whole array is also checked against + `mosaic_fixtures.deepest_interior`, a node-by-node oracle that places + tiles by coordinates and shares no code with `tin_engine.mosaic`. + """ + + ROWS, WIDTH = 7, 8 + + def two(self, overlap: int, west: str, east: str) -> tuple[DemTile, dict[str, DemTile]]: + """Two 7 x 8 tiles side by side sharing `overlap` columns; west + 1000, + east + 2000.""" + cols = 2 * self.WIDTH - overlap + source = whole(self.ROWS, cols) + return source, { + west: shifted_by(piece(source, 0, self.ROWS, 0, self.WIDTH), 1000.0), + east: shifted_by(piece(source, 0, self.ROWS, self.WIDTH - overlap, cols), 2000.0), + } + + #: The overlap columns only, row by row. W: the west tile is deeper; E: the + #: east; T: equally deep, so the name that sorts first. Rows 0 and 6 lie on + #: both tiles' borders. An odd overlap has a tie column down the middle; an + #: even one splits cleanly where the rows are deep enough (rows 2-4), and + #: rows 1 and 5, 1 deep, tie the middle two columns. + MAPS: ClassVar[dict[int, list[str]]] = { + 3: ["TTT", "WTE", "WTE", "WTE", "WTE", "WTE", "TTT"], + 4: ["TTTT", "WTTE", "WWEE", "WWEE", "WWEE", "WTTE", "TTTT"], + } + + @pytest.mark.parametrize("overlap", [3, 4], ids=["odd-tie-column", "even-midline"]) + @pytest.mark.parametrize( + ("west", "east"), [("w.tif", "e.tif"), ("a.tif", "b.tif")], ids=["east-first", "west-first"] + ) + def test_two_tiles_split_down_the_middle_ties_by_name( + self, build: Any, overlap: int, west: str, east: str + ) -> None: + source, tiles = self.two(overlap, west, east) + result, _ = build(tiles) + letter = {"W": west[0], "E": east[0], "T": min(west, east)[0]} + own = self.WIDTH - overlap + expected = [ + west[0] * own + "".join(letter[k] for k in row) + east[0] * own + for row in self.MAPS[overlap] + ] + assert taken_from(result, source.array, {west: 1000.0, east: 2000.0}) == expected + oracle = deepest_interior(tiles, result.tile.meta).astype(np.float32) + assert same_array(result.tile.array, oracle) + + def test_one_shared_line_is_all_ties(self, build: Any) -> None: + """Point-registered neighbours share one line, 0 deep in both: every + node is a tie, and the name that sorts first takes the whole line.""" + source, tiles = self.two(1, "w.tif", "e.tif") + result, _ = build(tiles) + taken = taken_from(result, source.array, {"w.tif": 1000.0, "e.tif": 2000.0}) + assert [row[self.WIDTH - 1] for row in taken] == ["e"] * self.ROWS + + def test_depth_is_to_the_tiles_own_border_not_the_requests_window(self, build: Any) -> None: + """A box of rows 2-4 and columns 4-8 cuts both tiles of the odd pair. + The window's first and last rows are neither tile's border, so the + answer is rows 2-4 of the full tiles' map (W T E in columns 5-7), not + the window's (all ties, `e.tif`, on its first and last row).""" + source, tiles = self.two(3, "w.tif", "e.tif") + box = (X0 + 4 * DX, Y0 - 4 * DY, X0 + 8 * DX, Y0 - 2 * DY) + result, _ = build(tiles, box) + assert (result.tile.meta.rows, result.tile.meta.cols) == (3, 5) + taken = taken_from(result, source.array[2:5, 4:9], {"w.tif": 1000.0, "e.tif": 2000.0}) + assert taken == ["wweee"] * 3 + + #: Rows 2-6, columns 7-11 of a 9 x 12 tile holding a 5 x 5 one flush with + #: its east border. The small tile's own depth never exceeds the big one's, + #: so it takes only the ties, and only when its name sorts first. + INSIDE: ClassVar[dict[str, list[str]]] = { + "a.tif": ["bbbba", "bbbaa", "bbaaa", "bbbaa", "bbbba"], + "s.tif": ["bbbbb"] * 5, + } + + @pytest.mark.parametrize("small", ["a.tif", "s.tif"], ids=["small-first", "small-last"]) + def test_unequal_sizes_measure_depth_to_each_tiles_own_border( + self, build: Any, small: str + ) -> None: + """Depth is to each tile's own border: measured to the mosaic's border + (here the big tile's), both would tie at every node.""" + source = whole(9, 12) + tiles = { + "b.tif": shifted_by(piece(source, 0, 9, 0, 12), 1000.0), + small: shifted_by(piece(source, 2, 7, 7, 12), 2000.0), + } + result, _ = build(tiles) + taken = taken_from(result, source.array, {"b.tif": 1000.0, small: 2000.0}) + assert [row[7:] for row in taken[2:7]] == self.INSIDE[small] + assert all(set(row) == {"b"} for row in taken[:2] + taken[7:]) + oracle = deepest_interior(tiles, result.tile.meta).astype(np.float32) + assert same_array(result.tile.array, oracle) + + def test_a_corner_of_three_tiles(self, build: Any) -> None: + """`a` 10 x 7 down the west, `b` 7 x 8 and `c` 6 x 8 stacked on the + east; rows 4-6, columns 4-6 are in all three. At (6, 6) `c` is 2 deep + and the others 0; at (5, 5) all three are 1 deep, so `a`.""" + source = whole(10, 12) + tiles, offsets = offset_tiles( + { + "a.tif": piece(source, 0, 10, 0, 7), + "b.tif": piece(source, 0, 7, 4, 12), + "c.tif": piece(source, 4, 10, 4, 12), + } + ) + result, _ = build(tiles) + assert taken_from(result, source.array, offsets) == [ + "aaaaaaabbbbb", + "aaaaaabbbbbb", + "aaaaaabbbbbb", + "aaaaaabbbbbb", + "aaaaaabbbbbb", + "aaaaaabbbbbb", + "aaaaaacccccb", + "aaaaaacccccc", + "aaaaaacccccc", + "aaaaaaaccccc", + ] + oracle = deepest_interior(tiles, result.tile.meta).astype(np.float32) + assert same_array(result.tile.array, oracle) + + #: Quadrants of a 10 x 12 grid cut at row 5 and column 6 with overlap 3: + #: 8 x 9, 8 x 6, 5 x 9 and 5 x 6 tiles, all four sharing rows 5-7 and + #: columns 6-8. Named so the name order is nw < ne < sw < se, and then its + #: reverse; the ties move with it, the deeper tile does not. + CORNERS: ClassVar[dict[str, list[str]]] = { + "abcd": [ + "aaaaaaaaabbb", + "aaaaaaaabbbb", + "aaaaaaaabbbb", + "aaaaaaaabbbb", + "aaaaaaaabbbb", + "aaaaaaaabbbb", + "aaaaaaaabbbb", + "acccccccdddb", + "ccccccccdddd", + "cccccccccddd", + ], + "dcba": [ + "ddddddcccccc", + "dddddddccccc", + "dddddddccccc", + "dddddddccccc", + "dddddddccccc", + "bddddddcccca", + "bbbbbbbaaaaa", + "bbbbbbbaaaaa", + "bbbbbbbaaaaa", + "bbbbbbaaaaaa", + ], + } + + @staticmethod + def four( + letters: str, nodata: float | None = None + ) -> tuple[DemTile, dict[str, DemTile], dict[str, float]]: + source = whole(10, 12, nodata=nodata) + cut = quadrants(source, row_cut=5, col_cut=6, overlap=3) + named = { + f"{letter}.tif": cut[q] + for letter, q in zip(letters, ("nw.tif", "ne.tif", "sw.tif", "se.tif"), strict=True) + } + tiles, offsets = offset_tiles(named) + return source, tiles, offsets + + @pytest.mark.parametrize("letters", ["abcd", "dcba"]) + def test_a_corner_of_four_tiles(self, build: Any, letters: str) -> None: + source, tiles, offsets = self.four(letters) + result, _ = build(tiles) + assert taken_from(result, source.array, offsets) == self.CORNERS[letters] + oracle = deepest_interior(tiles, result.tile.meta).astype(np.float32) + assert same_array(result.tile.array, oracle) + + @pytest.mark.parametrize("nodata", [np.nan, SENTINEL], ids=["nan", "sentinel"]) + def test_nodata_in_the_deepest_tile_loses_to_a_shallower_valid_value( + self, build: Any, nodata: float + ) -> None: + """At (5, 6) `a.tif` (nw) is 2 deep and the other three 0: with NoData + there, the three valid values tie, and `b.tif` sorts first. At (4, 7), + in `a.tif` and `b.tif` only, `a.tif` is 1 deep and `b.tif` 1: a tie + `a.tif` would take, but its NoData leaves `b.tif`.""" + sentinel = None if np.isnan(nodata) else SENTINEL + source, tiles, offsets = self.four("abcd", sentinel) + array = np.asarray(tiles["a.tif"].array).copy() + array[5, 6] = array[4, 7] = nodata + tiles["a.tif"] = DemTile(meta=tiles["a.tif"].meta, array=array) + result, _ = build(tiles) + expected = [list(row) for row in self.CORNERS["abcd"]] + assert (expected[5][6], expected[4][7]) == ("a", "a") # with no NoData + expected[5][6] = expected[4][7] = "b" + assert taken_from(result, source.array, offsets) == ["".join(r) for r in expected] + + def test_every_order_gives_the_same_mosaic_and_report(self, mz: ModuleType, plan: Any) -> None: + """I2 under the new rule: all 24 assembly orders of the four-tile + corner, with NoData in the deepest tile at one node, give one array bit + for bit and one seam report.""" + _, tiles, _ = self.four("dcba") + array = np.asarray(tiles["d.tif"].array).copy() + array[5, 6] = np.nan + tiles["d.tif"] = DemTile(meta=tiles["d.tif"].meta, array=array) + planned = plan(tiles) + first = mz.assemble(planned, Loads(tiles)) + for order in itertools.permutations(planned.tiles): + result = mz.assemble(planned.model_copy(update={"tiles": order}), Loads(tiles)) + names = [p.name for p in order] + assert same_array(result.tile.array, first.tile.array), names + assert seams(result) == seams(first), names + oracle = deepest_interior(tiles, first.tile.meta).astype(np.float32) + assert same_array(first.tile.array, oracle) + + +class TestQ1SeamReport: + """Ola's Q1 revised: each disagreeing seam is reported, the two tiles (by + name, the one that sorts first first), the number of nodes where both hold + a valid value and the values differ, and the largest and the median + |difference| over those nodes. Pairs that agree are not listed. A + `Mosaic`'s `seams` is a tuple sorted by the pair's names. + """ + + def test_every_pair_of_the_four_corner(self, build: Any) -> None: + """Every overlap of the offset quadrants disagrees by a constant, so + each pair's count is its overlap's node count: 8 x 3 for a | b (nw, + ne), 3 x 9 for a | c (nw, sw), the 3 x 3 corner for a | d and b | c, + 3 x 6 for b | d and 5 x 3 for c | d.""" + _, tiles, _ = TestQ1DeepestInterior.four("abcd") + result, _ = build(tiles) + assert seams(result) == [ + ("a.tif", "b.tif", 24, 1000.0, 1000.0), + ("a.tif", "c.tif", 27, 2000.0, 2000.0), + ("a.tif", "d.tif", 9, 3000.0, 3000.0), + ("b.tif", "c.tif", 9, 1000.0, 1000.0), + ("b.tif", "d.tif", 18, 2000.0, 2000.0), + ("c.tif", "d.tif", 15, 1000.0, 1000.0), + ] + assert seams(result) == seams_of(tiles, result.tile.meta) + + @pytest.mark.parametrize("overlap", [0, 1, 3], ids=["abutting", "shared-line", "overlap-3"]) + def test_agreeing_seams_are_not_listed(self, build: Any, overlap: int) -> None: + area = overlap == 0 + source = whole(8, 12, area=area) if area else whole(9, 13) + result, _ = build(quadrants(source, row_cut=4, col_cut=6, overlap=overlap)) + assert result.seams == () + + def test_one_tile_has_no_seams(self, build: Any) -> None: + result, _ = build({"only.tif": whole()}) + assert result.seams == () + + def test_only_the_disagreeing_pair_is_listed(self, build: Any) -> None: + """Four quadrants that agree but for one node on the nw | ne seam + (outside the other two tiles): one entry, not six.""" + source = whole(9, 13) + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=3) + array = np.asarray(tiles["ne.tif"].array).copy() + array[1, 1] -= 6.0 + tiles["ne.tif"] = DemTile(meta=tiles["ne.tif"].meta, array=array) + result, _ = build(tiles) + assert seams(result) == [("ne.tif", "nw.tif", 1, 6.0, 6.0)] + + def test_a_three_way_node_is_counted_in_each_disagreeing_pair(self, build: Any) -> None: + """At node (0, 0) `a` and `b` hold 1.0 and `c` 5.0: a | c and b | c + each count it; a | b agree and are not listed.""" + source = whole(rows=3, cols=3) + tiles = {} + for name, first in (("a.tif", 1.0), ("b.tif", 1.0), ("c.tif", 5.0)): + array = source.array.copy() + array[0, 0] = first + tiles[name] = piece(source, 0, 3, 0, 3, array=array) + result, _ = build(tiles) + assert seams(result) == [ + ("a.tif", "c.tif", 1, 4.0, 4.0), + ("b.tif", "c.tif", 1, 4.0, 4.0), + ] + + def test_the_report_covers_the_mosaics_nodes_only(self, build: Any) -> None: + """A difference outside `--bbox` is in no node the run reads, so it is + not reported; one inside is.""" + source = whole(9, 13) + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=3) + array = np.asarray(tiles["ne.tif"].array).copy() + array[0, 0] += 2.0 # global (0, 6): outside the box + array[3, 1] += 5.0 # global (3, 7): inside it + tiles["ne.tif"] = DemTile(meta=tiles["ne.tif"].meta, array=array) + box = (X0 + 5 * DX, Y0 - 6 * DY, X0 + 8 * DX, Y0 - 2 * DY) # rows 2-6, cols 5-8 + result, _ = build(tiles, box) + assert seams(result) == [("ne.tif", "nw.tif", 1, 5.0, 5.0)] + + def test_differences_are_taken_in_float64(self, build: Any) -> None: + """0.5 against 2**24 + 2, both exact in float32: the difference is + 16777217.5 in float64 and would round to 16777218 in float32.""" + source = whole(rows=3, cols=3) + tiles = {} + for name, first in (("a.tif", 0.5), ("b.tif", 2.0**24 + 2)): + array = source.array.copy() + array[1, 1] = first + tiles[name] = piece(source, 0, 3, 0, 3, array=array) + result, _ = build(tiles) + assert seams(result) == [("a.tif", "b.tif", 1, 16777217.5, 16777217.5)] class TestM9SplitAndRestitch: From cba0073a79ae2bb033f269b9fce5e2493a8db056 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 07:45:00 +0200 Subject: [PATCH 20/28] 15a: overlaps split down the middle and reported (Ola's Q1 revised) Green for 0c97ac9. An overlap node takes the valid value of the tile it lies deepest in (min(r, c, rows-1-r, cols-1-c) in the whole tile's own indices), ties to the name that sorts first; NoData still loses to a valid value, and with none valid the sentinel wins over NaN in every order. The overlap-disagreement refusal and its message are gone. assemble writes each tile whole, keeps its values where it meets another placement as a strip, and drops the tile; once all are in, each pairwise overlap is decided from the strips (_decide) and each pair whose valid values differ is reported as a Seam (nodes, largest and median |a - b| in float64). Peak beyond the canvas and one loaded tile: the strips plus one overlap's temporaries (an int64 depth, a value and a mask of the overlap's size), never a canvas-sized array. One tile without bounds is returned as loaded, as before. Mosaic.seams and DemInput.seams carry the report; cli.py records dem_seams whenever it records dem_tiles ("none" when all agree), and --stats gets a "DEM seams" table between Sizes and Quality when some pair disagrees. Co-Authored-By: Claude Opus 5.5 --- src_python/tin_engine/cli.py | 18 +++- src_python/tin_engine/dem_input.py | 11 +- src_python/tin_engine/mosaic.py | 165 +++++++++++++++++++++-------- src_python/tin_engine/stats.py | 6 ++ 4 files changed, 149 insertions(+), 51 deletions(-) diff --git a/src_python/tin_engine/cli.py b/src_python/tin_engine/cli.py index 68e8341b..34fb2b31 100644 --- a/src_python/tin_engine/cli.py +++ b/src_python/tin_engine/cli.py @@ -76,7 +76,7 @@ from tin_engine.io.models import DemTile, RasterMeta from tin_engine.io.ply import write_ply from tin_engine.io.vtk_legacy import write_vtk -from tin_engine.mosaic import Bounds +from tin_engine.mosaic import Bounds, Seam from tin_engine.raster import to_core from tin_engine.stats import PhaseClock, Refinement, Report, Sizes, _exact, quality, render from tin_engine.viz.fixtures import GALLERY, Fixture @@ -662,6 +662,7 @@ def mesh( """ clock = PhaseClock() dem_run: _DemMesh | None = None + seams: tuple[Seam, ...] = () if (name is None) == (not dem): raise typer.BadParameter( "give a gallery fixture name or --dem PATH, exactly one of the two", @@ -745,8 +746,10 @@ def mesh( fields = [("crs", f"EPSG:{epsg}"), ("elevation_source", sentence)] comments = [f"crs EPSG:{epsg}", f"elevation {sentence}"] if len(dem) > 1 or dem[0].is_dir(): # R11: the files used, named - listed = "; ".join(names).encode("ascii", "backslashreplace").decode("ascii") - fields.append(("dem_tiles", listed)) + fields.append(("dem_tiles", _ascii("; ".join(names)))) + seams = opened.seams + listed = "; ".join(s.entry() for s in seams) or "none" + fields.append(("dem_seams", _ascii(listed))) # Ola's Q1 revised if described: fields.append(("domain", described)) comments.append(f"domain {described}") @@ -832,7 +835,7 @@ def mesh( target.write_bytes(data) typer.echo(f"{target}") if stats is not None: - _write_report(clock, report_target, surface_mesh, dem_run, targets) + _write_report(clock, report_target, surface_mesh, dem_run, targets, seams) def _report_target( @@ -856,6 +859,7 @@ def _write_report( trimmed: Trimmed, dem_run: _DemMesh | None, files: list[Path], + seams: tuple[Seam, ...], ) -> None: """Build the report from what ran and write it, or print it for ``-``. The total stops here; the quality pass is timed on its own line (R4).""" @@ -887,6 +891,7 @@ def _write_report( total=total, stats_seconds=(time.perf_counter_ns() - t0) / 1e9, threads=os.cpu_count() if refinement else None, + seams=[s.cells() for s in seams], ) ) if target is None: @@ -940,6 +945,11 @@ def _open_dem( raise typer.BadParameter(_words(exc), param_hint="--dem") from exc +def _ascii(text: str) -> str: + """A file field's text with non-ASCII escaped, as `dem_tiles` records names.""" + return text.encode("ascii", "backslashreplace").decode("ascii") + + def _words(exc: ValueError) -> str: """A refusal's own words: Pydantic's messages without its wrapper.""" if isinstance(exc, ValidationError): diff --git a/src_python/tin_engine/dem_input.py b/src_python/tin_engine/dem_input.py index 5ed83ee8..43a8cd37 100644 --- a/src_python/tin_engine/dem_input.py +++ b/src_python/tin_engine/dem_input.py @@ -18,7 +18,7 @@ from tin_engine.io.models import DemTile from tin_engine.io.repository import TiffDemRepository -from tin_engine.mosaic import Bounds, MosaicPlan, assemble, plan_mosaic +from tin_engine.mosaic import Bounds, MosaicPlan, Seam, assemble, plan_mosaic class DemRequest(BaseModel): @@ -45,12 +45,14 @@ def _one_form(self) -> Self: @dataclass(frozen=True, slots=True) class DemInput: - """The tile to mesh, the plan it was assembled by, and a name for the run: - the directory's name, or the stem of the first file as given.""" + """The tile to mesh, the plan it was assembled by, a name for the run (the + directory's name, or the stem of the first file as given), and the + mosaic's disagreeing seams (Ola's Q1 revised).""" tile: DemTile plan: MosaicPlan label: str + seams: tuple[Seam, ...] = () def open_dem(request: DemRequest) -> DemInput: @@ -64,4 +66,5 @@ def open_dem(request: DemRequest) -> DemInput: repository = TiffDemRepository(request.sources, nodata=request.nodata) label = first.stem plan = plan_mosaic(repository.footprints(), request.bounds, None) - return DemInput(tile=assemble(plan, repository.load).tile, plan=plan, label=label) + mosaic = assemble(plan, repository.load) + return DemInput(tile=mosaic.tile, plan=plan, label=label, seams=mosaic.seams) diff --git a/src_python/tin_engine/mosaic.py b/src_python/tin_engine/mosaic.py index 5770ade5..eda456b0 100644 --- a/src_python/tin_engine/mosaic.py +++ b/src_python/tin_engine/mosaic.py @@ -102,12 +102,35 @@ class MosaicPlan(BaseModel): tiles: tuple[TilePlacement, ...] +@dataclass(frozen=True, slots=True) +class Seam: + """Two tiles whose valid values differ at `nodes` of the mosaic's nodes, + by `largest` and `median` there (float64); `first < second` by name.""" + + first: str + second: str + nodes: int + largest: float + median: float + + def entry(self) -> str: + """The `dem_seams` entry, unescaped.""" + _, _, n, largest, median = self.cells() + return f"{self.first} | {self.second}: nodes {n}, max {largest}, median {median}" + + def cells(self) -> tuple[str, str, str, str, str]: + """The `--stats` table row, numbers formatted as in `entry`.""" + return self.first, self.second, f"{self.nodes}", f"{self.largest:g}", f"{self.median:g}" + + @dataclass(frozen=True, slots=True) class Mosaic: - """The assembled tile, and the plan it came from, as provenance.""" + """The assembled tile, the plan it came from, as provenance, and each pair + of tiles whose overlap disagrees, sorted by name (Ola's Q1 revised).""" tile: DemTile plan: MosaicPlan + seams: tuple[Seam, ...] = () @dataclass(frozen=True, slots=True) @@ -206,6 +229,12 @@ def assemble(plan: MosaicPlan, load: Callable[[str], DemTile]) -> Mosaic: planned dtype (the placements' result dtype, from the headers), and handed to `DemTile` without a copy (R7). A hand-built footprint may under-state its dtype, so a loaded tile that promotes further re-casts the canvas. + + Each tile is written whole, then dropped; what it holds where it meets + another tile's placement is kept as a strip. Once all are in, every + overlap is decided from the strips (Ola's Q1 revised, `_decide`) and each + pair that disagrees is reported (`_seam`). Beyond the canvas and one + loaded tile, the peak is the strips plus one overlap's temporaries. """ if len(plan.tiles) == 1 and plan.tiles[0].meta == plan.meta: return Mosaic(tile=_loaded(plan.tiles[0], load), plan=plan) @@ -213,14 +242,30 @@ def assemble(plan: MosaicPlan, load: Callable[[str], DemTile]) -> Mosaic: raise MosaicError("the plan has no tiles") planned = np.result_type(*(t.dtype for t in plan.tiles)) canvas: npt.NDArray[Any] = np.full((plan.meta.rows, plan.meta.cols), np.nan, dtype=planned) - merged: list[TilePlacement] = [] + ordered = sorted(plan.tiles, key=lambda t: t.name) + strips: dict[tuple[str, str], npt.NDArray[Any]] = {} for placement in plan.tiles: array = _loaded(placement, load).array if np.result_type(canvas.dtype, array.dtype) != canvas.dtype: canvas = canvas.astype(np.result_type(canvas.dtype, array.dtype)) - _merge(canvas, placement, array, plan.meta.nodata, merged, load) - merged.append(placement) - return Mosaic(tile=DemTile._adopt(plan.meta, canvas), plan=plan) + c, s = placement.canvas, placement.source + incoming = array[s.row0 : s.row0 + s.rows, s.col0 : s.col0 + s.cols] + canvas[c.row0 : c.row0 + c.rows, c.col0 : c.col0 + c.cols] = incoming + for other in ordered: + box = _meet(c, other.canvas) + if other.name != placement.name and box is not None: + strips[placement.name, other.name] = incoming[_within(box, c)].copy() + seams = [] + for i, a in enumerate(ordered): + for b in ordered[i + 1 :]: + box = _meet(a.canvas, b.canvas) + if box is None: + continue + _decide(canvas, box, ordered, strips, (a.name, b.name), plan.meta.nodata) + seam = _seam(a.name, b.name, strips[a.name, b.name], strips[b.name, a.name], plan) + if seam is not None: + seams.append(seam) + return Mosaic(tile=DemTile._adopt(plan.meta, canvas), plan=plan, seams=tuple(seams)) def _aligned(a: RasterMeta, b: RasterMeta) -> bool: @@ -398,48 +443,82 @@ def _loaded(placement: TilePlacement, load: Callable[[str], DemTile]) -> DemTile return tile -def _merge( +def _meet(a: IndexWindow, b: IndexWindow) -> IndexWindow | None: + """The canvas nodes both windows hold, or None.""" + r0, c0 = max(a.row0, b.row0), max(a.col0, b.col0) + r1, c1 = min(a.row0 + a.rows, b.row0 + b.rows), min(a.col0 + a.cols, b.col0 + b.cols) + if r0 >= r1 or c0 >= c1: + return None + return IndexWindow(row0=r0, col0=c0, rows=r1 - r0, cols=c1 - c0) + + +def _within(inner: IndexWindow, outer: IndexWindow) -> tuple[slice, slice]: + """`inner`'s slices in an array holding `outer` (both in canvas indices).""" + r, c = inner.row0 - outer.row0, inner.col0 - outer.col0 + return slice(r, r + inner.rows), slice(c, c + inner.cols) + + +def _valid(values: npt.NDArray[Any], nodata: float | None) -> npt.NDArray[np.bool_]: + valid = ~np.isnan(values) + if nodata is not None: + valid &= values != nodata + return valid + + +def _decide( canvas: npt.NDArray[Any], - placement: TilePlacement, - array: npt.NDArray[Any], + box: IndexWindow, + ordered: list[TilePlacement], + strips: dict[tuple[str, str], npt.NDArray[Any]], + pair: tuple[str, str], nodata: float | None, - merged: list[TilePlacement], - load: Callable[[str], DemTile], ) -> None: - """R5's overlap rule, per node: valid beats NoData; two valid values must - be equal (`==`); NoData against NoData keeps the sentinel over NaN, in - every order. A refusal names the merged tile whose value the incoming one - disagrees with, re-loading candidates (B2): the request is refused anyway.""" - c, s = placement.canvas, placement.source - region = canvas[c.row0 : c.row0 + c.rows, c.col0 : c.col0 + c.cols] - incoming = array[s.row0 : s.row0 + s.rows, s.col0 : s.col0 + s.cols] - old_nan, new_nan = np.isnan(region), np.isnan(incoming) - old_valid, new_valid = ~old_nan, ~new_nan - if nodata is not None: - old_valid &= region != nodata - new_valid &= incoming != nodata - clash = old_valid & new_valid & (region != incoming) - if clash.any(): - row, col = (int(v) for v in np.argwhere(clash)[0]) - held = region[row, col] - other = next( - m.name - for m in merged - if m.canvas.row0 <= c.row0 + row < m.canvas.row0 + m.canvas.rows - and m.canvas.col0 <= c.col0 + col < m.canvas.col0 + m.canvas.cols - and _loaded(m, load).array[ - m.source.row0 + c.row0 + row - m.canvas.row0, - m.source.col0 + c.col0 + col - m.canvas.col0, - ] - == held - ) - largest = float(np.max(np.abs(region[clash].astype(np.float64) - incoming[clash]))) - raise MosaicError( - f"{other} and {placement.name} disagree at {int(clash.sum())} overlapping " - f"nodes, by up to {largest!r}" + """Ola's Q1 revised over `box`, the overlap of `pair`: each node takes the + valid value of the tile it lies deepest in, depth being + `min(r, c, rows - 1 - r, cols - 1 - c)` in the whole tile's own indices; + ties to the name that sorts first (`ordered` is by name, and only a deeper + tile replaces). No valid value: the sentinel if any tile holds it, else + NaN, in every order. A third tile's values come from its strip with one + of the pair, which holds every node of `box` it covers.""" + best = np.full((box.rows, box.cols), -1, dtype=np.int64) + value = np.full((box.rows, box.cols), np.nan, dtype=canvas.dtype) + sentinel = np.zeros((box.rows, box.cols), dtype=bool) + for t in ordered: + part = _meet(t.canvas, box) + if part is None: + continue + other = next(p for p in ordered if p.name == (pair[1] if t.name == pair[0] else pair[0])) + held = _meet(t.canvas, other.canvas) + assert held is not None # it holds `part` + values = strips[t.name, other.name][_within(part, held)] + row = np.arange(part.rows) + part.row0 - t.canvas.row0 + t.source.row0 + col = np.arange(part.cols) + part.col0 - t.canvas.col0 + t.source.col0 + depth = np.minimum.outer( + np.minimum(row, t.meta.rows - 1 - row), np.minimum(col, t.meta.cols - 1 - col) ) - write = (old_nan & ~new_nan) | (~old_nan & ~old_valid & new_valid) - region[write] = incoming[write] + here = _within(part, box) + take = _valid(values, nodata) & (depth > best[here]) + value[here][take] = values[take] + best[here][take] = depth[take] + if nodata is not None: + sentinel[here] |= values == nodata + if nodata is not None: + value[(best < 0) & sentinel] = nodata + canvas[box.row0 : box.row0 + box.rows, box.col0 : box.col0 + box.cols] = value + + +def _seam( + first: str, second: str, a: npt.NDArray[Any], b: npt.NDArray[Any], plan: MosaicPlan +) -> Seam | None: + """The pair's report over its overlap in the mosaic: nodes where both hold + a valid value and the two differ (`!=`), and the largest and median + `|a - b|` over those, in float64. None when they agree.""" + a64, b64 = a.astype(np.float64), b.astype(np.float64) + differ = _valid(a, plan.meta.nodata) & _valid(b, plan.meta.nodata) & (a64 != b64) + if not differ.any(): + return None + gaps = np.abs(a64[differ] - b64[differ]) + return Seam(first, second, int(gaps.size), float(gaps.max()), float(np.median(gaps))) def _mixed(a: TileFootprint, b: TileFootprint) -> str: diff --git a/src_python/tin_engine/stats.py b/src_python/tin_engine/stats.py index 6d52a00e..60ab9a50 100644 --- a/src_python/tin_engine/stats.py +++ b/src_python/tin_engine/stats.py @@ -153,6 +153,9 @@ class Report: total: float stats_seconds: float threads: int | None = None + #: One row per disagreeing pair of DEM tiles: two names, nodes, max and + #: median, already formatted (`mosaic.Seam.cells`; Ola's Q1 revised). + seams: Sequence[Sequence[str]] = () def _exact(value: float) -> str: @@ -279,6 +282,9 @@ def render(report: Report) -> str: """The Markdown report (R4). Pure: no clock, no I/O.""" lines = ["# rasputin mesh — statistics", "", f"`{report.command}`", ""] lines += ["## Sizes", "", *_sizes(report.sizes), ""] + if report.seams: + header = ("tile", "tile", "nodes", "max", "median") + lines += ["## DEM seams", "", *_table(header, report.seams), ""] lines += ["## Quality (plan view, x/y)", "", *_quality(report.quality), ""] if report.refinement is not None: lines += ["## Refinement", "", *_refinement(report.refinement), ""] From 7b02810c63e38de78fb4150b37ff200f0b3a6550 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 07:47:05 +0200 Subject: [PATCH 21/28] 15a: LOC after the midline rule Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 9b4b1e64..ef01df3a 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -839,6 +839,7 @@ order. 15c and 15d follow later. | | `cli.py`: `--dem` list, `--bbox`, refusals, fields | 45 | | | | **15a total** | **375** | **520** | | | *15a as built, measured at review (branch diff, CLAUDE.md §2 unit): 533, of which `mosaic.py` 325 against 200 estimated* | | | +| | *15a after Ola's Q1 revised (cba0073): 596 added against master, of which `mosaic.py` 372 against 200 estimated* | | | | **15b** | **Norway: the domain in its own CRS** | | | | | `crs.py`: `parse_crs`, `reprojector` | 30 | | | | `domain.py`: `crs: str`, `to_crs`, extent against coverage; `check_crs` removed | 40 | | From 9ea10cebe58f9e2167d52316bf19b2818c21715a Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 07:56:04 +0200 Subject: [PATCH 22/28] 15a review fixes: the Q1-revised evidence re-measured with a committed probe (120 pairs; a same-date pair can disagree); R5 row, prior art, assembly prose; ROADMAP 15 row Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 2 +- docs/increments/15-dem-mosaic.md | 28 +++++-- docs/increments/15-probes/dtm10_dates.py | 101 +++++++++++++++++++++++ 3 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 docs/increments/15-probes/dtm10_dates.py diff --git a/ROADMAP.md b/ROADMAP.md index c1fb1069..09dc9062 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -44,7 +44,7 @@ increment that most needs a picture to check against | — | `tools/bench.py`: the 1 m benchmark and the thread-scaling sweep from one checked-in command, with power state, quality and commit recorded per run; the one-off scripts in `docs/benchmarks/2026-09-26/` are its specification. Rule 2's acceptance run needs it | shipped with branch `tools-bench`'s PR | `docs/benchmarks/bench-py.md` | | — | The serial phase: profile refine's serial insert-and-flip phase, then parallelise what the profile blames. Scaling tops out at about 2.0-2.2×. Profiled 2026-09-27: serial part about a third of 1-thread refine, mostly Lawson legalisation; the scan stops near 5× from load imbalance | profiled; designed as increment 21 (Ola's rulings 2026-09-27: L1 determinism, one path, at most 2 % more triangles). **21a shipped with branch `increment21a-quick-wins`'s PR**: dynamic scan blocks, active merge, reused flip stack; mesh bit-identical; on AC refine -10 to -15 % at 8 threads, ceiling 2.1x -> 2.3x (`docs/benchmarks/2026-09-27/21a-acceptance.md`). **21b shipped with branch `increment21b-lattice-incircle`'s PR**: an int64 lattice incircle answers 99.97-100 % of refine's incircle tests; mesh bit-identical; on battery refine -15 % at 8 threads, ceiling 2.3x -> 2.5x (`docs/benchmarks/2026-09-27/21b-acceptance.md`). **21c measured** (`docs/benchmarks/2026-09-27/21c/README.md`): option C costs 6.5-10 % more triangles; A1 about 1 %; A0 is bit-identical (on these two inputs, not proven), and with evaluate-once is modelled at only 7-11 % faster refine at 8 threads and slower at 4. **21d is deferred** (Ola, 2026-09-27: "Review and push 21c, then basin-work"), behind the work in "Order of work" below; domain decomposition parallelises scan and split together | `docs/increments/21-parallel-refine.md`, `docs/benchmarks/2026-09-27/serial-profile/README.md` | | — | Release hardening: measure libc++'s `_LIBCPP_HARDENING_MODE_FAST` (and libstdc++'s assertions on the GCC leg), which bounds-check `std::vector`, `span` and the like in Release, on the 1 m benchmark; switch it on if the cost is small. Today an out-of-range index in shipped code the tests miss is undefined behaviour and crashes Python (Ola, 2026-09-27: "I'm surprised we don't have proper memory control"; CI's ASan/UBSan/TSan cover what the tests reach) | to measure (Ola, 2026-09-27); 21d, which it was placed after, is deferred behind the basin work | none yet | -| 15 | A DEM in several tiles, and the domain in its own CRS: 15a Norway, many tiles in one projected CRS (Ola's 254 DTM10 UTM33 tiles; only the selected tiles must share a lattice, since 8 of them sit half a cell off); 15b the domain polygon reprojected into the DEM's CRS; 15c and 15d the São Francisco basin (geographic DEMs meshed in the DEM's own lattice frame, not resampled; window decoding and memory) | **15a shipped with branch `increment15-dem-mosaic`'s PR**: `--dem DIR` or several files, `--bbox`, tiles selected and stitched on one lattice, Q1-Q5 refusals; a box over tiles whose overlaps agree meshes (the design's acceptance box is refused: real DTM10 overlaps disagree by up to 25 m, which Ola's Q1 refuses; the overlap policy is open, 2026-09-27). 15b next (Ola, 2026-09-27: Norway first) | `docs/increments/15-dem-mosaic.md` | +| 15 | A DEM in several tiles, and the domain in its own CRS: 15a Norway, many tiles in one projected CRS (Ola's 254 DTM10 UTM33 tiles; only the selected tiles must share a lattice, since 8 of them sit half a cell off); 15b the domain polygon reprojected into the DEM's CRS; 15c and 15d the São Francisco basin (geographic DEMs meshed in the DEM's own lattice frame, not resampled; window decoding and memory) | **15a shipped with branch `increment15-dem-mosaic`'s PR**: `--dem DIR` or several files, `--bbox`, tiles selected and stitched on one lattice, Q2-Q5 refusals; overlaps that disagree (real DTM10 tiles exported on different dates, up to 52 m) are split down the middle and reported per seam (Ola's Q1 revised, 2026-09-28). The design's acceptance box, 9 tiles and 10,051² nodes, meshes: 11.05 M triangles in 31 s on battery. 15b next (Ola, 2026-09-27: Norway first) | `docs/increments/15-dem-mosaic.md` | | 16b | Interior polygons and polylines as constraints ("terrain polygons": lakes, land cover, roads, rivers): `--features PATH`, a GeoJSON `FeatureCollection`, each feature naming a vocabulary property; closed or open `Breakline`s, crossings noded, off-node vertices with bilinear z. **Its working example is real data** (Ola, 2026-09-27): CORINE Land Cover 2018 over the benchmark tile `7908_3_10m_z33.tif`. The source is Ola's local copy, `rasputin_data/corine_sql/.../U2018_CLC2018_V2020_20u1.gpkg` (8.2 GB, EPSG:3035, a sibling of this repository, which also holds the 254-tile DTM10 archive for gap 6). It reads without GDAL: sqlite3 over its R-tree, the GeoPackage blob header stripped, `shapely.wkb`, then `pyproj` to 25833, so CRS stops in Python as before. Probed 2026-09-27 in 0.2 s: 60 polygons in 8 classes (heath, bare rock, sparse vegetation, bogs, intertidal flats, water, sea, urban), 11 068 vertices clipped to the tile, median segment 54 m against 10 m cells. The EEA's public ArcGIS service (`image.discomap.eea.europa.eu`, `Corine/CLC2018_WM`) returns the same 11 068 clipped vertices and is the route for anyone without the file. What it forces on 16b's design: neighbouring polygons share their boundaries, so each shared edge arrives twice; the polygons run past the domain and must be clipped; the extract is committed as a fixture with the Copernicus attribution. Placed before 20c because 20c may split constraint segments and should be designed and measured on inputs that have interior ones | designed in 16's R6, no increment file yet; after the serial phase, before 20c | `docs/increments/16-domain-polygon.md` (R6) | | 20c | Soft quality criterion: a penalty that each Steiner node or constraint split must pay for in angle gained, instead of 20's hard 25°; applied at the start and during DEM refinement; may split constraint segments when that improves the mesh. Ola's rulings on 20's C1-C3 | to design after 16b (`@architect` measures cost against 20 first) | `docs/increments/20-start-quality.md` (Ola's rulings) | | — | Auto-catchment: the watershed upstream of a coordinate, computed from the DEM and handed to `--domain`, so a catchment no longer has to be supplied as a file (Ola, 2026-09-27: "not far into the future"). The textbook route is depression handling (Priority-Flood, Barnes, Lehman and Mulla 2014), D8 flow directions (O'Callaghan and Mark 1984) and accumulation, the pour point snapped to the strongest flow nearby, the upstream cells traced and their outline turned into a polygon; the literature check is `@architect`'s. Open for its design: whether it runs in the C++ core (a 10 m tile is 25 M cells); how a stair-stepped cell outline becomes a domain polygon, which meets input coarsening; and that a real catchment crosses tile edges, so it needs gap 6 (a DEM in several tiles) first. Legacy has nothing on it (`grep -rliE "watershed|flow.?acc|flow.?dir|pour.?point|catchment" legacy` returns no files) | to design; after gap 6, which it needs; placed after 20c, can move ahead of it on Ola's word | none yet | diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index ef01df3a..649c20ea 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -28,10 +28,17 @@ are designed, not implemented; Q6-Q10 are open. Written by `@architect` before Q6-Q10 (the basin) wait until after the Norwegian sub-increments. - **2026-09-28, Q1 revised: overlaps that disagree are split down the middle - ("b", Ola).** Measured first: in 60 sampled neighbour pairs of Ola's - archive, the 21 made on the same export date agree bit for bit and 18 of the - 39 made on different dates disagree (mean difference about 0, median 0 on - flat ground, growing with slope, up to 30 m on cliffs). Kartverket's + ("b", Ola).** Measured first, and widened after review with the committed + probe `15-probes/dtm10_dates.py` (120 random neighbour pairs, seed 1; a + tile's *export date* is the date of its side files, `.tif.aux.xml` or + `.tfw`; the `.tif` files' own dates are the download date and the TIFF tags + carry none): of 42 same-date pairs, 40 agree exactly and 41 within 1 mm, but + 7204_4 | 7304_3 differs at 156,379 nodes by up to 5.07 m; of 77 + different-date pairs, 49 agree within 1 mm and 28 do not, up to 52.1 m + (6500_1 | 6500_2). So a shared date makes agreement likely, not certain. + The differences have a mean near 0, are often exactly 0 on flat ground and + grow with slope (the first 60-pair sample, and three pairs examined node by + node). Kartverket's metadata explains it: the current 10 m model is exported from NDH laser data where it exists, supplemented by the 2013 contour-based DTM10 (±2-6 m), and tiles were exported one by one as laser coverage grew. Not land uplift @@ -103,7 +110,7 @@ What is carried across, and what changes: | R3: `read_meta` split out of `decode_dem` | carried (R3); window decoding added in 15d | an ANADEM tile is 2.5 GiB | | R4: every tile in the directory on one lattice, or refuse | **changed** (R4): only the **selected** tiles must share a lattice | 8 of Ola's 254 tiles are half a cell off (N2); the parked rule refuses the whole archive | | R4: canvas origin from the tiles as decoded | **changed** (R4): each lattice has a **reference node**, and every node has a global index | frame coordinates must not depend on the window, for domain decomposition (R12) | -| R5: valid beats NoData, disagreement split down the middle and reported (Q1 revised), order-independent | carried (R5), now **with real data behind it** | DTM10 (N3) and ANADEM (B4) overlaps agree bit for bit | +| R5: valid beats NoData, disagreement split down the middle and reported (Q1 revised), order-independent | carried (R5), now **with real data behind it** | DTM10 overlaps between tiles exported on different dates often disagree, up to 52 m (Q1 revised); same-date ones nearly always agree; ANADEM's sampled seams agree (B4) | | R5: gaps stay NaN | **changed** (R5): a node the request needs that no tile covers is refused | Ola's ruling, `18-row-span-scan.md` R6: "a missing tile inside the extent is a data error" | | R6: `--bbox` in the DEM's CRS, no transform | carried (R6); the domain is transformed in 15b (R9) | "inputs in their own CRS" | | R6: `GeoPolygon` lands with the clip | superseded: 16 shipped `DomainPolygon`; 15b gives it a transform | | @@ -274,8 +281,9 @@ modified data apply at least (its notice, plus a notice of modification). `gdalwarp` maps every destination pixel back through the inverse transform, approximated by linear interpolation along rows within an error threshold (default 0.125 pixel). GDAL documentation, recalled. **Differences:** no order - precedence, since disagreement is refused and valid beats NoData in any - order (R5); and no warp (R8). + precedence by source order: a disagreeing overlap goes to the tile the node + lies deepest in, ties by name, and valid beats NoData in any order (R5, Q1 + revised); and no warp (R8). - **Projection choice**: Snyder, *Map Projections — A Working Manual*, USGS Professional Paper 1395, 1987, recalled. A Lambert conformal conic suits an extent wide east-west at mid latitudes, with the standard parallels about one @@ -483,12 +491,14 @@ of `TileFootprint`. ### R5. Assembly: stitching and overlaps [15a] -Carried from the parked R5 almost unchanged. +Carried from the parked R5, with assembly reworked for Q1 revised. - The canvas is allocated once, NaN-filled, in `np.result_type` of the selected tiles' dtypes (float32 for both archives). - Tiles are loaded one at a time in plan order. Each tile's used window is - merged into the canvas, and the tile is dropped. + written whole into the canvas; where it meets another tile, a copy of its + values there (a strip) is kept; then the tile is dropped. Once all tiles + are in, each overlap is decided from the strips. - **Overlaps**, per node: - one value NoData (NaN or the sentinel), the other valid: **valid wins**; - both NoData: NoData; diff --git a/docs/increments/15-probes/dtm10_dates.py b/docs/increments/15-probes/dtm10_dates.py new file mode 100644 index 00000000..6dd559f1 --- /dev/null +++ b/docs/increments/15-probes/dtm10_dates.py @@ -0,0 +1,101 @@ +"""Do DTM10 overlaps agree when the two tiles share an export date? + +Evidence for "Ruled by Ola", Q1 revised (docs/increments/15-dem-mosaic.md). +The export date of a tile is the modification date of its side files +(`.tif.aux.xml`, else `.tfw`), which carry Kartverket's per-tile date. The +`.tif` files' own dates are the download date and say nothing. The TIFF tags +carry no date at all. + +Usage: python dtm10_dates.py [ARCHIVE] [N_PAIRS] [SEED] +Prints, for N random neighbour pairs on one lattice, whether they share an +export date and how their jointly valid overlap nodes compare: exact, within +1 mm, or beyond. +""" + +import datetime +import glob +import os +import random +import sys + +import numpy as np +import tifffile + +NODATA = -32767.0 + + +def export_date(tif): + for side in (tif + ".aux.xml", tif[:-4] + ".tfw"): + if os.path.exists(side): + return datetime.date.fromtimestamp(os.path.getmtime(side)) + return None + + +def origin(tif): + with tifffile.TiffFile(tif) as f: + page = f.pages[0] + tie = page.tags["ModelTiepointTag"].value + return tie[3], tie[4], page.shape + + +def overlap(a, b, info): + (xa, ya, sa), (xb, yb, sb) = info[a], info[b] + x0, x1 = max(xa, xb), min(xa + sa[1] * 10, xb + sb[1] * 10) + y0, y1 = max(ya - sa[0] * 10, yb - sb[0] * 10), min(ya, yb) + if x1 <= x0 or y1 <= y0: + return None + arr_a, arr_b = tifffile.imread(a), tifffile.imread(b) + + def window(arr, xm, ym): + r0, r1 = round((ym - y1) / 10), round((ym - y0) / 10) + c0, c1 = round((x0 - xm) / 10), round((x1 - xm) / 10) + return arr[r0:r1, c0:c1].astype(np.float64) + + wa, wb = window(arr_a, xa, ya), window(arr_b, xb, yb) + ok = (wa != NODATA) & (wb != NODATA) + return np.abs(wa[ok] - wb[ok]) + + +def main(): + archive = sys.argv[1] if len(sys.argv) > 1 else "../rasputin_data/DTM10_UTM33_20220924" + n = int(sys.argv[2]) if len(sys.argv) > 2 else 60 + seed = int(sys.argv[3]) if len(sys.argv) > 3 else 1 + tiles = sorted(glob.glob(f"{archive}/*_10m_z33.tif")) + info = {t: origin(t) for t in tiles} + pairs = [ + (a, b) + for i, a in enumerate(tiles) + for b in tiles[i + 1 :] + if (info[a][0] - info[b][0]) % 10 == 0 + and (info[a][1] - info[b][1]) % 10 == 0 + and abs(info[a][0] - info[b][0]) <= 50510 + and abs(info[a][1] - info[b][1]) <= 50510 + ] + random.seed(seed) + rows = [] + for a, b in random.sample(pairs, min(n, len(pairs))): + d = overlap(a, b, info) + if d is None or d.size == 0: + continue + same = export_date(a) == export_date(b) + rows.append((same, int((d > 0).sum()), int((d >= 1e-3).sum()), float(d.max()), a, b)) + for same in (True, False): + group = [r for r in rows if r[0] == same] + exact = sum(r[1] == 0 for r in group) + within = sum(r[2] == 0 for r in group) + worst = max((r[3] for r in group), default=0.0) + label = "same date" if same else "different dates" + print( + f"{label:16s} pairs {len(group):3d} exact {exact:3d}" + f" within 1 mm {within:3d} worst {worst:.3f} m" + ) + same_bad = [r for r in rows if r[0] and r[2] > 0] + worst_rows = sorted(rows, key=lambda r: -r[3])[:8] + for same, _n_any, n_mm, worst, a, b in same_bad + worst_rows: + tag = "same" if same else "diff" + name_a, name_b = os.path.basename(a)[:6], os.path.basename(b)[:6] + print(f" {tag} {name_a} | {name_b} >1mm {n_mm:7d} max {worst:.3f}") + + +if __name__ == "__main__": + main() From 3bb44a8ad5403b98ec562f4578a8a700e9bb3632 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 07:56:21 +0200 Subject: [PATCH 23/28] 15a: Ola's 1 mm threshold for the seam report Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 649c20ea..43b4e401 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -53,7 +53,14 @@ are designed, not implemented; Q6-Q10 are open. Written by `@architect` before 25 m jump cannot pass unnoticed; - the result stays independent of tile order. Ola will also download a fresh single-date set from hoydedata.no, whose - overlaps should agree. + overlaps should mostly agree (the probe found one same-date pair that does + not); the fresh set is not blocking ("the data is data", Ola). +- **2026-09-28, the seam report ignores differences below 1 mm** (Ola: "Ignore + below 1mm"). A seam counts, and the report lists, only nodes where + |a − b| ≥ 1 mm; float noise such as 7807_1 | 7808_4 (31 nodes, 1.5e-5 m) + and 7910_2 | 7910_3 (4 nodes, 2.4e-7 m) no longer appears. The midline rule + itself is unchanged: which tile's value a node takes does not depend on the + threshold. - **2026-09-27, Q5 as read after review ("Yes", to the main session's proposal):** @reviewer found that DTM10's 51-node overlaps make a box that only reaches into a half-cell tile's overlap strip select that tile and be From 008159190c35dcbeef5c138c419940e924061ac0 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 08:02:19 +0200 Subject: [PATCH 24/28] 15a tests: the seam report ignores differences below 1 mm; table cells escaped Ola, 2026-09-28: "Ignore below 1mm". A seam counts, and the report lists, only nodes with |a - b| >= 0.001 (float64, DEM units); nodes, largest and median are over those nodes only, and a pair with none is not listed. The midline decision does not look at the threshold. Separately (review suggestion), a `|` in a tile name is escaped `\|` in the --stats DEM seams table so the row keeps five cells. - New TestSeamThreshold (test_mosaic.py): exactly 1 mm counts and nextafter(0.001, 0) does not (float64 tiles, either sign); count/max/median over qualifying nodes only; a sub-mm-only pair is not listed; a sub-mm disagreement still resolves by depth then name, in every order. - Changed: TestM8Overlaps.test_one_ulp_is_a_disagreement is now test_one_ulp_is_below_the_threshold_and_still_decided_by_depth, since the ruling makes one ulp no seam; it keeps the value check. - The oracle seams_of (mosaic_fixtures.py) applies SEAM_THRESHOLD, so test_dem_input's shifted real seam now expects 12,787 nodes, not 12,800. - TestQ1Seams (CLI): a 0.5 mm disagreement records "none" with no stats section; a pipe in a tile name is escaped in the table, raw in dem_seams. Red at this commit: the 9 tests above that the ruling changes; the rest of tests/python is green. Test-only; no production code. Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 32 +++++++- tests/python/mosaic_fixtures.py | 14 +++- tests/python/test_cli_mesh_mosaic.py | 37 ++++++++- tests/python/test_mosaic.py | 113 +++++++++++++++++++++++++-- 4 files changed, 182 insertions(+), 14 deletions(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 43b4e401..754b92ec 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -1085,7 +1085,8 @@ that was not committed; `test_mosaic.py` killed 16 of 16 mutants of it. `(first, second)`, empty for one tile and when every overlap agrees. - A seam is a **pair** of tiles. `nodes` counts the mosaic's nodes (inside the window, never a node a `--bbox` leaves out) where both hold a valid value - and the two differ (`!=`, so one ulp counts). `largest` and `median` are of + and the two differ (`!=`, so one ulp counts; superseded by the 1 mm + threshold below). `largest` and `median` are of `|a - b|` over those nodes only, in float64; the median of an even count is the mean of the middle two (`np.median`). A node in three tiles counts once in each disagreeing pair. Pairs that agree are not listed. @@ -1103,6 +1104,35 @@ that was not committed; `test_mosaic.py` killed 16 of 16 mutants of it. entries, and where the report is computed (the scratch kept each pair's overlap strips, which needs no second load). +**Amended for Ola's 1 mm threshold (2026-09-28).** Pinned by the test +amendment `15a tests: the seam report ignores differences below 1 mm; table +cells escaped`: `TestSeamThreshold` in `test_mosaic.py`, the oracle +`seams_of` in `mosaic_fixtures.py` (`SEAM_THRESHOLD`), and two tests of +`test_cli_mesh_mosaic.py`'s `TestQ1Seams`. Run green against a scratch +implementation that was not committed; three mutants of it killed (`>` for +`>=`, the threshold consulted by the midline decision, the median over every +differing node). + +- A seam counts a node where both tiles hold a valid value and + `|a - b| >= 0.001` in float64 (the DEM's units), replacing `!=`. `nodes`, + `largest` and `median` are over those nodes only; a pair with none is not + listed, so `dem_seams` is `none` and `--stats` has no `## DEM seams` + section when no pair qualifies. The boundary is pinned with float64 tiles + holding 0.0 against `0.001` (counts) and against + `np.nextafter(0.001, 0.0)` (does not), in either tile. +- The midline decision does not look at the threshold: 0.5 mm apart, a node + still takes the deeper tile's value, ties by name, in every assembly order. + `TestM8Overlaps.test_one_ulp_is_a_disagreement` became + `test_one_ulp_is_below_the_threshold_and_still_decided_by_depth` (no seam; + the value is still the tie's `e.tif`). +- `TestRealDtm10.test_q1_the_seam_shifted_by_one_cell_is_reported_and_split` + (`test_dem_input.py`) is unchanged but follows the oracle: 13 of the + shifted real seam's 12,800 differing nodes are below 1 mm. +- **Table cells:** a `|` in a tile name is written `\|` in the + `## DEM seams` table, so the row keeps five cells. The `dem_seams` field is + not a table and keeps the name as listed (`ne.tif | n|w.tif: ...`). Which + layer escapes is not pinned. + **`io/models.py`.** `DemTile._adopt(meta, array)` is a classmethod. It raises `ValueError` on a shape, dtype, ndim or non-C-contiguous mismatch, sets the **passed** buffer read-only, and keeps it without a copy. Only `io/models.py` diff --git a/tests/python/mosaic_fixtures.py b/tests/python/mosaic_fixtures.py index 48d01818..ee6c9539 100644 --- a/tests/python/mosaic_fixtures.py +++ b/tests/python/mosaic_fixtures.py @@ -228,21 +228,27 @@ def winners(tiles: Mapping[str, DemTile], grid: RasterMeta) -> list[list[str]]: ] +#: Ola, 2026-09-28: "Ignore below 1mm". A seam counts a node only where +#: |a - b| >= 1 mm, 0.001 in the DEM's (metre) units, compared in float64. +SEAM_THRESHOLD = 0.001 + + def seams_of( tiles: Mapping[str, DemTile], grid: RasterMeta ) -> list[tuple[str, str, int, float, float]]: """The seam report the rule asks for, pair by pair, sorted by name: `(first, second, nodes, largest, median)` over the mosaic's nodes where - both tiles hold a valid value and the two differ; `|difference|` in - float64, the median of an even count the mean of the middle two. Pairs - that agree everywhere are left out.""" + both tiles hold a valid value and `|a - b| >= SEAM_THRESHOLD`; + `|difference|` in float64, the median of an even count the mean of the + middle two. Pairs with no such node are left out.""" placed = on_canvas(tiles, grid) out = [] names = sorted(placed) for i, a in enumerate(names): for b in names[i + 1 :]: va, vb = placed[a][0], placed[b][0] - differ = ~np.isnan(va) & ~np.isnan(vb) & (va != vb) + both = ~np.isnan(va) & ~np.isnan(vb) + differ = both & (np.abs(np.where(both, va - vb, 0.0)) >= SEAM_THRESHOLD) if differ.any(): gaps = np.abs(va[differ] - vb[differ]) out.append((a, b, int(differ.sum()), float(gaps.max()), float(np.median(gaps)))) diff --git a/tests/python/test_cli_mesh_mosaic.py b/tests/python/test_cli_mesh_mosaic.py index a89e79bd..c39eccb4 100644 --- a/tests/python/test_cli_mesh_mosaic.py +++ b/tests/python/test_cli_mesh_mosaic.py @@ -17,6 +17,7 @@ from __future__ import annotations import io +import re from pathlib import Path import numpy as np @@ -243,14 +244,14 @@ class TestQ1Seams: """ @staticmethod - def disagreeing(tmp_path: Path, source: DemTile, name: str = "nw.tif") -> Path: - """The quadrants, `ne.tif` 4.0 higher at one node of the shared column + def disagreeing(tmp_path: Path, source: DemTile, name: str = "nw.tif", by: float = 4.0) -> Path: + """The quadrants, `ne.tif` `by` higher at one node of the shared column (global (2, 6), in `ne.tif` and the tile named `name` only). Was `TestC4Refusals.test_an_overlap_disagreement`'s directory.""" tiles = quadrants(source, row_cut=4, col_cut=6, overlap=1) tiles[name] = tiles.pop("nw.tif") changed = np.array(tiles["ne.tif"].array) - changed[2, 0] += 4.0 + changed[2, 0] += np.float32(by) tiles["ne.tif"] = piece(source, 0, 5, 6, 13, array=changed) write_tiles(tmp_path / "disagree", tiles) return tmp_path / "disagree" @@ -336,6 +337,36 @@ def test_stats_has_no_seams_section_when_every_overlap_agrees( assert "## Sizes" in lines assert "## DEM seams" not in lines + def test_a_sub_millimetre_disagreement_records_none_and_no_section( + self, tmp_path: Path, source: DemTile + ) -> None: + """Ola, 2026-09-28: "Ignore below 1mm". The one differing node is + 0.5 mm off, so no pair qualifies: `none`, and `--stats` has no + "DEM seams" section.""" + directory = self.disagreeing(tmp_path, source, by=0.0005) + vtk = run_vtk(tmp_path / "m.vtk", "--dem", str(directory)) + assert field(vtk, "dem_seams") == "none" + lines = report(tmp_path, "--dem", str(directory)).splitlines() + assert "## Sizes" in lines + assert "## DEM seams" not in lines + + def test_a_pipe_in_a_tile_name_is_escaped_in_the_stats_table( + self, tmp_path: Path, source: DemTile + ) -> None: + """Review suggestion on 15a: a `|` in a tile name would end its + Markdown table cell early. In the "DEM seams" table it is written + `\\|`, so the row still has five cells (split on unescaped pipes). + The `dem_seams` field is not a table and records the name as listed + (`ne.tif` sorts first: `e` < `|`).""" + directory = self.disagreeing(tmp_path, source, name="n|w.tif") + lines = report(tmp_path, "--dem", str(directory)).splitlines() + at = lines.index("## DEM seams") + row = next(line for line in lines[at + 1 :] if line.startswith("| ne.tif")) + cells = [cell.strip() for cell in re.split(r"(?= 0.001; the midline rule does not look at +the threshold (`TestSeamThreshold`; `TestM8Overlaps`' one-ulp test inverted). + HOW THIS FILE GOES RED. `tin_engine.mosaic` and `tin_engine.io.repository` are imported in module-scoped fixtures, as `test_bench.py` loads its tool, so while they are missing each test fails on its own with `ModuleNotFoundError` @@ -1034,17 +1038,18 @@ def test_the_report_names_the_tiles_whose_values_disagree( assert seams(result) == [("b.tif", "c.tif", 1, 4.0, 4.0)], names assert result.tile.array[0, 0] == 1.0, names - def test_one_ulp_is_a_disagreement(self, build: Any) -> None: - """`==`, not `isclose`: DTM10's and ANADEM's overlaps agree bit for bit - on one export date (N3, B4), so one ulp is reported. Overlap 1: the - shared column is on both tiles' borders, a tie, `e.tif`'s value.""" + def test_one_ulp_is_below_the_threshold_and_still_decided_by_depth(self, build: Any) -> None: + """Was `test_one_ulp_is_a_disagreement`, which pinned `!=` (one ulp + reported). Ola, 2026-09-28: "Ignore below 1mm", so one ulp is not + reported. What it kept: the node's value is still decided by the + midline rule. Overlap 1: the shared column is on both tiles' borders, + a tie, `e.tif`'s value.""" source, arrays = self.pair(1) value = arrays["e.tif"][2, 0] bumped = np.nextafter(value, np.float32(np.inf), dtype=np.float32) arrays["e.tif"][2, 0] = bumped result, _ = build(self.tiles(source, arrays)) - ulp = float(bumped) - float(value) - assert seams(result) == [("e.tif", "w.tif", 1, ulp, ulp)] + assert result.seams == () assert result.tile.array[2, 4] == bumped def test_a_disagreement_against_a_sentinel_tile_is_still_one(self, build: Any) -> None: @@ -1385,6 +1390,102 @@ def test_differences_are_taken_in_float64(self, build: Any) -> None: assert seams(result) == [("a.tif", "b.tif", 1, 16777217.5, 16777217.5)] +class TestSeamThreshold: + """Ola, 2026-09-28: "Ignore below 1mm". A seam counts, and the report + lists, only nodes where both tiles hold a valid value and + |a - b| >= 1 mm, 0.001 in the DEM's units, in float64; `nodes`, `largest` + and `median` are over those nodes only, and a pair with none is not + listed. Which tile's value a node takes does not depend on the threshold. + """ + + #: Exactly 1 mm (the float64 0.001 the rule compares with) and the float64 + #: just below it. Planted as `gap` against 0.0, so `|a - b|` is the float64 + #: itself: no rounding stands between the planted value and the compare. + AT = 0.001 + BELOW = float(np.nextafter(0.001, 0.0)) + + @staticmethod + def same_grid(first: float, second: float) -> dict[str, DemTile]: + """`a.tif` and `b.tif` on one float64 3 x 3 grid, equal but at the + centre, where they hold `first` and `second`. Both are 1 deep there: + a tie, `a.tif`'s value.""" + source = whole(rows=3, cols=3, dtype=np.float64) + tiles = {} + for name, centre in (("a.tif", first), ("b.tif", second)): + array = source.array.copy() + array[1, 1] = centre + tiles[name] = piece(source, 0, 3, 0, 3, array=array) + return tiles + + @pytest.mark.parametrize("larger", ["a.tif", "b.tif"]) + def test_exactly_one_millimetre_counts(self, build: Any, larger: str) -> None: + assert self.AT == 1e-3 # the literal the ruling names, not a neighbour + first, second = (self.AT, 0.0) if larger == "a.tif" else (0.0, self.AT) + result, _ = build(self.same_grid(first, second)) + assert seams(result) == [("a.tif", "b.tif", 1, self.AT, self.AT)] + assert result.tile.array[1, 1] == first + + @pytest.mark.parametrize("larger", ["a.tif", "b.tif"]) + def test_just_below_one_millimetre_does_not(self, build: Any, larger: str) -> None: + assert 0.0 < self.BELOW < self.AT + first, second = (self.BELOW, 0.0) if larger == "a.tif" else (0.0, self.BELOW) + result, _ = build(self.same_grid(first, second)) + assert result.seams == () + assert result.tile.array[1, 1] == first + + def test_nodes_largest_and_median_are_over_the_qualifying_nodes_only(self, build: Any) -> None: + """`TestM8Overlaps`' pair with overlap 3; `e.tif` differs at four + nodes, by 0.0005, 0.0009, 2 and 4. Two qualify: nodes 2, largest 4, + median 3. Over all four the count would be 4 and the median ~1.""" + source, arrays = TestM8Overlaps.pair(3) + arrays["e.tif"][1, 0] += np.float32(0.0005) # global (1, 4) + arrays["e.tif"][1, 1] += np.float32(0.0009) # global (1, 5) + arrays["e.tif"][2, 1] += np.float32(2.0) # global (2, 5) + arrays["e.tif"][2, 2] += np.float32(4.0) # global (2, 6) + tiles = TestM8Overlaps.tiles(source, arrays) + result, _ = build(tiles) + assert seams(result) == [("e.tif", "w.tif", 2, 4.0, 3.0)] + assert seams(result) == seams_of(tiles, result.tile.meta) + + def test_a_pair_below_the_threshold_everywhere_is_not_listed(self, build: Any) -> None: + """Quadrants with overlap 3: `ne.tif` 0.5 mm off `nw.tif` at global + (1, 7), in those two only; `sw.tif` 3 off `nw.tif` at global (5, 1), + in those two only. One entry, nw | sw.""" + source = whole(9, 13) + tiles = quadrants(source, row_cut=4, col_cut=6, overlap=3) + for name, (r, c), delta in (("ne.tif", (1, 1), 0.0005), ("sw.tif", (1, 1), 3.0)): + array = np.asarray(tiles[name].array).copy() + array[r, c] += np.float32(delta) + tiles[name] = DemTile(meta=tiles[name].meta, array=array) + result, _ = build(tiles) + assert seams(result) == [("nw.tif", "sw.tif", 1, 3.0, 3.0)] + + def test_a_sub_millimetre_disagreement_is_still_decided_by_depth_then_name( + self, mz: ModuleType, plan: Any + ) -> None: + """The midline rule does not look at the threshold. `TestM8Overlaps`' + pair with overlap 3, 0.5 mm apart at three nodes of row 1: at column 4 + `w.tif` is deeper (1 against 0), at 5 a tie (`e.tif`, by name), at 6 + `e.tif` is deeper. So the name that sorts first does not take every + node, and in every assembly order; nothing is reported.""" + source, arrays = TestM8Overlaps.pair(3) + arrays["w.tif"][1, 4] += np.float32(0.0005) # global (1, 4): w.tif deeper + arrays["e.tif"][1, 1] += np.float32(0.0005) # global (1, 5): a tie + arrays["e.tif"][1, 2] -= np.float32(0.0005) # global (1, 6): e.tif deeper + tiles = TestM8Overlaps.tiles(source, arrays) + expected = source.array.copy() + expected[1, 4] = arrays["w.tif"][1, 4] + expected[1, 5] = arrays["e.tif"][1, 1] + expected[1, 6] = arrays["e.tif"][1, 2] + assert (expected[1, 4:7] != source.array[1, 4:7]).all() # all three planted + planned = plan(tiles) + for order in itertools.permutations(planned.tiles): + result = mz.assemble(planned.model_copy(update={"tiles": order}), Loads(tiles)) + names = [p.name for p in order] + assert same_array(result.tile.array, expected), names + assert result.seams == (), names + + class TestM9SplitAndRestitch: """M9 and I5: a grid cut into tiles reassembles to itself, meta and array.""" From 776757e689491302abeb0842129952388328cb03 Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 08:47:34 +0200 Subject: [PATCH 25/28] 15a: the seam report ignores differences below 1 mm; table cells escaped; assemble frees each tile before the next load - mosaic.py: SEAM_THRESHOLD = 0.001 (Ola, 2026-09-28: "Ignore below 1mm"). _seam counts only nodes where both tiles are valid and |a - b| >= 1 mm in float64; nodes, max and median are over those; a pair with none is not listed. _decide does not consult the threshold. - stats.py: _table escapes a `|` in a cell as `\|`; the dem_seams field (Seam.entry) keeps the name as listed. - assemble (review finding B4): the previous tile's `array`/`incoming` are dropped before the next load. tracemalloc peak on the acceptance box (--bbox 799750 7849750 900250 7950250, nine DTM10 tiles, 404 MB canvas, 102 MB tiles): 822 MB (canvas + 4.1 tiles) before, 720 MB (canvas + 3.1 tiles) after. One DTM10 load alone peaks at 2.8 tiles (decoder buffers plus DemTile's copy), so the docstring now states that, not "one tile". Co-Authored-By: Claude Opus 5.5 --- src_python/tin_engine/mosaic.py | 38 ++++++++++++++++++++++----------- src_python/tin_engine/stats.py | 3 ++- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src_python/tin_engine/mosaic.py b/src_python/tin_engine/mosaic.py index eda456b0..e696c803 100644 --- a/src_python/tin_engine/mosaic.py +++ b/src_python/tin_engine/mosaic.py @@ -32,6 +32,10 @@ if TYPE_CHECKING: from tin_engine.io.repository import TileFootprint +#: DEM units (metres for DTM10). A seam counts a node only where the two +#: tiles differ by at least this (Ola, 2026-09-28: "Ignore below 1mm"). +SEAM_THRESHOLD = 0.001 + #: Cells. Two tiles share a lattice when their node offsets are integers to #: within this. Measured noise is at most 2.2e-11 cells (B3), and 0 for DTM10. ALIGN_TOLERANCE = 1e-6 @@ -104,8 +108,9 @@ class MosaicPlan(BaseModel): @dataclass(frozen=True, slots=True) class Seam: - """Two tiles whose valid values differ at `nodes` of the mosaic's nodes, - by `largest` and `median` there (float64); `first < second` by name.""" + """Two tiles whose valid values differ by at least `SEAM_THRESHOLD` at + `nodes` of the mosaic's nodes, by `largest` and `median` there (float64); + `first < second` by name.""" first: str second: str @@ -230,11 +235,16 @@ def assemble(plan: MosaicPlan, load: Callable[[str], DemTile]) -> Mosaic: to `DemTile` without a copy (R7). A hand-built footprint may under-state its dtype, so a loaded tile that promotes further re-casts the canvas. - Each tile is written whole, then dropped; what it holds where it meets - another tile's placement is kept as a strip. Once all are in, every - overlap is decided from the strips (Ola's Q1 revised, `_decide`) and each - pair that disagrees is reported (`_seam`). Beyond the canvas and one - loaded tile, the peak is the strips plus one overlap's temporaries. + Each tile is written whole, then dropped before the next load; what it + holds where it meets another tile's placement is kept as a strip. Once all + are in, every overlap is decided from the strips (Ola's Q1 revised, + `_decide`) and each pair that disagrees is reported (`_seam`). + + Memory: the peak is the canvas, the strips, and one load's own peak, which + is not one tile: decoding a DTM10 tile peaks at about 2.8 tiles (the + decoder's buffers and `DemTile`'s read-only copy). Measured with + tracemalloc on the 15a acceptance box (nine DTM10 tiles, 404 MB canvas, + 102 MB tiles): 720 MB, the canvas plus 3.1 tiles. """ if len(plan.tiles) == 1 and plan.tiles[0].meta == plan.meta: return Mosaic(tile=_loaded(plan.tiles[0], load), plan=plan) @@ -255,6 +265,7 @@ def assemble(plan: MosaicPlan, load: Callable[[str], DemTile]) -> Mosaic: box = _meet(c, other.canvas) if other.name != placement.name and box is not None: strips[placement.name, other.name] = incoming[_within(box, c)].copy() + del array, incoming # before the next load, or two tiles outlive this one seams = [] for i, a in enumerate(ordered): for b in ordered[i + 1 :]: @@ -511,13 +522,14 @@ def _seam( first: str, second: str, a: npt.NDArray[Any], b: npt.NDArray[Any], plan: MosaicPlan ) -> Seam | None: """The pair's report over its overlap in the mosaic: nodes where both hold - a valid value and the two differ (`!=`), and the largest and median - `|a - b|` over those, in float64. None when they agree.""" - a64, b64 = a.astype(np.float64), b.astype(np.float64) - differ = _valid(a, plan.meta.nodata) & _valid(b, plan.meta.nodata) & (a64 != b64) - if not differ.any(): + a valid value and `|a - b| >= SEAM_THRESHOLD`, and the largest and median + `|a - b|` over those, in float64. None when no node qualifies. The + threshold is the report's only: `_decide` never consults it.""" + both = _valid(a, plan.meta.nodata) & _valid(b, plan.meta.nodata) + gaps = np.abs(a[both].astype(np.float64) - b[both].astype(np.float64)) + gaps = gaps[gaps >= SEAM_THRESHOLD] + if not gaps.size: return None - gaps = np.abs(a64[differ] - b64[differ]) return Seam(first, second, int(gaps.size), float(gaps.max()), float(np.median(gaps))) diff --git a/src_python/tin_engine/stats.py b/src_python/tin_engine/stats.py index 60ab9a50..367835ca 100644 --- a/src_python/tin_engine/stats.py +++ b/src_python/tin_engine/stats.py @@ -178,8 +178,9 @@ def _bytes(n: int) -> str: def _table(header: Sequence[str], rows: Sequence[Sequence[str]]) -> list[str]: + """A Markdown table; a `|` in a cell (a tile name) is escaped `\\|`.""" lines = ["| " + " | ".join(header) + " |", "|" + "---|" * len(header)] - return lines + ["| " + " | ".join(row) + " |" for row in rows] + return lines + ["| " + " | ".join(c.replace("|", "\\|") for c in row) + " |" for row in rows] def _sizes(s: Sizes) -> list[str]: From a253a772815af147cc9fed34a0a2169e9cb6bcae Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 08:55:14 +0200 Subject: [PATCH 26/28] 15a review fixes: why the export date is the side files' (measured), the per-load memory range Co-Authored-By: Claude Opus 5.5 --- docs/increments/15-dem-mosaic.md | 9 ++++++--- docs/increments/15-probes/dtm10_dates.py | 6 +++--- src_python/tin_engine/mosaic.py | 5 +++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 754b92ec..9f9469ff 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -31,9 +31,12 @@ are designed, not implemented; Q6-Q10 are open. Written by `@architect` before ("b", Ola).** Measured first, and widened after review with the committed probe `15-probes/dtm10_dates.py` (120 random neighbour pairs, seed 1; a tile's *export date* is the date of its side files, `.tif.aux.xml` or - `.tfw`; the `.tif` files' own dates are the download date and the TIFF tags - carry none): of 42 same-date pairs, 40 agree exactly and 41 within 1 mm, but - 7204_4 | 7304_3 differs at 156,379 nodes by up to 5.07 m; of 77 + `.tfw`, which agree on all 254 tiles; 143 of the 254 `.tif` files share one + date, 2021-12-10, later than their side files, so the `.tif` date does not + tell the exports apart, and the TIFF tags carry none). Of 120 sampled pairs, + 119 share a valid node: of 42 same-date pairs, 40 agree exactly and 41 + within 1 mm, but 7204_4 | 7304_3 differs by 1 mm or more at 156,379 nodes, + by up to 5.07 m; of 77 different-date pairs, 49 agree within 1 mm and 28 do not, up to 52.1 m (6500_1 | 6500_2). So a shared date makes agreement likely, not certain. The differences have a mean near 0, are often exactly 0 on flat ground and diff --git a/docs/increments/15-probes/dtm10_dates.py b/docs/increments/15-probes/dtm10_dates.py index 6dd559f1..ca7edd71 100644 --- a/docs/increments/15-probes/dtm10_dates.py +++ b/docs/increments/15-probes/dtm10_dates.py @@ -2,9 +2,9 @@ Evidence for "Ruled by Ola", Q1 revised (docs/increments/15-dem-mosaic.md). The export date of a tile is the modification date of its side files -(`.tif.aux.xml`, else `.tfw`), which carry Kartverket's per-tile date. The -`.tif` files' own dates are the download date and say nothing. The TIFF tags -carry no date at all. +(`.tif.aux.xml`, else `.tfw`; the two agree on all 254 tiles). 143 of the +254 `.tif` files share one date, 2021-12-10, later than their side files, so +the `.tif` date does not tell the exports apart. The TIFF tags carry no date. Usage: python dtm10_dates.py [ARCHIVE] [N_PAIRS] [SEED] Prints, for N random neighbour pairs on one lattice, whether they share an diff --git a/src_python/tin_engine/mosaic.py b/src_python/tin_engine/mosaic.py index e696c803..ff8d46b2 100644 --- a/src_python/tin_engine/mosaic.py +++ b/src_python/tin_engine/mosaic.py @@ -241,8 +241,9 @@ def assemble(plan: MosaicPlan, load: Callable[[str], DemTile]) -> Mosaic: `_decide`) and each pair that disagrees is reported (`_seam`). Memory: the peak is the canvas, the strips, and one load's own peak, which - is not one tile: decoding a DTM10 tile peaks at about 2.8 tiles (the - decoder's buffers and `DemTile`'s read-only copy). Measured with + is not one tile: decoding a DTM10 tile peaks at 2.0 to 3.0 tiles, + depending on the tile (the decoder's buffers and `DemTile`'s read-only + copy). Measured with tracemalloc on the 15a acceptance box (nine DTM10 tiles, 404 MB canvas, 102 MB tiles): 720 MB, the canvas plus 3.1 tiles. """ From c626ef5babfd056b6b7843b9bc279229e8004bfe Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 09:03:26 +0200 Subject: [PATCH 27/28] =?UTF-8?q?CLAUDE.md=20=C2=A72:=20blank=20lines=20do?= =?UTF-8?q?=20not=20count=20(Ola);=2020b's=20contrary=20note=20marked=20su?= =?UTF-8?q?perseded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 8 +++++--- docs/increments/20b-min-insertion-distance.md | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a819734e..2f1d081c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,9 +14,11 @@ This project is governed by specialized sub-agents. Always defer tasks to the co ## 2. Core Constraints & Technical Mandates * **Strict Size Limit:** Under **700 lines of production code per pull request**, - where a line counts unless it is a comment, a docstring, or the body of a raw - literal; tests excluded. The exclusions exist so the ceiling does not penalise - the comment density this project asks for. Lines count as written: packing + where a line counts unless it is blank, a comment, a docstring, or the body of + a raw literal; tests excluded. The exclusions exist so the ceiling does not + penalise the comment density this project asks for, and blank lines add no + reading (Ola, 2026-09-28; before that, 20b counted blank lines). The count is + of lines a PR adds or changes. Lines count as written: packing code by hand under `# fmt: skip` / `# fmt: off` is allowed, provided the packed lines stay readable and the review says why each new region is packed (Ola, 2026-09-27, on `tools/bench.py`). This is the only statement of diff --git a/docs/increments/20b-min-insertion-distance.md b/docs/increments/20b-min-insertion-distance.md index d3d83419..105ec11c 100644 --- a/docs/increments/20b-min-insertion-distance.md +++ b/docs/increments/20b-min-insertion-distance.md @@ -421,7 +421,8 @@ below included (3 of them); 90 in `refine.hpp` against ~70. Method: for each production file the branch touches (`include/`, `bindings/`, `src_python/`), count the lines at the branch tip and at `d32ce59` (the last increment-20 commit), skipping comments, docstrings and raw-literal bodies per `CLAUDE.md` -§2; blank lines count. The figure is the difference. Settled at green: `--no-constraint-feet` needs +§2; blank lines count (superseded 2026-09-28: `CLAUDE.md` §2 now excludes +blank lines). The figure is the difference. Settled at green: `--no-constraint-feet` needs `--tolerance` and `--dem`, like `--start-min-angle`; only the first constrained edge closer than ε is tried; a refusal is counted only when `N` is then inserted; when `G` is 0, ε is the cap (otherwise 0/0 at tolerance 0 on From 201a4e7a4f6b924c2871e8f775b0f47dd9e90d4d Mon Sep 17 00:00:00 2001 From: Ola Skavhaug Date: Mon, 28 Sep 2026 09:05:57 +0200 Subject: [PATCH 28/28] =?UTF-8?q?CLAUDE.md=20=C2=A72:=20only=20Ola's=20rul?= =?UTF-8?q?ing=20(blank=20lines=20excluded);=2015a's=20LOC=20at=20the=20ti?= =?UTF-8?q?p?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 4 ++-- docs/increments/15-dem-mosaic.md | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2f1d081c..2cdca9bd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,8 +17,8 @@ This project is governed by specialized sub-agents. Always defer tasks to the co where a line counts unless it is blank, a comment, a docstring, or the body of a raw literal; tests excluded. The exclusions exist so the ceiling does not penalise the comment density this project asks for, and blank lines add no - reading (Ola, 2026-09-28; before that, 20b counted blank lines). The count is - of lines a PR adds or changes. Lines count as written: packing + reading (Ola, 2026-09-28; before that, 20b counted blank lines). Lines count + as written: packing code by hand under `# fmt: skip` / `# fmt: off` is allowed, provided the packed lines stay readable and the review says why each new region is packed (Ola, 2026-09-27, on `tools/bench.py`). This is the only statement of diff --git a/docs/increments/15-dem-mosaic.md b/docs/increments/15-dem-mosaic.md index 9f9469ff..8736b157 100644 --- a/docs/increments/15-dem-mosaic.md +++ b/docs/increments/15-dem-mosaic.md @@ -860,6 +860,7 @@ order. 15c and 15d follow later. | | **15a total** | **375** | **520** | | | *15a as built, measured at review (branch diff, CLAUDE.md §2 unit): 533, of which `mosaic.py` 325 against 200 estimated* | | | | | *15a after Ola's Q1 revised (cba0073): 596 added against master, of which `mosaic.py` 372 against 200 estimated* | | | +| | *15a at the tip (a253a77): 597 added against master, blank lines excluded (`CLAUDE.md` §2 as ruled by Ola 2026-09-28); 554 net* | | | | **15b** | **Norway: the domain in its own CRS** | | | | | `crs.py`: `parse_crs`, `reprojector` | 30 | | | | `domain.py`: `crs: str`, `to_crs`, extent against coverage; `check_crs` removed | 40 | |