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.
brightpath/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ __all__ = ("BrightwayConverter", "SimaproConverter", "DATA_DIR")
2
+ __version__ = (0, 0, 2)
3
+
4
+ from pathlib import Path
5
+
6
+ DATA_DIR = Path(__file__).resolve().parent / "data"
7
+
8
+ from .bwconverter import BrightwayConverter
9
+ from .simaproconverter import SimaproConverter
@@ -0,0 +1,616 @@
1
+ """
2
+ This module contains the class BrightwayConverter, which is used to convert
3
+ Brightway2 inventories to Simapro CSV files.
4
+ """
5
+
6
+ import csv
7
+ import datetime
8
+ from pathlib import Path
9
+
10
+ from .utils import (
11
+ add_distri_transport,
12
+ check_exchanges_for_conversion,
13
+ convert_sd_to_sd2,
14
+ find_production_exchange,
15
+ flag_exchanges,
16
+ format_exchange_name,
17
+ get_biosphere_exchanges,
18
+ get_simapro_biosphere,
19
+ get_simapro_ecoinvent_blacklist,
20
+ get_simapro_fields_list,
21
+ get_simapro_headers,
22
+ get_simapro_subcompartments,
23
+ get_simapro_technosphere,
24
+ get_simapro_uncertainty_type,
25
+ get_simapro_units,
26
+ get_subcategory,
27
+ get_technosphere_exchanges,
28
+ import_bw_inventories,
29
+ is_a_waste_treatment,
30
+ is_activity_waste_treatment,
31
+ is_blacklisted,
32
+ load_inventory_metadata,
33
+ print_unused_exchanges,
34
+ round_floats_in_string,
35
+ )
36
+
37
+
38
+ class BrightwayConverter:
39
+ """Convert Brightway2 inventories to SimaPro CSV files.
40
+
41
+ The converter loads inventories exported from Brightway2 and prepares
42
+ them for the SimaPro import format. The instance keeps references to the
43
+ different lookup tables that are needed throughout the conversion.
44
+
45
+ :param filepath: Path to the Brightway inventory spreadsheet.
46
+ :type filepath: str | None
47
+ :param data: Brightway inventories provided directly instead of loading
48
+ them from ``filepath``.
49
+ :type data: list | None
50
+ :param metadata: Optional path to a YAML file containing additional
51
+ metadata to append to the export.
52
+ :type metadata: str | None
53
+ :param ecoinvent_version: Version string of the ecoinvent database the
54
+ inventories are linked to.
55
+ :type ecoinvent_version: str
56
+ :param export_dir: Directory where generated SimaPro CSV files are saved.
57
+ :type export_dir: str | None
58
+ :ivar filepath: Path to the Brightway inventory spreadsheet.
59
+ :vartype filepath: str | None
60
+ :ivar inventories: Brightway activities that are going to be converted.
61
+ :vartype inventories: list[dict] | None
62
+ :ivar simapro_blacklist: Exchanges that should not be exported.
63
+ :vartype simapro_blacklist: dict
64
+ :ivar simapro_fields: Order and structure of SimaPro sections.
65
+ :vartype simapro_fields: list[str]
66
+ :ivar simapro_units: Mapping of Brightway units to SimaPro units.
67
+ :vartype simapro_units: dict[str, str]
68
+ :ivar simapro_headers: Header rows used when writing the CSV file.
69
+ :vartype simapro_headers: list[str]
70
+ :ivar simapro_technosphere: Mapping from technosphere exchanges to
71
+ SimaPro names.
72
+ :vartype simapro_technosphere: dict[tuple[str, str], str]
73
+ :ivar simapro_biosphere: Mapping from biosphere exchanges to SimaPro
74
+ names.
75
+ :vartype simapro_biosphere: dict[str, str]
76
+ :ivar simapro_subcompartment: Mapping of biosphere subcompartments to
77
+ SimaPro names.
78
+ :vartype simapro_subcompartment: dict[str, str]
79
+ :ivar ei_version: Version of the ecoinvent database in use.
80
+ :vartype ei_version: str
81
+ :ivar metadata: Optional metadata loaded from the YAML file.
82
+ :vartype metadata: dict | None
83
+ :ivar export_dir: Output directory for converted CSV files.
84
+ :vartype export_dir: pathlib.Path
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ filepath: str = None,
90
+ data: list = None,
91
+ metadata: str = None,
92
+ ecoinvent_version: str = "3.9",
93
+ export_dir: str = None,
94
+ ):
95
+ """Instantiate a converter that targets the SimaPro CSV format.
96
+
97
+ When ``filepath`` is provided the inventories are loaded from the
98
+ spreadsheet using :func:`brightpath.utils.import_bw_inventories`. If
99
+ ``filepath`` is omitted, pre-loaded ``data`` can be supplied instead.
100
+ Optional ``metadata`` is validated and attached to the export.
101
+
102
+ :param filepath: Path to the Brightway inventory spreadsheet file.
103
+ :type filepath: str | None
104
+ :param data: Inventories loaded in memory. Used when ``filepath`` is
105
+ ``None``.
106
+ :type data: list | None
107
+ :param metadata: Path to the metadata YAML file.
108
+ :type metadata: str | None
109
+ :param ecoinvent_version: Version of the linked ecoinvent database.
110
+ :type ecoinvent_version: str
111
+ :param export_dir: Directory where SimaPro exports will be written.
112
+ :type export_dir: str | None
113
+ :raises FileNotFoundError: If the metadata file does not exist.
114
+ :raises ValueError: If the metadata file is not a YAML document.
115
+ """
116
+ self.filepath = filepath
117
+ self.inventories = import_bw_inventories(filepath) if self.filepath else data
118
+ self.simapro_blacklist = get_simapro_ecoinvent_blacklist()
119
+ self.simapro_fields = get_simapro_fields_list()
120
+ self.simapro_units = get_simapro_units()
121
+ self.simapro_headers = get_simapro_headers()
122
+ self.simapro_technosphere = get_simapro_technosphere()
123
+ self.simapro_biosphere = get_simapro_biosphere()
124
+ self.simapro_subcompartment = get_simapro_subcompartments()
125
+ self.ei_version = ecoinvent_version
126
+
127
+ if metadata:
128
+ self.metadata = load_inventory_metadata(metadata)
129
+
130
+ # export directory is the current working
131
+ # directory unless specified otherwise
132
+ self.export_dir = Path(export_dir) or Path.cwd()
133
+
134
+ def format_inventories_for_simapro(self, database: str):
135
+ """Transform the Brightway inventories into the SimaPro structure.
136
+
137
+ This method orchestrates the conversion of each activity in the
138
+ Brightway dataset into the row-based structure expected by SimaPro.
139
+ The resulting structure is compatible with the CSV export performed
140
+ by :meth:`convert_to_simapro`.
141
+
142
+ :param database: Name of the target database to link to. Valid values
143
+ are ``"ecoinvent"`` and ``"uvek"``.
144
+ :type database: str
145
+ :return: Rows ready to be written to a SimaPro CSV file.
146
+ :rtype: list[list[str]]
147
+ :raises ValueError: If required information is missing from the
148
+ inventories.
149
+ """
150
+
151
+ rows = [
152
+ (
153
+ [
154
+ item.replace(
155
+ "today_date", datetime.datetime.today().strftime("%d.%m.%Y")
156
+ )
157
+ ]
158
+ if item.startswith("{Date")
159
+ else [item]
160
+ )
161
+ for item in self.simapro_headers
162
+ ]
163
+ rows.append([])
164
+
165
+ for activity in self.inventories:
166
+
167
+ # first, add transport, if uvek
168
+ if database == "uvek":
169
+ activity = add_distri_transport(activity)
170
+ # and flag exchanges
171
+ activity = flag_exchanges(activity)
172
+ dataset_name = ""
173
+ is_a_waste_treatment_activity = is_activity_waste_treatment(
174
+ activity, database
175
+ )
176
+
177
+ for field in self.simapro_fields:
178
+ if is_a_waste_treatment_activity is True and field == "Products":
179
+ continue
180
+
181
+ if (
182
+ is_a_waste_treatment_activity is False
183
+ and field == "Waste treatment"
184
+ ):
185
+ continue
186
+
187
+ rows.append([field])
188
+ if field in ["Process", "End"]:
189
+ rows.append([])
190
+ continue
191
+
192
+ if field == "Process name":
193
+ dataset_name = format_exchange_name(
194
+ activity["name"],
195
+ activity["reference product"],
196
+ activity["location"],
197
+ activity["unit"],
198
+ database,
199
+ )
200
+ rows.extend(
201
+ [
202
+ [dataset_name],
203
+ [],
204
+ ]
205
+ )
206
+
207
+ if field == "Type":
208
+ rows.extend(
209
+ [
210
+ ["Unit process"],
211
+ [],
212
+ ]
213
+ )
214
+
215
+ if field == "Comment":
216
+ string = ""
217
+ if activity.get("comment"):
218
+ string = f"{round_floats_in_string(activity['comment'])} "
219
+
220
+ if activity.get("source"):
221
+ string += f"Source: {activity['source']} "
222
+ # remove line breaks in string
223
+ string = string.replace("\n", " ")
224
+ rows.extend(
225
+ [
226
+ [string],
227
+ [],
228
+ ]
229
+ )
230
+
231
+ if field == "Category type":
232
+ prod_exchange = find_production_exchange(activity)
233
+ if "simapro category" in prod_exchange:
234
+ rows.extend(
235
+ [
236
+ [prod_exchange["simapro category"].split("/")[0]],
237
+ [],
238
+ ]
239
+ )
240
+ else:
241
+ rows.extend([["Others"], []])
242
+
243
+ if field == "Geography":
244
+ rows.extend(
245
+ [
246
+ [activity["location"]],
247
+ [],
248
+ ]
249
+ )
250
+
251
+ if field == "Date":
252
+ rows.extend(
253
+ [
254
+ [f"{datetime.datetime.today():%d.%m.%Y}"],
255
+ [],
256
+ ]
257
+ )
258
+
259
+ if field in (
260
+ "Time period",
261
+ "Record",
262
+ "Generator",
263
+ "Cut off rules",
264
+ "Capital goods",
265
+ "Technology",
266
+ "Representativeness",
267
+ "Boundary with nature",
268
+ "Infrastructure",
269
+ "External documents",
270
+ "System description",
271
+ "Allocation rules",
272
+ "Literature references",
273
+ "Collection method",
274
+ "Data treatment",
275
+ "Verification",
276
+ ):
277
+ if field.lower() in activity:
278
+ rows.extend(
279
+ [
280
+ [activity[field.lower()]],
281
+ [],
282
+ ]
283
+ )
284
+ else:
285
+ if field == "Infrastructure":
286
+ rows.extend([["No"], []])
287
+ elif (
288
+ field == "System description"
289
+ and "system description" in self.metadata
290
+ ):
291
+ rows.extend(
292
+ [[self.metadata["system description"]["name"]], []]
293
+ )
294
+ elif (
295
+ field == "Literature references"
296
+ and "literature reference" in self.metadata
297
+ ):
298
+ rows.extend(
299
+ [[self.metadata["literature reference"]["name"]], []]
300
+ )
301
+ else:
302
+ rows.extend([["Unspecified"], []])
303
+
304
+ if field in (
305
+ "Final waste flows",
306
+ "Non material emission",
307
+ "Social issues",
308
+ "Economic issues",
309
+ ):
310
+ rows.append([])
311
+
312
+ if field == "Waste treatment":
313
+ prod_exchange = find_production_exchange(activity)
314
+ category = get_subcategory(prod_exchange["simapro category"])
315
+
316
+ if prod_exchange["amount"] < 0:
317
+ prod_exchange["amount"] *= -1
318
+
319
+ rows.extend(
320
+ [
321
+ [
322
+ dataset_name,
323
+ self.simapro_units[prod_exchange["unit"]],
324
+ "{:.3E}".format(prod_exchange["amount"]),
325
+ "100",
326
+ "not defined",
327
+ category,
328
+ "not defined",
329
+ ],
330
+ [],
331
+ ]
332
+ )
333
+ prod_exchange["used"] = True
334
+
335
+ if field == "Products":
336
+ prod_exchange = find_production_exchange(activity)
337
+ category = get_subcategory(prod_exchange["simapro category"])
338
+ rows.extend(
339
+ [
340
+ [
341
+ dataset_name,
342
+ self.simapro_units[prod_exchange["unit"]],
343
+ "{:.3E}".format(prod_exchange["amount"]),
344
+ "100",
345
+ "not defined",
346
+ category,
347
+ "not defined",
348
+ ],
349
+ [],
350
+ ]
351
+ )
352
+ prod_exchange["used"] = True
353
+
354
+ if field in ["Materials/fuels", "Electricity/heat"]:
355
+
356
+ if field == "Materials/fuels":
357
+ techno_excs = list(
358
+ filter(
359
+ lambda x: x["unit"]
360
+ not in ["megajoule", "kilowatt hour"],
361
+ get_technosphere_exchanges(activity),
362
+ )
363
+ )
364
+ else:
365
+ techno_excs = list(
366
+ filter(
367
+ lambda x: x["unit"] in ["megajoule", "kilowatt hour"],
368
+ get_technosphere_exchanges(activity),
369
+ )
370
+ )
371
+
372
+ techno_excs = list(
373
+ filter(
374
+ lambda x: is_blacklisted(x, database) is False,
375
+ list(techno_excs),
376
+ )
377
+ )
378
+ techno_excs = check_exchanges_for_conversion(techno_excs, database)
379
+
380
+ for exc in filter(
381
+ lambda x: is_a_waste_treatment(x["name"], database=database)
382
+ is not True,
383
+ techno_excs,
384
+ ):
385
+ exchange_name = format_exchange_name(
386
+ exc["name"],
387
+ exc["reference product"],
388
+ exc.get("location", "GLO"),
389
+ exc["unit"],
390
+ database,
391
+ )
392
+
393
+ u_type = get_simapro_uncertainty_type(
394
+ exc.get("uncertainty type")
395
+ )
396
+
397
+ if is_a_waste_treatment_activity is True and exc["amount"] < 0:
398
+ exc["amount"] *= -1
399
+
400
+ rows.append(
401
+ [
402
+ f"{exchange_name}",
403
+ self.simapro_units[exc["unit"]],
404
+ "{:.3E}".format(exc["amount"]),
405
+ u_type,
406
+ "{:.3E}".format(
407
+ convert_sd_to_sd2(exc.get("scale", 1), u_type)
408
+ ),
409
+ "{:.3E}".format(exc.get("min", 0)),
410
+ "{:.3E}".format(exc.get("max", 0)),
411
+ exc.get("comment"),
412
+ ]
413
+ )
414
+ exc["used"] = True
415
+
416
+ rows.append([])
417
+
418
+ if field == "Resources":
419
+ biosphere_excs = get_biosphere_exchanges(
420
+ activity, "natural resource"
421
+ )
422
+ for exc in filter(
423
+ lambda x: is_blacklisted(x, database) is False, biosphere_excs
424
+ ):
425
+
426
+ u_type = get_simapro_uncertainty_type(
427
+ exc.get("uncertainty type")
428
+ )
429
+
430
+ if len(exc["categories"]) > 1:
431
+ sub_compartment = self.simapro_subcompartment[
432
+ exc["categories"][1]
433
+ ]
434
+ else:
435
+ sub_compartment = ""
436
+
437
+ rows.append(
438
+ [
439
+ f"{self.simapro_biosphere.get(exc['name'], exc['name'])}",
440
+ sub_compartment,
441
+ self.simapro_units[exc["unit"]],
442
+ "{:.3E}".format(exc["amount"]),
443
+ u_type,
444
+ "{:.3E}".format(
445
+ convert_sd_to_sd2(exc.get("scale", 1), u_type)
446
+ ),
447
+ "{:.3E}".format(exc.get("min", 0)),
448
+ "{:.3E}".format(exc.get("max", 0)),
449
+ exc.get("comment"),
450
+ ]
451
+ )
452
+ exc["used"] = True
453
+ rows.append([])
454
+
455
+ if field.startswith("Emissions to"):
456
+ biosphere_excs = get_biosphere_exchanges(
457
+ activity, field.split(" ")[-1].lower()
458
+ )
459
+ for exc in filter(
460
+ lambda x: is_blacklisted(x, database) is False, biosphere_excs
461
+ ):
462
+
463
+ if exc["name"].lower() == "water":
464
+ exc["unit"] = "kilogram"
465
+ exc["amount"] *= 1000
466
+
467
+ u_type = get_simapro_uncertainty_type(
468
+ exc.get("uncertainty type")
469
+ )
470
+
471
+ if len(exc["categories"]) > 1:
472
+ sub_compartment = self.simapro_subcompartment[
473
+ exc["categories"][1]
474
+ ]
475
+ else:
476
+ sub_compartment = ""
477
+
478
+ rows.append(
479
+ [
480
+ f"{self.simapro_biosphere.get(exc['name'], exc['name'])}",
481
+ sub_compartment,
482
+ self.simapro_units[exc["unit"]],
483
+ "{:.3E}".format(exc["amount"]),
484
+ u_type,
485
+ "{:.3E}".format(
486
+ convert_sd_to_sd2(exc.get("scale", 1), u_type)
487
+ ),
488
+ "{:.3E}".format(exc.get("min", 0)),
489
+ "{:.3E}".format(exc.get("max", 0)),
490
+ exc.get("comment"),
491
+ ]
492
+ )
493
+ exc["used"] = True
494
+ rows.append([])
495
+
496
+ if field == "Waste to treatment":
497
+ techno_excs = get_technosphere_exchanges(activity)
498
+ techno_excs = list(
499
+ filter(
500
+ lambda x: is_blacklisted(x, database) is False, techno_excs
501
+ )
502
+ )
503
+ techno_excs = check_exchanges_for_conversion(techno_excs, database)
504
+
505
+ for exc in filter(
506
+ lambda x: is_a_waste_treatment(x["name"], database=database)
507
+ is True,
508
+ techno_excs,
509
+ ):
510
+
511
+ # In SimaPro, waste inputs are positive numbers
512
+ if exc["amount"] < 0:
513
+ exc["amount"] *= -1
514
+
515
+ exchange_name = format_exchange_name(
516
+ exc["name"],
517
+ exc["reference product"],
518
+ exc.get("location", "GLO"),
519
+ exc["unit"],
520
+ database,
521
+ )
522
+
523
+ u_type = get_simapro_uncertainty_type(
524
+ exc.get("uncertainty type")
525
+ )
526
+
527
+ rows.append(
528
+ [
529
+ f"{exchange_name}",
530
+ self.simapro_units[exc["unit"]],
531
+ "{:.3E}".format(exc["amount"]),
532
+ u_type,
533
+ "{:.3E}".format(
534
+ convert_sd_to_sd2(exc.get("scale", 1), u_type)
535
+ ),
536
+ "{:.3E}".format(exc.get("min", 0)),
537
+ "{:.3E}".format(exc.get("max", 0)),
538
+ exc.get("comment"),
539
+ ]
540
+ )
541
+ exc["used"] = True
542
+
543
+ rows.append([])
544
+
545
+ rows.append([])
546
+
547
+ # Add metadata: system descriptions and literature references
548
+ for field in (
549
+ "System description",
550
+ "Literature reference",
551
+ ):
552
+ if field.lower() in self.metadata:
553
+ rows.extend([[field], []])
554
+ for key, val in self.metadata[field.lower()].items():
555
+ rows.extend(
556
+ [
557
+ [key],
558
+ [val],
559
+ [],
560
+ ]
561
+ )
562
+ rows.append([])
563
+ rows.extend([["End"], []])
564
+
565
+ # check that all exchanges of all activities
566
+ # have been used
567
+ # print it in a prettytable
568
+ print_unused_exchanges(self.inventories)
569
+
570
+ return rows
571
+
572
+ def convert_to_simapro(
573
+ self, database: str = "ecoinvent", format: str = "csv"
574
+ ) -> [str, list]:
575
+ """Export the converted inventories.
576
+
577
+ The inventories are formatted using
578
+ :meth:`format_inventories_for_simapro` and either returned as raw data
579
+ or written to disk as a CSV file, depending on ``format``.
580
+
581
+ :param database: Database to use when resolving exchanges. Accepted
582
+ values are ``"ecoinvent"`` and ``"uvek"``.
583
+ :type database: str
584
+ :param format: Output mode. Use ``"data"`` to receive the converted
585
+ rows instead of writing a CSV file.
586
+ :type format: str
587
+ :return: The CSV filepath when ``format`` is ``"csv"`` or the raw
588
+ SimaPro data rows when ``format`` is ``"data"``.
589
+ :rtype: str | list[list[str]]
590
+ :raises ValueError: If an unsupported ``database`` value is supplied.
591
+ """
592
+
593
+ if database not in ("ecoinvent", "uvek"):
594
+ raise ValueError("Database must be either `ecoinvent` or `uvek`")
595
+
596
+ data = self.format_inventories_for_simapro(database)
597
+
598
+ if format == "data":
599
+ return data
600
+
601
+ # check that export direct exists
602
+ # otherwise we create it
603
+ self.export_dir.mkdir(parents=True, exist_ok=True)
604
+
605
+ filepath = (
606
+ self.export_dir
607
+ / f"simapro_{database}_{datetime.datetime.today().strftime('%d-%m-%Y')}.csv"
608
+ )
609
+
610
+ with open(filepath, "w", newline="", encoding="utf-8") as csvFile:
611
+ writer = csv.writer(csvFile, delimiter=";")
612
+ for row in data:
613
+ writer.writerow(row)
614
+ csvFile.close()
615
+
616
+ return f"Inventories export to: {filepath}"