brightpath 0.0.1__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.
@@ -0,0 +1,15 @@
1
+ - Gangue, in ground
2
+ - Water, turbine use, unspecified natural origin
3
+ - Oxygen
4
+ - Volume occupied, reservoir
5
+ - Xenon-135
6
+ - Noble gases, radioactive, unspecified
7
+ - Radon-222
8
+ - Xenon-133
9
+ - Hydrogen-3, Tritium
10
+ - Radon-222
11
+ - Radon-220
12
+ - Oxygen
13
+ - Occupation, traffic area, road network
14
+ - Energy, gross calorific value, in biomass, primary forest
15
+ - Carbon-14
@@ -0,0 +1,5 @@
1
+ market:
2
+ - ethylene vinyl acetate copolymer
3
+
4
+ waste:
5
+ - treatment of aluminium scrap
@@ -0,0 +1,37 @@
1
+ - Process
2
+ - Category type
3
+ - Type
4
+ - Process name
5
+ - Time period
6
+ - Geography
7
+ - Technology
8
+ - Comment
9
+ - Representativeness
10
+ - Cut off rules
11
+ - Capital goods
12
+ - Date
13
+ - Boundary with nature
14
+ - Infrastructure
15
+ - Record
16
+ - Generator
17
+ - Literature references
18
+ - External documents
19
+ - Collection method
20
+ - Data treatment
21
+ - Verification
22
+ - System description
23
+ - Allocation rules
24
+ - Products
25
+ - Waste treatment
26
+ - Materials/fuels
27
+ - Electricity/heat
28
+ - Resources
29
+ - Emissions to air
30
+ - Emissions to water
31
+ - Emissions to soil
32
+ - Final waste flows
33
+ - Non material emission
34
+ - Social issues
35
+ - Economic issues
36
+ - Waste to treatment
37
+ - End
@@ -0,0 +1,15 @@
1
+ - "{SimaPro 9.1.1.1}"
2
+ - "{processes}"
3
+ - "{Project: project_name}"
4
+ - "{Date: today_date}"
5
+ - "{CSV Format version: 9.0.0}"
6
+ - "{CSV separator: Semicolon}"
7
+ - "{Decimal separator: .}"
8
+ - "{Date separator: .}"
9
+ - "{Short date format: dd.MM.yyyy}"
10
+ - "{Export platform IDs: No}"
11
+ - "{Skip empty fields: No}"
12
+ - "{Convert expressions to constants: No}"
13
+ - "{Selection: Selection(1)}"
14
+ - "{Related objects(system descriptions, substances, units, etc.): Yes}"
15
+ - "{Include sub product stages and processes: Yes}"
@@ -0,0 +1,16 @@
1
+ low population density, long-term: low. pop., long-term
2
+ lower stratosphere + upper troposphere: stratosphere + troposphere
3
+ non-urban air or from high stacks: low. pop.
4
+ urban air close to ground: high. pop.
5
+ biotic: biotic
6
+ in air: in air
7
+ in ground: in ground
8
+ in water: in water
9
+ land: land
10
+ agricultural: agricultural
11
+ forestry: forestry
12
+ industrial: industrial
13
+ ground-: groundwater
14
+ ground-, long-term: groundwater, long-term
15
+ ocean: ocean
16
+ surface water: river
@@ -0,0 +1,15 @@
1
+ kilogram: kg
2
+ cubic meter: m3
3
+ kilowatt hour: kWh
4
+ kilometer: km
5
+ ton kilometer: tkm
6
+ megajoule: MJ
7
+ unit: p
8
+ square meter: m2
9
+ kilowatt: p
10
+ hour: hr
11
+ square meter-year: m2a
12
+ meter: m
13
+ vehicle-kilometer: vkm
14
+ person-kilometer: personkm
15
+ meter-year: my
@@ -0,0 +1,7 @@
1
+ - market for activated carbon, granular
2
+ - market for iodine
3
+ - market for molybdenum trioxide
4
+ - market for fly ash and scrubber sludge
5
+ - market for barium oxide
6
+ - market for strontium carbonate
7
+ - market for polysulfone
@@ -0,0 +1,31 @@
1
+ market for heat, from steam, in chemical industry:
2
+ factor: 0.36
3
+ unit: kilogram
4
+ steam production, as energy carrier, in chemical industry:
5
+ factor: 0.36
6
+ unit: kilogram
7
+ market for natural gas, high pressure, vehicle grade:
8
+ factor: 0.842
9
+ unit: megajoule
10
+ market for chemical factory:
11
+ factor: 833333
12
+ unit: unit
13
+ market for used powertrain from electric passenger car, manual dismantling:
14
+ factor: -1
15
+ market for manganese sulfate:
16
+ factor: 0.34
17
+ market for nickel sulfate:
18
+ factor: 0.445
19
+ market for manual dismantling of used electric passenger car:
20
+ factor: 0.000833
21
+ unit: kilogram
22
+ market for manual dismantling of electric scooter:
23
+ unit: kilogram
24
+ market for transport, pipeline, onshore, petroleum:
25
+ unit: kilometer
26
+ manual dismantling of used passenger car with internal combustion engine:
27
+ factor: 0.000833
28
+ market for manual dismantling of used passenger car with internal combustion engine:
29
+ factor: 0.000833
30
+ manual dismantling of used electric scooter:
31
+ factor: 0.0111
@@ -0,0 +1,11 @@
1
+ - waste
2
+ - emissions
3
+ - treatment of
4
+ - scrap
5
+ - used
6
+ - disposal
7
+ - sludge
8
+ - furnace dust
9
+ - mineral oil storage
10
+ - condensate from
11
+ - decontamination
@@ -0,0 +1,463 @@
1
+ """
2
+ This module contains the class SimaproConverter, which is used to convert
3
+ Simapro inventories to Brightway inventory files.
4
+ """
5
+
6
+ import csv
7
+ import logging
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+
11
+ import bw2io
12
+
13
+ from . import DATA_DIR
14
+ from .utils import (
15
+ check_simapro_inventory,
16
+ get_simapro_biosphere,
17
+ get_simapro_subcompartments,
18
+ get_simapro_technosphere,
19
+ get_waste_exchange_names,
20
+ load_biosphere_correspondence,
21
+ load_ei_biosphere_flows,
22
+ lower_cap_first_letter,
23
+ remove_duplicates,
24
+ )
25
+
26
+ WASTE_TERMS = get_waste_exchange_names()
27
+
28
+
29
+ def format_technosphere_exchange(txt: str):
30
+ """Split and normalise a technosphere exchange name from SimaPro.
31
+
32
+ :param txt: Raw exchange string as found in a SimaPro CSV export.
33
+ :type txt: str
34
+ :return: Tuple containing the cleaned name, reference product and location.
35
+ :rtype: tuple[str, str, str]
36
+ """
37
+
38
+ location_correction = {
39
+ "WECC, US only": "US-WECC",
40
+ "ASCC, US only": "US-ASCC",
41
+ "HICC, US only": "US-HICC",
42
+ "MRO, US only": "US-MRO",
43
+ "NPCC, US only": "US-NPCC",
44
+ "PR, US only": "US-PR",
45
+ "RFC, US only": "US-RFC",
46
+ "SERC, US only": "US-SERC",
47
+ "TRE, US only": "US-TRE",
48
+ "HICC": "US-HICC",
49
+ "SERC": "US-SERC",
50
+ "RFC": "US-RFC",
51
+ "ASCC": "US-ASCC",
52
+ "TRE": "US-TRE",
53
+ "FRCC": "US-FRCC",
54
+ "SPP": "US-SPP",
55
+ }
56
+
57
+ correct_name = [
58
+ "production",
59
+ "production mix",
60
+ "processing",
61
+ "market for",
62
+ "market group for",
63
+ "production, at grid",
64
+ "at market",
65
+ "cut-off, U",
66
+ "generic production",
67
+ "market",
68
+ "processing, mass based",
69
+ "transport",
70
+ "treatment of",
71
+ "purification",
72
+ ]
73
+
74
+ reference_product, name, location = "", "", ""
75
+ if len(txt.split("|")) == 1:
76
+ reference_product = txt.split("|")[0]
77
+ name = reference_product
78
+
79
+ if len(txt.split("|")) == 2:
80
+ reference_product, name = txt.split("|")
81
+
82
+ if len(txt.split("|")) == 3:
83
+ reference_product, name, _ = txt.split("|")
84
+
85
+ if len(txt.split("|")) == 4:
86
+ reference_product, name, _, _ = txt.split("|")
87
+
88
+ reference_product = lower_cap_first_letter(reference_product)
89
+
90
+ if "{" in reference_product:
91
+ reference_product, location = reference_product.split("{")
92
+ location, reference_product_ = location.split("}")
93
+ if len(reference_product_) > 0:
94
+ reference_product_ = reference_product_.strip()
95
+ reference_product = f"{reference_product} {reference_product_}"
96
+ reference_product = reference_product.replace(" ", " ")
97
+ else:
98
+ location = "GLO"
99
+
100
+ if location in ["French Guiana", "French Guinana"]:
101
+ location = "FG"
102
+
103
+ name = name.replace(
104
+ "Cut-off, U",
105
+ "",
106
+ )
107
+ name = name.replace(
108
+ "cut-off, U",
109
+ "",
110
+ )
111
+
112
+ if name[-3:] in [", U", ", S"]:
113
+ name = name[:-3]
114
+
115
+ if "{" in name:
116
+ name = name.split("{")[0]
117
+
118
+ name = name.strip()
119
+
120
+ if name == "":
121
+ name = reference_product
122
+
123
+ reference_product = reference_product.strip()
124
+ if reference_product[-1] in [",", "."]:
125
+ reference_product = reference_product[:-1]
126
+
127
+ if name in correct_name:
128
+ if name in ["market for", "market group for", "treatment of"]:
129
+ name = f"{name} {reference_product}"
130
+ elif name == "market":
131
+ name = f"market for {reference_product}"
132
+ elif name == "production":
133
+ if ", " in reference_product:
134
+ name = f"{reference_product.split(', ')[0]} production, {', '.join(reference_product.split(', ')[1:])}"
135
+ else:
136
+ name = f"{reference_product} production"
137
+ elif name == "processing":
138
+ name = reference_product
139
+ else:
140
+ name = f"{reference_product} {name}"
141
+
142
+ if name.startswith("treatment of,"):
143
+ name = f"treatment of {reference_product}, {name.split(', ')[-1]}"
144
+
145
+ if name == "construction":
146
+ if ", " in reference_product:
147
+ name = f"{reference_product.split(', ')[0]} construction, {', '.join(reference_product.split(', ')[1:])}"
148
+ else:
149
+ name = f"{reference_product} construction"
150
+
151
+ if name.startswith("production, from") or name.startswith("production from"):
152
+ name = f"{reference_product} {name}"
153
+
154
+ location = location.replace("}", "").strip()
155
+ location = location_correction.get(location, location)
156
+
157
+ return name, reference_product, location
158
+
159
+
160
+ def load_ecoinvent_activities(version: str) -> list:
161
+ """Load the list of ecoinvent activities for the given version.
162
+
163
+ :param version: Ecoinvent version identifier, e.g. ``"3.9"``.
164
+ :type version: str
165
+ :return: Rows describing ecoinvent activities.
166
+ :rtype: list[list[str]]
167
+ """
168
+ with open(DATA_DIR / "export" / f"list_ei{version}_cutoff_activities.csv") as f:
169
+ reader = csv.reader(f)
170
+ next(reader)
171
+ return [l for l in reader]
172
+
173
+
174
+ def format_biosphere_exchange(exc, ei_version, bio_flows, bio_mapping):
175
+ """Normalise a biosphere exchange to match ecoinvent conventions.
176
+
177
+ :param exc: Exchange to adjust in-place.
178
+ :type exc: dict
179
+ :param ei_version: Version of ecoinvent used for interpretation.
180
+ :type ei_version: str
181
+ :param bio_flows: Known biosphere flows for the version.
182
+ :type bio_flows: list[tuple[str, str, str]]
183
+ :param bio_mapping: Mapping to resolve outdated flow names.
184
+ :type bio_mapping: dict
185
+ :return: The updated exchange dictionary.
186
+ :rtype: dict
187
+ """
188
+ if "in ground" in exc["name"]:
189
+ if ei_version not in ["3.5", "3.6", "3.7", "3.8"]:
190
+ exc["name"] = exc["name"].replace(", in ground", "")
191
+ exc["categories"] = ("natural resource", "in ground")
192
+
193
+ if exc["name"].startswith("Water, well"):
194
+ exc["name"] = "Water, well, in ground"
195
+
196
+ if exc["name"].startswith("Water, lake"):
197
+ exc["name"] = "Water, lake"
198
+
199
+ if exc["name"].startswith("Water, cooling"):
200
+ exc["name"] = "Water, cooling, unspecified natural origin"
201
+
202
+ if exc["name"].startswith("Water,"):
203
+ if "in air" not in exc["name"]:
204
+ exc["categories"] = ("natural resource", "in water")
205
+
206
+ key = (
207
+ exc["name"],
208
+ exc["categories"][0],
209
+ "unspecified" if len(exc["categories"]) == 1 else exc["categories"][1],
210
+ )
211
+
212
+ if key not in bio_flows:
213
+ try:
214
+ if exc["name"] in bio_mapping[exc["categories"][0]]:
215
+ exc["name"] = bio_mapping[exc["categories"][0]][exc["name"]]
216
+ key = list(key)
217
+ key[0] = exc["name"]
218
+ key = tuple(key)
219
+ except:
220
+ logging.warning(f"Could not find biosphere flow for {exc['name']}.")
221
+ pass
222
+
223
+ if key not in bio_flows:
224
+ if exc["categories"][0] == "natural resource":
225
+ for i in [
226
+ "in ground",
227
+ "biotic",
228
+ "in air",
229
+ "land",
230
+ ]:
231
+ key = list(key)
232
+ key[2] = i
233
+ key = tuple(key)
234
+ if key in bio_flows:
235
+ exc["categories"] = (exc["categories"][0], i)
236
+ break
237
+
238
+ if exc["categories"][0] == "soil":
239
+ for i in [
240
+ "agricultural",
241
+ "forestry",
242
+ "industrial",
243
+ ]:
244
+ key = list(key)
245
+ key[2] = i
246
+ key = tuple(key)
247
+ if key in bio_flows:
248
+ exc["categories"] = (exc["categories"][0], i)
249
+ break
250
+
251
+ if exc["categories"][0] == "air":
252
+ for i in [
253
+ "low population density, long-term",
254
+ "lower stratosphere + upper troposphere",
255
+ "non-urban air or from high stacks",
256
+ "urban air close to ground",
257
+ ]:
258
+ key = list(key)
259
+ key[2] = i
260
+ key = tuple(key)
261
+ if key in bio_flows:
262
+ exc["categories"] = (exc["categories"][0], i)
263
+ break
264
+
265
+ if exc["categories"] == ("natural resource", "in ground"):
266
+ if ei_version in ["3.5", "3.6", "3.7", "3.8"]:
267
+ if "in ground" not in exc["name"]:
268
+ exc["name"] += ", in ground"
269
+
270
+ return exc
271
+
272
+
273
+ class SimaproConverter:
274
+ """Convert SimaPro CSV exports into Brightway-compatible datasets.
275
+
276
+ :param filepath: Path to the SimaPro CSV file.
277
+ :type filepath: str
278
+ :param ecoinvent_version: Version of ecoinvent to align biosphere data to.
279
+ :type ecoinvent_version: str
280
+ :param db_name: Optional name of the Brightway database to create.
281
+ :type db_name: str | None
282
+ :ivar filepath: Normalised path to the validated CSV file.
283
+ :vartype filepath: pathlib.Path
284
+ :ivar i: Instance of :class:`bw2io.SimaProCSVImporter` handling the data.
285
+ :vartype i: bw2io.SimaProCSVImporter
286
+ :ivar db_name: Name of the Brightway database that will be created.
287
+ :vartype db_name: str
288
+ :ivar ecoinvent_version: Version of the ecoinvent database in use.
289
+ :vartype ecoinvent_version: str
290
+ :ivar biosphere: Mapping between SimaPro and Brightway biosphere flows.
291
+ :vartype biosphere: dict
292
+ :ivar technosphere: Mapping between SimaPro and Brightway technosphere
293
+ exchanges.
294
+ :vartype technosphere: dict
295
+ :ivar subcompartments: Mapping of sub-compartments between the databases.
296
+ :vartype subcompartments: dict
297
+ :ivar ei_biosphere_flows: Known biosphere flows for the selected version.
298
+ :vartype ei_biosphere_flows: list[tuple[str, str, str]]
299
+ :ivar biosphere_flows_correspondence: Mapping of outdated biosphere names.
300
+ :vartype biosphere_flows_correspondence: dict
301
+ """
302
+
303
+ def __init__(
304
+ self, filepath: str, ecoinvent_version: str = "3.9", db_name: str = None
305
+ ):
306
+ """Initialise the converter and load the SimaPro inventory.
307
+
308
+ :param filepath: Path to the SimaPro CSV export to convert.
309
+ :type filepath: str
310
+ :param ecoinvent_version: Ecoinvent version that should be used when
311
+ reconciling biosphere flows.
312
+ :type ecoinvent_version: str
313
+ :param db_name: Optional Brightway database name override.
314
+ :type db_name: str | None
315
+ :raises FileNotFoundError: If the provided CSV file cannot be found.
316
+ """
317
+
318
+ logging.basicConfig(
319
+ level=logging.DEBUG,
320
+ filename="brightpath.log", # Log file to save the entries
321
+ filemode="a", # Append to the log file if it exists, 'w' to overwrite
322
+ format="%(asctime)s - %(levelname)s - %(module)s - %(message)s",
323
+ datefmt="%Y-%m-%d %H:%M:%S",
324
+ )
325
+
326
+ if not Path(filepath).exists():
327
+ raise FileNotFoundError(f"File {filepath} not found.")
328
+
329
+ self.filepath = Path(check_simapro_inventory(filepath))
330
+ self.i = bw2io.SimaProCSVImporter(filepath=self.filepath, name=db_name)
331
+ self.db_name = db_name or self.filepath.stem
332
+ self.i = bw2io.SimaProCSVImporter(filepath=self.filepath, name=self.db_name)
333
+ self.i.apply_strategies()
334
+ self.i.data = remove_duplicates(self.i.data)
335
+
336
+ self.ecoinvent_version = ecoinvent_version
337
+ self.biosphere = get_simapro_biosphere()
338
+ self.technosphere = get_simapro_technosphere()
339
+ self.subcompartments = get_simapro_subcompartments()
340
+
341
+ self.ei_biosphere_flows = load_ei_biosphere_flows()
342
+ self.biosphere_flows_correspondence = load_biosphere_correspondence()
343
+
344
+ self.i.db_name = self.db_name
345
+
346
+ def check_database_name(self):
347
+ """Ensure exchanges reference the correct Brightway database name."""
348
+ for act in self.i.data:
349
+ act["database"] = self.i.db_name
350
+
351
+ for exc in act["exchanges"]:
352
+ if exc.get("type") in ["production", "technosphere"]:
353
+ if "input" in exc:
354
+ if exc["input"][0] is None:
355
+ exc["input"] = (self.i.db_name, exc["input"][1])
356
+
357
+ def convert_to_brightway(self):
358
+ """Convert the imported SimaPro data into Brightway inventories."""
359
+ print("- format exchanges")
360
+ internal_datasets = []
361
+ for ds in self.i.data:
362
+
363
+ if "Comment" in ds.get("simapro metadata", {}).keys():
364
+ # rename "Comment" to "comment"
365
+ ds["comment"] = ds["simapro metadata"]["Comment"]
366
+ del ds["simapro metadata"]["Comment"]
367
+
368
+ ds["name"], ds["reference product"], ds["location"] = (
369
+ format_technosphere_exchange(ds["name"])
370
+ )
371
+ internal_datasets.append(
372
+ (ds["name"], ds["reference product"], ds["location"])
373
+ )
374
+
375
+ for exc in ds["exchanges"]:
376
+ exc["simapro name"] = exc["name"]
377
+
378
+ if exc["type"] == "production":
379
+ exc["name"] = ds["name"]
380
+ exc["product"] = ds["reference product"]
381
+ exc["reference product"] = ds["reference product"]
382
+ exc["location"] = ds["location"]
383
+
384
+ if any(x in exc["name"] for x in WASTE_TERMS):
385
+ logging.info(
386
+ msg=f"{exc['name']} considered waste treatment "
387
+ f"(input amount made negative)."
388
+ )
389
+ print(
390
+ f"{exc['name']} considered waste treatment: sign of production exchange made negative."
391
+ )
392
+ exc["amount"] *= -1
393
+
394
+ if exc["type"] in ["technosphere", "substitution"]:
395
+ exc["name"], exc["product"], exc["location"] = (
396
+ format_technosphere_exchange(exc["name"])
397
+ )
398
+ exc["reference product"] = exc["product"]
399
+
400
+ if any(x in exc["name"] for x in WASTE_TERMS):
401
+ logging.info(
402
+ msg=f"{exc['name']} considered waste treatment "
403
+ f"(input amount made negative)."
404
+ )
405
+ exc["amount"] *= -1
406
+
407
+ if exc["type"] == "substitution":
408
+ exc["type"] = "technosphere"
409
+ exc["amount"] *= -1
410
+
411
+ if exc["type"] == "biosphere":
412
+ format_biosphere_exchange(
413
+ exc,
414
+ self.ecoinvent_version,
415
+ self.ei_biosphere_flows,
416
+ self.biosphere_flows_correspondence,
417
+ )
418
+
419
+ self.check_database_name()
420
+
421
+ print("- remove empty datasets")
422
+ self.remove_empty_datasets()
423
+ print("- remove empty exchanges")
424
+ self.remove_empty_exchanges()
425
+ print("- check inventories")
426
+ self.check_inventories()
427
+ print("Done!")
428
+
429
+ def remove_empty_datasets(self):
430
+ """Remove datasets that contain no exchanges."""
431
+ self.i.data = [ds for ds in self.i.data if len(ds["exchanges"]) >= 1]
432
+
433
+ def remove_empty_exchanges(self):
434
+ """Remove exchanges that have a zero amount."""
435
+ for ds in self.i.data:
436
+ ds["exchanges"] = [e for e in ds["exchanges"] if e["amount"] != 0.0]
437
+
438
+ def check_inventories(self):
439
+ """Perform basic validation of the converted inventories."""
440
+ for ds in self.i.data:
441
+ if len([x for x in ds["exchanges"] if x["type"] == "production"]) != 1:
442
+ print(
443
+ f"WARNING: {ds['name'], ds['reference product'], ds['location']} has more"
444
+ f"than one production flow."
445
+ )
446
+
447
+ for e in ds["exchanges"]:
448
+ if e["type"] not in ["production", "technosphere", "biosphere"]:
449
+ print(
450
+ f"WARNING: {ds['name'], ds['reference product'], ds['location']} has an"
451
+ f"unknown flow type {e['type']}."
452
+ )
453
+
454
+ if e["type"] == "production":
455
+ if (e["name"], e["product"], e["location"]) != (
456
+ ds["name"],
457
+ ds["reference product"],
458
+ ds["location"],
459
+ ):
460
+ print(
461
+ f"WARNING: {ds['name'], ds['reference product'], ds['location']} has an"
462
+ f"incorrect production flow {e}."
463
+ )