Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cumulative updates #126

Merged
merged 23 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bcfdc1a
more flexible coding on Reactor
yalinli2 Oct 17, 2024
beda378
fix bug related to `SanUnit.add_OPEX`
yalinli2 Oct 18, 2024
838cc63
add new inherited units from biosteam
yalinli2 Oct 18, 2024
ea1e790
more flexible setting in `Hydrocracking`
yalinli2 Oct 18, 2024
0164999
more flexible catalyst ID in hydroprocessing units
yalinli2 Oct 18, 2024
5e2d8f0
improve setting in CHP utility stream prices
yalinli2 Oct 21, 2024
0613ba9
fix minor doc issue
yalinli2 Oct 21, 2024
0be6a66
fix minor doc issue
yalinli2 Oct 21, 2024
30e918b
Merge branch 'main' of https://github.com/QSD-Group/QSDsan into beta
yalinli2 Oct 22, 2024
8914bfc
better steam vs. natural gas utility accounting in `CHP`
yalinli2 Oct 22, 2024
bce6a2b
fix minor bug in `Reactor`
yalinli2 Oct 23, 2024
117de0f
Merge branch 'main' of https://github.com/QSD-Group/QSDsan into beta
yalinli2 Oct 23, 2024
066f42f
fix bug for biosteam units without results
yalinli2 Oct 24, 2024
3f6be56
Merge branch 'main' of https://github.com/QSD-Group/QSDsan into beta
yalinli2 Oct 25, 2024
6bb5b85
add TEA indices
yalinli2 Oct 31, 2024
95f17c3
add TEA-related indices
yalinli2 Nov 3, 2024
7e6b99d
Merge branch 'main' of https://github.com/QSD-Group/QSDsan into beta
yalinli2 Nov 4, 2024
b494632
better solution for cleaning numba cache
yalinli2 Nov 5, 2024
cac89b3
fix bug in `SanStream.impact_item`
yalinli2 Nov 12, 2024
d9b4f4e
minor fix in LCA table formatting
yalinli2 Nov 12, 2024
ceb1439
fix `LCA` annual results
yalinli2 Nov 15, 2024
ae8b888
Merge branch 'main' of https://github.com/QSD-Group/QSDsan into beta
yalinli2 Nov 18, 2024
19486fa
more info on chemical indices
yalinli2 Nov 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading