# Changelog — RiverBankErosionAccretion_V2

## v4.1 (2026-07-09) — Grid-clip tiling fix for hyper-braided stress test
### Fixed
- **`compute_per_grid_change`'s grid-clip step did not complete on the Brahmaputra/Majuli hyper-braided stress test** (~2.13M cells, Year1/Year2 split into 700+ disjoint parts): a single whole-grid `gpd.overlay(raw, grid, 'intersection')` call did not finish in a 25-minute session budget, even after the v4.0 vectorization (which was verified only up to ~2.19M cells on less complex geometry). Added `_overlay_intersection_tiled` + `_tile_edges` to `change_analysis.py`: splits the fishnet into geographic tiles (`GRID_TILE_THRESHOLD` = 150,000 cells, `TARGET_CELLS_PER_TILE` = 5,000) and, per tile, truncates candidate raw polygons to that tile's bbox (padded by one cell size, to avoid clipping off area for cells straddling a tile edge) before overlaying against just that tile's cells.
  - A first attempt tiled the grid but queried the *un-truncated* raw polygons per tile (via `.cx[]` only); this was **slower** than no tiling (2430s vs. an unmeasured but comparably long hang), because a single differencing sliver spanning the whole corridor got re-intersected at full complexity against every tile it touched. Truncating the raw polygons first (so each tile only processes its own small fragment) was the actual fix.
  - A bulk-sindex-query + vectorized `shapely.intersection` alternative (bypassing `gpd.overlay` entirely) was also tried and rejected — 2021s at full scale, still working with un-truncated full-complexity polygons.
  - Verified numerically identical to a direct (non-tiled) overlay on a subset test (exact area match, 0.000000% diff) after fixing an initial version that omitted the tile-edge margin and silently lost a small amount of area for boundary-straddling cells.
- **Verified against all three brief test datasets** with the fix in place:
  - Narmada (376,857 cells): erosion 82.546 km² / accretion 6.795 km² — exact match to the pre-refactor baseline, 247.5s (unchanged).
  - Palasbari (382,284 cells): erosion 58.552 km² / accretion 85.561 km² — unchanged, and now 106.2s vs. the previous 793.3s (braided geometry hit the same complexity trap at smaller scale).
  - Majuli (2,131,871 cells): grid-clip step ~328s (down from not completing in 25 min); full `run_module1()` ~1175s (~19.6 min); erosion 617.125 km² / accretion 259.532 km², Module 2 correctly auto-disabled (BI up to 18.46). The gap between the clip step and the full `run_module1()` time is stretch-aggregation/braiding-index computation, out of scope for this fix.
- README "Known limitation" section updated to describe the fix instead of "not yet implemented".
- **Crash on opening the dialog at all**, unrelated to the grid-clip fix above: `riverbank_erosion_accretion_v2_dialog.py`'s `_build_run_section()` called `layout.addLayout(self._row(self.btnStart, self.btnCancel))`, but `_row()` deliberately wraps its `QHBoxLayout` in a `QWidget` container and returns that widget (the correct form for its other call site, `form.addRow(label, self._row(edit, btn))`). `addLayout()` rejects a `QWidget` argument, so clicking the plugin's toolbar icon raised `TypeError: addLayout(self, QLayout, stretch: int = 0): argument 1 has unexpected type 'QWidget'` every time, before any analysis code ran. Found via a real QGIS 3.30/OSGeo4W install (Python 3.9.5, geopandas 0.12.2) after installing the packaged v4.1 zip — reproduced outside the GUI by constructing the dialog directly against a `QApplication` instance, confirmed fixed by changing the call to `addWidget()` (matching every other use of `_row()` in the file), and re-verified the dialog now constructs cleanly in the same environment.
- **Dialog window not resizable, making the Run section (including Start Analysis) unreachable on some screens**: `__init__` called only `self.resize(580, 820)` with no window flags, no size grip, and no scroll fallback. On Windows, `QDialog` renders without a resize border/maximize button unless explicitly requested, so an 820 px-tall dialog that didn't fit the available screen or a non-maximized QGIS window left the bottom of the form permanently out of reach with no way to grow the window into view. Fixed by adding `Qt.WindowMinMaxButtonsHint` to the window flags, enabling `setSizeGripEnabled(True)`, setting an explicit `setMinimumSize(480, 360)`, and wrapping all five sections in a `QScrollArea` (`setWidgetResizable(True)`) inside `_build_ui`, so every control stays reachable by scrolling even if the window itself can't be grown large enough. Verified: window flags carry `WindowMinMaxButtonsHint`, the scroll area is present and resizable, and shrinking the dialog to 500×300 correctly clamps to the 480×360 minimum rather than clipping content.
- **Module 2 (transect) incorrectly auto-disabled in the dialog for ALL rivers, due to faulty quick_bi_estimate**: The dialog's `quick_bi_estimate()` function was using the perimeter of a merged union of all Year1+Year2 geometries as the "wetted perimeter" input to BI. For any river with multiple separate water polygons, merging them creates a huge outer boundary plus internal edges where polygons touch, vastly inflating the BI (e.g., Narmada got BI = 20.8 instead of 0.957, Palasbari = 27.7, Majuli = 48.6). Fixed `transect_module.py`'s `quick_bi_estimate()` to sum individual polygon perimeters instead of merged.length; this corrects the numerator (though the function still lacks true stretch length for accurate BI, it no longer wildly overestimates). Also refactored the dialog's `_on_layers_changed()` to NOT auto-disable Module 2 based on quick_bi_estimate — the checkbox is now always enabled for manual user choice, and the authoritative BI-based auto-disable gate runs AFTER Module 1 completes (when real per-stretch BI is available). Added explanatory text to the label so users understand Module 2 is optional and its availability is determined post-analysis.

## v4.0 (2026-07-08) — Three-Module Architecture (journal resubmission)
### Added
- **Module 3 (uncertainty_module.py)** — JRC occurrence-threshold sensitivity (70/85/100%), per-cell/per-stretch min/max/range/mean erosion & accretion envelopes, degrades gracefully to single-threshold when only the standard pair is supplied
- **Multi-epoch rate acceleration** — `RA = (rate_interval2 - rate_interval1) / rate_interval1`, per-stretch and whole-reach
- **5-class regime taxonomy (regime_classifier.py)** — REGULATED_SINGLE_THREAD / MEANDERING / BRAIDED / HYPER_BRAIDED / DELTAIC, replacing the old BRAIDED/MEANDERING/TRANSITIONAL taxonomy
- **Along-reach centerline ordering** — grid cells are now grouped into stretches by true along-channel distance (Voronoi-skeleton centerline projection), giving real `From_m`/`To_m` and a sinuosity index, instead of raster fishnet-scan order
- **Dynamic riparian buffer is now the only mode** — `max(channel_width x 2, 100 m)` from Year1 hydraulic width; the buffer is no longer a user-editable spinbox
- **MMU is now a polygon-area pre-filter** (1/2/5 ha/custom) applied to Year1/Year2 before grid analysis, combined with always-on connected-component labelling — replaces the old post-hoc contiguous-grid-cell filter
- **Module 2 two-tier auto-disable** — cheap BI hint on layer load, authoritative gate after Module 1 runs (BI ≥ 3 or channel clusters > 3 disables the module)
- **output_manager.py** — brief-exact folder tree (`Grid/Stretch/Classification/Transect/Uncertainty/Summary`), shapefile field-name abbreviation for the 10-char DBF limit with full names preserved in CSV, `metadata.json`, `Analysis_Log.txt`, auto-symbology on load (guarded, no-op outside QGIS)
- **input_validator.py** — pre-run layer/CRS/year/overlap checks and post-run output sanity checks, separate from the existing JRC-accuracy `validation.py`
- **5-section GUI** rebuilt programmatically in `riverbank_erosion_accretion_v2_dialog.py` (inputs, Module 1 params always-on, Module 2 with auto-disable, Module 3 with 6 JRC file pickers + multi-epoch sub-panel, progress bar + log)

### Fixed (found via real-data verification against the brief's Narmada/Palasbari/Majuli test datasets)
- `create_fishnet` built one Shapely object per cell across the corridor's *bounding box* before clipping — for a diagonal reach this produced tens of millions of candidate objects and exhausted memory. Added a `shapely.vectorized` numpy pre-filter.
- `compute_per_grid_change` used a per-cell Python loop, which did not complete within 25 minutes on a ~2.19M-cell corridor. Rewritten around bulk `geopandas.overlay` difference/intersection operations — verified 3.2x faster and numerically identical on the Narmada baseline (82.546 / 6.795 km² erosion/accretion).
- The fishnet-to-corridor clip used an unprepared `.intersects()` against a complex multi-part corridor geometry, which alone did not finish in 25+ minutes on the same dataset. Fixed with `shapely.prepared.prep()` (22.4s afterward).
- **Known remaining limitation** (resolved in v4.1): on extreme corridors (>2M cells, input polygons split into 700+ disjoint parts), the grid-clip of the raw Year1/Year2 difference polygons was still too slow to complete in a 25-minute session budget. Fixed by the tiled/truncated overlay rewrite in v4.1 — see README "Known limitation" section.

### Changed
- Module 2 logic (centerline, transects, rates, consistency index) extracted from `change_analysis.py` into `transect_module.py`
- Consistency Index rewritten to the brief's exact formula/thresholds (0.7-1.3 CONSISTENT / <0.7 GRID_EXCEEDS_TRANSECT / >1.3 TRANSECT_EXCEEDS_GRID)
- Top-N range changed from 5-100 (default 15) to 1-30 (default 15)
- Fishnet cell size fixed at 30 m (no longer a GUI parameter)
- Braiding index now computed unconditionally per stretch, not gated by a user-selected river type
- All module functions return GeoDataFrames/DataFrames instead of writing files directly — writing is now centralized in `output_manager.py`

## v3.1 (2026-06-18) — Bug Fixes
### Fixed
- **Transect geometry alignment bug**: each result row now stores its own geometry
  at collection time, so that transects skipped via `continue` (no waterbody
  intersection) no longer shift the geometry index and mis-assign rates to the
  wrong transect location. Previously, `Transect_Rates.shp` could have rates
  attached to incorrect geographic positions whenever any transect was skipped.
- **Consistency Index unit mismatch**: `transect_erosion_m2` proxy now multiplied
  by `years_interval` so it is in m² (total erosion) rather than m²/yr, making it
  directly comparable to `grid_erosion_m2`. `years_interval` is now a named
  parameter of `compute_consistency_index()`.
- **MMU filter km2 column not zeroed**: sub-MMU cells now correctly zero both
  `eros_m2`/`accr_m2` and their `_km2` counterparts by capturing the boolean mask
  before the first assignment rather than re-evaluating after zeroing.

## v3.0 (2026-05-30) — Regime-Adaptive Architecture
### Added
- **River morphological type selector** (Meandering / Braided / Deltaic / Straight / Anastomosing) — algorithm configuration adapts automatically per regime
- **Riparian buffer masking** — Union(T1,T2) → Buffer → clip fishnet; excludes non-river waterbodies from analysis (user-configurable, 100–5000 m)
- **Minimum Mapping Unit (MMU) filter** — contiguous patch threshold (default 3 cells = 0.0027 km²); removes sub-detection-limit artifacts
- **Voronoi skeleton centerline extraction** — replaces convex-hull method; works on sinuous and complex reaches; fallback to hull for straight reaches
- **Braiding Index auto-computation** — BI = wetted_perimeter / (2 × reach_length); per-stretch output with centerline_reliability flag (HIGH/MODERATE/LOW)
- **Connected Component Labelling (CCL)** — channel_id per distributary for deltaic regimes; min_polygon_area_ha filter excludes non-riverine features
- **Regime classification output** — stretch_regime_class.shp classifying each stretch as BRAIDED / MEANDERING / TRANSITIONAL based on BI, width:length ratio, and component count
- **Grid-Transect Consistency Index** — CI = transect_erosion_area / grid_erosion_area per stretch; confidence flag (HIGH/MODERATE/LOW)
- **Years interval parameter** — user-specified years between epochs (replaces hardcoded 10 years)
- **New output layers:** Riparian_Corridor.shp, PerGrid_MMU_Filtered.shp, Stretch_Regime_Class.shp, GridTransect_Consistency.csv, labelled waterbody shapefiles (deltaic)

### Changed
- **Plugin UI redesigned** — River Settings group, regime-adaptive parameter toggling, MMU threshold spinbox
- `change_analysis.py` expanded from 436→786 lines — 6 new functions
- `extract_centerline()` now uses Voronoi skeleton (Shapely 2.0+) as primary method
- GEE script removed from plugin (standalone companion file)
- Pipeline passes river_type, buffer_m, mmu_cells, years_interval, min_polygon_area_ha through all stages

### Fixed
- Hardcoded `years_interval=10` → user-configurable (was producing wrong rates)
- Fishnet spanned full extent → clipped to riparian corridor (reduces cells 40–70%)
- Convex-hull centerline produced incorrect geometry for sinuous reaches → Voronoi skeleton

## v2.0 (2026-05-15) — Grid + Transect Analysis
- Grid-based per-cell erosion/accretion computation
- Stretch aggregation with Top-N ranking
- Transect-based bank retreat rates (convex-hull centerline)
- GEE companion script tab (placeholder)
- CRS projection selection widget
- Configurable cell size (5–500 m)

## v1.0 — Original Plugin
- Basic polygon overlay differencing
- Manual input only
- No regime awareness, no buffer, no MMU, no transect rates
