Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ def __init__(self, activity: tuple | int | bd.Node, parent=None):
# Initialize tab widget to hold various detail tabs
self.tabs = QtWidgets.QTabWidget(self)

# Initialize and add the Exchanges tab
# Initialize and add the Flows tab
self.exchanges_tab = ExchangesTab(activity, self)
self.tabs.addTab(self.exchanges_tab, "Exchanges")
self.tabs.addTab(self.exchanges_tab, "Flows")

# Initialize and add the Description tab
self.description_tab = DescriptionTab(activity, self)
Expand Down
16 changes: 12 additions & 4 deletions activity_browser/app/pages/activity_details/exchanges_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,11 @@ def column(self):

class ContextMenu(widgets.ABMenu):
menuSetup = [
lambda m: m.add(app.actions.ActivityOpen, [x.input for x in m.exchanges],
enable=bool(m.exchanges),
text="Open process" if len(m.exchanges) == 1 else "Open processes",
),
lambda m: m.addSeparator(),
lambda m: m.add(app.actions.ActivityNewProduct, [m.activity.key],
enable=not m.locked and not database_is_legacy(m.activity["database"])
),
Expand All @@ -556,11 +561,14 @@ class ContextMenu(widgets.ABMenu):
text="Create waste"
),
lambda m: m.addSeparator(),
lambda m: m.add(app.actions.ExchangeDelete, m.exchanges, enable=bool(m.exchanges) and not m.locked),
lambda m: m.add(app.actions.ExchangeSDFToClipboard, m.exchanges, enable=bool(m.exchanges)),
lambda m: m.add(app.actions.ActivityOpen, [x.input for x in m.exchanges],
lambda m: m.add(app.actions.ExchangeDelete, m.exchanges,
enable=bool(m.exchanges) and not m.locked,
text="Delete flow" if len(m.exchanges) == 1 else "Delete flows",
),
lambda m: m.addSeparator(),
lambda m: m.add(app.actions.ExchangeSDFToClipboard, m.exchanges,
enable=bool(m.exchanges),
text="Open processs" if len(m.exchanges) == 1 else "Open processes",
text="Copy for scenario file",
),
]

Expand Down
30 changes: 16 additions & 14 deletions activity_browser/app/panes/database_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,10 +277,6 @@ class ContextMenu(ui.widgets.ABMenu):
lambda m, p: m.add(app.actions.ActivityNewProcess, p.db_name,
enable=not database_is_locked(p.db_name),
),
lambda m, p: m.add(app.actions.NewElementaryFlow, p.db_name,
enable=p.db_name in get_writable_databases(),
text="New elementary flow",
),
lambda m, p: m.add(app.actions.ActivityDuplicate, p.selected_activities,
text="Duplicate process" if len(p.selected_activities) == 1 else "Duplicate processes",
enable=len(p.selected_activities) > 0 and not database_is_locked(p.db_name),
Expand All @@ -289,15 +285,6 @@ class ContextMenu(ui.widgets.ABMenu):
text="Duplicate process to database" if len(p.selected_activities) == 1 else "Duplicate processes to database",
enable=len(p.selected_activities) > 0,
),
lambda m: m.addSeparator(),
lambda m, p: m.add(app.actions.EditElementaryFlow, p.selected_elementary_flows,
text="Edit elementary flow",
enable=len(p.selected_elementary_flows) == 1 and p.db_name in get_writable_databases(),
),
lambda m, p: m.add(app.actions.DeleteElementaryFlow, p.selected_elementary_flows,
text="Delete elementary flow" if len(p.selected_elementary_flows) == 1 else "Delete elementary flows",
enable=len(p.selected_elementary_flows) > 0 and p.db_name in get_writable_databases(),
),
lambda m, p: m.add(app.actions.ActivityDelete, p.selected_activities,
text="Delete process" if len(p.selected_activities) == 1 else "Delete processes",
enable=len(p.selected_activities) > 0 and not database_is_locked(p.db_name),
Expand All @@ -309,13 +296,28 @@ class ContextMenu(ui.widgets.ABMenu):
database_is_legacy(p.db_name),
),
lambda m: m.addSeparator(),
lambda m, p: m.add(app.actions.NewElementaryFlow, p.db_name,
enable=p.db_name in get_writable_databases(),
text="New elementary flow",
),
lambda m, p: m.add(app.actions.EditElementaryFlow, p.selected_elementary_flows,
text="Edit elementary flow",
enable=len(p.selected_elementary_flows) == 1 and p.db_name in get_writable_databases(),
),
lambda m, p: m.add(app.actions.DeleteElementaryFlow, p.selected_elementary_flows,
text="Delete elementary flow" if len(p.selected_elementary_flows) == 1 else "Delete elementary flows",
enable=len(p.selected_elementary_flows) > 0 and p.db_name in get_writable_databases(),
),
lambda m: m.addSeparator(),
lambda m, p: m.add(app.actions.CSNew,
functional_units=[{prod: m.get_functional_unit_amount(prod)} for prod in p.selected_products],
enable=len(p.selected_products) > 0,
text="Create setup",
text="New setup",
),
lambda m: m.addSeparator(),
lambda m, p: m.add(app.actions.ActivitySDFToClipboard, p.selected_products,
enable=len(p.selected_products) > 0,
text="Copy for scenario file",
),
]

Expand Down
12 changes: 8 additions & 4 deletions activity_browser/bwutils/exporters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import numbers
import json
import math
import numbers
from datetime import datetime as dt
from pathlib import Path
from typing import Union
Expand All @@ -16,7 +17,6 @@

# Copied most of this code wholesale from bw2io package.
# TODO: reminder to make a pull-request for these things in bw2io repo.
# - Add the 'nan_inf_to_errors' option when opening the xlsxwriter.Workbook.
# - Add handler for pedigree data to exporter
# - Add 'database' field as required CSVFormatter export field.
# - Add code to ensure no second 'activity' field is exported, as this
Expand Down Expand Up @@ -105,7 +105,9 @@ def write_lci_excel(db_name: str, path: str, objs=None, sections=None) -> Path:
else:
out_file = path

workbook = xlsxwriter.Workbook(out_file, {"nan_inf_to_errors": True})
# Leave non-finite floats blank. Uncertainty dicts often store np.nan for
# unused stats_arrays fields; nan_inf_to_errors would turn those into #NUM!.
workbook = xlsxwriter.Workbook(out_file)
bold = workbook.add_format({"bold": True})
bold.set_font_size(12)
highlighted = {
Expand All @@ -127,7 +129,9 @@ def write_lci_excel(db_name: str, path: str, objs=None, sections=None) -> Path:
if value is None:
continue
elif isinstance(value, numbers.Number):
sheet.write_number(row_index, col_index, value, frmt(value))
# Keep bools as 0/1 (previous write_number behaviour); skip NaN/Inf.
if isinstance(value, bool) or math.isfinite(float(value)):
sheet.write_number(row_index, col_index, value, frmt(value))
else:
sheet.write_string(row_index, col_index, frmt_str(value), frmt(value))

Expand Down
6 changes: 3 additions & 3 deletions activity_browser/bwutils/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
csv_restore_booleans, csv_restore_tuples,
drop_falsey_uncertainty_fields_but_keep_zeros,
link_iterable_by_fields,
link_technosphere_by_activity_hash,
normalize_biosphere_categories,
normalize_biosphere_names, normalize_units,
set_code_by_activity_hash,
Expand All @@ -24,7 +23,8 @@
from .errors import LinkingFailed
from .strategies import (alter_database_name, csv_rewrite_product_key,
hash_parameter_group, link_exchanges_without_db,
link_functional_processors, relink_exchanges_bw2package,
link_functional_processors, link_technosphere_same_database,
relink_exchanges_bw2package,
relink_exchanges_with_db, rename_db_bw2package,
parse_JSON_fields, metadatastore_link,
alter_exchange_database_name)
Expand Down Expand Up @@ -57,7 +57,7 @@ def _excel_link_strategies(relink: dict | None = None):
other=bd.Database(bd.config.biosphere),
kind="biosphere",
),
link_technosphere_by_activity_hash,
link_technosphere_same_database,
]
if relink is not None:
strategies.extend([
Expand Down
31 changes: 31 additions & 0 deletions activity_browser/bwutils/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from bw2io.errors import StrategyError
from bw2io.strategies.generic import (format_nonunique_key_error,
link_iterable_by_fields)
from bw2io.strategies import link_technosphere_by_activity_hash
from bw2io.utils import DEFAULT_FIELDS, activity_hash

import bw2data as bd
Expand Down Expand Up @@ -63,6 +64,36 @@ def _relink_exchange_input(exc: dict, relink: dict) -> None:
exc["input"] = new_key


_SKIP_INTERNAL_LINK = ("__ab_skip_internal_link__", "")


def link_technosphere_same_database(data: list) -> list:
"""Link technosphere exchanges internally, respecting exchange ``database``.

``bw2io``'s ``link_technosphere_by_activity_hash`` matches on name/location/unit
fields only. That wrongly self-links exchanges that already name another
database (e.g. a local market activity that also imports the same-named market
from an external DB). Leave those unlinked for later external linking.
"""
protected = []
for ds in data:
db = ds.get("database")
for exc in ds.get("exchanges", []):
if exc.get("input") or exc.get("type") not in TECHNOSPHERE_TYPES:
continue
exc_db = exc.get("database")
if exc_db and db and exc_db != db:
exc["input"] = _SKIP_INTERNAL_LINK
protected.append(exc)

link_technosphere_by_activity_hash(data)

for exc in protected:
if exc.get("input") == _SKIP_INTERNAL_LINK:
del exc["input"]
return data


def link_functional_processors(data: list) -> list:
"""Ensure product ``processor`` fields reference process nodes in the same import.

Expand Down
63 changes: 63 additions & 0 deletions tests/test_excel_link_technosphere.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Excel import: internal technosphere linking must respect exchange database."""
from copy import deepcopy

from activity_browser.bwutils.strategies import link_technosphere_same_database


def _ev_case_like_dataset():
"""Local market plus a same-named technosphere input from another database."""
return [
{
"database": "EV case",
"code": "local_market",
"name": "market for electricity, low voltage",
"location": "NL",
"unit": "kilowatt hour",
"reference product": "electricity, low voltage",
"exchanges": [
{
"type": "production",
"amount": 1,
"name": "market for electricity, low voltage",
"database": "EV case",
"location": "NL",
"unit": "kilowatt hour",
"reference product": "electricity, low voltage",
},
{
"type": "technosphere",
"amount": 1,
"name": "market for electricity, low voltage",
"database": "ei3.11-REMIND",
"location": "NL",
"unit": "kilowatt hour",
"reference product": "electricity, low voltage",
},
{
"type": "technosphere",
"amount": 0.25,
"name": "market for electricity, low voltage",
"database": "EV case",
"location": "NL",
"unit": "kilowatt hour",
"reference product": "electricity, low voltage",
},
],
}
]


def test_foreign_database_technosphere_not_self_linked():
data = link_technosphere_same_database(deepcopy(_ev_case_like_dataset()))
exchanges = data[0]["exchanges"]
production = next(e for e in exchanges if e["type"] == "production")
foreign = next(e for e in exchanges if e.get("database") == "ei3.11-REMIND")
local = next(
e
for e in exchanges
if e["type"] == "technosphere" and e.get("database") == "EV case"
)

assert production.get("input") == ("EV case", "local_market")
assert local.get("input") == ("EV case", "local_market")
assert "input" not in foreign
Loading
Loading