Skip to content

[Econ] Preprocess Seed Costs#3112

Open
allisterakun wants to merge 10 commits into
economicsfrom
econ_seed_cost_preprocess
Open

[Econ] Preprocess Seed Costs#3112
allisterakun wants to merge 10 commits into
economicsfrom
econ_seed_cost_preprocess

Conversation

@allisterakun

@allisterakun allisterakun commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Adds seed cost preprocessing to the Economics module, computing per-field seed costs by mapping crop specifications to commodity prices and converting field sizes to m².

Context

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

What

  • Added seed cost preprocessing logic in preprocessing.py that reads field.crop_specification and field.field_size from the simulation data, converts field size to m² (to match economics inputs), and maps crops grown to the corresponding commodity prices to compute seed costs per field.
  • Added "seed_cost": [0.0] to the fallback values dictionary in fallback_values.py.
  • Added a print statement in EEE_manager.py for the metadata load failure case (debugging aid).
  • Updated seed cost reporting per Braden's feedback.
  • Added unit tests in test_economics_preprocessing.py for the new seed cost preprocessing logic.
  • Updated test_economics.py to account for the new seed cost data flow.

Why

The Economics module needed to process seed costs as part of the farm economic analysis, but there was no preprocessing step to extract crop specification and field size data from the biophysical simulation outputs, convert units, and map crops to their seed cost commodity prices. Without this preprocessing, seed costs could not be included in the economic analysis.

How

The preprocessing step reads field.crop_specification (which crops are grown on each field) and field.field_size (the area of each field) from the simulation data. Field sizes are converted to m² to match the units expected by the economics inputs. Crops are mapped many-to-one to commodity price categories using the existing crop-to-commodity mapping. The resulting seed cost per field is computed and made available to the economic framework. When biophysical data is unavailable, the fallback value of 0.0 is used for seed cost.

Test plan

  • Added unit tests in test_economics_preprocessing.py covering seed cost preprocessing with valid crop/field data, missing data fallback, and crop-to-commodity mapping.
  • Updated test_economics.py to include seed cost in the economics data flow.

Input Changes

  • N/A (uses existing field.crop_specification and field.field_size data from the biophysical simulation)

Output Changes

  • N/A

Filter

@allisterakun
allisterakun requested a review from bradenlimb June 26, 2026 20:02
@allisterakun allisterakun linked an issue Jul 2, 2026 that may be closed by this pull request
@allisterakun
allisterakun marked this pull request as ready for review July 13, 2026 16:09
@bradenlimb

Copy link
Copy Markdown
Collaborator

Hey Allister

Looking at the inputs/outputs I have one question on the field size. The biophysical aggregate output is 400,000 m2 are planted with corn. However, in the inputs I only see corn_silage being planted twice in 2013/2017 and the field size is 10. I assume this field size is in hectares (please verify) which would mean a size of 100,000 m2 so the corn planted size should be 200,000 m2

Similar but different issues with alfalfa hay vs silage. Both are reported as 172,706 m2 by the code, but I only see alfafa_silage between planted twice so should have the same area as corn and alfalfa_hay should be zero.

I might be misfollowing the input json so please correct me if this is the case

@allisterakun

Copy link
Copy Markdown
Collaborator Author

Hey Allister

Looking at the inputs/outputs I have one question on the field size. The biophysical aggregate output is 400,000 m2 are planted with corn. However, in the inputs I only see corn_silage being planted twice in 2013/2017 and the field size is 10. I assume this field size is in hectares (please verify) which would mean a size of 100,000 m2 so the corn planted size should be 200,000 m2

Similar but different issues with alfalfa hay vs silage. Both are reported as 172,706 m2 by the code, but I only see alfafa_silage between planted twice so should have the same area as corn and alfalfa_hay should be zero.

I might be misfollowing the input json so please correct me if this is the case

I believe this is because there are two fields in this simulation. Both have a field size of 10 hectares.
Field 1 plants corn on 2013:127 and harvests on 2013:298, then again plants on 2017:127 with harvest on 2017:298.
Field 2 plants corn on 2013:127 and harvests on 2013:263, then again plants on 2017:127 with harvest on 2017:263.
That's why, when summing the total field size for corn, it comes out double what you expected.

Regarding alfalfa silage and hay: both are expected to be planted (silage in field 1 and hay in field 2). I'll look into the values for their field size!

@bradenlimb

Copy link
Copy Markdown
Collaborator

Hey Allister
Looking at the inputs/outputs I have one question on the field size. The biophysical aggregate output is 400,000 m2 are planted with corn. However, in the inputs I only see corn_silage being planted twice in 2013/2017 and the field size is 10. I assume this field size is in hectares (please verify) which would mean a size of 100,000 m2 so the corn planted size should be 200,000 m2
Similar but different issues with alfalfa hay vs silage. Both are reported as 172,706 m2 by the code, but I only see alfafa_silage between planted twice so should have the same area as corn and alfalfa_hay should be zero.
I might be misfollowing the input json so please correct me if this is the case

I believe this is because there are two fields in this simulation. Both have a field size of 10 hectares. Field 1 plants corn on 2013:127 and harvests on 2013:298, then again plants on 2017:127 with harvest on 2017:298. Field 2 plants corn on 2013:127 and harvests on 2013:263, then again plants on 2017:127 with harvest on 2017:263. That's why, when summing the total field size for corn, it comes out double what you expected.

Regarding alfalfa silage and hay: both are expected to be planted (silage in field 1 and hay in field 2). I'll look into the values for their field size!

Hey Allister

You are correct. I missed that there were two fields. Corn grain is correct at 400,000 m2. As far as I can tell, alfalfa is also planted twice on each field so it should have the same total area - 200,000 m2 for each hay and silage.

@allisterakun

Copy link
Copy Markdown
Collaborator Author

As far as I can tell, alfalfa is also planted twice on each field so it should have the same total area - 200,000 m2 for each hay and silage.

Thanks Braden, I have just updated the PR to fix this. Now the total area for both alfalfa (hay and silage) should be 200,000 m2.

What went wrong:

  • The planned harvest_kill date was after the simulation end date
  • I divided the field size by the original planned growing period (including extra days after the end of simulation)
  • Then I clipped the data to only account for days within the simulation window
  • Result: the daily average was too small → total sum ~172,000

Fix:

  • Now I divide by the actual growing period within the simulation window, so the daily average and total both match 200,000.

@bradenlimb bradenlimb 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.

Looks like it is working correctly to me! Thanks for fixing this!

@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.

Need to spend some more time with the larger functions but thought this feedback would be a good start. Nice work, just multiple complex mappings and loops and exceptions all happening and want to make sure we're defensively guarding properly.

)
return values

def _scenario_names(self) -> list[str]:

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.

Suggested change
def _scenario_names(self) -> list[str]:
def _get_scenario_names(self) -> list[str]:

or _fetch_scenario_names()

"cereal_rye_hay": "commodity_prices_wheat_seed_dollar_per_square_meter",
}

_HA_TO_M2: float = 10_000.0

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.

I believe this already exists in general_constants.py and if not, it should be added there.

_HA_TO_M2: float = 10_000.0

# Harvest operations that terminate a crop's life in the field.
_FINAL_HARVEST_OPS: frozenset[str] = frozenset({"harvest_kill", "kill_only"})

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.

This already exists in harvest_operations.py

Comment on lines +683 to +688
"""Return (planting_date, kill_date) pairs for one crop schedule entry.

Pairs each planting event with its corresponding final harvest
(``harvest_kill`` or ``kill_only``). Intermediate ``harvest_only``
operations are ignored. Pattern expansion (``pattern_repeat``,
``planting_skip``, ``harvesting_skip``) is applied before pairing.

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.

I think a clearer docstring function overview here would be helpful. Also update to numpy style with Returns section, Parameters, etc

# Harvest operations that terminate a crop's life in the field.
_FINAL_HARVEST_OPS: frozenset[str] = frozenset({"harvest_kill", "kill_only"})

def _growing_periods(

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.

Add action verb to function name. gather e.g.

Comment on lines +694 to +698
raw_p_years: list[int] = list(schedule.get("planting_years") or [])
raw_p_days: list[int] = list(schedule.get("planting_days") or [])
raw_h_years: list[int] = list(schedule.get("harvest_years") or [])
raw_h_days: list[int] = list(schedule.get("harvest_days") or [])
raw_h_ops: list[str] = list(schedule.get("harvest_operations") or [])

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.

I would use full words here for the p and h.

Comment on lines +703 to +707
p_years = Schedule.repeat_pattern(raw_p_years, planting_skip, pattern_repeat)
p_days = Utility.elongate_list(raw_p_days * (pattern_repeat + 1), len(p_years))
h_years = Schedule.repeat_pattern(raw_h_years, harvesting_skip, pattern_repeat)
h_days = Utility.elongate_list(raw_h_days * (pattern_repeat + 1), len(h_years))
h_ops = Utility.elongate_list(raw_h_ops * (pattern_repeat + 1), len(h_years))

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.

same comment as above.

Comment on lines +729 to +732
For each field, each crop's planting-to-kill periods are located within
the simulation window. The field's area in m² is spread evenly over
each growing period (``field_size_m² / period_duration``), then
accumulated into a per-simulation-day array keyed by seed commodity.

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.

Add to a Notes section.

return periods

def _preprocess_seed_costs(self) -> dict[str, list[float]]:
"""Build a daily time-series of responsible field area (m²) per seed key.

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.

What is responsible field area?

plant_date = None
return periods

def _preprocess_seed_costs(self) -> dict[str, list[float]]:

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.

This function is a bit large and doing multiple things - what do you think about extracting some small helper functions?

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.

[Econ] Process seed costs

3 participants