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,413 @@
1
+ """Database models related to road-traffic."""
2
+
3
+ import ast
4
+ from itertools import groupby
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from django.contrib.gis.db import models
9
+ from django.contrib.gis.geos import Point
10
+ from django.db.models import F, Q
11
+ from django.utils.translation import gettext_lazy as _
12
+
13
+ from cetk.edb.const import CHAR_FIELD_LENGTH, CODE_FIELD_LENGTH, WGS84_SRID
14
+
15
+ from .common_models import Settings, Substance
16
+ from .source_models import SourceBase
17
+ from .timevar_models import FlowTimevar
18
+
19
+ # FlowTimeVar in fleets.py, import from there?
20
+
21
+
22
+ VELOCITY_CHOICES = list(range(20, 150, 10))
23
+
24
+
25
+ class RoadClassValidationError(Exception):
26
+ pass
27
+
28
+
29
+ def default_congestion_profile_data():
30
+ return str(24 * [7 * [1]])
31
+
32
+
33
+ class CongestionProfile(models.Model):
34
+ """A level of service time profile."""
35
+
36
+ class LevelOfService(models.IntegerChoices):
37
+ FREEFLOW = 1, _("freeflow")
38
+ HEAVY = 2, _("heavy")
39
+ SATURATED = 3, _("saturated")
40
+ STOPNGO = 4, _("stopngo")
41
+
42
+ name = models.CharField(
43
+ verbose_name="name of congestion profile",
44
+ max_length=CHAR_FIELD_LENGTH,
45
+ unique=True,
46
+ )
47
+ # traffic_condition: a 2d-field representing level of service
48
+ # typical conditions given for a typeweek
49
+ # hours are rows and days are columns
50
+
51
+ traffic_condition = models.CharField(
52
+ max_length=CODE_FIELD_LENGTH,
53
+ default=default_congestion_profile_data,
54
+ )
55
+
56
+ class Meta:
57
+ default_related_name = "congestion_profiles"
58
+
59
+ def __str__(self):
60
+ return self.name
61
+
62
+ def get_fractions(self, timevar):
63
+ """Return the fraction of each condition.
64
+ args:
65
+ timevar: time-variation to estimate fractions for
66
+ """
67
+
68
+ cond = np.array(ast.literal_eval(self.traffic_condition))
69
+ flow = np.array(ast.literal_eval(timevar.typeday))
70
+ flow_sum = flow.sum()
71
+ return {
72
+ los.name.lower(): np.where(cond == los, flow, 0).sum() / flow_sum
73
+ for los in self.LevelOfService
74
+ }
75
+
76
+ def to_series(self, time_index, timezone=None):
77
+ if timezone is None:
78
+ timezone = Settings.get_current().timezone
79
+ traffic_condition = np.array(ast.literal_eval(self.traffic_condition))
80
+ local_time_index = time_index.tz_convert(timezone)
81
+ return pd.Series(
82
+ traffic_condition[local_time_index.hour, local_time_index.weekday],
83
+ index=time_index,
84
+ )
85
+
86
+
87
+ class TrafficSituation(models.Model):
88
+ """A traffic situation."""
89
+
90
+ ts_id = models.CharField(
91
+ max_length=CODE_FIELD_LENGTH, verbose_name="traffic situation id", unique=True
92
+ )
93
+
94
+ class Meta:
95
+ default_related_name = "traffic_situations"
96
+
97
+
98
+ class VehicleEF(models.Model):
99
+ """An emission factor for a vehicle on a specific roadclass."""
100
+
101
+ # note! emission factors for vehicle exhaust are stored in si-units kg/(veh*m)
102
+ freeflow = models.FloatField(default=0)
103
+ heavy = models.FloatField(default=0)
104
+ saturated = models.FloatField(default=0)
105
+ stopngo = models.FloatField(default=0)
106
+ coldstart = models.FloatField(default=0)
107
+ traffic_situation = models.ForeignKey(TrafficSituation, on_delete=models.PROTECT)
108
+ substance = models.ForeignKey(Substance, on_delete=models.PROTECT, related_name="+")
109
+ vehicle = models.ForeignKey("Vehicle", on_delete=models.PROTECT)
110
+ fuel = models.ForeignKey("VehicleFuel", on_delete=models.PROTECT, related_name="+")
111
+
112
+ class Meta:
113
+ constraints = [
114
+ models.UniqueConstraint(
115
+ fields=("vehicle", "fuel", "substance", "traffic_situation"),
116
+ name="vehicleef_unique_in_efset",
117
+ ),
118
+ ]
119
+ default_related_name = "emissionfactors"
120
+ verbose_name_plural = "Vehicle emission factors"
121
+
122
+
123
+ class Vehicle(models.Model):
124
+ """A vehicle."""
125
+
126
+ name = models.CharField(max_length=CHAR_FIELD_LENGTH, unique=True)
127
+ info = models.CharField(max_length=CHAR_FIELD_LENGTH, null=True, blank=True)
128
+ isheavy = models.BooleanField(default=False)
129
+ max_speed = models.IntegerField(
130
+ null=True,
131
+ blank=True,
132
+ default=130,
133
+ choices=((s, f"{s}") for s in VELOCITY_CHOICES),
134
+ )
135
+
136
+ class Meta:
137
+ default_related_name = "vehicles"
138
+
139
+ def __str__(self):
140
+ """Unicode representation of vehicle."""
141
+ return self.name
142
+
143
+
144
+ class VehicleFuel(models.Model):
145
+ """A vehicle fuel or energy source."""
146
+
147
+ name = models.CharField(max_length=CHAR_FIELD_LENGTH, unique=True)
148
+
149
+ class Meta:
150
+ default_related_name = "vehicle_fuels"
151
+
152
+ def __str__(self):
153
+ return self.name
154
+
155
+
156
+ class VehicleFuelComb(models.Model):
157
+ """A valid combination of fuel and vehicle."""
158
+
159
+ vehicle = models.ForeignKey(Vehicle, on_delete=models.PROTECT)
160
+ fuel = models.ForeignKey(VehicleFuel, on_delete=models.PROTECT)
161
+ activitycode1 = models.ForeignKey(
162
+ "ActivityCode",
163
+ on_delete=models.PROTECT,
164
+ related_name="+",
165
+ null=True,
166
+ blank=True,
167
+ )
168
+ activitycode2 = models.ForeignKey(
169
+ "ActivityCode",
170
+ on_delete=models.PROTECT,
171
+ related_name="+",
172
+ null=True,
173
+ blank=True,
174
+ )
175
+ activitycode3 = models.ForeignKey(
176
+ "ActivityCode",
177
+ on_delete=models.PROTECT,
178
+ related_name="+",
179
+ null=True,
180
+ blank=True,
181
+ )
182
+
183
+ class Meta:
184
+ constraints = [
185
+ models.UniqueConstraint(
186
+ fields=("vehicle", "fuel"),
187
+ name="vehiclefuelcomb_vehicle_fuel_unique_together",
188
+ ),
189
+ ]
190
+ default_related_name = "vehiclefuelcombs"
191
+ indexes = [
192
+ models.Index(fields=("vehicle", "fuel")),
193
+ ]
194
+
195
+
196
+ class RoadSource(SourceBase):
197
+ """A road source."""
198
+
199
+ sourcetype = "road"
200
+
201
+ aadt = models.IntegerField("Annual average day traffic", default=0)
202
+ nolanes = models.IntegerField("Number of lanes", default=2)
203
+ SPEED_CHOICES = [(v, str(v)) for v in range(20, 150, 10)]
204
+ speed = models.IntegerField(
205
+ "Road sign speed [km/h]", default=70, choices=SPEED_CHOICES
206
+ )
207
+ width = models.FloatField("Road width [meters]", default=20.0)
208
+ median_strip_width = models.FloatField(default=0)
209
+ slope = models.IntegerField(
210
+ "Slope [°]", default=0, choices=((s, f"{s}") for s in range(-10, 10))
211
+ )
212
+ heavy_vehicle_share = models.FloatField(null=True, blank=True)
213
+ geom = models.LineStringField(
214
+ "the road coordinates", srid=WGS84_SRID, geography=True, db_index=True
215
+ )
216
+ roadclass = models.ForeignKey(
217
+ "RoadClass", on_delete=models.PROTECT, related_name="+"
218
+ )
219
+ fleet = models.ForeignKey(
220
+ "Fleet", on_delete=models.SET_NULL, related_name="+", null=True, blank=True
221
+ )
222
+ congestion_profile = models.ForeignKey(
223
+ CongestionProfile,
224
+ on_delete=models.SET_NULL,
225
+ related_name="+",
226
+ null=True,
227
+ blank=True,
228
+ )
229
+
230
+ class Meta:
231
+ default_related_name = "roadsources"
232
+ constraints = [
233
+ models.CheckConstraint(check=Q(aadt__gte=0), name="road_source_aadt_gte_0"),
234
+ models.CheckConstraint(
235
+ check=Q(nolanes__gt=0), name="road_source_nolanes_gt_0"
236
+ ),
237
+ models.CheckConstraint(check=Q(width__gt=0), name="road_source_width_gt_0"),
238
+ models.CheckConstraint(
239
+ check=(
240
+ Q(median_strip_width__gte=0) & Q(median_strip_width__lt=F("width"))
241
+ ),
242
+ name="road_source_median_strip_width_between_0_and_width",
243
+ ),
244
+ models.CheckConstraint(
245
+ check=Q(heavy_vehicle_share__range=(0, 1)),
246
+ name="road_source_heavy_vehicle_share_between_0_and_1",
247
+ ),
248
+ ]
249
+
250
+ @property
251
+ def light_vehicle_share(self):
252
+ if self.heavy_vehicle_share is None:
253
+ return None
254
+ return 1 - self.heavy_vehicle_share
255
+
256
+ @property
257
+ def drivable_width(self):
258
+ return self.width - self.median_strip_width
259
+
260
+ def get_heavy_vehicle_share(self):
261
+ if self.heavy_vehicle_share is None:
262
+ return self.fleet.default_heavy_vehicle_share
263
+ return self.heavy_vehicle_share
264
+
265
+ def get_segments(self, rel_dist, *, polar=True):
266
+ """Get segments for a road given a normalized distribution.
267
+
268
+ args:
269
+ rel_dist: sequence of relative receptor distances
270
+
271
+ returns:
272
+ tuple: (center_point_of_segment, orientation)
273
+ """
274
+ srid = Settings.get_current().srid
275
+ geom = self.geom.transform(srid, clone=True)
276
+ # calculate absolute distribution along road
277
+ dist = np.array(rel_dist) * geom.length
278
+ # create an array of nodes with any consecutive duplicates removed
279
+ nodes = np.array([k for k, g in groupby(geom.coords)])
280
+ # create cross sections along road
281
+ node_dist = np.zeros(nodes.shape[0])
282
+
283
+ i = 0 # index for road node
284
+ j = 0 # index for distance along road
285
+ for p2 in nodes[1:]:
286
+ p1 = nodes[i, :]
287
+ seg = p2 - p1 # segment vector
288
+ seg_length = np.linalg.norm(seg)
289
+ seg_dir = seg / seg_length # normalized segment direction
290
+ node_dist[i + 1] = node_dist[i] + seg_length # node dist
291
+ while j < len(dist) and dist[j] <= node_dist[i + 1]:
292
+ coords = p1 + seg_dir * (dist[j] - node_dist[i])
293
+ if polar:
294
+ seg_dir_degrees = 90 - np.degrees(
295
+ np.arctan2(seg_dir[1], seg_dir[0])
296
+ )
297
+ if seg_dir_degrees < 0:
298
+ seg_dir_degrees += 360
299
+ yield (
300
+ Point(*coords, srid=srid).transform(4326, clone=True),
301
+ seg_dir_degrees,
302
+ )
303
+ else:
304
+ yield (p1, p2)
305
+ j += 1
306
+ i += 1
307
+
308
+ def emission( # noqa: C901, PLR0912
309
+ self,
310
+ *,
311
+ by_vehicle=True,
312
+ substance=None,
313
+ ac1=None,
314
+ ac2=None,
315
+ ac3=None,
316
+ ):
317
+ """Calculate emission for a roadsource.
318
+
319
+ Default is to calculate emissions for all substances and vehicles
320
+
321
+ optional:
322
+ substance: only calculate emissions for this substance
323
+ by_vehicle: If False, sum of emissions for all vehicles is returned
324
+ ac1: a list of activitycode instances
325
+ ac2: a list of activitycode instances
326
+ ac3: a list of activitycode instances
327
+ """
328
+
329
+ vehicle_fuel_combs = {}
330
+ for rec in VehicleFuelComb.objects.all():
331
+ vehicle_fuel_combs[(rec.vehicle_id, rec.fuel_id)] = (
332
+ rec.activitycode1,
333
+ rec.activitycode2,
334
+ rec.activitycode3,
335
+ )
336
+ srid = Settings.get_current().srid
337
+ emis_by_veh_and_subst = {}
338
+ for fleet_member in self.fleet.vehicles.all():
339
+ veh = fleet_member.vehicle
340
+ if self.congestion_profile is None:
341
+ conditions = {
342
+ "freeflow": 1.0,
343
+ "heavy": 0.0,
344
+ "saturated": 0.0,
345
+ "stopngo": 0.0,
346
+ }
347
+ else:
348
+ timevar = fleet_member.timevar or FlowTimevar()
349
+ conditions = self.congestion_profile.get_fractions(timevar)
350
+
351
+ for fleet_member_fuel in fleet_member.fuels.all():
352
+ try:
353
+ activitycodes = vehicle_fuel_combs[
354
+ (veh.id, fleet_member_fuel.fuel_id)
355
+ ]
356
+ except KeyError:
357
+ # if vehicle fuel combination is not valid
358
+ # no emissions are calculated
359
+ continue
360
+
361
+ # Check if activity codes matches code filters
362
+ # if not, the vehicle-fuel combination is excluded
363
+ for ac, ac_filter in zip(activitycodes, (ac1, ac2, ac3)):
364
+ if ac_filter is not None and (
365
+ ac is None or not ac.matches(ac_filter)
366
+ ):
367
+ continue
368
+
369
+ efs = self.roadclass.traffic_situation.emissionfactors.filter(
370
+ vehicle=veh,
371
+ fuel=fleet_member_fuel.fuel,
372
+ )
373
+ if substance is not None:
374
+ efs = efs.filter(substance=substance)
375
+ for ef in efs:
376
+ heavy_share = (
377
+ self.heavy_vehicle_share
378
+ or self.fleet.default_heavy_vehicle_share
379
+ )
380
+ fleet_share = heavy_share if veh.isheavy else 1 - heavy_share
381
+
382
+ emis = (
383
+ self.aadt
384
+ * fleet_share
385
+ * fleet_member.fraction
386
+ * fleet_member_fuel.fraction
387
+ * self.geom.transform( # annual average day vehicle flow
388
+ srid, clone=True
389
+ ).length
390
+ * (
391
+ ef.coldstart * fleet_member.coldstart_fraction
392
+ + ef.freeflow * conditions["freeflow"]
393
+ + ef.heavy * conditions["heavy"]
394
+ + ef.saturated * conditions["saturated"]
395
+ + ef.stopngo * conditions["stopngo"]
396
+ )
397
+ / (3600 * 24) # convert day⁻¹ to s⁻¹
398
+ )
399
+ if veh not in emis_by_veh_and_subst:
400
+ emis_by_veh_and_subst[veh] = {}
401
+ if ef.substance not in emis_by_veh_and_subst[veh]:
402
+ emis_by_veh_and_subst[veh][ef.substance] = emis
403
+ else:
404
+ emis_by_veh_and_subst[veh][ef.substance] += emis
405
+
406
+ if by_vehicle is False:
407
+ agg_emis = {}
408
+ for substances in emis_by_veh_and_subst.values():
409
+ for subst, emis in substances.items():
410
+ agg_emis[subst] = agg_emis.get(subst, 0) + emis
411
+ return agg_emis
412
+
413
+ return emis_by_veh_and_subst