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
cetk/__init__.py ADDED
@@ -0,0 +1,87 @@
1
+ """cetk, a python library for editing Clair emission inventories offline."""
2
+
3
+ __version__ = "0.0.4.dev"
4
+
5
+ import os
6
+
7
+ import django
8
+ from django.conf import settings
9
+
10
+ from cetk import logging
11
+
12
+ DEFAULT_SETTINGS = {
13
+ "DEBUG": False,
14
+ "INSTALLED_APPS": ["django.contrib.gis", "cetk.edb.apps.EdbConfig"],
15
+ "LANGUAGE_CODE": "en-us",
16
+ "TIME_ZONE": "UTC",
17
+ "USE_I18N": True,
18
+ "USE_TZ": True,
19
+ }
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+
24
+ def configure():
25
+ if hasattr(settings, "configured") and not settings.configured:
26
+ default_config_home = os.path.expanduser("~/.config")
27
+ config_home = os.path.join(
28
+ os.environ.get("XDG_CONFIG_HOME", default_config_home), "eclair"
29
+ )
30
+ default_db = os.path.abspath(os.path.join(config_home, "eclair.gpkg"))
31
+ db_path = os.environ.get("CETK_DATABASE_PATH", default_db)
32
+ DEBUG = os.environ.get("CETK_DEBUG", False)
33
+ level = "DEBUG" if DEBUG else "INFO"
34
+ if level == "DEBUG":
35
+ cetk_logger = {"handlers": ["console_debug"], "level": level}
36
+ else:
37
+ cetk_logger = {"handlers": ["console"], "level": level}
38
+
39
+ if "FLATPAK_ID" in os.environ:
40
+ spatialite_path = os.environ.get(
41
+ "SPATIALITE_LIBRARY_PATH", "/app/lib/mod_spatialite.so"
42
+ )
43
+ elif os.name == "posix":
44
+ spatialite_path = os.environ.get(
45
+ "SPATIALITE_LIBRARY_PATH", "/usr/lib64/mod_spatialite.so"
46
+ )
47
+ elif os.name == "nt":
48
+ spatialite_path = os.environ.get(
49
+ "SPATIALITE_LIBRARY_PATH", r"C:\OSGeo4W\bin\mod_spatialite.dll"
50
+ )
51
+ else:
52
+ spatialite_path = os.environ.get(
53
+ "SPATIALITE_LIBRARY_PATH", "/usr/lib64/mod_spatialite.so"
54
+ )
55
+
56
+ settings.configure(
57
+ **DEFAULT_SETTINGS,
58
+ SPATIALITE_LIBRARY_PATH=spatialite_path,
59
+ DATABASES={
60
+ "default": {
61
+ "ENGINE": "django.contrib.gis.db.backends.spatialite",
62
+ "NAME": db_path,
63
+ "TEST": {"NAME": os.path.join(config_home, "test.eclair.gpkg")},
64
+ },
65
+ },
66
+ LOGGING={
67
+ "version": 1,
68
+ "disable_existing_loggers": False,
69
+ "formatters": {
70
+ "cetk": {"format": "%(levelname)s: %(message)s"}, # noqa
71
+ "cetk_debug": {
72
+ "format": "%(asctime)s %(levelname)s: %(name)s %(message)s"
73
+ }, # noqa
74
+ },
75
+ "handlers": {
76
+ "console": {"class": "logging.StreamHandler", "formatter": "cetk"},
77
+ "console_debug": {
78
+ "class": "logging.StreamHandler",
79
+ "formatter": "cetk_debug",
80
+ },
81
+ },
82
+ "loggers": {"cetk": cetk_logger},
83
+ }
84
+ )
85
+ django.setup()
86
+ log.debug("configured django")
87
+ return settings
cetk/db.py ADDED
@@ -0,0 +1,41 @@
1
+ """Database management."""
2
+
3
+ import os
4
+ import sqlite3
5
+ import subprocess
6
+
7
+
8
+ class CetkDatabaseError(Exception):
9
+ pass
10
+
11
+
12
+ def run_migrate(db_path=None):
13
+ env = {**os.environ}
14
+ if db_path is not None:
15
+ env["CETK_DATABASE_PATH"] = str(db_path)
16
+
17
+ # handle error in geodjango starting with sqlite 3.36
18
+ # see: https://groups.google.com/g/spatialite-users/c/SnNZt4AGm_o
19
+ sqlite_version = list(map(int, sqlite3.sqlite_version.split(".")))
20
+ if sqlite_version[0] >= 3 and sqlite_version[1] >= 36:
21
+ proc = subprocess.run(
22
+ [
23
+ "cetkmanage",
24
+ "shell",
25
+ "-c",
26
+ "import django;django.db.connection.cursor().execute('SELECT InitSpatialMetaData(1);')", # noqa: E501
27
+ ],
28
+ stdout=subprocess.PIPE,
29
+ stderr=subprocess.PIPE,
30
+ check=True,
31
+ env=env,
32
+ )
33
+
34
+ proc = subprocess.run(
35
+ ["cetkmanage", "migrate"],
36
+ stdout=subprocess.PIPE,
37
+ stderr=subprocess.PIPE,
38
+ check=True,
39
+ env=env,
40
+ )
41
+ return proc.stdout, proc.stderr
cetk/edb/__init__.py ADDED
File without changes
cetk/edb/apps.py ADDED
@@ -0,0 +1,9 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class EdbConfig(AppConfig):
5
+ name = "cetk.edb"
6
+ verbose_name = "CETK Emission Database"
7
+
8
+ def ready(self):
9
+ from . import signals # noqa
cetk/edb/cache.py ADDED
@@ -0,0 +1,249 @@
1
+ """Module to manage caching of emission data."""
2
+
3
+ import pickle
4
+ from collections import namedtuple
5
+ from pathlib import Path
6
+ from tempfile import TemporaryDirectory
7
+
8
+ # nr of records per page
9
+ PAGE_SIZE = 10000
10
+
11
+
12
+ # source specific parameters to be added to dataset
13
+ # parameters must be included in emission query
14
+ STATIC_DATA_PARAMETERS = {
15
+ "road": [
16
+ "length",
17
+ "aadt",
18
+ "nolanes",
19
+ "speed",
20
+ "width",
21
+ "slope",
22
+ "heavy_vehicle_share",
23
+ ],
24
+ "point": [
25
+ "chimney_height",
26
+ "chimney_outer_diameter",
27
+ "chimney_inner_diameter",
28
+ "chimney_gas_speed",
29
+ "chimney_gas_temperature",
30
+ "house_height",
31
+ "house_width",
32
+ ],
33
+ }
34
+
35
+
36
+ class NotInCacheError(Exception):
37
+ """Page not found in cache"""
38
+
39
+
40
+ class EmissionCache:
41
+
42
+ """An emission out-of-memory cache."""
43
+
44
+ def __init__(self, querysets):
45
+ """create an EmissionCache
46
+
47
+ args
48
+ querysets: dict with a sourcetype as key and a cursor queryset as value
49
+
50
+ """
51
+
52
+ self.cache_dir = None
53
+
54
+ # page count per sourcetype
55
+ self._pages = {}
56
+
57
+ # temporary in-memory storage
58
+ # write to "page" on disk when recs_in_memory > PAGE_SIZE
59
+ self._features = {}
60
+ self._weights = {}
61
+ self._emissions = {}
62
+ # counter for features
63
+ self._feature_index = {"point": 0, "road": 0}
64
+ self._rasterized_sources = set()
65
+
66
+ # nr of emission records not yet written to disk
67
+ self._recs_in_memory = {"road": 0, "point": 0, "area": 0, "grid": 0}
68
+
69
+ # map feature id to index in narc variable
70
+ self.feature_ids = {"road": {}, "point": {}}
71
+
72
+ # keep track of all sources that have already been gridded
73
+ self.gridded_sources = {
74
+ "road": set(),
75
+ "point": set(),
76
+ "area": set(),
77
+ "grid": set(),
78
+ }
79
+
80
+ # create mapping between column name and index using a named tuple
81
+ self.col_maps = {}
82
+ for sourcetype, qs in querysets.items():
83
+ if qs is not None:
84
+ desc = qs.cursor.description
85
+ column_index_tuple = namedtuple("ColumnIndex", [col[0] for col in desc])
86
+ self.col_maps[sourcetype] = column_index_tuple(*range(len(desc)))
87
+
88
+ def __enter__(self):
89
+ # create temporary directory for cache
90
+ self.cache_dir = TemporaryDirectory(prefix="cache_")
91
+ return self
92
+
93
+ def __exit__(self, exc_type, exc_val, exc_tb):
94
+ if self.cache_dir is not None:
95
+ self.cache_dir.cleanup()
96
+ self.cache_dir = None
97
+
98
+ def has_sourcetype(self, sourcetype):
99
+ return sourcetype in self._pages
100
+
101
+ def has_substance(self, sourcetype, substance_id):
102
+ return self.emis_page_count(sourcetype, substance_id) > 0
103
+
104
+ def emis_page_count(self, sourcetype, substance_id):
105
+ return self._pages.get(sourcetype, {}).get(f"emission_{substance_id}", 0)
106
+
107
+ def weight_page_count(self, sourcetype):
108
+ return self._pages.get(sourcetype, {}).get("weights", 0)
109
+
110
+ def feature_page_count(self, sourcetype):
111
+ return self._pages.get(sourcetype, {}).get("features", 0)
112
+
113
+ def add_rec(
114
+ self, rec, sourcetype, *, write_feature=False, write_weights=False, weights=None
115
+ ):
116
+ """add feature to cache."""
117
+
118
+ col_map = self.col_maps[sourcetype]
119
+ # extract data from record
120
+ source_id = rec[col_map.source_id]
121
+ substance_id = rec[col_map.substance_id]
122
+
123
+ # only write to disk if source not already in self.feature_ids
124
+ # or in self.gridded_sources
125
+ # (to keep all data related to a specific source on the same cache page)
126
+ if (
127
+ self._recs_in_memory[sourcetype] > PAGE_SIZE
128
+ and source_id not in self.feature_ids[sourcetype]
129
+ and source_id not in self.gridded_sources[sourcetype]
130
+ ):
131
+ if write_weights:
132
+ self.write_weights(sourcetype)
133
+ if write_feature:
134
+ self.write_features(sourcetype)
135
+ self.write_emissions(sourcetype)
136
+
137
+ # reset nr of records in memory
138
+ self._recs_in_memory[sourcetype] = 1
139
+
140
+ # emissions currently in memory
141
+ emissions = self._emissions.setdefault(sourcetype, {})
142
+
143
+ # add record
144
+ # avoid writing emission rec if source is gridded but there are no resulting
145
+ # weights. This may happen if emission filter polygon is bigger than dataset
146
+ # extent. The source is then included, but no weights are generated.
147
+ # It could potentially also happen due to uncertainties in coordinate
148
+ # transformation in PostGIS
149
+ if not (write_weights and weights is None):
150
+ emissions.setdefault(substance_id, []).append(rec)
151
+
152
+ self._recs_in_memory[sourcetype] += 1
153
+
154
+ if write_feature and source_id not in self.feature_ids[sourcetype]:
155
+ geom_wkt = rec[col_map.wkt]
156
+ params = STATIC_DATA_PARAMETERS[sourcetype]
157
+ data = {key: rec[getattr(col_map, key)] for key in params}
158
+ self.feature_ids[sourcetype][source_id] = self._feature_index[sourcetype]
159
+ # features currently in memory
160
+ features = self._features.setdefault(
161
+ sourcetype, {"ids": [], "geoms": [], "data": []}
162
+ )
163
+ features["ids"].append(source_id)
164
+ features["geoms"].append(geom_wkt)
165
+ features["data"].append(data)
166
+ self._feature_index[sourcetype] += 1
167
+
168
+ if sourcetype != "grid":
169
+ source_key = source_id
170
+ else:
171
+ raster = rec[col_map.raster]
172
+ source_key = (source_id, raster)
173
+
174
+ if weights is not None and source_key not in self.gridded_sources[sourcetype]:
175
+ self._weights.setdefault(sourcetype, {})[source_key] = weights
176
+ self.gridded_sources[sourcetype].add(source_key)
177
+
178
+ def write_features(self, sourcetype):
179
+ if sourcetype in self._features:
180
+ self._write_page_to_cache(
181
+ sourcetype, "features", self._features[sourcetype]
182
+ )
183
+ self._features[sourcetype] = {"ids": [], "geoms": [], "data": []}
184
+
185
+ def read_features(self, sourcetype, page_nr):
186
+ return self._read_page_from_cache(sourcetype, "features", page_nr)
187
+
188
+ def write_weights(self, sourcetype):
189
+ if sourcetype in self._weights:
190
+ self._write_page_to_cache(sourcetype, "weights", self._weights[sourcetype])
191
+ self._weights[sourcetype] = {}
192
+
193
+ def read_weights(self, sourcetype, page_nr):
194
+ return self._read_page_from_cache(sourcetype, "weights", page_nr)
195
+
196
+ def write_emissions(self, sourcetype):
197
+ if sourcetype in self._emissions:
198
+ for substance_id, data in self._emissions[sourcetype].items():
199
+ self._write_page_to_cache(sourcetype, f"emission_{substance_id}", data)
200
+ self._emissions[sourcetype] = {}
201
+
202
+ def read_emissions(self, sourcetype, substance_id, page_nr):
203
+ return self._read_page_from_cache(
204
+ sourcetype, f"emission_{substance_id}", page_nr
205
+ )
206
+
207
+ def _write_page_to_cache(self, sourcetype, namespace, data):
208
+ """Write temporary data to disk"""
209
+
210
+ # if data is empty, do not write any page
211
+ if len(data) == 0:
212
+ return
213
+
214
+ # set page count for sourcetype in namespace
215
+ pages = self._pages.setdefault(sourcetype, {})
216
+ if namespace not in pages:
217
+ pages[namespace] = 1
218
+ else:
219
+ pages[namespace] += 1
220
+
221
+ page_nr = pages[namespace]
222
+ cache_subdir = Path(self.cache_dir.name) / sourcetype / namespace
223
+ cache_subdir.mkdir(parents=True, exist_ok=True)
224
+ with cache_subdir.joinpath(f"page_{page_nr}.tmp").open("wb") as page_file:
225
+ pickle.dump(data, page_file, protocol=-1)
226
+
227
+ def _read_page_from_cache(self, sourcetype, namespace, page_nr):
228
+ """Read page of data from disk."""
229
+
230
+ cache_subdir = Path(self.cache_dir.name) / sourcetype / namespace
231
+ page_file_name = cache_subdir / f"page_{page_nr}.tmp"
232
+ if not page_file_name.exists():
233
+ raise NotInCacheError(f"file {page_file_name} not found in cache")
234
+ with page_file_name.open("rb") as page_file:
235
+ return pickle.load(page_file)
236
+
237
+
238
+ def cache_queryset(queryset, fields):
239
+ """Return dict of model instances with fields as key
240
+ If several fields are specified, a tuple of the fields is used as key
241
+ """
242
+
243
+ def fields2key(inst, fields):
244
+ if hasattr(fields, "__iter__") and not isinstance(fields, str):
245
+ return tuple([getattr(inst, field) for field in fields])
246
+ else:
247
+ return getattr(inst, fields)
248
+
249
+ return dict(((fields2key(instance, fields), instance) for instance in queryset))
cetk/edb/const.py ADDED
@@ -0,0 +1,26 @@
1
+ WGS84_SRID = 4326
2
+ DEFAULT_SRID = 3857
3
+ CHAR_FIELD_LENGTH = 100
4
+ CODE_FIELD_LENGTH = 50
5
+ NODATA = -9999.0
6
+ DEFAULT_EMISSION_UNIT = "kg/year"
7
+
8
+ # sheet names which are valid for data import
9
+ SHEET_NAMES = [
10
+ "CodeSet",
11
+ "ActivityCode",
12
+ "EmissionFactor",
13
+ "Timevar",
14
+ "PointSource",
15
+ "AreaSource",
16
+ "GridSource",
17
+ "VehicleFuel",
18
+ "Fleet",
19
+ "CongestionProfile",
20
+ "FlowTimevar",
21
+ "ColdstartTimevar",
22
+ "RoadAttribute",
23
+ "TrafficSituation",
24
+ "VehicleEmissionFactor",
25
+ "RoadSource",
26
+ ]
cetk/edb/copy.py ADDED
@@ -0,0 +1,18 @@
1
+ # from django.db import transaction
2
+
3
+ # from cetk.edb import models
4
+
5
+
6
+ def copy_model_instance(instance, **updated_fields):
7
+ """Create a copy of a model instance in the database."""
8
+ meta = instance._meta
9
+ copy = meta.model(
10
+ **{
11
+ f.name: updated_fields.get(f.name, getattr(instance, f.name))
12
+ for f in meta.get_fields()
13
+ if not (f.one_to_many or f.many_to_many)
14
+ }
15
+ )
16
+ copy.pk = None
17
+ copy.save()
18
+ return copy