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,115 @@
1
+ import numpy as np
2
+ from django.db import IntegrityError
3
+
4
+ from cetk.edb.models import ColdstartTimevar, FlowTimevar, Timevar
5
+
6
+ from .utils import import_error, worksheet_to_dataframe
7
+
8
+
9
+ def import_timevarsheet(workbook, validation=False, sheetname="Timevar"):
10
+ return_message = []
11
+ timevar_data = workbook[sheetname].values
12
+ df_timevar = worksheet_to_dataframe(timevar_data)
13
+ if sheetname == "Timevar":
14
+ keyname = "emission"
15
+ if sheetname == "FlowTimevar":
16
+ keyname = "flow"
17
+ if sheetname == "ColdstartTimevar":
18
+ keyname = "coldstart"
19
+ timevar_dict = {keyname: {}}
20
+
21
+ # NB this only works if Excel file has exact same format
22
+ nr_timevars = (len(df_timevar["ID"]) + 1) // 27
23
+ for i in range(nr_timevars):
24
+ label = df_timevar["ID"][i * 27]
25
+ typeday = np.asarray(
26
+ df_timevar[
27
+ [
28
+ "monday",
29
+ "tuesday",
30
+ "wednesday",
31
+ "thursday",
32
+ "friday",
33
+ "saturday",
34
+ "sunday",
35
+ ]
36
+ ][i * 27 : i * 27 + 24]
37
+ )
38
+ month = np.asarray(df_timevar.iloc[i * 27 + 25, 2:14])
39
+ typeday_str = np.array2string(typeday).replace("\n", "").replace(" ", ", ")
40
+ month_str = np.array2string(month).replace("\n", "").replace(" ", ", ")
41
+ timevar_dict[keyname].update(
42
+ {label: {"typeday": typeday_str, "month": month_str}}
43
+ )
44
+ tv, return_append = import_timevars(
45
+ timevar_dict, overwrite=True, validation=validation
46
+ )
47
+ return_message += return_append
48
+ return_dict = {"timevar": {"updated or created": len(tv[keyname])}}
49
+ return return_dict, return_message
50
+
51
+
52
+ def import_timevars(timevar_data, overwrite=False, validation=False):
53
+ """import time-variation profiles."""
54
+
55
+ # Timevar instances must not be created by bulk_create as the save function
56
+ # is overloaded to calculate the normation constant.
57
+ def make_timevar(data, timevarclass, subname=None, validation=False):
58
+ retdict = {}
59
+ for name, timevar_data in data.items():
60
+ try:
61
+ typeday = timevar_data["typeday"]
62
+ month = timevar_data["month"]
63
+ if type(typeday) == list:
64
+ typeday = str(typeday)
65
+ if type(month) == list:
66
+ month = str(month)
67
+ if overwrite:
68
+ newobj, _ = timevarclass.objects.update_or_create(
69
+ name=name,
70
+ defaults={"typeday": typeday, "month": month},
71
+ )
72
+ else:
73
+ try:
74
+ newobj = timevarclass.objects.create(
75
+ name=name, typeday=typeday, month=month
76
+ )
77
+ except IntegrityError:
78
+ raise IntegrityError(
79
+ f"{timevarclass.__name__} {name} "
80
+ f"already exist in inventory."
81
+ )
82
+ retdict[name] = newobj
83
+ except KeyError:
84
+ return_message.append(
85
+ import_error(
86
+ f"Invalid specification of timevar {name}"
87
+ f", are 'typeday' and 'month' given?",
88
+ validation=validation,
89
+ )
90
+ )
91
+ return retdict
92
+
93
+ return_message = []
94
+ timevars = {}
95
+ for vartype, subdict in timevar_data.items():
96
+ if vartype == "emission":
97
+ timevars.setdefault("emission", {}).update(
98
+ make_timevar(timevar_data[vartype], Timevar)
99
+ )
100
+ elif vartype == "flow":
101
+ timevars.setdefault("flow", {}).update(
102
+ make_timevar(timevar_data[vartype], FlowTimevar)
103
+ )
104
+ elif vartype == "coldstart":
105
+ timevars.setdefault("coldstart", {}).update(
106
+ make_timevar(timevar_data[vartype], ColdstartTimevar)
107
+ )
108
+ else:
109
+ return_message.append(
110
+ import_error(
111
+ f"invalid time-variation type '{vartype}' specified",
112
+ validation=validation,
113
+ )
114
+ )
115
+ return timevars, return_message
@@ -0,0 +1,92 @@
1
+ import pandas as pd
2
+
3
+ from cetk.edb.cache import cache_queryset
4
+ from cetk.edb.models import Settings
5
+
6
+
7
+ class ValidationError(Exception):
8
+ """Error while validating emission data."""
9
+
10
+ pass
11
+
12
+
13
+ class EmptySheet(Exception):
14
+ """Error trying to read data from an empty sheet."""
15
+
16
+ pass
17
+
18
+
19
+ def nan2None(d):
20
+ out = d.copy()
21
+ for k, val in out.items():
22
+ out[k] = val if pd.notna(k) else val
23
+ return out
24
+
25
+
26
+ def import_error(message, return_message="", validation=False):
27
+ """import error management"""
28
+ if not validation:
29
+ raise ValidationError(f"VALIDATION: {message}")
30
+ else:
31
+ return_message += f"VALIDATION: {message}\n"
32
+ return return_message
33
+
34
+
35
+ def import_row_error(msg, row_nr, validation=False):
36
+ """import error managment with row nr"""
37
+ return import_error(f"{msg}, on row {row_nr}", validation=validation)
38
+
39
+
40
+ def import_row_substance_error(msg, row_nr, substance, validation=False):
41
+ """import error managment with row nr"""
42
+ return import_error(
43
+ f"{msg}, for '{substance}' on row {row_nr}", validation=validation
44
+ )
45
+
46
+
47
+ def cache_codeset(code_set):
48
+ """return dict {code: activitycode} for codeset."""
49
+ if code_set is None:
50
+ return {}
51
+ return cache_queryset(code_set.codes.all(), "code")
52
+
53
+
54
+ def cache_codesets():
55
+ """
56
+ return list of dictionaries with activity-codes
57
+ for all code-sets in Settings.
58
+ """
59
+ settings = Settings.get_current()
60
+ code_sets = {}
61
+ if settings.codeset1 is not None:
62
+ code_sets[settings.codeset1.slug] = cache_codeset(settings.codeset1)
63
+ if settings.codeset2 is not None:
64
+ code_sets[settings.codeset2.slug] = cache_codeset(settings.codeset2)
65
+ if settings.codeset3 is not None:
66
+ code_sets[settings.codeset3.slug] = cache_codeset(settings.codeset3)
67
+ return code_sets
68
+
69
+
70
+ def worksheet_to_dataframe(data):
71
+ data = list(data)
72
+ if not data:
73
+ raise EmptySheet("Sheet is empty")
74
+ df = pd.DataFrame(data)
75
+ # Set the first row as the header
76
+ df.columns = df.iloc[0]
77
+ df = df[1:].reset_index(drop=True)
78
+ # Remove completely empty rows
79
+ df = df.dropna(how="all")
80
+ # Remove completely empty columns without a header
81
+ df = df.loc[:, ~(df.isna().all() & df.columns.isna())]
82
+ return df
83
+
84
+
85
+ def get_substance_emission_columns(df):
86
+ """return columns with substance emissions."""
87
+ return [col for col in df.columns if col.startswith("subst:")]
88
+
89
+
90
+ def get_activity_rate_columns(df):
91
+ """return columns with activity rates."""
92
+ return [col for col in df.columns if col.startswith("act:")]
@@ -0,0 +1,207 @@
1
+ import os
2
+ from os.path import basename, splitext
3
+
4
+ import pandas as pd
5
+ import rasterio as rio
6
+
7
+ from cetk.edb.units import emission_unit_to_si
8
+
9
+
10
+ def with_rownr(msg, row_nr):
11
+ return f"{msg}, at row {row_nr}"
12
+
13
+
14
+ def with_rownr_and_substance(msg, row_nr, substance):
15
+ return f"{msg}, for '{substance}' at row {row_nr}"
16
+
17
+
18
+ def validate_columns(
19
+ df, required_cols, code_sets=None, substances=None, activities=None
20
+ ):
21
+ messages = []
22
+ missing_cols = [
23
+ col for col in required_cols if col not in df.columns and df.index.name != col
24
+ ]
25
+ if len(missing_cols) > 0:
26
+ messages.append(
27
+ f"GridSource: Missing required columns: '{', '.join(missing_cols)}'"
28
+ )
29
+
30
+ if code_sets is not None:
31
+ for codeset_slug in code_sets:
32
+ col_name = f"activitycode_{codeset_slug}"
33
+ if col_name not in df.columns:
34
+ messages.append(
35
+ f"GridSource: Missing column for code-set '{codeset_slug}'"
36
+ )
37
+ if substances is not None:
38
+ expected_substances = {
39
+ col[6:] for col in df.columns if col.startswith("subst:")
40
+ }
41
+ invalid_substances = expected_substances.difference(substances.keys())
42
+ if len(invalid_substances) > 0:
43
+ messages.append(f"GridSource: Invalid substances: {invalid_substances}")
44
+
45
+ if activities is not None:
46
+ expected_activities = {
47
+ col[4:].strip() for col in df.columns if col.startswith("act:")
48
+ }
49
+ invalid_activities = expected_activities.difference(activities.keys())
50
+ if len(invalid_activities) > 0:
51
+ messages.append(f"GridSource: Invalid activities: {invalid_activities}")
52
+ return messages
53
+
54
+
55
+ def validate_unit(values, row_nr):
56
+ """validate substance emission unit."""
57
+ messages = []
58
+ unit = values["emission_unit"]
59
+ try:
60
+ emission_unit_to_si(1.0, unit)
61
+ except (KeyError, ValueError) as err:
62
+ messages.append(with_rownr(f"GridSource: invalid unit, {err}", row_nr))
63
+ return messages
64
+
65
+
66
+ def validate_activitycodes(values, code_sets, row_nr, src=None):
67
+ """validate and set activity codes."""
68
+ messages = []
69
+ # get activitycodes
70
+ for code_ind, (codeset_slug, codes) in enumerate(code_sets.items(), 1):
71
+ code_attribute = f"activitycode{code_ind}"
72
+ code_col = f"activitycode_{codeset_slug}"
73
+ code = values[code_col]
74
+ if code is None:
75
+ messages.append(f"GridSource: missing value in column '{code_col}'")
76
+ try:
77
+ activitycode = codes[code]
78
+ except KeyError:
79
+ messages.append(f"GridSource: unknown code '{code}' in column '{code_col}'")
80
+ if src is not None:
81
+ setattr(src, code_attribute, activitycode)
82
+ return [with_rownr(msg, row_nr) for msg in messages]
83
+
84
+
85
+ def validate_timevar(values, timevars, row_nr, src=None):
86
+ """validate and set timevar."""
87
+ timevar_name = values["timevar"]
88
+ messages = []
89
+ if pd.notna(timevar_name):
90
+ try:
91
+ timevar = timevars[timevar_name]
92
+ except KeyError:
93
+ messages.append(f"GridSource: timevar '{timevar_name}' does not exist")
94
+ else:
95
+ timevar = None
96
+ if src is not None:
97
+ src.timevar = timevar
98
+ return [with_rownr(msg, row_nr) for msg in messages]
99
+
100
+
101
+ def data_to_raster(raster_name, raster_path, datadir, substance):
102
+ if raster_path is not None:
103
+ # generate raster name form path
104
+ if raster_name is None:
105
+ raster_name = splitext(basename(raster_path))[0]
106
+ # if placeholder is used for substance in path
107
+ # the raster-name will be given a suffix
108
+
109
+ if "{subst}" in raster_path:
110
+ if "{subst}" not in raster_name:
111
+ raster_name += "-{subst}"
112
+ rpath = raster_path.format(subst=substance)
113
+ rname = raster_name.format(subst=substance)
114
+ else:
115
+ rpath = raster_path
116
+ rname = raster_name
117
+
118
+ if not os.path.isabs(rpath):
119
+ rpath = os.path.join(datadir, rpath)
120
+ else:
121
+ rpath = None
122
+ rname = (
123
+ raster_name.format(subst=substance)
124
+ if "{subst}" in raster_name
125
+ else raster_name
126
+ )
127
+ return rname, rpath
128
+
129
+
130
+ def validate_raster(
131
+ values: dict,
132
+ raster_names: list,
133
+ datadir: str,
134
+ raster_dict: dict,
135
+ row_nr: int,
136
+ substance: str,
137
+ ):
138
+ messages = []
139
+ rasters_without_path = []
140
+ raster_path = values["path"] if not pd.isna(values["path"]) else None
141
+ raster_name = values["rastername"] if not pd.isna(values["rastername"]) else None
142
+ if raster_name is None and raster_path is None:
143
+ messages.append("neither rastername nor path specified")
144
+ rname, rpath = data_to_raster(raster_name, raster_path, datadir, substance)
145
+ if rpath is not None:
146
+ if not os.path.exists(rpath):
147
+ messages.append(f"raster with path {rpath} does not exist")
148
+ elif rname not in raster_dict:
149
+ with rio.open(rpath, "r") as dset:
150
+ try:
151
+ _ = int(dset.crs.to_authority()[1])
152
+ except ValueError:
153
+ messages.append(
154
+ "raster coordinate reference system has no epsg code"
155
+ )
156
+ rdata = dset.read()
157
+ raster_dict.setdefault(rname, {})["sum"] = rdata[rdata != dset.nodata].sum()
158
+ raster_dict[rname]["path"] = rpath
159
+ else:
160
+ rasters_without_path.append(rname)
161
+
162
+ for rname in rasters_without_path:
163
+ if rname not in raster_names and rname not in raster_dict:
164
+ messages.append(
165
+ f"raster '{rname}' not found in database, and no path specified"
166
+ )
167
+
168
+ return [with_rownr_and_substance(msg, row_nr, substance) for msg in messages]
169
+
170
+
171
+ def validate_emission(values, row_nr, substance):
172
+ """validate emission value"""
173
+ messages = []
174
+ emis_value = values[f"subst:{substance}"]
175
+ if emis_value != "sum":
176
+ try:
177
+ val = float(emis_value)
178
+ if val < 0:
179
+ messages.append(
180
+ with_rownr_and_substance("emission value < 0", row_nr, substance)
181
+ )
182
+ except ValueError:
183
+ messages.append(
184
+ with_rownr_and_substance(
185
+ f"Invalid emission value '{emis_value}'", row_nr, substance
186
+ )
187
+ )
188
+ return messages
189
+
190
+
191
+ def validate_activity(values, row_nr, activity):
192
+ """validate emission value"""
193
+ messages = []
194
+ rate = values[f"act:{activity}"]
195
+ if rate != "sum":
196
+ try:
197
+ if float(rate) < 0:
198
+ messages.append(
199
+ with_rownr_and_substance("activity rate < 0", row_nr, activity)
200
+ )
201
+ except ValueError:
202
+ messages.append(
203
+ with_rownr_and_substance(
204
+ f"invalid activity rate '{rate}'", row_nr, activity
205
+ )
206
+ )
207
+ return messages
cetk/edb/ltreefield.py ADDED
@@ -0,0 +1,76 @@
1
+ """A django field definition for Postgres hierarchical ltree field.
2
+
3
+ Source: https://github.com/whitglint/ltreefield
4
+ license: MIT, https://github.com/whitglint/ltreefield/blob/master/LICENSE
5
+ """
6
+
7
+ from django.db import models
8
+ from django.db.models import Lookup
9
+
10
+
11
+ class LtreeField(models.CharField):
12
+ description = "SQLite alternative to ltree (up to %(max_length)s)"
13
+
14
+ def __init__(self, *args, **kwargs):
15
+ kwargs["max_length"] = 256
16
+ super().__init__(*args, **kwargs)
17
+
18
+ # def db_type(self, connection):
19
+ # return "ltree"
20
+
21
+ def deconstruct(self):
22
+ name, path, args, kwargs = super().deconstruct()
23
+ del kwargs["max_length"]
24
+ return name, path, args, kwargs
25
+
26
+
27
+ class AncestorOrEqual(Lookup):
28
+ lookup_name = "aore"
29
+
30
+ def as_sql(self, qn, connection):
31
+ lhs, lhs_params = self.process_lhs(qn, connection)
32
+ rhs, rhs_params = self.process_rhs(qn, connection)
33
+ params = lhs_params + rhs_params
34
+ if len(rhs_params) > 1:
35
+ raise ValueError("This alternative for LTree is not implemented")
36
+ elif len(rhs_params[0]) > 2:
37
+ return (
38
+ '%s like %s or %s like %s or %s like "" '
39
+ % (lhs, rhs, lhs, rhs_params[0][:-2], lhs),
40
+ params,
41
+ )
42
+ else:
43
+ return '%s like %s or %s like "" ' % (lhs, rhs, lhs), params
44
+
45
+
46
+ LtreeField.register_lookup(AncestorOrEqual)
47
+
48
+
49
+ class DescendantOrEqual(Lookup):
50
+ lookup_name = "dore"
51
+
52
+ def as_sql(self, qn, connection):
53
+ lhs, lhs_params = self.process_lhs(qn, connection)
54
+ rhs, rhs_params = self.process_rhs(qn, connection)
55
+ params = lhs_params + rhs_params
56
+ if len(params) > 1:
57
+ raise ValueError("This alternative for LTree is not implemented")
58
+ else:
59
+ params = [params[0] + "%"]
60
+ return "%s like %s" % (lhs, rhs), params
61
+
62
+
63
+ LtreeField.register_lookup(DescendantOrEqual)
64
+
65
+
66
+ class Match(Lookup):
67
+ lookup_name = "match"
68
+
69
+ def as_sql(self, qn, connection):
70
+ lhs, lhs_params = self.process_lhs(qn, connection)
71
+ rhs, rhs_params = self.process_rhs(qn, connection)
72
+ params = lhs_params + rhs_params
73
+ return "%s like %s" % (lhs, rhs), params
74
+
75
+
76
+ LtreeField.register_lookup(Match)