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,399 @@
1
+ """Emission database models."""
2
+
3
+
4
+ from django.contrib.gis.db import models
5
+
6
+ from cetk.edb.const import CHAR_FIELD_LENGTH, WGS84_SRID
7
+ from cetk.edb.ltreefield import LtreeField
8
+ from cetk.edb.models.base import BaseNamedModel, NaturalKeyManager
9
+
10
+ SRID = WGS84_SRID
11
+
12
+
13
+ class SourceSubstance(models.Model):
14
+ """An abstract models for source substance emissions."""
15
+
16
+ value = models.FloatField(default=0, verbose_name="source emission")
17
+ substance = models.ForeignKey(
18
+ "Substance", on_delete=models.PROTECT, related_name="+"
19
+ )
20
+ updated = models.DateTimeField(verbose_name="date of last update", auto_now=True)
21
+
22
+ class Meta:
23
+ abstract = True
24
+ default_related_name = "substances"
25
+ unique_together = ("source", "substance")
26
+
27
+ def __str__(self):
28
+ return self.substance.name
29
+
30
+
31
+ def default_vertical_dist():
32
+ return "[[5.0, 1.0]]"
33
+
34
+
35
+ class VerticalDist(BaseNamedModel):
36
+ """Vertical distribution of GridSource emissions."""
37
+
38
+ objects = NaturalKeyManager()
39
+
40
+ name = models.CharField(max_length=64)
41
+ slug = models.SlugField(max_length=64, unique=True)
42
+ weights = models.CharField(
43
+ max_length=CHAR_FIELD_LENGTH, default=default_vertical_dist
44
+ )
45
+
46
+ class Meta:
47
+ db_table = "vertical_distributions"
48
+ default_related_name = "vertical_distributions"
49
+
50
+ natural_key_fields = "slug"
51
+
52
+ def natural_key(self):
53
+ return self.slug
54
+
55
+ def __str__(self):
56
+ return self.name
57
+
58
+
59
+ class Activity(models.Model):
60
+ """An emitting activity."""
61
+
62
+ name = models.CharField(
63
+ verbose_name="name of activity", max_length=CHAR_FIELD_LENGTH, unique=True
64
+ )
65
+ unit = models.CharField(
66
+ verbose_name="unit of activity", max_length=CHAR_FIELD_LENGTH
67
+ )
68
+
69
+ class Meta:
70
+ default_related_name = "activities"
71
+
72
+ def __str__(self):
73
+ """Return a unicode representation of this activity."""
74
+ return self.name
75
+
76
+
77
+ class CodeSet(BaseNamedModel):
78
+ """A set of activity codes."""
79
+
80
+ objects = NaturalKeyManager()
81
+
82
+ name = models.CharField(max_length=64)
83
+ slug = models.SlugField(max_length=64, unique=True)
84
+ description = models.CharField(
85
+ verbose_name="description", max_length=200, null=True, blank=True
86
+ )
87
+
88
+ class Meta:
89
+ db_table = "codesets"
90
+ default_related_name = "codesets"
91
+
92
+ natural_key_fields = "slug"
93
+
94
+ def natural_key(self):
95
+ return self.slug
96
+
97
+ def __str__(self):
98
+ return self.name
99
+
100
+
101
+ class ActivityCode(models.Model):
102
+ """An abstract model for an activity code."""
103
+
104
+ objects = NaturalKeyManager()
105
+ code = LtreeField(verbose_name="activity code")
106
+ label = models.CharField(verbose_name="activity code label", max_length=100)
107
+ code_set = models.ForeignKey(
108
+ CodeSet, on_delete=models.CASCADE, related_name="codes"
109
+ )
110
+ vertical_dist = models.ForeignKey(
111
+ VerticalDist, on_delete=models.SET_NULL, related_name="+", null=True, blank=True
112
+ )
113
+ natural_key_fields = ("code_set__slug", "code")
114
+
115
+ class Meta:
116
+ constraints = [
117
+ models.UniqueConstraint(
118
+ fields=["code_set", "code"], name="unique code in codeset"
119
+ )
120
+ ]
121
+
122
+ def natural_key(self):
123
+ return (self.code_set.slug, self.code)
124
+
125
+ def __lt__(self, other):
126
+ return self.code < other.code
127
+
128
+ def __str__(self):
129
+ """Return a unicode representation of this activity code."""
130
+ return self.code
131
+
132
+ def matches(self, filters):
133
+ """Compare with a (list of) filter code(s).
134
+ args
135
+ filters: list of accepted codes
136
+ Filters should be '.'-separated codes
137
+ comparison is only made for code levels included in filter
138
+ i.e. the code 1.A.2.i will match the filter 1.A
139
+ """
140
+ code_parts = self.code.split(".")
141
+ for f in filters:
142
+ matches = True
143
+ filter_parts = f.code.split(".")
144
+ # filter has more code-parts than code
145
+ if len(filter_parts) > len(code_parts):
146
+ matches = False
147
+ continue
148
+ # compare code with filter part by part
149
+ for i, filter_part in enumerate(filter_parts):
150
+ if filter_part != code_parts[i]:
151
+ matches = False
152
+ break
153
+ if matches:
154
+ return matches
155
+ return matches
156
+
157
+ def get_decendents(self):
158
+ return self.get_decendents_and_self().exclude(pk=self.pk)
159
+
160
+ def get_decendents_and_self(self):
161
+ return ActivityCode.objects.filter(code__dore=self.code).filter(
162
+ code_set=self.code_set
163
+ )
164
+
165
+ def get_ancestors(self):
166
+ return self.get_ancestors_and_self().exclude(pk=self.pk)
167
+
168
+ def get_ancestors_and_self(self):
169
+ return ActivityCode.objects.filter(code__aore=self.code).filter(
170
+ code_set=self.code_set
171
+ )
172
+
173
+ def get_parent(self):
174
+ if "." not in self.code:
175
+ raise RuntimeError(
176
+ f"The code: {self} cannot have a parent as it is a root node"
177
+ )
178
+ return ActivityCode.objects.get(
179
+ code__match=".".join(self.code.split(".")[:-1]), code_set=self.code_set
180
+ )
181
+
182
+ def get_siblings_and_self(self):
183
+ return ActivityCode.objects.filter(
184
+ code__match=".".join(self.code.split(".")[:-1]) + "._",
185
+ code_set=self.code_set,
186
+ )
187
+
188
+ def get_siblings(self):
189
+ return self.get_siblings_and_self().exclude(pk=self.pk)
190
+
191
+ def get_children(self):
192
+ return self.code_set.codes.filter(code__match=self.code + "._")
193
+
194
+ def is_leaf(self):
195
+ """Return True if code is a leaf (i.e. has no sub-codes)."""
196
+ return not self.get_decendents().exists()
197
+
198
+
199
+ class SourceBase(models.Model):
200
+ """Abstract base model for an emission source."""
201
+
202
+ name = models.CharField("name", max_length=CHAR_FIELD_LENGTH, blank=False)
203
+ created = models.DateTimeField(verbose_name="time of creation", auto_now_add=True)
204
+ updated = models.DateTimeField(verbose_name="time of last update", auto_now=True)
205
+ # TODO: do such tags work for Spatialite? Best to keep for CLAIR compatability?
206
+ tags = models.JSONField(
207
+ verbose_name="user-defined key-value pairs", blank=True, null=True
208
+ )
209
+
210
+ class Meta:
211
+ abstract = True
212
+
213
+ def __str__(self):
214
+ """Return a unicode representation of this source."""
215
+ return self.name
216
+
217
+
218
+ class PointAreaGridSourceBase(SourceBase):
219
+ """Abstract base model for point, area and grid sources"""
220
+
221
+ timevar = models.ForeignKey(
222
+ "Timevar", on_delete=models.SET_NULL, related_name="+", null=True, blank=True
223
+ )
224
+ activitycode1 = models.ForeignKey(
225
+ "ActivityCode",
226
+ on_delete=models.PROTECT,
227
+ related_name="+",
228
+ null=True,
229
+ blank=True,
230
+ )
231
+ activitycode2 = models.ForeignKey(
232
+ "ActivityCode",
233
+ on_delete=models.PROTECT,
234
+ related_name="+",
235
+ null=True,
236
+ blank=True,
237
+ )
238
+ activitycode3 = models.ForeignKey(
239
+ "ActivityCode",
240
+ on_delete=models.PROTECT,
241
+ related_name="+",
242
+ null=True,
243
+ blank=True,
244
+ )
245
+
246
+ class Meta:
247
+ abstract = True
248
+ indexes = [
249
+ models.Index(
250
+ fields=("activitycode1", "activitycode2", "activitycode3"),
251
+ name="%(class)s_activities_idx",
252
+ ),
253
+ ]
254
+
255
+
256
+ class Facility(SourceBase):
257
+ """A facility."""
258
+
259
+ official_id = models.CharField(
260
+ "official_id",
261
+ max_length=CHAR_FIELD_LENGTH,
262
+ blank=False,
263
+ db_index=True,
264
+ unique=True,
265
+ )
266
+
267
+ class Meta:
268
+ default_related_name = "facilities"
269
+
270
+ def __str__(self):
271
+ return str(self.official_id)
272
+
273
+ def __repr__(self):
274
+ return str(self)
275
+
276
+
277
+ class PointSource(PointAreaGridSourceBase):
278
+ """A point-source."""
279
+
280
+ sourcetype = "point"
281
+
282
+ chimney_height = models.FloatField("chimney height [m]", default=0)
283
+ chimney_outer_diameter = models.FloatField(
284
+ "chimney outer diameter [m]", default=1.0
285
+ )
286
+ chimney_inner_diameter = models.FloatField(
287
+ "chimney inner diameter [m]", default=0.9
288
+ )
289
+ chimney_gas_speed = models.FloatField("chimney gas speed [m/s]", default=1.0)
290
+ chimney_gas_temperature = models.FloatField(
291
+ "chimney gas temperature [K]", default=373.0
292
+ )
293
+ house_width = models.IntegerField(
294
+ "house width [m] (to estimate down draft)", default=0
295
+ )
296
+ house_height = models.IntegerField(
297
+ "house height [m] (to estimate down draft)", default=0
298
+ )
299
+ geom = models.PointField(
300
+ "the position of the point-source",
301
+ srid=WGS84_SRID,
302
+ geography=True,
303
+ db_index=True,
304
+ )
305
+ facility = models.ForeignKey(
306
+ "Facility", on_delete=models.SET_NULL, null=True, blank=True
307
+ )
308
+
309
+ class Meta(PointAreaGridSourceBase.Meta):
310
+ default_related_name = "pointsources"
311
+ unique_together = ("facility", "name")
312
+
313
+
314
+ class PointSourceSubstance(SourceSubstance):
315
+ """A point-source substance emission."""
316
+
317
+ source = models.ForeignKey("PointSource", on_delete=models.CASCADE)
318
+
319
+
320
+ class SourceActivity(models.Model):
321
+ """Base class for an emitting activity."""
322
+
323
+ rate = models.FloatField(verbose_name="activity rate")
324
+ activity = models.ForeignKey("Activity", on_delete=models.PROTECT, related_name="+")
325
+
326
+ class Meta:
327
+ abstract = True
328
+ constraints = [
329
+ models.UniqueConstraint(
330
+ fields=("source", "activity"),
331
+ name="%(class)s_unique_activity_in_source",
332
+ ),
333
+ ]
334
+ default_related_name = "activities"
335
+
336
+
337
+ class PointSourceActivity(SourceActivity):
338
+ """An emitting activity of a point source."""
339
+
340
+ source = models.ForeignKey("PointSource", on_delete=models.CASCADE)
341
+
342
+ def __str__(self):
343
+ return "{}".format(self.activity.name)
344
+
345
+
346
+ class EmissionFactor(models.Model):
347
+ """An emission factor."""
348
+
349
+ factor = models.FloatField(default=0)
350
+ activity = models.ForeignKey("Activity", on_delete=models.PROTECT)
351
+ substance = models.ForeignKey(
352
+ "Substance", on_delete=models.PROTECT, related_name="+"
353
+ )
354
+
355
+ class Meta:
356
+ constraints = [
357
+ models.UniqueConstraint(
358
+ fields=("activity", "substance"),
359
+ name="emissionfactor_activity_substance_unique_together",
360
+ ),
361
+ ]
362
+ default_related_name = "emissionfactors"
363
+
364
+
365
+ class AreaSource(PointAreaGridSourceBase):
366
+ """An area source."""
367
+
368
+ sourcetype = "area"
369
+
370
+ geom = models.PolygonField(
371
+ "the extent of the area source", srid=WGS84_SRID, geography=True, db_index=True
372
+ )
373
+ facility = models.ForeignKey(
374
+ "Facility", on_delete=models.SET_NULL, null=True, blank=True
375
+ )
376
+
377
+ class Meta(PointAreaGridSourceBase.Meta):
378
+ constraints = [
379
+ models.UniqueConstraint(
380
+ fields=("facility", "name"),
381
+ name="areasource_unique_facility_and_name",
382
+ ),
383
+ ]
384
+ default_related_name = "areasources"
385
+
386
+
387
+ class AreaSourceActivity(SourceActivity):
388
+ """An emitting activity of an area source."""
389
+
390
+ source = models.ForeignKey("AreaSource", on_delete=models.CASCADE)
391
+
392
+ def __str__(self):
393
+ return f"{self.activity.name}"
394
+
395
+
396
+ class AreaSourceSubstance(SourceSubstance):
397
+ """An area-source substance emission."""
398
+
399
+ source = models.ForeignKey("AreaSource", on_delete=models.CASCADE)
@@ -0,0 +1,111 @@
1
+ import ast
2
+
3
+ import numpy as np
4
+ import pandas as pd
5
+ from django.contrib.gis.db import models
6
+
7
+ from cetk.edb.const import CHAR_FIELD_LENGTH
8
+ from cetk.edb.models.common_models import Settings
9
+
10
+
11
+ def default_timevar_typeday():
12
+ return str(24 * [7 * [100.0]])
13
+
14
+
15
+ def default_timevar_month():
16
+ return str(12 * [100.0])
17
+
18
+
19
+ def get_normalization_constant(typeday, month, timezone):
20
+ commonyear = pd.date_range("2018", periods=24 * 365, freq="h", tz=timezone)
21
+ values = typeday[commonyear.hour, commonyear.weekday] * month[commonyear.month - 1]
22
+ return len(values) / values.sum()
23
+
24
+
25
+ def timevar_to_series(time_index, *timevars, timezone=None):
26
+ """Produce normalized time series from one or more timevar instances."""
27
+ if not timevars:
28
+ raise TypeError("at least one timevar must be given")
29
+ if timezone is None:
30
+ timezone = Settings.get_current().timezone
31
+ typeday = np.multiply.reduce([ast.literal_eval(t.typeday) for t in timevars])
32
+ month = np.multiply.reduce([ast.literal_eval(t.month) for t in timevars])
33
+ if len(timevars) > 1:
34
+ normalization_constant = get_normalization_constant(typeday, month, timezone)
35
+ else:
36
+ normalization_constant = timevars[0].normalization_constant
37
+
38
+ local_time_index = time_index.tz_convert(timezone)
39
+ values = (
40
+ typeday[local_time_index.hour, local_time_index.weekday]
41
+ * month[local_time_index.month - 1]
42
+ )
43
+ return pd.Series(normalization_constant * values, index=time_index)
44
+
45
+
46
+ def timevar_normalize(timevar, timezone=None):
47
+ """Set the normalization constants on a timevar instance."""
48
+ if timezone is None:
49
+ timezone = Settings.get_current().timezone
50
+ typeday = np.array(ast.literal_eval(timevar.typeday))
51
+ month = np.array(ast.literal_eval(timevar.month))
52
+ timevar.typeday_sum = typeday.sum()
53
+ timevar._normalization_constant = get_normalization_constant(
54
+ typeday, month, timezone
55
+ )
56
+ return timevar
57
+
58
+
59
+ class TimevarBase(models.Model):
60
+ name = models.CharField(max_length=CHAR_FIELD_LENGTH, unique=True)
61
+
62
+ typeday = models.CharField(
63
+ max_length=10 * len(default_timevar_typeday()),
64
+ default=default_timevar_typeday(),
65
+ )
66
+ month = models.CharField(
67
+ max_length=10 * len(default_timevar_month()), default=default_timevar_month()
68
+ )
69
+
70
+ # pre-calculated normalization constants
71
+ typeday_sum = models.FloatField(editable=False)
72
+ _normalization_constant = models.FloatField(editable=False)
73
+
74
+ class Meta:
75
+ abstract = True
76
+
77
+ def __str__(self):
78
+ """Return a unicode representation of this timevariation."""
79
+ return self.name
80
+
81
+ @property
82
+ def normalization_constant(self):
83
+ if self._normalization_constant is None:
84
+ timevar_normalize(self)
85
+ return self._normalization_constant
86
+
87
+ def save(self, *args, **kwargs):
88
+ """Overloads save to ensure normalizing factors are calculated."""
89
+ timevar_normalize(self)
90
+ super(TimevarBase, self).save(*args, **kwargs)
91
+
92
+
93
+ class Timevar(TimevarBase):
94
+ """A source time-variation profile."""
95
+
96
+ class Meta(TimevarBase.Meta):
97
+ default_related_name = "timevars"
98
+
99
+
100
+ class FlowTimevar(TimevarBase):
101
+ """A road time-variation profile."""
102
+
103
+ class Meta(TimevarBase.Meta):
104
+ default_related_name = "flow_timevars"
105
+
106
+
107
+ class ColdstartTimevar(TimevarBase):
108
+ """A vehicle cold start time-variation profile."""
109
+
110
+ class Meta(TimevarBase.Meta):
111
+ default_related_name = "coldstart_timevars"
@@ -0,0 +1 @@
1
+ from .rasterizer import EmissionRasterizer, Output # noqa: F401