[Econ] Fix biophysical-to-economics bedding mapping and per-pen cost processing - #3117
[Econ] Fix biophysical-to-economics bedding mapping and per-pen cost processing #3117matthew7838 wants to merge 12 commits into
Conversation
bradenlimb
left a comment
There was a problem hiding this comment.
All looks great to me - thanks for fixing this!
| "Bedding requirements": { | ||
| "biophysical_simulation": ["AnimalModuleReporter.report_daily_pen_total.number_of_animals_in_pen_.*"], | ||
| "input_manager": ["animal.pen_information.*.manure_streams.0.bedding_name"], | ||
| "match_source": "input_manager", | ||
| "wildcard_value_map": {"0_CALF": "0", "1_GROWING": "1", "2_CLOSE_UP": "2", "3_LAC_COW": "3"}, | ||
| # Handled by EconomicPreprocessor._preprocess_bedding (issue #3088): | ||
| # each pen's bedding price is paired with that pen's animal count | ||
| # and converted from the annual dollar-per-head price. | ||
| "dedicated_processor": "bedding", | ||
| "bedding_configs_path": "animal.bedding_configs", | ||
| # SME-confirmed: the dollar-per-head bedding prices are per LACTATING | ||
| # cow, so only lactating-cow pens are billed. Other pens still get | ||
| # physical bedding in the biophysical simulation; they are just not | ||
| # part of this economic line item. | ||
| "billable_pen_combinations": ["LAC_COW"], | ||
| # A pen's manure_stream bedding_name is a user config name; resolve | ||
| # it to the config's canonical bedding_type, then to an economics | ||
| # file key below. Types with no entry here (e.g. "none") cost nothing. | ||
| "bedding_type_to_file_key": { | ||
| "sand": "sand", | ||
| "sawdust": "sawdust", | ||
| "straw": "straw", | ||
| "CBPB sawdust": "CBPB", | ||
| "manure solids": "manure_solids", | ||
| }, | ||
| "economics_files": { | ||
| "CBPB": "commodity_prices_bedding_compost_bedded_pack_dollar_per_head", | ||
| "manure_solids": "commodity_prices_bedding_manure_solids_dollar_per_head", | ||
| "sand": "commodity_prices_bedding_sand_dollar_per_head", | ||
| "sawdust": "commodity_prices_bedding_sawdust_dollar_per_head", | ||
| "straw": "commodity_prices_bedding_straw_dollar_per_head", | ||
| }, | ||
| "preprocessing": "average number of animals in each pen", | ||
| "preprocessing": "per-pen annual bedding cost: average head per year x dollar-per-head-per-year", | ||
| }, |
There was a problem hiding this comment.
Why the duplicate information?
|
|
||
| path = item.bedding_configs_path or "animal.bedding_configs" | ||
| configs = self.im.get_data(path) | ||
| name_to_type: dict[str, Any] = {} |
There was a problem hiding this comment.
| name_to_type: dict[str, Any] = {} | |
| name_to_type: dict[str, str] = {} |
any reason that we'd expect any other type besides str?
| if isinstance(configs, (list, tuple)): | ||
| for config in configs: | ||
| if isinstance(config, dict) and "name" in config: | ||
| name_to_type[str(config["name"])] = self._normalize_bedding_type(config.get("bedding_type")) |
There was a problem hiding this comment.
add error handling for if configs is not the expected type
| if isinstance(config, dict) and "name" in config: | ||
| name_to_type[str(config["name"])] = self._normalize_bedding_type(config.get("bedding_type")) |
There was a problem hiding this comment.
Same, error handling
| def _normalize_bedding_type(self, raw: Any) -> Any: | ||
| """ | ||
| Normalizes a bedding type into a plain value. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| raw : Any | ||
| The bedding type value to normalize. | ||
|
|
||
| Returns | ||
| ------- | ||
| Any | ||
| ``raw.value`` when ``raw`` exposes a ``value`` attribute, otherwise ``raw`` | ||
| unchanged. | ||
|
|
||
| Notes | ||
| ----- | ||
| A bedding type may be a plain string (read directly from JSON input) or an | ||
| enum-like object exposing a ``value`` attribute. Returning the underlying | ||
| value in both cases lets downstream lookups always compare against strings. | ||
|
|
||
| """ | ||
| return getattr(raw, "value", raw) |
There was a problem hiding this comment.
| def _normalize_bedding_type(self, raw: Any) -> Any: | |
| """ | |
| Normalizes a bedding type into a plain value. | |
| Parameters | |
| ---------- | |
| raw : Any | |
| The bedding type value to normalize. | |
| Returns | |
| ------- | |
| Any | |
| ``raw.value`` when ``raw`` exposes a ``value`` attribute, otherwise ``raw`` | |
| unchanged. | |
| Notes | |
| ----- | |
| A bedding type may be a plain string (read directly from JSON input) or an | |
| enum-like object exposing a ``value`` attribute. Returning the underlying | |
| value in both cases lets downstream lookups always compare against strings. | |
| """ | |
| return getattr(raw, "value", raw) | |
| def _normalize_bedding_type(self, raw: Any) -> str: | |
| """ | |
| Normalizes a bedding type into a plain value. | |
| Parameters | |
| ---------- | |
| raw : Any | |
| The bedding type value to normalize. | |
| Returns | |
| ------- | |
| Any | |
| ``raw.value`` when ``raw`` exposes a ``value`` attribute, otherwise ``raw`` | |
| unchanged. | |
| Notes | |
| ----- | |
| A bedding type may be a plain string (read directly from JSON input) or an | |
| enum-like object exposing a ``value`` attribute. Returning the underlying | |
| value in both cases lets downstream lookups always compare against strings. | |
| """ | |
| return getattr(raw, "value", raw) |
Can we modify the logic here a bit to make sure it returns a str?
| def _get_simulation_start_date(self) -> tuple[int, datetime]: | ||
| """ | ||
| Returns the simulation start year and start date. | ||
|
|
||
| Returns | ||
| ------- | ||
| tuple[int, datetime] | ||
| The start year of the simulation. | ||
| The calendar date of the first simulation day. | ||
|
|
||
| Notes | ||
| ----- | ||
| The start date is read from ``config.start_date`` in the ``YYYY:day_of_year`` | ||
| format (for example ``"2013:20"``) and converted to a calendar date. | ||
|
|
||
| """ | ||
|
|
||
| raw = self.im.get_data("config.start_date") | ||
| parts = str(raw).split(":") | ||
| year = int(parts[0]) | ||
| day_of_year = int(parts[1]) if len(parts) > 1 and str(parts[1]).strip() else 1 | ||
| start_date = datetime(year, 1, 1) + timedelta(days=day_of_year - 1) | ||
| return year, start_date |
There was a problem hiding this comment.
| def _get_simulation_start_date(self) -> tuple[int, datetime]: | |
| """ | |
| Returns the simulation start year and start date. | |
| Returns | |
| ------- | |
| tuple[int, datetime] | |
| The start year of the simulation. | |
| The calendar date of the first simulation day. | |
| Notes | |
| ----- | |
| The start date is read from ``config.start_date`` in the ``YYYY:day_of_year`` | |
| format (for example ``"2013:20"``) and converted to a calendar date. | |
| """ | |
| raw = self.im.get_data("config.start_date") | |
| parts = str(raw).split(":") | |
| year = int(parts[0]) | |
| day_of_year = int(parts[1]) if len(parts) > 1 and str(parts[1]).strip() else 1 | |
| start_date = datetime(year, 1, 1) + timedelta(days=day_of_year - 1) | |
| return year, start_date | |
| def _get_simulation_start_date(self) -> datetime: | |
| """ | |
| Returns the simulation start date. | |
| Returns | |
| ------- | |
| datetime | |
| The start year of the simulation. | |
| Notes | |
| ----- | |
| The start date is read from ``config.start_date`` in the ``YYYY:day_of_year`` | |
| format (for example ``"2013:20"``) and converted to a calendar date. | |
| """ | |
| return datetime.strptime(str(self.im.get_data("config.start_date")), "%Y:%j") |
The start year should be easy to get: start_year.year.
| except (TypeError, ValueError): | ||
| continue |
| info_maps.append(raw_info_maps[index] if index < len(raw_info_maps) else {}) | ||
| return values, info_maps | ||
|
|
||
| def _collect_pen_data(self, sim_paths: Iterable[str]) -> dict[str, dict[str, dict[str, list[Any]]]]: |
There was a problem hiding this comment.
| def _collect_pen_data(self, sim_paths: Iterable[str]) -> dict[str, dict[str, dict[str, list[Any]]]]: | |
| def _collect_pen_data(self, sim_paths: list[str]) -> dict[str, dict[str, dict[str, list[Any]]]]: |
why not just list?
| matched variable name (for example ``"0_CALF"``) identifies the pen. Results are | ||
| grouped by scenario, which is ``"baseline"`` for a single run or the run's name | ||
| in a multi-run comparison. | ||
|
|
There was a problem hiding this comment.
Can you add an Example to show what the data would look like?
|
|
||
| def _get_annual_bedding_price( | ||
| self, | ||
| price_dict: Any, |
There was a problem hiding this comment.
| price_dict: Any, | |
| price_dict: dict[str, list[float | str]], |


Context
Issue(s) closed by this pull request: closes #3088
What
Reworks how the economics module computes the bedding cost line item. The "Bedding requirements" mapping entry is now routed to a dedicated per-pen processor (EconomicPreprocessor._preprocess_bedding) instead of the generic quantity×price engine, which could not represent bedding correctly. Also extracts two shared helpers (_package_line_item, _warn_if_pricing_missing) so the generic path and the new path build their output through one definition.
Why
On the example freestall farm the old code produced ~$42.50/yr of bedding cost when the correct figure is ~$18,700/yr (verified end-to-end; see Test plan). Three underlying bugs, all in the shared engine:
Price never matched. A pen's bedding_name is a user config name (e.g. calf_straw), but it was compared directly against the price-file keys (e.g. straw), so no price file ever matched and every pen fell back to a $1/head placeholder.
Pens were lumped. All pens' daily animal counts were averaged into one number and multiplied by one averaged price, never pairing a pen's count with that pen's own bedding price.
Annual units ignored. Prices are dollar_per_head per year while counts are per day, and a multi-year run needs each year priced separately.
How
Test plan
Input Changes
input/data/EEE/economics_map.json — updated the "Bedding requirements" block to mirror mapping.py
Output Changes
Filter
{ "direction": "landscape", "multiple": [ { "name": "Bedding total cost ($)", "filters": ["econ_bedding_total_cost$"] }, { "name": "Billed lactating head-years", "filters": ["econ_bedding_billed_head_years$"] }, { "name": "Average bedding price ($/head/yr)", "filters": ["econ_bedding_avg_price_per_head_year$"] }, { "name": "Average lactating head per day", "filters": ["number_of_animals_in_pen_.*LAC_COW$"], "vertical_aggregation": "average" } ] }