# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2020-2023, Yoel Cortes-Pena <yoelcortes@gmail.com>
#
# This module is under the UIUC open-source license. See
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
"""
from __future__ import annotations
from thermosteam import settings
from thermosteam.utils import units_of_measure
from thermosteam.units_of_measure import (
DisplayUnits, convert, power_utility_units_of_measure, AbsoluteUnitsOfMeasure
)
from typing import Optional
__all__ = ('PowerUtility',)
impact_indicator_basis = AbsoluteUnitsOfMeasure('kWhr')
default_price = 0.0782
[docs]
@units_of_measure(power_utility_units_of_measure)
class PowerUtility:
"""
Create an PowerUtility object that stores data on consumption and production
of electricity.
Notes
-----
The default price is 0.0782 USD/kWhr as suggested in [1]_.
References
----------
.. [1] Seider, W. D., Lewin, D. R., Seader, J. D., Widagdo, S., Gani, R.,
& Ng, M. K. (2017). Product and Process Design Principles. Wiley.
Examples
--------
Create a PowerUtility object:
>>> pu = PowerUtility()
>>> pu
PowerUtility(consumption=0.0, production=0.0)
PowerUtility objects have `consumption` and `production` attributes
which are updated when setting the power with the assumption that
a positive power means no production (only consumption) and a negative
power means no consumption (only production).
>>> pu(power=-500)
>>> pu.consumption, pu.production
(0.0, 500.0)
>>> pu(power=500.)
>>> pu.consumption, pu.production
(500.0, 0.0)
It is possible to have both consumption and production by setting these
attributes individually (instead of setting power)
>>> pu.production = 100.
>>> pu.power
400.0
Notice how the power is equal to the consumption minus the production.
The cost is available as a property:
>>> pu.cost # USD/hr
31.28
It may be useful to print results in different units of measure:
>>> pu.show(power='BTU/s')
PowerUtility:
consumption: 474 BTU/s
production: 94.8 BTU/s
power: 379 BTU/s
cost: 31.3 USD/hr
"""
__slots__ = ('consumption', 'production')
#: Characterization factors for life cycle assessment [impact/kWhr] by impact key and kind (None, 'consumption', or 'production').
characterization_factors: dict[tuple[str, str], float] = {}
#: Units of measure for IPython display
display_units: DisplayUnits = DisplayUnits(power='kW', cost='USD/hr')
def __init__(self, consumption: float=0., production: float=0.):
#: Electricity consumption [kW]
self.consumption: float = consumption
#: Electricity production [kW]
self.production: float = production
[docs]
def empty(self):
"""Set consumption and production to zero."""
self.consumption = self.production = 0.
[docs]
@classmethod
def get_CF(cls, key: str, consumption: Optional[bool]=True,
production: Optional[bool]=True, basis: Optional[str]=None,
units: Optional[str]=None):
"""
Return the life-cycle characterization factor for consumption and
production on a kWhr basis given the impact key.
Parameters
----------
key :
Name of impact indicator.
consumption :
Whether to return impact indicator for electricity consumption.
production :
Whether to return impact indicator for electricity production.
basis :
Basis of characterization factor. Energy is the only valid dimension.
Defaults to 'kWhr'.
units :
Units of impact indicator. Before using this argument, the default units
of the impact indicator should be defined with
:meth:`settings.define_impact_indicator <thermosteam._settings.ProcessSettings.define_impact_indicator>`.
Units must also be dimensionally consistent with the default units.
"""
try:
value = cls.characterization_factors[key]
except KeyError:
value = (0., 0.)
if consumption:
if not production:
value = value[0]
elif production:
value = value[1]
else:
return None
if units is not None:
original_units = settings.get_impact_indicator_units(key)
f = original_units.conversion_factor(units)
if consumption and production:
value = (consumption * f,
production * f)
else:
value *= f
if basis is not None:
f = impact_indicator_basis.conversion_factor(basis)
consumption /= f
production /= f
return value
[docs]
@classmethod
def set_CF(cls, key: str, consumption: Optional[float]=None,
production: Optional[float]=None, basis: Optional[str]=None,
units: Optional[str]=None):
"""
Set the life-cycle characterization factors for consumption and production
on a kWhr basis given the impact key.
Parameters
----------
key :
Name of impact indicator.
consumption :
Impact indicator for electricity consumption.
production :
Impact indicator for electricity production.
basis :
Basis of characterization factor. Energy is the only valid dimension.
Defaults to 'kWhr'.
units :
Units of impact indicator. Before using this argument, the default units
of the impact indicator should be defined with
:meth:`settings.define_impact_indicator <thermosteam._settings.ProcessSettings.define_impact_indicator>`.
Units must also be dimensionally consistent with the default units.
"""
if consumption is None:
if production is None:
raise ValueError("must pass at least one of either 'consumption' or 'production'")
consumption = production
elif production is None:
production = consumption
if units is not None:
original_units = settings.get_impact_indicator_units(key)
f = original_units.conversion_factor(units)
consumption /= f
production /= f
if basis is not None:
f = impact_indicator_basis.conversion_factor(basis)
consumption *= f
production *= f
cls.characterization_factors[key] = (consumption, production)
[docs]
@classmethod
def default_price(cls):
"""Reset price back to BioSTEAM's default."""
cls.price: float = default_price #: Electricity price [USD/kWhr]
@property
def power(self) -> float:
"""Power requirement [kW]."""
return self.consumption - self.production
@power.setter
def power(self, power: float):
power = float(power)
if power >= 0.:
self.consumption = power
self.production = 0.
else:
self.consumption = 0.
self.production = -power
rate = power # For backwards compatibility
@property
def cost(self) -> float:
"""Cost [USD/hr]"""
return self.price * self.power
[docs]
def get_impact(self, key: str):
"""Return the impact [impact/hr] given characterization factor keys
for consumption and production. If no production key given, it defaults
to the consumption key."""
power = self.consumption - self.production
try:
cf = self.characterization_factors[key]
except:
return 0.
return (cf[0] if power > 0. else cf[1]) * power
def __bool__(self):
return bool(self.consumption or self.production)
def __call__(self, power: float):
"""Set power [kW]."""
self.power = power
def copy(self):
return self.__class__(self.consumption, self.production)
[docs]
def mix_from(self, power_utilities: list[PowerUtility]):
"""
Mix in requirements of power utilities.
Examples
--------
>>> pus = [PowerUtility(production=100),
... PowerUtility(consumption=50),
... PowerUtility(production=20)]
>>> pu = PowerUtility()
>>> pu.mix_from(pus)
>>> print(pu)
PowerUtility(consumption=50.0, production=120.0)
"""
self.consumption = sum([i.consumption for i in power_utilities])
self.production = sum([i.production for i in power_utilities])
[docs]
def copy_like(self, power_utility: PowerUtility):
"""Copy consumption and production from another power utility."""
self.consumption = power_utility.consumption
self.production = power_utility.production
[docs]
def scale(self, scale: int):
"""Scale consumption and production accordingly."""
self.consumption *= scale
self.production *= scale
[docs]
@classmethod
def sum(cls, power_utilities: list[PowerUtility]):
"""
Return a PowerUtility object that represents the sum of power utilities.
Examples
--------
>>> pus = [PowerUtility(production=100),
... PowerUtility(consumption=50),
... PowerUtility(production=20)]
>>> pu = PowerUtility.sum(pus)
>>> print(pu)
PowerUtility(consumption=50.0, production=120.0)
"""
power_utility = cls()
power_utility.mix_from(power_utilities)
return power_utility
def show(self, power: Optional[str]=None, cost: Optional[str]=None):
# Get units of measure
display_units = self.display_units
power_units = power or display_units.power
cost_units = cost or display_units.cost
production = convert(self.production, 'kW', power_units)
consumption = convert(self.consumption, 'kW', power_units)
power = consumption - production
cost = convert(self.cost, 'USD/hr', cost_units)
print(f'{type(self).__name__}:\n'
f'consumption: {consumption:.3g} {power_units}\n'
f'production: {production:.3g} {power_units}\n'
f'power: {power:.3g} {power_units}\n'
f'cost: {cost:.3g} {cost_units}')
_ipython_display_ = show
def __repr__(self) -> str:
return f'{type(self).__name__}(consumption={self.consumption}, production={self.production})'
def __add__(self, other: PowerUtility) -> PowerUtility:
if other == 0: return self # Special case to get Python built-in sum to work
return PowerUtility.sum([self, other])
def __radd__(self, other: PowerUtility) -> PowerUtility:
return self.__add__(other)
PowerUtility.default_price()
settings.__class__.set_electricity_CF = PowerUtility.set_CF
del units_of_measure, AbsoluteUnitsOfMeasure