BioSTEAMDevelopmentGroup / thermosteam

BioSTEAM's Premier Thermodynamic Engine
Other
57 stars 12 forks source link

Heat of reaction does not account for state of water (liquid/gaseous) after reaction #29

Closed fwitte closed 3 years ago

fwitte commented 3 years ago

Hi Yoel, again looking at methane combustion. Feeding a stream of Methane and O2 into a reaction at a temperature of T = 100 °C (could be less, I wanted to make sure after reaction temperature is larger than saturation temperature at partial pressure of the water) the outlet temperature is the same and therefore water should be gaseous.

Is there a way to calculate the actual heat of reaction in respect to the before/after reaction state? Or is the heat of reaction not supposed to be calculated on this basis?

import thermosteam as tmo
from thermosteam import reaction as rxn

chemicals = tmo.Chemicals(['Water', 'Methane', 'CO2', 'O2', 'Glucose', 'Ethanol'])
tmo.settings.set_thermo(chemicals)
combustion = rxn.Reaction('Methane + 2O2 -> 2Water + CO2', reactant='Methane', X=1)

heat_of_combustion = combustion.dH / chemicals['Methane'].MW

feed = tmo.Stream(Methane=1, O2=2, T=373.15, phase='g')
print('BEFORE REACTION')
feed.show(N=100)

# React feed molar flow rate
combustion(feed)

print('AFTER REACTION')
feed.show(N=100)
print('This result is zero, if the state of the fluids is not considered in the heat of reaction:')
print(heat_of_combustion - combustion.dH / chemicals['Methane'].MW)

Thank you very much and best regards Francesco

yoelcortes commented 3 years ago

Hi Francesco,

I am not too sure if I understand your question correctly, but I think I can help clarify some details (that are not too apparent) and hopefully answer it in the process.

The dH property is the heat of reaction in J/mol-reactant. It takes into account the conversion of the reactant too, so dH at X=0.5 is half dH at X=1.

Calling the Reaction object with a Stream object assumes an isothermal reaction (as you already know). For an adiabatic reaction, you can use the adiabatic_reaction method:

The following example shows that the flow rate of reactant multiplied by dH is equal to the total heat produced from the reaction:

import thermosteam as tmo
from thermosteam import reaction as rxn

chemicals = tmo.Chemicals(['Water', 'Methane', 'CO2', 'O2', 'N2'])
tmo.settings.set_thermo(chemicals)
combustion = rxn.Reaction('Methane + 2O2 -> 2Water + CO2', reactant='Methane', X=1)
heat_of_combustion = combustion.dH

feed = tmo.Stream(Methane=1, O2=2, N2=100, T=373.15, phase='g') # Dilute with nitrogen to keep temperature change low
reactant_mol = feed.imol[combustion.reactant]
H0 = feed.H # Enthalpy not including formation enthalpies 
Hnet0 = feed.Hnet # Enthalpy including formation enthalpies
print('BEFORE ADIABATIC REACTION')
feed.show(N=100)

# React feed molar flow rate
combustion.adiabatic_reaction(feed)

print('AFTER ADIABATIC REACTION')
feed.show(N=100)
Hf = feed.H
Hnetf = feed.Hnet 
heat_gained = Hf - H0
dHnet = Hnetf - Hnet0
print(f"Error in energy conservation: {dHnet:.2f}")
print(f'Heat of combution {reactant_mol * heat_of_combustion:.2f} is equal to the heat gained by the system {heat_gained:.2f}')

Output:

BEFORE ADIABATIC REACTION
Stream: s21
 phase: 'g', T: 373.15 K, P: 101325 Pa
 flow (kmol/hr): Methane  1
                 O2       2
                 N2       100
AFTER ADIABATIC REACTION
Stream: s21
 phase: 'g', T: 631.91 K, P: 101325 Pa
 flow (kmol/hr): Water  2
                 CO2    1
                 N2     100
Error in energy conservation: 0.00
Heat of combution -890590.00 is equal to the heat gained by the system 890590.00

I hope I didn't complicate things more, please let me know if I haven't answered your question (or if you have anymore).

Thanks!

yoelcortes commented 3 years ago

Hi Francesco,

I just realized what you meant after reading the title of the issue! Sorry, there is no direct way of taking the state of chemicals. You could use a multistream to change the state of water after an adiabatic reaction and compute the enthalpy difference. If you are just working with combustion reactions, I would suggest you use the "LHV" and "HHV" properties of streams and chemicals:

import thermosteam as tmo

chemicals = tmo.Chemicals(['Water', 'Methane', 'CO2', 'O2', 'N2'])
tmo.settings.set_thermo(chemicals)

feed = tmo.Stream(Methane=1, O2=2, N2=100, T=373.15, phase='g')
print(f'Lower heating value: {feed.LHV} [kJ/hr]')
print(f'Higher heating value: {feed.HHV} [kJ/hr]')
print(f'Lower heating value of methane: {chemicals.Methane.LHV} [J/mol]')
print(f'Higher heating value of methane: {chemicals.Methane.HHV} [J/mol]')

Output:

Lower heating value: -802567.008 [kJ/hr]
Higher heating value: -890590.0 [kJ/hr]
Lower heating value of methane: -802567.008 [J/mol]
Higher heating value of methane: -890590.0 [J/mol]

Hope this helps, Thanks!

fwitte commented 3 years ago

Hi Yoel, thanks for your detailed reply. I was talking about LHV/HHV :), I should have written that more clearly.

I think adding a sentence or two the corresponding parts of your documentation, where you state, that the heat of reaction is not considering the state of the reaction educts (the same "issue" happens when modeling water electrolysis with evaporated water as input) and reaction products.

Thank you, have a nice day! Francesco

yoelcortes commented 3 years ago

Sounds good!

yoelcortes commented 3 years ago

Hi Francesco,

I added some cool new features to the Reaction, ParallelReaction, and SeriesReaction objects to be able to handle phase changes:

import thermosteam as tmo
from thermosteam import reaction as rxn

chemicals = tmo.Chemicals(['Water', 'Methane', 'CO2', 'O2', 'N2'])
tmo.settings.set_thermo(chemicals)
combustion = rxn.Reaction('Methane,g + O2,g -> Water,l + CO2,g',
                          reactant='Methane', X=1,
                          correct_atomic_balance=True)

feed = tmo.Stream(Methane=1, O2=2, N2=100, T=373.15, phase='g') # Dilute with nitrogen to keep temperature change low
feed.phases = 'lg'
print('BEFORE ADIABATIC REACTION')
feed.show(N=100)

# React feed molar flow rate
combustion.adiabatic_reaction(feed)

print('AFTER ADIABATIC REACTION')
feed.show(N=100)

Output:

BEFORE ADIABATIC REACTION
MultiStream: s1
 phases: ('g', 'l'), T: 373.15 K, P: 101325 Pa
 flow (kmol/hr): (g) Methane  1
                     O2       2
                     N2       100
AFTER ADIABATIC REACTION
MultiStream: s1
 phases: ('g', 'l'), T: 648.24 K, P: 101325 Pa
 flow (kmol/hr): (g) CO2    1
                     N2     100
                 (l) Water  2

Note that latent heats will change depending on the temperature, so Reaction objects can only take into account heats of formation (not latent heats). In any case, by making the reaction adiabatic, these latent heats are accounted for in the energy balance. I added a comment in the tutorial that dH does not account for latent heats, and a warning in the documentation as well:

I'll go ahead and close the issue, but feel free to reopen if anything comes up.

Thanks!

yoelcortes commented 3 years ago

@fwitte Almost forgot to mention. I'm not too sure if this interests you, but it is possible to calculate heats of reaction with latent heats as follows:

import thermosteam as tmo
chemicals = tmo.Chemicals(['Water', 'Methane', 'CO2', 'O2', 'N2'])
tmo.settings.set_thermo(chemicals)
combustion = tmo.Reaction('Methane,g + O2,g -> Water,l + CO2,g',
                          reactant='Methane', X=1,
                          correct_atomic_balance=True)
feed = tmo.Stream(Methane=1, O2=2, N2=100, T=298.15, phase='g') 
feed.phases = 'lg'
H0 = feed.Hnet
combustion(feed)
Hf = feed.Hnet
print(f'Heat of reaction with latent heats: {Hf - H0:.2f} kJ/hr')
print(f'Higher heating value of methane: {chemicals.Methane.HHV:.2f} J/mol')

Output:

Heat of reaction with latent heats: -890590.00 kJ/hr
Higher heating value of methane: -890590.00 J/mol

Thanks!

fwitte commented 3 years ago

Hi Yoel, that is a very good addition to thermosteam, I think!

But, I actually have one more question: There seems to be a little mismatch between the lower heating value and the heat of reaction when water is in gaseous state. I cannot really spot the origin of that. Maybe an issue in the reference point definition?


chemicals = tmo.Chemicals(['Water', 'Methane', 'CO2', 'O2', 'N2'])
tmo.settings.set_thermo(chemicals)
combustion = tmo.Reaction('Methane + O2 -> Water + CO2',
                          reactant='Methane', X=1,
                          correct_atomic_balance=True)
feed = tmo.Stream(Methane=1, O2=2, N2=100, T=350.15, phase='g')
H0 = feed.Hnet
combustion(feed)
Hf = feed.Hnet
print(f'Heat of reaction with latent heats: {Hf - H0:.2f} kJ/hr')
print(f'Lower heating value of methane: {chemicals.Methane.LHV:.2f} J/mol')
Heat of reaction with latent heats: -797156.83 kJ/hr
Lower heating value of methane: -802567.01 J/mol

It is more obvious with the electrolysis of water: The heat of reaction with liquid water is correct, the heat of reaction with gaseous water is slightly below the expected value (which should be the enthalpy of formation of gaseous water, so about -241.8 kJ/mol, or?).


chemicals = tmo.Chemicals(['H2O', 'H2', 'O2'], cache=True)
tmo.settings.set_thermo(chemicals)
reaction = rxn.Reaction('2H2O,l -> 2H2,g + O2,g', reactant='H2O', X=1)
reaction.show() # Note that the default basis is by 'mol'
reaction.reactant # The reactant is a tuple of phase and chemical ID
feed = tmo.Stream('feed', H2O=1)
H0 = feed.Hnet
feed.phases = ('g', 'l') # Gas and liquid phases must be available
reaction(feed) # Call to run reaction on molar flow
Hf = feed.Hnet
print(f'Heat of reaction with latent heats: {Hf - H0:.2f} kJ/hr')

reaction = rxn.Reaction('2H2O -> 2H2 + O2', reactant='H2O', X=1)
reaction.show() # Note that the default basis is by 'mol'
reaction.reactant # The reactant is a tuple of phase and chemical ID
feed = tmo.Stream('feed', H2O=1, T=298.15, phase='g')
H0 = feed.Hnet
reaction(feed) # Call to run reaction on molar flow
Hf = feed.Hnet
print(f'Heat of reaction with latent heats: {Hf - H0:.2f} kJ/hr')
Reaction (by mol):
 stoichiometry             reactant    X[%]
 H2O,l -> H2,g + 0.5 O2,g  H2O,l     100.00
Heat of reaction with latent heats: 285825.00 kJ/hr
Reaction (by mol):
 stoichiometry       reactant    X[%]
 H2O -> H2 + 0.5 O2  H2O       100.00
Heat of reaction with latent heats: 239365.21 kJ/hr

The difference between heat of reaction and lower heating value in the combustion reaction of methane is actually very close to the offset in the gaseous water.

Maybe you can help me spot the issue. Thank you and best regards Francesco

yoelcortes commented 3 years ago

Hi Fracesco,

This one is easy to spot for me, but I know its hard to find at first sight. The heat capacities of reactants and products are different. Thus, this adds changes in sensible heats (in addition to the latent heats and heats of formation). Note that in my example, I used a temperature of 298.15 K. At this temperature, the enthalpy of chemicals at their reference state is 0 (since this is the reference point). That is why it worked before. Perhaps this threw you off to generalize, sorry!

You might ask yourself why is the electrolysis of water in the gas phase appear to be off as well? This is because the enthalpy of gaseous water at 298.15 K is not at the reference state (the reference state is a liquid). Note that heats of vaporization change depending on the temperature (another way to view it is to go from 25C to 100C as liquid water, boil, then go from 100C to 25 as steam). Perhaps this quick example may help:

import thermosteam as tmo
Water = tmo.Chemical('Water')
print(f"T       H_vap")
for i in range(20):
    T = 298.15 + 5*i
    H_liq = Water.H('l', T)
    H_gas = Water.H('g', T)
    H_vap = H_gas - H_liq
    print(f"{T:.2f}, {H_vap:.2f}") # Values may differ depending what models you use. Eventually I hope to implement IAPWS models...

Output [EDITED after bug in gas enthalpy was fixed]:

T       H_vap
298.15, 43927.16
303.15, 43718.68
308.15, 43510.46
313.15, 43302.43
318.15, 43094.50
323.15, 42886.59
328.15, 42678.65
333.15, 42470.61
338.15, 42262.42
343.15, 42054.05
348.15, 41845.45
353.15, 41636.60
358.15, 41427.48
363.15, 41218.08
368.15, 41008.39
373.15, 40798.42
378.15, 40588.18
383.15, 40377.68
388.15, 40166.96
393.15, 39956.01

If you are looking to get the exact value for HHVs and LHV (without taking into account sensible heats), it is best to perform these calculations separately. Thermosteam uses this function from the chemicals library to compute HHV and LHV:

from chemicals import combustion_data, Hfg, CAS_from_any
CAS = CAS_from_any('Methane')
cd = combustion_data('CH4', Hf=Hfg(CAS)) # If Hf is not passed, combustion is estimated using Dulong's formula (not accurate here)
print(cd)
print(cd.LHV) # A property

Output:

CombustionData(stoichiometry={'CO2': 1, 'O2': -2.0, 'H2O': 2.0}, HHV=-890590.0, Hf=-74534.0, MW=16.04246)
-802567.008

I hope this helps, Let me know if you have any more questions!

yoelcortes commented 3 years ago

@fwitte, I just made a correction to the combustion_data code block. I forgot to pass Hf (heat of formation of the chemical).

Thanks, Sorry for the confusion!

yoelcortes commented 3 years ago

Hi Francesco,

I went through the calculations to be able to show that everything balances out, and I found a bug in the calculation of the gas phase enthalpy. Thanks for looking at everything in detail!

Here is the demonstration to show that everything balances out by removing the sensible heats. Please pull my changes before you try the example.

Methane example

import thermosteam as tmo
chemicals = Water, Methane, CO2, O2, N2 = tmo.Chemicals(['Water', 'Methane', 'CO2', 'O2', 'N2'])
Water.H.g.Hvap_Tb = 44011.496 # Depending on the model, this value may be different.
tmo.settings.set_thermo(chemicals)
combustion = tmo.Reaction('Methane + O2 -> Water + CO2',
                          reactant='Methane', X=1,
                          correct_atomic_balance=True)
Tref = 298.15
Tb = Water.Tb
feed = tmo.Stream(Methane=1, O2=2, T=Tb, phase='g')
H0 = feed.Hnet - Methane.Cn.g.integrate_by_T(Tref, Tb) - 2 * O2.Cn.g.integrate_by_T(Tref, Tb) 
combustion(feed)
Hf = feed.Hnet - 2 * Water.Cn.l.integrate_by_T(Tref, Tb) - CO2.Cn.g.integrate_by_T(Tref, Tb)
print(f'Heat of reaction with latent heats minus sensible heats: {Hf - H0:.2f} kJ/hr')
print(f'Lower heating value of methane: {chemicals.Methane.LHV:.2f} J/mol')
Heat of reaction with latent heats minus sensible heats: -802567.01 kJ/hr
Lower heating value of methane: -802567.01 J/mol

Electrolysis example:

import thermosteam as tmo
H2O, H2, O2 = chemicals = tmo.Chemicals(['H2O', 'H2', 'O2'], cache=True)
H2O.H.g.Hvap_Tb = 44011.496
tmo.settings.set_thermo(chemicals)
reaction = tmo.Reaction('2H2O,l -> 2H2,g + O2,g', reactant='H2O', X=1)
Tref = 298.15
feed = tmo.Stream('feed', H2O=1)
H0 = feed.Hnet
feed.phases = ('g', 'l') # Gas and liquid phases must be available
reaction(feed) # Call to run reaction on molar flow
Hf = feed.Hnet
print(f'Heat of reaction: {Hf - H0:.2f} kJ/hr')
print(f'HHV: {H2.HHV:.2f} kJ/hr')
reaction = tmo.Reaction('2H2O -> 2H2 + O2', reactant='H2O', X=1)
feed = tmo.Stream('feed', H2O=1, T=Tref, phase='g')
H0 = feed.Hnet - H2O.Cn.l.integrate_by_T(Tref, H2O.Tb) - H2O.Cn.g.integrate_by_T(H2O.Tb, Tref)
reaction(feed) # Call to run reaction on molar flow
Hf = feed.Hnet
print(f'Heat of reaction: {Hf - H0:.2f} kJ/hr')
print(f'LHV: {H2.LHV:.2f} kJ/hr')
Heat of reaction: 285825.00 kJ/hr
HHV: -285825.00 kJ/hr
Heat of reaction: 241813.50 kJ/hr
LHV: -241813.50 kJ/hr

Thanks again, Have a nice day!

fwitte commented 3 years ago

Hi Yoel, great, thank you for taking your time with this. Honestly, I was questioning, if I did implement the combustion chamber balance over at https://github.com/oemof/tespy incorrectly. I am somewhat relieved now :D. Thanks again, have a nice week! Francesco