SFMS: FMC - #5734
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5734 +/- ##
==========================================
+ Coverage 67.74% 67.81% +0.07%
==========================================
Files 386 386
Lines 22432 22436 +4
Branches 3105 3105
==========================================
+ Hits 15196 15216 +20
+ Misses 6079 6060 -19
- Partials 1157 1160 +3 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
| raster_addresser: SFMSNGRasterAddresser, | ||
| s3_client: S3Client, | ||
| ) -> None: | ||
| """Validate complete FMC rasters and publish dates without exisiting FMC rasters.""" |
There was a problem hiding this comment.
| """Validate complete FMC rasters and publish dates without exisiting FMC rasters.""" | |
| """Validate complete FMC rasters and publish dates without existing FMC rasters.""" |
|
|
||
| for target_date, output_key in inputs.output_keys.items(): | ||
| result = calculate_foliar_moisture_content(datasets, target_date) | ||
| with create_masked_output_dataset( |
There was a problem hiding this comment.
This opens the BC mask from S3 once per output date, I think we create a function that opens the BC mask once and yields a ContextManager generator for other datasets, then we can hoist this above the loop.
| return FoliarMoistureContentResult(output) | ||
|
|
||
|
|
||
| class FoliarMoistureContentProcessor: |
There was a problem hiding this comment.
FoliarMoistureContentProcessor and SurfaceFuelConsumptionProcessor have near-identical dependency-exists, open-datasets and grid validation logic. We could pull the shared behavior into a GriddedRasterDependencies composition object that both processors hold as self._deps.
There was a problem hiding this comment.
E.g.
class GriddedRasterDependencies:
"""Loads and validates raster dependencies against a reference grid."""
@staticmethod
async def assert_keys_exist(
s3_client: S3Client,
keys: Iterable[GDALPath],
error_label: str,
) -> None:
keys = tuple(keys)
if not await s3_client.all_objects_exist(*keys):
details = ", ".join(str(key) for key in keys)
raise RuntimeError(f"Missing {error_label}: {details}")
@staticmethod
def index_by_key(datasets: list[WPSDataset]) -> dict[GDALPath, WPSDataset]:
return {dataset.ds_path: dataset for dataset in datasets}
@staticmethod
def validate_grids(
reference: WPSDataset,
reference_key: GDALPath,
candidates: Iterable[Tuple[str, GDALPath, WPSDataset]],
) -> None:
reference_ds = reference.as_gdal_ds()
for label, key, dataset in candidates:
if not rasters_match(dataset.as_gdal_ds(), reference_ds):
raise ValueError(
f"{label} raster does not match the fuel grid: {key} vs {reference_key}"
)
| ) | ||
|
|
||
|
|
||
| async def ensure_fmc_rasters( |
There was a problem hiding this comment.
Callers don't need to know about ensuring FMC rasters, they should just be able to rely on FoliarMoistureContentProcessor.process() to guarantee it. If these functions live in the FoliarMoistureContentProcessor, then FoliarMoistureContentProcessor().process(...) becomes the same shape as the SFC call site, caller doesn't know or care that anything gets skipped.



Adds daily Foliar Moisture Content (FMC) raster generation to the SFMS pipeline.
FMC is calculated once per calendar day using elevation, latitude, longitude, and Julian date, then shared between actual and forecast processing.
sfms_ng/static/fmc/YYYY/MM/DD/fmc_YYYYMMDD.tifAddresses the backend piece of #5656
Test Links:
Landing Page
MoreCast
Percentile Calculator
FireCalc
FireCalc bookmark
Auto Spatial Advisory (ASA)
HFI Calculator
SFMS Insights
Fire Watch
Weather Toolkit