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,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
|