BioSTEAMDevelopmentGroup / Bioindustrial-Park

BioSTEAM's Premier Repository for Biorefinery Models and Results
MIT License
36 stars 17 forks source link

Recycle error due to Stream changing to MultiStream #96

Closed zasddsgg closed 7 months ago

zasddsgg commented 7 months ago

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 actually H101 was needed in the process. 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=0.3,
    Ethanol=0.35,
    Water=0.35,
    T=100+273.15,
    P=101325,
)
recycle=bst.Stream()

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()

The error information is as follows:

ValueError                                Traceback (most recent call last)
Cell In[1], line 36
     33 S101 = units.Splitter('S101', H101-0, outs=('', recycle), split=0.01)
     35 Riboflavin_sys = bst.main_flowsheet.create_system('Riboflavin_sys')
---> 36 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:2375, in System.simulate(self, update_configuration, units, design_and_cost, **kwargs)
   2373 else:
   2374     try:
-> 2375         outputs = self.converge(**kwargs)
   2376         if design_and_cost: self._summary()
   2377     except Exception as error:

File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_system.py:2075, in System.converge(self, recycle_data, update_recycle_data)
   2073     for i in range(self._N_runs): method()
   2074 else:
-> 2075     method()
   2076 if update_recycle_data:
   2077     try: recycle_data.update()

File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_system.py:2019, in System.run(self)
   2017 for i in self._path:
   2018     if isa(i, Unit): f(i, i.run)
-> 2019     elif isa(i, System): f(i, i.converge)
   2020     else: i()

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\_system.py:2075, in System.converge(self, recycle_data, update_recycle_data)
   2073     for i in range(self._N_runs): method()
   2074 else:
-> 2075     method()
   2076 if update_recycle_data:
   2077     try: recycle_data.update()

File D:\anaconda\envs\zddd\lib\site-packages\biosteam\_system.py:2028, in System._solve(self)
   2026 data = self._get_recycle_data()
   2027 f = self._iter_run_conditional if conditional else self._iter_run
-> 2028 try: solver(f, data, **kwargs)
   2029 except IndexError as error:
   2030     data = self._get_recycle_data()

File D:\anaconda\envs\zddd\lib\site-packages\flexsolve\iterative_solvers.py:145, in conditional_aitken(f, x)
    143     if not condition: return g
    144     gg, condition = f(g)
--> 145     x = aitken_iter(x, gg, x - g, gg - g)
    146 return x

ValueError: <System: SYS1> operands could not be broadcast together with shapes (5,) (8,) 
yoelcortes commented 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!

zasddsgg commented 7 months ago

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
zasddsgg commented 7 months ago

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
yoelcortes commented 7 months ago

@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,

zasddsgg commented 7 months ago

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
yoelcortes commented 7 months ago

@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,

zasddsgg commented 7 months ago

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)?

yoelcortes commented 7 months ago

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
    )
zasddsgg commented 7 months ago

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)?

yoelcortes commented 7 months ago

@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.

zasddsgg commented 7 months ago

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
yoelcortes commented 7 months ago

@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,

yoelcortes commented 7 months ago

@zasddsgg, I updated my answer b above. I hope this helps!

zasddsgg commented 7 months ago

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?

yoelcortes commented 7 months ago

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,

zasddsgg commented 7 months ago

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?

yoelcortes commented 7 months ago

@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,

zasddsgg commented 7 months ago

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')