Closed zasddsgg closed 7 months ago
@zasddsgg,
Thanks for submitting this issue. The issue is with BioSTEAM when a recycle single-phase stream changes to a recycle multi-phase stream, but here is a workaround (use a multistream instead):
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=0.3,
Ethanol=0.35,
Water=0.35,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream() # USE MULTISTEAM TO AVOID THE BUG
P101=units.Pump('P101', riboflavin, P=101325+10132.5)
M101=units.Mixer('M101', (P101-0, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, y_top=0.9, x_bot=0.001, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
P103=units.Pump('P103', D101-1, P=101325)
H101 = units.HXutility('H101', P103-0, T=400, rigorous=True)
S101 = units.Splitter('S101', H101-0, outs=('', recycle), split=0.01)
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.simulate()
Thanks!
Thank you for your advice. I added a process specification to ensure that the total amount of ethanol in the riboflavin
stream and P103
outlet stream is 350 kg/h (ethanol can be recycled), but after running, the mass of ethanol in the riboflavin
stream is negative, and the amount of ethanol in the M101
exit stream is not 350 kg/h I set, but 700, which is so strange. Could I consult you how to fix this bug?
The code is as follows:
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
Water=350,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream()
M101=units.Mixer('M101', (riboflavin, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, y_top=0.3, x_bot=0.001, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
H101 = units.HXutility('H101', D101-0, T=300, rigorous=True)
P103=units.Pump('P103', H101-0, recycle, P=101325)
@P103.add_specification(run=True)
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
riboflavin.imass['Ethanol'] = 350 - F_mass_Ethanol
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.simulate()
print(riboflavin.imass['Ethanol']) # return -349.84
A=P103-0
B=M101-0
print(A.imass['Ethanol']) # return 699.02
print(B.imass['Ethanol']) # return 699.92
If I change y_top=0.3
in D101
to y_top=0.9
, the following error will be reported, but the ethanol ratio at the top of the D101
tower seems to can reach 0.9, because the ratio of ethanol to water in the riboflavin
stream is close to 1:1.
Could I consult you why above error happens?
Thanks for your help. Wish you a good day.
The code is as follows:
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
Water=350,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream()
M101=units.Mixer('M101', (riboflavin, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, y_top=0.9, x_bot=0.001, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
# just change y_top=0.3 to y_top=0.9
H101 = units.HXutility('H101', D101-0, T=300, rigorous=True)
P103=units.Pump('P103', H101-0, recycle, P=101325)
@P103.add_specification(run=True)
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
riboflavin.imass['Ethanol'] = 350 - F_mass_Ethanol
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.simulate()
The error information is as follows:
UnboundLocalError Traceback (most recent call last)
Cell In[9], line 38
35 riboflavin.imass['Ethanol'] = 350 - F_mass_Ethanol
37 Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
---> 38 Riboflavin_sys.simulate()
File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_system.py:2332, in System.simulate(self, update_configuration, units, design_and_cost, **kwargs)
2312 def simulate(self, update_configuration: Optional[bool]=None, units=None,
2313 design_and_cost=None, **kwargs):
2314 """
2315 If system is dynamic, run the system dynamically. Otherwise, converge
2316 the path of unit operations to steady state. After running/converging
(...)
2330
2331 """
-> 2332 with self.flowsheet.temporary():
2333 specifications = self._specifications
2334 if specifications and not self._running_specifications:
File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_flowsheet.py:36, in TemporaryFlowsheet.__exit__(self, type, exception, traceback)
34 def __exit__(self, type, exception, traceback):
35 main_flowsheet.set_flowsheet(self.original)
---> 36 if exception: raise exception
File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_system.py:2388, in System.simulate(self, update_configuration, units, design_and_cost, **kwargs)
2382 outputs = self.simulate(
2383 update_configuration=True,
2384 design_and_cost=design_and_cost,
2385 **kwargs
2386 )
2387 else:
-> 2388 raise error
2389 else:
2390 if (not update_configuration # Avoid infinite loop
2391 and self._connections != [i.get_connection() for i in self.streams]):
2392 # Connections has been updated within simulation.
File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_system.py:2376, in System.simulate(self, update_configuration, units, design_and_cost, **kwargs)
2374 try:
2375 outputs = self.converge(**kwargs)
-> 2376 if design_and_cost: self._summary()
2377 except Exception as error:
2378 if update_configuration: raise error # Avoid infinite loop
File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_system.py:2089, in System._summary(self)
2087 if i in simulated_units: continue
2088 simulated_units.add(i)
-> 2089 f(i, i._summary)
2090 for i in self._facilities:
2091 if isa(i, Unit): f(i, i.simulate)
File D:\anaconda\envs\zddd\lib\site-packages\thermosteam\exceptions.py:90, in try_method_with_object_stamp(object, method, args)
88 raise StampedKeyError(message_with_object_stamp(object, repr(error.args[0])))
89 except Exception as error:
---> 90 raise_error_with_object_stamp(object, error)
File D:\anaconda\envs\zddd\lib\site-packages\thermosteam\exceptions.py:80, in raise_error_with_object_stamp(object, error)
78 error.args = (message_with_object_stamp(object, msg), *args)
79 except: pass
---> 80 raise error
File D:\anaconda\envs\zddd\lib\site-packages\thermosteam\exceptions.py:84, in try_method_with_object_stamp(object, method, args)
82 def try_method_with_object_stamp(object, method, args=()):
83 try:
---> 84 return method(*args)
85 except StampedKeyError as error:
86 raise StampedKeyError(message_with_object_stamp(object, error.args[0]))
File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_unit.py:1507, in Unit._summary(self, design_kwargs, cost_kwargs, lca_kwargs)
1505 if not (self._design or self._cost): return
1506 if not self._skip_simulation_when_inlets_are_empty or not all([i.isempty() for i in self._ins]):
-> 1507 self._design(**design_kwargs) if design_kwargs else self._design()
1508 self._cost(**cost_kwargs) if cost_kwargs else self._cost()
1509 self._lca(**lca_kwargs) if lca_kwargs else self._lca()
File D:\anaconda\envs\zddd\lib\site-packages\biosteam\units\distillation.py:1371, in BinaryDistillation._design(self)
1370 def _design(self):
-> 1371 self._run_McCabeThiele()
1372 self._run_condenser_and_reboiler()
1373 self._complete_distillation_column_design()
File D:\anaconda\envs\zddd\lib\site-packages\biosteam\units\distillation.py:1346, in BinaryDistillation._run_McCabeThiele(self)
1344 # Results
1345 Design = self.design_results
-> 1346 if error is None:
1347 Design['Theoretical feed stage'] = N_stages - feed_stage
1348 Design['Theoretical stages'] = N_stages
UnboundLocalError: <BinaryDistillation: D101> local variable 'error' referenced before assignment
@zasddsgg,
I just update biosteam so that the real error is raised. You should be getting the following error:
RuntimeError: cannot meet specifications! stages > 100
The reason you are getting this error is because the azeotrope is 91% by mol. Setting the vapor composition at 90% ethanol leads to unreasonably high number of stages (could be 1,000 or more!). Even at 87%, it may not make sense economically, but that value may serve as an upper bound for optimization purposes.
Thanks,
Thank you for your update. Could I trouble you have a look at this error?
I added a process specification to ensure that the total amount of ethanol in the riboflavin
stream and P103
outlet stream is 350 kg/h (ethanol can be recycled), but after running, the mass of ethanol in the riboflavin
stream is negative, and the amount of ethanol in the M101
exit stream is not 350 kg/h I set, but 700, which is so strange. Could I consult you how to fix this bug?
Thanks for your help. Wish you a good day.
The code is as follows:
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
Water=350,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream()
M101=units.Mixer('M101', (riboflavin, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, y_top=0.3, x_bot=0.001, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
H101 = units.HXutility('H101', D101-0, T=300, rigorous=True)
P103=units.Pump('P103', H101-0, recycle, P=101325)
@P103.add_specification(run=True)
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
riboflavin.imass['Ethanol'] = 350 - F_mass_Ethanol
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.simulate()
print(riboflavin.imass['Ethanol']) # return -349.84
A=P103-0
B=M101-0
print(A.imass['Ethanol']) # return 699.02
print(B.imass['Ethanol']) # return 699.92
@zasddsgg,
Please post new questions by opening new issues, unless they are directly related to the original issue.
The recycle flow rate of ethanol is an result of the system design (changing that value will not work because it will simply be overridden). You can vary design parameters such as distillation recovery or fresh feed flow rate to achieve a specification (e.g., using an analytical formulation or a numerical bounded specification as in https://biosteam.readthedocs.io/en/latest/tutorial/Process_specifications.html).
Also make sure your design specification is sensible. Aiming to recover 100% of the ethanol means that none of it will be in the bottoms product, which is impossible in particular given the azeotrope.
Thanks,
Thank you for your answer. I just don't understand why the amount of ethanol (699) in the recycle
stream is higher than the value set by the process specification (350), and it seems that the amount of ethanol in the recycle
stream should not be higher than the value set by the process specification.
In addition, the amount of ethanol in the feed stream riboflavin
is negative, which does not seem reasonable. Moreover, the amount of ethanol in the M101
outlet stream is 700, not 350 (which is the value set by the process specification)?
Remember that there is some accumulation with recycle streams in each loop until it converges. For example, if you are recycling 100% of the ethanol, your flow rate of ethanol in your recycle would go to infinity.
Please note that the specification you wrote is overriding a result of your model (so it does nothing) and just messes up the converging algorithm. You need to change an input parameter to achieve a target.
Also note that your recycle system is recycling the whole distillate. An overall mass balance around the feeds (i.e., riboflabin) and products (i.e., the distillation bottoms product) makes your product composition exactly equal to your feed composition (see code example below). So I'm not sure what is goal of this recycle.
Lastly, the bottoms composition at steady state (when the flowsheet is converged) will be exactly the composition of the feed riboflavin stream and can never be equal to the value you set at x_bot. So setting y_top and x_bot at the distillation column is infeasible and the system will not be able to converge with this design specification.
import biosteam as bst
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
Water=350,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream()
M101 = units.Mixer('M101', (riboflavin, recycle))
P102 = units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, Lr=0.8, Hr=0.8, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
H101 = units.HXutility('H101', D101-0, T=300, rigorous=True)
P103 = units.Pump('P103', H101-0, recycle, P=101325)
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.set_tolerance(rmol=1e-6, mol=1e-6)
for Lr in (0.5, 0.6, 0.7, 0.8):
D101.Lr = Lr
Riboflavin_sys.simulate()
print(
(((riboflavin.mol - D101.outs[1].mol) / riboflavin.mol) < 0.01).all() # This is always true
)
Thank you for your analysis. My goal is to recover the ethanol in the distillation column as a supplement to the ethanol in the feed stream riboflavin
, and at the same time control the ethanol mass of M101
outlet to a constant 350. But since I don't know what the amount of ethanol in the feed stream riboflavin
will be after the process converging, Therefore, I just set an initial value of 350 in the feed stream riboflavin
, and added a specification to control the quality of ethanol output of M101
to be constant at 350, so as to obtain the amount of ethanol in the feed stream riboflavin
. If I want to achieve this goal, could I consult you do you have any suggestions?
For “Please note that the specification you wrote is overriding a result of your model (so it does nothing) and just messes up the converging algorithm. You need to change an input parameter to achieve a target”, what does it mean? I understand Analytical specifications
is to control the target value to be realized through the set formula and obtain the quality of specific components in the stream. For example, in this case, I control the quality of M101
outlet ethanol to 350 through Analytical specifications
and the amount of ethanol in the feed stream riboflavin
is also obtained.
If y_top=0.3, x_bot=0.001
is set, the system does not report an error and can converge. But you mentioned that "So setting y_top and x_bot at the distillation column is infeasible and the system will not be able to converge with this design specification ", so even if the system does not report an error, does it mean that y_top=0.3, x_bot=0.001
is not feasible, and system will not be able to converge with this design specification (y_top=0.3, x_bot=0.001
)?
@zasddsgg,
I ran your system code with the recycle using y_top=0.3
and x_bot=0.001
and it does not converge... Not sure why you are getting different results. Regardless, I outline here in mathematical terms why changing the flow rate in the recycle is not sensible:
The steady state (converged) criteria is $f(x) = x$, where $f$ is a function that runs every unit operation within the recycle loop and $x$ are the flow rates, temperature, and pressure of the recycle stream.
Generally (ignoring special cases of reaction networks with multiple steady states), $x$ is a unique solution. If the recycle stream conditions are $a$ and $f(a) = a$ then $a = x$.
What you are trying to do is set $a \neq x$ and achieve $f(a) = a$, which is a contradiction because $a = x$ if that were true.
Moral of the story: Do not set flow rates that originate from a unit operation in a specification, only change fresh feeds and design inputs.
Thank you for your examples and explanations.
a) For “Do not set flow rates that originate from a unit operation in a specification, only change fresh feeds and design inputs.”, in https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park/blob/master/biorefineries/cellulosic/systems/fermentation/integrated_bioprocess.py#L71-L83, the specification uses the flow rate of recycle stream (feed.F_mass) and recycle stream is actually the flow rate that originates from R303
. Besides, in https://biosteam.readthedocs.io/en/latest/tutorial/Process_specifications.html#:~:text=Let%E2%80%99s%20say%20we%20have%20a%20mixture%20of%20water%2C%20ethanol%20and%20propanol%20and%20we%20would%20like%20to%20evaporate%2050%25%20of%20the%20liquid%20by%20mass%20(not%20by%20mol).%20We%20can%20solve%20this%20problem%20numerically%20by%20testing%20whether%20the%20specification%20is%20met%20at%20a%20given%20temperature%3A, temperature is adjusted to control vapor.F_mass / feed.F_mass
is 0.5, and vapor.F_mass
is also low rates that originate from Flash
. So I am a little confused about “Do not set flow rates that originate from a unit operation in a specification, only change fresh feeds and design inputs.”.
b) What I want to do is to ensure that the total amount of ethanol in the outlet stream of M101
is 350, which is composed of the feed stream riboflavin
and recycle
stream. However, since I do not know the amount of ethanol in the recycle stream after operation, I don't know how much ethanol is in the feed stream riboflavin
, so I'm going to arbitrarily set an initial value for the amount of ethanol in the feed stream riboflavin
, Therefore, I want to control the amount of ethanol in the feed stream riboflavin
through the specification, so as to ensure that the total amount of ethanol in the outlet stream of M101
is 350. If I want to achieve this goal, could I consult you if you have some suggestions?
c) For y_top=0.3 and x_bot=0.001
, verision I use is biosteam 2.38.3, thermosteam 0.38.2, biorefineries 2.26.2
. Processes can converge and output results. For "I ran your system code with the recycle using y_top=0.3 and x_bot=0.001 and it does not converge", do you mean there is an error alerts? Could I consult you as long as the process converges and the process can output results, it means there is no problem?
The code I use is as follows:
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
Water=350,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream()
M101=units.Mixer('M101', (riboflavin, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, y_top=0.3, x_bot=0.001, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
H101 = units.HXutility('H101', D101-0, T=300, rigorous=True)
P103=units.Pump('P103', H101-0, recycle, P=101325)
@P103.add_specification(run=True)
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
riboflavin.imass['Ethanol'] = 350 - F_mass_Ethanol
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.simulate()
print(riboflavin.imass['Ethanol']) # return -349.84
A=P103-0
B=M101-0
print(A.imass['Ethanol']) # return 699.02
print(B.imass['Ethanol']) # return 699.92
@zasddsgg,
I misread your process specification. I thought you were changing the flow rate of your recycle, but you are just changing the flow rated of your feed (which is OK). Sorry for the misunderstanding! I hope I did not confuse you too much.
The error I get was from running the code without the specification (I should have noted this). With the specification, it converges the first time but then fails the second time. You were missing a parameter in your process specification called impacted_units
to note that you are changing the feed from another unit (the mixer) through the specification:
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
Water=350,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream()
M101=units.Mixer('M101', (riboflavin, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, y_top=0.3, x_bot=0.001, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
H101 = units.HXutility('H101', D101-0, T=300, rigorous=True)
P103=units.Pump('P103', H101-0, recycle, P=101325)
@P103.add_specification(run=True, impacted_units=[M101]) # ADD IMPACTED UNITS
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
riboflavin.imass['Ethanol'] = 350 - F_mass_Ethanol
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.simulate()
print(riboflavin.imass['Ethanol']) # return -349.84
A=P103-0
B=M101-0
print(A.imass['Ethanol']) # return 699.02
print(B.imass['Ethanol']) # return 699.92
This code will error due to the negative value.
So it's OK to change flow rates streams that do not originate from a unit operations (i.e., fresh feeds) like in the first github example. It is also OK to vary to change the operating conditions of your unit operation (like in the second github example). Flow rates from recycle streams are not changed.
b) You can try the following:
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
Water=350,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream()
M101=units.Mixer('M101', (riboflavin, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, Lr=0.9, Hr=0.9, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
H101 = units.HXutility('H101', D101-0, T=300, rigorous=True)
P103=units.Pump('P103', H101-0, recycle, P=101325)
@P103.add_specification(run=True, impacted_units=[M101]) # ADD IMPACTED UNITS
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
riboflavin.imass['Ethanol'] = max(350 - F_mass_Ethanol, 0)
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.set_tolerance(rmol=1e-6, mol=1e-6)
Riboflavin_sys.simulate()
print(riboflavin.imass['Ethanol']) # return -349.84
A=P103-0
B=M101-0
print(A.imass['Ethanol']) # return 315
print(B.imass['Ethanol']) # return 350
c) The problem was that you did not specify the impacted units when changing feeds to units other than the process specification unit (you changed the feed to the mixer but the specification was at the pump). This system does not actually converge.
It can be difficult to understand all the requirements for process specifications. Even I got confused. I hope this helps!
Thanks,
@zasddsgg, I updated my answer b above. I hope this helps!
Thank you for your advice and help.
a) For analytical specifications and numerical specifications in tutorial (https://biosteam.readthedocs.io/en/latest/tutorial/Process_specifications.html), if the @ unit is the same as the unit whose feed to be changed by analytical specifications or numerical specifications, I don't need to add impact_unit. Do I understand that right? If the @ unit is not the unit whose feed to be changed by analytical specifications or numerical specifications, I need to add impact_unit and impact_unit is the unit that the changed feed stream enters into. Do I understand that right? If the @ unit is not the unit whose feed to be changed by analytical specifications or numerical specifications, the process result is not correct even if it converges. Do I understand that right?
b) For “Riboflavin_sys.set_tolerance(rmol=1e-6, mol=1e-6)” in the above answer b
, what is its role? Add it or not, the ethanol flow of riboflavin
results seems not exactly the same.
For “riboflavin.imass['Ethanol'] = max(350 - F_mass_Ethanol, 0)”, if the ethanol in recycle stream
is more than 350, isn't the M101
outlet still higher than 350, even though ethanol in riboflavin
stream is controlled to be zero ethanol by “riboflavin.imass['Ethanol'] = max(350 - F_mass_Ethanol, 0)”?
I also do not understand that the ethanol of M101
outlet has been controlled to 350, and the impact_unit has been added. Why does ethanol in recycle
stream sometimes exceed 350?
c) For “With the specification, it converges the first time but then fails the second time”, could I consult you which code you are using (I have not encountered this case)? If the process converges the first time, does it mean that the process is fine, so why does it report an error when running the second time?
d) May I ask you whether as long as the process converges and no error message is reported (except the case when the impact_unit
was not added in the specifications), it means that the process result is no problem?
a) You are correct except for the special case of a bounded_numerical_specification
where you must run the units within your objective function using <upstream unit>.run_until(<downstream unit>)
(https://biosteam.readthedocs.io/en/latest/API/Unit.html#biosteam.Unit.run_until)
b) A process is converged once the error is below the tolerance. BioSTEAM defaults to a tolerance more suitable to an high-rate industrial process. I decreased the tolerance so that the result looks like it is exactly 350.
You actually don't need the max
in this case. The following works too!
@P103.add_specification(run=True, impacted_units=[M101]) # ADD IMPACTED UNITS
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
riboflavin.imass['Ethanol'] = 350 - F_mass_Ethanol
However, you might need max
later because BioSTEAM's internal solver may overshoot (guess a recycle flow rate too high). The internal solver (e.g., Wegnstein or Aitken-Steffensen) helps speed up convergence, but may guess values too high sometimes.
The impacted_units
is important because your are adjusting a flow rate of a another unit because BioSTEAM must change the order of running unit operations (and sometimes even include another loop) to ensure the flow of information is correct.
c) I meant to write "without the specification". Here is the code I was using:
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
Water=350,
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream()
M101=units.Mixer('M101', (riboflavin, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
D101 = units.BinaryDistillation('D101', P102-0, P=10132.5, y_top=0.3, x_bot=0.001, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
H101 = units.HXutility('H101', D101-0, T=300, rigorous=True)
P103=units.Pump('P103', H101-0, recycle, P=101325)
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.simulate()
Riboflavin_sys.simulate()
d) That is correct.
Thanks,
Thanks for your answer.
a) For the code in above answer c, why use twice Riboflavin_sys.simulate()
? Do I need to use Riboflavin_sys.simulate()
twice when building my own process?
When I run this code, it doesn't converge the first time (report error: RuntimeError: <System: Riboflavin_sys> could not converge Highest convergence error among components in recycle stream P103-0 after 200 loops
) and stops before the second Riboflavin_sys.simulate()
(i.e. the second Riboflavin_sys.simulate() was not run
), not the first time converge and the second time not converge, which is different from you.
b) If the ethanol in the recycle
stream is more than 350 and ethanol in riboflavin
stream is 0, then ethanol in M101
outlet is not 350 (but higher than 350), so it still cannot achieve 350 of ethanol in M101
outlet?
c) For other processes I build, is it OK to specify only one unit as impact_unit
in analytical specifications
, or do I need to specify more than one unit as impact_unit
? Do I need to add run_until
in analytical specifications
?
d) For other processes I build, do I need to add impact_unit
in bounded_numerical_specification
?
e) For other processes I build, must run_until
be added in the bounded_numerical_specification
? Is it OK not to add? Does not
add run_until
mean that the whole process runs within bounded_numerical_specification
?
f) For other processes I build, should I add inclusive=True
when using <upstream unit>.run_until(<downstream unit>)
? Does inclusive=True
indicate whether the running of bounded_numerical_specification
include upstream unit and downstream unit? When should I set inclusive=True
and when should I set inclusive=False
in bounded_numerical_specification
?
g) In terms of other processes I build, for <unit a>.run_until(<unit b>)
in bounded_numerical_specification
, how to choose unit a and unit b
? If the argument x (such as V
in F301.V = V
in https://biosteam.readthedocs.io/en/latest/tutorial/Building_a_biorefinery.html#:~:text=(V)%3A-,F301.V%20%3D%20V,-F301.run_until) to be adjusted is located in unit a
, then @ unit a
, and code is unit a.run_until (unit b)
, then if the target parameter (such as sugar_concentration
in https://biosteam.readthedocs.io/en/latest/tutorial/Building_a_biorefinery.html#:~:text=sugar_concentration%20%3D%20M301.outs%5B0%5D.get_mass_fraction(%27Sugar%27)) is at the outlet of which unit, which unit is unit b
, and finally set inclusive=True
, do I understand that right?
@zasddsgg,
a) I get the same as you. You never need to simulate twice. The simulate twice code was for when your process specification was missing impacted units. In this manner, simulating twice could help you check if anything is wrong with your code. b) You are correct but that is only the case when the process is not in steady state (zero ethanol feed would mean there is no ethanol in the process at steady state). Try printing the flow rate to see it converge:
@P103.add_specification(run=True, impacted_units=[M101]) # ADD IMPACTED UNITS
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
print(F_mass_Ethanol)
riboflavin.imass['Ethanol'] = 350 - F_mass_Ethanol
c) You need to specify all the units being directly impacted (which can be 0, 1, or more). You don't need to use run_until in analytical specifications.
d) No, that is not an argument to bounded specs.
e) If you are changing inputs to another unit operation and do not use run_until
, your results will be wrong.
f) You can set inclusive=True if you are unsure. The first unit is always included.
g) Yes, you understand well. unit_a
should be the unit you are changing inputs and unit_b
is a unit you are using to get results for you specification. For example, your mixer would be unit_a
and the distillation column would be unit_b
in a bounded specification.
Thanks,
Thanks for your answer.
a) According to your said “The internal solver (e.g., Wegnstein or Aitken-Steffensen) helps speed up convergence, but may guess values too high sometimes”, if the ethanol in the recycle stream after convergence is higher than 350 and the ethanol in riboflavin stream is zero, then the ethanol in M101 outlet cannot achieve 350 (high than 350). Could I consult you do you have any suggestions?
b) Could I confirm with you the following questions? The code is at the end.
b.i) Does the specification for adjusting Flash or pump pressure need to add impact_unit? If so, is the code @P102.add_specification(run=True, impacted_units=[H101]), @F101.add_specification(run=True, impacted_units=[P104, D101]), @P104.add_specification(run=True, impacted_units=[H103]), @P103.add_specification(run=True, impacted_units=[M101]) def adjust_pump_pressure_P103():
right?
Or is the code @P102.add_specification(run=True, impacted_units=[P102]), @F101.add_specification(run=True, impacted_units=[F101]), @P104.add_specification(run=True, impacted_units=[P104]), @P103.add_specification(run=True, impacted_units=[P103]) def adjust_pump_pressure_P103():
right?
Since for changing the pressure of the pump or Flash through analytical specifications
, I'm not sure the impact_unit is pump or Flash themselves or the units that use pump or Flash’s outlet stream as the inlet stream.
b.ii) Like F101
, there are two affected streams (F101’s gas stream and liquid stream), which are connected to different units, so is the impact unit written like this: @F101.add_specification(run=True, impacted_units=[P104, D101])
(i.e. the units connected to F101’s gas stream and liquid stream are both taken as impact_units)? How to determine the relationship between adjusted stream (i.e. F101’s gas stream and liquid stream) and impact_unit, i.e., is there an order of units in impacted_units=[P104, D101]
? Or is impacted_units=[P104, D101]
the same as impacted_units=[D101, P104]
? For other similar situations, there are multiple adjusted streams, so have multiple affected units, is there a sequence for unit a,b,c in impacted_units=[a, b, c]
, i.e. is impacted_units=[a, b, c]
the same as impacted_units=[a, c, b]
, and impacted_units=[b, c, a]
? Does the order of the unit a,b,c in impacted_units=[]
correspond to the order in which the adjusted streams appear in analytical specifications?
b.iii) Is it OK to use one unit as the impact_unit of the previous specification and also as the @ unit of the next specification? For example, P104
b.iv) If two specifications @ are the same unit, such as @P103
, can impact_unit not be the same?
For example, assume there is another unit such as Storgetank (S101)
between P103
and M101
, in @P103.add_specification(run=True, impacted_units=[M101]), def adjust_ethanol_flow(): , impacted_units
is M101
, while in @P103.add_specification(run=True), def adjust_pump_pressure_P103():
, impact_unit
is S101
, so code is @P103.add_specification(run=True, impacted_units=[M101]), def adjust_ethanol_flow():, @P103.add_specification(run=True, impacted_units=[S101]), def adjust_pump_pressure_P103():
, is it ok?
b.v) If there is more than one specification in a process, can the impact_unit
not be the same? Such as M101
in @P103.add_specification(run=True, impacted_units=[M101])
and H101
in @P102.add_specification(run=True, impacted_units=[H101])
, M101
and H101
is not the same unit, is it ok?
b.vi) In the same specification, there are two affected streams connected to the same unit. Is it OK to write the impact_unit
once? For example, in @P103.add_specification(run=True, impacted_units=[M101])
, stream riboflavin
and stream Water
both affect M101
, So in impacted_units=[M101]
of @P103.add_specification(run=True, impacted_units=[M101]), M101
is only written once, not impacted_units=[M101, M101]
, is it ok?
c) How to choose which unit is @ in analytical specification? Sometimes unit which is @ is the unit to be adjusted, sometimes it is the mixer, and sometimes it is the pump for recycle stream.
d.i) How can I determine which unit is directly affected in analytical specification, for example, in @P103.add_specification(run=True, impacted_units=[M101])
, all units behind M101
seem to be also affected. Is this done by using the unit that uses the stream changed by analytical specification as the inlet stream as impact_unit?
d.ii) If two streams are changed in a specification, are both unit a and unit b
that use two streams as inlet streams respectively taken as impact_unit? Is this form impacted_units=[a, b]
used when more than one impact_unit is involved?
d.iii) For “You need to specify all the units being directly impacted (which can be 0, 1, or more)”, what does it mean that the impact_unit
is 0? If there are no units after P104
, is this the case where impact_unit
is 0? Do I still need to add impact_unit
when the impact_unit
is 0?
d.iv) It seems that unit that is @ and impact_unit
cannot be the same unit? Is it right? Do I still need to add impact_unit
if the unit that is @ and impact_unit
are the same unit?
e) Does the unit that is @ and unit in unit.run_until
in bounded_numerical_specification
refer to the unit that the adjusted argument (such as V in F301.V = V in https://biosteam.readthedocs.io/en/latest/tutorial/Building_a_biorefinery.html#:~:text=(V)%3A-,F301.V%20%3D%20V,-F301.run_until) is located in?
f) As in the tutorial (https://biosteam.readthedocs.io/en/latest/tutorial/Process_specifications.html#:~:text=def%20f(x)%3A%0A%20%20%20%20%23%20Objective%20function%20where%20f(x)%20%3D%200%20at%20a%0A%20%20%20%20%23%20vapor%20fraction%20of%2050%20wt.%20%25.%0A%20%20%20%20F1.T%20%3D%20x%0A%20%20%20%20F1.run()), If I want to adjust the input of a unit so that the outlet gas fraction of the unit is 0.5, do I need to add unit.run_until
(there are other units in the process)? If not, do I need to add unit.run
(such as F1.run
in the tutorial)? Does either unit.run_until
or unit.run
must be added?
g) If like in the tutorial (https://biosteam.readthedocs.io/en/latest/tutorial/Building_a_biorefinery.html#:~:text=%40F301.add_bounded_numerical_specification(x0%3D0%2C%20x1%3D1%2C%20xtol%3D1e%2D5%2C%20ytol%3D1e%2D2)), I want to control the output of another unit by adjusting the input of one unit, I must add unit.run_until
, is it right?
h) Is it better to set inclusive=True
than inclusive=False
when using unit.run_until
in bounded_numerical_specification
?
The code is as follows:
import biosteam as bst
bst.nbtutorial()
from biosteam import settings
from biosteam import units
import thermosteam as tmo
Riboflavin = bst.Chemical('Riboflavin', Hf=-55700, Tc=650+273.15, Tb=240+273.15, Hvap=70000)
Riboflavin.mu.l.add_method(f=0.0003259750718128194)
Ethanol = bst.Chemical('Ethanol')
Water = bst.Chemical('Water')
bst.settings.set_thermo([Riboflavin, Ethanol, Water])
riboflavin = bst.Stream(
ID='riboflavin',
price=0.0916,
total_flow=1000,
units='kg/hr',
Riboflavin=300,
Ethanol=350,
T=100+273.15,
P=101325,
)
Water = bst.Stream(
ID='Water',
price=0.0916,
total_flow=350,
Water=1,
units='kg/hr',
T=100+273.15,
P=101325,
)
recycle=bst.MultiStream('recycle')
M101=units.Mixer('M101', (riboflavin, Water, recycle))
P102=units.Pump('P102', M101-0, P=101325*2)
@P102.add_specification(run=True)
def adjust_pump_pressure_P102():
P102.P = riboflavin.P + 10132.5
H101 = units.HXutility('H101', P102-0, T=370, rigorous=True)
F101 = units.Flash('F101', H101-0, outs=('F101_vapor', 'F101_liquid'), P=10132.5, Q=0)
@F101.add_specification(run=True)
def adjust_flash_pressure_F101():
a = H101-0
F101.P = a.P
P104=units.Pump('P104', F101-0, P=101325)
H103 = units.HXutility('H103', P104-0, T=298)
@P104.add_specification(run=True)
def adjust_pump_pressure_P104():
a = F101-0
P104.P = a.P + 10132.5
D101 = units.BinaryDistillation('D101', F101-1, P=10132.5, Lr=0.9, Hr=0.9, k=1.2, Rmin=0.001, LHK=('Ethanol', 'Water'))
H102 = units.HXutility('H102', D101-0, T=300, rigorous=True)
P103=units.Pump('P103', H102-0, recycle, P=101325)
@P103.add_specification(run=True, impacted_units=[M101])
def adjust_ethanol_flow():
F_mass_Ethanol = recycle.imass['Ethanol']
F_mass_Water = recycle.imass['Water']
riboflavin.imass['Ethanol'] = max(350 - F_mass_Ethanol, 0)
Water.imass['Water'] = 350 - F_mass_Water
@P103.add_specification(run=True)
def adjust_pump_pressure_P103():
a = H102-0
P103.P = a.P + 10132.5
Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
Riboflavin_sys.simulate()
Riboflavin_sys.diagram('thorough', format='png')
Hello, I encountered the following error when creating recycle stream, could I consult you how to solve it? I tried to remove unit
H101
and the error disappeared, but actuallyH101
was needed in the process. Thanks for your help. Wish you a good day.The code is as follows:
The error information is as follows: