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,958 @@
1
+ """Data importers for the edb application."""
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from django.contrib.gis.geos import GEOSGeometry, Point
6
+ from openpyxl import load_workbook
7
+
8
+ from cetk import logging
9
+ from cetk.edb.cache import cache_queryset
10
+ from cetk.edb.const import SHEET_NAMES, WGS84_SRID
11
+ from cetk.edb.models import (
12
+ Activity,
13
+ AreaSource,
14
+ AreaSourceActivity,
15
+ AreaSourceSubstance,
16
+ CodeSet,
17
+ Facility,
18
+ PointSource,
19
+ PointSourceActivity,
20
+ PointSourceSubstance,
21
+ Substance,
22
+ )
23
+ from cetk.edb.models.timevar_models import Timevar
24
+ from cetk.edb.units import activity_rate_unit_to_si, emission_unit_to_si
25
+
26
+ from .activity_import import import_emissionfactorsheet
27
+ from .codeset_import import import_activitycodesheet, import_codesetsheet
28
+ from .timevar_import import import_timevarsheet
29
+ from .utils import cache_codeset, import_error, worksheet_to_dataframe
30
+
31
+ # from cetk.edb.models.common_models import Settings
32
+ # import sys
33
+ # from datetime import datetime
34
+ # uncomment for automatic debugging when receiving error
35
+ # def info(type, value, tb):
36
+ # # if hasattr(sys, "ps1") or not sys.stderr.isatty():
37
+ # # # we are in interactive mode or we don't have a tty-like
38
+ # # # device, so we call the default hook
39
+ # # sys.__excepthook__(type, value, tb)
40
+ # # else:
41
+ # import pdb
42
+ # import traceback
43
+
44
+ # # we are NOT in interactive mode, print the exception...
45
+ # traceback.print_exception(type, value, tb)
46
+ # print
47
+ # # ...then start the debugger in post-mortem mode.
48
+ # # pdb.pm() # deprecated
49
+ # pdb.post_mortem(tb) # more "modern"
50
+
51
+
52
+ # sys.excepthook = info
53
+
54
+
55
+ # column facility and name are used as index and is therefore not included here
56
+ REQUIRED_COLUMNS_AREA = {
57
+ "facility_id": np.str_,
58
+ "facility_name": np.str_,
59
+ "source_name": np.str_,
60
+ "geometry": np.str_,
61
+ "timevar": np.str_,
62
+ }
63
+
64
+ REQUIRED_COLUMNS_POINT = {
65
+ "facility_id": np.str_,
66
+ "facility_name": np.str_,
67
+ "source_name": np.str_,
68
+ "lat": float,
69
+ "lon": float,
70
+ }
71
+
72
+ OPTIONAL_COLUMNS_POINT = {
73
+ "timevar": np.str_,
74
+ "chimney_height": float,
75
+ "outer_diameter": float,
76
+ "inner_diameter": float,
77
+ "gas_speed": float,
78
+ "gas_temperature[K]": float,
79
+ "house_width": float,
80
+ "house_height": float,
81
+ }
82
+
83
+
84
+ log = logging.getLogger(__name__)
85
+
86
+
87
+ def cache_sources(queryset):
88
+ """Return dict of model instances with (facility__official_id, name): instance"""
89
+ sources = {}
90
+ for source in queryset:
91
+ if source.facility is not None:
92
+ sources[source.facility.official_id, source.name] = source
93
+ else:
94
+ sources[None, source.name] = source
95
+ return sources
96
+
97
+
98
+ def import_sources(
99
+ filepath,
100
+ validation=False,
101
+ encoding=None,
102
+ srid=None,
103
+ sourcetype="point",
104
+ ):
105
+ """Import point- or area-sources from xlsx or csv-file.
106
+ PointSource- and AreaSourceSubstances only, not Activities.
107
+
108
+ args
109
+ filepath: path to file
110
+
111
+ options
112
+ encoding: encoding of file (default is utf-8)
113
+ srid: srid of file, default is same srid as domain
114
+ """
115
+ return_message = []
116
+ extension = filepath.suffix
117
+ if extension == ".csv":
118
+ # read csv-file
119
+ if sourcetype == "point":
120
+ with open(filepath, encoding=encoding or "utf-8") as csvfile:
121
+ log.debug("reading point-sources from csv-file")
122
+ df = pd.read_csv(
123
+ csvfile,
124
+ sep=";",
125
+ skip_blank_lines=True,
126
+ comment="#",
127
+ dtype=REQUIRED_COLUMNS_POINT,
128
+ )
129
+ else:
130
+ with open(filepath, encoding=encoding or "utf-8") as csvfile:
131
+ log.debug("reading area-sources from csv-file")
132
+ df = pd.read_csv(
133
+ csvfile,
134
+ sep=";",
135
+ skip_blank_lines=True,
136
+ comment="#",
137
+ dtype=REQUIRED_COLUMNS_AREA,
138
+ )
139
+ elif extension == ".xlsx":
140
+ # read spreadsheet
141
+ try:
142
+ workbook = load_workbook(filename=filepath, data_only=True, read_only=True)
143
+ except Exception as exc:
144
+ return_message.append(import_error(str(exc), validation))
145
+ worksheet = workbook.worksheets[0]
146
+ if len(workbook.worksheets) > 1:
147
+ if sourcetype == "point":
148
+ log.debug(
149
+ "Multiple sheets in spreadsheet, importing sheet 'PointSource'."
150
+ )
151
+ data = workbook["PointSource"].values
152
+ elif sourcetype == "area":
153
+ log.debug(
154
+ "Multiple sheets in spreadsheet, importing sheet 'AreaSource'."
155
+ )
156
+ data = workbook["AreaSource"].values
157
+ else:
158
+ data = worksheet.values
159
+ df = worksheet_to_dataframe(data)
160
+ workbook.close()
161
+ else:
162
+ return_message.append(
163
+ import_error(
164
+ "Only xlsx and csv files are supported for import",
165
+ validation=validation,
166
+ )
167
+ )
168
+
169
+ df = set_datatypes(df, sourcetype)
170
+
171
+ return create_or_update_sources(
172
+ df,
173
+ validation=validation,
174
+ srid=None,
175
+ sourcetype=sourcetype,
176
+ )
177
+
178
+
179
+ def set_datatypes(df, sourcetype):
180
+ if sourcetype == "point":
181
+ df = df.astype(dtype=REQUIRED_COLUMNS_POINT)
182
+ optional_columns = {
183
+ k: OPTIONAL_COLUMNS_POINT[k]
184
+ for k in OPTIONAL_COLUMNS_POINT
185
+ if k in df.columns
186
+ }
187
+ df = df.astype(dtype=optional_columns)
188
+ else:
189
+ df = df.astype(dtype=REQUIRED_COLUMNS_AREA)
190
+ # below is necessary not to create facilities with name 'None'
191
+ df = df.replace(to_replace="None", value=None)
192
+ df = df.replace(to_replace="nan", value=None)
193
+ return df
194
+
195
+
196
+ # @profile
197
+ def create_or_update_sources(
198
+ df,
199
+ validation=False,
200
+ srid=None,
201
+ sourcetype="point",
202
+ cache=True,
203
+ ):
204
+ return_message = []
205
+ # user defined SRID for import or WGS84 if nothing specified
206
+ # as long as we do not have functions in Eclair to edit the "settings_SRID"
207
+ # it does not make sense to use that SRID as default for import.
208
+ srid = srid or WGS84_SRID
209
+ # cache related models
210
+ substances = cache_queryset(Substance.objects.all(), "slug")
211
+ timevars = cache_queryset(Timevar.objects.all(), "name")
212
+ facilities = cache_queryset(Facility.objects.all(), "official_id")
213
+
214
+ if cache:
215
+ if sourcetype == "point":
216
+ sources = cache_sources(
217
+ PointSource.objects.select_related("facility").all()
218
+ ) # .prefetch_related("substances")
219
+ elif sourcetype == "area":
220
+ sources = cache_sources(
221
+ AreaSource.objects.select_related("facility").all()
222
+ ) # .prefetch_related("substances")
223
+ else:
224
+ return_message.append(
225
+ import_error(
226
+ "this sourcetype is not implemented",
227
+ validation=validation,
228
+ )
229
+ )
230
+
231
+ code_sets = [
232
+ cache_codeset(CodeSet.objects.filter(id=i).first()) for i in range(1, 4)
233
+ ]
234
+ code_set_slugs = {}
235
+ for i in range(1, 4):
236
+ try:
237
+ code_set_slug = CodeSet.objects.get(id=i).slug
238
+ except CodeSet.DoesNotExist:
239
+ code_set_slug = None
240
+ code_set_slugs[i] = code_set_slug
241
+
242
+ if sourcetype == "point":
243
+ for col in REQUIRED_COLUMNS_POINT.keys():
244
+ if col not in df.columns:
245
+ return_message.append(
246
+ import_error(
247
+ f"Missing required column '{col}'", validation=validation
248
+ )
249
+ )
250
+ else:
251
+ for col in REQUIRED_COLUMNS_AREA.keys():
252
+ if col not in df.columns:
253
+ return_message.append(
254
+ import_error(
255
+ f"Missing required column '{col}'", validation=validation
256
+ )
257
+ )
258
+
259
+ # set dataframe index
260
+ try:
261
+ df.set_index(
262
+ ["facility_id", "source_name"], verify_integrity=True, inplace=True
263
+ )
264
+ except ValueError as err:
265
+ return_message.append(
266
+ import_error(
267
+ f"Non-unique combination of facility_id and source_name: {err}",
268
+ validation=validation,
269
+ )
270
+ )
271
+ update_facilities = []
272
+ create_facilities = {}
273
+ drop_substances = []
274
+ create_substances = []
275
+ update_sources = []
276
+ create_sources = {}
277
+ activitycode_columns = [key for key in df.columns if key.startswith("activitycode")]
278
+ row_nr = 2
279
+ for row_key, row in df.iterrows():
280
+ row_dict = row.to_dict()
281
+
282
+ # initialize activitycodes
283
+ source_data = {
284
+ "activitycode1": None,
285
+ "activitycode2": None,
286
+ "activitycode3": None,
287
+ }
288
+
289
+ if sourcetype == "point":
290
+ # get pointsource coordinates
291
+ try:
292
+ if pd.isna(row_dict["lat"]) or pd.isna(row_dict["lon"]):
293
+ return_message.append(
294
+ import_error(
295
+ f"missing coordinates for source '{row_key}'",
296
+ validation=validation,
297
+ )
298
+ )
299
+ x = float(row_dict["lon"])
300
+ y = float(row_dict["lat"])
301
+ except ValueError:
302
+ return_message.append(
303
+ import_error(
304
+ f"Invalid {sourcetype} coordinates on row {row_nr}",
305
+ validation=validation,
306
+ )
307
+ )
308
+ # create geometry
309
+ source_data["geom"] = Point(x, y, srid=srid).transform(4326, clone=True)
310
+ # get chimney properties
311
+ for attr, key in {
312
+ "chimney_height": "chimney_height",
313
+ "chimney_inner_diameter": "inner_diameter",
314
+ "chimney_outer_diameter": "outer_diameter",
315
+ "chimney_gas_speed": "gas_speed",
316
+ "chimney_gas_temperature": "gas_temperature[K]",
317
+ }.items():
318
+ if pd.isna(row_dict[key]):
319
+ return_message.append(
320
+ import_error(
321
+ "Missing value in PointSource sheet "
322
+ f"for {key} on row {row_nr}",
323
+ validation=validation,
324
+ )
325
+ )
326
+ else:
327
+ source_data[attr] = row_dict[key]
328
+
329
+ # get downdraft parameters
330
+ try:
331
+ if not pd.isna(row_dict["house_width"]):
332
+ source_data["house_width"] = row_dict["house_width"]
333
+ except KeyError:
334
+ if row_nr == 2:
335
+ log.debug("house_width is skipped from import.")
336
+ try:
337
+ if not pd.isna(row_dict["house_height"]):
338
+ source_data["house_height"] = row_dict["house_height"]
339
+ except KeyError:
340
+ if row_nr == 2:
341
+ log.debug("house_heigth is skipped from import.")
342
+
343
+ elif sourcetype == "area":
344
+ try:
345
+ if pd.isna(row_dict["geometry"]):
346
+ return_message.append(
347
+ import_error(
348
+ f"missing area polygon for source '{row_key}'",
349
+ validation=validation,
350
+ )
351
+ )
352
+ wkt_polygon = row_dict["geometry"]
353
+ # TODO add check that valid WKT polygon
354
+ except ValueError:
355
+ return_message.append(
356
+ import_error(
357
+ f"Invalid polygon geometry in AreaSource sheet on row {row_nr}",
358
+ validation=validation,
359
+ )
360
+ )
361
+ source_data["geom"] = GEOSGeometry(f"SRID={4326};" + wkt_polygon)
362
+ else:
363
+ return_message.append(
364
+ import_error(
365
+ "this sourcetype is not implemented", validation=validation
366
+ )
367
+ )
368
+
369
+ # get activitycodes
370
+ for code_ind, code_set in enumerate(code_sets, 1):
371
+ try:
372
+ code_set_slug = code_set_slugs[code_ind]
373
+ code_attribute = f"activitycode_{code_set_slug}"
374
+ if code_attribute in row_dict:
375
+ code = row_dict[code_attribute]
376
+ if len(code_set) == 0:
377
+ if code is not None and code is not np.nan:
378
+ return_message.append(
379
+ import_error(
380
+ f"Unknown activitycode_{code_set_slug} '{code}'"
381
+ f" for {sourcetype} source on row {row_nr}",
382
+ validation=validation,
383
+ )
384
+ )
385
+ if not pd.isna(code):
386
+ try:
387
+ # note this can be problematic with codes 01 etc as SNAP
388
+ # TODO activitycodes should be string directly on import!
389
+ activity_code = code_set[str(code)]
390
+ codeset_id = activity_code.code_set_id
391
+ source_data[f"activitycode{codeset_id}"] = activity_code
392
+ except KeyError:
393
+ return_message.append(
394
+ import_error(
395
+ f"Unknown activitycode_{code_set_slug} '{code}'"
396
+ f" for {sourcetype} source on row {row_nr}",
397
+ validation=validation,
398
+ )
399
+ )
400
+ except AttributeError:
401
+ # no such codeset exists
402
+ if len(activitycode_columns) > CodeSet.objects.count():
403
+ # need to check if activitycode is specified for unimported codeset
404
+ codeset_slug = [
405
+ column.split("_", 1)[-1] for column in activitycode_columns
406
+ ]
407
+ for index, column in enumerate(activitycode_columns):
408
+ if not pd.isna(row_dict[column]):
409
+ try:
410
+ CodeSet.objects.get(slug=codeset_slug[index])
411
+ except CodeSet.DoesNotExist:
412
+ return_message.append(
413
+ import_error(
414
+ "Specified activitycode "
415
+ f"{row_dict[column]} for "
416
+ f" unknown codeset {codeset_slug[index]}"
417
+ f" for {sourcetype} source on row {row_nr}",
418
+ validation=validation,
419
+ )
420
+ )
421
+ pass
422
+
423
+ # get columns with tag values for the current row
424
+ tag_keys = [key for key in row_dict.keys() if key.startswith("tag:")]
425
+ # set tags dict for source
426
+ source_data["tags"] = {
427
+ key[4:]: row_dict[key] for key in tag_keys if pd.notna(row_dict[key])
428
+ }
429
+
430
+ # get timevar name and corresponding timevar
431
+ timevar_name = row_dict["timevar"]
432
+ if pd.notna(timevar_name):
433
+ try:
434
+ source_data["timevar"] = timevars[timevar_name]
435
+ except KeyError:
436
+ return_message.append(
437
+ import_error(
438
+ f"Timevar '{timevar_name}' "
439
+ f"on row {row_nr} for {sourcetype} source does not exist",
440
+ validation=validation,
441
+ )
442
+ )
443
+ # get all column-names starting with "subst" whith value for the current row
444
+ subst_keys = [
445
+ key
446
+ for key in row_dict.keys()
447
+ if key.startswith("subst:") and pd.notna(row_dict[key])
448
+ ]
449
+ # create list of data dict for each substance emission
450
+ emissions = {}
451
+ for subst_key in subst_keys:
452
+ subst = subst_key[6:]
453
+ # dict with substance emission properties (value and substance)
454
+ emis = {}
455
+ emissions[subst] = emis
456
+
457
+ # get substance
458
+ try:
459
+ emis["substance"] = substances[subst]
460
+ except KeyError:
461
+ return_message.append(
462
+ import_error(f"Undefined substance {subst}", validation)
463
+ )
464
+
465
+ try:
466
+ if "emission_unit" in row_dict and not pd.isnull(
467
+ row_dict["emission_unit"]
468
+ ):
469
+ emis["value"] = emission_unit_to_si(
470
+ float(row_dict[subst_key]), row_dict["emission_unit"]
471
+ )
472
+ else:
473
+ return_message.append(
474
+ import_error(
475
+ f"No unit specified for {sourcetype}-source"
476
+ f" emissions on row {row_nr}",
477
+ validation=validation,
478
+ )
479
+ )
480
+ except ValueError:
481
+ return_message.append(
482
+ import_error(
483
+ f"Invalid {sourcetype}-source emission value "
484
+ f"{row_dict[subst_key]} on row {row_nr}",
485
+ validation=validation,
486
+ )
487
+ )
488
+ except KeyError as err:
489
+ return_message.append(
490
+ import_error(
491
+ f"Missing data {err} on row {row_nr} for {sourcetype} sources.",
492
+ validation=validation,
493
+ )
494
+ )
495
+
496
+ official_facility_id, source_name = row_key
497
+ if pd.isna(official_facility_id):
498
+ official_facility_id = None
499
+
500
+ if pd.isna(source_name):
501
+ return_message.append(
502
+ import_error(
503
+ f"No name specified for {sourcetype} source on row {row_nr}",
504
+ validation=validation,
505
+ )
506
+ )
507
+ if pd.isna(row_dict["facility_name"]):
508
+ facility_name = None
509
+ else:
510
+ facility_name = row_dict["facility_name"]
511
+
512
+ try:
513
+ facility = facilities[official_facility_id]
514
+ update_facilities.append(facility)
515
+ except KeyError:
516
+ if official_facility_id is not None:
517
+ if official_facility_id in create_facilities:
518
+ facility = create_facilities[official_facility_id]
519
+ else:
520
+ if facility_name is None:
521
+ return_message.append(
522
+ import_error(
523
+ f"No name specified for facility on row {row_nr}",
524
+ validation=validation,
525
+ )
526
+ )
527
+ facility_name = "unspecified"
528
+
529
+ facility = Facility(
530
+ name=facility_name,
531
+ official_id=official_facility_id,
532
+ )
533
+ create_facilities[official_facility_id] = facility
534
+ else:
535
+ facility = None
536
+
537
+ source_data["facility"] = facility
538
+ source_key = (official_facility_id, source_name)
539
+ try:
540
+ if cache:
541
+ source = sources[source_key]
542
+ else:
543
+ if official_facility_id is None:
544
+ facility_id = None
545
+ else:
546
+ # this could through a keyerror for the wrong reason,
547
+ # because facility is None, not because pointsource exists already
548
+ facility_id = facilities[str(official_facility_id)].id
549
+ if sourcetype == "point":
550
+ source = PointSource.objects.get(
551
+ name=str(source_name), facility_id=facility_id
552
+ )
553
+ elif sourcetype == "area":
554
+ source = AreaSource.objects.get(
555
+ name=str(source_name), facility_id=facility_id
556
+ )
557
+ for key, val in source_data.items():
558
+ setattr(source, key, val)
559
+ update_sources.append(source)
560
+ drop_substances += list(source.substances.all())
561
+ if sourcetype == "point":
562
+ create_substances += [
563
+ PointSourceSubstance(source=source, **emis)
564
+ for emis in emissions.values()
565
+ ]
566
+ elif sourcetype == "area":
567
+ create_substances += [
568
+ AreaSourceSubstance(source=source, **emis)
569
+ for emis in emissions.values()
570
+ ]
571
+ except (PointSource.DoesNotExist, AreaSource.DoesNotExist, KeyError):
572
+ if sourcetype == "point":
573
+ source = PointSource(name=source_name, **source_data)
574
+ if source_key not in create_sources:
575
+ create_sources[source_key] = source
576
+ create_substances += [
577
+ PointSourceSubstance(source=source, **emis)
578
+ for emis in emissions.values()
579
+ ]
580
+ else:
581
+ return_message.append(
582
+ import_error(
583
+ f"multiple rows for the same point-source '{source_name}'",
584
+ validation=validation,
585
+ )
586
+ )
587
+ else:
588
+ source = AreaSource(name=source_name, **source_data)
589
+ if source_key not in create_sources:
590
+ create_sources[source_key] = source
591
+ create_substances += [
592
+ AreaSourceSubstance(source=source, **emis)
593
+ for emis in emissions.values()
594
+ ]
595
+ else:
596
+ return_message.append(
597
+ import_error(
598
+ f"multiple rows for the same area-source '{source_name}'",
599
+ validation=validation,
600
+ )
601
+ )
602
+ row_nr += 1
603
+
604
+ existing_facility_names = set([f.name for f in facilities.values()])
605
+ duplicate_facility_names = []
606
+ for official_id, f in create_facilities.items():
607
+ if f.name in existing_facility_names:
608
+ duplicate_facility_names.append(f.name)
609
+ if len(duplicate_facility_names) > 0:
610
+ return_message.append(
611
+ import_error(
612
+ "The following facility names are already "
613
+ "used in inventory but for facilities with "
614
+ f"different official_id: {duplicate_facility_names}",
615
+ validation=validation,
616
+ )
617
+ )
618
+ duplicate_facility_names = {}
619
+ for f in create_facilities.values():
620
+ if f.name in duplicate_facility_names:
621
+ duplicate_facility_names[f.name] += 1
622
+ else:
623
+ duplicate_facility_names[f.name] = 1
624
+ duplicate_facility_names = [
625
+ name for name, nr in duplicate_facility_names.items() if nr > 1
626
+ ]
627
+ if len(duplicate_facility_names) > 0:
628
+ return_message.append(
629
+ import_error(
630
+ "The same facility name is used on multiple rows but "
631
+ f"with different facility_id: {duplicate_facility_names}",
632
+ validation=validation,
633
+ )
634
+ )
635
+
636
+ Facility.objects.bulk_create(create_facilities.values())
637
+ Facility.objects.bulk_update(update_facilities, ["name"])
638
+
639
+ facilities = cache_queryset(Facility.objects.all(), "id")
640
+ # ensure PointSource.facility_id is not None if facility exists.
641
+ for source in create_sources.values():
642
+ if source.facility is not None:
643
+ # find the facility_id corresponding to official id, or set None
644
+ source.facility_id = next(
645
+ (
646
+ key
647
+ for key, value in facilities.items()
648
+ if str(value) == str(source.facility)
649
+ ),
650
+ None,
651
+ )
652
+ if source.facility_id is None:
653
+ raise ImportError(
654
+ f"Could not link pointsource {source.name} to "
655
+ + f"facility {source.facility.name}"
656
+ )
657
+ if sourcetype == "point":
658
+ PointSource.objects.bulk_create(create_sources.values())
659
+ PointSource.objects.bulk_update(
660
+ update_sources,
661
+ [
662
+ "name",
663
+ "geom",
664
+ "tags",
665
+ "chimney_gas_speed",
666
+ "chimney_gas_temperature",
667
+ "chimney_height",
668
+ "chimney_inner_diameter",
669
+ "chimney_outer_diameter",
670
+ "house_height",
671
+ "house_width",
672
+ "activitycode1",
673
+ "activitycode2",
674
+ "activitycode3",
675
+ ],
676
+ )
677
+
678
+ # drop existing substance emissions of point-sources that will be updated
679
+ PointSourceSubstance.objects.filter(
680
+ pk__in=[inst.id for inst in drop_substances]
681
+ ).delete()
682
+
683
+ # ensure PointSourceSubstance.source_id is not None
684
+ for emis in create_substances:
685
+ emis.source_id = PointSource.objects.get(
686
+ name=emis.source, facility_id=emis.source.facility_id
687
+ ).id
688
+ PointSourceSubstance.objects.bulk_create(create_substances)
689
+ return_dict = {
690
+ "facility": {
691
+ "updated": len(update_facilities),
692
+ "created": len(create_facilities),
693
+ },
694
+ "pointsource": {
695
+ "updated": len(update_sources),
696
+ "created": len(create_sources),
697
+ },
698
+ }
699
+ if sourcetype == "area":
700
+ AreaSource.objects.bulk_create(create_sources.values())
701
+ AreaSource.objects.bulk_update(
702
+ update_sources,
703
+ [
704
+ "name",
705
+ "geom",
706
+ "tags",
707
+ "activitycode1",
708
+ "activitycode2",
709
+ "activitycode3",
710
+ ],
711
+ )
712
+
713
+ # drop existing substance emissions of point-sources that will be updated
714
+ AreaSourceSubstance.objects.filter(
715
+ pk__in=[inst.id for inst in drop_substances]
716
+ ).delete()
717
+
718
+ # ensure PointSourceSubstance.source_id is not None
719
+ for emis in create_substances:
720
+ emis.source_id = AreaSource.objects.get(
721
+ name=emis.source, facility_id=emis.source.facility_id
722
+ ).id
723
+ AreaSourceSubstance.objects.bulk_create(create_substances)
724
+ return_dict = {
725
+ "facility": {
726
+ "updated": len(update_facilities),
727
+ "created": len(create_facilities),
728
+ },
729
+ "areasource": {
730
+ "updated": len(update_sources),
731
+ "created": len(create_sources),
732
+ },
733
+ }
734
+ # print(datetime.now().strftime("%H:%M:%S") + "finish point import")
735
+ return return_dict, return_message
736
+
737
+
738
+ # @profile
739
+ def import_sourceactivities(
740
+ filepath,
741
+ encoding=None,
742
+ srid=None,
743
+ import_sheets=SHEET_NAMES,
744
+ validation=False,
745
+ ):
746
+ """Import sheets Timevar, Codeset, ActivityCode, EmissionFactor,
747
+ PointSource and AreaSource from xlsx. If GridSource in import_sheets,
748
+ it will be ignored.
749
+
750
+ args
751
+ filepath: path to file
752
+
753
+ options
754
+ encoding: encoding of file (default is utf-8)
755
+ srid: srid of file, default is wgs84
756
+ import_sheets: sheets to be imported, default is all.
757
+ """
758
+ return_message = []
759
+ try:
760
+ workbook = load_workbook(filename=filepath, data_only=True, read_only=True)
761
+ except Exception as exc:
762
+ return_message.append(import_error(str(exc), validation))
763
+
764
+ return_dict = {}
765
+ sheet_names = [sheet.title for sheet in workbook.worksheets]
766
+ if ("Timevar" in sheet_names) and ("Timevar" in import_sheets):
767
+ log.debug("validating/importing timevars")
768
+ updates, msgs = import_timevarsheet(workbook, validation)
769
+ return_dict.update(updates)
770
+ return_message += msgs
771
+
772
+ if ("CodeSet" in sheet_names) and ("CodeSet" in import_sheets):
773
+ log.debug("validating/importing code-sets")
774
+ updates, msgs = import_codesetsheet(workbook, validation)
775
+ return_dict.update(updates)
776
+ return_message += msgs
777
+
778
+ if ("ActivityCode" in sheet_names) and ("ActivityCode" in import_sheets):
779
+ log.debug("validating/importing activity-codes")
780
+ updates, msgs = import_activitycodesheet(workbook, validation)
781
+ return_dict.update(updates)
782
+ return_message += msgs
783
+
784
+ if ("EmissionFactor" in sheet_names) and ("EmissionFactor" in import_sheets):
785
+ log.debug("validating/importing emission-factors")
786
+ updates, msgs = import_emissionfactorsheet(workbook, validation)
787
+ return_dict.update(updates)
788
+ return_message += msgs
789
+
790
+ if ("PointSource" in sheet_names) and ("PointSource" in import_sheets):
791
+ log.debug("validating/importing sheet PointSource")
792
+ data = workbook["PointSource"].values
793
+ df_pointsource = worksheet_to_dataframe(data)
794
+ df_pointsource = set_datatypes(df_pointsource, "point")
795
+ # import pointsources and pointsourcesubstances
796
+ caching_sources = len(df_pointsource) > PointSource.objects.count()
797
+ ps, msgs = create_or_update_sources(
798
+ df_pointsource,
799
+ srid=srid,
800
+ validation=validation,
801
+ sourcetype="point",
802
+ cache=caching_sources,
803
+ )
804
+ return_dict.update(ps)
805
+ return_message += msgs
806
+
807
+ # import pointsourceactivities
808
+ activities = cache_queryset(Activity.objects.all(), "name")
809
+ facilities = cache_queryset(Facility.objects.all(), "official_id")
810
+ if caching_sources:
811
+ log.debug("caching sources to speed up updates")
812
+ pointsourceactivities = cache_queryset(
813
+ PointSourceActivity.objects.select_related("activity", "source").all(),
814
+ ["activity", "source"],
815
+ )
816
+ pointsources = cache_sources(
817
+ PointSource.objects.select_related("facility").all()
818
+ )
819
+ log.debug("Reading sources")
820
+ create_pointsourceactivities = []
821
+ update_pointsourceactivities = []
822
+ # NB: does not work if column header starts with space, but same for subst:
823
+ activity_keys = [k for k in df_pointsource.columns if k.startswith("act:")]
824
+ row_nr = 1
825
+ for row_key, row in df_pointsource.iterrows():
826
+ # original unit stored in activity.unit, but
827
+ # pointsourceactivity.rate stored as activity / s.
828
+ if pd.isna(row.name[0]):
829
+ facility_id = None
830
+ else:
831
+ facility_id = str(row.name[0])
832
+ if caching_sources:
833
+ # row index set as ["facility_id", "source_name"]
834
+ # in create_or_update_source
835
+ pointsource = pointsources[facility_id, str(row.name[1])]
836
+ else:
837
+ facility = facilities[facility_id] if facility_id is not None else None
838
+ pointsource = PointSource.objects.get(
839
+ name=str(row.name[1]), facility=facility
840
+ )
841
+ log.debug(f"pointsource {row_nr}: {pointsource.name}")
842
+ for activity_key in activity_keys:
843
+ if not pd.isnull(row[activity_key]):
844
+ rate = row[activity_key]
845
+ activity_name = activity_key[4:]
846
+ try:
847
+ activity = activities[activity_name]
848
+ except KeyError:
849
+ return_message += import_error(
850
+ f"unknown activity '{activity_name}'"
851
+ + f" for pointsource '{row['source_name']}'",
852
+ validation=validation,
853
+ )
854
+ rate = activity_rate_unit_to_si(rate, activity.unit)
855
+ try:
856
+ if caching_sources:
857
+ psa = pointsourceactivities[activity, pointsource]
858
+ else:
859
+ psa = PointSourceActivity.objects.get(
860
+ activity_id=activity.id, source_id=pointsource.id
861
+ )
862
+ setattr(psa, "rate", rate)
863
+ update_pointsourceactivities.append(psa)
864
+ except (PointSourceActivity.DoesNotExist, KeyError):
865
+ psa = PointSourceActivity(
866
+ activity=activity, source=pointsource, rate=rate
867
+ )
868
+ create_pointsourceactivities.append(psa)
869
+ row_nr += 1
870
+ log.debug("Creating point-sources")
871
+ PointSourceActivity.objects.bulk_create(create_pointsourceactivities)
872
+ PointSourceActivity.objects.bulk_update(
873
+ update_pointsourceactivities, ["activity", "source", "rate"]
874
+ )
875
+ return_dict.update(
876
+ {
877
+ "pointsourceactivity": {
878
+ "updated": len(update_pointsourceactivities),
879
+ "created": len(create_pointsourceactivities),
880
+ }
881
+ }
882
+ )
883
+ if ("AreaSource" in sheet_names) and ("AreaSource" in import_sheets):
884
+ data = workbook["AreaSource"].values
885
+ df_areasource = worksheet_to_dataframe(data)
886
+ df_areasource = set_datatypes(df_areasource, "area")
887
+ ps, msgs = create_or_update_sources(
888
+ df_areasource,
889
+ srid=srid,
890
+ validation=validation,
891
+ sourcetype="area",
892
+ )
893
+ return_dict.update(ps)
894
+ return_message += msgs
895
+ # for now always caching areasources, change if case with many areasources
896
+ # becomes relevant.
897
+ areasourceactivities = cache_queryset(
898
+ AreaSourceActivity.objects.select_related("activity", "source").all(),
899
+ ["activity", "source"],
900
+ )
901
+ activities = cache_queryset(Activity.objects.all(), "name")
902
+ areasources = cache_sources(
903
+ AreaSource.objects.select_related("facility")
904
+ .prefetch_related("substances")
905
+ .all()
906
+ )
907
+ create_areasourceactivities = []
908
+ update_areasourceactivities = []
909
+ for row_key, row in df_areasource.iterrows():
910
+ # NB: does not work if column header starts with space, but same for subst:
911
+ activity_keys = [k for k in row.keys() if k.startswith("act:")]
912
+ for activity_key in activity_keys:
913
+ if not pd.isna(row[activity_key]):
914
+ rate = float(row[activity_key])
915
+ try:
916
+ activity = activities[activity_key[4:]]
917
+ except KeyError:
918
+ return_message += import_error(
919
+ f"unknown activity '{activity_name}'"
920
+ f" for areasource '{row['source_name']}'",
921
+ validation=validation,
922
+ )
923
+
924
+ rate = activity_rate_unit_to_si(rate, activity.unit)
925
+ # original unit stored in activity.unit, but
926
+ # areasourceactivity.rate stored as activity / s.
927
+ # facility_id and source_name set as df index
928
+ if pd.isna(row.name[0]):
929
+ facility_id = None
930
+ else:
931
+ facility_id = str(row.name[0])
932
+ areasource = areasources[facility_id, str(row.name[1])]
933
+ try:
934
+ psa = areasourceactivities[activity, areasource]
935
+ setattr(psa, "rate", rate)
936
+ update_areasourceactivities.append(psa)
937
+ except KeyError:
938
+ psa = AreaSourceActivity(
939
+ activity=activity, source=areasource, rate=rate
940
+ )
941
+ create_areasourceactivities.append(psa)
942
+
943
+ AreaSourceActivity.objects.bulk_create(create_areasourceactivities)
944
+ AreaSourceActivity.objects.bulk_update(
945
+ update_areasourceactivities, ["activity", "source", "rate"]
946
+ )
947
+ return_dict.update(
948
+ {
949
+ "areasourceactivity": {
950
+ "updated": len(update_areasourceactivities),
951
+ "created": len(create_areasourceactivities),
952
+ }
953
+ }
954
+ )
955
+
956
+ workbook.close()
957
+ # print(datetime.now().strftime("%H:%M:%S") + "finish import")
958
+ return return_dict, return_message