Coverage for test/test_agent_plant.py: 100%
197 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1import copy
2import datetime
3import pytest
4import numpy as np
5from ..agent_model.agents import PlantAgent, BaseAgent
6from ..agent_model.util import get_default_currency_data, get_default_agent_data
8@pytest.fixture
9def basic_kwargs():
10 return {
11 'flows': {
12 'in': {'co2': {'value': 1, 'connections': ['test_greenhouse']},
13 'par': {'value': 1, 'connections': ['test_greenhouse']}},
14 'out': {'biomass': {'value': 1, 'connections': ['test_plant']},
15 'inedible_biomass': {'value': 0.1, 'connections': ['test_greenhouse']}},
16 },
17 'capacity': {'biomass': 10},
18 'properties': {'photoperiod': {'value': 12},
19 'lifetime': {'value': 600},
20 'par_baseline': {'value': 2}}}
22@pytest.fixture
23def wheat_kwargs():
24 return get_default_agent_data('wheat')
26@pytest.fixture
27def mock_model():
28 class MockModel:
29 floating_point_precision = 6
30 agents = {}
31 time = datetime.datetime(2020, 1, 1)
32 currencies = get_default_currency_data()
33 step_num = 0
34 def register(self):
35 for agent in self.agents.values():
36 agent.register()
37 def step(self):
38 self.step_num += 1
39 self.time += datetime.timedelta(hours=1)
40 for agent in self.agents.values():
41 agent.step()
42 return MockModel()
44@pytest.fixture
45def basic_model(mock_model, basic_kwargs):
46 test_agent = PlantAgent(mock_model, 'test_plant', **basic_kwargs)
47 test_greenhouse = BaseAgent(
48 mock_model, 'test_greenhouse',
49 capacity={'co2': 10, 'par': 10, 'inedible_biomass': 10},
50 storage={'co2': 10, 'par': 10},
51 )
52 mock_model.agents = {
53 'test_plant': test_agent,
54 'test_greenhouse': test_greenhouse,
55 }
56 return mock_model
58@pytest.fixture
59def wheat_model(mock_model, wheat_kwargs):
60 wheat_kwargs['flows']['in']['par']['connections'] = ['lamp']
61 wheat_agent = PlantAgent(mock_model, 'wheat', **wheat_kwargs)
62 greenhouse = BaseAgent(
63 mock_model, 'greenhouse',
64 capacity={'co2': 1e5, 'o2': 1e5, 'h2o': 1e5},
65 storage={'co2': 1e5},
66 )
67 water_storage = BaseAgent(
68 mock_model, 'water_storage',
69 capacity={'potable': 1e5},
70 storage={'potable': 1e5},
71 )
72 nutrient_storage = BaseAgent(
73 mock_model, 'nutrient_storage',
74 capacity={'fertilizer': 1e5, 'inedible_biomass': 10},
75 storage={'fertilizer': 1e5},
76 )
77 food_storage = BaseAgent(
78 mock_model, 'food_storage',
79 capacity={'wheat': 1e5},
80 )
81 lamp = BaseAgent(
82 mock_model, 'lamp',
83 capacity={'par': 1e5},
84 storage={'par': 1e5},
85 )
86 mock_model.agents = {
87 'wheat': wheat_agent,
88 'greenhouse': greenhouse,
89 'water_storage': water_storage,
90 'nutrient_storage': nutrient_storage,
91 'food_storage': food_storage,
92 'lamp': lamp,
93 }
94 return mock_model
96class TestAgentPlant:
97 def test_agent_plant_init_basic(self, basic_kwargs):
98 """
99 - iniitalize default attributes correctly
100 - respond to delay correctly
101 - initialize non-serialized vars
102 """
103 test_model = object()
105 # Test required attributes
106 kwargs = copy.deepcopy(basic_kwargs)
107 del kwargs['flows']['out']['inedible_biomass']
108 with pytest.raises(ValueError):
109 test_plant = PlantAgent(test_model, 'test_plant', **kwargs)
111 # Initialize with default attributes and non-serialized vars
112 test_plant = PlantAgent(test_model, 'test_plant', **basic_kwargs)
113 for attr, value in test_plant.default_attributes.items():
114 assert test_plant.attributes[attr] == value
115 assert test_plant.daily_growth == []
116 assert test_plant.max_growth == 0
117 assert test_plant.active == 1
119 # Initialize with custom attributes, check delay_start
120 test_attributes = {'delay_start': 10, 'test_attribute': 1}
121 kwargs = {**basic_kwargs, 'attributes': test_attributes}
122 test_plant = PlantAgent(test_model, 'test_plant', **kwargs)
123 assert test_plant.attributes['delay_start'] == 10
124 assert test_plant.attributes['test_attribute'] == 1
125 assert test_plant.active == 0
127 def test_agent_plant_init_wheat(self, wheat_kwargs):
128 test_model = object()
129 PlantAgent(test_model, 'wheat', **wheat_kwargs)
130 assert True
132 def test_agent_plant_register(self, basic_model):
133 """
134 - calculate growth rate array correctly
135 - calculate max_growth correctly
136 - intitialize cache in Model
137 """
138 plant_agent = basic_model.agents['test_plant']
139 plant_agent.register()
140 assert plant_agent.registered
142 expected_daily_growth = np.zeros(24)
143 expected_daily_growth[6:18] = 2
144 assert np.array_equal(plant_agent.daily_growth, expected_daily_growth)
145 expected_max_growth = 1 * 600
146 assert plant_agent.max_growth == expected_max_growth
147 expected_co2_response_cache = dict(step_num=0, cu_factor=1, te_factor=1)
148 assert basic_model._co2_response_cache == expected_co2_response_cache
150 def test_agent_plant_get_flow_value(self, wheat_model):
151 """
152 - return 0 for non-grown values when grown
153 """
154 wheat_model.register()
155 wheat_plant = wheat_model.agents['wheat']
157 # Check all non-grown flows are positive when plant is not grown
158 assert wheat_plant.attributes['grown'] == False
159 influx = {}
160 wheat_plant.attributes['growth_rate'] = 0.5 # Mid-life so all flows > 0
161 wheat_model.time = datetime.datetime(2020, 1, 1, 12)
162 for direction, flows in wheat_plant.flows.items():
163 for currency, flow in flows.items():
164 if currency == 'par':
165 continue # Dummy flow
166 flow_value = wheat_plant.get_flow_value(1, direction, currency, flow, influx)
167 if ('criteria' in flow and flow['criteria'][0]['path'] == 'grown'):
168 assert flow_value == 0, f'Flow value for {currency} should be 0'
169 else:
170 assert flow_value > 0, f'Flow value for {currency} should be > 0'
171 influx[currency] = 1
173 # Check all non-grown flows are when plant is grown, vice versa
174 wheat_plant.attributes['grown'] = True
175 wheat_plant.storage['biomass'] = 10 # So biomass-wieghted flows > 0
176 for direction, flows in wheat_plant.flows.items():
177 for currency, flow in flows.items():
178 if currency == 'par':
179 continue # Dummy flow
180 flow_value = wheat_plant.get_flow_value(1, direction, currency, flow, influx)
181 if ('criteria' in flow and flow['criteria'][0]['path'] == 'grown'):
182 assert flow_value > 0, f'Flow value for {currency} should be > 0'
183 else:
184 assert flow_value == 0, f'Flow value for {currency} should be 0'
185 influx[currency] = 1
187 def test_agent_plant_calculate_co2_response(self, wheat_model):
188 """
189 - Return correct values for different co2 levels
190 - Return floor/ceiling values outside range
191 - Return 1 for c4 plants
192 - Manage cache correctly
193 """
194 wheat_model.register()
195 wheat_agent = wheat_model.agents['wheat']
196 # test cache
197 wheat_model._co2_response_cache = dict(
198 step_num=0, cu_factor='test_cu', te_factor='test_te')
199 assert wheat_agent._calculate_co2_response() == ('test_cu', 'test_te')
201 # test c3 plant
202 def expected_cu_factor(co2_actual, t_mean=25):
203 """Copied from the body of PlantAgent"""
204 tt = (163 - t_mean) / (5 - 0.1 * t_mean)
205 t_mean = 25 # Mean temperature for timestep.
206 tt = (163 - t_mean) / (5 - 0.1 * t_mean) # co2 compensation point
207 numerator = (co2_actual - tt) * (350 + 2 * tt)
208 denominator = (co2_actual + 2 * tt) * (350 - tt)
209 cu_ratio = numerator/denominator
210 # Invert the above to give *decrease* in growth for less than ideal CO2
211 crf_ideal = 1.2426059597016264 # At 700ppm, the above equation gives this value
212 cu_ratio = cu_ratio / crf_ideal
213 return cu_ratio
214 def expected_te_factor(co2_actual):
215 return np.interp(co2_actual, [350, 700], [1/1.37, 1])
216 test_cases = [
217 # ppm, cu_factor, te_factor
218 (300, 1/1.242605, 1/1.37),
219 (500, expected_cu_factor(500), expected_te_factor(500)),
220 (600, expected_cu_factor(600), expected_te_factor(600)),
221 (750, 1, 1),
222 ]
223 for ppm, expected_cu, expected_te in test_cases:
224 wheat_model._co2_response_cache['step_num'] = -1
225 greenhouse_atmosphere = {
226 'co2': ppm/1e6,
227 'o2': 1 - ppm/1e6,
228 }
229 wheat_model.agents['greenhouse'].storage = greenhouse_atmosphere
230 cu_factor, te_factor = wheat_agent._calculate_co2_response()
231 assert cu_factor == pytest.approx(expected_cu), f'cu_factor failed at {ppm} ppm'
232 assert te_factor == pytest.approx(expected_te), f'te_factor failed at {ppm} ppm'
233 assert wheat_model._co2_response_cache['step_num'] == 0
235 # test non-c3 plant
236 wheat_model._co2_response_cache['step_num'] = -1
237 gh_atmo = {'co2': 500/1e6, 'o2': 1 - 500/1e6}
238 wheat_model.agents['greenhouse'].storage = gh_atmo
239 wheat_agent.properties['carbon_fixation']['value'] = 'c4'
240 cu_factor, te_factor = wheat_agent._calculate_co2_response()
241 assert cu_factor == 1
243 def test_agent_plant_kill(self, basic_model, basic_kwargs):
244 """
245 - Reduce biomass and add to storage correctly, with or without n_dead spec
246 """
247 basic_kwargs['amount'] = 10
248 basic_kwargs['storage'] = {'biomass': 10}
249 basic_model.agents['test_plant'] = PlantAgent(
250 basic_model, 'test_agent', **basic_kwargs)
251 basic_model.register()
252 test_plant = basic_model.agents['test_plant']
253 test_greenhouse = basic_model.agents['test_greenhouse']
255 test_plant.kill('test_reason', 5)
256 assert test_plant.storage['biomass'] == 5
257 assert test_plant.cause_of_death == None
258 assert test_greenhouse.storage['inedible_biomass'] == 5
260 test_plant.kill('test_reason_2')
261 assert test_plant.storage['biomass'] == 0
262 assert test_plant.cause_of_death == 'test_reason_2'
263 assert test_greenhouse.storage['inedible_biomass'] == 10
265class TestAgentPlantStep:
267 def test_agent_plant_step_delay(self, wheat_model, wheat_kwargs):
268 wheat_model.time = datetime.datetime(2020, 1, 1, 12)
269 wheat_kwargs['attributes']['delay_start'] = 2
270 wheat_model.agents['wheat'] = PlantAgent(wheat_model, 'wheat', **wheat_kwargs)
271 wheat_agent = wheat_model.agents['wheat']
272 wheat_model.register()
273 for i in range(3):
274 assert wheat_agent.attributes['delay_start'] == max(0, 2 - i)
275 if i < 2:
276 assert wheat_agent.active == 0
277 assert wheat_agent.attributes['age'] == 0
278 else:
279 assert wheat_agent.active > 0
280 wheat_agent.step()
281 wheat_model.time += datetime.timedelta(hours=1)
282 assert wheat_agent.attributes['age'] > 0
283 wheat_records = wheat_agent.get_records()
284 wheat_biomass_out = wheat_records['flows']['out']['biomass']['wheat']
285 assert wheat_biomass_out[:2] == [0, 0]
286 assert wheat_biomass_out[2] > 0
288 def test_agent_plant_step_reproduce(self, wheat_model, wheat_kwargs):
289 wheat_model.register()
290 wheat_agent = wheat_model.agents['wheat']
291 ns_agent = wheat_model.agents['nutrient_storage']
292 fs_agent = wheat_model.agents['food_storage']
293 lifetime = wheat_agent.properties['lifetime']['value']
294 expected_lifetime_biomass = lifetime * wheat_agent.flows['out']['biomass']['value']
295 for _ in range(lifetime):
296 wheat_model.step()
297 assert wheat_agent.attributes['age'] == lifetime
298 assert wheat_agent.storage['biomass'] == pytest.approx(expected_lifetime_biomass)
299 assert 'inedible_biomass' not in ns_agent.storage
300 assert 'wheat' not in fs_agent.storage
302 wheat_model.step()
303 assert wheat_agent.attributes['age'] == 0
304 assert wheat_agent.storage['biomass'] == 0
305 harvest_index = wheat_agent.properties['harvest_index']['value']
306 expected_wheat = pytest.approx(expected_lifetime_biomass * harvest_index)
307 expected_inedib = pytest.approx(expected_lifetime_biomass * (1 - harvest_index))
308 assert fs_agent.storage['wheat'] == expected_wheat
309 assert ns_agent.storage['inedible_biomass'] == expected_inedib
311 def test_agent_plant_step(self):
312 """
313 - handle delay start correctly
314 - handle grown/reproduce correctly
315 - Set daily_growth_factor correctly
316 - Set par_factor correctly with both electric and sun light
317 - Set growth rate correctly
318 - Call/Set _calculate_co2_response correctly
319 """
321 pass