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

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 

7 

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}}} 

21 

22@pytest.fixture 

23def wheat_kwargs(): 

24 return get_default_agent_data('wheat') 

25 

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() 

43 

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 

57 

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 

95 

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() 

104 

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) 

110 

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 

118 

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 

126 

127 def test_agent_plant_init_wheat(self, wheat_kwargs): 

128 test_model = object() 

129 PlantAgent(test_model, 'wheat', **wheat_kwargs) 

130 assert True 

131 

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 

141 

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 

149 

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'] 

156 

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 

172 

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 

186 

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') 

200 

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 

234 

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 

242 

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'] 

254 

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 

259 

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 

264 

265class TestAgentPlantStep: 

266 

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 

287 

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 

301 

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 

310 

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 """ 

320 

321 pass