Skip to content

Commit 477848b

Browse files
committed
improve size_batch func and add documentation; also add N reactor option to bioreactos
1 parent 31c300b commit 477848b

8 files changed

Lines changed: 157 additions & 74 deletions

File tree

biosteam/_system.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3853,7 +3853,7 @@ def get_product_impact(self,
38533853
38543854
.. code-block:: python
38553855
3856-
>>> bst.settings.define_impact_indicator('GWP', 'kgCO2e')
3856+
>>> bst.settings.define_impact_indicator('GWP', 'kg*CO2e')
38573857
>>> system.get_property_allocated_impact(
38583858
... product, key='GWP', allocation_method='mass', basis='kg'
38593859
... ) # -> GWP [kgCO2e / kg-product]
@@ -3910,7 +3910,7 @@ def get_property_allocated_impact(self, key, name, basis=None, ignored=None, pro
39103910
39113911
.. code-block:: python
39123912
3913-
>>> bst.settings.define_impact_indicator('GWP', 'kgCO2e')
3913+
>>> bst.settings.define_impact_indicator('GWP', 'kg*CO2e')
39143914
>>> system.get_property_allocated_impact(
39153915
... key='GWP', name='mass', basis='kg'
39163916
... ) # -> GWP [kgCO2e / kg-products]

biosteam/units/abstract_stirred_tank_reactor.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ class AbstractStirredTankReactor(PressureVessel, Unit, isabstract=True):
6565
Defaults to `continuous`.
6666
tau_0 :
6767
Cleaning and unloading time (if batch mode). Defaults to 3 hr.
68-
N_reactors :
68+
N :
6969
Number of reactors.
7070
heat_exchanger_configuration :
7171
What kind of heat exchanger to default to (if any). Valid options include
@@ -77,6 +77,8 @@ class AbstractStirredTankReactor(PressureVessel, Unit, isabstract=True):
7777
loading_time :
7878
Loading time of batch reactor. If not given, it will assume each vessel is constantly
7979
being filled.
80+
N_vessels :
81+
Number of vessels.
8082
8183
Notes
8284
-----
@@ -232,6 +234,9 @@ class AbstractStirredTankReactor(PressureVessel, Unit, isabstract=True):
232234
#: Default maximum volume of a reactor in m3.
233235
V_max_default: float = 355
234236

237+
#: Default number of vessels.
238+
N_default: Optional[int] = None
239+
235240
#: Default length to diameter ratio.
236241
length_to_diameter_default: float = 3
237242

@@ -283,10 +288,16 @@ def _init(
283288
jacket_annular_diameter: Optional[float]=None,
284289
reactions: Optional[bst.ReactionSystem]=None,
285290
loading_time: Optional[float]=None,
291+
N: Optional[int]=None,
286292
):
287293
if adiabatic is None: adiabatic = False
288294
self.adiabatic = adiabatic
289295
self.reactions = reactions
296+
self.N = (
297+
self.N_default
298+
if (N is None and not adiabatic) else
299+
N
300+
)
290301
self.T = (
291302
self.T_default
292303
if (T is None and not adiabatic) else
@@ -414,23 +425,27 @@ def _design(self, size_only=False):
414425
tau_0 = self.tau_0
415426
V_wf = self.V_wf
416427
Design = self.design_results
417-
V_max = self.V_max
418-
N = v_0 / V_max / V_wf * (tau + tau_0) + 1
419-
if N < 2:
420-
N = 2
421-
else:
422-
N = ceil(N)
423-
Design.update(size_batch(v_0, tau, tau_0, N, V_wf, self.loading_time))
428+
Design.update(
429+
size_batch(
430+
v_0, tau, tau_0, V_wf,
431+
self.V_max, self.N, self.loading_time
432+
)
433+
)
424434
V_reactor = Design['Reactor volume']
435+
N = Design['Number of reactors']
425436
else:
426-
V_total = ins_F_vol * self.tau / self.V_wf
427-
N = ceil(V_total/self.V_max)
428-
if N == 0:
429-
V_reactor = 0
437+
if self.N is None:
438+
V_total = ins_F_vol * self.tau / self.V_wf
439+
N = ceil(V_total/self.V_max)
440+
if N == 0:
441+
V_reactor = 0
442+
else:
443+
V_reactor = V_total / N
430444
else:
431-
V_reactor = V_total / N
445+
V_total = ins_F_vol * self.tau / self.V_wf
446+
V_reactor = V_total / self.N
432447
Design['Reactor volume'] = V_reactor
433-
self.N_reactors = N
448+
Design['Number of reactors'] = N
434449
D = cylinder_diameter_from_volume(V_reactor, self.length_to_diameter)
435450
D *= 3.28084 # Convert from m to ft
436451
L = D * length_to_diameter

biosteam/units/aerated_bioreactor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ class AeratedBioreactor(AbstractStirredTankReactor):
9090
Defaults to `continuous`.
9191
tau_0 :
9292
Cleaning and unloading time (if batch mode). Defaults to 3 hr.
93-
N_reactors :
93+
N :
9494
Number of reactors.
9595
heat_exchanger_configuration :
9696
What kind of heat exchanger to default to (if any). Valid options include
@@ -550,7 +550,7 @@ class GasFedBioreactor(AbstractStirredTankReactor):
550550
Defaults to `continuous`.
551551
tau_0 :
552552
Cleaning and unloading time (if batch mode). Defaults to 3 hr.
553-
N_reactors :
553+
N :
554554
Number of reactors.
555555
heat_exchanger_configuration :
556556
What kind of heat exchanger to default to (if any). Valid options include

biosteam/units/anaerobic_bioreactor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class AnaerobicBioreactor(AbstractStirredTankReactor):
5656
Defaults to `continuous`.
5757
tau_0 :
5858
Cleaning and unloading time (if batch mode). Defaults to 3 hr.
59-
N_reactors :
59+
N :
6060
Number of reactors.
6161
heat_exchanger_configuration :
6262
What kind of heat exchanger to default to (if any). Valid options include

biosteam/units/design_tools/batch.py

Lines changed: 114 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@
99
General functional algorithms for batch design.
1010
1111
"""
12+
from math import ceil
13+
from flexsolve import wegstein
1214

1315
__all__ = ('size_batch',)
1416

15-
def size_batch(F_vol, tau_reaction, tau_cleaning, N_reactors, V_wf, loading_time=None) -> dict:
17+
def size_batch(F_vol, tau_reaction, tau_cleaning, V_wf,
18+
V_max=None, N_reactors=None, loading_time=None) -> dict:
1619
r"""
1720
Solve for batch reactor volume, cycle time, and loading time.
1821
@@ -24,10 +27,12 @@ def size_batch(F_vol, tau_reaction, tau_cleaning, N_reactors, V_wf, loading_time
2427
Reaction time.
2528
tau_cleaning : float
2629
Cleaning in place time.
27-
N_reactors : int
28-
Number of reactors.
2930
V_wf : float
3031
Fraction of working volume.
32+
V_max : int
33+
Maximum volume of a reactor.
34+
N_reactors : int
35+
Number of reactors.
3136
loading_time :
3237
Loading time of batch reactor. If not given, it will assume each vessel is constantly
3338
being filled.
@@ -44,24 +49,34 @@ def size_batch(F_vol, tau_reaction, tau_cleaning, N_reactors, V_wf, loading_time
4449
By assuming no downtime, the total volume of all reactors is:
4550
4651
.. math::
52+
4753
V_T = F_{vol}(\tau_{reaction} + \tau_{cleaning} + \tau_{loading})
4854
49-
where :math:`V_T` is the total volume of all reactors, :math:`F_{vol}` is the
55+
where :math:`V_T` is the total volume required, :math:`F_{vol}` is the
5056
volumetric flow rate of the feed, :math:`\tau_{reaction}` is the
5157
reaction time, :math:`\tau_{cleaning}` is the cleaning and unloading time,
5258
and :math:`\tau_{loading}` is the time required to load a vessel. This
5359
equation makes the conservative assumption that no reaction takes place
5460
when the tank is being filled.
5561
62+
The number of reactors is:
63+
64+
.. math::
65+
66+
N_{reactors} = ceil(\frac{V_T}{V_{max} \cdot V_{wf}})
67+
68+
where :math:`N_{reactors}` is the number of reactor vessels,
69+
:math:`V_{max}` is the maximum reactor volume, and
70+
:math:`V_{wf}` is the fraction of working volume in a reactor.
71+
5672
The working volume of an individual reactor is:
5773
5874
.. math::
5975
6076
V_{i,working} = \frac{V_T}{N_{reactors}}
6177
62-
where :math:`N_{reactors}` is the number of reactor vessels.
63-
64-
The time required to load a reactor (assuming no downtime) is:
78+
If there is no upstream storage and a vessel is constantly being filled,
79+
the time required to load a reactor is:
6580
6681
.. math::
6782
@@ -72,39 +87,116 @@ def size_batch(F_vol, tau_reaction, tau_cleaning, N_reactors, V_wf, loading_time
7287
.. math::
7388
7489
V_i = \frac{V_{i,working}}{f}
75-
76-
where f is the fraction of working volume in a reactor.
7790
7891
Plugging in and solving for the total volume, :math:`V_{T}`, we have:
7992
8093
.. math::
8194
8295
V_T = F_{vol}\frac{\tau_{reaction} + \tau_{cleaning}}{1 - \frac{1}{N_{reactors}}}
8396
84-
Using this equation, :math:`V_T` is first calculated, then :math:`V_{i, working}`,
85-
:math:`\tau_{loading}`, and :math:`V_i`.
97+
If the number of reactors is specified but not the loading time, :math:`V_T` is first calculated,
98+
then :math:`V_{i, working}`, :math:`\tau_{loading}`, and :math:`V_i`.
99+
100+
If neither the number of reactors nor the loading time are specified, we solve the equations iteratively
101+
until :math:`\tau_{loading}` converges.
102+
103+
If the loading time is given but not the number of reactors,
104+
then the equation for :math:`tau_{loading}` does not apply and
105+
we first compute :math:`N_reactors` given :math:`V_{max}.
86106
87107
Units of measure may vary so long as they are consistent. The loading time
88108
can be considered the cycle time in this scenario.
89109
110+
Examples
111+
--------
112+
Size batch given a maximum reactor volume of 1,000 m3 and zero loading time:
113+
114+
>>> from biosteam.units.design_tools import size_batch
115+
>>> F_vol = 1e3; tau_reaction = 30; tau_cleaning = 3; V_wf = 0.95
116+
>>> size_batch(
117+
... F_vol, tau_reaction, tau_cleaning, V_wf,
118+
... V_max=1e3, loading_time=0
119+
... )
120+
{'Reactor volume': 992.48,
121+
'Batch time': 33,
122+
'Loading time': 0,
123+
'Number of reactors': 35}
124+
125+
Size batch given 35 reactors and zero loading time:
126+
127+
>>> size_batch(
128+
... F_vol, tau_reaction, tau_cleaning, V_wf,
129+
... N_reactors=35, loading_time=0
130+
... )
131+
{'Reactor volume': 992.48,
132+
'Batch time': 33,
133+
'Loading time': 0,
134+
'Number of reactors': 35}
135+
136+
Size batch given a maximum reactor volume of 1,000 m3 and assume
137+
the constant loading:
138+
139+
>>> size_batch(
140+
... F_vol, tau_reaction, tau_cleaning, V_wf,
141+
... V_max=1000,
142+
... )
143+
{'Reactor volume': 992.4812030075188,
144+
'Batch time': 33.94285714285714,
145+
'Loading time': 0.9428571428571428,
146+
'Number of reactors': 36}
147+
148+
Size batch given 36 reactors and assume
149+
the constant loading:
150+
151+
>>> size_batch(
152+
... F_vol, tau_reaction, tau_cleaning, V_wf,
153+
... N_reactors=36
154+
... )
155+
{'Reactor volume': 992.4812030075188,
156+
'Batch time': 33.94285714285714,
157+
'Loading time': 0.9428571428571428,
158+
'Number of reactors': 36}
159+
160+
90161
"""
162+
if sum([i is None for i in [N_reactors, V_max]]) != 1:
163+
raise ValueError('must pass either `N_reactors` or `V_max`')
91164
if loading_time is None:
92-
# Total volume of all reactors, assuming no downtime
93-
V_T = F_vol * (tau_reaction + tau_cleaning) / (1 - 1 / N_reactors)
94-
95-
# Volume of an individual reactor
96-
V_i = V_T/N_reactors
97-
98-
# Time required to load a reactor
99-
tau_loading = V_i/F_vol
165+
if N_reactors is None:
166+
# Solve iteratively
167+
def f(tau_loading):
168+
N_reactors = ceil(F_vol * (tau_reaction + tau_cleaning + tau_loading) / (V_max * V_wf))
169+
if N_reactors == 1: N_reactors = 2 # Minimum
170+
V_T = F_vol * (tau_reaction + tau_cleaning) / (1 - 1 / N_reactors)
171+
V_i = V_T/N_reactors
172+
tau_loading = V_i/F_vol
173+
return tau_loading
174+
175+
tau_loading = wegstein(f, 0)
176+
N_reactors = ceil(F_vol * (tau_reaction + tau_cleaning + tau_loading) / (V_max * V_wf))
177+
V_T = F_vol * (tau_reaction + tau_cleaning + tau_loading)
178+
V_i = V_T / N_reactors
179+
else:
180+
# Total volume of all reactors, assuming no downtime
181+
V_T = F_vol * (tau_reaction + tau_cleaning) / (1 - 1 / N_reactors)
182+
183+
# Volume of an individual reactor
184+
V_i = V_T / N_reactors
185+
186+
# Time required to load a reactor
187+
tau_loading = V_i / F_vol
100188
else:
101189
tau_loading = loading_time
102190

191+
if N_reactors is None:
192+
# Number of reactors
193+
N_reactors = ceil(F_vol * (tau_reaction + tau_cleaning + tau_loading) / (V_max * V_wf))
194+
103195
# Total volume of all reactors
104196
V_T = F_vol * (tau_reaction + tau_cleaning + tau_loading)
105197

106198
# Volume of an individual reactor
107-
V_i = V_T/N_reactors
199+
V_i = V_T / N_reactors
108200

109201
# Total batch time
110202
tau_batch = tau_reaction + tau_cleaning + tau_loading
@@ -114,6 +206,7 @@ def size_batch(F_vol, tau_reaction, tau_cleaning, N_reactors, V_wf, loading_time
114206

115207
return {'Reactor volume': V_i,
116208
'Batch time': tau_batch,
117-
'Loading time': tau_loading}
209+
'Loading time': tau_loading,
210+
'Number of reactors': N_reactors}
118211

119212

biosteam/units/nrel_bioreactor.py

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,6 @@ class NRELAnaerobicBatchBioreactor(Unit, isabstract=True):
119119
'Recirculation flow rate': 'm3/hr'}
120120
_N_ins = _N_outs = 2
121121

122-
#: [bool] If True, number of reactors (N) is chosen as to minimize installation cost in every simulation. Otherwise, N remains constant.
123-
autoselect_N = False
124-
125122
#: [float] Cleaning and unloading time (hr).
126123
tau_0 = 3
127124

@@ -212,22 +209,6 @@ def tau(self):
212209
@tau.setter
213210
def tau(self, tau):
214211
self._tau = tau
215-
216-
@property
217-
def N_at_minimum_capital_cost(self):
218-
cost_old = np.inf
219-
self.autoselect_N = False
220-
self._N, N = 2, self._N
221-
cost_new = self.purchase_cost
222-
self._summary()
223-
while cost_new < cost_old:
224-
self._N += 1
225-
self._summary()
226-
cost_old = cost_new
227-
cost_new = self.purchase_cost
228-
self._N, N = N, self._N
229-
self.autoselect_N = True
230-
return N - 1
231212

232213
def _design(self):
233214
effluent = self.effluent
@@ -236,19 +217,13 @@ def _design(self):
236217
tau_0 = self.tau_0
237218
V_wf = self.V_wf
238219
Design = self.design_results
239-
if self.autoselect_N:
240-
N = self.N_at_minimum_capital_cost
241-
elif self.V_max:
242-
N = v_0 / self.V_max / V_wf * (tau + tau_0) + 1
243-
if N < 2:
244-
N = 2
245-
else:
246-
N = ceil(N)
247-
else:
248-
N = self._N
249-
Design.update(size_batch(v_0, tau, tau_0, N, V_wf, self.loading_time))
250-
Design['Number of reactors'] = N
251-
Design['Recirculation flow rate'] = v_0 / N
220+
Design.update(
221+
size_batch(
222+
v_0, tau, tau_0, V_wf,
223+
self.V_max, self.N, self.loading_time
224+
)
225+
)
226+
Design['Recirculation flow rate'] = v_0 / Design['Number of reactors']
252227
duty = self.Hnet
253228
Design['Reactor duty'] = duty
254229
self.add_heat_utility(duty, self.T)

0 commit comments

Comments
 (0)