Purpose: Answer the three questions that decide whether a property carrier stays solvent — where is my hazard, where am I concentrated, and what could a single season cost me.
Domain: Catastrophe modelling for wildfire exposure. The deliverables are the same ones a real CAT model produces: exposure accumulation, AAL (average annual loss), PML (probable maximum loss), and the OEP (occurrence exceedance probability) curve.
Dataset: A deterministic synthetic book of ~900 insured locations across six San Bernardino and Riverside county communities — genuine wildland-urban interface territory — paired with a 5,000-event stochastic wildfire catalog normalised to ~1.5 events per year.
Statelessness: No database and no persistence. The book is generated once from a fixed-seed LCG, exactly like the OpenStreetMap road network powering the Route Planner, so every run is byte-identical.
Scale: A default run is 900 locations × 5,000 events = 4.5 million site-event evaluations, plus an O(n²) ring accumulation pass over the book.
Algorithm
Ring accumulation: For each location, sum the Total Insured Value of every location within a radius. Brute-force O(n²) haversine, guarded by a conservative bounding-box reject that skips the trigonometry when a pair is provably too far apart. This is the classic concentration control — one canyon fire can consume a year of premium.
Hazard decay: Event intensity falls linearly from the epicenter to zero at the footprint edge, then scales by the location's own site hazard (derived from a slope proxy and distance to open fuel).
Vulnerability curve: Mean damage ratio is 1 − e^(−α·i) where i is site intensity. Monotone, bounded on [0, 1), and steep at low intensity — the α slider on the demo reshapes it live.
Financial terms: Ground-up loss is TIV × MDR. Gross loss applies a percentage deductible and a limit: clamp(groundUp − TIV·deductibleRate, 0, TIV·limitRate). Wildfire deductibles are percentage-of-value, not flat dollars.
Exceedance curve: For a loss level L the annual exceedance rate is the summed frequency of every event whose loss exceeds L. Sorting descending by loss and accumulating rate yields one curve point per event, with return period = 1 / cumulative rate. Benchmark losses are interpolated log-linearly at 10, 25, 50, 100, 250 and 500 years; PML is the 250-year loss.
Average annual loss: The rate-weighted mean of the per-event losses — the number that has to be priced into premium.
API Layer
Endpoints:GET /api/v1/catrisk/book, POST /api/v1/catrisk/accumulation, POST /api/v1/catrisk/simulate.
Versioning:Asp.Versioning with the version in the route template, matching every other API in the portfolio.
Payload guards:[RequestSizeLimit(8_000_000)] on both POSTs — a full book plus a 20,000-event catalog is a large body — and [EnableRateLimiting("expensive")] on the controller, since each call is millions of floating-point evaluations.
Caching: The book is a compile-time constant, so GET /book carries [ResponseCache(Duration = 3600)].
Error contract: Only ArgumentException is caught, returning 400 with { "error": "..." }. The service's validation messages are therefore the public API contract and are written as user-facing text.
Curve downsampling: A 20,000-event catalog would produce 20,000 curve points. The response emits 120 log-spaced samples instead, so the payload stays plottable regardless of catalog size.
Services and Native Boundary
Kernel:cat_risk_kernel, a C++20 shared library exposing Cat_ComputeRingAccumulation and Cat_SimulateEventLosses through a stable C ABI with #pragma pack(push, 8) structs and status-code returns.
Where the boundary sits: The kernel returns raw per-event losses and per-location ring totals. AAL, the exceedance curve, and the benchmark interpolation are derived in C#. Dense parallel arithmetic goes native; statistical shaping stays managed where it is readable and unit-testable.
Marshalling: Blittable [StructLayout(Sequential, Pack = 8)] structs with preallocated managed output arrays and explicit [In]/[Out] direction attributes. Native code never allocates across the boundary.
Managed fallback:CatRiskNativeBridge.IsAvailable gates the native path. The managed implementation mirrors the kernel line for line — same haversine, same decay, same clamp order — so results are identical either way. The fallback is production behaviour, not a test convenience: the deployed container does not build the native libraries, so what runs in production today is the managed path.
Optimisation worth naming: Both paths bounding-box reject a pair before computing haversine. On a spatially clustered book most locations lie outside most event footprints, so the reject eliminates the majority of the trigonometry — the difference between a naive loop and one that understands its data.
Measured speedup — and the honest answer: Native is only about 1.1× faster than the managed fallback, with numerically identical results. Measured on the same machine, warm, over the shipped book:
Native versus managed benchmark results for the catastrophe risk kernel, by workload.
Workload
Native
Managed
Speedup
Ring accumulation, 900 locations
5.4 ms
5.9 ms
1.09×
Ring accumulation, 5,000 locations
166 ms
186 ms
1.12×
Simulation, 900 × 5,000 events
51.6 ms
56.3 ms
1.09×
Simulation, 5,000 × 12,000 events
568 ms
645 ms
1.13×
Ring-TIV checksums match bit-for-bit; AAL differs only in the final ULP from floating-point summation order under /fp:fast.
Why the gap is small: RyuJIT generates good scalar code for tight double loops, so there is little headroom to reclaim. The inner loop is branch-heavy — the bounding-box reject short-circuits most iterations — which defeats auto-vectorisation on both sides, and P/Invoke array pinning eats part of what remains. Native code wins on this shape of workload only when it can do something the JIT will not: explicit SIMD intrinsics, thread-level parallelism over the event loop, or a memory layout the managed representation cannot express. None of those are in this kernel today.
Known wart:IsNativeInvocationException includes InvalidOperationException, which is exactly what the negative-status check throws. A genuine native error therefore logs one warning and silently degrades to managed rather than surfacing. This is consistent across all nine kernels and is a deliberate availability-over-visibility trade, but it means native failures are invisible in production metrics.
Engineering Decisions and Tradeoffs
Stateless over persisted: The book could have been an EF Core entity with a seeded migration. It is generated in code instead — the portfolio already demonstrates EF seeding six times over, and a stateless kernel is the honest shape for a compute demo.
O(n²) accumulation, deliberately: Ring analysis is quadratic and stays quadratic here. At 5,000 locations that is 25 million distance tests, which is exactly the workload that justifies the native path. A grid index or k-d tree would be the answer at 500,000 locations, and that is the interesting follow-up conversation rather than something to pre-optimise.
Catalog subsampling preserves frequency: Shrinking the catalog in the UI resamples events and rescales their annual rates so total regional frequency stays fixed. Dropping events without rescaling would silently lower the modelled hazard and quietly flatter the results.
Percentage deductibles: Modelled as a fraction of TIV rather than a flat dollar amount, because that is the wildfire convention. It materially changes the loss distribution's left tail — small events produce no claim at all.
Hazard is terrain-driven: Site hazard is derived from position within the community and a slope proxy rather than drawn independently, so the exposure map reads as a gradient rather than noise.
Self-contained SVG: Both the exposure map and the EP curve are hand-built SVG with CSS custom properties for colour, so the visuals follow the light/dark theme and the page carries no map-tile dependency.
Interview Discussion Points
This kernel measures at 1.1× over managed C#. Was the native path worth building, and how would you decide that before writing it rather than after?
What would you change to make the native path genuinely faster — SIMD intrinsics, parallelising the event loop, or restructuring the data layout? Which gives the most speedup per unit of complexity, and what does each cost in portability?
Why does the EP curve use exceedance rate rather than a simple percentile of the loss distribution — and what breaks if events are not independent?
Where is the boundary between OEP and AEP, and which one does a reinsurance treaty attach to?
The vulnerability curve is 1 − e^(−α·i). What real-world properties does that shape have, and where does it fail?
Ring accumulation is O(n²). At what book size does that stop being acceptable, and what would you replace it with — a grid index, an R-tree, or a k-d tree? What does each cost you?
Deductibles here are percentage-of-TIV rather than flat dollars. Why is that the wildfire convention, and how would flat deductibles change the loss distribution?
The native kernel returns per-event losses and the managed layer derives AAL and the EP curve. Why draw the boundary there instead of returning the finished curve from C++?
How would you validate this model against actual loss experience?