Coverage for wsimod/demo/create_oxford.py: 0%

181 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2024-01-11 16:39 +0000

1# -*- coding: utf-8 -*- 

2"""Created on Tue Nov 1 09:03:17 2022. 

3 

4@author: Barney 

5""" 

6import os 

7import re 

8 

9import pandas as pd 

10 

11from wsimod.arcs.arcs import Arc 

12from wsimod.core import constants 

13from wsimod.nodes.catchment import Catchment 

14from wsimod.nodes.demand import ResidentialDemand 

15from wsimod.nodes.land import Land 

16from wsimod.nodes.nodes import Node 

17from wsimod.nodes.sewer import Sewer 

18from wsimod.nodes.storage import Groundwater, Reservoir 

19from wsimod.nodes.waste import Waste 

20from wsimod.nodes.wtw import FWTW, WWTW 

21from wsimod.orchestration.model import Model 

22 

23# NOTE - these are only supplements to the demos in the documentation 

24# if you want to see demos, go to docs/demo/scripts! 

25 

26 

27def preprocess_oxford(data_dir=None): 

28 """ 

29 

30 Args: 

31 data_dir: 

32 """ 

33 # This function is used to create the timeseries data for the oxford demo 

34 if data_dir is None: 

35 data_dir = os.path.join( 

36 os.path.dirname( 

37 os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 

38 ), 

39 "demo", 

40 "data", 

41 ) 

42 

43 UG_L_TO_KG_M3 = 1e-6 

44 MG_L_TO_KG_M3 = 1e-3 

45 

46 q_lab = [ 

47 ("39008_gdf.csv", "thames"), 

48 ("39034_gdf.csv", "evenlode"), 

49 ("39021_gdf.csv", "cherwell"), 

50 ("39140_gdf.csv", "ray"), 

51 ] 

52 

53 flows = [] 

54 for fn, river in q_lab: 

55 df = pd.read_csv( 

56 os.path.join(data_dir, "raw", fn), skiprows=19, error_bad_lines=False 

57 ) 

58 if df.shape[1] == 2: 

59 df.columns = ["date", "flow"] 

60 else: 

61 df.columns = ["date", "flow", "missing"] 

62 df["site"] = river 

63 flows.append(df) 

64 

65 flows = pd.concat(flows) 

66 flows = flows.pivot(columns="site", index="date", values="flow") 

67 flows.index = pd.to_datetime(flows.index) 

68 

69 wq_data = pd.read_csv( 

70 os.path.join( 

71 data_dir, "raw", "CEHThamesInitiative_WaterQualityData_2009-2013.csv" 

72 ) 

73 ) 

74 

75 rain = pd.read_csv( 

76 os.path.join(data_dir, "raw", "39008_cdr.csv"), 

77 skiprows=19, 

78 error_bad_lines=False, 

79 ) 

80 rain.columns = ["date", "value", "misc"] 

81 rain["site"] = "oxford_land" 

82 rain["variable"] = "precipitation" 

83 rain = rain.drop("misc", axis=1) 

84 rain.date = pd.to_datetime(rain.date) 

85 

86 sites = { 

87 "River Thames at Swinford": "thames", 

88 "River Evenlode at Cassington Mill": "evenlode", 

89 "River Ray at Islip": "ray", 

90 "River Cherwell at Hampton Poyle": "cherwell", 

91 } 

92 wq = wq_data.loc[wq_data.Site.isin(sites.keys())] 

93 wq["Sampling date (dd/mm/yyyy)"] = pd.to_datetime( 

94 wq["Sampling date (dd/mm/yyyy)"], format="%d/%m/%Y" 

95 ) 

96 wq = wq.rename(columns={"Sampling date (dd/mm/yyyy)": "date", "Site": "site"}) 

97 

98 wq.site = [sites[x] for x in wq.site] 

99 

100 wq = wq.set_index("date").drop("Sampling time (hh:mm)", axis=1) 

101 wq[wq.columns.drop("site")] = wq[wq.columns.drop("site")].apply( 

102 lambda x: pd.to_numeric(x, errors="coerce") 

103 ) 

104 wq = wq.dropna(axis=1, how="any") 

105 wq = wq.drop("Mean daily river discharge (m3 s-1)", axis=1) 

106 wq = wq.groupby("site").resample("d").interpolate().drop("site", axis=1) 

107 wq = wq.reset_index() 

108 wq.loc[:, wq.columns.str.contains("ug")] *= UG_L_TO_KG_M3 

109 wq.loc[:, wq.columns.str.contains("mg")] *= MG_L_TO_KG_M3 

110 

111 columns = [] 

112 for pol in wq.columns.unique(): 

113 text = pol.lower() 

114 for sub in ["water", "dissolved", "total", " ", r"\(.*\)"]: 

115 text = re.sub(sub, "", text) 

116 columns.append(text) 

117 wq.columns = columns 

118 

119 # Convert to nitrate as N 

120 wq["nitrate"] /= 4.43 

121 

122 # Convert to Silica as SiO2 

123 wq["silicon"] *= 2.14 

124 

125 wq = wq.melt(id_vars=["site", "date"]) 

126 

127 # wq.date = pd.to_datetime(wq.date.dt.date) 

128 

129 flows = flows.loc[flows.index.isin(wq.date)] 

130 flows = flows.unstack().rename("value").reset_index() 

131 flows["variable"] = "flow" 

132 

133 rain = rain.loc[rain.date.isin(wq.date)] 

134 evaporation = create_timeseries(2 / 1000, rain.date, "et0") 

135 evaporation["site"] = "oxford_land" 

136 

137 temperature_ = ( 

138 wq.loc[wq.variable == "temperature"].groupby("date").mean().reset_index() 

139 ) 

140 temperature_["site"] = "oxford_land" 

141 temperature_["variable"] = "temperature" 

142 

143 input_data = pd.concat([wq, flows, rain, evaporation, temperature_], axis=0) 

144 input_data.to_csv( 

145 os.path.join(data_dir, "processed", "timeseries_data.csv"), index=False 

146 ) 

147 

148 

149def create_oxford_model(data_folder): 

150 """ 

151 

152 Args: 

153 data_folder: 

154 

155 Returns: 

156 

157 """ 

158 input_fid = os.path.join(data_folder, "processed", "timeseries_data.csv") 

159 input_data = pd.read_csv(input_fid) 

160 input_data.loc[input_data.variable == "flow", "value"] *= constants.M3_S_TO_M3_DT 

161 input_data.loc[input_data.variable == "precipitation", "value"] *= constants.MM_TO_M 

162 input_data.date = pd.to_datetime(input_data.date) 

163 data_input_dict = input_data.set_index(["variable", "date"]).value.to_dict() 

164 data_input_dict = ( 

165 input_data.groupby("site") 

166 .apply(lambda x: x.set_index(["variable", "date"]).value.to_dict()) 

167 .to_dict() 

168 ) 

169 dates = input_data.date.unique() 

170 dates = dates[dates.argsort()] 

171 dates = [pd.Timestamp(x) for x in dates] 

172 constants.POLLUTANTS = input_data.variable.unique().tolist() 

173 constants.POLLUTANTS.remove("flow") 

174 constants.POLLUTANTS.remove("precipitation") 

175 constants.POLLUTANTS.remove("et0") 

176 constants.NON_ADDITIVE_POLLUTANTS = ["temperature"] 

177 constants.ADDITIVE_POLLUTANTS = list( 

178 set(constants.POLLUTANTS).difference(constants.NON_ADDITIVE_POLLUTANTS) 

179 ) 

180 constants.FLOAT_ACCURACY = 1e-8 

181 thames_above_abingdon = Waste(name="thames_above_abingdon") 

182 farmoor_abstraction = Node(name="farmoor_abstraction") 

183 evenlode_thames = Node(name="evenlode_thames") 

184 cherwell_ray = Node(name="cherwell_ray") 

185 cherwell_thames = Node(name="cherwell_thames") 

186 thames_mixer = Node(name="thames_mixer") 

187 evenlode = Catchment(name="evenlode", data_input_dict=data_input_dict["evenlode"]) 

188 thames = Catchment(name="thames", data_input_dict=data_input_dict["thames"]) 

189 ray = Catchment(name="ray", data_input_dict=data_input_dict["ray"]) 

190 cherwell = Catchment(name="cherwell", data_input_dict=data_input_dict["cherwell"]) 

191 oxford_fwtw = FWTW( 

192 service_reservoir_storage_capacity=1e5, 

193 service_reservoir_storage_area=2e4, 

194 service_reservoir_initial_storage=0.9e5, 

195 treatment_throughput_capacity=4.5e4, 

196 name="oxford_fwtw", 

197 ) 

198 land_inputs = data_input_dict["oxford_land"] 

199 pollutant_deposition = { 

200 "boron": 100e-10, 

201 "calcium": 70e-7, 

202 "chloride": 60e-10, 

203 "fluoride": 0.2e-7, 

204 "magnesium": 6e-7, 

205 "nitrate": 2e-9, 

206 "nitrogen": 4e-7, 

207 "potassium": 7e-7, 

208 "silicon": 7e-9, 

209 "sodium": 30e-9, 

210 "sulphate": 70e-7, 

211 } 

212 surface = [ 

213 { 

214 "type_": "PerviousSurface", 

215 "area": 2e7, 

216 "pollutant_load": pollutant_deposition, 

217 "surface": "rural", 

218 "field_capacity": 0.3, 

219 "depth": 0.5, 

220 "initial_storage": 2e7 * 0.4 * 0.5, 

221 }, 

222 { 

223 "type_": "ImperviousSurface", 

224 "area": 1e7, 

225 "pollutant_load": pollutant_deposition, 

226 "surface": "urban", 

227 "initial_storage": 5e6, 

228 }, 

229 ] 

230 oxford_land = Land( 

231 surfaces=surface, name="oxford_land", data_input_dict=land_inputs 

232 ) 

233 oxford = ResidentialDemand( 

234 name="oxford", 

235 population=2e5, 

236 per_capita=0.15, 

237 pollutant_load={ 

238 "boron": 500 * constants.UG_L_TO_KG_M3 * 0.15, 

239 "calcium": 150 * constants.MG_L_TO_KG_M3 * 0.15, 

240 "chloride": 180 * constants.MG_L_TO_KG_M3 * 0.15, 

241 "fluoride": 0.4 * constants.MG_L_TO_KG_M3 * 0.15, 

242 "magnesium": 30 * constants.MG_L_TO_KG_M3 * 0.15, 

243 "nitrate": 60 * constants.MG_L_TO_KG_M3 * 0.15, 

244 "nitrogen": 50 * constants.MG_L_TO_KG_M3 * 0.15, 

245 "potassium": 30 * constants.MG_L_TO_KG_M3 * 0.15, 

246 "silicon": 20 * constants.MG_L_TO_KG_M3 * 0.15, 

247 "sodium": 200 * constants.MG_L_TO_KG_M3 * 0.15, 

248 "sulphate": 250 * constants.MG_L_TO_KG_M3 * 0.15, 

249 "temperature": 14, 

250 }, 

251 data_input_dict=land_inputs, 

252 ) 

253 farmoor = Reservoir( 

254 name="farmoor", capacity=1e7, initial_storage=1e7, area=1.5e6, datum=62 

255 ) 

256 distribution = Node(name="oxford_distribution") 

257 oxford_wwtw = WWTW( 

258 stormwater_storage_capacity=2e4, 

259 stormwater_storage_area=2e4, 

260 treatment_throughput_capacity=5e4, 

261 name="oxford_wwtw", 

262 ) 

263 combined_sewer = Sewer( 

264 capacity=4e6, pipe_timearea={0: 0.8, 1: 0.15, 2: 0.05}, name="combined_sewer" 

265 ) 

266 gw = Groundwater(capacity=3.2e9, area=3.2e8, name="gw", residence_time=20) 

267 nodelist = [ 

268 thames_above_abingdon, 

269 evenlode, 

270 thames, 

271 ray, 

272 cherwell, 

273 oxford, 

274 distribution, 

275 farmoor, 

276 oxford_fwtw, 

277 oxford_wwtw, 

278 combined_sewer, 

279 oxford_land, 

280 gw, 

281 farmoor_abstraction, 

282 evenlode_thames, 

283 cherwell_ray, 

284 cherwell_thames, 

285 thames_mixer, 

286 ] 

287 fwtw_to_distribution = Arc( 

288 in_port=oxford_fwtw, out_port=distribution, name="fwtw_to_distribution" 

289 ) 

290 abstraction_to_farmoor = Arc( 

291 in_port=farmoor_abstraction, 

292 out_port=farmoor, 

293 name="abstraction_to_farmoor", 

294 capacity=5e4, 

295 ) 

296 sewer_to_wwtw = Arc( 

297 in_port=combined_sewer, 

298 out_port=oxford_wwtw, 

299 preference=1e10, 

300 name="sewer_to_wwtw", 

301 ) 

302 sewer_overflow = Arc( 

303 in_port=combined_sewer, 

304 out_port=thames_mixer, 

305 preference=1e-10, 

306 name="sewer_overflow", 

307 ) 

308 evenlode_to_thames = Arc( 

309 in_port=evenlode, out_port=evenlode_thames, name="evenlode_to_thames" 

310 ) 

311 

312 thames_to_thames = Arc( 

313 in_port=thames, out_port=evenlode_thames, name="thames_to_thames" 

314 ) 

315 

316 ray_to_cherwell = Arc(in_port=ray, out_port=cherwell_ray, name="ray_to_cherwell") 

317 

318 cherwell_to_cherwell = Arc( 

319 in_port=cherwell, out_port=cherwell_ray, name="cherwell_to_cherwell" 

320 ) 

321 

322 thames_to_farmoor = Arc( 

323 in_port=evenlode_thames, out_port=farmoor_abstraction, name="thames_to_farmoor" 

324 ) 

325 

326 farmoor_to_mixer = Arc( 

327 in_port=farmoor_abstraction, out_port=thames_mixer, name="farmoor_to_mixer" 

328 ) 

329 

330 cherwell_to_mixer = Arc( 

331 in_port=cherwell_ray, out_port=thames_mixer, name="cherwell_to_mixer" 

332 ) 

333 

334 wwtw_to_mixer = Arc( 

335 in_port=oxford_wwtw, out_port=thames_mixer, name="wwtw_to_mixer" 

336 ) 

337 

338 mixer_to_waste = Arc( 

339 in_port=thames_mixer, out_port=thames_above_abingdon, name="mixer_to_waste" 

340 ) 

341 

342 distribution_to_demand = Arc( 

343 in_port=distribution, out_port=oxford, name="distribution_to_demand" 

344 ) 

345 

346 reservoir_to_fwtw = Arc( 

347 in_port=farmoor, out_port=oxford_fwtw, name="reservoir_to_fwtw" 

348 ) 

349 

350 fwtw_to_sewer = Arc( 

351 in_port=oxford_fwtw, out_port=combined_sewer, name="fwtw_to_sewer" 

352 ) 

353 

354 demand_to_sewer = Arc( 

355 in_port=oxford, out_port=combined_sewer, name="demand_to_sewer" 

356 ) 

357 

358 land_to_sewer = Arc( 

359 in_port=oxford_land, out_port=combined_sewer, name="land_to_sewer" 

360 ) 

361 

362 land_to_gw = Arc(in_port=oxford_land, out_port=gw, name="land_to_gw") 

363 

364 garden_to_gw = Arc(in_port=oxford, out_port=gw, name="garden_to_gw") 

365 

366 gw_to_mixer = Arc(in_port=gw, out_port=thames_mixer, name="gw_to_mixer") 

367 arclist = [ 

368 evenlode_to_thames, 

369 thames_to_thames, 

370 ray_to_cherwell, 

371 cherwell_to_cherwell, 

372 thames_to_farmoor, 

373 farmoor_to_mixer, 

374 cherwell_to_mixer, 

375 wwtw_to_mixer, 

376 sewer_overflow, 

377 mixer_to_waste, 

378 abstraction_to_farmoor, 

379 distribution_to_demand, 

380 demand_to_sewer, 

381 land_to_sewer, 

382 sewer_to_wwtw, 

383 fwtw_to_sewer, 

384 fwtw_to_distribution, 

385 reservoir_to_fwtw, 

386 land_to_gw, 

387 garden_to_gw, 

388 gw_to_mixer, 

389 ] 

390 my_model = Model() 

391 my_model.add_instantiated_nodes(nodelist) 

392 my_model.add_instantiated_arcs(arclist) 

393 my_model.dates = dates 

394 return my_model 

395 

396 

397def pull_check_mrf(node, vqip=None): 

398 """ 

399 

400 Args: 

401 node: 

402 vqip: 

403 

404 Returns: 

405 

406 """ 

407 reply = node.pull_check_basic() 

408 reply["volume"] = max( 

409 reply["volume"] - (node.mrf - node.mrf_satisfied_this_timestep), 0 

410 ) 

411 if vqip is not None: 

412 reply["volume"] = min(reply["volume"], vqip["volume"]) 

413 return reply 

414 

415 

416def pull_set_mrf(node, vqip): 

417 """ 

418 

419 Args: 

420 node: 

421 vqip: 

422 

423 Returns: 

424 

425 """ 

426 request = node.copy_vqip(vqip) 

427 request["volume"] += node.mrf - node.mrf_satisfied_this_timestep 

428 reply = node.pull_distributed(request) 

429 reply_to_mrf = min((node.mrf - node.mrf_satisfied_this_timestep), reply["volume"]) 

430 node.mrf_satisfied_this_timestep += reply_to_mrf 

431 reply_to_mrf = node.v_change_vqip(reply, reply_to_mrf) 

432 reply = node.extract_vqip(reply, reply_to_mrf) 

433 mrf_route_reply = node.push_distributed(reply_to_mrf, of_type=["Node"]) 

434 if mrf_route_reply["volume"] > constants.FLOAT_ACCURACY: 

435 print("warning MRF not able to push") 

436 return reply 

437 

438 

439def push_set_mrf(node, vqip): 

440 """ 

441 

442 Args: 

443 node: 

444 vqip: 

445 

446 Returns: 

447 

448 """ 

449 reply = node.push_distributed(vqip) 

450 total_pushed_downstream = vqip["volume"] - reply["volume"] 

451 node.mrf_satisfied_this_timestep = min( 

452 node.mrf, node.mrf_satisfied_this_timestep + total_pushed_downstream 

453 ) 

454 return reply 

455 

456 

457def end_timestep(node): 

458 """ 

459 

460 Args: 

461 node: 

462 """ 

463 node.mrf_satisfied_this_timestep = 0 

464 

465 

466def convert_to_mrf(node, mrf=5): 

467 """ 

468 

469 Args: 

470 node: 

471 mrf: 

472 """ 

473 node.mrf = mrf 

474 node.mrf_satisfied_this_timestep = 0 

475 node.pull_set_handler["default"] = lambda x: pull_set_mrf(node, x) 

476 node.pull_check_handler["default"] = lambda x: pull_check_mrf(node, x) 

477 node.end_timestep = lambda: end_timestep(node) 

478 

479 

480def create_oxford_model_mrf(data_folder): 

481 """ 

482 

483 Args: 

484 data_folder: 

485 

486 Returns: 

487 

488 """ 

489 my_model = create_oxford_model(data_folder) 

490 convert_to_mrf(my_model.nodes["farmoor_abstraction"], mrf=3 * 86400) 

491 return my_model 

492 

493 

494def create_timeseries(amount, dates, variable): 

495 """Create a timeseries with a constant value, formatted as a dataframe. 

496 

497 Args: 

498 amount (float): Constant value to be applied over the timeseries 

499 dates (list): list or iterable of dates to be generated 

500 variable (str): String to store in the 'variable' column of the dataframe 

501 

502 Returns: 

503 (DataFrame): the formatted dataframe 

504 """ 

505 df = pd.DataFrame(columns=["date", "variable", "value"]) 

506 df["date"] = dates 

507 df["variable"] = variable 

508 df["value"] = amount 

509 return df