Skip to content

Commit

Permalink
Merge 7e53124 into 21f38b2
Browse files Browse the repository at this point in the history
  • Loading branch information
mrvisscher committed Mar 4, 2024
2 parents 21f38b2 + 7e53124 commit 1998ce1
Show file tree
Hide file tree
Showing 105 changed files with 3,445 additions and 2,162 deletions.
50 changes: 50 additions & 0 deletions activity_browser/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from .activity.activity_relink import ActivityRelink
from .activity.activity_new import ActivityNew
from .activity.activity_duplicate import ActivityDuplicate
from .activity.activity_open import ActivityOpen
from .activity.activity_graph import ActivityGraph
from .activity.activity_duplicate_to_loc import ActivityDuplicateToLoc
from .activity.activity_delete import ActivityDelete
from .activity.activity_duplicate_to_db import ActivityDuplicateToDB

from .calculation_setup.cs_new import CSNew
from .calculation_setup.cs_delete import CSDelete
from .calculation_setup.cs_duplicate import CSDuplicate
from .calculation_setup.cs_rename import CSRename

from .database.database_import import DatabaseImport
from .database.database_export import DatabaseExport
from .database.database_new import DatabaseNew
from .database.database_delete import DatabaseDelete
from .database.database_duplicate import DatabaseDuplicate
from .database.database_relink import DatabaseRelink

from .exchange.exchange_new import ExchangeNew
from .exchange.exchange_delete import ExchangeDelete
from .exchange.exchange_modify import ExchangeModify
from .exchange.exchange_formula_remove import ExchangeFormulaRemove
from .exchange.exchange_uncertainty_modify import ExchangeUncertaintyModify
from .exchange.exchange_uncertainty_remove import ExchangeUncertaintyRemove
from .exchange.exchange_copy_sdf import ExchangeCopySDF

from .method.method_duplicate import MethodDuplicate
from .method.method_delete import MethodDelete

from .method.cf_uncertainty_modify import CFUncertaintyModify
from .method.cf_amount_modify import CFAmountModify
from .method.cf_remove import CFRemove
from .method.cf_new import CFNew
from .method.cf_uncertainty_remove import CFUncertaintyRemove

from .parameter.parameter_new import ParameterNew
from .parameter.parameter_new_automatic import ParameterNewAutomatic
from .parameter.parameter_rename import ParameterRename

from .project.project_new import ProjectNew
from .project.project_duplicate import ProjectDuplicate
from .project.project_delete import ProjectDelete

from .default_install import DefaultInstall
from .biosphere_update import BiosphereUpdate
from .plugin_wizard_open import PluginWizardOpen
from .settings_wizard_open import SettingsWizardOpen
45 changes: 45 additions & 0 deletions activity_browser/actions/activity/activity_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Union, Callable, List

from PySide2 import QtWidgets, QtCore

from activity_browser import application, activity_controller
from activity_browser.ui.icons import qicons
from activity_browser.actions.base import ABAction


class ActivityDelete(ABAction):
"""
ABAction to delete one or multiple activities if supplied by activity keys. Will check if an activity has any
downstream processes and ask the user whether they want to continue if so. Exchanges from any downstream processes
will be removed
"""
icon = qicons.delete
title = 'Delete ***'
activity_keys: List[tuple]

def __init__(self, activity_keys: Union[List[tuple], Callable], parent: QtCore.QObject):
super().__init__(parent, activity_keys=activity_keys)

def onTrigger(self, toggled):
# retrieve activity objects from the controller using the provided keys
activities = activity_controller.get_activities(self.activity_keys)

# check for downstream processes
if any(len(act.upstream()) > 0 for act in activities):
# warning text
text = ("One or more activities have downstream processes. Deleting these activities will remove the "
"exchange from the downstream processes, this can't be undone.\n\nAre you sure you want to "
"continue?")

# alert the user
choice = QtWidgets.QMessageBox.warning(application.main_window,
"Activity/Activities has/have downstream processes",
text,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No)

# return if the user cancels
if choice == QtWidgets.QMessageBox.No: return

# use the activity controller to delete multiple activities
activity_controller.delete_activities(self.activity_keys)
24 changes: 24 additions & 0 deletions activity_browser/actions/activity/activity_duplicate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Union, Callable, List

from PySide2 import QtCore

from activity_browser import activity_controller
from activity_browser.ui.icons import qicons
from activity_browser.actions.base import ABAction


class ActivityDuplicate(ABAction):
"""
Duplicate one or multiple activities using their keys. Proxy action to call the controller.
"""
icon = qicons.copy
title = 'Duplicate ***'
activity_keys: List[tuple]

def __init__(self, activity_keys: Union[List[tuple], Callable], parent: QtCore.QObject):
super().__init__(parent, activity_keys=activity_keys)

def onTrigger(self, toggled):
activity_controller.duplicate_activities(self.activity_keys)


56 changes: 56 additions & 0 deletions activity_browser/actions/activity/activity_duplicate_to_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Union, Callable, List

from PySide2 import QtWidgets, QtCore

from activity_browser import application, project_settings, activity_controller
from activity_browser.ui.icons import qicons
from activity_browser.actions.base import ABAction


class ActivityDuplicateToDB(ABAction):
"""
ABAction to duplicate an activity to another database. Asks the user to what database they want to copy the activity
to, returns if there are no valid databases or when the user cancels. Otherwise uses the activity controller to
duplicate the activities to the chosen database.
"""
icon = qicons.duplicate_to_other_database
title = 'Duplicate to other database'
activity_keys: List[tuple]

def __init__(self, activity_keys: Union[List[tuple], Callable], parent: QtCore.QObject):
super().__init__(parent, activity_keys=activity_keys)

def onTrigger(self, toggled):
# get bw activity objects from keys
activities = activity_controller.get_activities(self.activity_keys)

# get valid databases (not the original database, or locked databases)
origin_db = next(iter(activities)).get("database")
target_dbs = [db for db in project_settings.get_editable_databases() if db != origin_db]

# return if there are no valid databases to duplicate to
if not target_dbs:
QtWidgets.QMessageBox.warning(
application.main_window,
"No target database",
"No valid target databases available. Create a new database or set one to writable (not read-only)."
)
return

# construct a dialog where the user can choose a database to duplicate to
target_db, ok = QtWidgets.QInputDialog.getItem(
application.main_window,
"Copy activity to database",
"Target database:",
target_dbs,
0,
False
)

# return if the user didn't choose, or canceled
if not target_db or not ok: return

# otherwise move all supplied activities to the db using the controller
for activity in activities:
activity_controller.duplicate_activity_to_db(target_db, activity)

170 changes: 170 additions & 0 deletions activity_browser/actions/activity/activity_duplicate_to_loc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
from typing import Union, Callable, Optional

import pandas as pd
import brightway2 as bw
from PySide2 import QtCore

from activity_browser import signals, application, activity_controller, exchange_controller
from activity_browser.bwutils import AB_metadata
from activity_browser.ui.icons import qicons
from activity_browser.actions.base import ABAction
from ...ui.widgets import LocationLinkingDialog


class ActivityDuplicateToLoc(ABAction):
"""
ABAction to duplicate an activity and possibly their exchanges to a new location.
"""
icon = qicons.copy
title = 'Duplicate activity to new location'
activity_key: tuple
db_name: str

def __init__(self, activity_key: Union[tuple, Callable], parent: QtCore.QObject):
super().__init__(parent, activity_key=activity_key)

def onTrigger(self, toggled):
act = activity_controller.get_activities(self.activity_key)[0]
self.db_name = act.key[0]

# get list of dependent databases for activity and load to MetaDataStore
databases = []
for exchange in act.technosphere():
databases.append(exchange.input[0])
if self.db_name not in databases: # add own database if it wasn't added already
databases.append(self.db_name)

# load all dependent databases to MetaDataStore
dbs = {db: AB_metadata.get_database_metadata(db) for db in databases}
# get list of all unique locations in the dependent databases (sorted alphabetically)
locations = []
for db in dbs.values():
locations += db['location'].to_list() # add all locations to one list
locations = list(set(locations)) # reduce the list to only unique items
locations.sort()

# get the location to relink
db = dbs[self.db_name]
old_location = db.loc[db['key'] == act.key]['location'].iloc[0]

# trigger dialog with autocomplete-writeable-dropdown-list
options = (old_location, locations)
dialog = LocationLinkingDialog.relink_location(act['name'], options, application.main_window)

if dialog.exec_() != LocationLinkingDialog.Accepted: return

# read the data from the dialog
for old, new in dialog.relink.items():
alternatives = []
new_location = new
if dialog.use_rer.isChecked(): # RER
alternatives.append(dialog.use_rer.text())
if dialog.use_ews.isChecked(): # Europe without Switzerland
alternatives.append(dialog.use_ews.text())
if dialog.use_row.isChecked(): # RoW
alternatives.append(dialog.use_row.text())
# the order we add alternatives is important, they are checked in this order!
if len(alternatives) > 0:
use_alternatives = True
else:
use_alternatives = False

successful_links = {} # dict of dicts, key of new exch : {new values} <-- see 'values' below
# in the future, 'alternatives' could be improved by making use of some location hierarchy. From that we could
# get things like if the new location is NL but there is no NL, but RER exists, we use that. However, for that
# we need some hierarchical structure to the location data, which may be available from ecoinvent, but we need
# to look for that.

# get exchanges that we want to relink
for exch in act.technosphere():
candidate = self.find_candidate(dbs, exch, old_location, new_location, use_alternatives, alternatives)
if candidate is None:
continue # no suitable candidate was found, try the next exchange

# at this point, we have found 1 suitable candidate, whether that is new_location or alternative location
values = {
'amount': exch.get('amount', False),
'comment': exch.get('comment', False),
'formula': exch.get('formula', False),
'uncertainty': exch.get('uncertainty', False)
}
successful_links[candidate['key'].iloc[0]] = values

# now, create a new activity by copying the old one
new_code = activity_controller.generate_copy_code(act.key)
new_act = act.copy(new_code)
# update production exchanges
for exc in new_act.production():
if exc.input.key == act.key:
exc.input = new_act
exc.save()
# update 'products'
for product in new_act.get('products', []):
if product.get('input') == act.key:
product.input = new_act.key
new_act.save()
# save the new location to the activity
activity_controller.modify_activity(new_act.key, 'location', new_location)

# get exchanges that we want to delete
del_exch = [] # delete these exchanges
for exch in new_act.technosphere():
candidate = self.find_candidate(dbs, exch, old_location, new_location, use_alternatives, alternatives)
if candidate is None:
continue # no suitable candidate was found, try the next exchange
del_exch.append(exch)
# delete exchanges with old locations
exchange_controller.delete_exchanges(del_exch)

# add the new exchanges with all values carried over from last exchange
exchange_controller.add_exchanges(list(successful_links.keys()), new_act.key, successful_links)

# update the MetaDataStore and open new activity
AB_metadata.update_metadata(new_act.key)
signals.safe_open_activity_tab.emit(new_act.key)

# send signals to relevant locations
bw.databases.set_modified(self.db_name)
signals.database_changed.emit(self.db_name)
signals.databases_changed.emit()

def find_candidate(self, dbs, exch, old_location, new_location, use_alternatives, alternatives) -> Optional[object]:
"""Find a candidate to replace the exchange with."""
current_db = exch.input[0]
if current_db == self.db_name:
db = dbs[current_db]
else: # if the exchange is not from the current database, also check the current
# (user may have added their own alternative dependents already)
db = pd.concat([dbs[current_db], dbs[self.db_name]])

if db.loc[db['key'] == exch.input]['location'].iloc[0] != old_location:
return # this exchange has a location we're not trying to re-link

# get relevant data to match on
row = db.loc[db['key'] == exch.input]
name = row['name'].iloc[0]
prod = row['reference product'].iloc[0]
unit = row['unit'].iloc[0]

# get candidates to match (must have same name, product and unit)
candidates = db.loc[(db['name'] == name)
& (db['reference product'] == prod)
& (db['unit'] == unit)]
if len(candidates) <= 1:
return # this activity does not exist in this database with another location (1 is self)

# check candidates for new_location
candidate = candidates.loc[candidates['location'] == new_location]
if len(candidate) == 0 and not use_alternatives:
return # there is no candidate
elif len(candidate) > 1:
return # there is more than one candidate, we can't know what to use
elif len(candidate) == 0:
# there are no candidates, but we can try alternatives
for alt in alternatives:
candidate = candidates.loc[candidates['location'] == alt]
if len(candidate) == 1:
break # found an alternative in with this alternative location, stop looking
if len(candidate) != 1:
return # there are either no or multiple matches with alternative locations
return candidate
23 changes: 23 additions & 0 deletions activity_browser/actions/activity/activity_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Union, Callable, List

from PySide2 import QtCore

from activity_browser import signals
from activity_browser.actions.base import ABAction
from activity_browser.ui.icons import qicons


class ActivityGraph(ABAction):
"""
ABAction to open one or multiple activities in the graph explorer
"""
icon = qicons.graph_explorer
title = "'Open *** in Graph Explorer'"
activity_keys: List[tuple]

def __init__(self, activity_keys: Union[List[tuple], Callable], parent: QtCore.QObject):
super().__init__(parent, activity_keys=activity_keys)

def onTrigger(self, toggled):
for key in self.activity_keys:
signals.open_activity_graph_tab.emit(key)

0 comments on commit 1998ce1

Please sign in to comment.