Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parameter to flow scenarios #394

Merged
merged 5 commits into from
May 6, 2020
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
11 changes: 7 additions & 4 deletions activity_browser/app/bwutils/presamples/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,17 @@ def reformat_indices(self) -> np.ndarray:
result[i] = (idx.input, idx.output, idx.input.database_type)
return result

def presamples_from_scenarios(self, name: str, scenarios: Iterable[Tuple[str, Iterable]]) -> (str, str):
""" When given a iterable of multiple parameter scenarios, construct
a presamples package with all of the recalculated exchange amounts.
"""
def arrays_from_scenarios(self, scenarios) -> (np.ndarray, np.ndarray):
sample_data = [self.recalculate(list(values)) for _, values in scenarios]
samples = np.concatenate(sample_data, axis=1)
indices = self.reformat_indices()
return samples, indices

def presamples_from_scenarios(self, name: str, scenarios: Iterable[Tuple[str, Iterable]]) -> (str, str):
""" When given a iterable of multiple parameter scenarios, construct
a presamples package with all of the recalculated exchange amounts.
"""
samples, indices = self.arrays_from_scenarios(scenarios)
arrays = ps.split_inventory_presamples(samples, indices)
ps_id, ps_path = ps.create_presamples_package(
matrix_data=arrays, name=name, seed="sequential"
Expand Down
4 changes: 3 additions & 1 deletion activity_browser/app/bwutils/superstructure/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from .activities import all_flows_found, all_activities_found, fill_df_keys_with_fields
from .dataframe import build_superstructure, scenario_names_from_df
from .dataframe import (
build_superstructure, scenario_names_from_df, superstructure_from_arrays
)
from .excel import import_from_excel, get_sheet_names
from .exchanges import all_exchanges_found, filter_existing_exchanges
from .mlca import SuperstructureMLCA, SuperstructureContributions
Expand Down
10 changes: 10 additions & 0 deletions activity_browser/app/bwutils/superstructure/activities.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ def constuct_ad_data(row) -> tuple:
return key, data


def data_from_index(index: tuple) -> tuple:
"""Take the given 'Index' tuple and build a complete SUPERSTRUCTURE row
from it.
"""
from_key, to_key = index[0], index[1]
from_key, from_data = constuct_ad_data(ActivityDataset.get(database=from_key[0], code=from_key[1]))
to_key, to_data = constuct_ad_data(ActivityDataset.get(database=to_key[0], code=to_key[1]))
return tuple([*from_data, from_key, *to_data, to_key])


def all_flows_found(df: pd.DataFrame, part: str = "from") -> bool:
"""Determines if all activities from the given 'from' or 'to' chunk"""
select = FROM_BIOS if part == "from" else TO_BIOS
Expand Down
28 changes: 27 additions & 1 deletion activity_browser/app/bwutils/superstructure/dataframe.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# -*- coding: utf-8 -*-
from typing import Iterable
from typing import Iterable, List

import numpy as np
import pandas as pd

from .activities import data_from_index
from .utils import SUPERSTRUCTURE


Expand Down Expand Up @@ -68,6 +70,30 @@ def build_superstructure(data: list) -> pd.DataFrame:
return df


def superstructure_from_arrays(samples: np.ndarray, indices: np.ndarray, names: List[str] = None) -> pd.DataFrame:
"""Process indices into the superstructure itself, the samples represent
the scenarios.
"""
assert samples.ndim == 2, "Samples array should be 2-dimensional"
assert indices.ndim == 1, "Indices array should be 1-dimensional"
assert samples.shape[0] == indices.shape[0], "Length mismatch between arrays"
if names is not None:
assert len(names) == samples.shape[1], "Number of names should match number of samples columns"
names = pd.Index(names)
else:
names = pd.Index(["scenario{}".format(i+1) for i in range(samples.shape[1])])

# Construct superstructure from indices
superstructure = pd.DataFrame(
[data_from_index(idx) for idx in indices], columns=SUPERSTRUCTURE
)
# Construct scenarios from samples
scenarios = pd.DataFrame(samples, columns=names)

df = pd.concat([superstructure, scenarios], axis=1)
return df


def match_exchanges(origin: dict, delta: Iterable, db_name: str) -> dict:
"""Matches a delta iterable against the superstructure dictionary,
appending exchanges to the relevant keys.
Expand Down
75 changes: 55 additions & 20 deletions activity_browser/app/ui/tabs/parameters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
import uuid
from pathlib import Path

import brightway2 as bw
from bw2data.filesystem import safe_filename
Expand All @@ -19,7 +19,7 @@
ActivityParameterTable, DataBaseParameterTable, ExchangesTable,
ProjectParameterTable, ScenarioTable
)
from ..widgets import ForceInputDialog
from ..widgets import ChoiceSelectionDialog, ForceInputDialog
from .base import BaseRightTab


Expand Down Expand Up @@ -317,19 +317,19 @@ def build_tables(self) -> None:

@Slot(name="loadSenarioTable")
def select_read_file(self):
path, _ = QFileDialog().getOpenFileName(
path, _ = QFileDialog.getOpenFileName(
self, caption="Select prepared scenario file",
dir=project_settings.data_dir, filter=self.tbl.EXCEL_FILTER
filter=self.tbl.EXCEL_FILTER
)
if path:
df = ps_utils.load_scenarios_from_file(path)
self.tbl.sync(df=df)

@Slot(name="saveScenarioTable")
def save_scenarios(self):
filename, _ = QFileDialog().getSaveFileName(
filename, _ = QFileDialog.getSaveFileName(
self, caption="Save current scenarios to Excel",
dir=project_settings.data_dir, filter=self.tbl.EXCEL_FILTER
filter=self.tbl.EXCEL_FILTER
)
if filename:
try:
Expand All @@ -351,22 +351,57 @@ def calculate_scenarios(self):
QMessageBox.Ok, QMessageBox.Ok
)
return
dialog = ForceInputDialog.get_text(
self, "Add label", "Add a label to the calculated scenarios"
flow_scenarios = "Save as flow scenarios (excel)"
presamples = "Save as presamples package (presamples)"
choice_dlg = ChoiceSelectionDialog.get_choice(self, flow_scenarios, presamples)
if choice_dlg.exec_() != ChoiceSelectionDialog.Accepted:
return
if choice_dlg.choice == flow_scenarios:
self.build_flow_scenarios()
elif choice_dlg.choice == presamples:
dialog = ForceInputDialog.get_text(
self, "Add label", "Add a label to the calculated scenarios"
)
if dialog.exec_() == ForceInputDialog.Accepted:
result = dialog.output
if result in ps_utils.find_all_package_names():
overwrite = QMessageBox.question(
self, "Label already in use", "Overwrite the old calculations?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
)
if overwrite == QMessageBox.Yes:
older = ps_utils.get_package_path(result)
ps_utils.remove_package(older)
self.build_presamples_packages(safe_filename(result, False))
else:
self.build_presamples_packages(safe_filename(result, False))

def build_flow_scenarios(self) -> None:
"""Calculate exchange changes for each parameter scenario and construct
a flow scenarios template file.
"""
from ...bwutils.superstructure import superstructure_from_arrays

ppm = ps_utils.PresamplesParameterManager()
names, data = zip(*self.tbl.iterate_scenarios())
samples, indices = ppm.arrays_from_scenarios(zip(names, data))
df = superstructure_from_arrays(samples, indices, names)
filename, _ = QFileDialog.getSaveFileName(
self, caption="Save calculated flow scenarios to Excel",
filter=self.tbl.EXCEL_FILTER
)
if dialog.exec_() == ForceInputDialog.Accepted:
result = dialog.output
if result in ps_utils.find_all_package_names():
overwrite = QMessageBox.question(
self, "Label already in use", "Overwrite the old calculations?",
QMessageBox.Yes | QMessageBox.No, QMessageBox.No
if filename:
try:
path = Path(filename)
path = path if path.suffix in {".xlsx", ".xls"} else path.with_suffix(".xlsx")
df.to_excel(excel_writer=path, index=False)
except FileCreateError as e:
QMessageBox.warning(
self, "File save error",
"Cannot save the file, please see if it is opened elsewhere or "
"if you are allowed to save files in that location:\n\n{}".format(e),
QMessageBox.Ok, QMessageBox.Ok
)
if overwrite == QMessageBox.Yes:
older = ps_utils.get_package_path(result)
ps_utils.remove_package(older)
self.build_presamples_packages(safe_filename(result, False))
else:
self.build_presamples_packages(safe_filename(result, False))

def build_presamples_packages(self, name: str):
""" Calculate and store presamples arrays from parameter scenarios.
Expand Down
4 changes: 3 additions & 1 deletion activity_browser/app/ui/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from .comparison_switch import SwitchComboBox
from .cutoff_menu import CutoffMenu
from .database_copy import CopyDatabaseDialog
from .dialog import ForceInputDialog, TupleNameDialog, ExcelReadDialog
from .dialog import (
ForceInputDialog, TupleNameDialog, ExcelReadDialog, ChoiceSelectionDialog,
)
from .line_edit import (SignalledPlainTextEdit, SignalledComboEdit,
SignalledLineEdit)
from .message import parameter_save_errorbox, simple_warning_box
49 changes: 49 additions & 0 deletions activity_browser/app/ui/widgets/dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,55 @@ def get_text(cls, parent: QtWidgets.QWidget, title: str, label: str, text: str =
return obj


class ChoiceSelectionDialog(QtWidgets.QDialog):
"""Given a number of options, select one of them."""
def __init__(self, parent=None):
super().__init__(parent)

self.input_box = QtWidgets.QGroupBox(self)
self.input_box.setStyleSheet(style_group_box.border_title)
input_field_layout = QtWidgets.QVBoxLayout()
self.input_box.setLayout(input_field_layout)
self.group = QtWidgets.QButtonGroup(self)
self.group.setExclusive(True)

self.buttons = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel,
)
self.buttons.accepted.connect(self.accept)
self.buttons.rejected.connect(self.reject)

layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.input_box)
layout.addWidget(self.buttons)
self.setLayout(layout)

@property
def choice(self) -> str:
"""Returns the name of the chosen option, allowing for a comparison"""
checked = self.group.checkedButton()
return checked.text()

@classmethod
def get_choice(cls, parent: QtWidgets.QWidget, *choices) -> 'ChoiceSelectionDialog':
assert len(choices) > 0, "Must give choices to choose from."

obj = cls(parent)
obj.setWindowTitle("Select the option")

iterable = iter(choices)
first = QtWidgets.QRadioButton(str(next(iterable)))
first.setChecked(True)
obj.group.addButton(first)
obj.input_box.layout().addWidget(first)
for choice in iterable:
btn = QtWidgets.QRadioButton(str(choice))
obj.group.addButton(btn)
obj.input_box.layout().addWidget(btn)
obj.input_box.updateGeometry()
return obj


class TupleNameDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
Expand Down