brightway-lca / brightway2-data

Tools for the management of inventory databases and impact assessment methods. Part of the Brightway LCA framework.
https://docs.brightway.dev/
BSD 3-Clause "New" or "Revised" License
11 stars 24 forks source link

Pint Parameters for Brightway2 (legacy) #125

Open BenPortner opened 1 year ago

BenPortner commented 1 year ago

Description

This PR enables the usage of units in parameter formulas. It uses the newly introduced bw2parameter.PintInterpreter and bw2parameter.PintParameterSet classes for this. Units are permanently stored in the data field of the SQLite databases for ProjectParameter, DatabaseParameter and ActivityParameter so they become available again after reloading.

Example

from bw2data.parameters import ProjectParameter
from bw2data import config

config.use_pint_parameters = True

ProjectParameter.create(
    name="p_proj",
    formula="1 kg + 200 g",
)
ProjectParameter.recalculate()
obj = ProjectParameter.get(name="p_proj")
assert obj.amount == 1.2
assert obj.data["unit"] == "kilogram"

Additional Considerations

Optionality vs. usability

Unit parsing and interpretation relies on the pint package, which is currently not part of the mandatory Brightway2 dependencies. I tried to keep it that way. However, this approach requires extra caution from the user. By default, unit parsing is deactivated. It must be switched on actively by setting config.use_pint_parameters = True. If the switch is off and one tries to parse a formula including units, an error will be raised: bw2parameters.errors.MissingName: One or more symbols could not be interpreted. Please check the formula. If it contains units, please set 'bw2parameters.config.use_pint = True': 1 kg + 0.2 kg.

Once a parameter with a unit in its formula is stored in the database, any future manipulation of this parameter or a dependent parameter will require config.use_pint_parameters = True or above error will appear (note: non-unit containing and non-dependent parameters can be processed as usual). To prevent such errors, units may be defined optionally in the data field. This way, parameters can be evaluated with use_pint_parameters on or off. However, in this case the user must take care that parameters are defined compatibly, otherwise unexpected results may occur:

from bw2data.parameters import ProjectParameter
from bw2data import config

# define parameters
ProjectParameter.create(
    name="p_proj3",
    amount=1,
    data={"unit": "kilogram"},
)
ProjectParameter.create(
    name="p_proj4",
    amount=200,
    data={"unit": "gram"},
)
ProjectParameter.create(
    name="p_proj5",
    formula="p_proj3 + p_proj4",
)

# solve with pint -> 1.2 kg
config.use_pint_parameters = True
ProjectParameter.recalculate()
obj = ProjectParameter.get(name="p_proj5")
assert obj.amount == 1.2
assert obj.dict["unit"] == "kilogram"

# solve without pint -> 201 kg
config.use_pint_parameters = False
group = Group.get(name="project")
group.fresh = False
group.save()
ProjectParameter.recalculate()
obj = ProjectParameter.get(name="p_proj5")
assert obj.amount == 201
assert obj.dict["unit"] == "kilogram"

Note that the latter behavior corresponds to the current default: It is left to the user to correctly convert units between formulas. New is the possibility to do so automatically by setting config.use_pint_parameters = True.

Computation time

Invoking pint for the first time carries a time penalty. Any invocations thereafter are as fast as using the default (asteval) interpreter. I attached a benchmark script to this PR. Short summary:

Solving parameters without units 10 times without pint solver took 2.718524694442749 s.
Solving parameters without units 10 times with pint solver took 4.090916156768799 s.
Solving parameters with units 10 times with pint solver took 2.1366446018218994 s.

Requirements

This PR builds on top of two other PRs, which need to be approved and merged first:

Todo (after review):

Attachments

bw2data-pint-benchmark.py.txt