Module lca_algebraic
Expand source code
#
# This file defines several utility functions above brightway2 to be used by notebooks
#
import re
import sys
from collections import defaultdict
from typing import Dict, Union, List, Any
import types
import brightway2 as bw
import numpy as np
import pandas as pd
from IPython.display import display
from bw2data.backends.peewee import Activity, ActivityDataset
from bw2data.parameters import ActivityParameter, ProjectParameter, DatabaseParameter, Group, ExchangeDataset
from sympy import Symbol, Basic, simplify, symbols
from sympy.parsing.sympy_parser import parse_expr
from slugify import slugify
from sympy.utilities.lambdify import lambdify
from collections import OrderedDict
from bw2data.backends.peewee.utils import dict_as_exchangedataset
from copy import deepcopy
from itertools import chain
import builtins
import matplotlib.pyplot as plt
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
import warnings
from SALib.sample import saltelli
import math
from SALib.analyze import sobol
from scipy.stats import binned_statistic
import seaborn as sns
from sys import stderr
import math
from enum import Enum
from scipy.stats import triang
from scipy.stats import truncnorm
from math import sqrt
# -- Constants
DEBUG=False
def debug(*args, **kwargs) :
if DEBUG :
print(*args, **kwargs)
def error(*args, **kwargs):
print(*args, **kwargs, file=stderr)
# DB names
ECOINVENT_DB_NAME = 'ecoinvent 3.4 cut off'
BIOSPHERE3_DB_NAME = 'biosphere3'
USER_DB_NAME = None
DEFAULT_PARAM_GROUP = "acv"
# Global
def _param_registry() :
# Prevent reset upon auto reload in jupyter notebook
if not 'param_registry' in builtins.__dict__:
builtins.param_registry = dict()
return builtins.param_registry
# Sympy symbols
old_amount = symbols("old_amount") # Can be used in epxression of amount for updateExchanges, in order to reference the previous value
NumOrExpression = Union[float, Basic]
class ParamType:
'''Type of parameters'''
ENUM = "enum"
BOOL = "bool"
FLOAT = "float"
class DistributionType :
'''Type of distribution'''
LINEAR = "linear"
NORMAL = "normal"
TRIANGLE = "triangle"
FIXED = "fixed"
class ParamDef(Symbol):
'''Generic definition of a parameter, with name, bound, type, distribution
This definition will serve both to generate brightway2 parameters and to evaluate.
This class inherits sympy Symbol, making it possible to use in standard arithmetic python
while keeping it as a symbolic expression (delayed evaluation).
'''
def __new__(cls, name, *karg, **kargs):
return Symbol.__new__(cls, name)
def __init__(self, name, type: str, default, min=None, max=None, unit="", description="", label=None, label_fr=None, group=None, distrib=DistributionType.LINEAR, std=None):
self.name = name
self.type = type
self.default = default
self.description = description
self.min = min
self.max = max
self.unit = unit
self.label = label
self.label_fr = label_fr
self.group=group
self.distrib = distrib
if type == ParamType.FLOAT and self.min is None :
self.distrib = DistributionType.FIXED
if distrib == DistributionType.NORMAL and std is None :
raise Exception("Standard deviation is mandatory for normal distribution")
self.std = std
def label(self):
if self.label is not None :
return self.label
else :
return self.name.replace("_", " ")
def range(self, n) :
'''Used for parametric analysis'''
step = (self.max - self.min) / (n - 1)
return list(i * step + self.min for i in range(0, n))
def rand(self, alpha):
"""Transforms a random number between 0 and 1 to valid value according to the distribution of probability of the parameter"""
if self.distrib == DistributionType.LINEAR :
return self.min + alpha * (self.max - self.min)
elif self.distrib == DistributionType.TRIANGLE :
if not hasattr(self, "_distrib") :
scale = self.max - self.min
c = (self.default - self.min) / scale
self._distrib = triang(c, loc=self.min, scale=scale)
return self._distrib.ppf(alpha)
elif self.distrib == DistributionType.NORMAL :
if not hasattr(self, "_distrib") :
self._distrib = truncnorm(
(self.min - self.default) / self.std,
(self.max - self.min) / self.std,
loc=self.default,
scale=self.std)
return self._distrib.ppf(alpha)
else :
raise Exception("Unknowk distribution type " + self.distrib)
# Expand parameter (useful for enum param)
def expandParams(self, value=None) -> Dict[str, float]:
if value == None:
value = self.default
return {self.name: value}
# Useful for enum param, having several names
def names(self) :
return [self.name]
def __repr__(self):
return self.name
class BooleanDef(ParamDef):
"""Parameter with discrete value 0 or 1"""
def __init__(self, name, **argv):
super(BooleanDef, self).__init__(name, ParamType.BOOL, min=0, max=1, **argv)
def range(self, n):
return [0, 1]
def rand(self, alpha):
return round(alpha)
class EnumParam(ParamDef):
"""Enum param is a facility representing a choice / switch as many boolean parameters.
It is not itself a Sympy symbol. use #symbol("value") to access it"""
def __init__(self, name, values: List[str], **argv):
super(EnumParam, self).__init__(name, ParamType.ENUM, min=None, max=None, **argv)
self.values = values
def expandParams(self, currValue=None):
values = self.values + [None]
res = dict()
for enum_val in values:
var_name = "%s_%s" % (self.name, enum_val if enum_val is not None else "default")
res[var_name] = 1.0 if enum_val == currValue else 0.0
return res
def symbol(self, enumValue):
"""Access parameter for each enum value : <paramName>_<paramValue>"""
if enumValue is None:
return Symbol(self.name + '_default')
if not enumValue in self.values:
raise Exception("enumValue should be one of %s. Was %s" % (str(self.values), enumValue))
return Symbol(self.name + '_' + enumValue)
def names(self) :
return ["%s_%s" % (self.name, value) for value in (self.values + ["default"]) ]
def rand(self, alpha):
i = math.ceil(alpha * (len(self.values))) -1
return self.values[int(i)]
def range(self, n):
return self.values
class ActivityExtended(Activity):
"""Improved API for activity : adding a few useful methods.
Those methods are backported to #Activity in order to be directly available on all existing instances
"""
def getExchange(self, name=None, input=None, single=True):
"""Get exchange by name or input
Parameters
----------
name : name of the exchange. Name can be suffixed with '#LOCATION' to distinguish several exchanges with same name. \
It can also be suffised by '*' to match an exchange starting with this name. Location can be a negative match '!'
Exampple : "Wood*#!RoW" matches any exchange with name containing Wood, and location not "RoW"
single :True if a single match is expected. Otherwize, a list of result is returned
Returns
-------
Single exchange or list of exchanges (if _single is False or "name" contains a '*')
raise Exception if not matching exchange found
"""
def single_match(name, exch) :
# Name can be "Elecricity#RER"
if "#" in name:
name, loc = name.split("#")
negative = False
if loc.startswith("!") :
negative = True
loc = loc[1:]
act = getActByCode(*exch['input'])
if not 'location' in act or (negative and act['location'] == loc) or (not negative and act['location'] != loc) :
return False
if '*' in name :
name = name.replace('*', '')
return name in exch['name']
else :
return name == exch['name']
def match(exch):
if name :
if isinstance(name, list):
return any(single_match(iname, exch) for iname in name)
else:
return single_match(name, exch)
if input:
return input == exch['input']
exchs = list(exch for exch in self.exchangesNp() if match(exch))
if len(exchs) == 0:
raise Exception("Found no exchange matching name : %s" % name)
if single and len(exchs) != 1:
raise Exception("Expected 1 exchange with name '%s' found %d" % (name, len(exchs)))
if single:
return exchs[0]
else:
return exchs
def setOutputAmount(self, amount):
'''Set the amount for the single output exchange (1 by default)'''
self.addExchanges({self : amount})
def updateExchanges(self, updates: Dict[str, any] = dict()):
"""Update existing exchanges, by name.
Parameters
----------
updates : Dict of "<exchange name>" => <new value>
<exchange name> can be suffixed with '#LOCATION' to distinguish several exchanges with same name. \
It can also be suffixed by '*' to match an exchange starting with this name. Location can be a negative match '!'
Exampple : "Wood*#!RoW" matches any exchange with name containing Wood, and location not "RoW"
<New Value> : either single value (float or SympPy expression) for updating only amount, or activity for updating only input,
or dict of attributes, for updating both at once, or any other attribute.
The amount can reference the symbol 'old_amount' that will be replaced with the current amount of the exchange.
"""
# Update exchanges
for name, attrs in updates.items():
exchs = self.getExchange(name, single=not '*' in name)
if not isinstance(exchs, list):
exchs = [exchs]
for exch in exchs:
if attrs is None:
exch.delete()
exch.save()
continue
# Single value ? => amount
if not isinstance(attrs, dict):
if isinstance(attrs, Activity):
attrs = dict(input=attrs)
else :
attrs = dict(amount=attrs)
if 'amount' in attrs:
attrs.update(_amountToFormula(attrs['amount'], exch['amount']))
exch.update(attrs)
exch.save()
# We have a formula now ? => register it to parametrized exchange
if 'formula' in attrs:
bw.parameters.add_exchanges_to_group(DEFAULT_PARAM_GROUP, self)
def deleteExchanges(self, name, single=True):
''' Remove matching exchanges '''
exchs = self.getExchange(name, single=single)
if not isinstance(exchs, list):
exchs = [exchs]
if len(exchs) == 0 :
raise Exception("No exchange found for '%s'" % name)
for ex in exchs :
ex.delete()
ex.save()
self.save()
def substituteWithDefault(self, exchange_name: str, switch_act: Activity, paramSwitch: EnumParam, amount=None):
"""Substitutes one exchange with a switch on other activities, or fallback to the current one as default (parameter set to None)
For this purpose, we create a new exchange referencing the activity switch, and we multiply current activity by '<param_name>_default',
making it null as soon as one enum value is set.
This is useful for changing electricty mix, leaving the default one if needed
Parameters
----------
act : Activity to update
exchange_name : Name of the exchange to update
switch_act : Activity to substitue as input
amount : Amount of the input (uses previous amount by default)
"""
current_exch = self.getExchange(exchange_name)
prev_amount = amount if amount else _getAmountOrFormula(current_exch)
self.addExchanges({switch_act: prev_amount})
self.updateExchanges({exchange_name: paramSwitch.symbol(None) * prev_amount})
def addExchanges(self, exchanges: Dict[Activity, Union[NumOrExpression, dict]] = dict()):
"""Add exchanges to an existing activity, with a compact syntax :
Parameters
----------
exchanges : Dict of activity => amount or activity => attributes_dict. \
Amount being either a fixed value or Sympy expression (arithmetic expression of Sympy symbols)
"""
parametrized = False
for sub_act, attrs in exchanges.items():
if isinstance(attrs, dict):
amount = attrs.pop('amount')
else:
amount = attrs
attrs = dict()
exch = self.new_exchange(
input=sub_act.key,
name=sub_act['name'],
unit=sub_act['unit'] if 'unit' in sub_act else None,
type='production' if self == sub_act else 'biosphere' if sub_act['database'] == BIOSPHERE3_DB_NAME else 'technosphere')
exch.update(attrs)
exch.update(_amountToFormula(amount))
if 'formula' in exch:
parametrized = True
exch.save()
self.save()
if parametrized:
bw.parameters.add_exchanges_to_group(DEFAULT_PARAM_GROUP, self)
def getAmount(self, *args, sum=False, **kargs):
"""
Get the amount of one or several exchanges, selected by name or input. See #getExchange()
"""
exchs = self.getExchange(*args, single=not sum, **kargs)
if sum:
res = 0
if len(exchs) == 0:
raise Exception("No exchange found")
for exch in exchs:
res += _getAmountOrFormula(exch)
return res
else:
return _getAmountOrFormula(exchs)
def exchangesNp(self):
""" List of exchange, except production (output) one."""
for exch in self.exchanges():
if exch['input'] != exch['output']:
yield exch
# Backport new methods to vanilla Activity class in order to benefit from it for all existing instances
for name, item in ActivityExtended.__dict__.items():
if isinstance(item, types.FunctionType):
setattr(Activity, name, item)
def _eprint(*args, **kwargs):
print(*args, file=sys.stderr, **kwargs)
def _isnumber(value):
return isinstance(value, int) or isinstance(value, float)
def printAct(*activities, **params):
"""
Print activities and their exchanges.
If parameter values are provided, formulas will be evaluated accordingly
"""
tables = []
names = []
for act in activities:
df = pd.DataFrame(index=['input', 'amount', 'unit', 'type'])
data = dict()
for (i, exc) in enumerate(act.exchanges()):
input = bw.get_activity(exc.input.key)
amount = _getAmountOrFormula(exc)
# Params provided ? Evaluate formulas
if len(params) > 0 and isinstance(amount, Basic):
new_params = [(name, value) for name, value in _completeParamValues(params).items()]
amount = amount.subs(new_params)
name = exc['name']
if 'location' in input and input['location'] != "GLO":
name += "#%s" % input['location']
if exc.input.key[0] not in [BIOSPHERE3_DB_NAME, ECOINVENT_DB_NAME] :
name += " {user-db}"
iname = name
i=1
while iname in data :
iname = "%s#%d" % (name, i)
i += 1
data[iname] = [str(input), amount, exc.unit, exc['type']]
for key, values in data.items() :
df[key] = values
tables.append(df.T)
names.append(_actDesc(act))
full = pd.concat(tables, axis=1, keys=names, sort=True)
if len(activities) == 2 :
yellow = "background-color:yellow"
iamount1 = full.columns.get_loc((names[0], "amount"))
iamount2 = full.columns.get_loc((names[1], "amount"))
iact1 = full.columns.get_loc((names[0], "input"))
iact2 = full.columns.get_loc((names[1], "input"))
def same_amount(row) :
res = [""] * len(row)
if row[iamount1] != row[iamount2] :
res[iamount1] = yellow
res[iamount2] = yellow
if row[iact1] != row[iact2]:
res[iact1] = yellow
res[iact2] = yellow
return res
full = full.style.apply(same_amount, axis=1)
display(full)
def resetDb(db_name):
""" Create or cleanup a user DB"""
if db_name in bw.databases:
_eprint("Db %s was here. Reseting it" % db_name)
del bw.databases[db_name]
db = bw.Database(db_name)
db.write(dict())
def importDb(dbname, path):
'''Import eco invent DB'''
if dbname in bw.databases:
_eprint("Database '%s' has already been imported " % dbname)
else:
ei34 = bw.SingleOutputEcospold2Importer(path, dbname)
ei34.apply_strategies()
ei34.statistics()
ei34.write_database()
dbs = dict()
def _getDb(dbname) -> bw.Database:
"""Pool of Database instances"""
if not dbname in dbs:
dbs[dbname] = bw.Database(dbname)
return dbs[dbname]
def resetParams(db_name):
"""Reset project and activity parameters"""
_param_registry().clear()
ProjectParameter.delete().execute()
ActivityParameter.delete().execute()
DatabaseParameter.delete().execute()
Group.delete().execute()
# Index of activities per name, for fast search dict[db_name][activity_word] => list of activitites
db_index = dict()
def _split_words(name):
clean = re.sub('[^0-9a-zA-Z]+', ' ', name)
clean = re.sub(' +', ' ', clean)
clean = clean.lower()
return clean.split(' ')
def _build_index(db):
res = defaultdict(set)
for act in db:
words = _split_words(act['name'])
for word in words:
res[word].add(act)
return res
def _get_indexed_db(db_name):
if not db_name in db_index:
db_index[db_name] = _build_index(_getDb(db_name))
return db_index[db_name]
def _find_candidates(db_name, name):
res = []
index = _get_indexed_db(db_name)
words = _split_words(name)
for word in words:
candidates = index[word]
if len(res) == 0 or (0 < len(candidates) < len(res)):
res = list(candidates)
return res
def getActByCode(db_name, code):
""" Get activity by code """
return _getDb(db_name).get(code)
def findActivity(name=None, loc=None, in_name=None, code=None, categories=None, category=None, db_name=None,
single=True, unit=None):
"""
Find single activity by name & location
Uses index for fast fetching
"""
if name and '*' in name :
in_name = name.replace("*", "")
name = None
def act_filter(act):
if name and not name == act['name']:
return False
if in_name and not in_name in act['name']:
return False
if loc and not loc == act['location']:
return False
if unit and not unit == act['unit'] :
return False
if category and not category in act['categories']:
return False
if categories and not tuple(categories) == act['categories']:
return False
return True
if code:
acts = [getActByCode(db_name, code)]
else:
name_key = name if name else in_name
# Find candidates via index
candidates = _find_candidates(db_name, name_key)
# Exact match
acts = list(filter(act_filter, candidates))
if single and len(acts) == 0:
raise Exception("No activity found in '%s' with name '%s' and location '%s'" % (db_name, name, loc))
if single and len(acts) > 1:
raise Exception("Several activity found in '%s' with name '%s' and location '%s':\n%s" % (
db_name, name, loc, str(acts)))
if len(acts) == 1:
return acts[0]
else:
return acts
def findBioAct(name=None, loc=None, **kwargs):
"""Alias for findActivity(name, ... db_name=BIOSPHERE3_DB_NAME)
"""
return findActivity(name=name, loc=loc, db_name=BIOSPHERE3_DB_NAME, **kwargs)
def findTechAct(name=None, loc=None, **kwargs):
"""Alias for findActivity(name, ... db_name=ECOINVENT_DB_NAME)
"""
return findActivity(name=name, loc=loc, db_name=ECOINVENT_DB_NAME, **kwargs)
def interpolate(x, x1, x2, y1, y2):
"""Build an expression for linear interpolation between two points"""
return y1 + (y2 - y1) * (x - x1) / (x2 - x1)
def newInterpolatedAct(dbname: str, name: str, act1: ActivityExtended, act2: ActivityExtended, x1, x2, x, alpha1=1, alpha2=1, **kwargs):
"""Creates a new activity made of interpolation of two similar activities.
For each exchange :
amount = alpha1 * a1 + (x - X1) * (alpha2 * a2 - alpha1 * a1) / (x2 - x1)
Parameters
----------
name : Name of new activity
act1 : Activity 1
act2 : Activity 2
x1 : X for act1
x2 : X for act 2
x : Should be a parameter symbol
alpha1 : Ratio for act1 (Default value = 1)
alpha2 : Ratio for act2 (Default value = 1)
kwargs : Any other param will be added as attributes of new activity
"""
res = copyActivity(dbname, act1, name, withExchanges=False, **kwargs)
exch1_by_input = dict({exch['input']: exch for exch in act1.exchangesNp()})
exch2_by_input = dict({exch['input']: exch for exch in act2.exchangesNp()})
inputs = set(chain(exch1_by_input.keys(), exch2_by_input.keys()))
for input in inputs:
exch1 = exch1_by_input.get(input)
exch2 = exch2_by_input.get(input)
exch = exch1 if exch1 else exch2
amount1 = exch1['amount'] if exch1 else 0
amount2 = exch2['amount'] if exch2 else 0
if exch1 and exch2 and exch1['name'] != exch2['name']:
raise Exception("Input %s refer two different names : %s, %s" % (input, exch1['name'], exch2['name']))
amount = interpolate(x, x1, x2, amount1 * alpha1, amount2 * alpha2)
act = getActByCode(*input)
res.addExchanges({act: dict(amount=amount, name=exch['name'])})
return res
def newParamDef(name, type, **kwargs):
"""Creates a param and register it into a global registry and as a brightway parameter"""
if type == ParamType.ENUM:
param = EnumParam(name, **kwargs)
elif type == ParamType.BOOL :
param = BooleanDef(name, **kwargs)
else:
param = ParamDef(name, type=type, **kwargs)
# Put it in local registry (in memory)
if name in _param_registry():
_eprint("Param %s was already defined : overriding" % name)
_param_registry()[name] = param
# Save in brightway2 project
bwParams = [dict(name=key, amount=value) for key, value in param.expandParams().items()]
bw.parameters.new_project_parameters(bwParams)
return param
def newFloatParam(name, default, **kwargs):
return newParamDef(name, ParamType.FLOAT, default=default, **kwargs)
def newBoolParam(name, default, **kwargs):
return newParamDef(name, ParamType.BOOL, default=default, **kwargs)
def newEnumParam(name, default, **kwargs):
return newParamDef(name, ParamType.ENUM, default=default, **kwargs)
def _amountToFormula(amount: Union[float, str, Basic], currentAmount=None):
"""Transform amount in exchange to either simple amount or formula"""
res = dict()
if isinstance(amount, Basic):
if currentAmount != None:
amount = amount.subs(old_amount, currentAmount)
# Check the expression does not reference undefined params
all_symbols = list([key for param in _param_registry().values() for key, val in param.expandParams().items()])
for symbol in amount.free_symbols:
if not str(symbol) in all_symbols:
raise Exception("Symbol '%s' not found in params : %s" % (symbol, all_symbols))
res['formula'] = str(amount)
res['amount'] = 0
elif isinstance(amount, float) or isinstance(amount, int):
res['amount'] = amount
else:
raise Exception(
"Amount should be either a constant number or a Sympy expression (expression of ParamDef). Was : %s" % type(
amount))
return res
def _getAmountOrFormula(ex: ExchangeDataset) -> Union[Basic, float]:
""" Return either a fixed float value or an expression for the amount of this exchange"""
if 'formula' in ex:
try:
return parse_expr(ex['formula'])
except:
_eprint("Error while parsing formula '%s' : backing to amount" % ex['formula'])
return ex['amount']
def _newAct(db_name, code):
db = _getDb(db_name)
# Already present : delete it ?
for act in db:
if act['code'] == code:
_eprint("Activity '%s' was already in '%s'. Overwriting it" % (code, db_name))
act.delete()
return db.new_activity(code)
def newActivity(db_name, name, unit,
exchanges: Dict[Activity, Union[float, str]] = dict(),
code=None,
**argv):
"""Creates a new activity
Parameters
----------
name : Name ofthe new activity
db_name : Destination DB : ACV DB by default
exchanges : Dict of activity => amount. If amount is a string, is it considered as a formula with parameters
argv : extra params passed as properties of the new activity
"""
act = _newAct(db_name, code if code else name)
act['name'] = name
act['type'] = 'process'
act['unit'] = unit
act.update(argv)
# Add exchanges
act.addExchanges(exchanges)
return act
def copyActivity(db_name, activity: ActivityExtended, code=None , withExchanges=True, **kwargs) -> ActivityExtended:
"""Copy activity into a new DB"""
res = _newAct(db_name, code)
for key, value in activity.items():
if key not in ['database', 'code']:
res[key] = value
for k, v in kwargs.items():
res._data[k] = v
res._data[u'code'] = code
res['name'] = code
res.save()
if withExchanges:
for exc in activity.exchanges():
data = deepcopy(exc._data)
data['output'] = res.key
# Change `input` for production exchanges
if exc['input'] == exc['output']:
data['input'] = res.key
ExchangeDataset.create(**dict_as_exchangedataset(data))
return res
def newSwitchAct(dbname, name, paramDef: ParamDef, acts_dict: Dict[str, Activity]):
"""Create a new parametrized, virtual activity, made of a map of other activities, controlled by an enum parameter.
This enables to implement a "Switch" with brightway parameters
Internally, this will create a linear sum of other activities controlled by <param_name>_<enum_value> : 0 or 1
Parameters
----------
paramDef : parameter definition of type enum
acts_dict : dict of <enumValue> => activity
"""
# Transform map of enum values to correspoding formulas <param_name>_<enum_value>
exch = {act: paramDef.symbol(key) for key, act in acts_dict.items()}
res = newActivity(
dbname,
name,
unit=list(acts_dict.values())[0]['unit'],
exchanges=exch)
# Unit of switch activity is the one of the children
for key, act in acts_dict.items():
if 'unit' in act:
res['unit'] = act['unit']
res.save()
return res
def _actName(act: Activity):
"""Generate pretty name for activity, appending location if not 'GLO' """
res = act['name']
if act['location'] != 'GLO':
res += "[%s]" % act["location"]
return res
def _actDesc(act: Activity):
"""Generate pretty name for activity + basic information """
name = _actName(act)
amount = 1
for ex in act.exchanges() :
if ex['type'] == 'production' :
amount = ex['amount']
return "%s (%f %s)" % (name, amount, act['unit'])
def _multiLCA(activities, methods):
"""Simple wrapper around brightway API"""
bw.calculation_setups['process'] = {'inv': activities, 'ia': methods}
lca = bw.MultiLCA('process')
cols = [_actName(act) for act_amount in activities for act, amount in act_amount.items()]
return pd.DataFrame(lca.results.T, index=[method_name(method) for method in methods], columns=cols)
def _listOfDictToDictOflist(LD):
return {k: [dic[k] for dic in LD] for k in LD[0]}
def _completeParamValues(params):
"""Check parameters and expand enum params.
Returns
-------
Dict of param_name => float value
"""
# undef_params = param_registry.keys() - params.keys()
# if undef_params :
# raise Exception("Some model parameters are not set : %s" % undef_params)
res = dict()
for key, val in params.items():
if key in _param_registry():
param = _param_registry()[key]
else:
raise Exception("Parameter not found : %s. Valid parameters : %s" % (key, list(_param_registry().keys())))
if isinstance(val, list):
newvals = [param.expandParams(val) for val in val]
res.update(_listOfDictToDictOflist(newvals))
else:
res.update(param.expandParams(val))
return res
def multiLCA(model, methods, **params):
"""Compute LCA for a single activity and a set of methods, after settings the parameters and updating exchange amounts.
Parameters
----------
model : Single activity (root model) or list of activities
methods : Impact methods to consider
params : Other parameters of the model
"""
# Check and expand params
params = _completeParamValues(params)
# Update brightway parameters
bwParams = [dict(name=key, amount=value) for key, value in params.items()]
bw.parameters.new_project_parameters(bwParams)
# ActivityParameter.recalculate_exchanges(DEFAULT_PARAM_GROUP)
bw.parameters.recalculate()
if isinstance(model, list):
activities = [{act: 1} for act in model]
else:
activities = [{model: 1}]
return _multiLCA(activities, methods).transpose()
def preMultiLCAAlgebric(model:ActivityExtended, methods, param_names=None, amount=1) :
'''
This method transforms an activity into a set of functions ready to compute LCA very fast on a set on methods.
You may use is and pass the result to postMultiLCAAlgebric for fast computation on a model that does not change.
This method is used by multiLCAAlgebric
'''
# print("computing model to expression for %s" % model)
expr, actBySymbolName = actToExpression(model)
dbname = model.key[0]
param_names = _expand_param_names(param_names)
# Check missing params
free_names = set([str(symb) for symb in expr.free_symbols])
act_names = set([str(symb) for symb in actBySymbolName.keys()])
expected_names = free_names - act_names
missing_names = expected_names - set(param_names)
if len(missing_names) > 0 :
raise Exception('Missing parameter of the model : %s' % str(missing_names))
debug(param_names)
for name in param_names :
registry_names = _expand_param_names(_param_registry())
if name not in registry_names :
raise Exception('Model refers to unknown param "%s"' % name)
# Create dummy reference to biosphere
# We cannot run LCA to biosphere activities
# We create a technosphere activity mapping exactly to 1 biosphere item
pureTechActBySymbol = OrderedDict()
for name, act in actBySymbolName.items():
if act[0] == BIOSPHERE3_DB_NAME:
act = _getOrCreateDummyBiosphereActCopy(dbname, act[1])
else:
act = getActByCode(*act)
pureTechActBySymbol[name] = act
# List of activities, ordered
acts = pureTechActBySymbol.values()
# Transform to [{act1:1], {act2:1}, etc] for MultiLCA
actsWithAmount = [{act: 1} for act in acts]
# Compute LCA for all background activities and methods
lca = _multiLCA(actsWithAmount, methods)
# For each method, compute an algebric expression with activities replaced by their values
lambdas = []
for imethod, method in enumerate(methods):
# print("Generating lamba function for %s / %s" % (model, method))
# Replace activities by their value in expression for this method
sub = dict({symbol: lca.iloc[imethod, iact] for iact, symbol in enumerate(pureTechActBySymbol.keys())})
method_expr = expr.xreplace(sub)
# Tranform Sympy expression to lambda function, based on numpy to fast vectorial evaluation
lambd = lambdify(param_names, method_expr, 'numpy')
lambdas.append(lambd)
return lambdas
def method_name(method) :
return method[1] + " - " + method[2]
def postMultiLCAAlgebric(methods, lambdas, alpha=1, **params):
'''
Compute LCA for a given set of parameters and pre-compiled lambda functions.
This function is used by **multiLCAAlgebric**
Parameters
----------
methodAndLambdas : Output of preMultiLCAAlgebric
**params : Parameters of the model
'''
# Check and expand params
params = _completeParamValues(params)
# Expand parameters as list of parameters
param_length = 1
for key, val in params.items():
if isinstance(val, list):
if param_length == 1:
param_length = len(val)
elif param_length != len(val):
raise Exception("Parameters should be a single value or a list of same number of values")
# Expand params and transform lists to np.array for vector computation
for key in params.keys():
val = params[key]
if not isinstance(val, list):
val = list([val] * param_length)
params[key] = np.array(val)
res = np.zeros((len(methods), param_length))
# Compute result on whole vectors of parameter samples at a time : lambdas use numpy for vector computation
for imethod, lambd in enumerate(lambdas):
res[imethod, :] = alpha * lambd(**params)
return pd.DataFrame(res, index=[method_name(method) for method in methods]).transpose()
def _expand_param_names(param_names) :
'''Expand parameters names (with enum params) '''
return [name for key in param_names for name in _param_registry()[key].names()]
def multiLCAAlgebric(models, methods, **params):
"""Compute LCA by expressing the foreground model as symbolic expression of background activities and parameters.
Then, compute 'static' inventory of the referenced background activities.
This enables a very fast recomputation of LCA with different parameters, useful for stochastic evaluation of parametrized model
Parameters
----------
models : Single model or list of models or dict of model:amount : if list of models, you cannot use param lists
methods : List of methods / impacts to consider
params : You should provide named values of all the parameters declared in the model. \
Values can be single value or list of samples, all of the same size
"""
dfs = dict()
if not isinstance(models, list):
models = [models]
for model in models:
alpha = 1
if type(model) is tuple :
model, alpha=model
lambdas = preMultiLCAAlgebric(model, methods, params.keys())
df = postMultiLCAAlgebric(methods, lambdas, alpha=alpha, **params)
model_name = _actName(model)
# Single params ? => give the single row the name of the model activity
if df.shape[0] == 1:
df = df.rename(index={0: model_name})
dfs[model_name] = df
if len(dfs) == 1:
df = list(dfs.values())[0]
return df
else:
# Concat several dataframes for several models
return pd.concat(list(dfs.values()))
def _getOrCreateDummyBiosphereActCopy(dbname, code):
"""
We cannot reference directly biosphere in the model, since LCA can only be applied to products
We create a dummy activity in our DB, with same code, and single exchange of amount '1'
"""
code_to_find = code + "#asTech"
try:
return _getDb(dbname).get(code_to_find)
except:
bioAct = _getDb(BIOSPHERE3_DB_NAME).get(code)
name = bioAct['name'] + ' # asTech'
res = newActivity(dbname, name, bioAct['unit'], {bioAct: 1}, code=code_to_find)
return res
def actToExpression(act: Activity):
"""Computes a symbolic expression of the model, referencing background activities and model parameters as symbols
Returns
-------
(sympy_expr, dict of symbol => activity)
"""
act_symbols = dict() # Dict of act = > symbol
def act_to_symbol(db_name, code):
act = _getDb(db_name).get(code)
name = act['name']
base_slug = slugify(name, separator='_')
slug = base_slug
i = 1
while symbols(slug) in act_symbols.values():
slug = f"{base_slug}{i}"
i += 1
return symbols(slug)
def rec_func(act: Activity):
res = 0
outputAmount = 1
for exch in act.exchanges():
formula = _getAmountOrFormula(exch)
if isinstance(formula, types.FunctionType):
# Some amounts in EIDB are functions ... we ignore them
continue
input_db, input_code = exch['input']
# Different output ?
if exch['input'] == exch['output']:
if exch['amount'] != 1:
outputAmount = exch['amount']
continue
# Background DB => reference it as a symbol
if input_db in [BIOSPHERE3_DB_NAME, ECOINVENT_DB_NAME]:
if not (input_db, input_code) in act_symbols:
act_symbols[(input_db, input_code)] = act_to_symbol(input_db, input_code)
act_expr = act_symbols[(input_db, input_code)]
# Our model : recursively transform it to a symbolic expression
else:
if input_db == act['database'] and input_code == act['code']:
raise Exception("Recursive exchange : %s" % (act.__dict__))
sub_act = _getDb(input_db).get(input_code)
act_expr = rec_func(sub_act)
res += formula * act_expr
return res / outputAmount
expr = rec_func(act)
return (expr, _reverse_dict(act_symbols))
def _reverse_dict(dic):
return {v: k for k, v in dic.items()}
def _heatmap(df, title, vmax, ints=False):
''' Produce heatmap of a dataframe'''
fig, ax = plt.subplots(figsize=(17, 17))
sns.heatmap(df.transpose(), cmap="gist_heat_r", vmax=vmax, annot=True, fmt='.0f' if ints else 'f', square=True)
plt.title(title, fontsize=20)
ax.tick_params(axis="x", labelsize=18)
ax.tick_params(axis="y", labelsize=18)
def oat_matrix(model, impacts, n=10) :
'''Generates a heatmap of the incertitude of the model, varying input parameters one a a time.'''
# Compile model into lambda functions for fast LCA
lambdas = preMultiLCAAlgebric(model, impacts, _param_registry().keys())
change = np.zeros((len(_variable_params()), len(impacts)))
for iparam, param in enumerate(_variable_params().values()) :
params = {param.name: param.default for param in _param_registry().values()}
# Compute range of values for given param
params[param.name] = param.range(n)
# Compute LCA
df = postMultiLCAAlgebric(impacts, lambdas, **params)
# Compute change
change[iparam] = (df.max() - df.min()) / df.median() * 100
# Build final heatmap
change = pd.DataFrame(change, index=_variable_params().keys(), columns=[imp[2] for imp in impacts])
_heatmap(change.transpose(), 'Change of impacts per variability of the input parameters (%)', 100, ints=True)
def _method_unit(method) :
return bw.Method(method).metadata['unit']
def _display_tabs(titlesAndContentF) :
'''Generate tabs'''
tabs = []
titles= []
for title, content_f in titlesAndContentF :
titles.append(title)
tab = widgets.Output()
with tab :
content_f()
tabs.append(tab)
res = widgets.Tab(children=tabs)
for i, title in enumerate(titles) :
res.set_title(i, title)
display(res)
def oat_dasboard(modelOrLambdas, impacts, param: ParamDef, n=10) :
'''
Analyse the evolution of impacts for a single parameter. The other parameters are set to their default values.
Parameters
----------
model : activity, or lambdas as precomputed by preMultiLCAAlgebric, for faster computation
impacts : set of methods
param: parameter to analyse
n: number of samples of the parameter
'''
params = {param.name : param.default for param in _param_registry().values()}
# Compute range of values for given param
params[param.name] = param.range(n)
#print("Params: ", params)
if isinstance(modelOrLambdas, Activity) :
df = multiLCAAlgebric(modelOrLambdas, impacts, **params)
else :
df = postMultiLCAAlgebric(impacts, modelOrLambdas, **params)
# Add X values in the table
pname = param.name
if param.unit :
pname = '%s [%s]' % (pname, param.unit)
df.insert(0, pname, param.range(n))
df = df.set_index(pname)
def table() :
display(df)
def graph() :
with warnings.catch_warnings():
warnings.simplefilter("ignore")
nb_rows = len(impacts) // 3 + 1
fig, axes = plt.subplots(figsize=(15, 15))
axes = df.plot(
ax=axes, sharex=True, subplots=True,
layout=(nb_rows, 3),
#legend=None,
kind = 'line' if param.type == ParamType.FLOAT else 'bar')
axes = axes.flatten()
for ax, impact in zip(axes, impacts) :
ax.set_ylim(ymin=0)
ax.set_ylabel(_method_unit(impact))
plt.show(fig)
def change() :
ch = (df.max() - df.min()) / df.median() * 100
fig, ax = plt.subplots(figsize=(9, 6))
plt.title('Relative change for %s' % df.index.name)
ch.plot(kind='barh', rot=30)
ax.set_xlabel('Relative change of the median value (%)')
plt.tight_layout()
plt.show(fig)
_display_tabs([
("Graphs", graph),
("Data", table),
("Variation", change)
])
def oat_dashboard_interact(model, methods):
'''Interative dashboard, with a dropdown for selecting parameter'''
lambdas = preMultiLCAAlgebric(model, methods, _param_registry().keys())
def process_func(param) :
oat_dasboard(lambdas, methods, _param_registry()[param])
paramlist = list(_variable_params().keys())
interact(process_func, param=paramlist)
def _stochastics(modelOrLambdas, methods, n=1000) :
''' Compute stochastic impacts for later analysis of incertitude '''
# Extract variable names
param_names = list(_variable_params().keys())
problem = {
'num_vars': len(param_names),
'names': param_names,
'bounds': [[0, 1]] * len(param_names)
}
print("Generating samples ...")
X = saltelli.sample(problem, n, calc_second_order=True)
# Map normalized 0-1 random values into real values
print("Transforming samples ...")
params = dict()
for i, param_name in enumerate(param_names) :
param = _param_registry()[param_name]
vals = list(map(lambda v : param.rand(v), X[:, i]))
params[param_name] = vals
# Add static parameters
for param in _fixed_params().values() :
params[param.name] = param.default
print("Processing LCA ...")
if isinstance(modelOrLambdas, Activity):
Y = multiLCAAlgebric(modelOrLambdas, methods, **params)
else:
Y = postMultiLCAAlgebric(methods, modelOrLambdas, **params)
return problem, X, Y
def _variable_params():
return {key : param for key, param in _param_registry().items() if param.distrib != DistributionType.FIXED}
def _fixed_params():
return {key : param for key, param in _param_registry().items() if param.distrib == DistributionType.FIXED}
def _sobols(methods, problem, Y) :
''' Computes sobols indices'''
s1 = np.zeros((len(problem['names']), len(methods)))
st = np.zeros((len(problem['names']), len(methods)))
for i, method in enumerate(methods) :
try:
y = Y[Y.columns[i]]
res = sobol.analyze(problem, y.to_numpy(), calc_second_order=True)
st[:, i] = res["ST"]
s1[:, i] = res["S1"]
except Exception as e:
error("Sobol failed on %s" % method[2], e)
return (s1, st)
def _incer_stochastic_matrix(methods, param_names, Y, st):
''' Internal method computing matrix of parameter importance '''
def draw(mode) :
if mode == 'sobol' :
data = st
else :
# If percent, express result as percentage of standard deviation / mean
data = np.zeros((len(param_names), len(methods)))
for i, method in enumerate(methods):
# Total variance
var = np.var(Y[Y.columns[i]])
mean = np.mean(Y[Y.columns[i]])
if mean != 0 :
data[:, i] = np.sqrt((st[:, i] * var)) / mean * 100
df = pd.DataFrame(data, index=param_names, columns=[method_name(method) for method in methods])
_heatmap(
df.transpose(),
title="Relative deviation of impacts (%)" if mode == 'percent' else "Sobol indices (part of variability)",
vmax=100 if mode == 'percent' else 1,
ints= mode == 'percent')
interact(draw, mode=[('Raw sobol indices (ST)', 'sobol'), ('Deviation (ST) / mean', 'percent')])
def incer_stochastic_matrix(modelOrLambdas, methods, n=1000):
''' Method computing matrix of parameter importance '''
problem, X, Y = _stochastics(modelOrLambdas, methods, n)
print("Processing Sobol indices ...")
s1, st = _sobols(methods, problem, Y)
_incer_stochastic_matrix(methods, problem['names'], Y, st)
def _incer_stochastic_violin(methods, Y) :
''' Internal method for computing violin graph of impacts '''
nb_rows = math.ceil(len(methods) / 3)
fig, axes = plt.subplots(nb_rows, 3, figsize=(15, 15), sharex=True)
for imethod, method, ax in zip(range(len(methods)), methods, axes.flatten()) :
ax.violinplot(Y[Y.columns[imethod]], showmedians=True)
ax.title.set_text(method_name(method))
ax.set_ylim(ymin=0)
ax.set_ylabel(_method_unit(method))
plt.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False)
plt.show(fig)
def incer_stochastic_violin(modelOrLambdas, methods, n=1000):
''' Method for computing violin graph of impacts '''
problem, X, Y = _stochastics(modelOrLambdas, methods, n)
_incer_stochastic_violin(methods, Y)
def _incer_stochastic_variations(methods, Y, param_names, sobols1):
''' Method for computing violin graph of impacts '''
method_names=[method_name(method) for method in methods]
std = np.std(Y)
mean = np.mean(Y)
fig = plt.figure(num=None, figsize=(12, 6), dpi=80, facecolor='w', edgecolor='k')
ax = plt.gca()
tab20b = plt.get_cmap('tab20b')
tab20c = plt.get_cmap('tab20c')
ax.set_prop_cycle('color', [tab20b(k) if k < 1 else tab20c(k-1) for k in np.linspace(0, 2, 40)])
relative_variance_pct = std*std / (mean*mean) * 100
totplt = plt.bar(np.arange(len(method_names)), relative_variance_pct, 0.8)
sum = np.zeros(len(methods))
plots = [totplt[0]]
data = np.zeros((len(param_names) + 2, len(methods)))
data[0, :] = mean
data[1, :] = std
for i_param, param_name in enumerate(param_names) :
s1 = sobols1[i_param, :]
data[i_param+2, :] = s1
curr_bar = s1 * relative_variance_pct
curr_plt = plt.bar(np.arange(len(method_names)), curr_bar, 0.8, bottom=sum)
sum += curr_bar
plots.append(curr_plt[0])
plt.legend(plots, ['Higher order'] + param_names)
plt.xticks(np.arange(len(method_names)), method_names, rotation=90)
plt.title("variance / mean² (%)")
plt.show(fig)
# Show raw data
rows = ["mean", "std"] + ["s1(%s)" % param for param in param_names]
df = pd.DataFrame(data, index=rows, columns=[method_name(method) for method in methods])
display(df)
def incer_stochastic_dasboard(model, methods, n=1000) :
''' Generates a dashboard with several statistics : matrix of parameter incertitude, violin diagrams, ...'''
problem, X, Y = _stochastics(model, methods, n)
param_names = problem['names']
print("Processing Sobol indices ...")
s1, st = _sobols(methods, problem, Y)
def violin() :
_incer_stochastic_violin(methods, Y)
def variation():
_incer_stochastic_variations(methods, Y, param_names, s1)
def matrix() :
_incer_stochastic_matrix(methods, problem['names'], Y, st)
_display_tabs([
("Violin graphs", violin),
("Impact variations", variation),
("Sobol matrix", matrix)
])
Functions
def actToExpression(act)
-
Computes a symbolic expression of the model, referencing background activities and model parameters as symbols
Returns
(sympy_expr, dict of symbol => activity)
Expand source code
def actToExpression(act: Activity): """Computes a symbolic expression of the model, referencing background activities and model parameters as symbols Returns ------- (sympy_expr, dict of symbol => activity) """ act_symbols = dict() # Dict of act = > symbol def act_to_symbol(db_name, code): act = _getDb(db_name).get(code) name = act['name'] base_slug = slugify(name, separator='_') slug = base_slug i = 1 while symbols(slug) in act_symbols.values(): slug = f"{base_slug}{i}" i += 1 return symbols(slug) def rec_func(act: Activity): res = 0 outputAmount = 1 for exch in act.exchanges(): formula = _getAmountOrFormula(exch) if isinstance(formula, types.FunctionType): # Some amounts in EIDB are functions ... we ignore them continue input_db, input_code = exch['input'] # Different output ? if exch['input'] == exch['output']: if exch['amount'] != 1: outputAmount = exch['amount'] continue # Background DB => reference it as a symbol if input_db in [BIOSPHERE3_DB_NAME, ECOINVENT_DB_NAME]: if not (input_db, input_code) in act_symbols: act_symbols[(input_db, input_code)] = act_to_symbol(input_db, input_code) act_expr = act_symbols[(input_db, input_code)] # Our model : recursively transform it to a symbolic expression else: if input_db == act['database'] and input_code == act['code']: raise Exception("Recursive exchange : %s" % (act.__dict__)) sub_act = _getDb(input_db).get(input_code) act_expr = rec_func(sub_act) res += formula * act_expr return res / outputAmount expr = rec_func(act) return (expr, _reverse_dict(act_symbols))
def copyActivity(db_name, activity, code=None, withExchanges=True, **kwargs)
-
Copy activity into a new DB
Expand source code
def copyActivity(db_name, activity: ActivityExtended, code=None , withExchanges=True, **kwargs) -> ActivityExtended: """Copy activity into a new DB""" res = _newAct(db_name, code) for key, value in activity.items(): if key not in ['database', 'code']: res[key] = value for k, v in kwargs.items(): res._data[k] = v res._data[u'code'] = code res['name'] = code res.save() if withExchanges: for exc in activity.exchanges(): data = deepcopy(exc._data) data['output'] = res.key # Change `input` for production exchanges if exc['input'] == exc['output']: data['input'] = res.key ExchangeDataset.create(**dict_as_exchangedataset(data)) return res
def debug(*args, **kwargs)
-
Expand source code
def debug(*args, **kwargs) : if DEBUG : print(*args, **kwargs)
def error(*args, **kwargs)
-
Expand source code
def error(*args, **kwargs): print(*args, **kwargs, file=stderr)
def findActivity(name=None, loc=None, in_name=None, code=None, categories=None, category=None, db_name=None, single=True, unit=None)
-
Find single activity by name & location Uses index for fast fetching
Expand source code
def findActivity(name=None, loc=None, in_name=None, code=None, categories=None, category=None, db_name=None, single=True, unit=None): """ Find single activity by name & location Uses index for fast fetching """ if name and '*' in name : in_name = name.replace("*", "") name = None def act_filter(act): if name and not name == act['name']: return False if in_name and not in_name in act['name']: return False if loc and not loc == act['location']: return False if unit and not unit == act['unit'] : return False if category and not category in act['categories']: return False if categories and not tuple(categories) == act['categories']: return False return True if code: acts = [getActByCode(db_name, code)] else: name_key = name if name else in_name # Find candidates via index candidates = _find_candidates(db_name, name_key) # Exact match acts = list(filter(act_filter, candidates)) if single and len(acts) == 0: raise Exception("No activity found in '%s' with name '%s' and location '%s'" % (db_name, name, loc)) if single and len(acts) > 1: raise Exception("Several activity found in '%s' with name '%s' and location '%s':\n%s" % ( db_name, name, loc, str(acts))) if len(acts) == 1: return acts[0] else: return acts
def findBioAct(name=None, loc=None, **kwargs)
-
Alias for findActivity(name, … db_name=BIOSPHERE3_DB_NAME)
Expand source code
def findBioAct(name=None, loc=None, **kwargs): """Alias for findActivity(name, ... db_name=BIOSPHERE3_DB_NAME) """ return findActivity(name=name, loc=loc, db_name=BIOSPHERE3_DB_NAME, **kwargs)
def findTechAct(name=None, loc=None, **kwargs)
-
Alias for findActivity(name, … db_name=ECOINVENT_DB_NAME)
Expand source code
def findTechAct(name=None, loc=None, **kwargs): """Alias for findActivity(name, ... db_name=ECOINVENT_DB_NAME) """ return findActivity(name=name, loc=loc, db_name=ECOINVENT_DB_NAME, **kwargs)
def getActByCode(db_name, code)
-
Get activity by code
Expand source code
def getActByCode(db_name, code): """ Get activity by code """ return _getDb(db_name).get(code)
def importDb(dbname, path)
-
Import eco invent DB
Expand source code
def importDb(dbname, path): '''Import eco invent DB''' if dbname in bw.databases: _eprint("Database '%s' has already been imported " % dbname) else: ei34 = bw.SingleOutputEcospold2Importer(path, dbname) ei34.apply_strategies() ei34.statistics() ei34.write_database()
def incer_stochastic_dasboard(model, methods, n=1000)
-
Generates a dashboard with several statistics : matrix of parameter incertitude, violin diagrams, …
Expand source code
def incer_stochastic_dasboard(model, methods, n=1000) : ''' Generates a dashboard with several statistics : matrix of parameter incertitude, violin diagrams, ...''' problem, X, Y = _stochastics(model, methods, n) param_names = problem['names'] print("Processing Sobol indices ...") s1, st = _sobols(methods, problem, Y) def violin() : _incer_stochastic_violin(methods, Y) def variation(): _incer_stochastic_variations(methods, Y, param_names, s1) def matrix() : _incer_stochastic_matrix(methods, problem['names'], Y, st) _display_tabs([ ("Violin graphs", violin), ("Impact variations", variation), ("Sobol matrix", matrix) ])
def incer_stochastic_matrix(modelOrLambdas, methods, n=1000)
-
Method computing matrix of parameter importance
Expand source code
def incer_stochastic_matrix(modelOrLambdas, methods, n=1000): ''' Method computing matrix of parameter importance ''' problem, X, Y = _stochastics(modelOrLambdas, methods, n) print("Processing Sobol indices ...") s1, st = _sobols(methods, problem, Y) _incer_stochastic_matrix(methods, problem['names'], Y, st)
def incer_stochastic_violin(modelOrLambdas, methods, n=1000)
-
Method for computing violin graph of impacts
Expand source code
def incer_stochastic_violin(modelOrLambdas, methods, n=1000): ''' Method for computing violin graph of impacts ''' problem, X, Y = _stochastics(modelOrLambdas, methods, n) _incer_stochastic_violin(methods, Y)
def interpolate(x, x1, x2, y1, y2)
-
Build an expression for linear interpolation between two points
Expand source code
def interpolate(x, x1, x2, y1, y2): """Build an expression for linear interpolation between two points""" return y1 + (y2 - y1) * (x - x1) / (x2 - x1)
def method_name(method)
-
Expand source code
def method_name(method) : return method[1] + " - " + method[2]
def multiLCA(model, methods, **params)
-
Compute LCA for a single activity and a set of methods, after settings the parameters and updating exchange amounts.
Parameters
model
:Single
activity
(root
model
) orlist
ofactivities
methods
:Impact
methods
to
consider
params
:Other
parameters
ofthe
model
Expand source code
def multiLCA(model, methods, **params): """Compute LCA for a single activity and a set of methods, after settings the parameters and updating exchange amounts. Parameters ---------- model : Single activity (root model) or list of activities methods : Impact methods to consider params : Other parameters of the model """ # Check and expand params params = _completeParamValues(params) # Update brightway parameters bwParams = [dict(name=key, amount=value) for key, value in params.items()] bw.parameters.new_project_parameters(bwParams) # ActivityParameter.recalculate_exchanges(DEFAULT_PARAM_GROUP) bw.parameters.recalculate() if isinstance(model, list): activities = [{act: 1} for act in model] else: activities = [{model: 1}] return _multiLCA(activities, methods).transpose()
def multiLCAAlgebric(models, methods, **params)
-
Compute LCA by expressing the foreground model as symbolic expression of background activities and parameters. Then, compute 'static' inventory of the referenced background activities. This enables a very fast recomputation of LCA with different parameters, useful for stochastic evaluation of parametrized model
Parameters
models
:Single
model
orlist
ofmodels
ordict
ofmodel
:amount
:if
list
ofmodels
,you
cannot
use
param
lists
methods
:List
ofmethods
/impacts
to
consider
params
:You
should
provide
named
values
ofall
the
parameters
declared
in
the
model.
Values
can
be
single
value
orlist
ofsamples
,all
ofthe
same
size
Expand source code
def multiLCAAlgebric(models, methods, **params): """Compute LCA by expressing the foreground model as symbolic expression of background activities and parameters. Then, compute 'static' inventory of the referenced background activities. This enables a very fast recomputation of LCA with different parameters, useful for stochastic evaluation of parametrized model Parameters ---------- models : Single model or list of models or dict of model:amount : if list of models, you cannot use param lists methods : List of methods / impacts to consider params : You should provide named values of all the parameters declared in the model. \ Values can be single value or list of samples, all of the same size """ dfs = dict() if not isinstance(models, list): models = [models] for model in models: alpha = 1 if type(model) is tuple : model, alpha=model lambdas = preMultiLCAAlgebric(model, methods, params.keys()) df = postMultiLCAAlgebric(methods, lambdas, alpha=alpha, **params) model_name = _actName(model) # Single params ? => give the single row the name of the model activity if df.shape[0] == 1: df = df.rename(index={0: model_name}) dfs[model_name] = df if len(dfs) == 1: df = list(dfs.values())[0] return df else: # Concat several dataframes for several models return pd.concat(list(dfs.values()))
def newActivity(db_name, name, unit, exchanges={}, code=None, **argv)
-
Creates a new activity
Parameters
name
:Name
ofthe
new
activity
db_name
:Destination
DB
:ACV
DB
by
defaultexchanges
:Dict
ofactivity
=>amount.
If
amount
is
a
string
,is
it
considered
as
a
formula
with
parameters
argv
:extra
params
passed
as
properties
ofthe
new
activity
Expand source code
def newActivity(db_name, name, unit, exchanges: Dict[Activity, Union[float, str]] = dict(), code=None, **argv): """Creates a new activity Parameters ---------- name : Name ofthe new activity db_name : Destination DB : ACV DB by default exchanges : Dict of activity => amount. If amount is a string, is it considered as a formula with parameters argv : extra params passed as properties of the new activity """ act = _newAct(db_name, code if code else name) act['name'] = name act['type'] = 'process' act['unit'] = unit act.update(argv) # Add exchanges act.addExchanges(exchanges) return act
def newBoolParam(name, default, **kwargs)
-
Expand source code
def newBoolParam(name, default, **kwargs): return newParamDef(name, ParamType.BOOL, default=default, **kwargs)
def newEnumParam(name, default, **kwargs)
-
Expand source code
def newEnumParam(name, default, **kwargs): return newParamDef(name, ParamType.ENUM, default=default, **kwargs)
def newFloatParam(name, default, **kwargs)
-
Expand source code
def newFloatParam(name, default, **kwargs): return newParamDef(name, ParamType.FLOAT, default=default, **kwargs)
def newInterpolatedAct(dbname, name, act1, act2, x1, x2, x, alpha1=1, alpha2=1, **kwargs)
-
Creates a new activity made of interpolation of two similar activities. For each exchange : amount = alpha1 * a1 + (x - X1) * (alpha2 * a2 - alpha1 * a1) / (x2 - x1)
Parameters
name
:Name
ofnew
activity
act1
:Activity
1
act2
:Activity
2
x1
:X
for
act1
x2
:X
for
act
2
x
:Should
be
a
parameter
symbol
alpha1
:Ratio
for
act1
(Default
value
=1
)alpha2
:Ratio
for
act2
(Default
value
=1
)kwargs
:Any
other
param
will
be
added
as
attributes
ofnew
activity
Expand source code
def newInterpolatedAct(dbname: str, name: str, act1: ActivityExtended, act2: ActivityExtended, x1, x2, x, alpha1=1, alpha2=1, **kwargs): """Creates a new activity made of interpolation of two similar activities. For each exchange : amount = alpha1 * a1 + (x - X1) * (alpha2 * a2 - alpha1 * a1) / (x2 - x1) Parameters ---------- name : Name of new activity act1 : Activity 1 act2 : Activity 2 x1 : X for act1 x2 : X for act 2 x : Should be a parameter symbol alpha1 : Ratio for act1 (Default value = 1) alpha2 : Ratio for act2 (Default value = 1) kwargs : Any other param will be added as attributes of new activity """ res = copyActivity(dbname, act1, name, withExchanges=False, **kwargs) exch1_by_input = dict({exch['input']: exch for exch in act1.exchangesNp()}) exch2_by_input = dict({exch['input']: exch for exch in act2.exchangesNp()}) inputs = set(chain(exch1_by_input.keys(), exch2_by_input.keys())) for input in inputs: exch1 = exch1_by_input.get(input) exch2 = exch2_by_input.get(input) exch = exch1 if exch1 else exch2 amount1 = exch1['amount'] if exch1 else 0 amount2 = exch2['amount'] if exch2 else 0 if exch1 and exch2 and exch1['name'] != exch2['name']: raise Exception("Input %s refer two different names : %s, %s" % (input, exch1['name'], exch2['name'])) amount = interpolate(x, x1, x2, amount1 * alpha1, amount2 * alpha2) act = getActByCode(*input) res.addExchanges({act: dict(amount=amount, name=exch['name'])}) return res
def newParamDef(name, type, **kwargs)
-
Creates a param and register it into a global registry and as a brightway parameter
Expand source code
def newParamDef(name, type, **kwargs): """Creates a param and register it into a global registry and as a brightway parameter""" if type == ParamType.ENUM: param = EnumParam(name, **kwargs) elif type == ParamType.BOOL : param = BooleanDef(name, **kwargs) else: param = ParamDef(name, type=type, **kwargs) # Put it in local registry (in memory) if name in _param_registry(): _eprint("Param %s was already defined : overriding" % name) _param_registry()[name] = param # Save in brightway2 project bwParams = [dict(name=key, amount=value) for key, value in param.expandParams().items()] bw.parameters.new_project_parameters(bwParams) return param
def newSwitchAct(dbname, name, paramDef, acts_dict)
-
Create a new parametrized, virtual activity, made of a map of other activities, controlled by an enum parameter. This enables to implement a "Switch" with brightway parameters Internally, this will create a linear sum of other activities controlled by
_ : 0 or 1 Parameters
paramDef
:parameter
definition
oftype
enum
acts_dict
:dict
of <enumValue
> =>activity
Expand source code
def newSwitchAct(dbname, name, paramDef: ParamDef, acts_dict: Dict[str, Activity]): """Create a new parametrized, virtual activity, made of a map of other activities, controlled by an enum parameter. This enables to implement a "Switch" with brightway parameters Internally, this will create a linear sum of other activities controlled by <param_name>_<enum_value> : 0 or 1 Parameters ---------- paramDef : parameter definition of type enum acts_dict : dict of <enumValue> => activity """ # Transform map of enum values to correspoding formulas <param_name>_<enum_value> exch = {act: paramDef.symbol(key) for key, act in acts_dict.items()} res = newActivity( dbname, name, unit=list(acts_dict.values())[0]['unit'], exchanges=exch) # Unit of switch activity is the one of the children for key, act in acts_dict.items(): if 'unit' in act: res['unit'] = act['unit'] res.save() return res
def oat_dasboard(modelOrLambdas, impacts, param, n=10)
-
Analyse the evolution of impacts for a single parameter. The other parameters are set to their default values.
Parameters
model
:activity
, orlambdas
as
precomputed
by
preMultiLCAAlgebric()
,for
faster
computation
impacts
:set
ofmethods
param
:parameter
to
analyse
n
:number
ofsamples
ofthe
parameter
Expand source code
def oat_dasboard(modelOrLambdas, impacts, param: ParamDef, n=10) : ''' Analyse the evolution of impacts for a single parameter. The other parameters are set to their default values. Parameters ---------- model : activity, or lambdas as precomputed by preMultiLCAAlgebric, for faster computation impacts : set of methods param: parameter to analyse n: number of samples of the parameter ''' params = {param.name : param.default for param in _param_registry().values()} # Compute range of values for given param params[param.name] = param.range(n) #print("Params: ", params) if isinstance(modelOrLambdas, Activity) : df = multiLCAAlgebric(modelOrLambdas, impacts, **params) else : df = postMultiLCAAlgebric(impacts, modelOrLambdas, **params) # Add X values in the table pname = param.name if param.unit : pname = '%s [%s]' % (pname, param.unit) df.insert(0, pname, param.range(n)) df = df.set_index(pname) def table() : display(df) def graph() : with warnings.catch_warnings(): warnings.simplefilter("ignore") nb_rows = len(impacts) // 3 + 1 fig, axes = plt.subplots(figsize=(15, 15)) axes = df.plot( ax=axes, sharex=True, subplots=True, layout=(nb_rows, 3), #legend=None, kind = 'line' if param.type == ParamType.FLOAT else 'bar') axes = axes.flatten() for ax, impact in zip(axes, impacts) : ax.set_ylim(ymin=0) ax.set_ylabel(_method_unit(impact)) plt.show(fig) def change() : ch = (df.max() - df.min()) / df.median() * 100 fig, ax = plt.subplots(figsize=(9, 6)) plt.title('Relative change for %s' % df.index.name) ch.plot(kind='barh', rot=30) ax.set_xlabel('Relative change of the median value (%)') plt.tight_layout() plt.show(fig) _display_tabs([ ("Graphs", graph), ("Data", table), ("Variation", change) ])
def oat_dashboard_interact(model, methods)
-
Interative dashboard, with a dropdown for selecting parameter
Expand source code
def oat_dashboard_interact(model, methods): '''Interative dashboard, with a dropdown for selecting parameter''' lambdas = preMultiLCAAlgebric(model, methods, _param_registry().keys()) def process_func(param) : oat_dasboard(lambdas, methods, _param_registry()[param]) paramlist = list(_variable_params().keys()) interact(process_func, param=paramlist)
def oat_matrix(model, impacts, n=10)
-
Generates a heatmap of the incertitude of the model, varying input parameters one a a time.
Expand source code
def oat_matrix(model, impacts, n=10) : '''Generates a heatmap of the incertitude of the model, varying input parameters one a a time.''' # Compile model into lambda functions for fast LCA lambdas = preMultiLCAAlgebric(model, impacts, _param_registry().keys()) change = np.zeros((len(_variable_params()), len(impacts))) for iparam, param in enumerate(_variable_params().values()) : params = {param.name: param.default for param in _param_registry().values()} # Compute range of values for given param params[param.name] = param.range(n) # Compute LCA df = postMultiLCAAlgebric(impacts, lambdas, **params) # Compute change change[iparam] = (df.max() - df.min()) / df.median() * 100 # Build final heatmap change = pd.DataFrame(change, index=_variable_params().keys(), columns=[imp[2] for imp in impacts]) _heatmap(change.transpose(), 'Change of impacts per variability of the input parameters (%)', 100, ints=True)
def postMultiLCAAlgebric(methods, lambdas, alpha=1, **params)
-
Compute LCA for a given set of parameters and pre-compiled lambda functions. This function is used by multiLCAAlgebric
Parameters
methodAndLambdas
:Output
ofpreMultiLCAAlgebric()
**params
:Parameters
ofthe
model
Expand source code
def postMultiLCAAlgebric(methods, lambdas, alpha=1, **params): ''' Compute LCA for a given set of parameters and pre-compiled lambda functions. This function is used by **multiLCAAlgebric** Parameters ---------- methodAndLambdas : Output of preMultiLCAAlgebric **params : Parameters of the model ''' # Check and expand params params = _completeParamValues(params) # Expand parameters as list of parameters param_length = 1 for key, val in params.items(): if isinstance(val, list): if param_length == 1: param_length = len(val) elif param_length != len(val): raise Exception("Parameters should be a single value or a list of same number of values") # Expand params and transform lists to np.array for vector computation for key in params.keys(): val = params[key] if not isinstance(val, list): val = list([val] * param_length) params[key] = np.array(val) res = np.zeros((len(methods), param_length)) # Compute result on whole vectors of parameter samples at a time : lambdas use numpy for vector computation for imethod, lambd in enumerate(lambdas): res[imethod, :] = alpha * lambd(**params) return pd.DataFrame(res, index=[method_name(method) for method in methods]).transpose()
def preMultiLCAAlgebric(model, methods, param_names=None, amount=1)
-
This method transforms an activity into a set of functions ready to compute LCA very fast on a set on methods. You may use is and pass the result to postMultiLCAAlgebric for fast computation on a model that does not change.
This method is used by multiLCAAlgebric
Expand source code
def preMultiLCAAlgebric(model:ActivityExtended, methods, param_names=None, amount=1) : ''' This method transforms an activity into a set of functions ready to compute LCA very fast on a set on methods. You may use is and pass the result to postMultiLCAAlgebric for fast computation on a model that does not change. This method is used by multiLCAAlgebric ''' # print("computing model to expression for %s" % model) expr, actBySymbolName = actToExpression(model) dbname = model.key[0] param_names = _expand_param_names(param_names) # Check missing params free_names = set([str(symb) for symb in expr.free_symbols]) act_names = set([str(symb) for symb in actBySymbolName.keys()]) expected_names = free_names - act_names missing_names = expected_names - set(param_names) if len(missing_names) > 0 : raise Exception('Missing parameter of the model : %s' % str(missing_names)) debug(param_names) for name in param_names : registry_names = _expand_param_names(_param_registry()) if name not in registry_names : raise Exception('Model refers to unknown param "%s"' % name) # Create dummy reference to biosphere # We cannot run LCA to biosphere activities # We create a technosphere activity mapping exactly to 1 biosphere item pureTechActBySymbol = OrderedDict() for name, act in actBySymbolName.items(): if act[0] == BIOSPHERE3_DB_NAME: act = _getOrCreateDummyBiosphereActCopy(dbname, act[1]) else: act = getActByCode(*act) pureTechActBySymbol[name] = act # List of activities, ordered acts = pureTechActBySymbol.values() # Transform to [{act1:1], {act2:1}, etc] for MultiLCA actsWithAmount = [{act: 1} for act in acts] # Compute LCA for all background activities and methods lca = _multiLCA(actsWithAmount, methods) # For each method, compute an algebric expression with activities replaced by their values lambdas = [] for imethod, method in enumerate(methods): # print("Generating lamba function for %s / %s" % (model, method)) # Replace activities by their value in expression for this method sub = dict({symbol: lca.iloc[imethod, iact] for iact, symbol in enumerate(pureTechActBySymbol.keys())}) method_expr = expr.xreplace(sub) # Tranform Sympy expression to lambda function, based on numpy to fast vectorial evaluation lambd = lambdify(param_names, method_expr, 'numpy') lambdas.append(lambd) return lambdas
def printAct(*activities, **params)
-
Print activities and their exchanges. If parameter values are provided, formulas will be evaluated accordingly
Expand source code
def printAct(*activities, **params): """ Print activities and their exchanges. If parameter values are provided, formulas will be evaluated accordingly """ tables = [] names = [] for act in activities: df = pd.DataFrame(index=['input', 'amount', 'unit', 'type']) data = dict() for (i, exc) in enumerate(act.exchanges()): input = bw.get_activity(exc.input.key) amount = _getAmountOrFormula(exc) # Params provided ? Evaluate formulas if len(params) > 0 and isinstance(amount, Basic): new_params = [(name, value) for name, value in _completeParamValues(params).items()] amount = amount.subs(new_params) name = exc['name'] if 'location' in input and input['location'] != "GLO": name += "#%s" % input['location'] if exc.input.key[0] not in [BIOSPHERE3_DB_NAME, ECOINVENT_DB_NAME] : name += " {user-db}" iname = name i=1 while iname in data : iname = "%s#%d" % (name, i) i += 1 data[iname] = [str(input), amount, exc.unit, exc['type']] for key, values in data.items() : df[key] = values tables.append(df.T) names.append(_actDesc(act)) full = pd.concat(tables, axis=1, keys=names, sort=True) if len(activities) == 2 : yellow = "background-color:yellow" iamount1 = full.columns.get_loc((names[0], "amount")) iamount2 = full.columns.get_loc((names[1], "amount")) iact1 = full.columns.get_loc((names[0], "input")) iact2 = full.columns.get_loc((names[1], "input")) def same_amount(row) : res = [""] * len(row) if row[iamount1] != row[iamount2] : res[iamount1] = yellow res[iamount2] = yellow if row[iact1] != row[iact2]: res[iact1] = yellow res[iact2] = yellow return res full = full.style.apply(same_amount, axis=1) display(full)
def resetDb(db_name)
-
Create or cleanup a user DB
Expand source code
def resetDb(db_name): """ Create or cleanup a user DB""" if db_name in bw.databases: _eprint("Db %s was here. Reseting it" % db_name) del bw.databases[db_name] db = bw.Database(db_name) db.write(dict())
def resetParams(db_name)
-
Reset project and activity parameters
Expand source code
def resetParams(db_name): """Reset project and activity parameters""" _param_registry().clear() ProjectParameter.delete().execute() ActivityParameter.delete().execute() DatabaseParameter.delete().execute() Group.delete().execute()
Classes
class ActivityExtended (document=None, **kwargs)
-
Improved API for activity : adding a few useful methods. Those methods are backported to #Activity in order to be directly available on all existing instances
Create an
Activity
proxy object.If this is a new activity, can pass
kwargs
.If the activity exists in the database,
document
should be anActivityDataset
.Expand source code
class ActivityExtended(Activity): """Improved API for activity : adding a few useful methods. Those methods are backported to #Activity in order to be directly available on all existing instances """ def getExchange(self, name=None, input=None, single=True): """Get exchange by name or input Parameters ---------- name : name of the exchange. Name can be suffixed with '#LOCATION' to distinguish several exchanges with same name. \ It can also be suffised by '*' to match an exchange starting with this name. Location can be a negative match '!' Exampple : "Wood*#!RoW" matches any exchange with name containing Wood, and location not "RoW" single :True if a single match is expected. Otherwize, a list of result is returned Returns ------- Single exchange or list of exchanges (if _single is False or "name" contains a '*') raise Exception if not matching exchange found """ def single_match(name, exch) : # Name can be "Elecricity#RER" if "#" in name: name, loc = name.split("#") negative = False if loc.startswith("!") : negative = True loc = loc[1:] act = getActByCode(*exch['input']) if not 'location' in act or (negative and act['location'] == loc) or (not negative and act['location'] != loc) : return False if '*' in name : name = name.replace('*', '') return name in exch['name'] else : return name == exch['name'] def match(exch): if name : if isinstance(name, list): return any(single_match(iname, exch) for iname in name) else: return single_match(name, exch) if input: return input == exch['input'] exchs = list(exch for exch in self.exchangesNp() if match(exch)) if len(exchs) == 0: raise Exception("Found no exchange matching name : %s" % name) if single and len(exchs) != 1: raise Exception("Expected 1 exchange with name '%s' found %d" % (name, len(exchs))) if single: return exchs[0] else: return exchs def setOutputAmount(self, amount): '''Set the amount for the single output exchange (1 by default)''' self.addExchanges({self : amount}) def updateExchanges(self, updates: Dict[str, any] = dict()): """Update existing exchanges, by name. Parameters ---------- updates : Dict of "<exchange name>" => <new value> <exchange name> can be suffixed with '#LOCATION' to distinguish several exchanges with same name. \ It can also be suffixed by '*' to match an exchange starting with this name. Location can be a negative match '!' Exampple : "Wood*#!RoW" matches any exchange with name containing Wood, and location not "RoW" <New Value> : either single value (float or SympPy expression) for updating only amount, or activity for updating only input, or dict of attributes, for updating both at once, or any other attribute. The amount can reference the symbol 'old_amount' that will be replaced with the current amount of the exchange. """ # Update exchanges for name, attrs in updates.items(): exchs = self.getExchange(name, single=not '*' in name) if not isinstance(exchs, list): exchs = [exchs] for exch in exchs: if attrs is None: exch.delete() exch.save() continue # Single value ? => amount if not isinstance(attrs, dict): if isinstance(attrs, Activity): attrs = dict(input=attrs) else : attrs = dict(amount=attrs) if 'amount' in attrs: attrs.update(_amountToFormula(attrs['amount'], exch['amount'])) exch.update(attrs) exch.save() # We have a formula now ? => register it to parametrized exchange if 'formula' in attrs: bw.parameters.add_exchanges_to_group(DEFAULT_PARAM_GROUP, self) def deleteExchanges(self, name, single=True): ''' Remove matching exchanges ''' exchs = self.getExchange(name, single=single) if not isinstance(exchs, list): exchs = [exchs] if len(exchs) == 0 : raise Exception("No exchange found for '%s'" % name) for ex in exchs : ex.delete() ex.save() self.save() def substituteWithDefault(self, exchange_name: str, switch_act: Activity, paramSwitch: EnumParam, amount=None): """Substitutes one exchange with a switch on other activities, or fallback to the current one as default (parameter set to None) For this purpose, we create a new exchange referencing the activity switch, and we multiply current activity by '<param_name>_default', making it null as soon as one enum value is set. This is useful for changing electricty mix, leaving the default one if needed Parameters ---------- act : Activity to update exchange_name : Name of the exchange to update switch_act : Activity to substitue as input amount : Amount of the input (uses previous amount by default) """ current_exch = self.getExchange(exchange_name) prev_amount = amount if amount else _getAmountOrFormula(current_exch) self.addExchanges({switch_act: prev_amount}) self.updateExchanges({exchange_name: paramSwitch.symbol(None) * prev_amount}) def addExchanges(self, exchanges: Dict[Activity, Union[NumOrExpression, dict]] = dict()): """Add exchanges to an existing activity, with a compact syntax : Parameters ---------- exchanges : Dict of activity => amount or activity => attributes_dict. \ Amount being either a fixed value or Sympy expression (arithmetic expression of Sympy symbols) """ parametrized = False for sub_act, attrs in exchanges.items(): if isinstance(attrs, dict): amount = attrs.pop('amount') else: amount = attrs attrs = dict() exch = self.new_exchange( input=sub_act.key, name=sub_act['name'], unit=sub_act['unit'] if 'unit' in sub_act else None, type='production' if self == sub_act else 'biosphere' if sub_act['database'] == BIOSPHERE3_DB_NAME else 'technosphere') exch.update(attrs) exch.update(_amountToFormula(amount)) if 'formula' in exch: parametrized = True exch.save() self.save() if parametrized: bw.parameters.add_exchanges_to_group(DEFAULT_PARAM_GROUP, self) def getAmount(self, *args, sum=False, **kargs): """ Get the amount of one or several exchanges, selected by name or input. See #getExchange() """ exchs = self.getExchange(*args, single=not sum, **kargs) if sum: res = 0 if len(exchs) == 0: raise Exception("No exchange found") for exch in exchs: res += _getAmountOrFormula(exch) return res else: return _getAmountOrFormula(exchs) def exchangesNp(self): """ List of exchange, except production (output) one.""" for exch in self.exchanges(): if exch['input'] != exch['output']: yield exch
Ancestors
- bw2data.backends.peewee.proxies.Activity
- bw2data.proxies.ActivityProxyBase
- bw2data.proxies.ProxyBase
- collections.abc.MutableMapping
- collections.abc.Mapping
- collections.abc.Collection
- collections.abc.Sized
- collections.abc.Iterable
- collections.abc.Container
Methods
def addExchanges(self, exchanges={})
-
Add exchanges to an existing activity, with a compact syntax :
Parameters
exchanges
:Dict
ofactivity
=>amount
oractivity
=>attributes_dict.
Amount
being
either
a
fixed
value
orSympy
expression
(arithmetic
expression
ofSympy
symbols
)
Expand source code
def addExchanges(self, exchanges: Dict[Activity, Union[NumOrExpression, dict]] = dict()): """Add exchanges to an existing activity, with a compact syntax : Parameters ---------- exchanges : Dict of activity => amount or activity => attributes_dict. \ Amount being either a fixed value or Sympy expression (arithmetic expression of Sympy symbols) """ parametrized = False for sub_act, attrs in exchanges.items(): if isinstance(attrs, dict): amount = attrs.pop('amount') else: amount = attrs attrs = dict() exch = self.new_exchange( input=sub_act.key, name=sub_act['name'], unit=sub_act['unit'] if 'unit' in sub_act else None, type='production' if self == sub_act else 'biosphere' if sub_act['database'] == BIOSPHERE3_DB_NAME else 'technosphere') exch.update(attrs) exch.update(_amountToFormula(amount)) if 'formula' in exch: parametrized = True exch.save() self.save() if parametrized: bw.parameters.add_exchanges_to_group(DEFAULT_PARAM_GROUP, self)
def deleteExchanges(self, name, single=True)
-
Remove matching exchanges
Expand source code
def deleteExchanges(self, name, single=True): ''' Remove matching exchanges ''' exchs = self.getExchange(name, single=single) if not isinstance(exchs, list): exchs = [exchs] if len(exchs) == 0 : raise Exception("No exchange found for '%s'" % name) for ex in exchs : ex.delete() ex.save() self.save()
def exchangesNp(self)
-
List of exchange, except production (output) one.
Expand source code
def exchangesNp(self): """ List of exchange, except production (output) one.""" for exch in self.exchanges(): if exch['input'] != exch['output']: yield exch
def getAmount(self, *args, sum=False, **kargs)
-
Get the amount of one or several exchanges, selected by name or input. See #getExchange()
Expand source code
def getAmount(self, *args, sum=False, **kargs): """ Get the amount of one or several exchanges, selected by name or input. See #getExchange() """ exchs = self.getExchange(*args, single=not sum, **kargs) if sum: res = 0 if len(exchs) == 0: raise Exception("No exchange found") for exch in exchs: res += _getAmountOrFormula(exch) return res else: return _getAmountOrFormula(exchs)
def getExchange(self, name=None, input=None, single=True)
-
Get exchange by name or input
Parameters
name
:name
ofthe
exchange.
Name
can
be
suffixed
with
'
#LOCATION'
to
distinguish
several
exchanges
with
same
name.
It
can
also
be
suffised
by
'
*'
to
match
an
exchange
starting
with
this
name.
Location
can
be
a
negative
match
'
!'
- Exampple : "Wood*#!RoW" matches any exchange with name containing Wood, and location not "RoW"
single :True if a single match is expected. Otherwize, a list of result is returned
Returns
Single exchange or list of exchanges (if _single is False or "name" contains a '*') raise Exception if not matching exchange found
Expand source code
def getExchange(self, name=None, input=None, single=True): """Get exchange by name or input Parameters ---------- name : name of the exchange. Name can be suffixed with '#LOCATION' to distinguish several exchanges with same name. \ It can also be suffised by '*' to match an exchange starting with this name. Location can be a negative match '!' Exampple : "Wood*#!RoW" matches any exchange with name containing Wood, and location not "RoW" single :True if a single match is expected. Otherwize, a list of result is returned Returns ------- Single exchange or list of exchanges (if _single is False or "name" contains a '*') raise Exception if not matching exchange found """ def single_match(name, exch) : # Name can be "Elecricity#RER" if "#" in name: name, loc = name.split("#") negative = False if loc.startswith("!") : negative = True loc = loc[1:] act = getActByCode(*exch['input']) if not 'location' in act or (negative and act['location'] == loc) or (not negative and act['location'] != loc) : return False if '*' in name : name = name.replace('*', '') return name in exch['name'] else : return name == exch['name'] def match(exch): if name : if isinstance(name, list): return any(single_match(iname, exch) for iname in name) else: return single_match(name, exch) if input: return input == exch['input'] exchs = list(exch for exch in self.exchangesNp() if match(exch)) if len(exchs) == 0: raise Exception("Found no exchange matching name : %s" % name) if single and len(exchs) != 1: raise Exception("Expected 1 exchange with name '%s' found %d" % (name, len(exchs))) if single: return exchs[0] else: return exchs
def setOutputAmount(self, amount)
-
Set the amount for the single output exchange (1 by default)
Expand source code
def setOutputAmount(self, amount): '''Set the amount for the single output exchange (1 by default)''' self.addExchanges({self : amount})
def substituteWithDefault(self, exchange_name, switch_act, paramSwitch, amount=None)
-
Substitutes one exchange with a switch on other activities, or fallback to the current one as default (parameter set to None) For this purpose, we create a new exchange referencing the activity switch, and we multiply current activity by '
_default', making it null as soon as one enum value is set. This is useful for changing electricty mix, leaving the default one if needed
Parameters
act
:Activity
to
update
exchange_name
:Name
ofthe
exchange
to
update
switch_act
:Activity
to
substitue
as
input
amount
:Amount
ofthe
input
(uses
previous
amount
by
default)
Expand source code
def substituteWithDefault(self, exchange_name: str, switch_act: Activity, paramSwitch: EnumParam, amount=None): """Substitutes one exchange with a switch on other activities, or fallback to the current one as default (parameter set to None) For this purpose, we create a new exchange referencing the activity switch, and we multiply current activity by '<param_name>_default', making it null as soon as one enum value is set. This is useful for changing electricty mix, leaving the default one if needed Parameters ---------- act : Activity to update exchange_name : Name of the exchange to update switch_act : Activity to substitue as input amount : Amount of the input (uses previous amount by default) """ current_exch = self.getExchange(exchange_name) prev_amount = amount if amount else _getAmountOrFormula(current_exch) self.addExchanges({switch_act: prev_amount}) self.updateExchanges({exchange_name: paramSwitch.symbol(None) * prev_amount})
def updateExchanges(self, updates={})
-
Update existing exchanges, by name.
Parameters
updates
:Dict
of"
<exchange
name
>"
=> <new
value
>-
can be suffixed with '#LOCATION' to distinguish several exchanges with same name. It can also be suffixed by '' to match an exchange starting with this name. Location can be a negative match '!' Exampple : "Wood#!RoW" matches any exchange with name containing Wood, and location not "RoW" : either single value (float or SympPy expression) for updating only amount, or activity for updating only input, or dict of attributes, for updating both at once, or any other attribute. The amount can reference the symbol 'old_amount' that will be replaced with the current amount of the exchange.
Expand source code
def updateExchanges(self, updates: Dict[str, any] = dict()): """Update existing exchanges, by name. Parameters ---------- updates : Dict of "<exchange name>" => <new value> <exchange name> can be suffixed with '#LOCATION' to distinguish several exchanges with same name. \ It can also be suffixed by '*' to match an exchange starting with this name. Location can be a negative match '!' Exampple : "Wood*#!RoW" matches any exchange with name containing Wood, and location not "RoW" <New Value> : either single value (float or SympPy expression) for updating only amount, or activity for updating only input, or dict of attributes, for updating both at once, or any other attribute. The amount can reference the symbol 'old_amount' that will be replaced with the current amount of the exchange. """ # Update exchanges for name, attrs in updates.items(): exchs = self.getExchange(name, single=not '*' in name) if not isinstance(exchs, list): exchs = [exchs] for exch in exchs: if attrs is None: exch.delete() exch.save() continue # Single value ? => amount if not isinstance(attrs, dict): if isinstance(attrs, Activity): attrs = dict(input=attrs) else : attrs = dict(amount=attrs) if 'amount' in attrs: attrs.update(_amountToFormula(attrs['amount'], exch['amount'])) exch.update(attrs) exch.save() # We have a formula now ? => register it to parametrized exchange if 'formula' in attrs: bw.parameters.add_exchanges_to_group(DEFAULT_PARAM_GROUP, self)
class BooleanDef (name, *karg, **kargs)
-
Parameter with discrete value 0 or 1
Expand source code
class BooleanDef(ParamDef): """Parameter with discrete value 0 or 1""" def __init__(self, name, **argv): super(BooleanDef, self).__init__(name, ParamType.BOOL, min=0, max=1, **argv) def range(self, n): return [0, 1] def rand(self, alpha): return round(alpha)
Ancestors
- ParamDef
- sympy.core.symbol.Symbol
- sympy.core.expr.AtomicExpr
- sympy.core.basic.Atom
- sympy.core.expr.Expr
- sympy.logic.boolalg.Boolean
- sympy.core.basic.Basic
- sympy.core.evalf.EvalfMixin
Inherited members
class DistributionType
-
Type of distribution
Expand source code
class DistributionType : '''Type of distribution''' LINEAR = "linear" NORMAL = "normal" TRIANGLE = "triangle" FIXED = "fixed"
Class variables
var FIXED
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
var LINEAR
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
var NORMAL
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
var TRIANGLE
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
class EnumParam (name, *karg, **kargs)
-
Enum param is a facility representing a choice / switch as many boolean parameters. It is not itself a Sympy symbol. use #symbol("value") to access it
Expand source code
class EnumParam(ParamDef): """Enum param is a facility representing a choice / switch as many boolean parameters. It is not itself a Sympy symbol. use #symbol("value") to access it""" def __init__(self, name, values: List[str], **argv): super(EnumParam, self).__init__(name, ParamType.ENUM, min=None, max=None, **argv) self.values = values def expandParams(self, currValue=None): values = self.values + [None] res = dict() for enum_val in values: var_name = "%s_%s" % (self.name, enum_val if enum_val is not None else "default") res[var_name] = 1.0 if enum_val == currValue else 0.0 return res def symbol(self, enumValue): """Access parameter for each enum value : <paramName>_<paramValue>""" if enumValue is None: return Symbol(self.name + '_default') if not enumValue in self.values: raise Exception("enumValue should be one of %s. Was %s" % (str(self.values), enumValue)) return Symbol(self.name + '_' + enumValue) def names(self) : return ["%s_%s" % (self.name, value) for value in (self.values + ["default"]) ] def rand(self, alpha): i = math.ceil(alpha * (len(self.values))) -1 return self.values[int(i)] def range(self, n): return self.values
Ancestors
- ParamDef
- sympy.core.symbol.Symbol
- sympy.core.expr.AtomicExpr
- sympy.core.basic.Atom
- sympy.core.expr.Expr
- sympy.logic.boolalg.Boolean
- sympy.core.basic.Basic
- sympy.core.evalf.EvalfMixin
Methods
def expandParams(self, currValue=None)
-
Expand source code
def expandParams(self, currValue=None): values = self.values + [None] res = dict() for enum_val in values: var_name = "%s_%s" % (self.name, enum_val if enum_val is not None else "default") res[var_name] = 1.0 if enum_val == currValue else 0.0 return res
def names(self)
-
Expand source code
def names(self) : return ["%s_%s" % (self.name, value) for value in (self.values + ["default"]) ]
def symbol(self, enumValue)
-
Access parameter for each enum value :
_ Expand source code
def symbol(self, enumValue): """Access parameter for each enum value : <paramName>_<paramValue>""" if enumValue is None: return Symbol(self.name + '_default') if not enumValue in self.values: raise Exception("enumValue should be one of %s. Was %s" % (str(self.values), enumValue)) return Symbol(self.name + '_' + enumValue)
Inherited members
class ParamDef (name, *karg, **kargs)
-
Generic definition of a parameter, with name, bound, type, distribution This definition will serve both to generate brightway2 parameters and to evaluate.
This class inherits sympy Symbol, making it possible to use in standard arithmetic python while keeping it as a symbolic expression (delayed evaluation).
Expand source code
class ParamDef(Symbol): '''Generic definition of a parameter, with name, bound, type, distribution This definition will serve both to generate brightway2 parameters and to evaluate. This class inherits sympy Symbol, making it possible to use in standard arithmetic python while keeping it as a symbolic expression (delayed evaluation). ''' def __new__(cls, name, *karg, **kargs): return Symbol.__new__(cls, name) def __init__(self, name, type: str, default, min=None, max=None, unit="", description="", label=None, label_fr=None, group=None, distrib=DistributionType.LINEAR, std=None): self.name = name self.type = type self.default = default self.description = description self.min = min self.max = max self.unit = unit self.label = label self.label_fr = label_fr self.group=group self.distrib = distrib if type == ParamType.FLOAT and self.min is None : self.distrib = DistributionType.FIXED if distrib == DistributionType.NORMAL and std is None : raise Exception("Standard deviation is mandatory for normal distribution") self.std = std def label(self): if self.label is not None : return self.label else : return self.name.replace("_", " ") def range(self, n) : '''Used for parametric analysis''' step = (self.max - self.min) / (n - 1) return list(i * step + self.min for i in range(0, n)) def rand(self, alpha): """Transforms a random number between 0 and 1 to valid value according to the distribution of probability of the parameter""" if self.distrib == DistributionType.LINEAR : return self.min + alpha * (self.max - self.min) elif self.distrib == DistributionType.TRIANGLE : if not hasattr(self, "_distrib") : scale = self.max - self.min c = (self.default - self.min) / scale self._distrib = triang(c, loc=self.min, scale=scale) return self._distrib.ppf(alpha) elif self.distrib == DistributionType.NORMAL : if not hasattr(self, "_distrib") : self._distrib = truncnorm( (self.min - self.default) / self.std, (self.max - self.min) / self.std, loc=self.default, scale=self.std) return self._distrib.ppf(alpha) else : raise Exception("Unknowk distribution type " + self.distrib) # Expand parameter (useful for enum param) def expandParams(self, value=None) -> Dict[str, float]: if value == None: value = self.default return {self.name: value} # Useful for enum param, having several names def names(self) : return [self.name] def __repr__(self): return self.name
Ancestors
- sympy.core.symbol.Symbol
- sympy.core.expr.AtomicExpr
- sympy.core.basic.Atom
- sympy.core.expr.Expr
- sympy.logic.boolalg.Boolean
- sympy.core.basic.Basic
- sympy.core.evalf.EvalfMixin
Subclasses
Class variables
var default_assumptions
-
A FactKB specialised for the built-in rules
This is the only kind of FactKB that Basic objects should use.
Methods
def expandParams(self, value=None)
-
Expand source code
def expandParams(self, value=None) -> Dict[str, float]: if value == None: value = self.default return {self.name: value}
def label(self)
-
Expand source code
def label(self): if self.label is not None : return self.label else : return self.name.replace("_", " ")
def names(self)
-
Expand source code
def names(self) : return [self.name]
def rand(self, alpha)
-
Transforms a random number between 0 and 1 to valid value according to the distribution of probability of the parameter
Expand source code
def rand(self, alpha): """Transforms a random number between 0 and 1 to valid value according to the distribution of probability of the parameter""" if self.distrib == DistributionType.LINEAR : return self.min + alpha * (self.max - self.min) elif self.distrib == DistributionType.TRIANGLE : if not hasattr(self, "_distrib") : scale = self.max - self.min c = (self.default - self.min) / scale self._distrib = triang(c, loc=self.min, scale=scale) return self._distrib.ppf(alpha) elif self.distrib == DistributionType.NORMAL : if not hasattr(self, "_distrib") : self._distrib = truncnorm( (self.min - self.default) / self.std, (self.max - self.min) / self.std, loc=self.default, scale=self.std) return self._distrib.ppf(alpha) else : raise Exception("Unknowk distribution type " + self.distrib)
def range(self, n)
-
Used for parametric analysis
Expand source code
def range(self, n) : '''Used for parametric analysis''' step = (self.max - self.min) / (n - 1) return list(i * step + self.min for i in range(0, n))
class ParamType
-
Type of parameters
Expand source code
class ParamType: '''Type of parameters''' ENUM = "enum" BOOL = "bool" FLOAT = "float"
Class variables
var BOOL
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
var ENUM
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.
var FLOAT
-
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to 'strict'.