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(not opening? look at ./docs/)
:SRD(Software Requirements and Design) (ctrl + click to open doc on new tab)
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 to open the full documentation
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 |
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.geojsonThis project is implemented in the sequence below. Each group depends on the previous.
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.
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.
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.
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<Path> 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<Long> 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<long[][]> 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).
16. cvintegration/FirePerimeterData.java
Data class first — no dependencies. Fields: String perimeterGeoJson, List<Long> confirmedBurnedCellIndices,
List<Long> suppressedZoneCellIndices, Map<Long, Float> 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<Path> (downloads GeoTIFF to
local cache via IngestionCacheService), fetchLatestFirePerimeter() → Optional<FirePerimeterData> (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.
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.
20. correction/SuppressedZoneRegistry.java
Build here, before the engine, because CaSpreadEngine depends on it. @Service. Holds Map<Long, Instant> of cell
index → suppression expiry time. Methods: register(long, Instant), registerAll(Iterable<Long>, 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<Long> 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<Long> 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).
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<Long> 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.
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.
32. output/SimulationResultAssembler.java
Converts CaGrid state + List<SimulationStepResult> → 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.
35. history/RunRecord.java
Lombok @Value. Fields:
String runId, SimulationPhase phase, Instant startedAt, Instant completedAt, Map<String,Object> 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<RunRecord> sorted by date.
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<String> suppressedZoneCellIds,
Map<String,Float> updatedMoistureValues.
41. dto/response/SessionStatusResponse.java
Fields: SimulationMode mode (PRE_FIRE / ACTIVE_FIRE), grid summary (rows, cols, cellSize, bounds),
List<RunSummaryResponse> 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<PerimeterSnapshot> perimetersByTimestamp where each snapshot holds a GeoJSON polygon
string and an ISO timestamp.
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}.
RothermelRosCalculator— static methods only, zero Spring annotations, unit test in isolation first- Cell coordinates encoded as
long = row * gridWidth + coleverywhere — never useint[]as map keys - Monte Carlo runs — each task owns a full deep copy of
CaGrid,AtomicIntegerArrayfor accumulation - CV corrections are hard state overrides, not probabilistic adjustments
- Empty
suppressedZoneCellIndicesfrom CV is valid and expected —SuppressedZoneRegistryhandles it without error - API responses never contain raw grid arrays or GeoTIFF bytes — always compact numeric structures
GeoTiffBandReaderServicereads exactly 5 bands from the 11-band CV file — band indices are constants inBandLayout, single file to update if CV changes its layout. CRS: EPSG:32737, native pixel size: 10m- ESA WorldCover class codes mapped to
VegetationTypeviaEsaBandLayout— 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 VegetationTypeordinal order is fixed — do not reorder enum constants

