Skip to content

Commit

Permalink
Merge pull request #126 from QSD-Group/beta
Browse files Browse the repository at this point in the history
Cumulative updates
  • Loading branch information
yalinli2 authored Dec 6, 2024
2 parents cb6678a + 19486fa commit 30f7273
Show file tree
Hide file tree
Showing 13 changed files with 458 additions and 94 deletions.
20 changes: 20 additions & 0 deletions docs/source/FAQ.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,26 @@ There are multiple possible reasons:
Then when you open the Jupyter Notebook, select the ``<KERNEL NAME>`` kernel when you create a new notebook you can find more details in this post about `enabling multiple kernels in Jupyter Notebook <https://medium.com/@ace139/enable-multiple-kernels-in-jupyter-notebooks-6098c738fe72>`_.


``underlying object has vanished``
**********************************
This error is related to ``numba`` caching, we haven't figured out the exact mechanism, but clearing cache will help resolve it. One/both of the following approaches should work:

1. Clear cache. Remove all ``.pyc``, ``.nbc``, and ``.nbi`` files, you can do this in your CLI using (replace <DIR> with the directory to your ``thermosteam``, ``biosteam``, ``qsdsan``, and ``exposan`` directory):

.. code::
get-childitem . -recurse -include *.pyc, *.nbc, *.nbi | remove-item
2. Uninstalling and reinstalling a different version of ``numba``. Suppose you now have 0.58.1 and the newest version is 0.60.0, you can do:

.. code::
pip uninstall numba
pip install --no-cache-dir numba==0.60.0
The ``--no-cache-dir`` option is to do a fresh installation rather than using previously downloaded packages. Note that you need to exit out your editor/any other programs that are currently using numba. Otherwise the uninstallation is incomplete, you might be prompted to do a manual removal, or this won't work.


``UnicodeDecodeError``
**********************
When using non-English operating systems, you may run into errors similar to (cp949 is the case of Korean Windows):
Expand Down
2 changes: 1 addition & 1 deletion qsdsan/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
Flowsheet = _bst.Flowsheet
main_flowsheet = _bst.main_flowsheet
default_utilities = _bst.default_utilities
CEPCI_by_year = _bst.units.design_tools.CEPCI_by_year

# Global variables
currency = 'USD'
Expand All @@ -54,6 +53,7 @@


from . import utils
CEPCI_by_year = utils.indices.tea_indices['CEPCI']
from ._component import *
from ._components import *
from ._sanstream import *
Expand Down
2 changes: 1 addition & 1 deletion qsdsan/_impact_item.py
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ def linked_stream(self, new_s):
f'is replaced with {self.ID}.')
else:
warn(f'The original `StreamImpactItem` linked to stream {new_s.ID} '
f'is replaced with upon the creation of a new stream.')
f'is replaced upon the creation of a new stream.')
new_s._stream_impact_item = self
self._linked_stream = new_s

Expand Down
68 changes: 41 additions & 27 deletions qsdsan/_lca.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,9 @@ class LCA:
>>> # Retrieve impacts associated with a specific indicator
>>> lca.get_total_impacts()[GWP.ID] # doctest: +ELLIPSIS
349737809...
>>> # Annual results
>>> lca.get_total_impacts(annual=True)[GWP.ID] # doctest: +ELLIPSIS
34973780...
>>> # Or breakdowns of the different category
>>> lca.get_impact_table('Construction') # doctest: +SKIP
>>> # Below is for testing purpose, you do not need it
Expand Down Expand Up @@ -637,12 +640,13 @@ def get_unit_impacts(

return tot

def _append_cat_sum(self, cat_table, cat, tot):
def _append_cat_sum(self, cat_table, cat, tot, annual=False):
num = len(cat_table)
cat_table.loc[num] = '' # initiate a blank spot for value to be added later

suffix = '/yr' if annual else ''

for i in self.indicators:
cat_table[f'{i.ID} [{i.unit}]'][num] = tot[i.ID]
cat_table[f'{i.ID} [{i.unit}{suffix}]'][num] = tot[i.ID]
cat_table[f'Category {i.ID} Ratio'][num] = 1

if cat in ('construction', 'transportation'):
Expand All @@ -662,17 +666,21 @@ def get_impact_table(self, category, annual=False):
Parameters
----------
category : str
Can be 'construction', 'transportation', 'stream', or 'other'.
Can be 'Construction', 'Transportation', 'Stream', or 'Other'.
annual : bool
If True, will return the annual impacts considering `uptime_ratio`
instead of across the system lifetime.
'''
time = self.lifetime_hr
sys_yr = self.lifetime
cat = category.lower()
tot_f = getattr(self, f'get_{cat}_impacts')
kwargs = {'annual': annual} if cat != 'other' else {}
# kwargs = {'annual': annual} if cat != 'other' else {}
kwargs = {'annual': annual}
tot = tot_f(**kwargs)

suffix = '/yr' if annual else''
_append_cat_sum = self._append_cat_sum

if cat in ('construction', 'transportation'):
units = sorted(getattr(self, f'_{cat}_units'),
key=(lambda su: su.ID))
Expand All @@ -684,31 +692,35 @@ def get_impact_table(self, category, annual=False):
# Note that item_dct = dict.fromkeys([item.ID for item in items], []) won't work
item_dct = dict.fromkeys([item.ID for item in items])
for item_ID in item_dct.keys():
item_dct[item_ID] = dict(SanUnit=[], Quantity=[])
item_dct[item_ID] = {'SanUnit': [], f'Quantity{suffix}': []}
for su in units:
if not isinstance(su, SanUnit):
continue
for i in getattr(su, cat):
item_dct[i.item.ID]['SanUnit'].append(su.ID)
if cat == 'transportation':
item_dct[i.item.ID]['Quantity'].append(i.quantity*time/i.interval)
quantity = i.quantity*time/i.interval
quantity = quantity/sys_yr if annual else quantity
item_dct[i.item.ID][f'Quantity{suffix}'].append(quantity)
else: # construction
lifetime = i.lifetime or su.lifetime or self.lifetime
if isinstance(lifetime, dict): # in the case the the equipment is not in the unit lifetime dict
lifetime = lifetime.get(i.item.ID) or self.lifetime
constr_ratio = self.lifetime/lifetime if self.annualize_construction else ceil(self.lifetime/lifetime)
item_dct[i.item.ID]['Quantity'].append(i.quantity*constr_ratio)
constr_ratio = sys_yr/lifetime if self.annualize_construction else ceil(sys_yr/lifetime)
quantity = i.quantity * constr_ratio
quantity = quantity/sys_yr if annual else quantity
item_dct[i.item.ID][f'Quantity{suffix}'].append(quantity)

dfs = []
for item in items:
dct = item_dct[item.ID]
dct['SanUnit'].append('Total')
dct['Quantity'] = np.append(dct['Quantity'], sum(dct['Quantity']))
if dct['Quantity'].sum() == 0.: dct['Item Ratio'] = 0
else: dct['Item Ratio'] = dct['Quantity']/dct['Quantity'].sum()*2
dct[f'Quantity{suffix}'] = np.append(dct[f'Quantity{suffix}'], sum(dct[f'Quantity{suffix}']))
if dct[f'Quantity{suffix}'].sum() == 0.: dct['Item Ratio'] = 0
else: dct['Item Ratio'] = dct[f'Quantity{suffix}']/dct[f'Quantity{suffix}'].sum()*2
for i in self.indicators:
if i.ID in item.CFs:
dct[f'{i.ID} [{i.unit}]'] = impact = dct['Quantity']*item.CFs[i.ID]
dct[f'{i.ID} [{i.unit}{suffix}]'] = impact = dct[f'Quantity{suffix}']*item.CFs[i.ID]
dct[f'Category {i.ID} Ratio'] = impact/tot[i.ID]
else:
dct[f'{i.ID} [{i.unit}]'] = dct[f'Category {i.ID} Ratio'] = 0
Expand All @@ -721,55 +733,57 @@ def get_impact_table(self, category, annual=False):
dfs.append(df)

table = pd.concat(dfs)
return self._append_cat_sum(table, cat, tot)
return _append_cat_sum(table, cat, tot, annual=annual)

ind_head = sum(([f'{i.ID} [{i.unit}]',
ind_head = sum(([f'{i.ID} [{i.unit}{suffix}]',
f'Category {i.ID} Ratio'] for i in self.indicators), [])

if cat in ('stream', 'streams'):
headings = ['Stream', 'Mass [kg]', *ind_head]
headings = ['Stream', f'Mass [kg]{suffix}', *ind_head]
item_dct = dict.fromkeys(headings)
for key in item_dct.keys():
item_dct[key] = []
for ws_item in self.stream_inventory:
ws = ws_item.linked_stream
item_dct['Stream'].append(ws.ID)
mass = ws_item.flow_getter(ws) * time
item_dct['Mass [kg]'].append(mass)
mass = mass/sys_yr if annual else mass
item_dct[f'Mass [kg]{suffix}'].append(mass)
for ind in self.indicators:
if ind.ID in ws_item.CFs.keys():
impact = ws_item.CFs[ind.ID]*mass
item_dct[f'{ind.ID} [{ind.unit}]'].append(impact)
item_dct[f'{ind.ID} [{ind.unit}{suffix}]'].append(impact)
item_dct[f'Category {ind.ID} Ratio'].append(impact/tot[ind.ID])
else:
item_dct[f'{ind.ID} [{ind.unit}]'].append(0)
item_dct[f'{ind.ID} [{ind.unit}{suffix}]'].append(0)
item_dct[f'Category {ind.ID} Ratio'].append(0)
table = pd.DataFrame.from_dict(item_dct)
table.set_index(['Stream'], inplace=True)
return self._append_cat_sum(table, cat, tot)
return _append_cat_sum(table, cat, tot, annual=annual)

elif cat == 'other':
headings = ['Other', 'Quantity', *ind_head]
headings = ['Other', f'Quantity{suffix}', *ind_head]
item_dct = dict.fromkeys(headings)
for key in item_dct.keys():
item_dct[key] = []
for other_ID in self.other_items.keys():
other = self.other_items[other_ID]['item']
item_dct['Other'].append(f'{other_ID} [{other.functional_unit}]')
item_dct['Other'].append(f'{other_ID}')
quantity = self.other_items[other_ID]['quantity']
item_dct['Quantity'].append(quantity)
quantity = quantity/sys_yr if annual else quantity
item_dct[f'Quantity{suffix}'].append(quantity)
for ind in self.indicators:
if ind.ID in other.CFs.keys():
impact = other.CFs[ind.ID]*quantity
item_dct[f'{ind.ID} [{ind.unit}]'].append(impact)
item_dct[f'{ind.ID} [{ind.unit}{suffix}]'].append(impact)
item_dct[f'Category {ind.ID} Ratio'].append(impact/tot[ind.ID])
else:
item_dct[f'{ind.ID} [{ind.unit}]'].append(0)
item_dct[f'{ind.ID} [{ind.unit}{suffix}]'].append(0)
item_dct[f'Category {ind.ID} Ratio'].append(0)

table = pd.DataFrame.from_dict(item_dct)
table.set_index(['Other'], inplace=True)
return self._append_cat_sum(table, cat, tot)
return _append_cat_sum(table, cat, tot, annual=annual)

raise ValueError(
'category can only be "Construction", "Transportation", "Stream", or "Other", ' \
Expand Down
3 changes: 2 additions & 1 deletion qsdsan/_sanstream.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,12 +170,13 @@ def copy_flow(self, other, IDs=..., *, remove=False, exclude=False):
--------
:func:`copy` for the differences between ``copy``, ``copy_like``, and ``copy_flow``.
'''
stream_impact_item = self.stream_impact_item
Stream.copy_flow(self, other=other, IDs=IDs, remove=remove, exclude=exclude)

if not isinstance(other, SanStream):
return

self._stream_impact_item = None
self._stream_impact_item = stream_impact_item


def flow_proxy(self, ID=None):
Expand Down
5 changes: 3 additions & 2 deletions qsdsan/_sanunit.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,10 +631,11 @@ def results(self, with_units=True, include_utilities=True,
include_total_cost=True, include_installed_cost=False,
include_zeros=True, external_utilities=(), key_hook=None):

if super().results is None: return super().results
results = super().results(with_units, include_utilities,
include_total_cost, include_installed_cost,
include_zeros, external_utilities, key_hook)
if not self.add_OPEX: self.add_OPEX = {'Additional OPEX': 0}
if not hasattr(self, 'add_OPEX'): self.add_OPEX = {'Additional OPEX': 0}
for k, v in self.add_OPEX.items():
if not with_units:
results.loc[(k, '')] = v
Expand All @@ -647,7 +648,7 @@ def results(self, with_units=True, include_utilities=True,
results.insert(0, 'Units', '')
results.loc[(k, ''), :] = ('USD/hr', v)
results.columns.name = type(self).__name__
if with_units:
if with_units and results is not None:
results.replace({'USD': f'{currency}', 'USD/hr': f'{currency}/hr'},
inplace=True)
return results
Expand Down
74 changes: 61 additions & 13 deletions qsdsan/sanunits/_combustion.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

# %%

import biosteam as bst
from warnings import warn
from flexsolve import IQ_interpolation
from biosteam import HeatUtility, Facility
Expand Down Expand Up @@ -184,6 +185,7 @@ def __init__(self, ID='', ins=None, outs=(), thermo=None, init_with='WasteStream
self.system = None
self.supplement_power_utility = supplement_power_utility
self._sys_heating_utilities = ()
self._sys_steam_utilities = ()
self._sys_power_utilities = ()

def _init_lca(self):
Expand Down Expand Up @@ -238,11 +240,11 @@ def react(natural_gas_flow=0):
# Calculate all energy needs in kJ/hr as in H_net_feeds
kwds = dict(system=self.system, operating_hours=self.system.operating_hours, exclude_units=(self,))
pu = self.power_utility
H_heating_needs = sum_system_utility(**kwds, utility='heating', result_unit='kJ/hr')/self.combustion_eff
H_steam_needs = sum_system_utility(**kwds, utility='steam', result_unit='kJ/hr')/self.combustion_eff
H_power_needs = sum_system_utility(**kwds, utility='power', result_unit='kJ/hr')/self.combined_eff

# Calculate the amount of energy needs to be provided
H_supp = H_heating_needs+H_power_needs if self.supplement_power_utility else H_heating_needs
H_supp = H_steam_needs+H_power_needs if self.supplement_power_utility else H_steam_needs

# Objective function to calculate the heat deficit at a given natural gas flow rate
def H_deficit_at_natural_gas_flow(flow):
Expand All @@ -263,17 +265,19 @@ def H_deficit_at_natural_gas_flow(flow):
H_net_feeds = react(0)

# Update heating utilities
self.heat_utilities = HeatUtility.sum_by_agent(sum(self.sys_heating_utilities.values(), ()))
self.steam_utilities = HeatUtility.sum_by_agent(sum(self.sys_steam_utilities.values(), []))
ngu = self.natural_gas_utilities = HeatUtility.sum_by_agent(sum(self.sys_natural_gas_utilities.values(), []))
self.heat_utilities = HeatUtility.sum_by_agent(sum(self.sys_heating_utilities.values(), []))
natural_gas.imol['CH4'] += sum(i.flow for i in ngu) # natural gas is added on separately
for hu in self.heat_utilities: hu.reverse()


# Power production if there is sufficient energy
if H_net_feeds <= H_heating_needs:
if H_net_feeds <= H_steam_needs:
pu.production = 0
else:
pu.production = (H_net_feeds-H_heating_needs)/3600*self.combined_eff
pu.production = (H_net_feeds-H_steam_needs)/3600*self.combined_eff

self.H_heating_needs = H_heating_needs
self.H_steam_needs = H_steam_needs
self.H_power_needs = H_power_needs
self.H_net_feeds = H_net_feeds

Expand Down Expand Up @@ -302,18 +306,52 @@ def _cost(self):
unit_CAPEX = self.unit_CAPEX
unit_CAPEX /= 3600 # convert to $ per kJ/hr
self.baseline_purchase_costs['CHP'] = unit_CAPEX * self.H_net_feeds

# Update biosteam utility costs
uprices = bst.stream_utility_prices
uprices['Fuel'] = uprices['Natural gas'] = self.ins[1].price
uprices['Ash disposal'] = self.outs[1].price

def _refresh_sys(self):
sys = self._system
ng_dct = self._sys_natural_gas_utilities = {}
steam_dct = self._sys_steam_utilities = {}
pu_dct = self._sys_power_utilities = {}
if sys:
units = [u for u in sys.units if u is not self]
hu_dct = self._sys_heating_utilities = {}
pu_dct = self._sys_power_utilities = {}
for u in units:
hu_dct[u.ID] = tuple([i for i in u.heat_utilities if i.duty*i.flow>0])
pu_dct[u.ID] = u.power_utility
pu = u.power_utility
if pu: pu_dct[u.ID] = pu
steam_utilities = []
for hu in u.heat_utilities:
if hu.flow*hu.duty <= 0: continue # cooling utilities
if hu.ID=='natural_gas': ng_dct[u.ID] = [hu]
elif 'steam' in hu.ID: steam_utilities.append(hu)
else: raise ValueError(f'The heating utility {hu.ID} is not recognized by the CHP.')
if steam_utilities: steam_dct[u.ID] = steam_utilities
sys_hus = {k:ng_dct.get(k,[])+steam_dct.get(k, [])
for k in list(ng_dct.keys())+list(steam_dct.keys())}
self._sys_heating_utilities = sys_hus

@property
def fuel_price(self):
'''
[Float] Price of fuel (natural gas), set to be the same as the price of ins[1]
and `bst.stream_utility_prices['Natural gas']`.
'''
return self.ins[1].price

natural_gas_price = fuel_price

@property
def ash_disposal_price(self):
'''
[Float] Price of ash disposal, set to be the same as the price of outs[1]
and `bst.stream_utility_prices['Ash disposal']`.
Negative means need to pay for ash disposal.
'''

"""[Float] Price of ash disposal, same as `bst.stream_utility_prices['Ash disposal']`."""
return self.outs[1].price

@property
def CHP_type(self):
Expand Down Expand Up @@ -400,9 +438,19 @@ def system(self, i):

@property
def sys_heating_utilities(self):
'''[dict] Heating utilities of the given system (excluding this CHP unit).'''
'''[dict] Heating utilities (steams and natural gas) of the given system (excluding this CHP unit).'''
return self._sys_heating_utilities

@property
def sys_natural_gas_utilities(self):
'''[dict] Steam utilities of the given system (excluding this CHP unit).'''
return self._sys_natural_gas_utilities

@property
def sys_steam_utilities(self):
'''[dict] Steam utilities of the given system (excluding this CHP unit).'''
return self._sys_steam_utilities

@property
def sys_power_utilities(self):
'''[dict] Power utilities of the given system (excluding this CHP unit).'''
Expand Down
Loading

0 comments on commit 30f7273

Please sign in to comment.