[CropAndSoil] Re-implement nutrient cycling residue composition factor per SWAT 3:1.2.8#3150
[CropAndSoil] Re-implement nutrient cycling residue composition factor per SWAT 3:1.2.8#3150matthew7838 wants to merge 24 commits into
Conversation
|
Current Coverage: 99% Mypy errors on cs-decomp branch: 1135 |
|
🚨 Please update the changelog. This PR cannot be merged until |
|
Current Coverage: 99% Mypy errors on cs-decomp branch: 1135 |
|
🚨 Please update the changelog. This PR cannot be merged until |
9c8976c to
d8beed9
Compare
d8beed9 to
77989b0
Compare
|
Current Coverage: 99% Mypy errors on cs-decomp branch: 1135 |
|
Current Coverage: % Mypy errors on cs-decomp branch: 1135 |
|
🚨 Some tests have failed. |
|
Current Coverage: 99% Mypy errors on cs-decomp branch: 1135 |
| Notes | ||
| ----- | ||
| The values of the constant used to determine the nitrogen and phosphorus terms are 25 and 200, respectively. | ||
| Minimum value in the max() return was suggested by SME on issue#2990 |
There was a problem hiding this comment.
Maybe I'm misreading it but it seems like the suggestion comes from SWAT, not the SME?
There was a problem hiding this comment.
I believe that SWAT did not mention the floor value; it was mentioned by Clay in the issue comments, which functionally sets the floor as 0.
| milk_protein_content=1, | ||
| ) | ||
| for _ in range(randint(0, 100)) | ||
| for _ in range(randint(1, 100)) |
There was a problem hiding this comment.
So this is another bug that's causing a 1/400 failure that was never triggered before, so no one caught it. I got lucky (or unlucky) with this PR, and it got triggered.
|
Current Coverage: 99% Mypy errors on cs-decomp branch: 1135 |
|
Current Coverage: 99% Mypy errors on cs-decomp branch: 1135 |
Re-implements
_calculate_nutrient_cycling_residue_composition_factorinmineralization_decomp.pyso it actually uses the nitrogen and phosphorus terms per SWAT 3:1.2.8, instead of unconditionally returning1.Context
Issue(s) closed by this pull request: closes #2990
Background from the issue: the constant
return 1was introduced in #1865 to work around #1810, where very large C:N / C:P ratios (>30,000:1 in some soil layers) drove the factor to functionally zero. Per @morrowcj's SME grooming (after checking with KFosterReed), the nitrogen-modeling changes made since then have addressed the conditions behind #1810, so the SWAT logic can be restored with a clamped lower bound.What
MineralizationDecomposition._calculate_nutrient_cycling_residue_composition_factornow returnsmax(min([nitrogen_term, phosphorus_term, 1]), 1e-5)rather than discarding both terms and returning1.assert nitrogen_term is not None and phosphorus_term is not Noneline, which existed only to keep the unused terms from being flagged.# TODOpointing at [Crop and Soil] Confirm the function_calculate_nutrient_cycling_residue_composition_factorinmineralization_decomp.pyis working correctly. #2990, and documented where the1e-5lower bound comes from.test_calculate_nutrient_cycling_residue_composition_factorto exercise the real behavior.Why
The method is called by
mineralize_and_decompose_nitrogento get the residue composition factor that feeds the decay rate constant. Returning a hard-coded1meant the effect of soil C, N, and P on the decomposition rate factor was not modeled at all — the C:N and C:P ratios were computed and then thrown away, so residue decomposition ran at the same rate regardless of residue nutrient composition. That does not match SWAT 3:1.2.8.How
Implements the equation as specified in the issue:
1comes from SWAT — the factor should never accelerate decomposition above the base rate, and both terms exceed 1 whenever C:N < 25 or C:P < 200.1e-5(functionally zero) is the SME-suggested clamp that keeps pathological C:N / C:P ratios from collapsing the decay rate constant to exactly zero, which is the failure mode that motivated the original workaround in Temporary Replacement of Soil N Mineralization Rate factor calculation method with fixed value #1810.Test plan
Input Changes
No schema or user-facing input changes. The 12
input/data/end_to_end_testing/*/e2e_json_*_filter.jsonfiles are modified only because the end-to-end expected results are stored inline in them and were regenerated.Output Changes
N/A
Filter
{ "multiple": [ { "name": "issue_2990_soil_nitrogen", "filters": [ "FieldDataReporter\\.send_soil_layer_daily_variables\\.(fresh_organic|active_organic|stable_organic)_nitrogen_content.*", "FieldDataReporter\\.send_soil_layer_daily_variables\\.(nitrate_content|ammonium_content|percolated_nitrates|percolated_ammonium).*", "FieldDataReporter\\.send_soil_layer_daily_variables\\.(nitrous_oxide_emissions|dinitrogen_emissions|ammonia_emissions).*", "FieldDataReporter\\.send_soil_daily_variables\\.(profile_nitrates_total|profile_ammonium_total|profile_fresh_organic_nitrogen_total|profile_active_organic_nitrogen_total).*", "FieldDataReporter\\.send_soil_daily_variables\\.(nitrate_runoff|ammonium_runoff|eroded_(fresh|active|stable)_organic_nitrogen).*", "FieldDataReporter\\.send_soil_annual_variables\\.annual_.*(nitrogen|nitrates|ammonium|ammonia).*", "FieldDataReporter\\.send_soil_layer_annual_variables\\.annual_(nitrous_oxide|ammonia)_emissions_total.*" ] }, { "name": "issue_2990_soil_carbon", "filters": [ "FieldDataReporter\\.send_soil_layer_daily_variables\\.(active|slow|passive)_carbon.*", "FieldDataReporter\\.send_soil_layer_daily_variables\\.(passive_to_active|slow_to_active|active_carbon_to_slow|active_carbon_to_passive).*", "FieldDataReporter\\.send_soil_layer_daily_variables\\.(soil|plant)_(structural|metabolic)_(active|slow)_carbon.*", "FieldDataReporter\\.send_soil_layer_daily_variables\\.(soil|plant)_active_decompose_carbon.*", "FieldDataReporter\\.send_soil_layer_daily_variables\\.(structural_litter_amount|metabolic_litter_amount|total_soil_carbon_amount|soil_overall_carbon_fraction).*", "FieldDataReporter\\.send_soil_daily_variables\\.profile_carbon_emissions.*", "FieldDataReporter\\.send_soil_layer_annual_variables\\.annual_.*carbon.*" ] }, { "name": "issue_2990_crop_growth_and_uptake", "filters": [ "FieldDataReporter\\.send_crop_daily_variables\\.(biomass|biomass_growth|biomass_growth_max|above_ground_biomass|root_biomass|cut_biomass).*", "FieldDataReporter\\.send_crop_daily_variables\\.(wet_yield_collected|yield_nitrogen|yield_phosphorus|crop_nitrogen|optimal_crop_nitrogen).*", "FieldDataReporter\\.send_crop_daily_variables\\.(leaf_area_index|leaf_area_added|optimal_leaf_area_change|usable_light|growth_factor).*", "FieldDataReporter\\.send_crop_daily_variables\\.(total|potential)_(nitrogen|phosphorus)_uptake.*", "FieldDataReporter\\.send_crop_daily_variables\\.(nitrogen_stress|phosphorus_stress|water_stress).*" ] }, { "name": "issue_2990_water_phosphorus_and_erosion", "filters": [ "FieldDataReporter\\.send_soil_layer_daily_variables\\.(water_content|water_factor|temperature|evaporated_water_content|percolated_water|percolated_phosphorus).*", "FieldDataReporter\\.send_soil_layer_daily_variables\\.(labile_inorganic|active_inorganic)_phosphorus_content.*", "FieldDataReporter\\.send_soil_layer_daily_variables\\.(labile|active)_inorganic_unbalanced_counter.*", "FieldDataReporter\\.send_crop_daily_variables\\.(max_transpiration|cumulative_transpiration|cumulative_evaporation|cumulative_evapotranspiration|water_uptake|water_deficiency|canopy_water).*", "FieldDataReporter\\.send_soil_daily_variables\\.(infiltrated_water|water_evaporated|accumulated_runoff|eroded_sediment).*", "FieldDataReporter\\.send_soil_daily_variables\\.(soil_phosphorus_runoff|runoff_fertilizer_phosphorus|machine_.*phosphorus).*", "FieldDataReporter\\.send_soil_annual_variables\\.annual_(soil_evaporation_total|surface_runoff_total|eroded_sediment_total|soil_phosphorus_runoff).*", "FieldDataReporter\\.send_vadose_zone_layer_daily_variables\\..*" ] } ] }