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 @@ -308,8 +308,13 @@ def __init__(self, parent: ActivityHeader, disabled: bool = False):
database_widget = QtWidgets.QLabel(parent.activity.get("database", "unspecified"), self)
database_widget.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.TextSelectableByMouse)

db_icon = (
"database_functional_sqlite"
if parent._database_backend == "functional_sqlite"
else "database"
)
layout.addWidget(location_widget, 0)
layout.addWidget(parent._icon_label("database", "Database"))
layout.addWidget(parent._icon_label(db_icon, "Database"))
layout.addWidget(database_widget, 1)


Expand Down
24 changes: 3 additions & 21 deletions activity_browser/app/pages/activity_details/exchanges_tab.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
from activity_browser import app
from activity_browser.bwutils.commontasks import (refresh_node, database_is_locked, database_is_legacy,
is_node_product_or_waste, is_node_biosphere, parameters_in_scope,
is_node_product, is_node_waste, get_exchange_type)
is_node_product, is_node_waste, get_exchange_type,
classify_dragged_nodes)
from activity_browser.bwutils.uncertainty import uncertainty_cell_summary
from activity_browser.ui import widgets, icons, delegates, core

Expand Down Expand Up @@ -356,26 +357,7 @@ def action_from_mime(self, mime: core.ABMimeData) -> Literal["product", "waste",

"""
keys = mime.retrievePickleData("application/bw-nodekeylist")
actions: set[str] = set()
for key in keys:
if is_node_waste(key):
actions.add("waste")
elif is_node_product(key):
actions.add("product")
elif is_node_biosphere(key):
node_type = refresh_node(key)._document.type
if node_type == "natural resource":
actions.add("resource")
elif node_type == "emission":
actions.add("emission")
else:
actions.add("generic")
else:
actions.add("generic")

if len(actions) != 1:
return "generic"
return actions.pop() # type: ignore[return-value]
return classify_dragged_nodes(keys) # type: ignore[return-value]


class RelinkDelegate(delegates.StringDelegate):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,8 @@ def sync_inclusion_flags(self, flags: list[bool]) -> None:

def scenario_db_check(self, df: pd.DataFrame) -> pd.DataFrame:
dbs = set(df.loc[:, "from database"]).union(set(df.loc[:, "to database"]))
# Ignore missing / non-string cells (e.g. NaN) — they are not DB names.
dbs = {db for db in dbs if isinstance(db, str) and db.strip()}
unlinkable = dbs.difference(bd.databases)
db_lst = list(bd.databases)
relink = []
Expand Down Expand Up @@ -1149,11 +1151,12 @@ def __init__(self, parent=None):
"<h4>Template...</h4>"
"<p>Download an empty flow or parameter starter (.xlsx or .csv). "
"If the project has parameters, the parameter template is filled with "
"Name / Group / default and empty example scenario columns. "
"Lines or columns starting with <code>#</code> are ignored on import "
"(useful for your notes).</p>"
"Name / Group / default and empty example scenario columns.</p>"
"<h4>Save...</h4>"
"<p>Writes the currently loaded, merged flow-scenario table to a file.</p>"
"<h4>Notes</h4> "
"<p>Lines starting with <b>#</b> and columns starting with <b>_</b> are ignored on import "
"(useful for your notes).</p>"
)
text.setOpenExternalLinks(False)

Expand Down
32 changes: 32 additions & 0 deletions activity_browser/bwutils/commontasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,38 @@ def get_exchange_type(activity_key: tuple, output: bool = False) -> str | None:
return None


def classify_dragged_nodes(keys: list) -> str:
"""
Overlay / drop action for a set of dragged node keys.

``ProductModel.mimeData`` includes both product/waste keys and their processor
process keys; process nodes are ignored so a product+processor drag stays
``product`` (same idea as discarding process types from metadata).
"""
actions: set[str] = set()
for key in keys:
if is_node_waste(key):
actions.add("waste")
elif is_node_product(key):
actions.add("product")
elif is_node_biosphere(key):
node_type = refresh_node(key)._document.type
if node_type == "natural resource":
actions.add("resource")
elif node_type == "emission":
actions.add("emission")
else:
actions.add("generic")
elif is_node_process(key):
continue
else:
actions.add("generic")

if len(actions) != 1:
return "generic"
return actions.pop()


def is_node_process(node: tuple | int | bd.Node) -> bool:
node = refresh_node(node)
raw_type = node._document.type
Expand Down
17 changes: 6 additions & 11 deletions activity_browser/bwutils/superstructure/excel.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@
from .dataframe import ensure_string_scenario_names




def convert_tuple_str(x):
try:
return literal_eval(x)
Expand All @@ -39,7 +37,8 @@ def get_header_index(document_path: Union[str, Path], import_sheet: int):
sheet = wb.worksheets[import_sheet]
for i in range(10):
value = sheet.cell(i + 1, 1).value
if isinstance(value, str):
# Skip SDF comment rows (first cell starts with '#').
if isinstance(value, str) and not value.startswith("#"):
wb.close()
return i
except IndexError as e:
Expand All @@ -54,8 +53,8 @@ def get_header_index(document_path: Union[str, Path], import_sheet: int):


def valid_cols(name: str) -> bool:
"""Callable which evaluates if a specific column should be used."""
return False if str(name).startswith("#") else True
"""True for data columns; names starting with '_' are SDF comment columns (not imported)."""
return not str(name).startswith("_")


def import_from_excel(
Expand All @@ -67,12 +66,8 @@ def import_from_excel(
The default index chosen represents the second sheet (first after the
'information' sheet).

A '#' character at the start of a row causes that row to be excluded from
the import. A '#' character at the start of a column name causes that
column to be excluded from the import.

'usecols' is used to exclude specific columns from the excel document.
'comment' is used to exclude specific rows from the excel document.
Comment rows: a '#' at the start of a row (pandas ``comment='#'``).
Comment columns: a column name starting with '_' (``usecols=valid_cols``).
"""
data = pd.DataFrame({})
try:
Expand Down
3 changes: 3 additions & 0 deletions activity_browser/bwutils/superstructure/file_imports.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

from ..errors import *
from .dataframe import ensure_string_scenario_names
from .excel import valid_cols




Expand Down Expand Up @@ -228,6 +230,7 @@ def read_file(path: Optional[Union[str, Path]], **kwargs):
sep=separator,
index_col=False,
comment="#",
usecols=valid_cols,
converters={"from key": ast.literal_eval, "to key": ast.literal_eval},
)
# Scenario headers typed as numbers (e.g. 2025) must be strings.
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion activity_browser/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ flow = templates / "scenarios" / "flow-scenarios.xlsx"
Excel workbooks: **data sheet first**, then **`README`**.
CSV files: header row, blank rows, then notes on lines starting with `#` (ignored on import).

Rows or columns whose first cell / header starts with `#` are ignored by scenario import (Excel and CSV).
Scenario import comments (Excel and CSV):

- **Rows:** start with `#` (ignored via pandas `comment="#"`).
- **Columns:** name starts with `_` (e.g. `_notes`; dropped via `usecols`).

**Get template → flow-scenarios** always copies the empty starter file (does not generate from project parameters).

Expand Down
2 changes: 1 addition & 1 deletion activity_browser/templates/scenarios/flow-scenarios.csv
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ from activity name;from reference product;from location;from categories;from dat
;;;;;;;;;;;;;;;
# Flow scenario template (CSV);;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;
# Rows or columns whose first cell / header starts with # are ignored when Activity Browser loads the file. Use # to leave notes in the file.;;;;;;;;;;;;;;;
"# How to fill: in Activity Browser, select flows/products, right-click ""Copy for scenario file"", paste under the header row, then enter scenario amounts.";;;;;;;;;;;;;;;
"# Empty scenario cells mean ""use the default inventory amount"".";;;;;;;;;;;;;;;
# Rename S1/S2/S3 example headers to your scenario names as needed.;;;;;;;;;;;;;;;
# Comment rows: start the row with # (ignored on import). Comment columns: name them with a leading _ (e.g. _notes).;;;;;;;;;;;;;;;
# Load via calculation setup Scenario mode, Add scenarios...;;;;;;;;;;;;;;;
Binary file modified activity_browser/templates/scenarios/flow-scenarios.xlsx
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Name;Group;default;S1 example;S2 example;S3 example
;;;;;
# Parameter scenario template (CSV);;;;;
;;;;;
# Rows or columns whose first cell / header starts with # are ignored when Activity Browser loads the file. Use # to leave notes in the file.;;;;;
"# Columns: Name and Group must match parameters in your project; default is optional; add scenario columns with numeric values.";;;;;
"# Prefer ""Get template"" from the calculation setup Scenarios tools to start from your project parameters when available.";;;;;
# Comment rows: start the row with # (ignored on import). Comment columns: name them with a leading _ (e.g. _notes).;;;;;
# Load via calculation setup Scenario mode, Add scenarios... (AB converts parameter scenarios to flow scenarios for LCA).;;;;;
Binary file modified activity_browser/templates/scenarios/parameter-scenarios.xlsx
Binary file not shown.
1 change: 1 addition & 0 deletions activity_browser/ui/icons.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ def empty_icon(size: QSize = QSize(32, 32)) -> QIcon:
# activity details
location = create_path("activity details", "location.png"),
database = create_path("activity details", "database.png"),
database_functional_sqlite = create_path("activity details", "database_functional_sqlite.png"),
allocation = create_path("activity details", "allocation.png"),
properties = create_path("activity details", "properties.png"),

Expand Down
9 changes: 9 additions & 0 deletions docs/advanced-topics/scenario-calculations.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ Flow scenarios allow you to directly change the values of flows in the technosph

When you import a flow scenario into a calculation setup, the Activity Browser will directly substitute the values in the technosphere matrix with the ones defined in the scenario file during calculation.

### Comments in scenario files

Flow scenario (SDF) files may include comments that are ignored on import:

- **Comment rows:** start the row with `#` (first cell, or a full CSV line). Handled by pandas `comment="#"`.
- **Comment columns:** give the column a name that starts with `_` (for example `_notes`). These are dropped via `usecols`.

Do not start comment **column** names with `#` — that conflicts with pandas row comments and can corrupt the header.

## Combining or extending scenarios
You can add multiple scenarios to a calculation setup. This allows you to easily compare different versions of your model and see how changes in one scenario affect the results of another.

Expand Down
5 changes: 5 additions & 0 deletions tests/test_product_waste_links.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from bw2data.tests import bw2test

from activity_browser.bwutils.commontasks import (
classify_dragged_nodes,
get_exchange_type,
is_node_product,
is_node_waste,
Expand Down Expand Up @@ -190,6 +191,10 @@ def test_functional_product_waste_links():
assert is_node_waste((DB, "w")) and not is_node_product((DB, "w"))
_assert_drop_cases(FUNCTIONAL_DROP_CASES)

# ProductModel.mimeData attaches product/waste key + processor process
assert classify_dragged_nodes([(DB, "p"), (DB, "P")]) == "product"
assert classify_dragged_nodes([(DB, "w"), (DB, "W")]) == "waste"

for host, dragged, on_output in LINK_SPECS:
_add_drop_link((DB, host), (DB, dragged.lower()), on_output)
bf.FunctionalSQLiteDatabase(DB).process()
Expand Down
40 changes: 40 additions & 0 deletions tests/test_sdf_comment_columns.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""SDF comments: '#' rows (pandas comment=) and '_' columns (usecols).

Keep this file free of Excel/openpyxl I/O so it stays cheap in CI.
Excel uses the same pandas knobs; column rule is covered by ``valid_cols``.
"""
from activity_browser.bwutils.superstructure.excel import valid_cols
from activity_browser.bwutils.superstructure.file_imports import ABCSVImporter
from activity_browser.bwutils.superstructure.utils import SUPERSTRUCTURE


def test_valid_cols_drops_underscore_prefix():
assert valid_cols("_notes") is False
assert valid_cols("2025") is True
assert valid_cols("from database") is True


def test_csv_hash_rows_and_underscore_columns(tmp_path):
cols = list(SUPERSTRUCTURE) + ["_notes", "2025"]
header = ";".join(cols)
data = (
"A;p;GLO;;db1;('db1', 'a');B;q;GLO;;db2;('db2', 'b');technosphere;x;1.0"
)
text = "\n".join(
[
"# leading file comment",
header,
data,
"# skipped data row",
]
)
path = tmp_path / "sdf.csv"
path.write_text(text, encoding="utf-8")

df = ABCSVImporter.read_file(path, separator=";")

assert list(df.columns) == cols[:-2] + ["2025"] # _notes dropped
assert "_notes" not in df.columns
assert len(df) == 1
assert df.loc[0, "from database"] == "db1"
assert float(df.loc[0, "2025"]) == 1.0
Loading