Skip to content

[CropAndSoil] Re-implement nutrient cycling residue composition factor per SWAT 3:1.2.8#3150

Open
matthew7838 wants to merge 24 commits into
devfrom
cs-decomp
Open

[CropAndSoil] Re-implement nutrient cycling residue composition factor per SWAT 3:1.2.8#3150
matthew7838 wants to merge 24 commits into
devfrom
cs-decomp

Conversation

@matthew7838

@matthew7838 matthew7838 commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Re-implements _calculate_nutrient_cycling_residue_composition_factor in mineralization_decomp.py so it actually uses the nitrogen and phosphorus terms per SWAT 3:1.2.8, instead of unconditionally returning 1.

Context

Issue(s) closed by this pull request: closes #2990

Background from the issue: the constant return 1 was 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

  1. MineralizationDecomposition._calculate_nutrient_cycling_residue_composition_factor now returns max(min([nitrogen_term, phosphorus_term, 1]), 1e-5) rather than discarding both terms and returning 1.
  2. Removed the now-obsolete assert nitrogen_term is not None and phosphorus_term is not None line, which existed only to keep the unused terms from being flagged.
  3. Updated the docstring: dropped the "temporary fix" note and the # TODO pointing at [Crop and Soil] Confirm the function _calculate_nutrient_cycling_residue_composition_factor in mineralization_decomp.py is working correctly. #2990, and documented where the 1e-5 lower bound comes from.
  4. Updated test_calculate_nutrient_cycling_residue_composition_factor to exercise the real behavior.
  5. Regenerated the end-to-end expected results, since soil nitrogen outputs change (see Test plan).

Why

The method is called by mineralize_and_decompose_nitrogen to get the residue composition factor that feeds the decay rate constant. Returning a hard-coded 1 meant 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:

Test plan

  • Run with updated unit tests.
  • SME's review with different scenarios.

Input Changes

No schema or user-facing input changes. The 12 input/data/end_to_end_testing/*/e2e_json_*_filter.json files 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\\..*"
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on cs-decomp branch: 1135
Mypy errors on dev branch: 1136
1 fewer errors on cs-decomp branch

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Please update the changelog. This PR cannot be merged until changelog_WIP.md is updated.

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on cs-decomp branch: 1135
Mypy errors on dev branch: 1136
1 fewer errors on cs-decomp branch

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Please update the changelog. This PR cannot be merged until changelog_WIP.md is updated.

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on cs-decomp branch: 1135
Mypy errors on dev branch: 1136
1 fewer errors on cs-decomp branch

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: %

Mypy errors on cs-decomp branch: 1135
Mypy errors on dev branch: 1136
1 fewer errors on cs-decomp branch

@github-actions

Copy link
Copy Markdown
Contributor

🚨 Some tests have failed.

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on cs-decomp branch: 1135
Mypy errors on dev branch: 1136
1 fewer errors on cs-decomp branch

@ew3361zh ew3361zh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm!

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm misreading it but it seems like the suggestion comes from SWAT, not the SME?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread RUFAS/biophysical/field/soil/nitrogen_cycling/mineralization_decomp.py Outdated
milk_protein_content=1,
)
for _ in range(randint(0, 100))
for _ in range(randint(1, 100))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this updated?

@matthew7838 matthew7838 Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on cs-decomp branch: 1135
Mypy errors on dev branch: 1136
1 fewer errors on cs-decomp branch

@allisterakun allisterakun left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

matthew7838 and others added 3 commits July 23, 2026 19:49
Resolve conflicts in changelog_WIP.md (keep both #3128 and #3150 entries)
and the 12 end-to-end filter files. The e2e expected results are regenerated
from the merged code so they reflect both dev's heifer ADG floor (#3128) and
this branch's mineralization factor change (#3150).
@github-actions

Copy link
Copy Markdown
Contributor

Current Coverage: 99%

Mypy errors on cs-decomp branch: 1135
Mypy errors on dev branch: 1136
1 fewer errors on cs-decomp branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Crop and Soil] Confirm the function _calculate_nutrient_cycling_residue_composition_factor in mineralization_decomp.py is working correctly.

3 participants