SanUnit (basic)

Click the badge below to try this tutorial interactively in your browser:

Launch Binder

You can also run this tutorial inGoogle Colab. It takes a one-time setup per session: follow theColab instructions.

  • Prepared by:

  • Learning objectives. After this tutorial, you will be able to:

    • Instantiate existing SanUnit subclasses from qsdsan.unit_operations

    • Connect units via influent and effluent streams

    • Inspect a unit’s design and cost outputs

  • Prerequisites: 2. Component, 3. WasteStream

  • Covered topics:

      1. Understanding SanUnit/unit_operations/Unit

      1. Using existing SanUnit subclasses

Companion video. A walkthrough of this tutorial is available on YouTube, presented by Tori Morgan. Recorded against QSDsan v1.2.0. The concepts still apply, but if the code on screen differs from this notebook, follow the notebook.

Setup

Import QSDsan and confirm the installed version.

[1]:
import qsdsan as qs
print(f'This tutorial was made with qsdsan v{qs.__version__}.')
This tutorial was made with qsdsan v1.5.3.

1. Understanding SanUnit/unit_operations/Unit

The SanUnit class is used to model the design and operation of a unit operation through default or user-implemented algorithms. Upon simulation, it will generate the influent/effluent mass/energy flows and construction/operation inventories. It is the most extensible class in qsdsan and most directly relevant to new technologies.

In this tutorial, we will focus on how to use existing SanUnit classes, creation of new SanUnit subclasses will be covered in the next tutorial.

1.1. SanUnit and unit_operations

[2]:
# In qsdsan's top-level diretory, you will see two entries related to SanUnit:
qs.SanUnit
[2]:
qsdsan._sanunit.SanUnit

SanUnit is the class itself. The unit operations already built with qsdsan (subclasses of SanUnit) live in the unit_operations package, organized into three sub-namespaces (bst, static, dynamic). You access them as qs.<SanUnitName> or qs.unit_operations.<SanUnitName>, e.g. qs.PitLatrine or qs.unit_operations.PitLatrine. Similarly, qsdsan.equipments contains the pre-built Equipment classes, and qsdsan.process_models contains Process classes.

Note: qsdsan.sanunits is a legacy alias for qsdsan.unit_operations (just as qsdsan.processes aliases qsdsan.process_models). Both names point to the same package, but unit_operations is the current, preferred name.

Python Aside: modules, packages, and naming conventions (click to expand)

A few Python conventions explain the names you see:

  • Module vs. package. qs.SanUnit lives in a module (a single file, _sanunit.py); qs.unit_operations is a package (a directory with an __init__.py that wires up its contents). A directory is the same thing as a folder — just the more common programmatic name. The file path tells you which is which:

qs._sanunit         # .../qsdsan/_sanunit.py                 (a file)
qs.unit_operations  # .../qsdsan/unit_operations/__init__.py (a directory)
  • Dunder attributes. Names with leading/trailing double underscores (__path__, __name__, …) are Python built-ins; e.g. qs.unit_operations.__path__ gives the directory location. dir() lists these alongside the public names.

  • Leading underscore = private. A single leading underscore (as in _sanunit.py) marks something developers consider internal — you don’t use it directly, you just use SanUnit.

[3]:
# Hit Tab after `qs.unit_operations.` to autocomplete the available units,
# or list them with `dir`
# dir(qs.unit_operations)

1.2. SanUnit and Unit

SanUnit is a subclass with the Unit class in biosteam, like we mentioned in the previous tutorials, this means it inherits the attributes of the Unit class while having some new features (e.g., enables dynamic simulation, supports comprehensive LCA).

2. Using existing SanUnit subclasses

In Python terms, the “use” here means creating instances of a SanUnit subclass.

2.1. Instance initialization (creation) and simulation

[4]:
# Like always, we need to firstly tell `qsdsan` what components we will be working with,
# for demo purpose we will just use the default ones
cmps = qs.Components.load_default()
qs.set_thermo(cmps)
cmps.show()
CompiledComponents([
    S_H2,      S_CH4,       S_CH3OH,     S_Ac,
    S_Prop,    S_F,         S_U_Inf,     S_U_E,
    C_B_Subst, C_B_BAP,     C_B_UAP,     C_U_Inf,
    X_B_Subst, X_OHO_PHA,   X_GAO_PHA,   X_PAO_PHA,
    X_GAO_Gly, X_PAO_Gly,   X_OHO,       X_AOO,
    X_NOO,     X_AMO,       X_PAO,       X_MEOLO,
    X_FO,      X_ACO,       X_HMO,       X_PRO,
    X_U_Inf,   X_U_OHO_E,   X_U_PAO_E,   X_Ig_ISS,
    X_MgCO3,   X_CaCO3,     X_MAP,       X_HAP,
    X_HDP,     X_FePO4,     X_AlPO4,     X_AlOH,
    X_FeOH,    X_PAO_PP_Lo, X_PAO_PP_Hi, S_NH4,
    S_NO2,     S_NO3,       S_PO4,       S_K,
    S_Ca,      S_Mg,        S_CO3,       S_N2,
    S_O2,      S_CAT,       S_AN,        H2O,
])

Let’s firstly try using a Mixer, it is one of the most basic units, it takes N numbers of influents and the effluent is the mixture of all influents.

[5]:
# Make three random influents, I'm deliberately using different ways to make these streams
# as a recap previous tutorials

# Method 1: by directly providing the flow rates of select components
ins1 = qs.WasteStream('ins1', H2O=100)

# Method 2: using `copy` and adjust flow rates later
ins2 = ins1.copy('ins2')
ins2.imol['X_GAO_Gly'] = ins2.imol['X_GAO_PHA'] = 0.01

# Method 3: using default models
ins3 = qs.WasteStream.codstates_inf_model('ins3', flow_tot=50)
[6]:
# Use a shorthand to make our lives easier
su = qs.unit_operations

# This is the actual line used to initialize the instance,
# and we can pass the influents through the `ins` argument
M1 = su.Mixer(ins=(ins1, ins2, ins3))
[7]:
# Like many other classes, there is a `show` method
M1.show()
Mixer: M1
ins...
[0] ins1
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 1e+05 g/hr H2O
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 2.5 mmol/L
[1] ins2
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (g/hr): X_GAO_PHA  10
                X_GAO_Gly  10
                H2O        1e+05
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 2.5 mmol/L
     COD        : 199.4 mg/L
     BOD        : 115.6 mg/L
     TC         : 70.6 mg/L
     TOC        : 70.6 mg/L
     TSS        : 143.7 mg/L
[2] ins3
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (g/hr): S_F        4.3
                S_U_Inf    1.07
                C_B_Subst  2
                X_B_Subst  11.3
                X_U_Inf    2.79
                X_Ig_ISS   2.62
                S_NH4      1.25
                S_PO4      0.4
                S_K        1.4
                S_Ca       7
                S_Mg       2.5
                S_CO3      6
                S_N2       0.9
                S_CAT      0.15
                S_AN       0.6
                ...        4.98e+04
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 10.0 mmol/L
     COD        : 430.0 mg/L
     BOD        : 221.8 mg/L
     TC         : 265.0 mg/L
     TOC        : 137.6 mg/L
     TN         : 40.0 mg/L
     TP         : 10.0 mg/L
     TK         : 28.0 mg/L
     TSS        : 209.3 mg/L
outs...
[0] ws1
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
    WasteStream-specific properties: None for empty waste streams

Hold on — why is the effluent empty?

[8]:
# Well, we have to simulate the unit first
M1.simulate()
M1.show()
Mixer: M1
ins...
[0] ins1
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 1e+05 g/hr H2O
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 2.5 mmol/L
[1] ins2
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (g/hr): X_GAO_PHA  10
                X_GAO_Gly  10
                H2O        1e+05
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 2.5 mmol/L
     COD        : 199.4 mg/L
     BOD        : 115.6 mg/L
     TC         : 70.6 mg/L
     TOC        : 70.6 mg/L
     TSS        : 143.7 mg/L
[2] ins3
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (g/hr): S_F        4.3
                S_U_Inf    1.07
                C_B_Subst  2
                X_B_Subst  11.3
                X_U_Inf    2.79
                X_Ig_ISS   2.62
                S_NH4      1.25
                S_PO4      0.4
                S_K        1.4
                S_Ca       7
                S_Mg       2.5
                S_CO3      6
                S_N2       0.9
                S_CAT      0.15
                S_AN       0.6
                ...        4.98e+04
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 10.0 mmol/L
     COD        : 430.0 mg/L
     BOD        : 221.8 mg/L
     TC         : 265.0 mg/L
     TOC        : 137.6 mg/L
     TN         : 40.0 mg/L
     TP         : 10.0 mg/L
     TK         : 28.0 mg/L
     TSS        : 209.3 mg/L
outs...
[0] ws1
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (g/hr): S_F        4.3
                S_U_Inf    1.07
                C_B_Subst  2
                X_B_Subst  11.3
                X_GAO_PHA  10
                X_GAO_Gly  10
                X_U_Inf    2.79
                X_Ig_ISS   2.62
                S_NH4      1.25
                S_PO4      0.4
                S_K        1.4
                S_Ca       7
                S_Mg       2.5
                S_CO3      6
                S_N2       0.9
                ...        2.5e+05
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 4.0 mmol/L
     COD        : 165.6 mg/L
     BOD        : 90.5 mg/L
     TC         : 81.1 mg/L
     TOC        : 55.7 mg/L
     TN         : 8.0 mg/L
     TP         : 2.0 mg/L
     TK         : 5.6 mg/L
     TSS        : 99.3 mg/L

Aha! Now we are seeing the effluent being simulated.

[9]:
# If you recall the `mix_from` method of `WasteStream`,
# effluent of the `Mixer` is actually generated by it
ws4 = qs.WasteStream('ws4')
ws4.mix_from(M1.ins)
[10]:
# We can check if the flow rate of ws4 is the same as the effluent of the mixer M1
ws4.mass == M1.outs[0].mass
[10]:
sparse([ True,  True,  True,  True,  True,  True,  True,  True,  True,
         True,  True,  True,  True,  True,  True,  True,  True,  True,
         True,  True,  True,  True,  True,  True,  True,  True,  True,
         True,  True,  True,  True,  True,  True,  True,  True,  True,
         True,  True,  True,  True,  True,  True,  True,  True,  True,
         True,  True,  True,  True,  True,  True,  True,  True,  True,
         True,  True])
[11]:
# As you can see in the above examples,
# I use `ins` and `outs` to set/retrieve the influents and effluents of a unit
# Alternatively, there is the `diagram` method for a more intuitive look
M1.diagram()
../_images/tutorials_4_SanUnit_basic_24_0.svg

Tip: init_with sets which stream class each port uses: 'WasteStream' (default, or 'ws'), 'SanStream' ('ss'), or 'Stream' ('s'). Pass a single class name to apply it to every port, or a dict to set ports individually, which is handy when, say, the influents are WasteStream but a gas stream should be a plain Stream. Dict keys are ins/outs plus the port number (e.g., ins0, outs-1), a range (e.g., ins2:4), or else for any ports you did not name. Only WasteStream ports carry the wastewater properties from 3. WasteStream, while Stream ports are plain process streams.

[12]:
# A single class name applies to every port
M_ss = su.Mixer('M_ss', ins=('a', 'b'), outs='c', init_with='SanStream')
print('ins0:', type(M_ss.ins[0]).__name__, '| outs0:', type(M_ss.outs[0]).__name__)
ins0: SanStream | outs0: SanStream
[13]:
# A dict sets ports individually; 'else' covers the rest
M_mix = su.Mixer('M_mix', ins=('a', 'b'), outs='c',
                 init_with={'ins0': 'Stream', 'else': 'WasteStream'})
print('ins0:', type(M_mix.ins[0]).__name__,
      '| ins1:', type(M_mix.ins[1]).__name__,
      '| outs0:', type(M_mix.outs[0]).__name__)
ins0: Stream | ins1: WasteStream | outs0: WasteStream
c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\_stream.py:407: RuntimeWarning: <SanStream: a> has been replaced in registry
  self._register(ID)
c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\_stream.py:407: RuntimeWarning: <SanStream: b> has been replaced in registry
  self._register(ID)
c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\_stream.py:407: RuntimeWarning: <SanStream: c> has been replaced in registry
  self._register(ID)

Tip: By default, diagram() renders an inline picture of the flowsheet. You can pass a different format (notably format='html' for an interactive diagram with hover-able stream and unit information) or save it to a file with the file kwarg (e.g., M1.diagram(file='M1', format='png')). If diagram() produces nothing, you likely don’t have the graphviz package installed (it renders the diagrams); see QSDsan’s graphviz FAQ.

2.2. Retrieving design and cost

[14]:
# Note that `Mixer` does nothing other than mix the influents,
# let's using another example
qs.set_thermo(cmps) # here we need to reset the `cmps` since I introduced the `biosteam` environment
M2 = su.MixTank('M2', ins=(ins1, ins2, ins3), outs='M2out', # init_with='WasteStream',
                tau=1, kW_per_m3=1)
M2.show()
C:\Users\Yalin\AppData\Local\Temp\ipykernel_68680\3311111030.py:4: RuntimeWarning: undocked inlet ins1 from M1; ins1 is now docked at M2
  M2 = su.MixTank('M2', ins=(ins1, ins2, ins3), outs='M2out', # init_with='WasteStream',
C:\Users\Yalin\AppData\Local\Temp\ipykernel_68680\3311111030.py:4: RuntimeWarning: undocked inlet ins2 from M1; ins2 is now docked at M2
  M2 = su.MixTank('M2', ins=(ins1, ins2, ins3), outs='M2out', # init_with='WasteStream',
C:\Users\Yalin\AppData\Local\Temp\ipykernel_68680\3311111030.py:4: RuntimeWarning: undocked inlet ins3 from M1; ins3 is now docked at M2
  M2 = su.MixTank('M2', ins=(ins1, ins2, ins3), outs='M2out', # init_with='WasteStream',
MixTank: M2
ins...
[0] ins1
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 1e+05 g/hr H2O
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 2.5 mmol/L
[1] ins2
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (g/hr): X_GAO_PHA  10
                X_GAO_Gly  10
                H2O        1e+05
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 2.5 mmol/L
     COD        : 199.4 mg/L
     BOD        : 115.6 mg/L
     TC         : 70.6 mg/L
     TOC        : 70.6 mg/L
     TSS        : 143.7 mg/L
[2] ins3
phase: 'l', T: 298.15 K, P: 101325 Pa
flow (g/hr): S_F        4.3
                S_U_Inf    1.07
                C_B_Subst  2
                X_B_Subst  11.3
                X_U_Inf    2.79
                X_Ig_ISS   2.62
                S_NH4      1.25
                S_PO4      0.4
                S_K        1.4
                S_Ca       7
                S_Mg       2.5
                S_CO3      6
                S_N2       0.9
                S_CAT      0.15
                S_AN       0.6
                ...        4.98e+04
    WasteStream-specific properties:
     pH         : 7.0
     Alkalinity : 10.0 mmol/L
     COD        : 430.0 mg/L
     BOD        : 221.8 mg/L
     TC         : 265.0 mg/L
     TOC        : 137.6 mg/L
     TN         : 40.0 mg/L
     TP         : 10.0 mg/L
     TK         : 28.0 mg/L
     TSS        : 209.3 mg/L
outs...
[0] M2out
phase: 'l', T: 298.15 K, P: 101325 Pa
flow: 0
    WasteStream-specific properties: None for empty waste streams

Two things to take note of in the example above:

  • By setting outs='M2out', I set the ID of the effluent to be M2out

    • You can also make a stream ahead of time and set the effluent to that stream (e.g., outs=qs.WasteStream('M2out')

    • If the unit has multiple effluents, then you’ll want to use an Iterable (e.g., tuple, list), e.g., outs=['M2out1', qs.WasteStream('M2out2')]

    • This is applicable to ins as well

  • You will see warnings about streams being undocked from the previous unit and docked at the new unit, this is because we set the ins of M2 to be the same as the ins of M1. Since one stream can only go to one unit, these streams will be taken away from M1 and connect to M2

[15]:
# Because `M2` is a `MixTank`, we can look at its design
M2.simulate()  # runs _run then _summary (_design + _cost); see tutorial 5 §1.1
print(M2.results()) # you can see the design and capital/power cost
Mix tank                             Units       M2
Electricity         Power               kW    0.313
                    Cost            USD/hr   0.0245
Design              Residence time      hr        1
                    Total volume       m^3    0.313
Purchase cost       Tank               USD 7.09e+03
Total purchase cost                    USD 7.09e+03
Utility cost                        USD/hr   0.0245
[16]:
# If there is utility usage, it will be shown in the results as well
ws5 = qs.WasteStream('ws5', H2O=10, T=20)
H1 = su.HXutility(ins=ws5, T=50)
H1.simulate()
print(H1.results())
Heat exchanger                                            Units       H1
Low pressure steam  Duty                                  kJ/hr 3.18e+03
                    Flow                                kmol/hr   0.0822
                    Cost                                 USD/hr   0.0196
Design              Area                                   ft^2   0.0505
                    Overall heat transfer coefficient  kW/m^2/K      0.5
                    Log-mean temperature difference           K      377
                    Fouling correction factor                          1
                    Operating pressure                      psi       50
                    Total tube length                        ft    0.116
                    Inner pipe weight                        kg     0.12
                    Outer pipe weight                        kg    0.193
                    Total steel weight                       kg    0.313
Purchase cost       Double pipe                             USD     65.3
Total purchase cost                                         USD     65.3
Utility cost                                             USD/hr   0.0196
[17]:
# You can also retrieve information such as
M2.purchase_cost # this is the sum
[17]:
7093.933003892887
[18]:
M2.purchase_costs # this is a `dict` contains all the entries
[18]:
{'Tank': 7093.933003892887}
[19]:
# (this unit only has one cost item, but the pattern generalizes)
print(f'{M2.ID} contains the following cost items:')
for item_name, item_cost in M2.installed_costs.items():
    print(f'    {item_name} costs {item_cost:.0f}')
print(f'The sum of all these cost items is {M2.installed_cost:.0f}.')
M2 contains the following cost items:
    Tank costs 11705
The sum of all these cost items is 11705.

Baseline, purchase, and installed cost. Following the bare-module method (Seider et al.), BioSTEAM derives both costs from a baseline purchase cost (baseline_purchase_costs) using four per-item factors, each defaulting to 1:

  • F_D (design), F_P (pressure), and F_M (material) give the purchase cost: purchase = baseline × F_D × F_P × F_M.

  • F_BM (bare-module) rolls in installation, piping, instrumentation, etc. to give the installed cost: installed = baseline × (F_BM + F_D × F_P × F_M - 1).

Each factor lives in its own dict (F_BM, F_D, F_P, F_M). For the MixTank above, only F_M (2.0) and F_BM (2.3) differ from 1, so its installed cost is baseline × (2.3 + 2.0 - 1), not purchase × F_BM.

[20]:
M2.F_BM  # bare-module factor for each cost item
[20]:
{'Tank': 2.3}

Operating cost beyond utilities. The Additional OPEX row in results() is unit.add_OPEX — costs such as chemicals or labor, given as a float or a dict of USD/hr. The fraction of time a unit actually runs is unit.uptime_ratio (0–1), which scales these operating costs in TEA.

Tip: Some units may require specific components or certain number of influents, for example

[21]:
# You'll receive an `UndefinedComponent` error
bad_su = su.Excretion()
bad_su.simulate()
---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\_chemicals.py:1283, in CompiledChemicals._get_index_and_kind(self, key)
   1282     if key.__hash__ is None: key = tuple(key)
-> 1283     return index_cache[key]
   1284 except KeyError:

KeyError: 'NH3'

During handling of the above exception, another exception occurred:

KeyError                                  Traceback (most recent call last)
File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_components.py:751, in CompiledComponents.index(self, ID)
    750 '''Return index of specified component.'''
--> 751 try: return self._index[ID]
    752 except KeyError:

KeyError: 'NH3'

During handling of the above exception, another exception occurred:

UndefinedComponent                        Traceback (most recent call last)
Cell In[21], line 3
      1 # You'll receive an `UndefinedComponent` error
      2 bad_su = su.Excretion()
----> 3 bad_su.simulate()

File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_sanunit.py:436, in SanUnit.simulate(self, run, design_kwargs, cost_kwargs, **kwargs)
    408 def simulate(self, run=True, design_kwargs={}, cost_kwargs={}, **kwargs):
    409     '''
    410     Converge mass and energy flows, design, and cost the unit.
    411
   (...)    434     `scipy.integrate.solve_ivp <https://docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html>`_
    435     '''
--> 436     super().simulate(run=run, design_kwargs=design_kwargs, cost_kwargs=cost_kwargs)
    437     if self.isdynamic:
    438         sys = self._mock_dyn_sys

File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\biosteam\_unit.py:1464, in Unit.simulate(self, run, design_kwargs, cost_kwargs)
   1462     for ps in self._specifications: ps.compile_path(self)
   1463     self._load_stream_links()
-> 1464     self.run()
   1465 self._summary(design_kwargs, cost_kwargs)

File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\biosteam\_unit.py:48, in phenomena_based_run(self)
     46 def phenomena_based_run(self):
     47     if not (self._recycle_system and self._system.algorithm == 'Phenomena based'):
---> 48         Unit.run(self)
     49         return
     50     ins = self.ins

File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\network.py:1560, in AbstractUnit.run(self)
   1558     for i in self._outs: i.empty()
   1559     return
-> 1560 self._run_with_specifications()

File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\network.py:1576, in AbstractUnit._run_with_specifications(self)
   1574         if self.run_after_specifications: self._run()
   1575 else:
-> 1576     self._run()

File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\unit_operations\static\_excretion.py:85, in Excretion._run(self)
     81 factor = 24 * 1e3 # from g per person per day to kg per hour
     83 ur_N = (self.p_veg+self.p_anim)/factor*self.N_prot \
     84    * self.N_exc*self.N_ur*not_wasted
---> 85 ur.imass['NH3'] = ur_N * self.N_ur_NH3
     86 ur.imass['NonNH3'] = ur_N - ur.imass['NH3']
     88 ur.imass['P'] = (self.p_veg*self.P_prot_v+self.p_anim*self.P_prot_a)/factor \
     89     * self.P_exc*self.P_ur*not_wasted

File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\indexer.py:544, in ChemicalIndexer.__setitem__(self, key, data)
    543 def __setitem__(self, key, data):
--> 544     index, kind = self._chemicals._get_index_and_kind(key)
    545     if kind is None:
    546         reset_sparse_chemical_data(self.data, data)

File c:\Users\Yalin\Documents\Coding\QSDsan-platform\.venv\Lib\site-packages\thermosteam\_chemicals.py:1293, in CompiledChemicals._get_index_and_kind(self, key)
   1286 # [int|None] Kind of index:
   1287 # None - all
   1288 # 0 - chemical
   1289 # 1 - chemical group
   1290 # 2 - nested chemical group
   1291 # 3 - array
   1292 if isa(key, str):
-> 1293     index = self.index(key)
   1294     kind = 0 if isa(index, int) else 1
   1295 elif isa(key, tuple):

File ~\Documents\Coding\QSDsan-platform\QSDsan\qsdsan\_components.py:753, in CompiledComponents.index(self, ID)
    751 try: return self._index[ID]
    752 except KeyError:
--> 753     raise UndefinedComponent(ID)

UndefinedComponent: 'NH3'

For those units, the best way is to look at the documentation/examples (e.g., for the above case, check the bwaise system referenced in the Excretion documentation).


↑ Back to top