1 import copy

2 import datetime

3 import pytest

4 import numpy as np

5 from ..agent_model.agents import PlantAgent, BaseAgent

6 from ..agent_model.util import get_default_currency_data, get_default_agent_data

7

8@pytest.fixture

9 def 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

23 def wheat_kwargs():

24 return get_default_agent_data('wheat')

25

26@pytest.fixture

27 def 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

45 def 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

59 def 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

96 class 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

265 class 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