Spaces:
Sleeping
Sleeping
| """Integration tests for the full simulator.""" | |
| from agentic_rl.engine.simulator import FishFarmSimulator | |
| class TestSimulatorBasics: | |
| def test_reset_creates_valid_state(self): | |
| sim = FishFarmSimulator(seed=42) | |
| state = sim.reset() | |
| assert state["fish"]["weight_g"] > 0 | |
| assert state["fish"]["population"] > 0 | |
| assert state["water"]["DO"] > 0 | |
| assert state["water"]["temperature"] > 0 | |
| def test_step_advances_time(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| state = sim.step(feeding_rate=0.5, aeration_rate=0.5, | |
| heater_setting=0.0, water_exchange_rate=0.01, | |
| harvest=False, treatment="none") | |
| assert state["time"]["hour"] == 1 | |
| def test_24_hours_equals_one_day(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| for _ in range(24): | |
| state = sim.step(0.5, 0.5, 0.0, 0.01, False, "none") | |
| assert state["time"]["day"] == 1 | |
| def test_overfeeding_causes_ammonia_rise(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| initial_tan = sim.water.TAN | |
| for _ in range(48): # 2 days of overfeeding | |
| sim.step(1.0, 0.3, 0.0, 0.0, False, "none") # max feed, low aeration, no exchange | |
| assert sim.water.TAN > initial_tan | |
| def test_no_aeration_causes_do_drop(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| for _ in range(12): # 12 hours nighttime without aeration | |
| sim.step(0.0, 0.0, 0.0, 0.0, False, "none") | |
| assert sim.water.DO < 7.0 # should drop from initial | |
| def test_fish_grow_over_time(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| initial_weight = sim.fish.weight_g | |
| for _ in range(24 * 7): # 1 week | |
| sim.step(0.5, 0.5, 0.0, 0.02, False, "none") | |
| assert sim.fish.weight_g > initial_weight | |
| def test_harvest_ends_episode(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| state = sim.step(0.5, 0.5, 0.0, 0.01, True, "none") # harvest=True | |
| assert state["harvested"] is True | |
| def test_mass_mortality_is_catastrophe(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| # Force lethal conditions | |
| sim.water.DO = 0.5 | |
| sim.water.TAN = 5.0 | |
| sim.water.temperature = 40.0 | |
| state = sim.step(0.0, 0.0, 0.0, 0.0, False, "none") | |
| assert state["fish"]["mortality_today"] > 0 | |
| def test_cascade_overfeed_to_mortality(self): | |
| """The signature RL challenge: overfeed -> ammonia -> DO crash -> deaths.""" | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| # Heavy overfeeding for 3 days with no aeration or exchange | |
| for _ in range(72): | |
| sim.step(1.0, 0.0, 0.0, 0.0, False, "none") | |
| # Should see elevated ammonia and reduced survival | |
| assert sim.water.TAN > 1.0 or sim.fish.population < 10000 | |
| def test_state_includes_enhanced_economics(self): | |
| """State dict should include new economics fields from engine enhancement.""" | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| state = sim.step(0.5, 0.5, 0.0, 0.02, False, "none") | |
| econ = state["economics"] | |
| assert "feed_price_per_kg" in econ | |
| assert "marginal_cost_per_hour" in econ | |
| assert "roi_pct" in econ | |
| assert econ["feed_price_per_kg"] > 0 | |
| def test_stochastic_feed_price_varies(self): | |
| """Feed price should vary stochastically over time.""" | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| prices = [] | |
| for _ in range(48): | |
| state = sim.step(0.5, 0.5, 0.0, 0.02, False, "none") | |
| prices.append(state["economics"]["feed_price_per_kg"]) | |
| # Price should not be perfectly constant (OU process adds noise) | |
| assert len(set(prices)) > 1 | |
| def test_seasonal_price_varies_by_day(self): | |
| """Market price multiplier should reflect seasonal demand.""" | |
| from agentic_rl.engine.economics import EconomicsEngine | |
| econ = EconomicsEngine() | |
| econ.reset() | |
| econ.apply_seasonal_price(day_of_year=360) # Christmas → premium | |
| xmas_price = econ.market_price_multiplier | |
| econ.market_price_multiplier = 1.0 # reset | |
| econ.apply_seasonal_price(day_of_year=180) # mid-year → dip | |
| midyear_price = econ.market_price_multiplier | |
| assert xmas_price > midyear_price | |
| def test_vaccination_treatment_option(self): | |
| """Vaccination should move susceptible fish to recovered.""" | |
| from agentic_rl.engine.disease import DiseaseEngine | |
| de = DiseaseEngine() | |
| de.reset(population=10000) | |
| initial_susceptible = de.susceptible | |
| de.apply_treatment("vaccination") | |
| assert de.recovered > 0 | |
| assert de.susceptible < initial_susceptible | |
| # 80% should be vaccinated | |
| assert de.recovered >= int(initial_susceptible * 0.79) | |
| def test_temperature_affects_disease_virulence(self): | |
| """Disease should progress differently at different temperatures.""" | |
| from agentic_rl.engine.disease import DiseaseEngine | |
| de_warm = DiseaseEngine() | |
| de_warm.reset(population=10000) | |
| de_warm.trigger_outbreak(50) | |
| de_cold = DiseaseEngine() | |
| de_cold.reset(population=10000) | |
| de_cold.trigger_outbreak(50) | |
| # Run for 5 days | |
| for _ in range(120): | |
| de_warm.step(1.0, de_warm.susceptible + de_warm.exposed + | |
| de_warm.infected + de_warm.recovered, temperature=30.0) | |
| de_cold.step(1.0, de_cold.susceptible + de_cold.exposed + | |
| de_cold.infected + de_cold.recovered, temperature=15.0) | |
| # Warm conditions should produce more disease deaths | |
| assert de_warm.total_disease_deaths >= de_cold.total_disease_deaths | |
| class TestObservationCompleteness: | |
| """Verify the FarmObservation includes all enhanced fields.""" | |
| def test_observation_has_fish_growth_fields(self): | |
| """Observation should include FCR, SGR, growth rate, stocking density.""" | |
| from agentic_rl.server.environment import FishFarmEnvironment | |
| env = FishFarmEnvironment() | |
| obs = env.reset(task_id="feeding_basics") | |
| assert hasattr(obs, "fcr") | |
| assert hasattr(obs, "sgr") | |
| assert hasattr(obs, "growth_rate_g_day") | |
| assert hasattr(obs, "stocking_density") | |
| def test_observation_has_economics_fields(self): | |
| """Observation should include stochastic feed price, ROI, marginal cost.""" | |
| from agentic_rl.server.environment import FishFarmEnvironment | |
| from agentic_rl.models import FarmAction | |
| env = FishFarmEnvironment() | |
| env.reset(task_id="feeding_basics") | |
| step_obs = env.step(FarmAction(feeding_rate=0.5, aeration_rate=0.5)) | |
| assert hasattr(step_obs, "feed_price_per_kg") | |
| assert hasattr(step_obs, "market_price_multiplier") | |
| assert hasattr(step_obs, "marginal_cost_per_hour") | |
| assert hasattr(step_obs, "roi_pct") | |
| assert step_obs.feed_price_per_kg > 0 | |
| def test_observation_has_weather_fields(self): | |
| """Observation should include daytime, storm, humidity.""" | |
| from agentic_rl.server.environment import FishFarmEnvironment | |
| env = FishFarmEnvironment() | |
| obs = env.reset(task_id="feeding_basics") | |
| assert hasattr(obs, "is_daytime") | |
| assert hasattr(obs, "storm_active") | |
| assert hasattr(obs, "humidity") | |
| def test_observation_has_disease_signal(self): | |
| """Observation should have disease_suspected (behavioral indicator).""" | |
| from agentic_rl.server.environment import FishFarmEnvironment | |
| env = FishFarmEnvironment() | |
| obs = env.reset(task_id="feeding_basics") | |
| assert hasattr(obs, "disease_suspected") | |
| # No disease initially | |
| assert obs.disease_suspected is False | |
| def test_observation_has_survival_fields(self): | |
| """Observation should include cumulative mortality and survival rate.""" | |
| from agentic_rl.server.environment import FishFarmEnvironment | |
| env = FishFarmEnvironment() | |
| obs = env.reset(task_id="feeding_basics") | |
| assert hasattr(obs, "cumulative_mortality") | |
| assert hasattr(obs, "survival_rate") | |
| assert obs.survival_rate == 1.0 | |
| def test_observation_has_nitrate_and_algae(self): | |
| """Observation should include NO3 and algae bloom status.""" | |
| from agentic_rl.server.environment import FishFarmEnvironment | |
| env = FishFarmEnvironment() | |
| obs = env.reset(task_id="feeding_basics") | |
| assert hasattr(obs, "nitrate") | |
| assert hasattr(obs, "algae_bloom") | |
| class TestHeuristicAgent: | |
| """Test the rule-based heuristic fallback agent.""" | |
| def test_heuristic_reduces_feed_on_low_do(self): | |
| from inference import heuristic_action | |
| obs = {"dissolved_oxygen": 2.0, "ammonia_toxic": 0.01, | |
| "temperature": 28.0, "stress_level": 0.3, | |
| "feeding_response": "sluggish", "avg_fish_weight": 100.0, | |
| "population": 5000, "feed_remaining_kg": 200.0, | |
| "biofilter_working": True, "aerator_working": True, | |
| "disease_suspected": False, "is_daytime": True, | |
| "market_price_multiplier": 1.0} | |
| action = heuristic_action(obs, "feeding_basics", 10, 168) | |
| assert action["feeding_rate"] <= 0.2 | |
| assert action["aeration_rate"] == 1.0 # emergency DO | |
| def test_heuristic_treats_disease(self): | |
| from inference import heuristic_action | |
| obs = {"dissolved_oxygen": 6.0, "ammonia_toxic": 0.01, | |
| "temperature": 28.0, "stress_level": 0.5, | |
| "feeding_response": "sluggish", "avg_fish_weight": 200.0, | |
| "population": 5000, "feed_remaining_kg": 200.0, | |
| "biofilter_working": True, "aerator_working": True, | |
| "disease_suspected": True, "mortality_today": 15, | |
| "is_daytime": True, "market_price_multiplier": 1.0} | |
| action = heuristic_action(obs, "disease_outbreak", 50, 240) | |
| assert action["treatment"] == "antibiotics" | |
| def test_heuristic_harvests_at_market_weight(self): | |
| from inference import heuristic_action | |
| obs = {"dissolved_oxygen": 7.0, "ammonia_toxic": 0.01, | |
| "temperature": 28.0, "stress_level": 0.1, | |
| "feeding_response": "eager", "avg_fish_weight": 550.0, | |
| "population": 5000, "feed_remaining_kg": 200.0, | |
| "biofilter_working": True, "aerator_working": True, | |
| "disease_suspected": False, "is_daytime": True, | |
| "market_price_multiplier": 1.15, "mortality_today": 0} | |
| action = heuristic_action(obs, "full_growout", 1400, 1440) | |
| assert action["harvest_decision"] is True | |
| def test_heuristic_heats_cold_water(self): | |
| from inference import heuristic_action | |
| obs = {"dissolved_oxygen": 7.0, "ammonia_toxic": 0.01, | |
| "temperature": 22.0, "stress_level": 0.2, | |
| "feeding_response": "normal", "avg_fish_weight": 100.0, | |
| "population": 5000, "feed_remaining_kg": 200.0, | |
| "biofilter_working": True, "aerator_working": True, | |
| "disease_suspected": False, "is_daytime": True, | |
| "market_price_multiplier": 1.0, "mortality_today": 0} | |
| action = heuristic_action(obs, "temperature_stress", 10, 120) | |
| assert action["heater_setting"] > 0 | |
| def test_heuristic_increases_exchange_for_high_ammonia(self): | |
| from inference import heuristic_action | |
| obs = {"dissolved_oxygen": 6.0, "ammonia_toxic": 0.15, "ammonia": 2.5, | |
| "temperature": 28.0, "stress_level": 0.3, | |
| "feeding_response": "sluggish", "avg_fish_weight": 150.0, | |
| "population": 5000, "feed_remaining_kg": 200.0, | |
| "biofilter_working": True, "aerator_working": True, | |
| "disease_suspected": False, "is_daytime": True, | |
| "market_price_multiplier": 1.0, "mortality_today": 0} | |
| action = heuristic_action(obs, "ammonia_crisis", 10, 72) | |
| assert action["water_exchange_rate"] >= 0.05 | |
| class TestStochasticGrowth: | |
| """Test stochastic growth noise (KB-03 Sec 9.2).""" | |
| def test_growth_has_variance_across_seeds(self): | |
| """Different seeds should produce slightly different growth outcomes.""" | |
| weights = [] | |
| for seed in [1, 2, 3, 4, 5]: | |
| sim = FishFarmSimulator(seed=seed) | |
| sim.reset(seed=seed) | |
| for _ in range(24): | |
| sim.step(0.5, 0.5, 0.0, 0.02, False, "none") | |
| weights.append(sim.fish.weight_g) | |
| # All should be close (same conditions) but not identical (stochastic noise) | |
| assert max(weights) > min(weights) # some variation exists | |
| # But within reasonable bounds (<2% spread for 24h) | |
| spread = (max(weights) - min(weights)) / min(weights) | |
| assert spread < 0.05 # less than 5% spread in 24h | |
| def test_deterministic_with_same_seed(self): | |
| """Same seed should produce identical results.""" | |
| results = [] | |
| for _ in range(2): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset(seed=42) | |
| for _ in range(24): | |
| sim.step(0.5, 0.5, 0.0, 0.02, False, "none") | |
| results.append(sim.fish.weight_g) | |
| assert results[0] == results[1] | |
| class TestNighttimeDORisk: | |
| """Test nighttime DO crash risk tracking.""" | |
| def test_state_includes_nighttime_do_risk(self): | |
| sim = FishFarmSimulator(seed=42) | |
| state = sim.reset() | |
| assert "nighttime_do_risk" in state["water"] | |
| assert 0.0 <= state["water"]["nighttime_do_risk"] <= 1.0 | |
| def test_observation_has_nighttime_do_risk(self): | |
| from agentic_rl.server.environment import FishFarmEnvironment | |
| env = FishFarmEnvironment() | |
| obs = env.reset(task_id="feeding_basics") | |
| assert hasattr(obs, "nighttime_do_risk") | |
| assert 0.0 <= obs.nighttime_do_risk <= 1.0 | |
| def test_high_algae_increases_nighttime_risk(self): | |
| """Algae bloom should raise nighttime DO crash risk.""" | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| # Force algae bloom | |
| sim.water.chlorophyll_a = 100.0 | |
| # Run through a day-night cycle (24h) | |
| for _ in range(24): | |
| sim.step(0.5, 0.3, 0.0, 0.02, False, "none") | |
| # Risk should be non-zero with high algae | |
| assert sim.water.nighttime_do_risk >= 0.0 | |
| def test_heuristic_boosts_aeration_on_high_risk(self): | |
| """Heuristic should increase aeration when nighttime DO risk is high.""" | |
| from inference import heuristic_action | |
| obs = {"dissolved_oxygen": 6.0, "ammonia_toxic": 0.01, | |
| "temperature": 28.0, "stress_level": 0.1, | |
| "feeding_response": "normal", "avg_fish_weight": 100.0, | |
| "population": 5000, "feed_remaining_kg": 200.0, | |
| "biofilter_working": True, "aerator_working": True, | |
| "disease_suspected": False, "is_daytime": False, | |
| "market_price_multiplier": 1.0, "mortality_today": 0, | |
| "nighttime_do_risk": 0.8} | |
| action = heuristic_action(obs, "oxygen_management", 10, 72) | |
| assert action["aeration_rate"] >= 0.9 # should boost for high risk | |
| class TestHeatWaveEvent: | |
| """Test that heat_wave events actually raise water temperature.""" | |
| def test_heat_wave_raises_temperature(self): | |
| """Heat wave event should increase water temperature over time.""" | |
| from agentic_rl.engine.events import Event | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset( | |
| initial_temp=28.0, | |
| base_air_temp=30.0, | |
| scheduled_events=[ | |
| Event(type="heat_wave", trigger_hour=0, severity=0.7, | |
| duration_hours=48, description="Heat wave test"), | |
| ], | |
| ) | |
| # Run 24 hours with heat wave active | |
| for _ in range(24): | |
| sim.step(0.3, 0.5, 0.0, 0.02, False, "none") | |
| # Water should warm up from the heat wave | |
| assert sim.water.temperature > 28.0 | |
| def test_heat_wave_ends_correctly(self): | |
| """After heat wave ends, temperature should not keep rising.""" | |
| from agentic_rl.engine.events import Event | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset( | |
| initial_temp=28.0, | |
| base_air_temp=28.0, | |
| scheduled_events=[ | |
| Event(type="heat_wave", trigger_hour=0, severity=0.7, | |
| duration_hours=6, description="Short heat wave"), | |
| ], | |
| ) | |
| # Run through the 6-hour heat wave | |
| for _ in range(6): | |
| sim.step(0.3, 0.5, 0.0, 0.02, False, "none") | |
| temp_at_end = sim.water.temperature | |
| # Run 12 more hours after heat wave ended — temp should stabilize/drop | |
| for _ in range(12): | |
| sim.step(0.3, 0.5, -0.3, 0.02, False, "none") # slight cooling | |
| # Should not have increased further (heat wave is over) | |
| assert sim.water.temperature <= temp_at_end + 1.0 # small tolerance for thermal inertia | |
| class TestVaccinationProphylaxis: | |
| """Test that vaccination works as preventive measure (KB-03 Sec 4.2).""" | |
| def test_vaccination_without_active_disease(self): | |
| """Vaccination should work even when no disease is active.""" | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| assert sim.disease.is_active is False | |
| initial_susceptible = sim.disease.susceptible | |
| sim.step(0.5, 0.5, 0.0, 0.02, False, "vaccination") | |
| # 80% of susceptible should be vaccinated (moved to recovered) | |
| assert sim.disease.recovered > 0 | |
| assert sim.disease.susceptible < initial_susceptible | |
| def test_vaccination_cost_charged(self): | |
| """Vaccination cost should be recorded even without active disease.""" | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| sim.step(0.5, 0.5, 0.0, 0.02, False, "vaccination") | |
| assert sim.economics.total_treatment_cost > 0 | |
| def test_antibiotics_blocked_without_disease(self): | |
| """Non-vaccination treatments should NOT apply without active disease.""" | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| sim.step(0.5, 0.5, 0.0, 0.02, False, "antibiotics") | |
| assert sim.economics.total_treatment_cost == 0.0 | |
| class TestCostBreakdown: | |
| """Test that cost breakdown is exposed in state dict.""" | |
| def test_state_includes_cost_breakdown(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| state = sim.step(0.5, 0.5, 0.0, 0.02, False, "none") | |
| assert "cost_breakdown" in state["economics"] | |
| breakdown = state["economics"]["cost_breakdown"] | |
| assert "feed" in breakdown | |
| assert "energy" in breakdown | |
| assert "total" in breakdown | |
| def test_cost_breakdown_components_sum(self): | |
| sim = FishFarmSimulator(seed=42) | |
| sim.reset() | |
| for _ in range(24): | |
| state = sim.step(0.5, 0.5, 0.0, 0.02, False, "none") | |
| breakdown = state["economics"]["cost_breakdown"] | |
| component_sum = sum( | |
| v["amount"] for v in breakdown.values() if isinstance(v, dict) | |
| ) | |
| assert abs(component_sum - breakdown["total"]) < 0.1 | |
| class TestHarvestRevenue: | |
| """Test weight-dependent harvest revenue.""" | |
| def test_harvest_revenue_uses_weight_premium(self): | |
| """Harvest revenue should reflect weight-dependent pricing.""" | |
| from agentic_rl.engine.economics import EconomicsEngine | |
| econ = EconomicsEngine() | |
| econ.reset() | |
| # Underweight fish should get less revenue than market-weight fish | |
| rev_small = econ.calculate_harvest_revenue(100.0, avg_weight_g=100.0) | |
| rev_large = econ.calculate_harvest_revenue(100.0, avg_weight_g=500.0) | |
| assert rev_large > rev_small | |
| def test_harvest_matches_fish_value(self): | |
| """Harvest revenue should equal fish value (same pricing curve).""" | |
| from agentic_rl.engine.economics import EconomicsEngine | |
| econ = EconomicsEngine() | |
| econ.reset() | |
| value = econ.calculate_fish_value(200.0, avg_weight_g=350.0) | |
| revenue = econ.calculate_harvest_revenue(200.0, avg_weight_g=350.0) | |
| assert abs(value - revenue) < 0.01 | |
| class TestTaskSpecificHeuristics: | |
| """Test task-specific heuristic strategies in inference.py.""" | |
| def _base_obs(self, **overrides): | |
| obs = { | |
| "dissolved_oxygen": 6.5, "ammonia_toxic": 0.01, "ammonia": 0.2, | |
| "nitrite": 0.05, "temperature": 28.0, "stress_level": 0.1, | |
| "feeding_response": "normal", "avg_fish_weight": 150.0, | |
| "population": 5000, "feed_remaining_kg": 200.0, | |
| "biofilter_working": True, "aerator_working": True, | |
| "disease_suspected": False, "is_daytime": True, | |
| "market_price_multiplier": 1.0, "mortality_today": 0, | |
| "nighttime_do_risk": 0.1, "feed_price_per_kg": 0.50, | |
| "water_quality_score": 0.85, "algae_bloom": False, | |
| } | |
| obs.update(overrides) | |
| return obs | |
| def test_storm_pre_positioning(self): | |
| """Storm response: pre-storm phase should boost aeration and reduce feeding.""" | |
| from inference import heuristic_action | |
| obs = self._base_obs() | |
| action = heuristic_action(obs, "storm_response", step=10, max_hours=120) | |
| assert action["aeration_rate"] >= 0.8 | |
| assert action["water_exchange_rate"] >= 0.04 | |
| def test_storm_power_outage_minimal_feeding(self): | |
| """During power outage (h24-36), feeding should be minimal.""" | |
| from inference import heuristic_action | |
| obs = self._base_obs(aerator_working=False) | |
| action = heuristic_action(obs, "storm_response", step=28, max_hours=120) | |
| assert action["feeding_rate"] <= 0.1 | |
| def test_ammonia_crisis_aggressive_exchange(self): | |
| """Ammonia crisis with high UIA should trigger aggressive water exchange.""" | |
| from inference import heuristic_action | |
| obs = self._base_obs(ammonia_toxic=0.08, ammonia=1.5, biofilter_working=False) | |
| action = heuristic_action(obs, "ammonia_crisis", step=5, max_hours=72) | |
| assert action["water_exchange_rate"] >= 0.06 | |
| assert action["feeding_rate"] <= 0.15 | |
| def test_disease_outbreak_early_vaccination(self): | |
| """Disease outbreak task: should vaccinate at step 1 (before h12 trigger).""" | |
| from inference import heuristic_action | |
| obs = self._base_obs() | |
| action = heuristic_action(obs, "disease_outbreak", step=1, max_hours=240) | |
| assert action["treatment"] == "vaccination" | |
| def test_multi_objective_stress_reduction(self): | |
| """Multi-objective: high stress should reduce feeding for welfare.""" | |
| from inference import heuristic_action | |
| obs = self._base_obs(stress_level=0.35) | |
| action = heuristic_action(obs, "multi_objective", step=100, max_hours=720) | |
| assert action["feeding_rate"] <= 0.35 | |
| # Multi-objective minimizes aeration when DO is good to save costs | |
| assert action["aeration_rate"] <= 0.4 | |
| def test_temperature_stress_cooling(self): | |
| """Temperature stress: hot temps should trigger cooling + more aeration.""" | |
| from inference import heuristic_action | |
| obs = self._base_obs(temperature=35.0) | |
| action = heuristic_action(obs, "temperature_stress", step=30, max_hours=120) | |
| assert action["aeration_rate"] >= 0.85 | |
| assert action["feeding_rate"] <= 0.3 | |
| def test_nitrite_triggers_salt_treatment(self): | |
| """High nitrite should trigger salt treatment.""" | |
| from inference import heuristic_action | |
| obs = self._base_obs(nitrite=0.8) | |
| action = heuristic_action(obs, "water_quality_balance", step=10, max_hours=168) | |
| assert action["treatment"] == "salt" | |
| def test_full_growout_harvest_at_market_weight(self): | |
| """Full growout: harvest when weight >= 400 near episode end.""" | |
| from inference import heuristic_action | |
| obs = self._base_obs(avg_fish_weight=460.0, market_price_multiplier=1.05) | |
| # hours_left = 1440 - 1420 = 20 → triggers weight >= 400 and hours_left <= 24 | |
| action = heuristic_action(obs, "full_growout", step=1420, max_hours=1440) | |
| assert action["harvest_decision"] is True | |
| def test_season_management_conserve_low_feed(self): | |
| """Season management: conserve feed when inventory critically low.""" | |
| from inference import heuristic_action | |
| obs = self._base_obs(feed_remaining_kg=25.0) # < 30 triggers min 0.2 | |
| action = heuristic_action(obs, "season_management", step=500, max_hours=2160) | |
| assert action["feeding_rate"] <= 0.25 | |
| def test_feed_price_sensitivity(self): | |
| """Expensive feed should reduce feeding rate by 15%.""" | |
| from inference import heuristic_action | |
| obs_cheap = self._base_obs(feed_price_per_kg=0.40) | |
| obs_expensive = self._base_obs(feed_price_per_kg=0.70) | |
| action_cheap = heuristic_action(obs_cheap, "feeding_basics", step=10, max_hours=168) | |
| action_expensive = heuristic_action(obs_expensive, "feeding_basics", step=10, max_hours=168) | |
| assert action_expensive["feeding_rate"] < action_cheap["feeding_rate"] | |
| def test_catastrophe_survives_through_crises(self): | |
| """Catastrophe: endure crises, harvest after engagement threshold (≥84h).""" | |
| from inference import heuristic_action | |
| obs = self._base_obs(avg_fish_weight=250.0) | |
| # Early: should NOT harvest (engagement penalty in grader) | |
| action_early = heuristic_action(obs, "catastrophe_prevention", step=2, max_hours=336) | |
| assert action_early["harvest_decision"] is False | |
| # After engagement threshold: harvest to lock in survival | |
| action_late = heuristic_action(obs, "catastrophe_prevention", step=90, max_hours=336) | |
| assert action_late["harvest_decision"] is True | |