-
Notifications
You must be signed in to change notification settings - Fork 100
EPPT-2585: Fire Severity Index: Add a DuffMoistureCode class to IMPROVER #2250
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
mo-philrelton
merged 10 commits into
metoppv:EPPT_2411_fire_severity_index_workflow_development
from
mo-philrelton:EPPT_2585_duffmoisturecode
Jan 6, 2026
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
08d5aff
Adding working, documented implementation of the DuffMoistureCode class
mo-philrelton 655379e
Adding initial versions of tests for DMC
mo-philrelton 4bea2c1
Fixing warnings from log(0)s
mo-philrelton 0ad206b
Adding test for day length factors
mo-philrelton 0a53f96
Adding DMC to API
mo-philrelton 69f6823
Changes from ABC refactor
mo-philrelton ec9c528
Adding missing tests for valid inputs/outputs
mo-philrelton 3e99680
Adding docstring changes that standardise the docstring format to mat…
mo-philrelton 35d0574
Changes from review
mo-philrelton 0258d0f
Fixing review typo
mo-philrelton File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| # (C) Crown Copyright, Met Office. All rights reserved. | ||
| # | ||
| # This file is part of 'IMPROVER' and is released under the BSD 3-Clause license. | ||
| # See LICENSE in the root of the repository for full licensing details. | ||
|
|
||
| import numpy as np | ||
| from iris.cube import Cube | ||
|
|
||
| from improver.fire_weather import FireWeatherIndexBase | ||
|
|
||
|
|
||
| class DuffMoistureCode(FireWeatherIndexBase): | ||
| """ | ||
| Plugin to calculate the Duff Moisture Code (DMC) following | ||
| the Canadian Forest Fire Weather Index System. | ||
|
|
||
| The DMC is a numerical rating of the average moisture content of loosely | ||
| compacted organic layers of moderate depth. It indicates fuel consumption | ||
| in moderate duff layers and medium-size woody material. | ||
| Higher values indicate drier conditions. | ||
|
|
||
| This process is adapted directly from: | ||
| Equations and FORTRAN Program for the | ||
| Canadian Forest Fire Weather Index System | ||
| (C.E. Van Wagner and T.L. Pickett, 1985). | ||
| Page 6, Equations 11-16. | ||
|
|
||
| Expected input units: | ||
| - Temperature: degrees Celsius | ||
| - Precipitation: mm (24-hour accumulation) | ||
| - Relative humidity: percentage (0-100) | ||
| - Previous DMC: dimensionless | ||
| - Month: integer (1-12) for day length factor lookup | ||
| """ | ||
|
|
||
| INPUT_CUBE_NAMES = [ | ||
| "air_temperature", | ||
| "lwe_thickness_of_precipitation_amount", | ||
| "relative_humidity", | ||
| "duff_moisture_code", | ||
| ] | ||
| OUTPUT_CUBE_NAME = "duff_moisture_code" | ||
| REQUIRES_MONTH = True | ||
| # Disambiguate input DMC (yesterday's value) from output DMC (today's calculated value) | ||
| INPUT_ATTRIBUTE_MAPPINGS = {"duff_moisture_code": "input_dmc"} | ||
|
|
||
| temperature: Cube | ||
| precipitation: Cube | ||
| relative_humidity: Cube | ||
| input_dmc: Cube | ||
| month: int | ||
| previous_dmc: np.ndarray | ||
|
|
||
| # Day length factors for DMC calculation (L_e values from Table 3) | ||
| # Index 0 is unused, indices 1-12 correspond to months January-December | ||
| DMC_DAY_LENGTH_FACTORS = [ | ||
| 0.0, # Placeholder for index 0 | ||
| 6.5, # January | ||
| 7.5, # February | ||
| 9.0, # March | ||
| 12.8, # April | ||
| 13.9, # May | ||
| 13.9, # June | ||
| 12.4, # July | ||
| 10.9, # August | ||
| 9.4, # September | ||
| 8.0, # October | ||
| 7.0, # November | ||
| 6.0, # December | ||
| ] | ||
|
|
||
| def _calculate(self) -> np.ndarray: | ||
| """Calculate the Duff Moisture Code (DMC). | ||
|
|
||
| Returns: | ||
| np.ndarray: The calculated DMC values for the current day. | ||
| """ | ||
| # Step 1: Set today's DMC value to the previous day's DMC value | ||
| self.previous_dmc = self.input_dmc.data.copy() | ||
|
|
||
| # Step 2: Perform rainfall adjustment, if precipitation > 1.5 mm | ||
| self._perform_rainfall_adjustment() | ||
|
|
||
| # Steps 3 & 4: Calculate drying rate | ||
| drying_rate = self._calculate_drying_rate() | ||
|
|
||
| # Step 5: Calculate DMC from adjusted previous DMC and drying rate | ||
| dmc = self._calculate_dmc(drying_rate) | ||
|
|
||
| return dmc | ||
|
|
||
| def _perform_rainfall_adjustment(self): | ||
| """Updates the previous DMC value based on available precipitation | ||
| accumulation data for the previous 24 hours. This is done element-wise | ||
| for each grid point. | ||
|
|
||
| From Van Wagner and Pickett (1985), Page 6: Equations 11-15, | ||
| and Steps 2a-2e corresponding to rainfall adjustment. | ||
| """ | ||
| # Only adjust if precipitation > 1.5 mm | ||
| precip_mask = self.precipitation.data > 1.5 | ||
|
|
||
| # Step 2a: Calculate effective rainfall via Equation 11 | ||
| effective_rain = 0.92 * self.precipitation.data - 1.27 | ||
|
|
||
| # Step 2b: Calculate initial moisture content from previous DMC via Equation 12 | ||
| moisture_content_initial = 20.0 + np.exp(5.6348 - self.previous_dmc / 43.43) | ||
|
|
||
| # Step 2c: Calculate the slope variable based on previous DMC via Equations 13a, 13b, 13c | ||
| # In the original algorithm, the slope variable is referred to as 'b' | ||
| # VECTORIZATION NOTE: This structure matches the original algorithm while being vectorized | ||
| # Clip previous_dmc to avoid log(0) warnings in equations 13b and 13c | ||
| dmc_clipped = np.maximum(self.previous_dmc, 1e-10) | ||
| slope_variable = np.where( | ||
| self.previous_dmc <= 33.0, | ||
| # Equation 13a: DMC <= 33 | ||
| 100.0 / (0.5 + 0.3 * self.previous_dmc), | ||
| np.where( | ||
| self.previous_dmc <= 65.0, | ||
| # Equation 13b: 33 < DMC <= 65 | ||
| 14.0 - 1.3 * np.log(dmc_clipped), | ||
| # Equation 13c: DMC > 65 | ||
| 6.2 * np.log(dmc_clipped) - 17.2, | ||
| ), | ||
| ) | ||
|
|
||
| # Step 2d: Calculate moisture content after rain via Equation 14 | ||
| # Protect against division by zero (though mathematically unlikely) | ||
| denominator = 48.77 + slope_variable * effective_rain | ||
| moisture_content_after_rain = moisture_content_initial + ( | ||
| 1000.0 * effective_rain | ||
| ) / np.maximum(denominator, 1e-10) | ||
|
|
||
| # Step 2e: Calculate DMC after rain via Equation 15 | ||
| # This is modified to avoid log of zero or negative values | ||
| log_arg = np.clip(moisture_content_after_rain - 20.0, 1e-10, None) | ||
| dmc_after_rain = 244.72 - 43.43 * np.log(log_arg) | ||
|
|
||
| # Apply lower bound of 0 | ||
| dmc_after_rain = np.maximum(dmc_after_rain, 0.0) | ||
|
|
||
| # Update previous_dmc where precipitation > 1.5 | ||
| self.previous_dmc = np.where(precip_mask, dmc_after_rain, self.previous_dmc) | ||
|
|
||
| def _calculate_drying_rate(self) -> np.ndarray: | ||
| """Calculates the drying rate for DMC. This is multiplied by 100 for | ||
| computational efficiency in the final DMC calculation. The original | ||
| algorithm calculates K and then multiplies it by 100 in the DMC equation. | ||
|
|
||
| Temperature is bounded to a minimum of -1.1°C. Uses the day length factor | ||
| for the current month from DMC_DAY_LENGTH_FACTORS. | ||
|
|
||
| From Van Wagner and Pickett (1985), Page 6: Equation 16, Steps 3 & 4. | ||
|
|
||
| Returns: | ||
| The drying rate (dimensionless). Shape matches input cube data | ||
| shape. This value is added to previous DMC to get today's DMC. | ||
| """ | ||
| # Apply temperature lower bound of -1.1°C | ||
| temp_adjusted = np.maximum(self.temperature.data, -1.1) | ||
|
|
||
| # Step 3: Get day length factor for current month | ||
| day_length_factor = self.DMC_DAY_LENGTH_FACTORS[self.month] | ||
|
|
||
| # Step 4: Calculate drying rate via Equation 16 | ||
| drying_rate = ( | ||
| 1.894 | ||
| * (temp_adjusted + 1.1) | ||
| * (100.0 - self.relative_humidity.data) | ||
| * day_length_factor | ||
| * 1e-4 | ||
| ) | ||
|
|
||
| return drying_rate | ||
|
|
||
| def _calculate_dmc(self, drying_rate: np.ndarray) -> np.ndarray: | ||
| """Calculates the Duff Moisture Code from previous DMC and drying rate. | ||
| Note that the drying rate is expected to be pre-multiplied by 100 | ||
| for computational efficiency. This mathematically matches the original | ||
| algorithm, but is more efficient to implement this way. | ||
|
|
||
| From Van Wagner and Pickett (1985), Page 6: Equation 16. | ||
|
|
||
| Args: | ||
| drying_rate: | ||
| The drying rate, represented by 'K' in the original equations. | ||
|
|
||
| Returns: | ||
| The calculated DMC value. | ||
| """ | ||
| # Equation 16: Calculate DMC | ||
| dmc = self.previous_dmc + drying_rate | ||
|
|
||
| # Apply lower bound of 0 | ||
| dmc = np.maximum(dmc, 0.0) | ||
|
|
||
| return dmc | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.