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.
- cetk/__init__.py +87 -0
- cetk/db.py +41 -0
- cetk/edb/__init__.py +0 -0
- cetk/edb/apps.py +9 -0
- cetk/edb/cache.py +249 -0
- cetk/edb/const.py +26 -0
- cetk/edb/copy.py +18 -0
- cetk/edb/exporters.py +551 -0
- cetk/edb/importers/__init__.py +36 -0
- cetk/edb/importers/activity_import.py +279 -0
- cetk/edb/importers/codeset_import.py +124 -0
- cetk/edb/importers/gridsource_import.py +256 -0
- cetk/edb/importers/roadsource_import.py +1602 -0
- cetk/edb/importers/source_import.py +958 -0
- cetk/edb/importers/timevar_import.py +115 -0
- cetk/edb/importers/utils.py +92 -0
- cetk/edb/importers/validation.py +207 -0
- cetk/edb/ltreefield.py +76 -0
- cetk/edb/migrations/0001_initial.py +1731 -0
- cetk/edb/migrations/0002_data.py +156 -0
- cetk/edb/migrations/0003_create_gpkg_tables.py +14 -0
- cetk/edb/migrations/__init__.py +0 -0
- cetk/edb/models/__init__.py +49 -0
- cetk/edb/models/base.py +52 -0
- cetk/edb/models/common_models.py +144 -0
- cetk/edb/models/fleets.py +160 -0
- cetk/edb/models/gridsource_models.py +163 -0
- cetk/edb/models/road_classes.py +197 -0
- cetk/edb/models/road_models.py +413 -0
- cetk/edb/models/source_models.py +399 -0
- cetk/edb/models/timevar_models.py +111 -0
- cetk/edb/rasterize/__init__.py +1 -0
- cetk/edb/rasterize/rasterizer.py +1544 -0
- cetk/edb/signals.py +43 -0
- cetk/edb/traffic.py +75 -0
- cetk/edb/units.py +160 -0
- cetk/emissions/__init__.py +0 -0
- cetk/emissions/aggregate_emissions.sql +303 -0
- cetk/emissions/areasource_emissions.sql +53 -0
- cetk/emissions/calc.py +148 -0
- cetk/emissions/filters.py +179 -0
- cetk/emissions/gridsource_emissions.sql +52 -0
- cetk/emissions/pointsource_emissions.sql +53 -0
- cetk/emissions/queries.py +221 -0
- cetk/emissions/roadsource_emissions.sql +211 -0
- cetk/emissions/used_substances.sql +56 -0
- cetk/emissions/views.py +89 -0
- cetk/logging.py +68 -0
- cetk/settings.py +85 -0
- cetk/tools/__init__.py +1 -0
- cetk/tools/cetk_command.py +599 -0
- cetk/tools/manage.py +24 -0
- cetk/tools/utils.py +334 -0
- cetk/utils.py +62 -0
- cetk-0.0.4.dev0.dist-info/LICENSE +201 -0
- cetk-0.0.4.dev0.dist-info/METADATA +297 -0
- cetk-0.0.4.dev0.dist-info/RECORD +60 -0
- cetk-0.0.4.dev0.dist-info/WHEEL +5 -0
- cetk-0.0.4.dev0.dist-info/entry_points.txt +3 -0
- 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
|