### WRaP CA Engine ### Project Identity ``` Language: Java 21 (Temurin 21.0.10) Framework: Spring Boot 4.0.3 Build: Maven Base package: com.victorkithinji.wrap.wrapca Project name: WrapCa ``` > **[!IMPORTANT]**: > _Consider going through the following documents to better understand the project_ > > : [`Documentation`](https://github.com/VictorCodebase/wrapca/blob/5f11456315dd6eefd8f50423e133484a84addfef/docs/Wildfire%20Risk%20and%20Progression%20Modelling%20Documentation.pdf) (not opening? look at > ./docs/) > : [`SRD` (Software Requirements and Design)](https://docs.google.com/document/d/11iYsi2c2p8eT4DeZwPl7QybqKE4Uoj_q/edit?usp=sharing&ouid=105294404014185386116&rtpof=true&sd=true) (ctrl + click to open doc on new tab) --- ### What this system is WRaP is a two-phase wildfire CA engine exposed as a Spring Boot REST API. A Chromium frontend consumes the API — all computation is Java-side. This is part of the Wildfire Risk and Progression Modelling project with the repositories: | Name | Stack | Desciption | |------|-------|-----------------------------------------------------------------------------------------------------------------------| |Wrap Ca Engine| Java 21, Springboot | (This repo) This is the solution's backend | | Wrap UI | ReactJS, Vite | [Repository Link] (https://github.com/VictorCodebase/wrap-ui). This offers an interactive Chromium UI for the backend | Wrap CA operates in two phases: **Phase 1 (pre-fire):** Monte Carlo ensemble of CA runs to produce two output layers — ignition probability map ( smoothed I(c) index) and damage potential map (burn frequency across N runs). **Phase 2 (active fire):** Rothermel-embedded CA spread simulation. CV corrections injected at each satellite overpass to prevent error compounding. The CA grid is a 2D array of 100m cells. Each cell holds a state (UNBURNED / BURNING / BURNED / NON_COMBUSTIBLE) and an environment vector (NDVI, NDMI, slope, aspect, vegetation type). Only cells with at least one BURNING neighbour are evaluated per generation — this is the core efficiency constraint. --- _these images have been fetched from the official documentation. Click [Documentation](docs/Wildfire%20Risk%20and%20Progression%20Modelling%20Documentation.pdf) to open the full documentation_ > Overall architecture ![Screenshot_20260826_155341.png](docs/images/Screenshot_20260826_155341.png) > WrapCa architecture ![Screenshot_20260826_155905.png](docs/images/Screenshot_20260826_155905.png) ### Project The project is broken into the following modular sections ``` api/ → HTTP only, no logic facade/ → startup orchestration, mode detection grid/ → CA grid domain objects ingestion/ → GeoTIFF reading, wind loading, ESA reading, road geometry loading, data cache rothermel/ → pure fire physics, no Spring dependencies simulation/ → CA engine, Moore neighbourhood, active frontier montecarlo/ → Phase 1 ensemble runner correction/ → CV re-injection, suppressed zone tracking output/ → result assembly, perimeter extraction history/ → JSON run persistence dto/ → API request/response shapes only config/ → reads application.properties into typed beans cvintegration/ → HTTP client to CV module, mode detection boundary ``` The modules' dependency on each other is listed below, modules listed in a group exist largely independent of each other, however modules lower in the list are largely dependent on those higher in the list. This grouping is what informs this project's CI. | Group | Packages | Why grouped | |-------|----------|--------| | 1 | grid, rothermel | Pure Java, zero Spring, fastest, foundational | | 2 | config | Spring context must bind before anything else runs | | 3 | ingestion, cvintegration | External data boundary, both I/O-heavy | | 4 |grid (init), simulation | The CA engine core | | 5 | montecarlo, correction | Both consume the engine, independent of each other | | 6 | output, history, dto | Pure transformation/serialization, no simulation logic | | 7 | facade, api | Full wiring — this is where CORS and REST/JSON actually get exercised | --- ### application.properties ```properties spring.application.name=WrapCa server.port=8080 wrap.data.root=./data wrap.simulation.cell-size-metres=100 wrap.simulation.time-step-minutes=5 wrap.simulation.monte-carlo-runs=200 wrap.simulation.thread-pool-size=8 wrap.simulation.phase1-horizon-hours=24 wrap.cv.geotiff-path=./data/geotiff/latest_cv_output.tif wrap.cv.base-url=http://localhost:5000/api/cv wrap.cv.stub-mode=true wrap.data.esa-path=./data/esa/esa_worldcover.tif wrap.data.roads-path=./data/osm/roads.geojson ``` --- ### Implementation order This project is implemented in the sequence below. Each group depends on the previous. --- #### GROUP 1 — Domain foundation (no Spring, pure Java) *These have zero dependencies on anything else in the project.* **1. `grid/CellState.java`** Enum: `UNBURNED, BURNING, BURNED, NON_COMBUSTIBLE` **2. `grid/CellEnvironment.java`** Data class (Lombok `@Value` — immutable). Fields: `float ndvi, ndmi, slopeRadians, aspectRadians` and a `VegetationType` enum reference. This is the static per-cell environmental vector assigned at grid init and refreshed by CV correction. **3. `grid/VegetationType.java`** Enum: `AFROMONTANE_FOREST, GRASSLAND, SHRUBLAND, BARE_SOIL, WATER, BUILT, CROPLAND`. Note: `GRASSLAND` (not `MONTANE_GRASSLAND` — see DEV-003). `CROPLAND` is appended last (see DEV-004). Do not reorder constants — ordinal stability is required for API responses. `WATER` and `BUILT` are non-combustible. `CROPLAND` uses grassland-equivalent fuel parameters. All combustible types must have a matching entry in `east_africa_fuel_models.json`. **4. `grid/CaGrid.java`** Holds: `int[][] states` (using CellState ordinals for speed), `CellEnvironment[][] environment`, `int rows`, `int cols`, `double cellSizeMetres`. No Spring annotations. This object is the simulation's entire spatial state. --- #### GROUP 2 — Fire physics (no Spring, pure Java, independently testable) *Implement and unit test these against known Rothermel values before touching the engine.* **5. `rothermel/FuelModelResolver.java`** Maps `VegetationType` → fuel parameters (load, moisture of extinction, heat content, SAV ratio). Values come from `fuelmodels/east_africa_fuel_models.json` in resources. Must include entries for all combustible types including `CROPLAND` (grassland-equivalent values). Keep a static lookup — no database, no complexity. **6. `rothermel/WindProjectionCalculator.java`** Given a wind vector (speed + direction in degrees) and a Moore direction index (0–7), returns the effective wind component Ue along that direction. Negative projections clamped to zero. **7. `rothermel/SlopeEffectCalculator.java`** Given elevation of source cell and target cell plus distance, returns slope angle φs. Distance is `cellSize` for cardinal directions, `cellSize × √2` for diagonals. **8. `rothermel/RothermelRosCalculator.java`** Pure static methods. Takes fuel params, Ue, φs → returns ROS in metres per minute. This is the simplified Rothermel ( 1972) surface fire formula. No Spring annotations. Validate against Andrews (2018) reference values before proceeding to Group 3. --- #### GROUP 3 — Configuration (Spring, simple) **9. `config/SimulationConfig.java`** `@Configuration @ConfigurationProperties(prefix = "wrap.simulation")`. Lombok `@Data`. Fields: `cellSizeMetres`, `timeStepMinutes`, `monteCarloRuns`, `threadPoolSize`, `phase1HorizonHours`. **10. `config/CorsConfig.java`** `@Configuration`. Permits localhost origins during development. One method, ~10 lines. --- #### GROUP 4 — Ingestion (Spring services, external data boundary) **11. `ingestion/IngestionCacheService.java`** Checks `data/cache/` for a file matching today's date before triggering a re-fetch of the CV fuel state GeoTIFF. Returns `Optional` for the CV fuel state. Also exposes existence-only cache methods for ESA and road layers: `getCachedEsaLayer()`, `storeEsaLayer(byte[])`, `getCachedRoadLayer()`, `storeRoadLayer(String)`. ESA and road caches never expire — these files do not change on a regular schedule. Existing CV fuel state methods check by date as before. **12. `ingestion/GeoTiffBandReaderService.java`** Two read methods. `read(Path tiffPath)` reads the CV fuel state GeoTIFF — extracts 5 bands from an 11-band file: NDVI ( index 5), NDMI (6), elevation (8), slope (9), aspect (10). Band selection governed by `BandLayout` constants class internal to this package. Returns `GridBands` at native 10m resolution. `readEsa(Path esaTiffPath)` reads the ESA WorldCover GeoTIFF and returns `EsaBands` holding `int[][] classCode` and spatial metadata. Band indices and ESA class code mappings are in `EsaBandLayout` constants class internal to this package. CRS confirmed EPSG:32737. Native pixel size confirmed 10m. **13. `ingestion/WindFieldLoaderService.java`** Loads ERA5 wind data from a local stub file. Interpolates to CA grid resolution. Returns a `WindField` object: two `float[][]` arrays for speed and direction per cell. Pass post-resampling rows and cols so WindField dimensions match the CA grid. **14. `ingestion/FirePerimeterParserService.java`** Parses a CV-provided fire perimeter GeoJSON polygon into a `Set` of encoded cell indices ( `row * gridWidth + col`). This is the initial BURNING cell set for Phase 2. **15. `ingestion/OsmRoadLoaderService.java`** `@Service`. Reads a pre-downloaded GeoJSON file from `wrap.data.roads-path`. Parses road and path linestring geometry ( highway tags: track, path, unclassified, tertiary) into a `RoadLayer` object holding `List` of UTM 37S linestring coordinates. Does not call any external API at runtime — the GeoJSON file is downloaded once and stored locally. If the file is missing, logs a warning and returns an empty `RoadLayer`; the simulation proceeds with zero road proximity influence on I(c). --- #### GROUP 4.5 — CV Integration **16. `cvintegration/FirePerimeterData.java`** Data class first — no dependencies. Fields: `String perimeterGeoJson`, `List confirmedBurnedCellIndices`, `List suppressedZoneCellIndices`, `Map updatedMoistureValues`, `Instant observationTime`. Any field may be null or empty — all consumers must handle this without throwing. Empty `suppressedZoneCellIndices` is a valid and expected case. **17. `cvintegration/CvApiClient.java`** `@Service`. Wraps Spring `RestClient`. Two methods: `fetchLatestFuelState()` → `Optional` (downloads GeoTIFF to local cache via `IngestionCacheService`), `fetchLatestFirePerimeter()` → `Optional` (returns empty when CV returns 404 — this is the fire/no-fire signal). Both methods return `Optional.empty()` silently when `wrap.cv.stub-mode=true`. Neither method throws — all HTTP failures caught and logged as warnings. Properties consumed: `wrap.cv.base-url`, `wrap.cv.stub-mode`. --- #### GROUP 5 — Grid initialisation (Spring services) **18. `ingestion/RasterResamplerService.java`** Two resampling paths. Continuous bands path: accepts `GridBands` at native resolution and target cell size from `SimulationConfig`, returns new `GridBands` at target resolution using block-averaging across all five bands (NDVI, NDMI, elevation, slope, aspect). Categorical path: accepts `EsaBands` and target cell size, returns resampled `int[][]` class codes using majority-class resampling. Tie-breaking rule: prefer combustible ESA class over non-combustible. **19. `grid/GridInitialiserService.java`** Receives three inputs: resampled `GridBands` (from `RasterResamplerService`), resampled ESA `int[][]` class codes (from `RasterResamplerService`), and `RoadLayer` (from `OsmRoadLoaderService`). Resolves ESA class codes to `VegetationType` per cell using `EsaBandLayout` mappings — does not infer vegetation type from NDVI thresholds. Marks `NON_COMBUSTIBLE` for `WATER` (ESA code 80) and `BUILT` (ESA code 50). Does not derive slope or aspect from elevation — CV provides both directly. Computes `float[][] roadProximityMetres` — minimum distance from each cell centre to nearest road segment in `RoadLayer` — and retains it for handoff to `IgnitionLikelihoodIndexBuilder` in Group 7. --- #### GROUP 6 — Simulation engine (Spring services) **20. `correction/SuppressedZoneRegistry.java`** Build here, before the engine, because `CaSpreadEngine` depends on it. `@Service`. Holds `Map` of cell index → suppression expiry time. Methods: `register(long, Instant)`, `registerAll(Iterable, Instant)`, `isActive(long)` (lazy expiry removal on read), `clear()`, `size()`. Empty suppressed zone list is fully valid — `isActive()` returns false for all cells when registry is empty. This is the expected state when CV does not report suppression data. **21. `simulation/ActiveCellFrontierTracker.java`** Maintains a `HashSet` of cells that have at least one BURNING neighbour. Updated each generation — cells added when a neighbour ignites, removed when they become BURNED or all neighbours are BURNED. **22. `simulation/IgnitionProbabilityResolver.java`** For one target cell, iterates its BURNING neighbours, calls `RothermelRosCalculator` for each, computes Pₑ per neighbour, resolves combined ignition probability: `1 - ∏(1 - Pₑⱼ)`. Returns a double. **23. `simulation/MooreNeighbourEvaluator.java`** For a given cell coordinate, returns the 8 Moore neighbours with their direction indices and distances. Handles grid boundary checks. **24. `simulation/SimulationStepResult.java`** Data class (Lombok `@Value`). Fields: `Set newlyBurnedCells`, `int generation`, `Instant timestamp`. **25. `simulation/CaSpreadEngine.java`** The core engine. Per generation: iterates frontier cells via `ActiveCellFrontierTracker`, checks `SuppressedZoneRegistry` before evaluating any cell, calls `IgnitionProbabilityResolver`, resolves state transitions stochastically, updates grid, updates frontier, produces `SimulationStepResult`. Takes `CaGrid` + `WindField` as inputs. Used by both Phase 1 (Monte Carlo) and Phase 2 (active spread). --- #### GROUP 7 — Monte Carlo ensemble (Spring services) **26. `montecarlo/IgnitionLikelihoodIndexBuilder.java`** Computes I(c) per cell: weighted combination of normalised NDMI, historical FIRMS fire density, and road proximity. Human activity proximity input is `roadProximityMetres` array passed from `GridInitialiserService` — not derived from OSM at this stage. Output is a `float[]` probability weight array used for seeding. Runs once before ensemble. **27. `montecarlo/IgnitionSeedSampler.java`** Samples N ignition seed cells from the grid with probability proportional to I(c). Uses Commons Math for weighted sampling. Returns `List` of encoded cell indices. **28. `montecarlo/BurnFrequencyAccumulator.java`** Thread-safe accumulation of burn counts across N parallel runs. Uses `AtomicIntegerArray` sized `rows × cols`. **29. `montecarlo/MonteCarloEnsembleRunner.java`** Spawns N independent `CaSpreadEngine` instances via `ForkJoinPool`. Each run gets its own deep copy of `CaGrid` and a single ignition seed. Aggregates into `BurnFrequencyAccumulator`. Each task is fully independent — no shared mutable state between runs. **30. `montecarlo/RiskMapAssembler.java`** Converts `BurnFrequencyAccumulator` counts → normalised damage potential values per cell. Combines with smoothed I(c) to produce the dual-layer Phase 1 output. --- #### GROUP 8 — CV correction (Spring services) **31. `correction/CvStateInjectorService.java`** Applies CV observation layer to a running `CaGrid` in fixed order: (1) force confirmed BURNED cells, (2) register suppressed zones in `SuppressedZoneRegistry` and force those cells to `NON_COMBUSTIBLE`, (3) refresh NDMI for UNBURNED cells only. Steps 2 and 3 are skipped cleanly if their respective lists are empty or null — this is the normal case when CV does not report suppression data. --- #### GROUP 9 — Output assembly **32. `output/SimulationResultAssembler.java`** Converts `CaGrid` state + `List` → response DTOs. Populates `vegetationTypeOrdinals` in `PhaseOneResultResponse` by iterating `CaGrid.environment` and extracting `vegetationType.ordinal()` per cell. Produces compact JSON-friendly structures. Does not send GeoTIFF bytes over API. **33. `output/PerimeterPolygonExtractor.java`** Traces boundary between BURNED/BURNING and UNBURNED cells → GeoJSON polygon with timestamp. **34. `output/HeatmapRasterWriter.java`** Writes Phase 1 risk maps to GeoTIFF for file export only. Not called during normal API responses. --- #### GROUP 10 — History persistence (JSON files) **35. `history/RunRecord.java`** Lombok `@Value`. Fields: `String runId, SimulationPhase phase, Instant startedAt, Instant completedAt, Map parameters, String resultFilePath`. **36. `history/RunLogWriterService.java`** Serialises `RunRecord` to `data/runs/{timestamp}_{phase}.json` on simulation completion. No database. **37. `history/RunLogReaderService.java`** Lists `data/runs/`, deserialises each file, returns `List` sorted by date. --- #### GROUP 11 — DTOs **38. `dto/request/PhaseOneRunRequest.java`** No required fields. Optional wind speed/direction overrides for scenario testing. **39. `dto/request/PhaseTwoRunRequest.java`** Fields: `boolean cvDisabled`, `boolean manualIgnition`, `String manualIgnitionPolygonGeoJson` (nullable), `int simulationHours`. **40. `dto/request/CvCorrectionRequest.java`** Fields: `String observedPerimeterGeoJson`, `List suppressedZoneCellIds`, `Map updatedMoistureValues`. **41. `dto/response/SessionStatusResponse.java`** Fields: `SimulationMode mode` (PRE_FIRE / ACTIVE_FIRE), grid summary (rows, cols, cellSize, bounds), `List pastRuns`. **42. `dto/response/PhaseOneResultResponse.java`** Fields: `String runId`, `float[] damagePotentialValues`, `float[] ignitionProbabilityValues`, `int[] vegetationTypeOrdinals`, `int rows`, `int cols`. All three arrays are the same length (rows × cols). `vegetationTypeOrdinals` values are `VegetationType` enum ordinals. **43. `dto/response/PhaseTwoResultResponse.java`** Fields: `String runId`, `List perimetersByTimestamp` where each snapshot holds a GeoJSON polygon string and an ISO timestamp. --- #### GROUP 12 — Facade and API (wire everything together last) **44. `facade/WrapSessionFacade.java`** `@Service`. Startup sequence: checks cache → loads CV GeoTIFF via `CvApiClient` → loads ESA layer via `GeoTiffBandReaderService.readEsa()` → loads road layer via `OsmRoadLoaderService` → resamples via `RasterResamplerService` → initialises grid via `GridInitialiserService` → checks for fire perimeter via `CvApiClient.fetchLatestFirePerimeter()` → sets mode. Three CV poll triggers: (1) server startup, (2) manual via `POST /api/session/refresh`, (3) scheduled every 3 hours via `@Scheduled`. Mode detection: perimeter present → `ACTIVE_FIRE`, empty → `PRE_FIRE`. `manualIgnition=true` in request bypasses CV perimeter check entirely. **45. `api/GridController.java`** `@RestController`. Endpoints: `GET /api/session/status`, `POST /api/session/refresh`. **46. `api/SimulationController.java`** `@RestController`. Endpoints: `POST /api/simulation/phase-one/run`, `POST /api/simulation/phase-two/run`, `POST /api/simulation/phase-two/correct`. **47. `api/RunHistoryController.java`** `@RestController`. Endpoints: `GET /api/runs`, `GET /api/runs/{runId}`. --- ### Key implementation constraints to preserve - `RothermelRosCalculator` — static methods only, zero Spring annotations, unit test in isolation first - Cell coordinates encoded as `long = row * gridWidth + col` everywhere — never use `int[]` as map keys - Monte Carlo runs — each task owns a full deep copy of `CaGrid`, `AtomicIntegerArray` for accumulation - CV corrections are hard state overrides, not probabilistic adjustments - Empty `suppressedZoneCellIndices` from CV is valid and expected — `SuppressedZoneRegistry` handles it without error - API responses never contain raw grid arrays or GeoTIFF bytes — always compact numeric structures - `GeoTiffBandReaderService` reads exactly 5 bands from the 11-band CV file — band indices are constants in `BandLayout`, single file to update if CV changes its layout. CRS: EPSG:32737, native pixel size: 10m - ESA WorldCover class codes mapped to `VegetationType` via `EsaBandLayout` — single source of truth for that mapping - Road geometry loaded from pre-downloaded GeoJSON at `wrap.data.roads-path` — no runtime OSM API calls - Run history: flat JSON files in `data/runs/`, no database - `VegetationType` ordinal order is fixed — do not reorder enum constants