@@ -75,6 +75,8 @@ class AeratedBioreactor(AbstractStirredTankReactor):
7575 Operating pressure [Pa].
7676 V_wf :
7777 Fraction of working volume over total volume. Defaults to 0.8.
78+ length_to_diameter :
79+ Length to diameter ratio of bioreactor.
7880 V_max :
7981 Maximum volume of a reactor [m3]. Defaults to 355.
8082 kW_per_m3 :
@@ -205,15 +207,23 @@ def _init(
205207 AbstractStirredTankReactor ._init (self , ** kwargs )
206208 self .theta_O2 = theta_O2 # Average concentration of O2 in the liquid as a fraction of saturation.
207209 self .Q_O2_consumption = Q_O2_consumption # Forced duty per O2 consummed [kJ/kmol].
208- self .optimize_power = True if optimize_power is None else optimize_power
209210 if design is None :
210- design = 'Stirred tank'
211+ if self .kW_per_m3 == 0 :
212+ design = 'Bubble column'
213+ else :
214+ design = 'Stirred tank'
211215 elif design not in aeration .kLa_method_names :
212216 raise ValueError (
213217 f"{ design !r} is not a valid design; only "
214218 f"{ list (aeration .kLa_method_names )} are valid"
215219 )
216220 self .design = design
221+ self .optimize_power = (
222+ design == 'Stirred tank'
223+ if optimize_power is None else
224+ optimize_power
225+ )
226+ self .design = design
217227 if method is None :
218228 method = self .default_methods [design ]
219229 if (key := (design , method )) in aeration .kLa_methods :
@@ -271,7 +281,10 @@ def get_agitation_power(self, kLa):
271281 self .superficial_gas_flow = U = F / A # m / s
272282 return aeration .P_at_kLa_Riet (kLa , V , U , ** self .kLa_kwargs )
273283 else :
274- raise NotImplementedError ('kLa method has not been implemented in BioSTEAM yet' )
284+ raise NotImplementedError (
285+ 'cannot solve for the required agitation power using the '
286+ f'kLa method { self .kLa !r} in BioSTEAM yet'
287+ )
275288
276289 def _get_duty (self ):
277290 if self .Q_O2_consumption is None :
@@ -368,11 +381,31 @@ def air_flow_rate_objective(O2):
368381 return OUR - self .get_OTR ()
369382
370383 f = air_flow_rate_objective
384+ x0 = OUR
371385 y0 = air_flow_rate_objective (OUR )
372- if y0 <= 0. : # Correlation is not perfect and special cases lead to OTR > OUR
373- return
374- flx .IQ_interpolation (f , x0 = OUR , x1 = 10 * OUR ,
375- y0 = y0 , ytol = 1e-3 , xtol = 1e-3 )
386+
387+ # Correlation is not perfect and special cases lead to OTR > OUR
388+ if y0 <= 0. : return
389+
390+ f = air_flow_rate_objective
391+ x1 = 10 * OUR
392+ y1 = air_flow_rate_objective (x1 )
393+
394+ # It is possible an infinite flow rate of air is not enough to
395+ # satisfy mass transfer if the titer is super high or gas O2 concentration
396+ # is too low.
397+ if y1 > 0 :
398+ raise RuntimeError (
399+ 'bioreactor conversion/titer cannot be satisfied; '
400+ 'even an infinite flow rate of gas is not enough'
401+ )
402+
403+ # There is a known solution because y1 < 0 < y0
404+ flx .IQ_interpolation (
405+ f , x0 = x0 , x1 = x1 ,
406+ y0 = y0 , y1 = y1 ,
407+ ytol = 1e-3 , xtol = 1e-3
408+ )
376409
377410 def _run_reactions (self , effluent ):
378411 self .reactions .force_reaction (effluent )
@@ -422,7 +455,7 @@ def get_OTR(self):
422455 return OTR
423456
424457 def _inlet_air_pressure (self ):
425- AbstractStirredTankReactor ._design (self )
458+ AbstractStirredTankReactor ._design (self , size_only = True )
426459 liquid = bst .Stream (None , thermo = self .thermo )
427460 liquid .mix_from ([i for i in self .ins if i .phase != 'g' ], energy_balance = False )
428461 liquid .copy_thermal_condition (self .outs [0 ])
@@ -433,14 +466,7 @@ def _inlet_air_pressure(self):
433466 def _design (self ):
434467 AbstractStirredTankReactor ._design (self )
435468 if self .air .isempty (): return
436- liquid = bst .Stream (None , thermo = self .thermo )
437- liquid .mix_from ([i for i in self .ins if i .phase != 'g' ], energy_balance = False )
438- liquid .copy_thermal_condition (self .outs [0 ])
439- rho = liquid .rho
440- length = self .get_design_result ('Length' , 'm' ) * self .V_wf
441- compressor = self .compressor
442- compressor .P = g * rho * length + 101325
443- compressor .simulate ()
469+ self .compressor .simulate ()
444470 air_cooler = self .air_cooler
445471 air_cooler .T = self .T
446472 air_cooler .simulate ()
@@ -508,6 +534,8 @@ class GasFedBioreactor(AbstractStirredTankReactor):
508534 Operating pressure [Pa].
509535 V_wf :
510536 Fraction of working volume over total volume. Defaults to 0.8.
537+ length_to_diameter :
538+ Length to diameter ratio of bioreactor.
511539 V_max :
512540 Maximum volume of a reactor [m3]. Defaults to 355.
513541 kW_per_m3 :
@@ -762,7 +790,8 @@ def _run(self):
762790 vent .T = effluent .T = self .T
763791 vent .empty ()
764792 vent .phase = 'g'
765- if not self .titer :
793+ if not self .titer :
794+ # Titer is given by the mass transfer.
766795 liquid_feeds = [i for i in self .ins if i .phase != 'g' ]
767796 T = self .T
768797 P = self ._inlet_gas_pressure ()
@@ -776,6 +805,7 @@ def _run(self):
776805 vent .empty ()
777806 self ._run_vent (vent , effluent )
778807 elif variable_gas_feeds :
808+ # Solve gas flow rates to meet titer.
779809 liquid_feeds = [i for i in self .ins if i .phase != 'g' ]
780810 effluent .mix_from (liquid_feeds , energy_balance = False )
781811 T = self .T
@@ -806,7 +836,13 @@ def load_flow_rates(F_feeds):
806836
807837 baseline_feed = bst .Stream .sum (self .normal_gas_feeds , energy_balance = False )
808838 baseline_flows = baseline_feed .get_flow ('mol/s' , self .gas_substrates )
809- bounds = np .array ([[max (1.01 * SURs [i ] - baseline_flows [i ], 1e-6 ), 10 * SURs [i ]] for i in index ])
839+
840+ # Bounds must meet substrate uptake rate (minimally).
841+ # At most, 10x the substrate uptake rate as an abritrarily high number.
842+ bounds = np .array ([
843+ [max (1.01 * SURs [i ] - baseline_flows [i ], 1e-6 ), 10 * SURs [i ]]
844+ for i in index
845+ ])
810846 if self .optimize_power :
811847 def total_power_at_substrate_flow (F_substrates ):
812848 load_flow_rates (F_substrates )
@@ -837,8 +873,13 @@ def gas_flow_rate_objective(F_substrates):
837873 bounds = bounds .T
838874 results = least_squares (f , 1.2 * SURs , bounds = bounds , ftol = SURs .min () * 1e-6 )
839875 self ._results = results
876+ if not results .success :
877+ raise RuntimeError (
878+ 'bioreactor conversion/titer could not be satisfied'
879+ )
840880 load_flow_rates (results .x / x_substrates )
841- else :
881+ else :
882+ # Titer given, must adjust liquid flows to meet mass transfer.
842883 try :
843884 feed , = [i for i in self .ins if i .phase != 'g' ]
844885 except :
@@ -871,8 +912,6 @@ def f(vent_flow_rates):
871912 return effluent .imass [product ] / effluent .ivol ['Water' ] - titer
872913
873914 flx .IQ_interpolation (liquid_flow_rate_objective , 0.05 * F_liquid_max , F_liquid_max , ytol = 1e-3 )
874- # self.show()
875- # breakpoint()
876915
877916 def _run_reactions (self , effluent ):
878917 data = effluent .get_data ()
0 commit comments