In [1]:
%load_ext autoreload
%autoreload 2

How to use ammonia-aqua system model from openACHP

This Jupyter Notebook is part of the documentation for openACHP. It has been exported to HTML and reveal.js slides.

If you are viewing the slides, just type space to advance each slide. Type ? to view keyboard shortcuts.

To start, I'll import the modules I need ...

In [2]:
import ammonia1
import system_aqua1
import numpy as np
import matplotlib.pyplot as plt

Boundary

Now I can create what I call a boundary for the chiller, in other words the specification of available resources, such as flow streams for bringing in or rejecting heat. I'll need that to evaluate heat exchangers later. In the output, we can see each of the flow streams defined where it goes into the chiller.

In [4]:
T_heat_reject = 305.
U = 100
xB = [400, 1, T_heat_reject, 3, T_heat_reject, 5, 285, 4, T_heat_reject, 0.15]
bdry = system_aqua1.makeBoundary(xB)
bdry
Out[4]:
stream T_inlet mdot cp
heat 400 1 4.179
absorberReject 305 3 4.179
condReject 305 5 4.179
cold 285 4 4.179
rectifierReject 305 0.154.179

Chiller

We select a chiller model. I'm showing ammonia-aqua, single-effect system. Just for reference, here is a drawing explaining the chiller model...

In [5]:
secret_sauce="""
<script class="comment">
/*
These are all the state points:
rich_abs_outlet
rich_pump_outlet
rich_shx_outlet
rich_gen_sat_liquid
weak_gen_outlet
weak_shx_outlet
weak_exp_outlet
gen_vapor_outlet
gen_reflux_inlet
refrig_rect_outlet
refrig_cond_outlet
refrig_cehx_liquid_outlet
refrig_exp_outlet
refrig_evap_outlet
refrig_cehx_sat_vapor
refrig_cehx_vapor_outlet
rectifier_liquid
gen_vapor_formation
abs_vapor_final
*/
;
</script>

<script type="application/json" id="stuff">
{
    "1": "rich_abs_outlet",
    "2": "rich_pump_outlet",
    "3": "rich_shx_outlet",
    "3'": "rich_gen_sat_liquid",
    "4": "weak_gen_outlet",
    "5": "weak_shx_outlet",
    "6": "weak_exp_outlet",
    "7(f)": "gen_reflux_inlet",
    "8(g)": "gen_vapor_outlet",
    "9": "refrig_rect_outlet",
    "10": "refrig_cond_outlet",
    "11": "refrig_cehx_liquid_outlet",
    "12": "refrig_exp_outlet",
    "13": "refrig_evap_outlet",
    "14": "refrig_cehx_vapor_outlet"
}
</script>

<script type="application/json" id="mydiagramdata">
{}
</script>

<script>
// TODO: security problem
map = JSON.parse($("#stuff").text());
$("svg:first").children("text").children().each(function(){
  key = $(this).text();
  if (key in map) {
    // If we got here, then the element is a point label
    
    // Note that this line won't work due to a bug with jQuery and svg
    //$(this).addClass(map[key]);
    
    this.classList.add('point_label');
    $(this).data('point_label',map[key]);
    $(this).hover(tooltipHoverIn, tooltipHoverOut);
  }
});

function tooltipHoverIn(event){
    var key = $(this).data('point_label');
    console.log('Creating a tooltip at ' + event.pageX + ',' + event.pageY + ' for "' + key +'"');
    // $('<div class="tooltip">test</div>').appendTo('body');
    var diagram_data = JSON.parse($("#mydiagramdata").text());
    var point = diagram_data[key];
    var divtext = '<b>' +key + '</b><br/>';
    for (point_key in point) {
       divtext += point_key + ': ' + point[point_key].toPrecision(6) + '<br/>';
    }
    $('<div class="tooltip">' + divtext + '</div>').appendTo('body');
    var tPosX = event.pageX + 10;
    var tPosY = event.pageY - 50;
    $('div.tooltip').css({'position': 'absolute', 'top': tPosY+'px', 'left': tPosX+'px',
      'opacity':1, 'background-color':'white', 'border':'1px solid black'});
}

function tooltipHoverOut(event){
  $('div.tooltip').remove();
  console.log("and you're out");
}

$("svg:first").click(function(){
  for (var key in map) {
    cls = "." + map[key]
    $(cls).html(map[key]);
    
  }
});
</script>

<p>Single-effect ammonia-water absorption cycle model. Mouse over state points to view fluid state data.</p>
"""
In [6]:
from IPython.display import HTML, SVG
filename='../img/Diagram_for_ammonia.svg'
img = SVG(filename=filename)
display(img)
display(HTML(secret_sauce))
9 1 2 3 4 5 44 Solution heat exchanger (SHX) 42 6 51 52 43 41 Expansion valve Pump Rectifier & Desorber Refrigerant expansion valve Evaporator 11 14 Vapor absorber Condenser 3 4 22 25 24 3' Vapor desorber 1 6 14 44 43 Vapor absorber Condenser-Evaporator Heat Exchanger (CEHX) 12 10 13 Reflux coil 8(g) 7(f) Rectifier 45 46 9 41 43 45 46 44 42 Cond Abs Rect Amb Rej Heat rejection configuration

Single-effect ammonia-water absorption cycle model. Mouse over state points to view fluid state data.

If you are viewing this as slides you may want to see that as a video...

Now I'll specify and create a chiller. This specification is an output from one of my optimization runs, so it is supposed to be the "best" chiller with a total heat exchange inventory of 100 kW/K. The specification I've chosen is based on temperatures at a few key points, which allows me to solve the model procedurally, without an iterative solver.

In [7]:
xC = np.array([0.51284472, 277.97717012, 312.16427764, 313.6952877,
               310.24856734, 374.14020482])
ch = system_aqua1.makeChiller(xC)

We can display a table of all the state points.

In [9]:
table = ch.getStateTable()
display(table)
T P x h s u v Qu
rich_abs_outlet 310.249 4.874440.512028 -73.18460.395262 -73.78440.00123041 0
rich_pump_outlet 310.41315.1396 0.512028 -71.60610.396279 -73.46820.00122993-0.001
rich_shx_outlet 345.85215.1396 0.512028 89.98850.889071 88.03340.00129135-0.001
rich_gen_sat_liquid 351.91 15.1396 0.512028 118.322 0.970284 116.346 0.00130467 0
weak_gen_outlet 374.14 15.1396 0.39475 225.935 1.26082 224.014 0.00126878 0
weak_shx_outlet 329.93815.1396 0.39475 25.493 0.691038 23.69040.00119066-0.001
weak_exp_outlet 330.108 4.874440.39475 25.493 0.694743 24.911 0.00119402 7.84229e-06
gen_vapor_outlet 351.91 15.1396 0.9881481424.52 4.5745 1268.66 0.102953 1
gen_reflux_inlet 351.91 15.1396 0.512027 118.318 0.970274 116.343 0.00130468 0
refrig_rect_outlet 313.69515.1396 0.9998691297.85 4.19504 1167.34 0.0862066 0.999598
refrig_cond_outlet 312.16415.1396 0.999869 186.035 0.641824 183.419 0.00172798 0
refrig_cehx_liquid_outlet295.02915.1396 0.999869 102.713 0.367562 100.22 0.00164681-0.001
refrig_exp_outlet 276.591 4.874440.999869 102.713 0.382108 93.34820.0192126 0.069211
refrig_evap_outlet 277.977 4.874440.9998691273.03 4.59441 1147.43 0.257979 0.998
refrig_cehx_sat_vapor 283.596 4.874440.9998691289.93 4.65374 1160.79 0.264932 1
refrig_cehx_vapor_outlet 310.343 4.874440.9998691356.35 4.87695 1211.83 0.296502 1.001
rectifier_liquid 313.69515.1396 0.957859 162.404 0.668534 159.871 0.00167345 0
gen_vapor_formation 374.14 15.1396 0.9585011515.65 4.8222 1347.4 0.111135 1
abs_vapor_final 330.108 4.874440.9826091422.49 5.08428 1267.55 0.317852 1
In [10]:
# This stores the table into the webpage ... now go mouse over the diagram.
display(HTML("<script>$('#mydiagramdata').text('{}');</script>".format(table.toJSON())))

We also get a table of the performance variables, such as heat flows (with a convention that positive indicates heat flowing into the chiller).

In [11]:
ch.getVariablesTable()
Out[11]:
name unit value
Q_abs kW -182.886
Q_gen kW 192.048
Q_cond kW -110.508
Q_evap kW 116.323
Q_reflux kW -15.7864
Q_shx kW 82.873
Q_cehx kW 8.28166
W_pump kW 0.809528
COP kW/kW 0.605697
ZeroCheck kW 1.97555e-05
ZeroCheckSHX kW 1.02529e-06
ZeroCheckCEHX kW -2.07808e-05
Q_gen_rect_combokW 176.262
Q_refrig_side kW -5.81476
ZeroCheckRect kg/s 0
m_rich kg/s 0.512845
m_weak kg/s 0.41345
m_gen_vapor kg/s 0.101841
m_gen_reflux kg/s 0.00244687
m_refrig kg/s 0.0993943

I'll also ask for some plots ...

In [12]:
ch.display()
plt.show()
C:\Users\user1\Miniconda3\envs\openachp\lib\site-packages\scipy\interpolate\_cubic.py:147: RuntimeWarning: divide by zero encountered in true_divide
  mk = (y[1:] - y[:-1]) / hk
C:\Users\user1\Miniconda3\envs\openachp\lib\site-packages\scipy\interpolate\_cubic.py:165: RuntimeWarning: invalid value encountered in true_divide
  whmean = (w1/mk[:-1] + w2/mk[1:]) / (w1 + w2)

The first one shows heat vs temperature curves for each of the heat exchangers facing the external streams. Temperature is measured on the internal side of the heat exchanger. These curves are used to calculate heat exchanger size vs. total heat rate.

The other two show some heat exchangers that are entirely internal, ie not facing the external streams. These were specified with effectiveness, so I don't need to calculate anything. But we can see that the curves are not linear because specific heats are changing or phase change is occuring through the process.

System

Next I'll couple the boundary and chiller into a system object. I'll use it to evaluate the heat exchange parameters:

In [13]:
sys = system_aqua1.System(boundary=bdry, chiller=ch)
display(sys)
C:\Users\user1\Miniconda3\envs\openachp\lib\site-packages\scipy\interpolate\_cubic.py:147: RuntimeWarning: divide by zero encountered in true_divide
  mk = (y[1:] - y[:-1]) / hk
C:\Users\user1\Miniconda3\envs\openachp\lib\site-packages\scipy\interpolate\_cubic.py:165: RuntimeWarning: invalid value encountered in true_divide
  whmean = (w1/mk[:-1] + w2/mk[1:]) / (w1 + w2)
name deltaT epsilon UA Q
gen 5.64522 0.890605 14.9004 192.062
rect 8.69718 0.783613 1.04478 15.7995
abs 5.25513 0.735319 27.2995 182.886
cond 2.20854 0.967334 27.3363 110.508
evap 1.45034 0.982521 29.4333 116.323
total 0 0 100.014 0
  • deltaT is the temperature difference at the pinch point.
  • epsilon is the effectiveness
  • UA is the heat exchanger overall heat transfer coefficient
  • Q is the heat flow through the exchanger. In this setup, it is determined by the chiller specification.

Interact

Okay, the next thing I want to do is show how we can interact with the model and manually adjust the specification. Jupyter has some useful widgets, but I had to get the ipywidgets package from different conda channel (conda-forge) to get a version compatible with the notebook extensions module (then enable it on command line).

In [14]:
import ipywidgets as widgets
print("widgets.__version__ = ",widgets.__version__)
import widgetsnbextension
print("widgetsnbextension.__version__ = ",widgetsnbextension.__version__)
print("Note: this won't work if the versions are incompatible.")
widgets.__version__ =  7.0.0
widgetsnbextension.__version__ =  3.0.2
Note: this won't work if the versions are incompatible.

This cell sets up the widgets ...

In [15]:
%matplotlib inline
import ipywidgets as widgets
from ipywidgets import interact, interactive, fixed, interact_manual


common_opts = dict(
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True
)
w_m_rich = widgets.FloatSlider(
    value=0.5,
    min=0.1,
    max=2.0,
    step=0.05,
    description='m_rich',
    readout_format='.2f',
    **common_opts
)
w_T_evap = widgets.FloatSlider(
    value=278.0,
    min=275.0,
    max=285.0,
    step=0.1,
    description='T_evap',
    readout_format='.1f',
    **common_opts
)
w_T_cond = widgets.FloatSlider(
    value= 312.2,
    min=290,
    max=330,
    step=0.1,
    description='T_cond',
    readout_format='.1f',
    **common_opts
)
w_T_rect = widgets.FloatSlider(
    value=313.7,
    min=290.0,
    max=330.0,
    step=0.1,
    description='T_rect',
    readout_format='.1f',
    **common_opts
)
w_T_abs_outlet = widgets.FloatSlider(
    value=310.2,
    min=290.0,
    max=330.0,
    step=0.1,
    description='T_abs_outlet',
    readout_format='.1f',
    **common_opts
)
w_T_gen_outlet = widgets.FloatSlider(
    value=374.1,
    min=360.0,
    max=400.0,
    step=0.1,
    description='T_gen_outlet',
    readout_format='.1f',
    **common_opts
)

And this cell launches the interaction handler...

In [24]:
modes = ['chiller glob','chiller state points','chiller performance','chiller plots','system text']
def f(m_rich, T_evap, T_cond, T_rect, T_abs_outlet, T_gen_outlet, mode='system text'):
    xC = (m_rich, T_evap, T_cond, T_rect, T_abs_outlet, T_gen_outlet)
    ch = system_aqua1.makeChiller(xC)
    if mode == modes[0]:
        display(ch)
    elif mode == modes[1]:
        display(ch.getStateTable())
    elif mode == modes[2]:
        display(ch.getVariablesTable())
    elif mode == modes[3]:
        ch.display()
        plt.plot()
    elif mode == modes[4]:
        sys = system_aqua1.System(boundary=bdry, chiller=ch)
        display(sys)
        
#widgets.VBox(w_m_rich,w_T_evap,w_T_cond,w_T_rect,w_T_abs_outlet,w_T_gen_outlet)
interactive(f,
            m_rich=w_m_rich,
            T_evap=w_T_evap,
            T_cond=w_T_cond,
            T_rect=w_T_rect,
            T_abs_outlet=w_T_abs_outlet,
            T_gen_outlet=w_T_gen_outlet,
            mode=modes)

Now I can play with the inputs and see how the system responds ...

What next?

Where do we go with this? For the purpose of fiddling with the inputs, we could implement a web app not requiring the Jupyter Notebook. However, at the moment the code is designed to run on server-side Python, so there would be some hosting cost for a reasonable amount of CPU time, or we would need our own server. Implementing a client-side model in javascript would be subject to the considerable challenge of getting fluid properties and convenience libraries. So, most likely I will publish it as a Python package.

That's it!

Please contact me for questions.

In [ ]: