From 8762ebb0f9d06325fc3652b18731ac1a5a7d6a29 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 22 Oct 2025 06:55:21 +0900 Subject: [PATCH 01/21] Adding inputs --- input/data/manure_schedule/daily_spread_config.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 input/data/manure_schedule/daily_spread_config.json diff --git a/input/data/manure_schedule/daily_spread_config.json b/input/data/manure_schedule/daily_spread_config.json new file mode 100644 index 0000000000..1e20065b51 --- /dev/null +++ b/input/data/manure_schedule/daily_spread_config.json @@ -0,0 +1,8 @@ +{ + "use_daily_spread": false, + "max_nitrogen_masses": 500, + "max_phosphorus_masses": 500, + "max_potassium_masses": 500, + "application_depths": 30, + "surface_remainder_fractions": 0.20 +} \ No newline at end of file From 5f786e2dfe4dd0be7da287f5464668ce6f7267ba Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Fri, 7 Nov 2025 21:37:39 +0900 Subject: [PATCH 02/21] Getting sd inputs --- RUFAS/simulation_engine.py | 1 + 1 file changed, 1 insertion(+) diff --git a/RUFAS/simulation_engine.py b/RUFAS/simulation_engine.py index 659fe5f8c1..ac342ac20b 100644 --- a/RUFAS/simulation_engine.py +++ b/RUFAS/simulation_engine.py @@ -188,6 +188,7 @@ def generate_daily_manure_applications(self) -> list[ManureEventNutrientRequestR A list containing the ManureEvents and corresponding NutrientRequestResults to be applied to fields. """ manure_applications: list[ManureEventNutrientRequestResults] = [] + is_daily_spread = self.im.get_data("manure") for field in self.field_manager.fields: manure_events_requests = self.field_manager.check_manure_schedules(field, self.time) for manure_event_request in manure_events_requests: From af0831bbcc76b6f3df746d572ad7537e9b894195 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 17 Feb 2026 23:34:10 +0900 Subject: [PATCH 03/21] Daily spread mods pre testing --- RUFAS/biophysical/field/field/field.py | 37 ++++- .../field/manager/field_manager.py | 14 +- RUFAS/biophysical/manure/manure_manager.py | 156 +++++++++++++++--- .../manure/storage/daily_spread.py | 9 +- RUFAS/data_structures/events.py | 6 + .../manure_to_crop_soil_connection.py | 3 + input/metadata/properties/default.json | 54 +++++- .../manager_tests/test_field_manager.py | 6 +- .../test_manure_manager.py | 78 ++++++++- .../test_storage/test_daily_spread.py | 6 +- 10 files changed, 329 insertions(+), 40 deletions(-) diff --git a/RUFAS/biophysical/field/field/field.py b/RUFAS/biophysical/field/field/field.py index b532446cca..78f53d68ed 100644 --- a/RUFAS/biophysical/field/field/field.py +++ b/RUFAS/biophysical/field/field/field.py @@ -1,6 +1,6 @@ import math from math import exp -from typing import Dict, List, Optional, Sequence +from typing import Any, Dict, List, Optional, Sequence from RUFAS.current_day_conditions import CurrentDayConditions from RUFAS.data_structures.crop_soil_to_feed_storage_connection import HarvestedCrop @@ -109,6 +109,7 @@ def __init__( fertilizer_events: Optional[List[FertilizerEvent]] = None, fertilizer_mixes: Optional[Dict[str, Dict[str, float]]] = None, manure_events: Optional[List[ManureEvent]] = None, + daily_spread_settings: Optional[dict[str, Any]] = None, ) -> None: # field-wide attributes self.om = OutputManager() @@ -141,6 +142,7 @@ def __init__( self.manure_applicator = ManureApplication(self.soil.data) self.manure_events: list[ManureEvent] = manure_events or [] + self.daily_spread_settings = daily_spread_settings def manage_field( self, @@ -1057,8 +1059,40 @@ def check_manure_application_schedule(self, time: RufasTime) -> list[ManureEvent for event in todays_manure_events: manure_request = self._create_manure_request(event) manure_requests.append(ManureEventNutrientRequest(self.field_data.name, event, manure_request)) + + daily_spread_event = self._create_daily_spread_event(time) + if daily_spread_event is not None: + manure_request = self._create_manure_request(daily_spread_event) + manure_requests.append(ManureEventNutrientRequest(self.field_data.name, daily_spread_event, manure_request)) return manure_requests + def _create_daily_spread_event(self, time: RufasTime) -> ManureEvent | None: + """Creates a year-round daily spread event when enabled for this field.""" + if not self.daily_spread_settings: + return None + if not self.daily_spread_settings.get("is_daily_spreading", False): + return None + + manure_type = ManureType(self.daily_spread_settings.get("manure_type", ManureType.SOLID.value)) + manure_supplement_method = ManureSupplementMethod( + self.daily_spread_settings.get( + "supplement_manure_nutrient_deficiencies", + ManureSupplementMethod.NONE.value, + ) + ) + return ManureEvent( + nitrogen_mass=self.daily_spread_settings.get("max_nitrogen", 0.0), + phosphorus_mass=self.daily_spread_settings.get("max_phosphorus", 0.0), + manure_type=manure_type, + manure_supplement_method=manure_supplement_method, + field_coverage=self.daily_spread_settings.get("coverage_fraction", 1.0), + application_depth=self.daily_spread_settings.get("application_depth", 0.0), + surface_remainder_fraction=self.daily_spread_settings.get("surface_remainder_fraction", 1.0), + year=time.current_calendar_year, + day=time.current_julian_day, + is_daily_spread=True, + ) + def _create_manure_request(self, event: ManureEvent) -> NutrientRequest | None: """ Creates a NutrientRequest object from the attributes of a ManureEvent. @@ -1097,6 +1131,7 @@ def _create_manure_request(self, event: ManureEvent) -> NutrientRequest | None: phosphorus=event.phosphorus_mass, manure_type=event.manure_type, use_supplemental_manure=use_supplemental_manure, + use_daily_spread_source=bool(getattr(event, "is_daily_spread", False)), ) def _check_crop_harvest_schedule( diff --git a/RUFAS/biophysical/field/manager/field_manager.py b/RUFAS/biophysical/field/manager/field_manager.py index b94ff5f30e..2391a63a27 100644 --- a/RUFAS/biophysical/field/manager/field_manager.py +++ b/RUFAS/biophysical/field/manager/field_manager.py @@ -178,7 +178,9 @@ def _setup_field(field_name: str, available_crop_configs: list[str]) -> Field: field_configuration_data["fertilizer_management_specification"] ) - manure_events = FieldManager._setup_manure_events(field_configuration_data["manure_management_specification"]) + manure_events, daily_spread_settings = FieldManager._setup_manure_events( + field_configuration_data["manure_management_specification"] + ) tillage_events = FieldManager._setup_tillage_events( field_configuration_data["tillage_management_specification"] @@ -197,6 +199,7 @@ def _setup_field(field_name: str, available_crop_configs: list[str]) -> Field: fertilizer_events=fertilizer_events, fertilizer_mixes=available_fertilizer_mixes, manure_events=manure_events, + daily_spread_settings=daily_spread_settings, ) @staticmethod @@ -321,7 +324,7 @@ def _setup_fertilizer_events( return available_fertilizer_mixes, fertilizer_application_events @staticmethod - def _setup_manure_events(manure_schedule: str) -> list[ManureEvent]: + def _setup_manure_events(manure_schedule: str) -> tuple[list[ManureEvent], dict[str, Any] | None]: """ Sets up a list of manure events from ManureSchedule. @@ -332,8 +335,8 @@ def _setup_manure_events(manure_schedule: str) -> list[ManureEvent]: Returns ------- - list[ManureEvent] - A list of generated manure events. + tuple[list[ManureEvent], dict[str, Any] | None] + A list of generated manure events and optional daily spread settings. """ im = InputManager() @@ -368,7 +371,8 @@ def _setup_manure_events(manure_schedule: str) -> list[ManureEvent]: pattern_repeat=manure_schedule_data["pattern_repeat"], ) manure_events = manure_schedule_instance.generate_manure_events() - return manure_events + daily_spread_settings = manure_schedule_data.get("daily_spread") + return manure_events, daily_spread_settings @staticmethod def _setup_tillage_events(tillage_schedule: str) -> list[TillageEvent]: diff --git a/RUFAS/biophysical/manure/manure_manager.py b/RUFAS/biophysical/manure/manure_manager.py index 767413cbb0..533901d313 100644 --- a/RUFAS/biophysical/manure/manure_manager.py +++ b/RUFAS/biophysical/manure/manure_manager.py @@ -147,7 +147,7 @@ def _build_nutrient_pools(self) -> None: Build the pool for aggregated storage type. """ for name, processor in self.all_processors.items(): - if isinstance(processor, Storage): + if isinstance(processor, Storage) and not isinstance(processor, DailySpread): manure_type = STORAGE_CLASS_TO_TYPE.get(type(processor)) nutrients = ManureNutrients( manure_type=manure_type, @@ -852,12 +852,55 @@ def request_nutrients( """ if simulate_animals: - request_result, is_nutrient_request_fulfilled = self._manure_nutrient_manager.handle_nutrient_request( - request - ) - self._record_manure_request_results(request_result, "on_farm_manure", time) - if request_result is not None: - self._remove_nutrients_from_storage(request_result, request.manure_type) + if request.use_daily_spread_source: + request_result, is_nutrient_request_fulfilled = self._handle_nutrient_request_for_storage_subset( + request=request, + include_daily_spread=True, + ) + if request_result is not None: + self._remove_nutrients_from_storage( + request_result, + request.manure_type, + include_daily_spread=True, + update_nutrient_manager_pool=False, + ) + else: + request_result, is_nutrient_request_fulfilled = self._manure_nutrient_manager.handle_nutrient_request( + request + ) + if request_result is not None: + self._remove_nutrients_from_storage( + request_result, + request.manure_type, + include_daily_spread=False, + ) + + combined_on_farm_result = request_result + if ( + request.use_daily_spread_source + and not is_nutrient_request_fulfilled + and request.use_supplemental_manure + ): + supplemental_request = self._calculate_supplemental_manure_needed(combined_on_farm_result, request) + supplemental_request_result, supplemental_fulfilled = self._handle_nutrient_request_for_storage_subset( + request=supplemental_request, + include_daily_spread=False, + ) + if supplemental_request_result is not None: + self._remove_nutrients_from_storage( + supplemental_request_result, + request.manure_type, + include_daily_spread=False, + ) + if combined_on_farm_result is None: + combined_on_farm_result = supplemental_request_result + else: + combined_on_farm_result = combined_on_farm_result + supplemental_request_result + is_nutrient_request_fulfilled = supplemental_fulfilled or self._is_request_fulfilled( + request, combined_on_farm_result + ) + + self._record_manure_request_results(combined_on_farm_result, "on_farm_manure", time) if not is_nutrient_request_fulfilled and request.use_supplemental_manure: self._om.add_log( @@ -865,17 +908,25 @@ def request_nutrients( "Attempting to fulfill manure nutrient request shortfall with supplemental manure.", {"class": self.__class__.__name__, "function": self.request_nutrients.__name__}, ) - amount_supplemental_manure_needed = self._calculate_supplemental_manure_needed(request_result, request) + amount_supplemental_manure_needed = self._calculate_supplemental_manure_needed( + combined_on_farm_result, request + ) supplemental_manure = FieldManureSupplier.request_nutrients(amount_supplemental_manure_needed) self._record_manure_request_results(supplemental_manure, "off_farm_manure", time) - if request_result is None: + if combined_on_farm_result is None: return supplemental_manure - return request_result + supplemental_manure - return request_result + return combined_on_farm_result + supplemental_manure + return combined_on_farm_result else: return FieldManureSupplier.request_nutrients(request) - def _remove_nutrients_from_storage(self, results: NutrientRequestResults, manure_type: ManureType) -> None: + def _remove_nutrients_from_storage( + self, + results: NutrientRequestResults, + manure_type: ManureType, + include_daily_spread: bool, + update_nutrient_manager_pool: bool = True, + ) -> None: """ Remove nutrients from the storage based on the results of a nutrient request by manure type. @@ -885,7 +936,10 @@ def _remove_nutrients_from_storage(self, results: NutrientRequestResults, manure The results of a nutrient request. See :class:`NutrientsRequestResults` for details. """ - nutrient_pool = self._manure_nutrient_manager.nutrients_by_manure_category[manure_type] + storage_processors = self._get_storage_processors_by_daily_spread_type(include_daily_spread) + nutrient_pool = self._build_nutrient_pool_for_storages(storage_processors, manure_type) + if math.isclose(nutrient_pool.total_manure_mass, 0.0, abs_tol=1e-5): + return is_nitrogen_limiting_nutrient = self._determine_limiting_nutrient( results.nitrogen, nutrient_pool.nitrogen_composition, @@ -914,17 +968,75 @@ def _remove_nutrients_from_storage(self, results: NutrientRequestResults, manure "bedding_non_degradable_volatile_solids", ] - for name, processor in self.all_processors.items(): - if isinstance(processor, Storage): - processor.stored_manure, removal_details = self._compute_stream_after_removal( - stored_manure=processor.stored_manure, - nutrient_removal_proportion=proportion_of_limiting_nutrient_to_remove, - is_nitrogen_limiting_nutrient=is_nitrogen_limiting_nutrient, - non_limiting_fields=non_limiting_fields.copy(), - ) - removal_details["manure_type"] = STORAGE_CLASS_TO_TYPE.get(type(processor)) + for processor in storage_processors: + storage_manure_type = STORAGE_CLASS_TO_TYPE.get(type(processor)) + if storage_manure_type != manure_type: + continue + processor.stored_manure, removal_details = self._compute_stream_after_removal( + stored_manure=processor.stored_manure, + nutrient_removal_proportion=proportion_of_limiting_nutrient_to_remove, + is_nitrogen_limiting_nutrient=is_nitrogen_limiting_nutrient, + non_limiting_fields=non_limiting_fields.copy(), + ) + if update_nutrient_manager_pool: + removal_details["manure_type"] = storage_manure_type self._manure_nutrient_manager.remove_nutrients(removal_details) + def _get_storage_processors_by_daily_spread_type(self, include_daily_spread: bool) -> list[Storage]: + """Return storages filtered by whether they are DailySpread processors.""" + return [ + processor + for processor in self.all_processors.values() + if isinstance(processor, Storage) and isinstance(processor, DailySpread) == include_daily_spread + ] + + def _build_nutrient_pool_for_storages(self, storages: list[Storage], manure_type: ManureType) -> ManureNutrients: + """Build an aggregated nutrient pool for a subset of storages and a manure type.""" + nitrogen = 0.0 + phosphorus = 0.0 + potassium = 0.0 + total_manure_mass = 0.0 + dry_matter = 0.0 + for storage in storages: + storage_manure_type = STORAGE_CLASS_TO_TYPE.get(type(storage)) + if storage_manure_type != manure_type: + continue + stored_manure = storage.stored_manure + nitrogen += stored_manure.nitrogen + phosphorus += stored_manure.phosphorus + potassium += stored_manure.potassium + total_manure_mass += stored_manure.mass + dry_matter += stored_manure.total_solids + return ManureNutrients( + manure_type=manure_type, + nitrogen=nitrogen, + phosphorus=phosphorus, + potassium=potassium, + total_manure_mass=total_manure_mass, + dry_matter=dry_matter, + ) + + def _handle_nutrient_request_for_storage_subset( + self, request: NutrientRequest, include_daily_spread: bool + ) -> tuple[NutrientRequestResults | None, bool]: + """Handle a nutrient request for a subset of storages filtered by DailySpread type.""" + storages = self._get_storage_processors_by_daily_spread_type(include_daily_spread) + nutrient_pool = self._build_nutrient_pool_for_storages(storages, request.manure_type) + subset_manager = ManureNutrientManager() + subset_manager.reset_nutrient_pools() + subset_manager.add_nutrients(nutrient_pool) + return subset_manager.handle_nutrient_request(request) + + @staticmethod + def _is_request_fulfilled(request: NutrientRequest, result: NutrientRequestResults | None) -> bool: + """Return True if requested nutrients are met by the supplied result.""" + if result is None: + return False + return ( + math.isclose(result.nitrogen, request.nitrogen, abs_tol=1e-5) + and math.isclose(result.phosphorus, request.phosphorus, abs_tol=1e-5) + ) + @staticmethod def _compute_stream_after_removal( stored_manure: ManureStream, diff --git a/RUFAS/biophysical/manure/storage/daily_spread.py b/RUFAS/biophysical/manure/storage/daily_spread.py index e9512f3e01..dbf10590df 100644 --- a/RUFAS/biophysical/manure/storage/daily_spread.py +++ b/RUFAS/biophysical/manure/storage/daily_spread.py @@ -52,4 +52,11 @@ def process_manure(self, current_day_conditions: CurrentDayConditions, time: Ruf """ self._report_manure_stream(self._received_manure, "received", time.simulation_day) - return super().process_manure(current_day_conditions, time) + manure_held_before_processing = self.stored_manure + self._received_manure + output_streams = super().process_manure(current_day_conditions, time) + emptied_manure_stream = output_streams.get("manure") + if emptied_manure_stream is not None and not emptied_manure_stream.is_empty: + self._report_manure_stream(emptied_manure_stream, "exported_excess", time.simulation_day) + elif manure_held_before_processing.is_empty: + self._report_manure_stream(ManureStream.make_empty_manure_stream(), "exported_excess", time.simulation_day) + return output_streams diff --git a/RUFAS/data_structures/events.py b/RUFAS/data_structures/events.py index 98e4783b85..b26d509d61 100644 --- a/RUFAS/data_structures/events.py +++ b/RUFAS/data_structures/events.py @@ -231,6 +231,8 @@ class ManureEvent(BaseFieldManagementEvent): Depth that manure is injected into the soil at (mm). surface_remainder_fraction : float Fraction of manure applied that remains on the soil surface (unitless). + is_daily_spread : bool + Whether this manure event is from the daily spread pathway. """ @@ -245,6 +247,7 @@ def __init__( surface_remainder_fraction: float, year: int, day: int, + is_daily_spread: bool = False, ): super().__init__(year=year, day=day) self.nitrogen_mass = nitrogen_mass @@ -254,6 +257,7 @@ def __init__( self.field_coverage = field_coverage self.application_depth = application_depth self.surface_remainder_fraction = surface_remainder_fraction + self.is_daily_spread = is_daily_spread def __eq__(self, other) -> bool: """Overrides the equality operator for ManureEvent objects.""" @@ -267,6 +271,7 @@ def __eq__(self, other) -> bool: and other.application_depth == self.application_depth and other.surface_remainder_fraction == self.surface_remainder_fraction and other.manure_supplement_method == self.manure_supplement_method + and other.is_daily_spread == self.is_daily_spread ) return False @@ -282,6 +287,7 @@ def __hash__(self) -> int: self.field_coverage, self.application_depth, self.surface_remainder_fraction, + self.is_daily_spread, ) ) diff --git a/RUFAS/data_structures/manure_to_crop_soil_connection.py b/RUFAS/data_structures/manure_to_crop_soil_connection.py index d82fd76c91..86ea6ce356 100644 --- a/RUFAS/data_structures/manure_to_crop_soil_connection.py +++ b/RUFAS/data_structures/manure_to_crop_soil_connection.py @@ -21,6 +21,9 @@ class NutrientRequest: use_supplemental_manure: bool """Whether to use supplemental manure if the request cannot be fulfilled by on-farm manure.""" + use_daily_spread_source: bool = False + """Whether this request should be sourced from DailySpread storages first.""" + def __post_init__(self) -> None: """ Validate the dataclass fields. diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index c5f9da8132..5a74ea4b09 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -2961,6 +2961,58 @@ "pattern": "^(manure|synthetic fertilizer|none|synthetic fertilizer and manure)$" } }, + "daily_spread": { + "type": "object", + "description": "Configuration for year-round daily manure spreading from DailySpread processors.", + "properties": { + "is_daily_spreading": { + "type": "bool", + "description": "Whether this field uses daily manure spreading." + }, + "max_nitrogen": { + "type": "number", + "description": "Maximum nitrogen mass to request each day from daily spread manure. Units: kg.", + "minimum": 0.0 + }, + "max_phosphorus": { + "type": "number", + "description": "Maximum phosphorus mass to request each day from daily spread manure. Units: kg.", + "minimum": 0.0 + }, + "max_potassium": { + "type": "number", + "description": "Maximum potassium mass to request each day from daily spread manure. Units: kg.", + "minimum": 0.0 + }, + "coverage_fraction": { + "type": "number", + "description": "Fraction of field area covered by each daily spread manure application.", + "minimum": 0.01, + "maximum": 1.0 + }, + "application_depth": { + "type": "number", + "description": "Depth at which daily spread manure is injected into the soil. Units: mm.", + "minimum": 0.0 + }, + "surface_remainder_fraction": { + "type": "number", + "description": "Fraction of daily spread manure that remains on the surface after application.", + "minimum": 0.0, + "maximum": 1.0 + }, + "manure_type": { + "type": "string", + "description": "Type of manure requested for daily spread applications.", + "pattern": "^(liquid|solid)$" + }, + "supplement_manure_nutrient_deficiencies": { + "type": "string", + "description": "Determines whether daily spread nutrient shortfalls can be supplemented by manure and/or synthetic fertilizer.", + "pattern": "^(manure|synthetic fertilizer|none|synthetic fertilizer and manure)$" + } + } + }, "pattern_repeat": { "type": "number", "description": "Number of times that this manure application schedule should be repeated.", @@ -6838,4 +6890,4 @@ } } } -} \ No newline at end of file +} diff --git a/tests/test_biophysical/test_crop_soil_field/manager_tests/test_field_manager.py b/tests/test_biophysical/test_crop_soil_field/manager_tests/test_field_manager.py index 1110ce4bfa..eb4dd3c94d 100644 --- a/tests/test_biophysical/test_crop_soil_field/manager_tests/test_field_manager.py +++ b/tests/test_biophysical/test_crop_soil_field/manager_tests/test_field_manager.py @@ -831,8 +831,9 @@ def test_setup_manure_schedule( """Tests that ManureSchedules are correctly initialized with data from the InputManager.""" mock_input_manager.get_data = mock.MagicMock(return_value=manure_schedule_data) expected_manure_events = expected_manure_schedule.generate_manure_events() - actual_manure_events = FieldManager._setup_manure_events("test_manure_schedule") + actual_manure_events, actual_daily_spread_settings = FieldManager._setup_manure_events("test_manure_schedule") assert actual_manure_events == expected_manure_events + assert actual_daily_spread_settings is None mock_input_manager.get_data.assert_called_once_with("test_manure_schedule") mock_input_manager.get_data = input_manager_original_method_states["get_data"] @@ -1974,7 +1975,7 @@ def test_setup_field( ) mock_setup_manure_events = mocker.patch( "RUFAS.biophysical.field.manager.field_manager.FieldManager._setup_manure_events", - return_value=mock_manure_events, + return_value=(mock_manure_events, {"is_daily_spreading": True}), ) mock_setup_tillage_events = mocker.patch( "RUFAS.biophysical.field.manager.field_manager.FieldManager._setup_tillage_events", @@ -2005,6 +2006,7 @@ def test_setup_field( "100_0_0": {"N": 1.0, "P": 0.0, "K": 0.0, "ammonium_fraction": 0.0}, "26_4_24": {"N": 0.26, "P": 0.04, "K": 0.24, "ammonium_fraction": 0.0}, } + assert new_field.daily_spread_settings == {"is_daily_spreading": True} mock_input_manager.get_data.assert_called_once_with(field_name) mock_setup_fertilizer_events.assert_called_once_with(field_config.get("fertilizer_management_specification")) diff --git a/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py b/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py index 041987a735..f9345b106c 100644 --- a/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py +++ b/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py @@ -11,7 +11,6 @@ from RUFAS.biophysical.manure.manure_nutrient_manager import ManureNutrientManager from RUFAS.biophysical.manure.processor import Processor from RUFAS.biophysical.manure.separator.separator import Separator -from RUFAS.biophysical.manure.storage.composting import Composting from RUFAS.biophysical.manure.storage.storage import Storage from RUFAS.biophysical.manure.storage.storage_cover import StorageCover from RUFAS.current_day_conditions import CurrentDayConditions @@ -919,6 +918,7 @@ def test_request_nutrients( mock_nutrient_request.use_supplemental_manure = use_supplemental_manure mock_nutrient_request.manure_type = ManureType.LIQUID + mock_nutrient_request.use_daily_spread_source = False request_result = ( None @@ -960,6 +960,55 @@ def test_request_nutrients( assert actual_results == supplemental_result +def test_request_nutrients_daily_spread_with_on_farm_supplement(mocker: MockerFixture) -> None: + """Tests daily spread requests are fulfilled from daily spread first, then non-daily storages.""" + mocker.patch("RUFAS.biophysical.manure.manure_manager.ManureManager.__init__", return_value=None) + manure_manager = ManureManager(0.6, 0.6, 0.6) + manure_manager._om = OutputManager() + manure_manager._manure_nutrient_manager = ManureNutrientManager() + + request = NutrientRequest( + nitrogen=10.0, + phosphorus=5.0, + manure_type=ManureType.SOLID, + use_supplemental_manure=True, + use_daily_spread_source=True, + ) + daily_result = NutrientRequestResults(nitrogen=3.0, phosphorus=2.0, total_manure_mass=10.0) + non_daily_result = NutrientRequestResults(nitrogen=7.0, phosphorus=3.0, total_manure_mass=20.0) + + mock_subset_request = mocker.patch.object( + manure_manager, + "_handle_nutrient_request_for_storage_subset", + side_effect=[(daily_result, False), (non_daily_result, True)], + ) + mock_remove = mocker.patch.object(manure_manager, "_remove_nutrients_from_storage") + mock_record = mocker.patch.object(manure_manager, "_record_manure_request_results") + mock_off_farm = mocker.patch.object(FieldManureSupplier, "request_nutrients") + + actual = manure_manager.request_nutrients(request, True, MagicMock(spec=RufasTime)) + + assert actual == daily_result + non_daily_result + assert mock_subset_request.call_args_list == [ + call(request=request, include_daily_spread=True), + call( + request=NutrientRequest( + nitrogen=7.0, + phosphorus=3.0, + manure_type=ManureType.SOLID, + use_supplemental_manure=True, + ), + include_daily_spread=False, + ), + ] + assert mock_remove.call_args_list == [ + call(daily_result, ManureType.SOLID, include_daily_spread=True, update_nutrient_manager_pool=False), + call(non_daily_result, ManureType.SOLID, include_daily_spread=False), + ] + mock_record.assert_called_once() + mock_off_farm.assert_not_called() + + @pytest.mark.parametrize("is_nitrogen_limiting_nutrient", [True, False]) def test_remove_nutrients_from_storage( manure_manager: ManureManager, is_nitrogen_limiting_nutrient: bool, mocker: MockerFixture @@ -975,11 +1024,28 @@ def test_remove_nutrients_from_storage( mock_compute = mocker.patch.object( ManureManager, "_compute_stream_after_removal", return_value=(MagicMock(ManureStream), {"nitrogen": 50}) ) - composting = MagicMock(Composting) - composting.stored_manure = MagicMock(ManureStream) - manure_manager.all_processors = {"non_storage": MagicMock(Digester), "storage": composting} + storage = MagicMock(Storage) + storage.stored_manure = ManureStream( + water=100.0, + ammoniacal_nitrogen=10.0, + nitrogen=100.0, + phosphorus=200.0, + potassium=20.0, + ash=5.0, + non_degradable_volatile_solids=1.0, + degradable_volatile_solids=2.0, + total_solids=120.0, + volume=1.0, + methane_production_potential=0.24, + pen_manure_data=None, + bedding_non_degradable_volatile_solids=0.5, + ) + STORAGE_CLASS_TO_TYPE[type(storage)] = ManureType.LIQUID + manure_manager.all_processors = {"non_storage": MagicMock(Digester), "storage": storage} - manure_manager._remove_nutrients_from_storage(NutrientRequestResults(nitrogen=10, phosphorus=20), ManureType.LIQUID) + manure_manager._remove_nutrients_from_storage( + NutrientRequestResults(nitrogen=10, phosphorus=20), ManureType.LIQUID, include_daily_spread=False + ) mock_determine_limiting_nutrient.assert_called_once() if is_nitrogen_limiting_nutrient: @@ -987,7 +1053,7 @@ def test_remove_nutrients_from_storage( else: mock_proportion.assert_called_once_with(20, 200) mock_compute.assert_called_once() - mock_remove.assert_called_once_with({"nitrogen": 50, "manure_type": None}) + mock_remove.assert_called_once_with({"nitrogen": 50, "manure_type": ManureType.LIQUID}) @pytest.mark.parametrize( diff --git a/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py b/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py index 647ca5856a..af158ba37a 100644 --- a/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py +++ b/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py @@ -67,11 +67,13 @@ def test_process_manure( mock_time.simulation_day = 50 daily_spread_instance._received_manure = received_manure mock_report = mocker.patch.object(Processor, "_report_manure_stream") - mock_process = mocker.patch.object(Storage, "process_manure") + exported_stream = received_manure + mock_process = mocker.patch.object(Storage, "process_manure", return_value={"manure": exported_stream}) daily_spread_instance.process_manure(mock_conditions, mock_time) - mock_report.assert_called_once_with(received_manure, "received", 50) + mock_report.assert_any_call(received_manure, "received", 50) + mock_report.assert_any_call(exported_stream, "exported_excess", 50) mock_process.assert_called_once_with(mock_conditions, mock_time) From 55c3617519449182c7b5c7bcd85bb2c555d34ccd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 17 Feb 2026 14:35:54 +0000 Subject: [PATCH 04/21] Apply Black Formatting --- RUFAS/biophysical/manure/manure_manager.py | 5 ++--- .../test_manure/test_storage/test_daily_spread.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/RUFAS/biophysical/manure/manure_manager.py b/RUFAS/biophysical/manure/manure_manager.py index 533901d313..cefe0e4004 100644 --- a/RUFAS/biophysical/manure/manure_manager.py +++ b/RUFAS/biophysical/manure/manure_manager.py @@ -1032,9 +1032,8 @@ def _is_request_fulfilled(request: NutrientRequest, result: NutrientRequestResul """Return True if requested nutrients are met by the supplied result.""" if result is None: return False - return ( - math.isclose(result.nitrogen, request.nitrogen, abs_tol=1e-5) - and math.isclose(result.phosphorus, request.phosphorus, abs_tol=1e-5) + return math.isclose(result.nitrogen, request.nitrogen, abs_tol=1e-5) and math.isclose( + result.phosphorus, request.phosphorus, abs_tol=1e-5 ) @staticmethod diff --git a/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py b/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py index af158ba37a..c867bb4d79 100644 --- a/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py +++ b/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py @@ -35,7 +35,7 @@ def received_manure() -> ManureStream: volume=10.12, methane_production_potential=0.24, pen_manure_data=None, - bedding_non_degradable_volatile_solids=10 + bedding_non_degradable_volatile_solids=10, ) From b47a9c8fa8021cd4a425cad71c9a9b3de4b8bf47 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Tue, 17 Feb 2026 14:39:17 +0000 Subject: [PATCH 05/21] Update badges on README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a9660d1af8..3d898a8abf 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -[![Flake8](https://img.shields.io/badge/Flake8-passed-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Pytest](https://img.shields.io/badge/Pytest-passed-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Coverage](https://img.shields.io/badge/Coverage-99%25-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Mypy](https://img.shields.io/badge/Mypy-1684%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1685%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 5ca25f301ab70bf26bc79dae029da69b06aafa40 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Tue, 24 Feb 2026 21:11:58 +0900 Subject: [PATCH 06/21] Temporary implementation before holding on for connection clean up --- RUFAS/biophysical/field/field/field.py | 41 ++++- .../field/manager/field_manager.py | 14 +- RUFAS/biophysical/manure/manure_manager.py | 155 ++++++++++++++++-- .../manure/storage/daily_spread.py | 17 +- RUFAS/data_structures/events.py | 6 + .../manure_to_crop_soil_connection.py | 3 + .../example_manure_schedule_file.json | 32 ++++ input/metadata/properties/default.json | 69 +++++++- .../field_tests/test_field.py | 72 ++++++++ .../manager_tests/test_field_manager.py | 6 +- .../test_manure_manager.py | 67 +++++++- .../test_storage/test_daily_spread.py | 7 +- tests/test_simulation_engine.py | 4 + 13 files changed, 458 insertions(+), 35 deletions(-) create mode 100644 input/data/manure_schedule/example_manure_schedule_file.json diff --git a/RUFAS/biophysical/field/field/field.py b/RUFAS/biophysical/field/field/field.py index a60c942fbd..43f6fbcfd2 100644 --- a/RUFAS/biophysical/field/field/field.py +++ b/RUFAS/biophysical/field/field/field.py @@ -1,6 +1,6 @@ import math from math import exp -from typing import Dict, List, Optional, Sequence +from typing import Any, Dict, List, Optional, Sequence from RUFAS.current_day_conditions import CurrentDayConditions from RUFAS.data_structures.crop_soil_to_feed_storage_connection import HarvestedCrop @@ -109,6 +109,7 @@ def __init__( fertilizer_events: Optional[List[FertilizerEvent]] = None, fertilizer_mixes: Optional[Dict[str, Dict[str, float]]] = None, manure_events: Optional[List[ManureEvent]] = None, + daily_spread_settings: Optional[dict[str, Any]] = None, ) -> None: # field-wide attributes self.om = OutputManager() @@ -141,6 +142,7 @@ def __init__( self.manure_applicator = ManureApplication(self.soil.data) self.manure_events: list[ManureEvent] = manure_events or [] + self.daily_spread_settings = daily_spread_settings def manage_field( self, @@ -1057,8 +1059,44 @@ def check_manure_application_schedule(self, time: RufasTime) -> list[ManureEvent for event in todays_manure_events: manure_request = self._create_manure_request(event) manure_requests.append(ManureEventNutrientRequest(self.field_data.name, event, manure_request)) + + daily_spread_event = self._create_daily_spread_event(time) + if daily_spread_event is not None: + manure_request = self._create_manure_request(daily_spread_event) + manure_requests.append(ManureEventNutrientRequest(self.field_data.name, daily_spread_event, manure_request)) return manure_requests + def _create_daily_spread_event(self, time: RufasTime) -> ManureEvent | None: + """Creates a daily manure event from daily spread settings, if enabled.""" + if not self.daily_spread_settings: + return None + if not self.daily_spread_settings.get("is_daily_spreading", False): + return None + + manure_type = ManureType(self.daily_spread_settings.get("manure_type", ManureType.SOLID.value)) + manure_supplement_method = ManureSupplementMethod( + self.daily_spread_settings.get( + "supplement_manure_nutrient_deficiencies", + ManureSupplementMethod.NONE.value, + ) + ) + nitrogen_spread_amount = self.daily_spread_settings.get("nitrogen_spread_amount", 0.0) + phosphorus_spread_amount = self.daily_spread_settings.get("phosphorus_spread_amount", 0.0) + nitrogen_cap = self.daily_spread_settings.get("max_nitrogen", nitrogen_spread_amount) + phosphorus_cap = self.daily_spread_settings.get("max_phosphorus", phosphorus_spread_amount) + return ManureEvent( + nitrogen_mass=min(nitrogen_spread_amount, nitrogen_cap), + phosphorus_mass=min(phosphorus_spread_amount, phosphorus_cap), + manure_type=manure_type, + manure_supplement_method=manure_supplement_method, + field_coverage=self.daily_spread_settings.get("coverage_fraction", 1.0), + application_depth=self.daily_spread_settings.get("application_depth", 0.0), + surface_remainder_fraction=self.daily_spread_settings.get("surface_remainder_fraction", 1.0), + year=time.current_calendar_year, + day=time.current_julian_day, + is_daily_spread=True, + ) + def _create_manure_request(self, event: ManureEvent) -> NutrientRequest | None: """ Creates a NutrientRequest object from the attributes of a ManureEvent. @@ -1097,6 +1135,7 @@ def _create_manure_request(self, event: ManureEvent) -> NutrientRequest | None: phosphorus=event.phosphorus_mass, manure_type=event.manure_type, use_supplemental_manure=use_supplemental_manure, + use_daily_spread_source=bool(getattr(event, "is_daily_spread", False)), ) def _check_crop_harvest_schedule( diff --git a/RUFAS/biophysical/field/manager/field_manager.py b/RUFAS/biophysical/field/manager/field_manager.py index e33ad9e3f0..7076931b20 100644 --- a/RUFAS/biophysical/field/manager/field_manager.py +++ b/RUFAS/biophysical/field/manager/field_manager.py @@ -178,7 +178,9 @@ def _setup_field(field_name: str, available_crop_configs: list[str]) -> Field: field_configuration_data["fertilizer_management_specification"] ) - manure_events = FieldManager._setup_manure_events(field_configuration_data["manure_management_specification"]) + manure_events, daily_spread_settings = FieldManager._setup_manure_events( + field_configuration_data["manure_management_specification"] + ) tillage_events = FieldManager._setup_tillage_events( field_configuration_data["tillage_management_specification"] @@ -197,6 +199,7 @@ def _setup_field(field_name: str, available_crop_configs: list[str]) -> Field: fertilizer_events=fertilizer_events, fertilizer_mixes=available_fertilizer_mixes, manure_events=manure_events, + daily_spread_settings=daily_spread_settings, ) @staticmethod @@ -313,7 +316,7 @@ def _setup_fertilizer_events( return available_fertilizer_mixes, fertilizer_application_events @staticmethod - def _setup_manure_events(manure_schedule: str) -> list[ManureEvent]: + def _setup_manure_events(manure_schedule: str) -> tuple[list[ManureEvent], dict[str, Any] | None]: """ Sets up a list of manure events from ManureSchedule. @@ -324,8 +327,8 @@ def _setup_manure_events(manure_schedule: str) -> list[ManureEvent]: Returns ------- - list[ManureEvent] - A list of generated manure events. + tuple[list[ManureEvent], dict[str, Any] | None] + A list of generated manure events and optional daily spread settings. """ im = InputManager() @@ -352,7 +355,8 @@ def _setup_manure_events(manure_schedule: str) -> list[ManureEvent]: pattern_repeat=manure_schedule_data["pattern_repeat"], ) manure_events = manure_schedule_instance.generate_manure_events() - return manure_events + daily_spread_settings = manure_schedule_data.get("daily_spread") + return manure_events, daily_spread_settings @staticmethod def _setup_tillage_events(tillage_schedule: str) -> list[TillageEvent]: diff --git a/RUFAS/biophysical/manure/manure_manager.py b/RUFAS/biophysical/manure/manure_manager.py index c17b79b44c..4d4309b089 100644 --- a/RUFAS/biophysical/manure/manure_manager.py +++ b/RUFAS/biophysical/manure/manure_manager.py @@ -145,7 +145,7 @@ def _build_nutrient_pools(self) -> None: Build the pool for aggregated storage type. """ for name, processor in self.all_processors.items(): - if isinstance(processor, Storage): + if isinstance(processor, Storage) and not isinstance(processor, DailySpread): manure_type = STORAGE_CLASS_TO_TYPE.get(type(processor)) nutrients = ManureNutrients( manure_type=manure_type, @@ -837,12 +837,31 @@ def request_nutrients( """ if simulate_animals: - request_result, is_nutrient_request_fulfilled = self._manure_nutrient_manager.handle_nutrient_request( - request - ) + if request.use_daily_spread_source: + daily_spread_storages, _ = self._split_storages_by_daily_spread() + request_result, is_nutrient_request_fulfilled = self._handle_nutrient_request_for_storages( + request=request, storages=daily_spread_storages + ) + if request_result is not None: + self._remove_nutrients_from_storage( + request_result, + request.manure_type, + include_daily_spread=True, + update_nutrient_manager_pool=False, + ) + + else: + request_result, is_nutrient_request_fulfilled = self._manure_nutrient_manager.handle_nutrient_request( + request + ) + if request_result is not None: + self._remove_nutrients_from_storage( + request_result, + request.manure_type, + include_daily_spread=False, + ) + self._record_manure_request_results(request_result, "on_farm_manure", time) - if request_result is not None: - self._remove_nutrients_from_storage(request_result, request.manure_type) if not is_nutrient_request_fulfilled and request.use_supplemental_manure: self._om.add_log( @@ -850,7 +869,9 @@ def request_nutrients( "Attempting to fulfill manure nutrient request shortfall with supplemental manure.", {"class": self.__class__.__name__, "function": self.request_nutrients.__name__}, ) - amount_supplemental_manure_needed = self._calculate_supplemental_manure_needed(request_result, request) + amount_supplemental_manure_needed = self._calculate_supplemental_manure_needed( + request_result, request + ) supplemental_manure = FieldManureSupplier.request_nutrients(amount_supplemental_manure_needed) self._record_manure_request_results(supplemental_manure, "off_farm_manure", time) if request_result is None: @@ -860,7 +881,13 @@ def request_nutrients( else: return FieldManureSupplier.request_nutrients(request) - def _remove_nutrients_from_storage(self, results: NutrientRequestResults, manure_type: ManureType) -> None: + def _remove_nutrients_from_storage( + self, + results: NutrientRequestResults, + manure_type: ManureType, + include_daily_spread: bool, + update_nutrient_manager_pool: bool = True, + ) -> None: """ Remove nutrients from the storage based on the results of a nutrient request by manure type. @@ -870,7 +897,35 @@ def _remove_nutrients_from_storage(self, results: NutrientRequestResults, manure The results of a nutrient request. See :class:`NutrientsRequestResults` for details. """ - nutrient_pool = self._manure_nutrient_manager.nutrients_by_manure_category[manure_type] + daily_spread_storages, non_daily_storages = self._split_storages_by_daily_spread() + storage_processors: list[Storage] = daily_spread_storages if include_daily_spread else non_daily_storages + nitrogen = 0.0 + phosphorus = 0.0 + potassium = 0.0 + total_manure_mass = 0.0 + dry_matter = 0.0 + for storage in storage_processors: + storage_manure_type = STORAGE_CLASS_TO_TYPE.get(type(storage)) + if storage_manure_type != manure_type: + continue + source_stream = ( + storage.available_for_field_application if isinstance(storage, DailySpread) else storage.stored_manure + ) + nitrogen += source_stream.nitrogen + phosphorus += source_stream.phosphorus + potassium += source_stream.potassium + total_manure_mass += source_stream.mass + dry_matter += source_stream.total_solids + nutrient_pool = ManureNutrients( + manure_type=manure_type, + nitrogen=nitrogen, + phosphorus=phosphorus, + potassium=potassium, + total_manure_mass=total_manure_mass, + dry_matter=dry_matter, + ) + if math.isclose(nutrient_pool.total_manure_mass, 0.0, abs_tol=1e-5): + return is_nitrogen_limiting_nutrient = self._determine_limiting_nutrient( results.nitrogen, nutrient_pool.nitrogen_composition, @@ -898,17 +953,81 @@ def _remove_nutrients_from_storage(self, results: NutrientRequestResults, manure "total_solids", ] - for name, processor in self.all_processors.items(): - if isinstance(processor, Storage): - processor.stored_manure, removal_details = self._compute_stream_after_removal( - stored_manure=processor.stored_manure, - nutrient_removal_proportion=proportion_of_limiting_nutrient_to_remove, - is_nitrogen_limiting_nutrient=is_nitrogen_limiting_nutrient, - non_limiting_fields=non_limiting_fields.copy(), - ) - removal_details["manure_type"] = STORAGE_CLASS_TO_TYPE.get(type(processor)) + for processor in storage_processors: + storage_manure_type = STORAGE_CLASS_TO_TYPE.get(type(processor)) + if storage_manure_type != manure_type: + continue + source_stream = ( + processor.available_for_field_application if isinstance(processor, DailySpread) else processor.stored_manure + ) + updated_stream, removal_details = self._compute_stream_after_removal( + stored_manure=source_stream, + nutrient_removal_proportion=proportion_of_limiting_nutrient_to_remove, + is_nitrogen_limiting_nutrient=is_nitrogen_limiting_nutrient, + non_limiting_fields=non_limiting_fields.copy(), + ) + if isinstance(processor, DailySpread): + processor.set_available_for_field_application(updated_stream) + else: + processor.stored_manure = updated_stream + if update_nutrient_manager_pool: + removal_details["manure_type"] = storage_manure_type self._manure_nutrient_manager.remove_nutrients(removal_details) + def _split_storages_by_daily_spread(self) -> tuple[list[Storage], list[Storage]]: + """Split all storages into daily spread and non-daily spread groups.""" + daily_spread_storages = [] + non_daily_storages = [] + for processor in self.all_processors.values(): + if not isinstance(processor, Storage): + continue + if isinstance(processor, DailySpread): + daily_spread_storages.append(processor) + else: + non_daily_storages.append(processor) + return daily_spread_storages, non_daily_storages + + def _handle_nutrient_request_for_storages( + self, request: NutrientRequest, storages: list[Storage] + ) -> tuple[NutrientRequestResults | None, bool]: + """Handle a nutrient request for an explicit set of storages.""" + nitrogen = 0.0 + phosphorus = 0.0 + potassium = 0.0 + total_manure_mass = 0.0 + dry_matter = 0.0 + for storage in storages: + storage_manure_type = STORAGE_CLASS_TO_TYPE.get(type(storage)) + if storage_manure_type != request.manure_type: + continue + source_stream = ( + storage.available_for_field_application if isinstance(storage, DailySpread) else storage.stored_manure + ) + nitrogen += source_stream.nitrogen + phosphorus += source_stream.phosphorus + potassium += source_stream.potassium + total_manure_mass += source_stream.mass + dry_matter += source_stream.total_solids + nutrient_pool = ManureNutrients( + manure_type=request.manure_type, + nitrogen=nitrogen, + phosphorus=phosphorus, + potassium=potassium, + total_manure_mass=total_manure_mass, + dry_matter=dry_matter, + ) + subset_manager = ManureNutrientManager() + subset_manager.reset_nutrient_pools() + subset_manager.add_nutrients(nutrient_pool) + return subset_manager.handle_nutrient_request(request) + + def finalize_daily_spread_exports(self, time: RufasTime) -> None: + """Report and clear remaining daily spread manure available for field application.""" + daily_spread_storages, _ = self._split_storages_by_daily_spread() + for processor in daily_spread_storages: + assert isinstance(processor, DailySpread) + processor.export_and_clear_remaining_available(time) + @staticmethod def _compute_stream_after_removal( stored_manure: ManureStream, diff --git a/RUFAS/biophysical/manure/storage/daily_spread.py b/RUFAS/biophysical/manure/storage/daily_spread.py index e9512f3e01..7fbca7cf69 100644 --- a/RUFAS/biophysical/manure/storage/daily_spread.py +++ b/RUFAS/biophysical/manure/storage/daily_spread.py @@ -22,6 +22,7 @@ def __init__( storage_time_period=storage_time_period, surface_area=surface_area, ) + self.available_for_field_application = ManureStream.make_empty_manure_stream() def receive_manure(self, manure: ManureStream) -> None: """ @@ -52,4 +53,18 @@ def process_manure(self, current_day_conditions: CurrentDayConditions, time: Ruf """ self._report_manure_stream(self._received_manure, "received", time.simulation_day) - return super().process_manure(current_day_conditions, time) + output_streams = super().process_manure(current_day_conditions, time) + self.available_for_field_application = output_streams.get("manure", ManureStream.make_empty_manure_stream()) + self._report_manure_stream( + self.available_for_field_application, "available_for_application", time.simulation_day + ) + return {} + + def set_available_for_field_application(self, stream: ManureStream) -> None: + """Set remaining manure available for daily spread field application.""" + self.available_for_field_application = stream + + def export_and_clear_remaining_available(self, time: RufasTime) -> None: + """Report and clear leftover daily spread manure at end of day.""" + self._report_manure_stream(self.available_for_field_application, "exported_excess", time.simulation_day) + self.available_for_field_application = ManureStream.make_empty_manure_stream() diff --git a/RUFAS/data_structures/events.py b/RUFAS/data_structures/events.py index 98e4783b85..9953010ad3 100644 --- a/RUFAS/data_structures/events.py +++ b/RUFAS/data_structures/events.py @@ -231,6 +231,8 @@ class ManureEvent(BaseFieldManagementEvent): Depth that manure is injected into the soil at (mm). surface_remainder_fraction : float Fraction of manure applied that remains on the soil surface (unitless). + is_daily_spread : bool + Whether this manure event is from daily spread handling. """ @@ -245,6 +247,7 @@ def __init__( surface_remainder_fraction: float, year: int, day: int, + is_daily_spread: bool = False, ): super().__init__(year=year, day=day) self.nitrogen_mass = nitrogen_mass @@ -254,6 +257,7 @@ def __init__( self.field_coverage = field_coverage self.application_depth = application_depth self.surface_remainder_fraction = surface_remainder_fraction + self.is_daily_spread = is_daily_spread def __eq__(self, other) -> bool: """Overrides the equality operator for ManureEvent objects.""" @@ -267,6 +271,7 @@ def __eq__(self, other) -> bool: and other.application_depth == self.application_depth and other.surface_remainder_fraction == self.surface_remainder_fraction and other.manure_supplement_method == self.manure_supplement_method + and other.is_daily_spread == self.is_daily_spread ) return False @@ -282,6 +287,7 @@ def __hash__(self) -> int: self.field_coverage, self.application_depth, self.surface_remainder_fraction, + self.is_daily_spread, ) ) diff --git a/RUFAS/data_structures/manure_to_crop_soil_connection.py b/RUFAS/data_structures/manure_to_crop_soil_connection.py index d82fd76c91..8a9c8d3c79 100644 --- a/RUFAS/data_structures/manure_to_crop_soil_connection.py +++ b/RUFAS/data_structures/manure_to_crop_soil_connection.py @@ -21,6 +21,9 @@ class NutrientRequest: use_supplemental_manure: bool """Whether to use supplemental manure if the request cannot be fulfilled by on-farm manure.""" + use_daily_spread_source: bool = False + """Whether this request should first use DailySpread processors.""" + def __post_init__(self) -> None: """ Validate the dataclass fields. diff --git a/input/data/manure_schedule/example_manure_schedule_file.json b/input/data/manure_schedule/example_manure_schedule_file.json new file mode 100644 index 0000000000..ab7a95aa39 --- /dev/null +++ b/input/data/manure_schedule/example_manure_schedule_file.json @@ -0,0 +1,32 @@ +{ + "years": [], + "days": [], + "nitrogen_masses": [], + "phosphorus_masses": [], + "potassium_masses": [], + "coverage_fractions": [], + "application_depths": [], + "surface_remainder_fractions": [], + "manure_types": [], + "supplement_manure_nutrient_deficiencies": [], + "pattern_repeat": 0, + "pattern_skip": 0, + + "daily_spread": { + "is_daily_spreading": true, + + "nitrogen_spread_amount": 8.0, + "phosphorus_spread_amount": 2.5, + "potassium_spread_amount": 3.0, + + "max_nitrogen": 8.0, + "max_phosphorus": 2.5, + "max_potassium": 3.0, + + "coverage_fraction": 1.0, + "application_depth": 0.0, + "surface_remainder_fraction": 1.0, + "manure_type": "solid", + "supplement_manure_nutrient_deficiencies": "none" + } +} diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index 0443e0ed0f..93bfcc8254 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -2591,6 +2591,73 @@ "pattern": "^(manure|synthetic fertilizer|none|synthetic fertilizer and manure)$" } }, + "daily_spread": { + "type": "object", + "description": "Configuration for daily spread manure application sourced from DailySpread processors.", + "properties": { + "is_daily_spreading": { + "type": "bool", + "description": "Whether this field uses daily spread manure applications." + }, + "nitrogen_spread_amount": { + "type": "number", + "description": "Target nitrogen amount for this field's daily spread request. Units: kg.", + "minimum": 0.0 + }, + "phosphorus_spread_amount": { + "type": "number", + "description": "Target phosphorus amount for this field's daily spread request. Units: kg.", + "minimum": 0.0 + }, + "potassium_spread_amount": { + "type": "number", + "description": "Target potassium amount for this field's daily spread request. Units: kg.", + "minimum": 0.0 + }, + "max_nitrogen": { + "type": "number", + "description": "Cap on nitrogen this field can receive per day from daily spread manure. Units: kg.", + "minimum": 0.0 + }, + "max_phosphorus": { + "type": "number", + "description": "Cap on phosphorus this field can receive per day from daily spread manure. Units: kg.", + "minimum": 0.0 + }, + "max_potassium": { + "type": "number", + "description": "Cap on potassium this field can receive per day from daily spread manure. Units: kg.", + "minimum": 0.0 + }, + "coverage_fraction": { + "type": "number", + "description": "Fraction of field area covered by each daily spread manure application.", + "minimum": 0.01, + "maximum": 1.0 + }, + "application_depth": { + "type": "number", + "description": "Depth of daily spread manure application. Units: mm.", + "minimum": 0.0 + }, + "surface_remainder_fraction": { + "type": "number", + "description": "Fraction of daily spread manure remaining on soil surface.", + "minimum": 0.0, + "maximum": 1.0 + }, + "manure_type": { + "type": "string", + "description": "Type of manure requested for daily spread applications.", + "pattern": "^(liquid|solid)$" + }, + "supplement_manure_nutrient_deficiencies": { + "type": "string", + "description": "Whether daily spread nutrient shortfalls can be supplemented.", + "pattern": "^(manure|synthetic fertilizer|none|synthetic fertilizer and manure)$" + } + } + }, "pattern_repeat": { "type": "number", "description": "Number of times that this manure application schedule should be repeated.", @@ -6228,5 +6295,3 @@ } } } - - diff --git a/tests/test_biophysical/test_crop_soil_field/field_tests/test_field.py b/tests/test_biophysical/test_crop_soil_field/field_tests/test_field.py index bcb71e8d58..e9a85afa4e 100644 --- a/tests/test_biophysical/test_crop_soil_field/field_tests/test_field.py +++ b/tests/test_biophysical/test_crop_soil_field/field_tests/test_field.py @@ -381,6 +381,77 @@ def test_check_manure_application_schedule_integration() -> None: assert manure_requests[0].nutrient_request.manure_type == ManureType.LIQUID +def test_create_daily_spread_event_uses_spread_amounts_and_caps() -> None: + """Daily spread event should request spread amounts while honoring max caps.""" + field = Field( + daily_spread_settings={ + "is_daily_spreading": True, + "manure_type": "solid", + "supplement_manure_nutrient_deficiencies": "none", + "nitrogen_spread_amount": 12.0, + "phosphorus_spread_amount": 4.0, + "potassium_spread_amount": 2.0, + "max_nitrogen": 10.0, + "max_phosphorus": 5.0, + "max_potassium": 3.0, + "coverage_fraction": 1.0, + "application_depth": 0.0, + "surface_remainder_fraction": 1.0, + } + ) + mocked_time = MagicMock(RufasTime) + mocked_time.current_calendar_year = 2025 + mocked_time.current_julian_day = 200 + + event = field._create_daily_spread_event(mocked_time) + + assert event is not None + assert event.nitrogen_mass == 10.0 + assert event.phosphorus_mass == 4.0 + assert event.manure_type == ManureType.SOLID + assert event.is_daily_spread is True + assert event.year == 2025 + assert event.day == 200 + + +def test_check_manure_application_schedule_daily_spread_request_uses_amounts() -> None: + """Daily spread request should use spread amounts and mark request source as daily spread.""" + field = Field( + manure_events=[], + daily_spread_settings={ + "is_daily_spreading": True, + "manure_type": "liquid", + "supplement_manure_nutrient_deficiencies": "none", + "nitrogen_spread_amount": 6.0, + "phosphorus_spread_amount": 3.0, + "potassium_spread_amount": 1.0, + "max_nitrogen": 8.0, + "max_phosphorus": 2.0, + "max_potassium": 2.0, + "coverage_fraction": 1.0, + "application_depth": 0.0, + "surface_remainder_fraction": 1.0, + }, + ) + field.field_data.name = "field_daily" + + mocked_time = MagicMock(RufasTime) + mocked_time.current_calendar_year = 2026 + mocked_time.current_julian_day = 10 + + manure_requests = field.check_manure_application_schedule(mocked_time) + + assert len(manure_requests) == 1 + request = manure_requests[0] + assert request.field_name == "field_daily" + assert request.event.is_daily_spread is True + assert request.event.nitrogen_mass == 6.0 + assert request.event.phosphorus_mass == 2.0 + assert request.nutrient_request.nitrogen == 6.0 + assert request.nutrient_request.phosphorus == 2.0 + assert request.nutrient_request.use_daily_spread_source is True + + @pytest.mark.parametrize( "nitrogen_mass, phosphorus_mass, manure_type, expected_request, expected_log", [ @@ -432,6 +503,7 @@ def test_create_manure_request( assert nutrient_request.nitrogen == expected_request.nitrogen assert nutrient_request.phosphorus == expected_request.phosphorus assert nutrient_request.manure_type == expected_request.manure_type + assert nutrient_request.use_daily_spread_source is False field.om.add_warning.assert_not_called() else: diff --git a/tests/test_biophysical/test_crop_soil_field/manager_tests/test_field_manager.py b/tests/test_biophysical/test_crop_soil_field/manager_tests/test_field_manager.py index cdf17380dd..6a43108767 100644 --- a/tests/test_biophysical/test_crop_soil_field/manager_tests/test_field_manager.py +++ b/tests/test_biophysical/test_crop_soil_field/manager_tests/test_field_manager.py @@ -823,8 +823,9 @@ def test_setup_manure_schedule( """Tests that ManureSchedules are correctly initialized with data from the InputManager.""" mock_input_manager.get_data = mock.MagicMock(return_value=manure_schedule_data) expected_manure_events = expected_manure_schedule.generate_manure_events() - actual_manure_events = FieldManager._setup_manure_events("test_manure_schedule") + actual_manure_events, actual_daily_spread_settings = FieldManager._setup_manure_events("test_manure_schedule") assert actual_manure_events == expected_manure_events + assert actual_daily_spread_settings is None mock_input_manager.get_data.assert_called_once_with("test_manure_schedule") mock_input_manager.get_data = input_manager_original_method_states["get_data"] @@ -1941,7 +1942,7 @@ def test_setup_field( ) mock_setup_manure_events = mocker.patch( "RUFAS.biophysical.field.manager.field_manager.FieldManager._setup_manure_events", - return_value=mock_manure_events + return_value=(mock_manure_events, {"is_daily_spreading": True}), ) mock_setup_tillage_events = mocker.patch( "RUFAS.biophysical.field.manager.field_manager.FieldManager._setup_tillage_events", @@ -1972,6 +1973,7 @@ def test_setup_field( "100_0_0": {"N": 1.0, "P": 0.0, "K": 0.0, "ammonium_fraction": 0.0}, "26_4_24": {"N": 0.26, "P": 0.04, "K": 0.24, "ammonium_fraction": 0.0}, } + assert new_field.daily_spread_settings == {"is_daily_spreading": True} mock_input_manager.get_data.assert_called_once_with(field_name) mock_setup_fertilizer_events.assert_called_once_with(field_config.get("fertilizer_management_specification")) diff --git a/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py b/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py index 1f78895cd1..1dee95eb2b 100644 --- a/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py +++ b/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py @@ -12,6 +12,7 @@ from RUFAS.biophysical.manure.processor import Processor from RUFAS.biophysical.manure.separator.separator import Separator from RUFAS.biophysical.manure.storage.composting import Composting +from RUFAS.biophysical.manure.storage.daily_spread import DailySpread from RUFAS.biophysical.manure.storage.storage import Storage from RUFAS.biophysical.manure.storage.storage_cover import StorageCover from RUFAS.current_day_conditions import CurrentDayConditions @@ -919,6 +920,7 @@ def test_request_nutrients( mock_nutrient_request.use_supplemental_manure = use_supplemental_manure mock_nutrient_request.manure_type = ManureType.LIQUID + mock_nutrient_request.use_daily_spread_source = False request_result = ( None @@ -975,11 +977,30 @@ def test_remove_nutrients_from_storage( mock_compute = mocker.patch.object( ManureManager, "_compute_stream_after_removal", return_value=(MagicMock(ManureStream), {"nitrogen": 50}) ) + mock_split = mocker.patch.object(manure_manager, "_split_storages_by_daily_spread") composting = MagicMock(Composting) - composting.stored_manure = MagicMock(ManureStream) - manure_manager.all_processors = {"non_storage": MagicMock(Digester), "storage": composting} + composting.stored_manure = ManureStream( + water=100.0, + ammoniacal_nitrogen=10.0, + nitrogen=100.0, + phosphorus=200.0, + potassium=20.0, + ash=5.0, + non_degradable_volatile_solids=1.0, + degradable_volatile_solids=2.0, + total_solids=120.0, + volume=1.0, + methane_production_potential=0.24, + pen_manure_data=None, + ) + STORAGE_CLASS_TO_TYPE[type(composting)] = ManureType.LIQUID + mock_split.return_value = ([], [composting]) - manure_manager._remove_nutrients_from_storage(NutrientRequestResults(nitrogen=10, phosphorus=20), ManureType.LIQUID) + manure_manager._remove_nutrients_from_storage( + NutrientRequestResults(nitrogen=10, phosphorus=20), + ManureType.LIQUID, + include_daily_spread=False, + ) mock_determine_limiting_nutrient.assert_called_once() if is_nitrogen_limiting_nutrient: @@ -987,7 +1008,45 @@ def test_remove_nutrients_from_storage( else: mock_proportion.assert_called_once_with(20, 200) mock_compute.assert_called_once() - mock_remove.assert_called_once_with({"nitrogen": 50, "manure_type": None}) + mock_remove.assert_called_once_with({"nitrogen": 50, "manure_type": ManureType.LIQUID}) + + +def test_request_nutrients_daily_spread_with_supplement(mocker: MockerFixture) -> None: + """Daily spread requests should pull DailySpread first, then use shared supplemental shortfall logic.""" + mocker.patch("RUFAS.biophysical.manure.manure_manager.ManureManager.__init__", return_value=None) + manure_manager = ManureManager() + manure_manager._manure_nutrient_manager = ManureNutrientManager() + manure_manager._om = OutputManager() + + request = NutrientRequest( + nitrogen=10.0, + phosphorus=5.0, + manure_type=ManureType.SOLID, + use_supplemental_manure=True, + use_daily_spread_source=True, + ) + daily_result = NutrientRequestResults(nitrogen=3.0, phosphorus=2.0, total_manure_mass=10.0) + off_farm_result = NutrientRequestResults(nitrogen=7.0, phosphorus=3.0, total_manure_mass=20.0) + + mock_split = mocker.patch.object(manure_manager, "_split_storages_by_daily_spread", return_value=([MagicMock(DailySpread)], [])) + mock_daily_request = mocker.patch.object( + manure_manager, "_handle_nutrient_request_for_storages", return_value=(daily_result, False) + ) + mock_remove = mocker.patch.object(manure_manager, "_remove_nutrients_from_storage") + mock_record = mocker.patch.object(manure_manager, "_record_manure_request_results") + mocker.patch.object(manure_manager, "_calculate_supplemental_manure_needed", return_value="daily_shortfall") + mock_off_farm = mocker.patch.object(FieldManureSupplier, "request_nutrients", return_value=off_farm_result) + + actual = manure_manager.request_nutrients(request, True, MagicMock(spec=RufasTime)) + + assert actual == daily_result + off_farm_result + mock_split.assert_called_once_with() + mock_daily_request.assert_called_once() + assert mock_remove.call_args_list == [ + call(daily_result, ManureType.SOLID, include_daily_spread=True, update_nutrient_manager_pool=False), + ] + mock_record.assert_called_once() + mock_off_farm.assert_called_once_with("daily_shortfall") @pytest.mark.parametrize( diff --git a/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py b/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py index 6765e6dca8..156a82624f 100644 --- a/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py +++ b/tests/test_biophysical/test_manure/test_storage/test_daily_spread.py @@ -66,12 +66,15 @@ def test_process_manure( mock_time.simulation_day = 50 daily_spread_instance._received_manure = received_manure mock_report = mocker.patch.object(Processor, "_report_manure_stream") - mock_process = mocker.patch.object(Storage, "process_manure") + available_stream = received_manure + mock_process = mocker.patch.object(Storage, "process_manure", return_value={"manure": available_stream}) daily_spread_instance.process_manure(mock_conditions, mock_time) - mock_report.assert_called_once_with(received_manure, "received", 50) + mock_report.assert_any_call(received_manure, "received", 50) + mock_report.assert_any_call(available_stream, "available_for_application", 50) mock_process.assert_called_once_with(mock_conditions, mock_time) + assert daily_spread_instance.available_for_field_application == available_stream def test_receive_manure(received_manure: ManureStream, daily_spread_instance: DailySpread) -> None: diff --git a/tests/test_simulation_engine.py b/tests/test_simulation_engine.py index 5f83f1d4bd..cae298cdfa 100644 --- a/tests/test_simulation_engine.py +++ b/tests/test_simulation_engine.py @@ -233,6 +233,9 @@ def test_daily_simulation( ) mock_manure_daily_update = mocker.patch.object(simulation_engine.manure_manager, "run_daily_update") + mock_finalize_daily_spread_exports = mocker.patch.object( + simulation_engine.manure_manager, "finalize_daily_spread_exports" + ) mock_om_add_warning = mocker.patch("RUFAS.output_manager.OutputManager.add_warning") mock_record_time = mocker.patch.object(mock_time, "record_time") @@ -295,6 +298,7 @@ def test_daily_simulation( simulation_engine.feed_manager.available_feeds, mock_time, mock_weather, mock_total_inventory ) mock_manure_daily_update.assert_called_once_with(mock_manure_streams, mock_time, mock_current_day_conditions) + mock_finalize_daily_spread_exports.assert_called_once_with(mock_time) mock_record_time.assert_called_once_with() mock_record_weather.assert_called_once_with(mock_time) mock_advance_time.assert_called_once_with() From 5ea8d3ad129f68edd64a02b76679aa1f13e452ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 14 Apr 2026 12:14:38 +0000 Subject: [PATCH 07/21] Apply Black Formatting From 5ea461a9e90e9d8d386818f0577c4ac660d5ae51 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Tue, 14 Apr 2026 12:17:02 +0000 Subject: [PATCH 08/21] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a5a6a7c0fa..eded346739 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) [![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) [![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Mypy](https://img.shields.io/badge/Mypy-1685%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1193%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From bf9d0bae348617401d346a4d9459aa4575d5c58b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Apr 2026 14:08:59 +0000 Subject: [PATCH 09/21] Apply Black Formatting From 3abff9fbffe45a2a0fc814cd814eb786e408585f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Apr 2026 14:44:59 +0000 Subject: [PATCH 10/21] Apply Black Formatting --- RUFAS/biophysical/manure/manure_manager.py | 8 ++++---- .../test_manure_manager/test_manure_manager.py | 4 +++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/RUFAS/biophysical/manure/manure_manager.py b/RUFAS/biophysical/manure/manure_manager.py index 8a5e1745ce..ca70027c56 100644 --- a/RUFAS/biophysical/manure/manure_manager.py +++ b/RUFAS/biophysical/manure/manure_manager.py @@ -873,9 +873,7 @@ def request_nutrients( "Attempting to fulfill manure nutrient request shortfall with supplemental manure.", {"class": self.__class__.__name__, "function": self.request_nutrients.__name__}, ) - amount_supplemental_manure_needed = self._calculate_supplemental_manure_needed( - request_result, request - ) + amount_supplemental_manure_needed = self._calculate_supplemental_manure_needed(request_result, request) supplemental_manure = FieldManureSupplier.request_nutrients(amount_supplemental_manure_needed) self._record_manure_request_results(supplemental_manure, "off_farm_manure", time) if request_result is None: @@ -962,7 +960,9 @@ def _remove_nutrients_from_storage( if storage_manure_type != manure_type: continue source_stream = ( - processor.available_for_field_application if isinstance(processor, DailySpread) else processor.stored_manure + processor.available_for_field_application + if isinstance(processor, DailySpread) + else processor.stored_manure ) updated_stream, removal_details = self._compute_stream_after_removal( stored_manure=source_stream, diff --git a/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py b/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py index 1dee95eb2b..dca6e831e2 100644 --- a/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py +++ b/tests/test_biophysical/test_manure/test_manure_manager/test_manure_manager.py @@ -1028,7 +1028,9 @@ def test_request_nutrients_daily_spread_with_supplement(mocker: MockerFixture) - daily_result = NutrientRequestResults(nitrogen=3.0, phosphorus=2.0, total_manure_mass=10.0) off_farm_result = NutrientRequestResults(nitrogen=7.0, phosphorus=3.0, total_manure_mass=20.0) - mock_split = mocker.patch.object(manure_manager, "_split_storages_by_daily_spread", return_value=([MagicMock(DailySpread)], [])) + mock_split = mocker.patch.object( + manure_manager, "_split_storages_by_daily_spread", return_value=([MagicMock(DailySpread)], []) + ) mock_daily_request = mocker.patch.object( manure_manager, "_handle_nutrient_request_for_storages", return_value=(daily_result, False) ) From c02174ad002f423df23c757d3a592c6484e1a979 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Wed, 15 Apr 2026 14:47:24 +0000 Subject: [PATCH 11/21] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index eded346739..a477ec13bb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) [![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) [![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Mypy](https://img.shields.io/badge/Mypy-1193%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1205%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From 8b04bc05ab55b95ac11ad5d82071d24aba5d3bdd Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Wed, 15 Apr 2026 23:55:30 +0900 Subject: [PATCH 12/21] Input changes --- RUFAS/biophysical/field/field/field.py | 2 +- input/metadata/properties/default.json | 1389 +++++++++++++++++------- 2 files changed, 1001 insertions(+), 390 deletions(-) diff --git a/RUFAS/biophysical/field/field/field.py b/RUFAS/biophysical/field/field/field.py index 0fc4c06dc2..28c884a0a6 100644 --- a/RUFAS/biophysical/field/field/field.py +++ b/RUFAS/biophysical/field/field/field.py @@ -1,6 +1,6 @@ import math from math import exp -from typing import Dict, List, Optional, Sequence, TypeVar +from typing import Dict, List, Optional, Sequence, TypeVar, Any from RUFAS.current_day_conditions import CurrentDayConditions from RUFAS.data_structures.crop_soil_to_feed_storage_connection import HarvestedCrop diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index 40cc053b53..6427c1afd6 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -72,7 +72,7 @@ "default": 100, "minimum": 6 }, - "replace_num": { + "replace_num": { "type": "number", "description": "Replacements (head) -- Number of replacement animals available in the replacement market", "default": 5000, @@ -565,7 +565,7 @@ "description": "Defines probabilities and distributions for death and six health-related reasons (feet-and-leg, injury, mastitis, disease, udder, unknown) an animal might be removed (culled) from the herd.", "cull_day_count": { "type": "array", - "description": "Defines breakpoints that partition the cumulative distribution function (CDF) for culling probabilities into segments. These values correspond to the 'cull_day_prob' array, allowing for more accurate definition of the CDF.The numbers in the array represent days into the lactation (days in milk).", + "description": "Defines breakpoints that partition the cumulative distribution function (CDF) for culling probabilities into segments. These values correspond to the 'cull_day_prob' array, allowing for more accurate definition of the CDF.The numbers in the array represent days into the lactation (days in milk).", "properties": { "type": "number", "minimum": 0 @@ -845,6 +845,12 @@ "type": "number", "minimum": 0, "default": 75 + }, + "maximum_ration_reformulation_attempts": { + "description": "The maximum number of attempts the ration formulation logic will make before stopping. Meant only to catch edge cases that could cause infinite loops. In normal operation, this value should never be reached.", + "type": "number", + "minimum": 1, + "default": 250 } }, "pen_information": { @@ -1650,23 +1656,41 @@ "1": { "type": "object", "description": "Wood lactation curve adjustments for parity 1 cows.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2": { "type": "object", "description": "Wood lactation curve adjustments for parity 2 cows.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "3": { "type": "object", "description": "Wood lactation curve adjustments for parity 3+ cows.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } } }, "year": { @@ -1675,79 +1699,145 @@ "2006": { "type": "object", "description": "Wood lactation curve adjustments for 2006.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2007": { "type": "object", "description": "Wood lactation curve adjustments for 2007.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2008": { "type": "object", "description": "Wood lactation curve adjustments for 2008.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2009": { "type": "object", "description": "Wood lactation curve adjustments for 2009.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2010": { "type": "object", "description": "Wood lactation curve adjustments for 2010.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2011": { "type": "object", "description": "Wood lactation curve adjustments for 2011.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2012": { "type": "object", "description": "Wood lactation curve adjustments for 2012.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2013": { "type": "object", "description": "Wood lactation curve adjustments for 2013.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2014": { "type": "object", "description": "Wood lactation curve adjustments for 2014.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2015": { "type": "object", "description": "Wood lactation curve adjustments for 2015.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2016": { "type": "object", "description": "Wood lactation curve adjustments for 2016.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } } }, "month": { @@ -1756,86 +1846,158 @@ "1": { "type": "object", "description": "Wood lactation curve adjustments for January.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "2": { "type": "object", "description": "Wood lactation curve adjustments for February.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "3": { "type": "object", "description": "Wood lactation curve adjustments for March.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "4": { "type": "object", "description": "Wood lactation curve adjustments for April.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "5": { "type": "object", "description": "Wood lactation curve adjustments for May.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "6": { "type": "object", "description": "Wood lactation curve adjustments for June.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "7": { "type": "object", "description": "Wood lactation curve adjustments for July.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "8": { "type": "object", "description": "Wood lactation curve adjustments for August.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "9": { "type": "object", "description": "Wood lactation curve adjustments for September.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "10": { "type": "object", "description": "Wood lactation curve adjustments for October.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "11": { "type": "object", "description": "Wood lactation curve adjustments for November.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "12": { "type": "object", "description": "Wood lactation curve adjustments for December.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } } }, "region": { @@ -1844,100 +2006,184 @@ "Appalachian": { "type": "object", "description": "Wood lactation curve adjustments for the Appalachian region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Corn Belt": { "type": "object", "description": "Wood lactation curve adjustments for the Corn Belt region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Delta": { "type": "object", "description": "Wood lactation curve adjustments for the Delta region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Lake": { "type": "object", "description": "Wood lactation curve adjustments for the Mountain region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Mountain": { "type": "object", "description": "Wood lactation curve adjustments for the Mountain region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Northeast": { "type": "object", "description": "Wood lactation curve adjustments for the Northeast region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Northern Plains": { "type": "object", "description": "Wood lactation curve adjustments for the Northern Plains region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "New York": { "type": "object", "description": "Wood lactation curve adjustments for New York.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Pennsylvania": { "type": "object", "description": "Wood lactation curve adjustments for Pennsylvania.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Southeast": { "type": "object", "description": "Wood lactation curve adjustments for the Southeast region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Southern Plains": { "type": "object", "description": "Wood lactation curve adjustments for the Southern Plains region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "West Coast": { "type": "object", "description": "Wood lactation curve adjustments for the West Coast region.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "Wisconsin": { "type": "object", "description": "Wood lactation curve adjustments for Wisconsin.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "None": { "type": "object", "description": "Wood lactation curve adjustments when simulating a farm that is not in one defined regions.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } } }, "milking_frequency": { @@ -1946,78 +2192,202 @@ "twice_daily": { "type": "object", "description": "Wood lactation curve adjustments for cows that are milked twice daily.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } }, "thrice_daily": { "type": "object", "description": "Wood lactation curve adjustments for cows that are milked thrice daily.", - "l": {"type": "number"}, - "m": {"type": "number"}, - "n": {"type": "number"} + "l": { + "type": "number" + }, + "m": { + "type": "number" + }, + "n": { + "type": "number" + } } } }, "state_to_region_mapping": { "type": "object", - "description" : "Maps the first two digits of FIPS codes (i.e. the state) to the corresponding geographic region as defined by Li et al, 2022. Reference: Li, M., et al. \"Investigating the effect of temporal, geographic, and management factors on US Holstein lactation curve parameters.\" Journal of Dairy Science 105.9 (2022): 7525-7538.", - "1": {"type": "string"}, - "2": {"type": "string"}, - "3": {"type": "string"}, - "4": {"type": "string"}, - "5": {"type": "string"}, - "6": {"type": "string"}, - "7": {"type": "string"}, - "8": {"type": "string"}, - "9": {"type": "string"}, - "10": {"type": "string"}, - "11": {"type": "string"}, - "12": {"type": "string"}, - "13": {"type": "string"}, - "14": {"type": "string"}, - "15": {"type": "string"}, - "16": {"type": "string"}, - "17": {"type": "string"}, - "18": {"type": "string"}, - "19": {"type": "string"}, - "20": {"type": "string"}, - "21": {"type": "string"}, - "22": {"type": "string"}, - "23": {"type": "string"}, - "24": {"type": "string"}, - "25": {"type": "string"}, - "26": {"type": "string"}, - "27": {"type": "string"}, - "28": {"type": "string"}, - "29": {"type": "string"}, - "30": {"type": "string"}, - "31": {"type": "string"}, - "32": {"type": "string"}, - "33": {"type": "string"}, - "34": {"type": "string"}, - "35": {"type": "string"}, - "36": {"type": "string"}, - "37": {"type": "string"}, - "38": {"type": "string"}, - "39": {"type": "string"}, - "40": {"type": "string"}, - "41": {"type": "string"}, - "42": {"type": "string"}, - "43": {"type": "string"}, - "44": {"type": "string"}, - "45": {"type": "string"}, - "46": {"type": "string"}, - "47": {"type": "string"}, - "48": {"type": "string"}, - "49": {"type": "string"}, - "50": {"type": "string"}, - "51": {"type": "string"}, - "52": {"type": "string"}, - "53": {"type": "string"}, - "54": {"type": "string"}, - "55": {"type": "string"}, - "56": {"type": "string"} + "description": "Maps the first two digits of FIPS codes (i.e. the state) to the corresponding geographic region as defined by Li et al, 2022. Reference: Li, M., et al. \"Investigating the effect of temporal, geographic, and management factors on US Holstein lactation curve parameters.\" Journal of Dairy Science 105.9 (2022): 7525-7538.", + "1": { + "type": "string" + }, + "2": { + "type": "string" + }, + "3": { + "type": "string" + }, + "4": { + "type": "string" + }, + "5": { + "type": "string" + }, + "6": { + "type": "string" + }, + "7": { + "type": "string" + }, + "8": { + "type": "string" + }, + "9": { + "type": "string" + }, + "10": { + "type": "string" + }, + "11": { + "type": "string" + }, + "12": { + "type": "string" + }, + "13": { + "type": "string" + }, + "14": { + "type": "string" + }, + "15": { + "type": "string" + }, + "16": { + "type": "string" + }, + "17": { + "type": "string" + }, + "18": { + "type": "string" + }, + "19": { + "type": "string" + }, + "20": { + "type": "string" + }, + "21": { + "type": "string" + }, + "22": { + "type": "string" + }, + "23": { + "type": "string" + }, + "24": { + "type": "string" + }, + "25": { + "type": "string" + }, + "26": { + "type": "string" + }, + "27": { + "type": "string" + }, + "28": { + "type": "string" + }, + "29": { + "type": "string" + }, + "30": { + "type": "string" + }, + "31": { + "type": "string" + }, + "32": { + "type": "string" + }, + "33": { + "type": "string" + }, + "34": { + "type": "string" + }, + "35": { + "type": "string" + }, + "36": { + "type": "string" + }, + "37": { + "type": "string" + }, + "38": { + "type": "string" + }, + "39": { + "type": "string" + }, + "40": { + "type": "string" + }, + "41": { + "type": "string" + }, + "42": { + "type": "string" + }, + "43": { + "type": "string" + }, + "44": { + "type": "string" + }, + "45": { + "type": "string" + }, + "46": { + "type": "string" + }, + "47": { + "type": "string" + }, + "48": { + "type": "string" + }, + "49": { + "type": "string" + }, + "50": { + "type": "string" + }, + "51": { + "type": "string" + }, + "52": { + "type": "string" + }, + "53": { + "type": "string" + }, + "54": { + "type": "string" + }, + "55": { + "type": "string" + }, + "56": { + "type": "string" + } }, "parity_milk_yield_adjustments": { "type": "object", @@ -2026,7 +2396,7 @@ "type": "number", "description": "The parity 2 milk adjustment factor for the 305 day milk yield." }, - "parity_3_305_day_milk_yield_adjustment": { + "parity_3_305_day_milk_yield_adjustment": { "type": "number", "description": "The parity 3 milk adjustment factor for the 305 day milk yield." } @@ -2038,7 +2408,7 @@ "type": "number", "description": "The mean value of Wood's parameter l." }, - "parameter_m_mean": { + "parameter_m_mean": { "type": "number", "description": "The mean value of Wood's parameter m." }, @@ -2795,7 +3165,7 @@ "corn_grain_tillage_emissions": { "type": "array", "description": "The amount of total tonnes CO2-eq generated from corn grain tillage", - "properties": { + "properties": { "type": "number", "minimum": 0, "default": 300 @@ -2824,1085 +3194,1085 @@ "type": "array", "properties": { "type": "number", - "minimum": 0.0 - } - }, + "minimum": 0.0 + } + }, "2": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "4": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "5": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "6": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "7": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "8": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "9": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "11": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "12": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "13": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "14": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "15": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "16": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "17": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "18": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "19": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "20": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "21": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "22": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "23": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "24": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "25": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "28": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "30": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "31": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "32": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "33": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "34": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "36": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "37": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "38": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "39": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "40": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "41": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "42": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "43": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "44": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "46": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "47": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "49": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "50": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "51": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "52": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "53": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "54": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "55": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "56": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "58": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "59": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "60": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "61": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "62": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "63": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "64": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "65": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "66": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "67": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "72": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "76": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "77": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "78": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "79": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "80": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "81": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "82": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "83": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "87": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "88": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "89": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "90": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "91": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "92": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "93": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "94": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "95": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "96": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "97": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "98": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "99": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "100": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "102": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "103": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "104": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "105": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "106": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "107": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "108": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "109": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "110": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "111": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "112": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "114": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "115": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "118": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "119": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "120": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "121": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "123": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "124": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "125": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "129": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "135": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "144": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "153": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "163": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "164": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "165": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "166": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "167": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "169": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "170": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "171": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "172": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "181": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "182": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "183": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "184": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "187": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "193": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "194": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "195": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "197": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "198": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "200": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "201": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "202": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "209": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "210": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "211": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "229": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "230": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "231": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "232": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "233": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "234": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "235": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "236": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "237": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "238": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "239": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "240": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "241": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "242": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "243": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "244": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "245": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "246": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "247": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "248": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "251": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "302": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "303": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "304": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } }, "305": { "type": "array", "properties": { "type": "number", - "minimum": 0.0 + "minimum": 0.0 } } }, @@ -3959,7 +4329,8 @@ "type": "number", "description": "Fractional number 0 to 1 as a cushion for feed purchases to help plan for herd fluctuations and feed shrink.", "minimum": 0, - "maximum": 1 + "maximum": 1, + "default": 0.15 } } }, @@ -4057,11 +4428,11 @@ } }, "max_daily_feed_recalculations_per_year": { - "type": "number", - "description": "The maximum number of feed recalculations that can be performed per year.", - "minimum": 1, - "maximum": 365, - "default": 4 + "type": "number", + "description": "The maximum number of feed recalculations that can be performed per year.", + "minimum": 1, + "maximum": 365, + "default": 4 }, "allowances": { "type": "array", @@ -4143,11 +4514,11 @@ } }, "feed_type": { - "type":"array", + "type": "array", "description": "The general type or category of the feed.", "properties": { "type": "string", - "pattern": "^(Aminoacids|Forage|Conc|Milk|Mineral|Vitamins|Starter|No)$" + "pattern": "^(Aminoacids|Forage|Conc|Milk|Mineral|Vitamins|Starter|No|Fat)$" } }, "Fd_Category": { @@ -4155,7 +4526,7 @@ "description": "Feed Category.", "properties": { "type": "string", - "pattern": "^(Animal Protein|By-Product/Other|Calf Liquid Feed|Energy Source|Fat Supplement|Fatty Acid Supplement|Grain Crop Forage|Grass/Legume Forage|Pasture|Plant Protein|Vitamin/Mineral)$" + "pattern": "^(Animal Protein|By-Product/Other|Calf Liquid Feed|Energy Source|Fat Supplement|Fatty Acid Supplement|Grain Crop Forage|Grass/Legume Forage|Pasture|Plant Protein|Vitamin/Mineral|NPN Supplement)$" } }, "DM": { @@ -4173,7 +4544,7 @@ "properties": { "type": "number", "minimum": 0, - "maximum": 288 + "maximum": 288 } }, "NDICP": { @@ -4275,7 +4646,6 @@ "maximum": 184, "default": 0 } - }, "N_A": { "type": "array", @@ -4426,7 +4796,7 @@ "description": "", "properties": { "type": "string" - } + } }, "starch": { "type": "array", @@ -4475,15 +4845,15 @@ "description": "Feed Category.", "properties": { "type": "string", - "pattern": "^(Animal Protein|By-Product/Other|Calf Liquid Feed|Energy Source|Fat Supplement|Fatty Acid Supplement|Grain Crop Forage|Grass/Legume Forage|Pasture|Plant Protein|Vitamin/Mineral)$" + "pattern": "^(Animal Protein|By-Product/Other|Calf Liquid Feed|Energy Source|Fat Supplement|Fatty Acid Supplement|Grain Crop Forage|Grass/Legume Forage|Pasture|Plant Protein|Vitamin/Mineral|NPN Supplement)$" } }, "feed_type": { "type": "array", "description": "The general type or category of the feed.", - "properties":{ + "properties": { "type": "string", - "pattern": "^(Aminoacids|Forage|Conc|Milk|Mineral|Vitamins)$" + "pattern": "^(Aminoacids|Forage|Conc|Milk|Mineral|Vitamins|Fat)$" } }, "DM": { @@ -4511,7 +4881,7 @@ "properties": { "type": "number", "minimum": 0, - "maximum": 288 + "maximum": 288 } }, "N_A": { @@ -5133,7 +5503,7 @@ "description": "", "properties": { "type": "string" - } + } }, "limit": { "type": "array", @@ -5164,7 +5534,7 @@ } } }, - "manure_management_properties" : { + "manure_management_properties": { "data_collection_app_compatible": false, "anaerobic_digester": { "type": "array", @@ -5755,7 +6125,7 @@ }, "precip": { "description": "Amount of precipitation (mm H2O).", - "type":"array", + "type": "array", "properties": { "type": "number", "minimum": 0, @@ -5763,7 +6133,7 @@ } }, "high": { - "description": "The maximum air temperature of the day (°C).", + "description": "The maximum air temperature of the day (\u00b0C).", "type": "array", "properties": { "type": "number", @@ -5771,7 +6141,7 @@ } }, "low": { - "description": "The minimum air temperature of the day (°C).", + "description": "The minimum air temperature of the day (\u00b0C).", "type": "array", "properties": { "type": "number", @@ -5779,7 +6149,7 @@ } }, "avg": { - "description": "The average air temperature of the day (°C).", + "description": "The average air temperature of the day (\u00b0C).", "type": "array", "properties": { "type": "number", @@ -5831,7 +6201,7 @@ "default": "medium" } }, - "Operation": { + "Operation": { "type": "array", "properties": { "type": "string" @@ -5839,7 +6209,7 @@ }, "Depth": { "type": "array", - "description": "How deep in the soil the implement goes", + "description": "How deep in the soil the implement goes, refer to the value generated by Crop and Soil module if application is tilling, manure application, or fertalizer application", "properties": { "type": "number", "minimum": 0, @@ -6066,9 +6436,13 @@ "type": "string", "description": "Name of crop to be stored in the feed storage unit." }, - "field_name": { - "type": "string", - "description": "Name of the field associated with the crop to be stored in feed storage unit." + "field_names": { + "type": "array", + "description": "Name of the all fields associated with the crop to be stored in feed storage unit.", + "properties": { + "type": "string", + "description": "Name of the field associated with the crop to be stored in feed storage unit." + } }, "storage_type": { "type": "string", @@ -6082,7 +6456,7 @@ "maximum": 0.1, "default": 0.1 }, - "initial_storage_dry_matter" :{ + "initial_storage_dry_matter": { "type": "number", "description": "Initial dry matter content of grain at time of storage (unitless).", "minimum": 0.0, @@ -6098,7 +6472,7 @@ "type": "number", "description": "Maximum capacity of the storage (kg dry matter).", "minimum": 0, - "default": 1e10 + "default": 10000000000.0 } } }, @@ -6116,9 +6490,13 @@ "type": "string", "description": "Name of crop to be stored in the feed storage unit." }, - "field_name": { - "type": "string", - "description": "Name of the field associated with the crop to be stored in feed storage unit." + "field_names": { + "type": "array", + "description": "Name of the all fields associated with the crop to be stored in feed storage unit.", + "properties": { + "type": "string", + "description": "Name of the field associated with the crop to be stored in feed storage unit." + } }, "storage_type": { "type": "string", @@ -6139,7 +6517,7 @@ "maximum": 0.1, "default": 0.0 }, - "initial_storage_dry_matter" :{ + "initial_storage_dry_matter": { "type": "number", "description": "Initial dry matter content of hay at time of storage (unitless).", "minimum": 0.0, @@ -6162,7 +6540,7 @@ "type": "number", "description": "Maximum capacity of the storage (kg dry matter).", "minimum": 0, - "default": 1e10 + "default": 10000000000.0 } } }, @@ -6180,9 +6558,13 @@ "type": "string", "description": "Name of crop to be stored in the feed storage unit." }, - "field_name": { - "type": "string", - "description": "Name of the field associated with the crop to be stored in feed storage unit." + "field_names": { + "type": "array", + "description": "Name of the all fields associated with the crop to be stored in feed storage unit.", + "properties": { + "type": "string", + "description": "Name of the field associated with the crop to be stored in feed storage unit." + } }, "storage_type": { "type": "string", @@ -6219,7 +6601,7 @@ "type": "number", "description": "Maximum capacity of the storage (kg dry matter).", "minimum": 0, - "default": 1e10 + "default": 10000000000.0 } } }, @@ -6237,9 +6619,13 @@ "type": "string", "description": "Name of crop to be stored in the feed storage unit." }, - "field_name": { - "type": "string", - "description": "Name of the field associated with the crop to be stored in feed storage unit." + "field_names": { + "type": "array", + "description": "Name of the all fields associated with the crop to be stored in feed storage unit.", + "properties": { + "type": "string", + "description": "Name of the field associated with the crop to be stored in feed storage unit." + } }, "storage_type": { "type": "string", @@ -6268,7 +6654,7 @@ "type": "number", "description": "Maximum capacity of the storage (kg dry matter).", "minimum": 0, - "default": 1e10 + "default": 10000000000.0 } } } @@ -6281,7 +6667,232 @@ "properties": { "type": "string", "description": "The reference to the name of the storage from the `feed_storage_configurations` input." - } } + } + }, + "farm_services_labor_hours_dollar_per_hour_csv_properties": { + "data_collection_app_compatible": false, + "fips": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1989": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1990": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1991": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1992": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1993": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1994": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1995": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1996": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1997": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1998": { + "type": "array", + "properties": { + "type": "number" + } + }, + "1999": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2000": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2001": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2002": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2003": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2004": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2005": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2006": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2007": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2008": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2009": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2010": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2011": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2012": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2013": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2014": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2015": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2016": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2017": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2018": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2019": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2020": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2021": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2022": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2023": { + "type": "array", + "properties": { + "type": "number" + } + }, + "2024": { + "type": "array", + "properties": { + "type": "number" + } + } } -} +} \ No newline at end of file From 80834919cb396d3b270bdda6ddbb5667234ca1a3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 15 Apr 2026 14:57:24 +0000 Subject: [PATCH 13/21] Apply Black Formatting From c8253868cdc2173f36140c62ccbfdf85dd0b9ea2 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Wed, 15 Apr 2026 15:01:23 +0000 Subject: [PATCH 14/21] Update badges on README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a477ec13bb..7f4f29a987 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) [![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) [![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Mypy](https://img.shields.io/badge/Mypy-1205%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1204%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems From d2175a0f6845118b2ee1cd9724c33b5e249cd646 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Thu, 16 Apr 2026 00:16:02 +0900 Subject: [PATCH 15/21] Updated change log --- input/data/tasks/example_freestall_task.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/input/data/tasks/example_freestall_task.json b/input/data/tasks/example_freestall_task.json index 5f703b52d8..c0ed219bca 100644 --- a/input/data/tasks/example_freestall_task.json +++ b/input/data/tasks/example_freestall_task.json @@ -9,7 +9,7 @@ "random_seed": 42, "exclude_info_maps": false, "cross_validation_file_paths": [ - "input/metadata/cross_validation/example_cross_validation.json" + "input/metadata/cross_validation/example_cross_validation.json", "input/metadata/cross_validation/weather_cross_validation.json" ] } From 1d143fe318d0dc56b9ff33316febf47c95139728 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Sat, 18 Apr 2026 18:14:16 +0900 Subject: [PATCH 16/21] Fixed cross validation failures --- .../example_sm_corn_alf_manure_schedule.json | 18 ++- input/metadata/properties/default.json | 136 ++++++++++-------- 2 files changed, 89 insertions(+), 65 deletions(-) diff --git a/input/data/manure_schedule/example_sm_corn_alf_manure_schedule.json b/input/data/manure_schedule/example_sm_corn_alf_manure_schedule.json index ac3aee742d..3b1d0a8e2b 100644 --- a/input/data/manure_schedule/example_sm_corn_alf_manure_schedule.json +++ b/input/data/manure_schedule/example_sm_corn_alf_manure_schedule.json @@ -55,5 +55,19 @@ ], "supplement_manure_nutrient_deficiencies": ["synthetic fertilizer and manure", "manure", "manure", "manure"], "pattern_repeat": 1, - "pattern_skip": 4 -} \ No newline at end of file + "pattern_skip": 4, + "daily_spread": { + "is_daily_spreading": false, + "nitrogen_spread_amount": 0.0, + "phosphorus_spread_amount": 0.0, + "potassium_spread_amount": 0.0, + "max_nitrogen": 0.0, + "max_phosphorus": 0.0, + "max_potassium": 0.0, + "coverage_fraction": 1.0, + "application_depth": 0.0, + "surface_remainder_fraction": 1.0, + "manure_type": "solid", + "supplement_manure_nutrient_deficiencies": "none" + } +} diff --git a/input/metadata/properties/default.json b/input/metadata/properties/default.json index 6427c1afd6..4ac21e4008 100644 --- a/input/metadata/properties/default.json +++ b/input/metadata/properties/default.json @@ -2965,68 +2965,78 @@ "daily_spread": { "type": "object", "description": "Configuration for daily spread manure application sourced from DailySpread processors.", - "properties": { - "is_daily_spreading": { - "type": "bool", - "description": "Whether this field uses daily spread manure applications." - }, - "nitrogen_spread_amount": { - "type": "number", - "description": "Target nitrogen amount for this field's daily spread request. Units: kg.", - "minimum": 0.0 - }, - "phosphorus_spread_amount": { - "type": "number", - "description": "Target phosphorus amount for this field's daily spread request. Units: kg.", - "minimum": 0.0 - }, - "potassium_spread_amount": { - "type": "number", - "description": "Target potassium amount for this field's daily spread request. Units: kg.", - "minimum": 0.0 - }, - "max_nitrogen": { - "type": "number", - "description": "Cap on nitrogen this field can receive per day from daily spread manure. Units: kg.", - "minimum": 0.0 - }, - "max_phosphorus": { - "type": "number", - "description": "Cap on phosphorus this field can receive per day from daily spread manure. Units: kg.", - "minimum": 0.0 - }, - "max_potassium": { - "type": "number", - "description": "Cap on potassium this field can receive per day from daily spread manure. Units: kg.", - "minimum": 0.0 - }, - "coverage_fraction": { - "type": "number", - "description": "Fraction of field area covered by each daily spread manure application.", - "minimum": 0.01, - "maximum": 1.0 - }, - "application_depth": { - "type": "number", - "description": "Depth of daily spread manure application. Units: mm.", - "minimum": 0.0 - }, - "surface_remainder_fraction": { - "type": "number", - "description": "Fraction of daily spread manure remaining on soil surface.", - "minimum": 0.0, - "maximum": 1.0 - }, - "manure_type": { - "type": "string", - "description": "Type of manure requested for daily spread applications.", - "pattern": "^(liquid|solid)$" - }, - "supplement_manure_nutrient_deficiencies": { - "type": "string", - "description": "Whether daily spread nutrient shortfalls can be supplemented.", - "pattern": "^(manure|synthetic fertilizer|none|synthetic fertilizer and manure)$" - } + "is_daily_spreading": { + "type": "bool", + "description": "Whether this field uses daily spread manure applications.", + "default": false + }, + "nitrogen_spread_amount": { + "type": "number", + "description": "Target nitrogen amount for this field's daily spread request. Units: kg.", + "minimum": 0.0, + "default": 0.0 + }, + "phosphorus_spread_amount": { + "type": "number", + "description": "Target phosphorus amount for this field's daily spread request. Units: kg.", + "minimum": 0.0, + "default": 0.0 + }, + "potassium_spread_amount": { + "type": "number", + "description": "Target potassium amount for this field's daily spread request. Units: kg.", + "minimum": 0.0, + "default": 0.0 + }, + "max_nitrogen": { + "type": "number", + "description": "Cap on nitrogen this field can receive per day from daily spread manure. Units: kg.", + "minimum": 0.0, + "default": 0.0 + }, + "max_phosphorus": { + "type": "number", + "description": "Cap on phosphorus this field can receive per day from daily spread manure. Units: kg.", + "minimum": 0.0, + "default": 0.0 + }, + "max_potassium": { + "type": "number", + "description": "Cap on potassium this field can receive per day from daily spread manure. Units: kg.", + "minimum": 0.0, + "default": 0.0 + }, + "coverage_fraction": { + "type": "number", + "description": "Fraction of field area covered by each daily spread manure application.", + "minimum": 0.01, + "maximum": 1.0, + "default": 1.0 + }, + "application_depth": { + "type": "number", + "description": "Depth of daily spread manure application. Units: mm.", + "minimum": 0.0, + "default": 0.0 + }, + "surface_remainder_fraction": { + "type": "number", + "description": "Fraction of daily spread manure remaining on soil surface.", + "minimum": 0.0, + "maximum": 1.0, + "default": 1.0 + }, + "manure_type": { + "type": "string", + "description": "Type of manure requested for daily spread applications.", + "pattern": "^(liquid|solid)$", + "default": "solid" + }, + "supplement_manure_nutrient_deficiencies": { + "type": "string", + "description": "Whether daily spread nutrient shortfalls can be supplemented.", + "pattern": "^(manure|synthetic fertilizer|none|synthetic fertilizer and manure)$", + "default": "none" } }, "pattern_repeat": { @@ -6895,4 +6905,4 @@ } } } -} \ No newline at end of file +} From 344024d7663363407f8dab179fa6dd93cd54fb9b Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 20 Apr 2026 13:27:41 +0900 Subject: [PATCH 17/21] Added variables for init --- RUFAS/biophysical/manure/manure_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RUFAS/biophysical/manure/manure_manager.py b/RUFAS/biophysical/manure/manure_manager.py index ca70027c56..96cc1e2cc1 100644 --- a/RUFAS/biophysical/manure/manure_manager.py +++ b/RUFAS/biophysical/manure/manure_manager.py @@ -59,7 +59,7 @@ class ManureManager: A list defining the execution order of processors. """ - def __init__(self) -> None: + def __init__(self, intercept_mean_temp: float, phase_shift: float, amplitude: float) -> None: self._om = OutputManager() self._manure_nutrient_manager = ManureNutrientManager() From 9b92a86c37e9f1de3ad900c155e9e8141693fb05 Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 20 Apr 2026 13:41:31 +0900 Subject: [PATCH 18/21] fix --- RUFAS/biophysical/manure/manure_manager.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/RUFAS/biophysical/manure/manure_manager.py b/RUFAS/biophysical/manure/manure_manager.py index 96cc1e2cc1..d4de4e4037 100644 --- a/RUFAS/biophysical/manure/manure_manager.py +++ b/RUFAS/biophysical/manure/manure_manager.py @@ -77,7 +77,8 @@ def __init__(self, intercept_mean_temp: float, phase_shift: float, amplitude: fl processor_connections_by_name = self._validate_and_parse_processor_connections( processor_connections_input, processor_configs_by_name ) - self._create_all_processors(processor_connections_by_name, processor_configs_by_name) + self._create_all_processors(processor_connections_by_name, processor_configs_by_name, + intercept_mean_temp, phase_shift, amplitude) self._populate_adjacency_matrix(processor_connections_by_name) self._validate_adjacency_matrix() @@ -676,6 +677,9 @@ def _create_all_processors( self, processor_connections_by_name: dict[str, dict[str, list[dict[str, Any]]]], processor_configs_by_name: dict[str, dict[str, Any]], + intercept_mean_temp: float, + phase_shift: float, + amplitude: float, ) -> None: """ Creates and initializes all processors based on their definitions. @@ -687,6 +691,12 @@ def _create_all_processors( processor_configs_by_name : dict[str, dict[str, Any]] A dictionary that contains processor definitions, where each key is the processor name and the value is a dictionary with the processor's parameters and type. + intercept_mean_temp : float + The intercept mean temperature calculate from linest function. + phase_shift : float + Temperature phase shift of the weather data. + amplitude : float + The temperature amplitude of the weather data. """ for processor_name in processor_connections_by_name: processor_config = processor_configs_by_name[processor_name] @@ -696,6 +706,10 @@ def _create_all_processors( if not (issubclass(processor_initializer, Handler) or issubclass(processor_initializer, Separator)): del processor_config["processor_type"] processor = processor_initializer(**processor_config) + if isinstance(processor, (AnaerobicLagoon, SlurryStorageOutdoor)): + processor.intercept_mean_temp = intercept_mean_temp + processor.phase_shift = phase_shift + processor.amplitude = amplitude self.all_processors[processor_name] = processor if isinstance(processor, Separator): From ca2c4b3022a3741bbeb7e2b9290cbed62ade69fd Mon Sep 17 00:00:00 2001 From: Matthew Liu Date: Mon, 20 Apr 2026 14:22:45 +0900 Subject: [PATCH 19/21] fix missing field --- RUFAS/biophysical/field/field/field.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RUFAS/biophysical/field/field/field.py b/RUFAS/biophysical/field/field/field.py index 28c884a0a6..6abef50bb8 100644 --- a/RUFAS/biophysical/field/field/field.py +++ b/RUFAS/biophysical/field/field/field.py @@ -1315,6 +1315,7 @@ def _record_planting( "year": MeasurementUnits.CALENDAR_YEAR, "day": MeasurementUnits.ORDINAL_DAY, "field_size": MeasurementUnits.HECTARE, + "field_name": MeasurementUnits.UNITLESS, "average_clay_percent": MeasurementUnits.PERCENT, } info_map = { @@ -1329,6 +1330,7 @@ def _record_planting( "year": year, "day": day, "field_size": self.field_data.field_size, + "field_name": self.field_data.name, "average_clay_percent": self.soil.data.average_clay_percent, } self.om.add_variable("crop_planting", value, info_map) From 15b61de316ac80e409b2e5b6e3fb99df78cf12ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 20 Apr 2026 05:25:07 +0000 Subject: [PATCH 20/21] Apply Black Formatting --- RUFAS/biophysical/manure/manure_manager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/RUFAS/biophysical/manure/manure_manager.py b/RUFAS/biophysical/manure/manure_manager.py index d4de4e4037..e8a69bafbf 100644 --- a/RUFAS/biophysical/manure/manure_manager.py +++ b/RUFAS/biophysical/manure/manure_manager.py @@ -77,8 +77,9 @@ def __init__(self, intercept_mean_temp: float, phase_shift: float, amplitude: fl processor_connections_by_name = self._validate_and_parse_processor_connections( processor_connections_input, processor_configs_by_name ) - self._create_all_processors(processor_connections_by_name, processor_configs_by_name, - intercept_mean_temp, phase_shift, amplitude) + self._create_all_processors( + processor_connections_by_name, processor_configs_by_name, intercept_mean_temp, phase_shift, amplitude + ) self._populate_adjacency_matrix(processor_connections_by_name) self._validate_adjacency_matrix() From 724ff092b5b3e5a71ea59394f648adae0aed5a45 Mon Sep 17 00:00:00 2001 From: matthew7838 Date: Mon, 20 Apr 2026 05:29:37 +0000 Subject: [PATCH 21/21] Update badges on README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2024cc3be2..04973ff2dc 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -[![Flake8](https://img.shields.io/badge/Flake8-passed-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Pytest](https://img.shields.io/badge/Pytest-passed-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Coverage](https://img.shields.io/badge/Coverage-99%25-brightgreen)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) -[![Mypy](https://img.shields.io/badge/Mypy-1180%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Flake8](https://img.shields.io/badge/Flake8-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Pytest](https://img.shields.io/badge/Pytest-failed-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Coverage](https://img.shields.io/badge/Coverage-%25-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) +[![Mypy](https://img.shields.io/badge/Mypy-1197%20errors-red)](https://github.com/RuminantFarmSystems/MASM/actions/workflows/combined_format_lint_test_mypy.yml) # RuFaS: Ruminant Farm Systems