cetk 0.0.4.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. cetk/__init__.py +87 -0
  2. cetk/db.py +41 -0
  3. cetk/edb/__init__.py +0 -0
  4. cetk/edb/apps.py +9 -0
  5. cetk/edb/cache.py +249 -0
  6. cetk/edb/const.py +26 -0
  7. cetk/edb/copy.py +18 -0
  8. cetk/edb/exporters.py +551 -0
  9. cetk/edb/importers/__init__.py +36 -0
  10. cetk/edb/importers/activity_import.py +279 -0
  11. cetk/edb/importers/codeset_import.py +124 -0
  12. cetk/edb/importers/gridsource_import.py +256 -0
  13. cetk/edb/importers/roadsource_import.py +1602 -0
  14. cetk/edb/importers/source_import.py +958 -0
  15. cetk/edb/importers/timevar_import.py +115 -0
  16. cetk/edb/importers/utils.py +92 -0
  17. cetk/edb/importers/validation.py +207 -0
  18. cetk/edb/ltreefield.py +76 -0
  19. cetk/edb/migrations/0001_initial.py +1731 -0
  20. cetk/edb/migrations/0002_data.py +156 -0
  21. cetk/edb/migrations/0003_create_gpkg_tables.py +14 -0
  22. cetk/edb/migrations/__init__.py +0 -0
  23. cetk/edb/models/__init__.py +49 -0
  24. cetk/edb/models/base.py +52 -0
  25. cetk/edb/models/common_models.py +144 -0
  26. cetk/edb/models/fleets.py +160 -0
  27. cetk/edb/models/gridsource_models.py +163 -0
  28. cetk/edb/models/road_classes.py +197 -0
  29. cetk/edb/models/road_models.py +413 -0
  30. cetk/edb/models/source_models.py +399 -0
  31. cetk/edb/models/timevar_models.py +111 -0
  32. cetk/edb/rasterize/__init__.py +1 -0
  33. cetk/edb/rasterize/rasterizer.py +1544 -0
  34. cetk/edb/signals.py +43 -0
  35. cetk/edb/traffic.py +75 -0
  36. cetk/edb/units.py +160 -0
  37. cetk/emissions/__init__.py +0 -0
  38. cetk/emissions/aggregate_emissions.sql +303 -0
  39. cetk/emissions/areasource_emissions.sql +53 -0
  40. cetk/emissions/calc.py +148 -0
  41. cetk/emissions/filters.py +179 -0
  42. cetk/emissions/gridsource_emissions.sql +52 -0
  43. cetk/emissions/pointsource_emissions.sql +53 -0
  44. cetk/emissions/queries.py +221 -0
  45. cetk/emissions/roadsource_emissions.sql +211 -0
  46. cetk/emissions/used_substances.sql +56 -0
  47. cetk/emissions/views.py +89 -0
  48. cetk/logging.py +68 -0
  49. cetk/settings.py +85 -0
  50. cetk/tools/__init__.py +1 -0
  51. cetk/tools/cetk_command.py +599 -0
  52. cetk/tools/manage.py +24 -0
  53. cetk/tools/utils.py +334 -0
  54. cetk/utils.py +62 -0
  55. cetk-0.0.4.dev0.dist-info/LICENSE +201 -0
  56. cetk-0.0.4.dev0.dist-info/METADATA +297 -0
  57. cetk-0.0.4.dev0.dist-info/RECORD +60 -0
  58. cetk-0.0.4.dev0.dist-info/WHEEL +5 -0
  59. cetk-0.0.4.dev0.dist-info/entry_points.txt +3 -0
  60. cetk-0.0.4.dev0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1602 @@
1
+ import copy
2
+ import logging
3
+ import os
4
+ from collections import OrderedDict
5
+ from operator import itemgetter
6
+ from pathlib import Path
7
+
8
+ # need to import fiona before geopandas due to gpd bug causing circular imports.
9
+ import fiona # noqa
10
+ import geopandas as gpd
11
+ import numpy as np # noqa
12
+ import pandas as pd
13
+ from django.contrib.gis.gdal import CoordTransform # noqa
14
+ from django.contrib.gis.gdal import SpatialReference
15
+ from django.contrib.gis.gdal.geometries import LineString as GDALLineString
16
+ from django.contrib.gis.geos import Point, Polygon # noqa
17
+ from django.core.exceptions import ObjectDoesNotExist, ValidationError # noqa
18
+ from django.core.management.base import CommandError # noqa
19
+ from django.db import IntegrityError # noqa
20
+ from openpyxl import load_workbook
21
+
22
+ from cetk.edb.const import WGS84_SRID
23
+ from cetk.edb.models import ColdstartTimevar # noqa
24
+ from cetk.edb.models import (
25
+ CodeSet,
26
+ CongestionProfile,
27
+ Fleet,
28
+ FleetMemberFuel,
29
+ FlowTimevar,
30
+ PrefetchRoadClassAttributes,
31
+ RoadAttribute,
32
+ RoadClass,
33
+ RoadSource,
34
+ Substance,
35
+ TrafficSituation,
36
+ Vehicle,
37
+ VehicleEF,
38
+ VehicleFuel,
39
+ VehicleFuelComb,
40
+ get_valid_road_attribute_values,
41
+ )
42
+ from cetk.edb.units import emission_unit_to_si, vehicle_ef_unit_to_si # noqa
43
+ from cetk.utils import inbatch
44
+
45
+ from .timevar_import import import_timevarsheet
46
+ from .utils import import_error, worksheet_to_dataframe
47
+
48
+ log = logging.getLogger(__name__)
49
+
50
+
51
+ STATIC_ROAD_ATTRIBUTES = [
52
+ "aadt",
53
+ "nolanes",
54
+ "name",
55
+ "speed",
56
+ "width",
57
+ "median_strip_width",
58
+ "heavy_vehicle_share",
59
+ "slope",
60
+ ]
61
+
62
+
63
+ def handle_msg(messages, msg, *, fail_early=False):
64
+ """handle repeated error without bloating stderr/stdout.
65
+
66
+ args
67
+ messages: dict where messages are accumulated
68
+ msg: message string
69
+ fail_early: exit directly
70
+ """
71
+
72
+ if fail_early:
73
+ raise ImportError(msg)
74
+
75
+ if msg not in messages:
76
+ log.debug(f"debug: {msg}")
77
+ messages[msg] = 1
78
+ else:
79
+ messages[msg] += 1
80
+
81
+
82
+ def filter_out(feature, exclude):
83
+ """filter roads by attribute."""
84
+
85
+ for attr_name, val in exclude.items():
86
+ if isinstance(val, (str, int, float)):
87
+ if feature.get(attr_name) != val:
88
+ return False
89
+ elif str(feature.get(attr_name)) not in list(val):
90
+ return False
91
+ return True
92
+
93
+
94
+ def vehicles_excel_to_dict(file_path):
95
+ dtype_dict = {"name": str, "isheavy": bool, "info": str, "fuel": str}
96
+ for cs in CodeSet.objects.all():
97
+ dtype_dict["activitycode_" + cs.slug] = str
98
+ df = pd.read_excel(file_path, sheet_name="VehicleFuel", dtype=dtype_dict).fillna("")
99
+
100
+ # Extract the code sets from the DataFrame
101
+ activity_columns = [col for col in df.columns if col.startswith("activitycode_")]
102
+ code_sets = {
103
+ f"code_set{i+1}": col.split("_")[1] for i, col in enumerate(activity_columns)
104
+ }
105
+
106
+ data = {**code_sets, "vehicles": []}
107
+ vehicles = []
108
+ for index, row in df.iterrows():
109
+ activitycodes = {}
110
+ for col in activity_columns:
111
+ ac_id = CodeSet.objects.get(slug=col.replace("activitycode_", "")).id
112
+ activitycodes[f"activitycode{ac_id}"] = row[col]
113
+ # Check if the vehicle already exists in the data dictionary
114
+ if row["name"] in vehicles:
115
+ vehicle = next(
116
+ (
117
+ vehicle
118
+ for vehicle in data["vehicles"]
119
+ if vehicle["name"] == row["name"]
120
+ ),
121
+ None,
122
+ )
123
+ # vehicle exists, set fuel type
124
+ # first check that fuel type didn't exist yet
125
+ vehicle["fuels"][row["fuel"]] = activitycodes
126
+ else:
127
+ vehicle_data = {
128
+ "name": row["name"],
129
+ "isheavy": row["isheavy"],
130
+ "info": row["info"],
131
+ "fuels": {},
132
+ }
133
+ vehicle_data["fuels"][row["fuel"]] = activitycodes
134
+ data["vehicles"].append(vehicle_data)
135
+ vehicles.append(row["name"])
136
+ return data
137
+
138
+
139
+ def roadclass_excel_to_dict(file_path, validation=False):
140
+ msg = []
141
+ df = pd.read_excel(file_path, sheet_name="RoadAttribute")
142
+ df_values = pd.read_excel(file_path, sheet_name="TrafficSituation", dtype=str)
143
+ data = {"attributes": []}
144
+ for index, row in df.iterrows():
145
+ try:
146
+ unique_values = list(set(df_values["attr:" + row["slug"]]))
147
+ data["attributes"].append(
148
+ {"name": row["name"], "slug": row["slug"], "values": unique_values}
149
+ )
150
+ except KeyError:
151
+ msg = import_error(
152
+ f"Attribute not found in TrafficSituation sheet: {row['slug']}",
153
+ validation=validation,
154
+ )
155
+ return data, msg
156
+
157
+
158
+ def fleet_excel_to_dict(file_path, validation=False):
159
+ df = pd.read_excel(file_path, sheet_name="Fleet")
160
+ data = {}
161
+ fuel_columns = [col for col in df.columns if col.startswith("fuel:")]
162
+ msg = []
163
+ for index, row in df.iterrows():
164
+ fuels = {}
165
+ for i, col in enumerate(fuel_columns):
166
+ if row[col] > 0:
167
+ fuels[col[5:]] = row[col]
168
+ vehicle_data = {
169
+ "fraction": row["vehicle_fraction"],
170
+ "coldstart_fraction": row["coldstart_fraction"],
171
+ "timevar": row["flow_timevar"],
172
+ "coldstart_timevar": row["coldstart_timevar"],
173
+ "fuels": fuels,
174
+ }
175
+ if row["name"] in data.keys():
176
+ if row["vehicle"] in data[row["name"]]["vehicles"].keys():
177
+ msg = import_error(
178
+ f"duplicated vehicle {row['vehicle']} for fleet {row['name']}",
179
+ validation=validation,
180
+ )
181
+ else:
182
+ data[row["name"]]["vehicles"][row["vehicle"]] = vehicle_data
183
+ else:
184
+ data[row["name"]] = {
185
+ "default_heavy_vehicle_share": row["default_heavy_vehicle_share"],
186
+ "vehicles": {row["vehicle"]: vehicle_data},
187
+ }
188
+ return data, msg
189
+
190
+
191
+ def roadsource_excel_to_dict(file_path):
192
+ df = pd.read_excel(file_path, sheet_name="RoadSource")
193
+ attribute_columns = [col for col in df.columns if col.startswith("attr:")]
194
+ attributes = {col[5:]: str(df[col].values[0]) for col in attribute_columns}
195
+ tag_columns = [col for col in df.columns if col.startswith("tag:")]
196
+ tags = {col[4:]: str(df[col].values[0]) for col in tag_columns}
197
+ data = {
198
+ col: str(df[col].values[0])
199
+ for col in df.columns
200
+ if not (
201
+ col.startswith("attr:")
202
+ or col.startswith("tag:")
203
+ or pd.isnull(df[col].values[0])
204
+ )
205
+ }
206
+ data["roadclass"] = attributes
207
+ data["tags"] = tags
208
+ return data
209
+
210
+
211
+ def import_traffic(filename, sheets, validation=False):
212
+ # in case the excel file is a big file, using workbook as argument instead
213
+ # of filename would be a good way to optimize import
214
+ workbook = load_workbook(filename=filename, data_only=True, read_only=True)
215
+ return_dict = {}
216
+ return_message = []
217
+ if "VehicleFuel" in sheets:
218
+ vehiclesettings = vehicles_excel_to_dict(filename)
219
+ elif "VehicleEmissionFactor" in sheets:
220
+ log.error(
221
+ "Cannot import or validate vehicle emfacs if VehicleFuel "
222
+ "is not set in the same file."
223
+ )
224
+ raise ImportError
225
+ if "VehicleEmissionFactor" in sheets:
226
+ df = worksheet_to_dataframe(workbook["VehicleEmissionFactor"].values)
227
+ unit = set(df["unit"])
228
+ if len(unit) > 1:
229
+ return_message.append(
230
+ import_error(
231
+ "Several units found for import " f"{unit}, can only use one",
232
+ validation=validation,
233
+ )
234
+ )
235
+ unit = str(list(unit)[0])
236
+ updates = import_vehicles(
237
+ filename,
238
+ vehiclesettings,
239
+ unit=unit, # "kg/m"
240
+ encoding="utf-8",
241
+ overwrite=True,
242
+ validation=validation,
243
+ )
244
+ return_dict.update(updates)
245
+ if ("RoadAttribute" in sheets) and ("TrafficSituation" in sheets):
246
+ roadclass_settings, msg = roadclass_excel_to_dict(filename, validation)
247
+ return_message += msg
248
+ import_roadclasses(
249
+ filename,
250
+ roadclass_settings,
251
+ encoding="utf-8",
252
+ overwrite=True,
253
+ validation=validation,
254
+ )
255
+ elif ("RoadAttribute" in sheets) and ("TrafficSituation" in sheets):
256
+ raise ImportError(
257
+ "Have to import TrafficSituation and RoadAttribute simultaneously."
258
+ )
259
+ if "CongestionProfile" in sheets:
260
+ _, msg = import_congestionsheet(workbook, validation=validation)
261
+ return_message += msg
262
+ if "FlowTimevar" in sheets:
263
+ _, msg = import_timevarsheet(workbook, validation, sheetname="FlowTimevar")
264
+ return_message += msg
265
+ if "ColdstartTimevar" in sheets:
266
+ _, msg = import_timevarsheet(workbook, validation, sheetname="ColdstartTimevar")
267
+ return_message += msg
268
+ if "Fleet" in sheets:
269
+ try:
270
+ fleet_data, msg = fleet_excel_to_dict(filename, validation)
271
+ return_message += msg
272
+ _, msg = import_fleets(fleet_data, overwrite=True, validation=validation)
273
+ return_message += msg
274
+ except ImportError as err:
275
+ return {}, [f"{err}"]
276
+ if "RoadSource" in sheets:
277
+ try:
278
+ config = roadsource_excel_to_dict(filename)
279
+ roadfile_path = config["filepath"]
280
+ if not os.path.isabs(roadfile_path):
281
+ datadir = os.path.dirname(filename)
282
+ roadfile_path = os.path.join(datadir, roadfile_path)
283
+ updates, msg = import_roads(roadfile_path, config, validation=validation)
284
+ return_dict.update(updates)
285
+ return_message += msg
286
+ except ImportError as err:
287
+ return {}, [f"{err}"]
288
+ workbook.close()
289
+ return return_dict, return_message # update dict and messages
290
+
291
+
292
+ def import_vehicles( # noqa: C901, PLR0912, PLR0915
293
+ vehicles_file,
294
+ config,
295
+ *,
296
+ only_ef=False,
297
+ overwrite=False,
298
+ validation=False,
299
+ unit="mg/km",
300
+ encoding="utf-8",
301
+ ):
302
+ """Import vehicles, fuels and traffic situations to ef-set.
303
+
304
+ args
305
+ vehicles_file: a csv-file or xlsx-file with vehicles and emission-factors
306
+ config: a dict with definitions of vehicle, fuels and codes
307
+
308
+ optional
309
+ only_ef: if True, only load emission factors - do not modify base-set tables
310
+ overwrite: if True, existing instances will be ovewritten
311
+ unit: unit of emission-factors to import, default is "mg/km"
312
+ """
313
+
314
+ # cache valid activity codes
315
+ valid_codes = OrderedDict()
316
+ code_sets = [None, None, None]
317
+ config = copy.deepcopy(config)
318
+ return_message = []
319
+
320
+ # cache activity-codes for code-sets specified in config file
321
+ for i in range(3):
322
+ code_nr = i + 1
323
+ code_sets[i] = config.pop(f"code_set{code_nr}", None)
324
+
325
+ if code_sets[i] is not None:
326
+ try:
327
+ code_sets[i] = CodeSet.objects.get(slug=code_sets[i])
328
+ except ObjectDoesNotExist:
329
+ return_message.append(
330
+ import_error(
331
+ f"Invalid codeset slug: {code_sets[i]}",
332
+ validation=validation,
333
+ )
334
+ )
335
+
336
+ valid_codes[code_sets[i].slug] = {
337
+ ac.code: ac for ac in code_sets[i].codes.all()
338
+ }
339
+
340
+ def validate_ac(code_data, valid_codes, validation=False, return_message=[]):
341
+ if "activitycode1" in code_data and len(valid_codes) == 0:
342
+ return_message.append(
343
+ import_error(
344
+ "no activity codes defined, but codes given for vehicle",
345
+ validation=validation,
346
+ )
347
+ )
348
+
349
+ for code_set_name, codes in valid_codes.items():
350
+ code_nr = CodeSet.objects.get(slug=code_set_name).id
351
+ try:
352
+ ac = code_data[f"activitycode{code_nr}"]
353
+ except KeyError:
354
+ return_message.append(
355
+ import_error(
356
+ f"no value specified for activity code {code_nr}"
357
+ f" ({code_set_name}) of vehicle/fuel combination "
358
+ f"'{vehicle_name}' - '{fuel_name}'",
359
+ validation=validation,
360
+ )
361
+ )
362
+ if ac not in codes and ac != "":
363
+ return_message.append(
364
+ import_error(
365
+ f"invalid value '{ac}' for activity code "
366
+ f"{code_nr} ({code_set_name})"
367
+ f" of vehicle/fuel combination "
368
+ f"'{vehicle_name}' - '{fuel_name}'",
369
+ validation=validation,
370
+ )
371
+ )
372
+ code_nr += 1
373
+ return return_message
374
+
375
+ try:
376
+ mass_unit, length_unit = unit.split("/")
377
+ except ValueError:
378
+ log.error(f"invalid emission-factor unit {unit} specified in config-file")
379
+ raise
380
+ log.debug(f"emission factor units is: {unit}")
381
+
382
+ messages = {}
383
+ with Path(vehicles_file).open(encoding=encoding) as veh_file:
384
+ log.debug("reading emission-factor table")
385
+ dtype_dict = {
386
+ "vehicle": str,
387
+ "fuel": str,
388
+ "traffic_situation": str,
389
+ "substance": str,
390
+ "freeflow": float,
391
+ "heavy": float,
392
+ "saturated": float,
393
+ "stopngo": float,
394
+ "coldstart": float,
395
+ }
396
+ if Path(vehicles_file).suffix == ".csv":
397
+ df = pd.read_csv(
398
+ veh_file,
399
+ sep=";",
400
+ dtype=dtype_dict,
401
+ )
402
+ elif Path(vehicles_file).suffix == ".xlsx":
403
+ df = pd.read_excel(
404
+ vehicles_file, dtype=dtype_dict, sheet_name="VehicleEmissionFactor"
405
+ )
406
+ else:
407
+ raise ValueError(
408
+ "File for import vehicle emissions factors should be " + ".csv or .xlsx"
409
+ )
410
+ # indexes are set after reading csv in order to allow
411
+ # specification of dtypes for index columns
412
+ try:
413
+ df = df.set_index(["vehicle", "fuel", "traffic_situation", "substance"])
414
+ except KeyError as err:
415
+ raise ImportError(f"Invalid csv-file: {err}")
416
+
417
+ for col in ("freeflow", "heavy", "saturated", "stopngo", "coldstart"):
418
+ if col not in df.columns:
419
+ raise ImportError(
420
+ f"Required column '{col}' not found in file '{vehicles_file}"
421
+ )
422
+
423
+ log.debug("checking that all substances exist in the database")
424
+ substances = {}
425
+ for subst in df.index.get_level_values(3).unique():
426
+ try:
427
+ substances[subst] = Substance.objects.get(slug=subst)
428
+ except ObjectDoesNotExist:
429
+ msg = f"substance {subst} does not exist in database"
430
+ return_message.append(import_error(msg), validation=validation)
431
+
432
+ # create vehicles
433
+ vehicle_defs = config.get("vehicles", [])
434
+ if len(vehicle_defs) > 0:
435
+ log.debug("processing vehicles")
436
+ df_vehicles = df.index.get_level_values(0).unique()
437
+ for veh in vehicle_defs:
438
+ veh_tmp = copy.deepcopy(veh)
439
+ fuel_defs = veh_tmp.pop("fuels", {})
440
+ try:
441
+ vehicle_name = veh_tmp.pop("name")
442
+ if overwrite and not only_ef:
443
+ (_, created,) = Vehicle.objects.update_or_create(
444
+ name=vehicle_name, defaults=veh_tmp
445
+ )
446
+ if created:
447
+ log.debug(f"created vehicle {vehicle_name}")
448
+ elif not only_ef:
449
+ try:
450
+ (_, created) = Vehicle.objects.get_or_create(
451
+ name=vehicle_name, defaults=veh_tmp
452
+ )
453
+ except IntegrityError:
454
+ return_message.append(
455
+ import_error(
456
+ "either duplicate specification or vehicle "
457
+ f"'{vehicle_name}' already exists.",
458
+ validation=validation,
459
+ )
460
+ )
461
+ if created:
462
+ log.debug(f"created vehicle {vehicle_name}")
463
+ elif not Vehicle.objects.filter(name=vehicle_name).exists():
464
+ return_message.append(
465
+ import_error( # noqa: TRY301
466
+ f"vehicle '{vehicle_name}' does not exist ",
467
+ validation=validation,
468
+ )
469
+ )
470
+ except Exception as err:
471
+ return_message.append(
472
+ import_error(
473
+ f"invalid specification of vehicle in config-file: {err}",
474
+ validation=validation,
475
+ )
476
+ )
477
+ if vehicle_name not in df_vehicles:
478
+ log.warning(
479
+ "warning: no emission-factors specified for vehicle"
480
+ f" '{vehicle_name}'",
481
+ )
482
+
483
+ vehicles = {veh.name: veh for veh in Vehicle.objects.all()}
484
+
485
+ for fuel_name, code_data in fuel_defs.items():
486
+ # fuel model only has a name, so overwrite/updating is not relevant
487
+
488
+ if not only_ef:
489
+ # allow modification of base-set tables
490
+ fuel, _ = VehicleFuel.objects.get_or_create(name=fuel_name)
491
+ else:
492
+ # only verify that fuel exist
493
+ try:
494
+ fuel = VehicleFuel.objects.get(name=fuel_name)
495
+ except ObjectDoesNotExist:
496
+ return_message.append(
497
+ import_error(
498
+ f"fuel {fuel_name} does not exist in database",
499
+ validation=validation,
500
+ )
501
+ )
502
+ return_message.append(
503
+ validate_ac(code_data, valid_codes, validation=validation)
504
+ )
505
+
506
+ # get activity code model instances for each activity code
507
+ ac_codes = [None, None, None]
508
+ for i in range(3):
509
+ code_nr = i + 1
510
+ ac_codes[i] = code_data.get(f"activitycode{code_nr}", None)
511
+ if ac_codes[i] is not None and ac_codes[i] != "":
512
+ try:
513
+ ac_codes[i] = valid_codes[
514
+ CodeSet.objects.get(id=code_nr).slug
515
+ ][ac_codes[i]]
516
+ except KeyError:
517
+ return_message.append(
518
+ import_error(
519
+ f"Found invalid activitycode {ac_codes[i]}",
520
+ validation=validation,
521
+ )
522
+ )
523
+ else:
524
+ ac_codes[i] = None
525
+
526
+ if overwrite and not only_ef:
527
+ _, created = VehicleFuelComb.objects.update_or_create(
528
+ vehicle=vehicles[vehicle_name],
529
+ fuel=fuel,
530
+ defaults={
531
+ "activitycode1": ac_codes[0],
532
+ "activitycode2": ac_codes[1],
533
+ "activitycode3": ac_codes[2],
534
+ },
535
+ )
536
+ if created:
537
+ log.debug(
538
+ "created vehicle-fuel combination "
539
+ f"'{vehicle_name}' - '{fuel_name}'"
540
+ )
541
+
542
+ elif not only_ef:
543
+ VehicleFuelComb.objects.get_or_create(
544
+ vehicle=vehicles[vehicle_name],
545
+ fuel=fuel,
546
+ defaults={
547
+ "activitycode1": ac_codes[0],
548
+ "activitycode2": ac_codes[1],
549
+ "activitycode3": ac_codes[2],
550
+ },
551
+ )
552
+ elif not VehicleFuelComb.objects.filter(
553
+ vehicle=vehicles[vehicle_name], fuel=fuel
554
+ ).exists():
555
+ return_message.append(
556
+ import_error(
557
+ "vehicle/fuel combination "
558
+ f"'{vehicle_name}' - '{fuel_name}' "
559
+ "does not exist.",
560
+ validation=validation,
561
+ )
562
+ )
563
+ fuels = {fuel.name: fuel for fuel in VehicleFuel.objects.all()}
564
+ veh_fuel_combs = {
565
+ (comb.vehicle.name, comb.fuel.name)
566
+ for comb in VehicleFuelComb.objects.all().select_related("vehicle", "fuel")
567
+ }
568
+
569
+ # check that all combinations of vehicle/fuel in ef-table exist in db
570
+ for vehicle_name, fuel_name in {row[:2] for row in df.index}:
571
+ if not VehicleFuelComb.objects.filter(
572
+ fuel__name=fuel_name, vehicle__name=vehicle_name
573
+ ).exists():
574
+ msg = (
575
+ f"emission-factors for undefined vehicle/fuel combination "
576
+ f"'{vehicle_name}' - '{fuel_name}' will not be loaded."
577
+ )
578
+ if msg not in messages:
579
+ messages[msg] = 1
580
+ else:
581
+ messages[msg] += 1
582
+
583
+ # create traffic situations
584
+ log.debug("creating traffic-situations")
585
+ existing_traffic_situations = {
586
+ ts.ts_id: ts for ts in TrafficSituation.objects.all()
587
+ }
588
+
589
+ # check if there are any new traffic-situations in ef table
590
+ traffic_situations = []
591
+ for ts_id in df.index.get_level_values(2).unique():
592
+ if ts_id not in existing_traffic_situations:
593
+ # traffic-situations are defined by ts_id only
594
+ # this means overwriting/updating is not relevant
595
+ if only_ef:
596
+ return_message.append(
597
+ import_error(
598
+ f"traffic-situation {ts_id} does not exist.",
599
+ validation=validation,
600
+ )
601
+ )
602
+ traffic_situations.append(TrafficSituation(ts_id=ts_id))
603
+ # create any new traffic-situations
604
+ if len(traffic_situations) > 0:
605
+ TrafficSituation.objects.bulk_create(traffic_situations)
606
+
607
+ # update look-up dict for traffic-situations
608
+ updated_traffic_situations = {ts.ts_id: ts for ts in TrafficSituation.objects.all()}
609
+
610
+ # create/update all vehicle emission factors
611
+ log.debug("creating/updating emission factors")
612
+
613
+ # get all pre-existing emission factors in ef-set
614
+ # store in dict with (veh, fuel, ts, subst) as keys
615
+ # and instance id as values
616
+ existing_ef_keys = {
617
+ vals[1:]: vals[0]
618
+ for vals in VehicleEF.objects.all().values_list(
619
+ "id",
620
+ "vehicle__name",
621
+ "fuel__name",
622
+ "traffic_situation__ts_id",
623
+ "substance__slug",
624
+ )
625
+ }
626
+
627
+ efs_to_create = []
628
+ efs_to_update = []
629
+ for index, row in df.iterrows():
630
+ vehicle_name, fuel_name, ts_id, subst_slug = index
631
+ valid_ef = True
632
+
633
+ # check if ef already exists
634
+ key = (vehicle_name, fuel_name, ts_id, subst_slug)
635
+ traffic_situation = updated_traffic_situations[ts_id]
636
+
637
+ try:
638
+ vehicle = vehicles[vehicle_name]
639
+ except KeyError:
640
+ msg = f"undefined vehicle '{vehicle_name}' found in emission factor table"
641
+ valid_ef = False
642
+
643
+ try:
644
+ fuel = fuels[fuel_name]
645
+ except KeyError:
646
+ msg = f"undefined fuel '{fuel_name}' found in emission factor table"
647
+ valid_ef = False
648
+
649
+ if (vehicle_name, fuel_name) not in veh_fuel_combs:
650
+ msg = (
651
+ f"emission-factors for undefined vehicle/fuel combination "
652
+ f"'{vehicle_name}' - '{fuel_name}' will not be loaded."
653
+ )
654
+ valid_ef = False
655
+
656
+ if not valid_ef:
657
+ if msg not in messages:
658
+ messages[msg] = 1
659
+ else:
660
+ messages[msg] += 1
661
+ continue
662
+
663
+ substance = substances[subst_slug]
664
+
665
+ def get_ef(val):
666
+ if pd.isna(val):
667
+ return 0
668
+ return vehicle_ef_unit_to_si(val, mass_unit, length_unit)
669
+
670
+ if key in existing_ef_keys:
671
+ efs_to_update.append(
672
+ VehicleEF(
673
+ id=existing_ef_keys[key],
674
+ traffic_situation=traffic_situation,
675
+ substance=substance,
676
+ vehicle=vehicle,
677
+ fuel=fuel,
678
+ freeflow=get_ef(row.freeflow),
679
+ heavy=get_ef(row.heavy),
680
+ saturated=get_ef(row.saturated),
681
+ stopngo=get_ef(row.stopngo),
682
+ coldstart=get_ef(row.coldstart),
683
+ )
684
+ )
685
+ else:
686
+ efs_to_create.append(
687
+ VehicleEF(
688
+ traffic_situation=traffic_situation,
689
+ substance=substance,
690
+ vehicle=vehicle,
691
+ fuel=fuel,
692
+ freeflow=get_ef(row.freeflow),
693
+ heavy=get_ef(row.heavy),
694
+ saturated=get_ef(row.saturated),
695
+ stopngo=get_ef(row.stopngo),
696
+ coldstart=get_ef(row.coldstart),
697
+ )
698
+ )
699
+
700
+ if not overwrite and len(efs_to_update) > 0:
701
+ msg = "\n".join(
702
+ (
703
+ f"{ef.vehicle.name}, {ef.fuel.name}, "
704
+ f"{ef.traffic_situation.ts_id}, {ef.substance.slug}"
705
+ )
706
+ for ef in efs_to_update
707
+ )
708
+ return_message.append(
709
+ import_error(
710
+ f"The following emission factors already exist in the ef-set: {msg}",
711
+ validation=validation,
712
+ )
713
+ )
714
+
715
+ if len(efs_to_update) > 0:
716
+ VehicleEF.objects.bulk_update(
717
+ efs_to_update,
718
+ ("freeflow", "heavy", "saturated", "stopngo", "coldstart"),
719
+ )
720
+ log.debug(f"updated {len(efs_to_update)} emission-factors")
721
+ return_dict = {"vehicle_emission_factors": {"updated": len(efs_to_update)}}
722
+ else:
723
+ return_dict = {"vehicle_emission_factors": {"updated": 0}}
724
+ for msg, nr in messages.items():
725
+ log.warning("warning: " + msg + f": {nr}")
726
+
727
+ if len(efs_to_create) > 0:
728
+ try:
729
+ VehicleEF.objects.bulk_create(efs_to_create)
730
+ log.debug(f"wrote {len(efs_to_create)} emission-factors")
731
+ return_dict["vehicle_emission_factors"]["created"] = len(efs_to_create)
732
+ except IntegrityError:
733
+ for ef in efs_to_create:
734
+ try:
735
+ ef.save()
736
+ except IntegrityError:
737
+ return_message.append(
738
+ import_error(
739
+ "duplicate emission-factors for: "
740
+ f"substance '{ef.substance.slug}, "
741
+ f"vehicle: '{ef.vehicle.name}', "
742
+ f"fuel: '{ef.fuel.name}', "
743
+ f"traffic-situation: '{ef.traffic_situation.ts_id}'",
744
+ validation=validation,
745
+ )
746
+ )
747
+ else:
748
+ return_dict["vehicle_emission_factors"]["created"] = 0
749
+ return return_dict
750
+
751
+
752
+ def import_roadclasses( # noqa: C901, PLR0912, PLR0915
753
+ roadclass_file, config, *, overwrite=False, validation=False, **kwargs
754
+ ):
755
+ """import roadclasses (traffic-situations must already exist in database)."""
756
+ return_message = []
757
+ encoding = kwargs.get("encoding", "utf-8")
758
+ try:
759
+ attributes = config["attributes"]
760
+ except KeyError:
761
+ ImportError("keyword 'attributes' not found in config")
762
+
763
+ log.debug("create road attributes")
764
+
765
+ # created objects are stored in a nested dict
766
+ defined_attributes = OrderedDict()
767
+ for ind, attr_dict in enumerate(attributes):
768
+ attr_dict_tmp = copy.deepcopy(attr_dict)
769
+ try:
770
+ values = attr_dict_tmp.pop("values")
771
+ except KeyError:
772
+ ImportError("keyword 'values' not found for in config")
773
+
774
+ try:
775
+ attr = RoadAttribute.objects.get(
776
+ name=attr_dict_tmp["name"], slug=attr_dict_tmp["slug"]
777
+ )
778
+ except RoadAttribute.DoesNotExist:
779
+ try:
780
+ attr = RoadAttribute.objects.create(
781
+ name=attr_dict_tmp["name"], slug=attr_dict_tmp["slug"], order=ind
782
+ )
783
+ except IntegrityError as err:
784
+ return_message.append(
785
+ import_error(
786
+ "invalid or duplicate road class attribute: "
787
+ f"{attr_dict['name']}: {err}",
788
+ validation=validation,
789
+ )
790
+ )
791
+
792
+ defined_attributes[attr] = {"attribute": attr}
793
+ for val in values:
794
+ defined_attributes[attr][val], _ = attr.values.get_or_create(value=val)
795
+ valid_attributes = get_valid_road_attribute_values()
796
+ for attr, values in valid_attributes.items():
797
+ if attr not in defined_attributes:
798
+ if overwrite:
799
+ attr.delete()
800
+ else:
801
+ return_message.append(
802
+ import_error(
803
+ "Unused road attribute '{attr.slug}' found",
804
+ validation=validation,
805
+ )
806
+ )
807
+ for label, value in values.items():
808
+ if label not in defined_attributes[attr]:
809
+ if overwrite:
810
+ value.delete()
811
+ else:
812
+ return_message.append(
813
+ import_error(
814
+ "Unused road attribute value '{label}' found",
815
+ validation=validation,
816
+ )
817
+ )
818
+
819
+ # cache all existing traffic situations and road-classes
820
+ traffic_situations = {ts.ts_id: ts for ts in TrafficSituation.objects.all()}
821
+
822
+ existing_roadclasses = {
823
+ tuple(rc.attributes.values()): rc.id
824
+ for rc in RoadClass.objects.prefetch_related(PrefetchRoadClassAttributes())
825
+ }
826
+
827
+ log.debug("reading roadclass table")
828
+ with Path(roadclass_file).open(encoding=encoding) as roadclass_stream:
829
+ roadclass_attributes = [a.slug for a in defined_attributes]
830
+ if Path(roadclass_file).suffix == ".csv":
831
+ try:
832
+ column_names = [*roadclass_attributes, "traffic_situation"]
833
+ df = pd.read_csv(
834
+ roadclass_stream, sep=";", dtype=str, usecols=column_names
835
+ ).set_index([a.slug for a in defined_attributes])
836
+ except Exception as err:
837
+ ImportError(
838
+ "could not read csv, are all roadclass attributes "
839
+ f"{roadclass_attributes} and 'traffic_situation' "
840
+ f"given as columns? (error message: {err})"
841
+ )
842
+ elif Path(roadclass_file).suffix == ".xlsx":
843
+ try:
844
+ column_names = ["attr:" + rc for rc in roadclass_attributes]
845
+ column_names.append("traffic_situation")
846
+ df = pd.read_excel(
847
+ roadclass_file,
848
+ dtype=str,
849
+ sheet_name="TrafficSituation",
850
+ usecols=column_names,
851
+ )
852
+ df.rename(
853
+ columns={"attr:" + rc: rc for rc in roadclass_attributes},
854
+ inplace=True,
855
+ )
856
+ df.set_index([a.slug for a in defined_attributes], inplace=True)
857
+ except Exception as err:
858
+ raise ImportError(
859
+ "could not read xlsx, are all roadclass attributes "
860
+ f"{roadclass_attributes} and 'traffic_situation' "
861
+ "given as columns? and does the sheet TrafficSituation exist? "
862
+ f"(error message: {err})"
863
+ )
864
+ else:
865
+ raise ImportError(
866
+ "File for import vehicle emissions factors should be " + ".csv or .xlsx"
867
+ )
868
+
869
+ invalid_traffic_situations = []
870
+ roadclasses_to_create = []
871
+ roadclasses_to_update = []
872
+ row_nr = 0
873
+ for index, row in df.iterrows():
874
+ row_nr += 1
875
+ attribute_values = OrderedDict()
876
+ indexes = [index] if isinstance(index, str) else index
877
+ for attr, val in zip(defined_attributes, indexes):
878
+ if val not in defined_attributes[attr]:
879
+ return_message.append(
880
+ import_error(
881
+ f"Invalid value '{val}' for road attribute '{attr.slug}'",
882
+ validation=validation,
883
+ )
884
+ )
885
+ attribute_values[attr] = defined_attributes[attr][val]
886
+
887
+ try:
888
+ ts = traffic_situations[row.traffic_situation]
889
+ except KeyError:
890
+ invalid_traffic_situations.append(row.traffic_situation)
891
+ continue
892
+ rc = RoadClass(traffic_situation=ts)
893
+ try:
894
+ rc.id = existing_roadclasses[
895
+ tuple([v.value for v in attribute_values.values()])
896
+ ]
897
+ roadclasses_to_update.append((rc, attribute_values.values()))
898
+ except KeyError:
899
+ roadclasses_to_create.append((rc, attribute_values.values()))
900
+
901
+ if len(invalid_traffic_situations) > 0:
902
+ return_message.append(
903
+ import_error(
904
+ "invalid traffic-situations:\n"
905
+ + "\n ".join(invalid_traffic_situations),
906
+ validation=validation,
907
+ )
908
+ )
909
+
910
+ if len(roadclasses_to_update) > 0:
911
+ for rc, attribute_values in roadclasses_to_update:
912
+ if overwrite:
913
+ rc.save()
914
+ rc.attribute_values.set(attribute_values)
915
+ else:
916
+ return_message.append(
917
+ import_error(
918
+ f"roadclass '{rc}' already exists.", validation=validation
919
+ )
920
+ )
921
+ if len(roadclasses_to_create) > 0:
922
+ RoadClass.objects.bulk_create(map(itemgetter(0), roadclasses_to_create))
923
+ # list of saved roadclasses to avoid problem unsaved related attributes
924
+ roadclasses_to_create_saved = []
925
+ # created roadclasses should only be those who have no attribute values yet!
926
+ # this could go wrong if none of the roadclasses has attributes
927
+ # but then does not make sense to have several roadclasses anyhow.
928
+ created_roadclasses = RoadClass.objects.filter(attribute_values=None)
929
+ for i, rc in enumerate(created_roadclasses):
930
+ roadclasses_to_create_saved.append((rc, roadclasses_to_create[i][1]))
931
+
932
+ through_model = RoadClass.attribute_values.through
933
+
934
+ values = [
935
+ through_model(roadclass=rc, roadattributevalue=v)
936
+ for rc, vals in roadclasses_to_create_saved
937
+ for v in vals
938
+ ]
939
+
940
+ through_model.objects.bulk_create(values)
941
+
942
+
943
+ def import_congestionsheet(workbook, sheetname="CongestionProfile", validation=False):
944
+ congestion_data = workbook[sheetname].values
945
+ df_congestion = worksheet_to_dataframe(congestion_data)
946
+ congestion_dict = {}
947
+ # NB this only works if Excel file has exact same format
948
+ nr_profiles = (len(df_congestion["ID"]) + 1) // 25
949
+ for i in range(nr_profiles):
950
+ label = df_congestion["ID"][i * 25]
951
+ typeday = np.asarray(
952
+ df_congestion[
953
+ [
954
+ "monday",
955
+ "tuesday",
956
+ "wednesday",
957
+ "thursday",
958
+ "friday",
959
+ "saturday",
960
+ "sunday",
961
+ ]
962
+ ][i * 25 : i * 25 + 24]
963
+ )
964
+ congestion_dict[label] = {"traffic_condition": typeday}
965
+ return_dict, return_message = import_congestion_profiles(
966
+ congestion_dict, overwrite=True, validation=validation
967
+ )
968
+ return_dict = {"congestion": {"updated or created": len(congestion_dict)}}
969
+ return return_dict, return_message
970
+
971
+
972
+ def import_congestion_profiles(profile_data, *, overwrite=False, validation=False):
973
+ """import congestion profiles."""
974
+ return_message = []
975
+ # Profile instances must not be created by bulk_create as the save function
976
+ # is overloaded to calculate the normation constant.
977
+
978
+ def make_profiles(data):
979
+ retdict = {}
980
+ for name, timevar_data in data.items():
981
+ try:
982
+ traffic_condition = timevar_data["traffic_condition"]
983
+ if type(traffic_condition) == list:
984
+ traffic_condition = str(traffic_condition)
985
+ else:
986
+ traffic_condition = np.array2string(
987
+ traffic_condition, separator=","
988
+ ).replace("\n", "")
989
+ if overwrite:
990
+ newobj = CongestionProfile.objects.update_or_create(
991
+ name=name,
992
+ defaults={"traffic_condition": traffic_condition},
993
+ )
994
+ else:
995
+ try:
996
+ newobj = CongestionProfile.objects.create(
997
+ name=name,
998
+ traffic_condition=traffic_condition,
999
+ )
1000
+ except IntegrityError:
1001
+ raise IntegrityError(
1002
+ f"Congestion-profile {name} " f"already exist in inventory "
1003
+ )
1004
+ retdict[name] = newobj
1005
+ except KeyError:
1006
+ return_message.append(
1007
+ import_error(
1008
+ f"Invalid specification of congestion-profile {name}"
1009
+ f", is 'traffic_condition' specified?",
1010
+ validation=validation,
1011
+ )
1012
+ )
1013
+ return retdict, return_message
1014
+
1015
+ profiles = {}
1016
+ profiles["profiles"], return_message = make_profiles(profile_data)
1017
+ return profiles, return_message
1018
+
1019
+
1020
+ def import_fleets(
1021
+ data, *, overwrite=False, validation=False
1022
+ ): # noqa: C901, PLR0912, PLR0915
1023
+ """import fleets
1024
+
1025
+ args
1026
+ data: a dict with fleets
1027
+
1028
+ optional
1029
+ overwrite: True means existing instances will be overwritten
1030
+
1031
+ """
1032
+
1033
+ existing_fuels = {fuel.name: fuel for fuel in VehicleFuel.objects.all()}
1034
+ existing_vehicles = {vehicle.name: vehicle for vehicle in Vehicle.objects.all()}
1035
+ existing_flow_timevars = {tvar.name: tvar for tvar in FlowTimevar.objects.all()}
1036
+ existing_coldstart_timevars = {
1037
+ tvar.name: tvar for tvar in ColdstartTimevar.objects.all()
1038
+ }
1039
+
1040
+ fleets = {}
1041
+ return_message = []
1042
+ for name, fleet_data in data.items():
1043
+ fleet_data_tmp = copy.deepcopy(fleet_data)
1044
+ try:
1045
+ members_data = fleet_data_tmp.pop("vehicles", [])
1046
+ default_heavy_vehicle_share = fleet_data_tmp["default_heavy_vehicle_share"]
1047
+ except KeyError:
1048
+ return_message.append(
1049
+ import_error(
1050
+ f"no 'default_heavy_vehicle_share' specified for fleet '{name}'",
1051
+ validation=validation,
1052
+ )
1053
+ )
1054
+ if overwrite:
1055
+ fleets[name], _ = Fleet.objects.update_or_create(
1056
+ name=name,
1057
+ defaults={
1058
+ "default_heavy_vehicle_share": default_heavy_vehicle_share,
1059
+ },
1060
+ )
1061
+ else:
1062
+ try:
1063
+ fleets[name] = Fleet.objects.create(
1064
+ name=name,
1065
+ default_heavy_vehicle_share=default_heavy_vehicle_share,
1066
+ )
1067
+ except IntegrityError:
1068
+ return_message.append(
1069
+ import_error(
1070
+ f"either duplicate specification in file or "
1071
+ f"fleet '{name}' already exist in inventory",
1072
+ validation=validation,
1073
+ )
1074
+ )
1075
+
1076
+ members = OrderedDict()
1077
+ heavy_member_sum = 0
1078
+ light_member_sum = 0
1079
+ for vehicle_name, member_data in members_data.items():
1080
+ fuels_data = member_data.pop("fuels", [])
1081
+ timevar_name = member_data.pop("timevar")
1082
+ coldstart_timevar_name = member_data.pop("coldstart_timevar")
1083
+
1084
+ try:
1085
+ veh = existing_vehicles[vehicle_name]
1086
+ except KeyError:
1087
+ return_message.append(
1088
+ import_error(
1089
+ f"Vehicle {vehicle_name} specified in sheet Fleet "
1090
+ "does not exist in sheet VehicleFuel.",
1091
+ validation=validation,
1092
+ )
1093
+ )
1094
+ continue
1095
+ # accumulate member fractions to ensure they sum up to 1.0
1096
+ if veh.isheavy:
1097
+ heavy_member_sum += member_data["fraction"]
1098
+ else:
1099
+ light_member_sum += member_data["fraction"]
1100
+
1101
+ if timevar_name is not None:
1102
+ try:
1103
+ member_data["timevar"] = existing_flow_timevars[timevar_name]
1104
+ except KeyError:
1105
+ return_message.append(
1106
+ import_error(
1107
+ f"timevar '{timevar_name}' specified for vehicle "
1108
+ f"'{vehicle_name}' in fleet '{name}' does not "
1109
+ f"exist in inventory",
1110
+ validation=validation,
1111
+ )
1112
+ )
1113
+ else:
1114
+ member_data["timevar"] = None
1115
+
1116
+ if coldstart_timevar_name is not None:
1117
+ try:
1118
+ member_data["coldstart_timevar"] = existing_coldstart_timevars[
1119
+ coldstart_timevar_name
1120
+ ]
1121
+ except KeyError:
1122
+ return_message.append(
1123
+ import_error(
1124
+ f"coldstart timevar '{coldstart_timevar_name}' "
1125
+ f"specified for vehicle '{vehicle_name}' "
1126
+ f"in fleet '{name}' does not exist in inventory",
1127
+ validation=validation,
1128
+ )
1129
+ )
1130
+ else:
1131
+ member_data["coldstart_timevar"] = None
1132
+
1133
+ try:
1134
+ if overwrite:
1135
+ members[vehicle_name], new = fleets[name].vehicles.update_or_create(
1136
+ vehicle=existing_vehicles[vehicle_name], defaults=member_data
1137
+ )
1138
+ # if updating fleet member, remove any old fleet member fuels
1139
+ # this allows overwriting with fewer fuels than before
1140
+ # and avoids remaining obsolete member fuels
1141
+ if not new:
1142
+ members[vehicle_name].fuels.all().delete()
1143
+ else:
1144
+ try:
1145
+ members[vehicle_name] = fleets[name].vehicles.create(
1146
+ vehicle=existing_vehicles[vehicle_name], **member_data
1147
+ )
1148
+ except IntegrityError:
1149
+ return_message.append(
1150
+ import_error(
1151
+ "Either duplicate specification of fleet member in"
1152
+ "config, or fleetmember already exist in inventory",
1153
+ validation=validation,
1154
+ )
1155
+ )
1156
+ except (KeyError, TypeError):
1157
+ return_message.append(
1158
+ import_error(
1159
+ f"invalid specification of vehicle '{vehicle_name}' in "
1160
+ f"fleet '{name}', must specify 'timevar','coldstart_timevar', "
1161
+ "fraction, 'coldstart_fraction' and 'fuels'",
1162
+ validation=validation,
1163
+ )
1164
+ )
1165
+
1166
+ fuel_sum = 0
1167
+ member_fuels = []
1168
+ if not isinstance(fuels_data, dict):
1169
+ return_message.append(
1170
+ import_error(
1171
+ f"invalid specification of fuels for '{vehicle_name}'"
1172
+ f"in fleet '{name}', fuels should be specified as:\n"
1173
+ f"fuels:\n fuel1: 0.4\n fuel2: 0.6",
1174
+ validation=validation,
1175
+ )
1176
+ )
1177
+ for fuel_name, fuel_fraction in fuels_data.items():
1178
+ fuel_sum += fuel_fraction
1179
+ member_fuels.append(
1180
+ FleetMemberFuel(
1181
+ fuel=existing_fuels[fuel_name],
1182
+ fleet_member=members[vehicle_name],
1183
+ fraction=fuel_fraction,
1184
+ )
1185
+ )
1186
+ if fuel_sum != 1.0:
1187
+ return_message.append(
1188
+ import_error(
1189
+ f"sum of fuel fractions does not sum up to 1.0 (sum={fuel_sum})"
1190
+ f" for '{vehicle_name}s' of fleet '{name}' in inventory ",
1191
+ validation=validation,
1192
+ )
1193
+ )
1194
+ FleetMemberFuel.objects.bulk_create(member_fuels)
1195
+ if heavy_member_sum > 0 and abs(heavy_member_sum - 1.0) >= 0.005:
1196
+ return_message.append(
1197
+ import_error(
1198
+ f"sum of heavy fleet members does not sum up to 1.0 "
1199
+ f"(sum={heavy_member_sum}) "
1200
+ f"for fleet '{name}' in inventory ",
1201
+ validation=validation,
1202
+ )
1203
+ )
1204
+ if light_member_sum > 0 and abs(light_member_sum - 1.0) >= 0.005:
1205
+ return_message.append(
1206
+ import_error(
1207
+ f"sum of light fleet members does not sum up to 1.0 "
1208
+ f"(sum={light_member_sum}) "
1209
+ f"for fleet '{name}' in inventory ",
1210
+ validation=validation,
1211
+ )
1212
+ )
1213
+ return len(fleets), return_message
1214
+
1215
+
1216
+ def import_roads( # noqa: C901, PLR0912, PLR0915
1217
+ roadfile,
1218
+ config,
1219
+ exclude=None,
1220
+ only=None,
1221
+ chunksize=1000,
1222
+ progress_callback=None,
1223
+ validation=False,
1224
+ ):
1225
+ """Import a road network."""
1226
+ return_message = []
1227
+ datasource = gpd.read_file(roadfile)
1228
+
1229
+ if "srid" in config:
1230
+ src_proj = SpatialReference(config["srid"])
1231
+ else:
1232
+ src_proj = SpatialReference(datasource.crs.to_epsg())
1233
+ target_proj = SpatialReference(WGS84_SRID)
1234
+ trans = CoordTransform(src_proj, target_proj)
1235
+
1236
+ # get attribute mappings from road input file to road-source fields
1237
+ # get dict of static attributes to read from road file
1238
+ attr_dict = {
1239
+ attr: config.get(attr) for attr in STATIC_ROAD_ATTRIBUTES if attr in config
1240
+ }
1241
+
1242
+ defaults = config.pop("defaults", {})
1243
+
1244
+ if "fleet" in config:
1245
+ # prefetch fleets and store in dict for quick lookups
1246
+ fleets = {fleet.name: fleet for fleet in Fleet.objects.all()}
1247
+ else:
1248
+ fleet_name = defaults.get("fleet", "default")
1249
+ default_fleet, created = Fleet.objects.get_or_create(
1250
+ name=fleet_name, defaults={"default_heavy_vehicle_share": 0.05}
1251
+ )
1252
+ if created:
1253
+ log.warning(
1254
+ "no entry for 'fleet' in road import config file"
1255
+ ", assigning an empty default fleet for all imported roads"
1256
+ )
1257
+ else:
1258
+ log.warning(
1259
+ f"assigning default fleet '{fleet_name}' for all imported roads"
1260
+ )
1261
+
1262
+ # prefetch congestion profiles and store in dict for quick lookups
1263
+ congestion_profiles = {prof.name: prof for prof in CongestionProfile.objects.all()}
1264
+ default_congestion_profile_name = defaults.get("congestion_profile")
1265
+ default_congestion_profile = None
1266
+ if config.get("congestion_profile") is None:
1267
+ if default_congestion_profile_name is None:
1268
+ log.warning(
1269
+ "no field specified for 'congestion_profile' in road import config"
1270
+ " and no default specified"
1271
+ ", no congestion profile will be specified for imported roads"
1272
+ )
1273
+ else:
1274
+ log.warning(
1275
+ "no field specified for 'congestion_profile' in road import config"
1276
+ f", default congestion profile '{default_congestion_profile_name}' "
1277
+ "will be used for all imported roads"
1278
+ )
1279
+ try:
1280
+ default_congestion_profile = congestion_profiles[
1281
+ default_congestion_profile_name
1282
+ ]
1283
+ except KeyError:
1284
+ return_message.append(
1285
+ import_error(
1286
+ "default congestin profile "
1287
+ f"'{default_congestion_profile_name}' does not exist",
1288
+ validation=validation,
1289
+ )
1290
+ )
1291
+
1292
+ valid_values = get_valid_road_attribute_values()
1293
+ # get valid roadclass attributes
1294
+ if "roadclass" in defaults:
1295
+ default_roadclass_attributes = {}
1296
+ for attr, values in valid_values.items():
1297
+ value = defaults["roadclass"][attr.slug]
1298
+ if attr.slug not in defaults["roadclass"]:
1299
+ return_message.append(
1300
+ import_error(
1301
+ f"incomplete default roadclass attribute mappings,"
1302
+ f" missing '{attr.slug}'",
1303
+ validation=validation,
1304
+ )
1305
+ )
1306
+ if value not in values:
1307
+ return_message.append(
1308
+ import_error(
1309
+ f"invalid roadclass attribute value {value} for"
1310
+ f" attribute {attr.slug}",
1311
+ validation=validation,
1312
+ )
1313
+ )
1314
+ default_roadclass_attributes[attr.slug] = value
1315
+ else:
1316
+ default_roadclass_attributes = {
1317
+ a.slug: a.values.first().value for a in valid_values
1318
+ }
1319
+
1320
+ def generate_key(attribute_values, defined_attributes):
1321
+ return tuple([attribute_values[attr.slug] for attr in defined_attributes])
1322
+
1323
+ roadclasses = {
1324
+ generate_key(rc.attributes, valid_values): rc
1325
+ for rc in RoadClass.objects.prefetch_related(
1326
+ PrefetchRoadClassAttributes()
1327
+ ).all()
1328
+ }
1329
+ if "roadclass" in config:
1330
+ # get attribute mappings for roadclass attributes
1331
+ roadclass_attr_dict = config["roadclass"]
1332
+ # prefetch roadclasses and store in dict for quick lookups
1333
+ for attr in valid_values:
1334
+ if attr.slug not in roadclass_attr_dict:
1335
+ return_message.append(
1336
+ import_error(
1337
+ "incomplete roadclass attribute mappings,"
1338
+ + f" missing '{attr.slug}'",
1339
+ validation=validation,
1340
+ )
1341
+ )
1342
+ else:
1343
+ log.warning(
1344
+ "no entry 'roadclass' found in config "
1345
+ "for road import, assigning a default roadclass to all imported roads"
1346
+ )
1347
+ roadclass_attr_dict = None
1348
+
1349
+ # if no mappings are specified for roadclass attributes,
1350
+ # a default roadclass and traffic situation will be created
1351
+
1352
+ default_ts, created = TrafficSituation.objects.get_or_create(ts_id="default")
1353
+ if created:
1354
+ log.warning(
1355
+ "a 'default' traffic situation is created fo the default roadclass"
1356
+ )
1357
+ try:
1358
+ default_roadclass = roadclasses[
1359
+ generate_key(default_roadclass_attributes, valid_values)
1360
+ ]
1361
+ except KeyError:
1362
+ default_roadclass = RoadClass.objects.create_from_attributes(
1363
+ default_roadclass_attributes, traffic_situation=default_ts
1364
+ )
1365
+
1366
+ # get dict of tags to read from road file
1367
+ tags_dict = config.get("tags", None)
1368
+ tag_defaults = defaults.pop("tags", {})
1369
+ messages = {}
1370
+
1371
+ def make_road(feature): # noqa: C901, PLR0912, PLR0915
1372
+ source_geom = feature.geometry
1373
+
1374
+ if np.shape(source_geom.xy)[1] < 2:
1375
+ msg = "invalid geometry (< 2 nodes), instance not imported"
1376
+ handle_msg(messages, msg)
1377
+ raise ValidationError(msg)
1378
+
1379
+ gdalgeom = GDALLineString(source_geom.wkt)
1380
+ gdalgeom.coord_dim = 2
1381
+ gdalgeom.transform(trans)
1382
+ geom = gdalgeom.geos
1383
+ road_data = {"geom": geom}
1384
+
1385
+ for target_name, source_name in attr_dict.items():
1386
+ default_value = defaults.get(target_name, None)
1387
+ if source_name is not None:
1388
+ try:
1389
+ val = feature.get(source_name)
1390
+ except UnicodeDecodeError:
1391
+ return_message.append(
1392
+ import_error(
1393
+ f"could not decode string in field {source_name}, "
1394
+ "only encoding utf-8 is supported",
1395
+ validation=validation,
1396
+ )
1397
+ )
1398
+ except KeyError:
1399
+ return_message.append(
1400
+ import_error(
1401
+ f"No field named '{source_name}' found in "
1402
+ f"input file '{roadfile}'",
1403
+ validation=validation,
1404
+ )
1405
+ )
1406
+ if pd.isnull(val):
1407
+ val = default_value
1408
+ else:
1409
+ msg = f"no source field specified for target field {target_name}"
1410
+ if default_value is not None:
1411
+ msg += f", using default value '{default_value}'"
1412
+ else:
1413
+ msg += " and no default value specified"
1414
+ handle_msg(messages, msg)
1415
+ val = default_value
1416
+
1417
+ if val is None:
1418
+ handle_msg(messages, f"field {target_name} of road is None")
1419
+ if target_name == "name":
1420
+ road_data[target_name] = ""
1421
+ else:
1422
+ road_data[target_name] = val
1423
+
1424
+ if "width" in road_data and (
1425
+ road_data["width"] == 0 or road_data["width"] == ""
1426
+ ):
1427
+ road_data["width"] = RoadSource._meta.get_field("width").default
1428
+ handle_msg(
1429
+ messages,
1430
+ "invalid value (0m) for road width, "
1431
+ f"using default value {road_data['width']}m",
1432
+ )
1433
+
1434
+ if (
1435
+ "width" in road_data
1436
+ and type(road_data["width"]) == str
1437
+ and ("m" in road_data["width"])
1438
+ ):
1439
+ try:
1440
+ road_data["width"] = float(road_data["width"].replace("m", ""))
1441
+ except ValueError:
1442
+ road_data["width"] = RoadSource._meta.get_field("width").default
1443
+ handle_msg(
1444
+ messages,
1445
+ "invalid value (including unit m) for road width, "
1446
+ f"using default value {road_data['width']}m",
1447
+ )
1448
+
1449
+ road = RoadSource(**road_data)
1450
+
1451
+ if "roadclass" in config:
1452
+ try:
1453
+ rc_key = tuple(
1454
+ [
1455
+ str(feature.get(roadclass_attr_dict[attr.slug])) or "-"
1456
+ for attr in valid_values
1457
+ ]
1458
+ )
1459
+ except (ValueError, IndexError):
1460
+ return_message.append(
1461
+ import_error(
1462
+ f"No field named '{roadclass_attr_dict[attr.slug]}' found in "
1463
+ f"input file '{roadfile}'",
1464
+ validation=validation,
1465
+ )
1466
+ )
1467
+
1468
+ try:
1469
+ road.roadclass = roadclasses[rc_key]
1470
+ except KeyError:
1471
+ handle_msg(
1472
+ messages,
1473
+ f"no roadclass with attribute values {rc_key} in inventory ",
1474
+ fail_early=True,
1475
+ )
1476
+ raise ValidationError(msg)
1477
+ else:
1478
+ road.roadclass = default_roadclass
1479
+
1480
+ if "congestion_profile" in config:
1481
+ try:
1482
+ field_name = config["congestion_profile"]
1483
+ name = feature.get(field_name) if field_name is not None else None
1484
+ except ValueError:
1485
+ return_message.append(
1486
+ import_error(
1487
+ f"No field named '{field_name}' found "
1488
+ f"in input file '{roadfile}'",
1489
+ validation=validation,
1490
+ )
1491
+ )
1492
+ except KeyError:
1493
+ return_message.append(
1494
+ import_error(
1495
+ f"No field named '{field_name}' found "
1496
+ f"in input file '{roadfile}'",
1497
+ validation=validation,
1498
+ )
1499
+ )
1500
+ if name is None:
1501
+ road.congestion_profile = default_congestion_profile
1502
+ elif name not in congestion_profiles:
1503
+ handle_msg(
1504
+ messages,
1505
+ f"no congestion profile with name '{name}' in inventory ",
1506
+ fail_early=True,
1507
+ )
1508
+ raise ValidationError(msg)
1509
+ else:
1510
+ road.congestion_profile = congestion_profiles[name]
1511
+ else:
1512
+ road.congestion_profile = default_congestion_profile
1513
+
1514
+ if "fleet" in config:
1515
+ try:
1516
+ field_name = config["fleet"]
1517
+ name = feature.get(field_name)
1518
+ except (KeyError, IndexError):
1519
+ return_message.append(
1520
+ import_error(
1521
+ f"No field named '{field_name}' found "
1522
+ f"in input file '{roadfile}'",
1523
+ validation=validation,
1524
+ )
1525
+ )
1526
+
1527
+ if name is None or name not in fleets:
1528
+ handle_msg(
1529
+ messages,
1530
+ f"no fleet with name '{name}' in inventory ",
1531
+ fail_early=True,
1532
+ )
1533
+ raise ValidationError(msg)
1534
+ road.fleet = fleets[name]
1535
+ else:
1536
+ road.fleet = default_fleet
1537
+
1538
+ if tags_dict is not None:
1539
+ tag_data = {}
1540
+ for tag_key, source_name in tags_dict.items():
1541
+ tag_default = tag_defaults.get(tag_key, None)
1542
+
1543
+ if source_name is not None:
1544
+ try:
1545
+ val = feature.get(source_name)
1546
+ except KeyError:
1547
+ return_message.append(
1548
+ import_error(
1549
+ f"No field named '{source_name}' found in "
1550
+ f"input file '{roadfile}'",
1551
+ validation=validation,
1552
+ )
1553
+ )
1554
+ else:
1555
+ val = None
1556
+
1557
+ if val is not None:
1558
+ tag_data[tag_key] = val
1559
+ elif tag_default is not None:
1560
+ tag_data[tag_key] = tag_default
1561
+ handle_msg(messages, f"road lack a value for tag '{tag_key}'")
1562
+ road.tags = tag_data
1563
+ return road
1564
+
1565
+ roads = []
1566
+ count = 0
1567
+ nroads = len(datasource)
1568
+ old_progress = -1
1569
+ ncreated = 0
1570
+ for features in inbatch(datasource.iterrows(), chunksize):
1571
+ for feature in features:
1572
+ count += 1
1573
+
1574
+ progress = count / nroads * 100
1575
+ if int(progress) > old_progress:
1576
+ if not validation:
1577
+ log.debug(f"done {int(progress)}%")
1578
+ old_progress = int(progress)
1579
+
1580
+ if exclude is not None and filter_out(feature[1], exclude):
1581
+ continue
1582
+ if only is not None and not filter_out(feature[1], only):
1583
+ continue
1584
+
1585
+ try:
1586
+ road = make_road(feature[1])
1587
+ except ValidationError:
1588
+ continue
1589
+ roads.append(road)
1590
+ RoadSource.objects.bulk_create(roads)
1591
+ ncreated += len(roads)
1592
+ roads = []
1593
+ if progress_callback:
1594
+ progress_callback(count)
1595
+ log.debug(f"created {ncreated} roads")
1596
+ return_dict = {"roads": {"created": ncreated, "updated": 0}}
1597
+ if len(messages) > 0:
1598
+ log.warning("Summary: ")
1599
+ for msg, nr in messages.items():
1600
+ log.warning("- " + msg + f": {nr} roads")
1601
+
1602
+ return return_dict, return_message