BioSTEAMDevelopmentGroup / Bioindustrial-Park

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

Request for reactor heat duty #63

Closed zasddsgg closed 1 year ago

zasddsgg commented 1 year ago

Hello, I built a reactor through class PretreatmentReactorSystem, but after the operation I found net duty of the reactor is zero. In fact, ethanol, sucrose reaction with oxygen should be exothermic reaction, but net duty shows 0. May I ask why this happened? Thank you a lot for your help. The code is as follows:

import biosteam as bst bst.nbtutorial() from biosteam.units.decorators import cost from biosteam import Unit from biosteam import units from biosteam.units.design_tools.geometry import cylinder_diameter_from_volume from thermosteam import MultiStream import thermosteam as tmo from biorefineries import cellulosic cs = cellulosic.Biorefinery('corn stover ethanol') chemicals_cs = cs.chemicals bst.settings.set_thermo(chemicals_cs)

bst.main_flowsheet.set_flowsheet('corn_ethanol') Rxn = tmo.reaction.Reaction ParallelRxn = tmo.reaction.ParallelReaction corn = bst.Stream( ID='corn', price=0.0516, units='kg/hr', Acetate=0.0635, O2=434.6, Protein=0.01, Ethanol=43.84, Sucrose=28.31, Arabinan=0.0476, Mannan=0.0069, Galactan=0.0159, )

@cost('Dry flow rate', 'Pretreatment reactor system', units='kg/hr', S=83333, CE=522, cost=19812400 * 0.993, n=0.6, kW=4578, BM=1.5) class PretreatmentReactorSystem(bst.units.design_tools.PressureVessel, Unit): _N_ins = 1 _N_outs = 2 _graphics = bst.Flash._graphics _units = {'Residence time': 'hr', 'Reactor volume': 'm3'}

def __init__(self, ID='', ins=None, outs=(), T=120+273.15, V=0.5, thermo=None, 
             tau=0.166, V_wf=0.8, length_to_diameter=2, 
             vessel_material='Stainless steel 316', 
             vessel_type='Horizontal',
             reactions=None,
             run_vle=True):
    Unit.__init__(self, ID, ins, outs, thermo)
    self._load_components()
    vapor, liquid = self.outs
    vapor.phase = 'g'
    self.T = T
    self.V = V
    chemicals = self.chemicals
    if reactions is None:
        self.reactions = ParallelRxn([
    #            Reaction definition                 Reactant    Conversion
    Rxn('Ethanol + 3O2 -> 2CO2 + 3H2O',              'Ethanol',   0.990),
    Rxn('Sucrose + 12O2 -> 12CO2 + 11H2O',           'Sucrose',   0.30)
        ])
        self.glucan_to_glucose = self.reactions[0]
        self.glucose_to_byproducts = self.reactions[1]
    else:
        self.reactions = reactions
    self.tau = tau
    self.V_wf = V_wf
    self.length_to_diameter = length_to_diameter
    self.vessel_material = vessel_material
    self.vessel_type = vessel_type
    self.run_vle = run_vle

def _load_components(self):
    thermo = self.thermo
    self._multistream = MultiStream(None, thermo=thermo)

def _run(self):
    feed = self.ins[0]
    vapor, liquid = self.outs
    liquid.copy_like(feed)
    self.reactions(liquid) 

    if self.T:
        if self.run_vle:
            ms = self._multistream
            ms.copy_like(liquid)
            ms.vle(T=self.T, V=self.V)
            vapor.mol[:] = ms.imol['g']
            liquid.mol[:] = ms.imol['l']
            vapor.T = liquid.T = ms.T
            vapor.P = liquid.P = ms.P
        else:
            liquid.T = self.T

def _design(self):
    Design = self.design_results
    ins_F_vol = self.F_vol_in
    V_reactor = ins_F_vol * self.tau / self.V_wf
    P = self.outs[0].P * 0.000145038 
    length_to_diameter = self.length_to_diameter
    D = cylinder_diameter_from_volume(V_reactor, self.length_to_diameter)
    D *= 3.28084 
    L = D * length_to_diameter
    Design['Residence time'] = self.tau
    Design['Reactor volume'] = V_reactor
    Design.update(self._vessel_design(float(P), float(D), float(L)))
    self._decorated_design()

def _cost(self):
    Design = self.design_results
    self.baseline_purchase_costs.update(
        self._vessel_purchase_cost(
            Design['Weight'], Design['Diameter'], Design['Length']
        )
    )
    self._decorated_cost()

R201 = PretreatmentReactorSystem('R201', corn) corn_sys = bst.main_flowsheet.create_system('corn_sys') corn_sys.simulate() R201.net_duty

yoelcortes commented 1 year ago

Duties and utilities are registered through HeatUtility objects, add the following line in _design or in _cost to add a heat utility:

self.add_heat_utility(duty, T_operation)

For more information, checkout: https://biosteam.readthedocs.io/en/latest/tutorial/Inheriting_from_Unit.html https://biosteam.readthedocs.io/en/latest/API/Unit.html#biosteam.Unit.add_heat_utility

Thanks,

zasddsgg commented 1 year ago

Thank you for your advice and help. The problem has been solved. Thank you again. Wish you a good day.

yoelcortes commented 1 year ago

@zasddsgg, that's great. Something I forgot to mention, don't forget to account for heats of formation (in addition to sensible and latent heats). Unit properties like, H_in and H_out don't include heats of formation. Stream.H does not either.

For heats of formation (without sensible and latent heats), use Unit.Hf_in, Unit.Hf_out, and Stream.Hf.

Thanks!

zasddsgg commented 1 year ago

Thanks for your reminding, but I am a little confused about net_duty and Hnet. Hnet should be H_out - H_in + Hf_out - Hf_in ((https://biosteam.readthedocs.io/en/latest/_modules/biosteam/_unit.html#Unit.show)). But how is net_duty calculated? Net_duty seems to add all duty in heat_utilities, but how is duty in heat_utilities calculated? Net_duty seems to take into account heat transfer losses, what is the coefficient of heat transfer losses by default? Or other, what's the difference between net_duty and Hnet? Is net_duty used to calculate utility flow? If so, what is Hnet used for? Besides, isn't the enthalpy of inflow calculated by the enthalpy of formation of 25 degrees plus Cp times the change in temperature (enthalpy of inflow=[Hf(25 degrees) + CpΔT]flow rate), and then times the flow? So why doesn't the enthalpy flow include Hf? Thank you for your help. Wish you a good day.

yoelcortes commented 1 year ago

@zasddsgg, here are the answers to your questions:

  1. But how is net_duty calculated? Net_duty seems to add all duty in heat_utilities, but how is duty in heat_utilities calculated? The HeatUtility.unit_duty is given using simple enthalpy balance with streams. For example, for HXutility it is self.outs[0].H - self.ins[0].H. For the CSTR it is self.Hnet. The HeatUtility.duty is the unit_duty divided by the heat_transfer_efficiency.
  2. Net_duty seems to take into account heat transfer losses, what is the coefficient of heat transfer losses by default? Net duty includes heat transfer losses, Hnet does not. Every heating agent has a heat transfer efficiency. The defaults are 95, 90, and 85% for low, medium, and high pressure steam (higher pressure and temperature lead to greater losses to environment). Cooling agents have 100% efficiency. Net duty is not used for anything within biosteam... it is mainly for users.
  3. Besides, isn't the enthalpy of inflow calculated by the enthalpy of formation of 25 degrees plus Cp times the change in temperature (enthalpy of inflow=[Hf(25 degrees) + CpΔT]flow rate), and then times the flow? So why doesn't the enthalpy flow include Hf? Enthalpy is always relative (unlike entropy, there is no absolute zero) and depends on the Href at the Tref. Aspen HYSIS uses the Hf as Href at 25 C, but different process simulators have different Href and Tref. We simply use 0 as Href and 298.15 as Tref (in which case Hf must be used whenever accounting for reactions). We can potentially use Hf as Href, but this would break convention in BioSTEAM and lead to errors.

Thanks

zasddsgg commented 1 year ago

Thank you for your answer. May I confirm with you whether my understanding is correct? Unit_duty refers to Hnet. When there is no reaction, such as in heat exchanger or Boiler, Hnet is obtained by self.outs. H-self.ins. H (H includes sensible and latent heats). When reaction occurs, Hnet is obtained by self.outs.H - self.ins.H+self.outs. Hf-self.ins.Hf. After obtaining Hnet (unit_duty), then divide Hnet (or unit_duty) by heat transfer efficiency to get the duty of utility. Then add the duty of all utility to get Net_duty.

In addition, I would like to consult you whether duty in self.add_heat_utility(duty, T_operation) refers to unit_duty? Does T_operation refer to the temperature of unit outlet? When defining Boiler (https://biosteam.readthedocs.io/en/latest/tutorial/Inheriting_from_Unit.html), why does temperature of utility inlet reduce Boiler outlet flow temperature to get dT? If use utility to heat Boiler, shouldn't use temperature of utility inlet to reduce Boiler inlet flow temperature to get dT? Also in add_heat_utility tutorial (https://biosteam.readthedocs.io/en/latest/API/Unit.html), add_heat_utility(unit_duty, T_in, T_out=None, agent=None, heat_transfer_efficiency=None, hxn_ok=False), seems to define T_in. So does T_operation in self.add_heat_utility(duty, T_operation) refer to T_in(the temperature at the entry stream of the unit)? Does T_out=None means that we don't have to give T_out? If not give, is the default value adopted? Does heat_transfer_efficiency=None means that if heat_transfer_efficiency is not set, is the default value for heat_transfer_efficiency used (e.g. 0.95 for low pressure steam)?

Also, if I want to view the utility flow for R201, could you help me check that is there anything wrong in the following code. In addition, may I ask you why the utility cost is 0 with the following code? Thank you a lot for your help. The code is as follows:

import biosteam as bst
bst.nbtutorial()
from biosteam.units.decorators import cost
from biosteam import Unit
from biosteam import units
from biosteam.units.design_tools.geometry import cylinder_diameter_from_volume
from thermosteam import MultiStream
import thermosteam as tmo
from biorefineries import cellulosic
cs = cellulosic.Biorefinery('corn stover ethanol')
chemicals_cs = cs.chemicals
bst.settings.set_thermo(chemicals_cs)
bst.main_flowsheet.set_flowsheet('corn_ethanol')
Rxn = tmo.reaction.Reaction
ParallelRxn = tmo.reaction.ParallelReaction
corn = bst.Stream(
ID='corn',
price=0.0516,
units='kg/hr',
Acetate=0.0635,
O2=434.6,
Protein=0.01,
Ethanol=43.84,
Sucrose=28.31,
Arabinan=0.0476,
Mannan=0.0069,
Galactan=0.0159,
)
@cost('Dry flow rate', 'Pretreatment reactor system', units='kg/hr',
S=83333, CE=522, cost=19812400 * 0.993, n=0.6, kW=4578, BM=1.5)
class PretreatmentReactorSystem(bst.units.design_tools.PressureVessel, Unit):
  _N_ins = 1
  _N_outs = 2
  _graphics = bst.Flash._graphics
  _units = {'Residence time': 'hr',
  'Reactor volume': 'm3'}
  def __init__(self, ID='', ins=None, outs=(), T=120+273.15, V=0.5, thermo=None, 
               tau=0.166, V_wf=0.8, length_to_diameter=2, 
               vessel_material='Stainless steel 316', 
               vessel_type='Horizontal',
               reactions=None,
               run_vle=True):
      Unit.__init__(self, ID, ins, outs, thermo)
      self._load_components()
      vapor, liquid = self.outs
      vapor.phase = 'g'
      self.T = T
      self.V = V
      chemicals = self.chemicals
      if reactions is None:
          self.reactions = ParallelRxn([
      #            Reaction definition                 Reactant    Conversion
      Rxn('Ethanol + 3O2 -> 2CO2 + 3H2O',              'Ethanol',   0.990),
      Rxn('Sucrose + 12O2 -> 12CO2 + 11H2O',           'Sucrose',   0.30)
          ])
          self.glucan_to_glucose = self.reactions[0]
          self.glucose_to_byproducts = self.reactions[1]
      else:
          self.reactions = reactions
      self.tau = tau
      self.V_wf = V_wf
      self.length_to_diameter = length_to_diameter
      self.vessel_material = vessel_material
      self.vessel_type = vessel_type
      self.run_vle = run_vle

  def _load_components(self):
      thermo = self.thermo
      self._multistream = MultiStream(None, thermo=thermo)

  def _run(self):
      feed = self.ins[0]
      vapor, liquid = self.outs
      liquid.copy_like(feed)
      self.reactions(liquid) 

      if self.T:
          if self.run_vle:
              ms = self._multistream
              ms.copy_like(liquid)
              ms.vle(T=self.T, V=self.V)
              vapor.mol[:] = ms.imol['g']
              liquid.mol[:] = ms.imol['l']
              vapor.T = liquid.T = ms.T
              vapor.P = liquid.P = ms.P
          else:
              liquid.T = self.T

  def _design(self):
      Design = self.design_results
      T_operation = self.ins[0].T
      duty = self.H_out - self.H_in + self.Hf_out - self.Hf_in
      ins_F_vol = self.F_vol_in
      V_reactor = ins_F_vol * self.tau / self.V_wf
      P = self.outs[0].P * 0.000145038 
      length_to_diameter = self.length_to_diameter
      D = cylinder_diameter_from_volume(V_reactor, self.length_to_diameter)
      D *= 3.28084 
      L = D * length_to_diameter
      Design['Residence time'] = self.tau
      Design['Reactor volume'] = V_reactor
      Design.update(self._vessel_design(float(P), float(D), float(L)))
  self._decorated_design()
  self.add_heat_utility(duty, T_operation)

  def _cost(self):
      Design = self.design_results
      self.baseline_purchase_costs.update(
          self._vessel_purchase_cost(
              Design['Weight'], Design['Diameter'], Design['Length']
          )
      )
      self._decorated_cost()
R201 = PretreatmentReactorSystem('R201', corn)
corn_sys = bst.main_flowsheet.create_system('corn_sys')
corn_sys.simulate()
print(R201.results())
yoelcortes commented 1 year ago

@zasddsgg, the assertions in your first paragraph are correct in a general sense.

Regarding add_heat_utility, yes unit_duty is given by user: https://biosteam.readthedocs.io/en/latest/API/Unit.html#biosteam.Unit.add_heat_utility

Regarding your question on calculating dT. The log-mean temperature difference (LMTD) may be a better choice for the boiler. I may fix this later. However, for CSTRs and tanks, only T_operation (your outlet temperature) matters, not the inlet. Remember that heat utilities assume countercurrent operation. For more details, please read the source code and docs: https://biosteam.readthedocs.io/en/latest/API/HeatUtility.html

You are loading the complete biorefinery in the previous code. The price of utility agents are set to zero because they are all produced on-site. Please adjust the prices of your utility agents or simply not load the biorefinery:

from biorefineries import cellulosic as cs
# Do not run this line
# cs = cellulosic.Biorefinery('corn stover ethanol') 
chemicals_cs = cs.create_cellulosic_ethanol_chemicals()

Don't forget to adjust your process settings before simulation: https://biosteam.readthedocs.io/en/latest/tutorial/Getting_started.html#Process-settings

Thanks!

zasddsgg commented 1 year ago

Thank you for your answer and reminding. For CSTRs and tanks, whether it is endothermic or exothermic, does T_operation both refer to the temperature of the outlet stream of unit (CSTRs and tanks)?

If I want to check the utility duty and flow of CSTRs and tanks, is it using the following code? duty = self.H_out - self.H_in + self.Hf_out - self.Hf_in(reaction occurs) or duty = self.H_out - self.H_in (no reaction occurs) T_operation = self.outs[0].T (CSTRs and tanks outlet stream temperature) self.add_heat_utility(duty, T_operation)

In add_heat_utility(unit_duty, T_in, T_out=None, agent=None, heat_transfer_efficiency=None, hxn_ok=False),does T_out=None means that if we don't give T_out, the default value is adopted? Also, does heat_transfer_efficiency=None means that if heat_transfer_efficiency is not set, the default value for heat_transfer_efficiency is used (e.g. 0.95 for low pressure steam)?

Additionally, in the source code (https://biosteam.readthedocs.io/en/latest/_modules/biosteam/_heat_utility.html#HeatUtility), the cost of utility is calculated using self.cost = agent._heat_transfer_price abs(duty) + agent._regeneration_price F_mol. Could I coinsult you that if I purchase the utility from external source, do I still need to set the regeneration_price, or is it enough to only set the heat_transfer_price?

Thank you a lot for your help. Wish you a good day.

yoelcortes commented 1 year ago

@zasddsgg, for a heating/cooling jacket at a theoretical CSTR/tank, the temperature at any point is the same as the outlet. So, yes, T_operation is the outlet temperature.

That code should be fine so long as you are using a jacket (which we don't have in biosteam yet). As explained earlier, the CSTR in BioSTEAM uses an auxiliary heat exchanger so T_in is T_operation and T_out will depend on the recirculated flow rate (https://github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/biosteam/units/cstr.py).

If I understand you correctly, your assertions are correct. Please read the source code for details: https://github.com/BioSTEAMDevelopmentGroup/biosteam/blob/135410ce3512fdcb939251c0168d760cecf7c291/biosteam/_heat_utility.py#L631

If you purchase utility, literature should let you know the price basis. It may be per kg (regeneration price), per kJ (heat transfer price), or both. Just follow their instructions.

Thanks,

zasddsgg commented 1 year ago

Thank you for your answer. If there is no jacket in BioSTEAM, is it still ok to use the following code to check utility duty and utility flow of CSTRs, or tanks, or other reactors such as pretreatment reactor, fermentation reactor. Thank you for your help. Wish you a good day.

duty = self.H_out - self.H_in + self.Hf_out - self.Hf_in(reaction occurs) or duty = self.H_out - self.H_in (no reaction occurs) T_operation = self.outs[0].T (CSTRs or tanks or other reactors outlet stream temperature) self.add_heat_utility(duty, T_operation)

yoelcortes commented 1 year ago

@zasddsgg, if you are creating your own unit operation and neglecting capital cost of your jacket, I would assume it theoretically correct.

zasddsgg commented 1 year ago

Thank you for your answer. I got it. Thank you for your help. Wish you a good day.