Usage

Introduction

After installation of fluprodia you can easily create fluid property diagrams for all pure and pseudo-pure fluids from the CoolProp fluid property database. For a list of available fluids please refer to the online documentation of CoolProp.

In order to start, import the package and create an object of the class fluprodia.fluid_property_diagram.FluidPropertyDiagram by passing the alias of the fluid. After that, it is possible to specify a unit system for all fluid properties available with the fluprodia.fluid_property_diagram.FluidPropertyDiagram.set_unit_system() method. The fluid properties available are:

  • pressure p

  • specific enthalpy h

  • specific entropy s

  • specific volume vol

  • temperature T

  • vapor mass fraction Q

>>> from fluprodia import FluidPropertyDiagram
>>> import numpy as np
>>> import matplotlib.pyplot as plt
>>> diagram = FluidPropertyDiagram('R290')
>>> diagram.set_unit_system(T='°C', p='bar', h='kJ/kg')

After that, you can use the default isolines or specify your own lines by using the fluprodia.fluid_property_diagram.FluidPropertyDiagram.set_isolines() method. If you do not specify custom isolines, generic isolines will be used instead. Next step is to calculate the isolines, drawing them and exporting the diagram in your favorite format. The formats available are the matplotlib file formats for figures. You will also need to specify the limits in order to determine the view. Also, different diagrams will have different value ranges for their x- and y-axes.

>>> iso_T = np.arange(-75, 151, 25)
>>> diagram.set_isolines(T=iso_T)
>>> diagram.calc_isolines()
>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> diagram.draw_isolines(fig, ax, 'Ts', x_min=500, x_max=3000, y_min=-50, y_max=150)
>>> plt.tight_layout()
>>> fig.savefig('Ts_diagram.svg')
_images/Ts_diagram.svg
_images/Ts_diagram_darkmode.svg

As all fluid properties will be stored in the object referenced by diagram, it is possible to change the diagram type and export a new diagram without recalculating the isolines. Only if you wish to draw a different set of isolines unlike specified in the set_isolines() method call, you need to recalculate the isolines.

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> diagram.draw_isolines(fig, ax, 'logph', x_min=0, x_max=750, y_min=1e-1, y_max=1e2)
>>> plt.tight_layout()
>>> fig.savefig('logph_diagram.svg')
_images/logph_diagram.svg
_images/logph_diagram_darkmode.svg

All available diagram types can be displayed by printing the following line.

>>> list(diagram.supported_diagrams.keys())
['Ts', 'hs', 'logph', 'Th', 'plogv']

You can also change the back-end of CoolProp, e.g. to use REFPROP:

diagram = FluidPropertyDiagram(fluid='R290', backend='REFPROP')

Customizing the Display

Customization is possible regarding

  • generation of isolines only within a specific region of the fluid,

  • the isovalues of the isolines,

  • the isolines to be displayed,

  • the linestyle of the isolines and

  • the position of the isolines’ labels.

Isolines only within a specific region

By default, every isoline is generated for the complete value space of the fluid properties, that means from minimum to maximum temperature, or from minimum to maximum pressure, density, etc.. It is possible to make a sub-selection of a temperature range. This automatically assigns values for all isolines within that range. The advantage of this implementation is that it can reduce the overall amount of isolines to be calculated, and that the amount of points per line is higher in the specified subsection, because all points are distributed on the full range otherwise.

>>> T_min = -75
>>> T_max = 150
>>> diagram.set_isolines_subcritical(T_min, T_max)
>>> diagram.calc_isolines()

Note

This feature is new in version 3.4. It is likely, that it will be refined, and more methods for other sections (transcritical and supercritical) are planned.

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> diagram.draw_isolines(fig, ax, 'logph', x_min=0, x_max=750, y_min=1e-1, y_max=1e2)
>>> plt.tight_layout()
>>> fig.savefig('logph_R290_isolines_subsection.svg')
>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> diagram.draw_isolines(fig, ax, 'Ts', x_min=500, x_max=3000, y_min=-50, y_max=150)
>>> plt.tight_layout()
>>> fig.savefig('Ts_R290_isolines_subsection.svg')
_images/logph_R290_isolines_subsection.svg
_images/logph_R290_isolines_subsection_darkmode.svg
_images/Ts_R290_isolines_subsection.svg
_images/Ts_R290_isolines_subsection_darkmode.svg

Isoline values available

As already mentioned, you can set the isolines for your diagram like this. All isolines you specify are available for drawing the diagram later. Therefore, the more values you specify, the more lines can be displayed. Also, the computation time will rise.

Still, it might be useful to specify a lot of values. E.g., if we want to create a full view of a logph diagram for R290 and a zoomed view in the two phase region with lines of constant vapor mass fraction for every 2.5 % and lines of constant temperature every 5 K.

>>> T = np.arange(-75, 151, 5)
>>> Q = np.linspace(0, 1, 41)
>>> diagram.set_isolines(T=T, Q=Q)
>>> diagram.calc_isolines()

The following sections shows how to select from all isolines available.

Lines displayed and Linestyle

As we do not want to display all values for temperature and vapor mass fraction for the full view diagram, we specify the values to be displayed for these properties. This is done by using the isoline_data property, which must be a dictionary holding the required information.

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> mydata = {
...    'Q': {'values': np.linspace(0, 1, 11)},
...    'T': {'values': np.arange(-75, 151, 25)}
... }
>>> diagram.draw_isolines(fig, ax, 'logph', isoline_data=mydata, x_min=0, x_max=750, y_min=1e-1, y_max=1e2)
>>> plt.tight_layout()
>>> fig.savefig('logph_R290_full.svg')
_images/logph_R290_full.svg
_images/logph_R290_full_darkmode.svg

Now, for the zoomed diagram we want the full temperature and vapor mass fraction data. At the same time, you might want to change the color or the linestyle of an isoline. For this example, we will color the lines of constant temperature in red. Additionally, the lines of constant specific volume should not be displayed at all. This can be done by passing an empty list or an empty numpy array.

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> mydata = {
...     'T': {
...         'style': {'color': '#ff0000'},
...         'values': T
...     },
...     'vol': {'values': np.array([])}
... }
>>> diagram.draw_isolines(fig, ax, 'logph', isoline_data=mydata, x_min=300, x_max=600, y_min=1, y_max=1e2)
>>> plt.tight_layout()
>>> fig.savefig('logph_R290_zoomed.svg')
_images/logph_R290_zoomed.svg
_images/logph_R290_zoomed_darkmode.svg

Note

For changing the style of a specific isoline pass the respective keyword and value pairs in a dictionary. The keywords available are the keywords of a matplotlib.lines.Line2D object. See https://matplotlib.org/stable/api/_as_gen/matplotlib.lines.Line2D.html for more information.

Positioning of the isoline lables

In the last section we briefly describe, how to change the placing of the labels for the isolines. Looking at the zoomed diagram, you see that some of the temperature labels are missing.

You can specify a positioning value between 0 and 1. Every label of an isoline type (e.g. constant temerature) will be placed at the relative position of each isoline within the limits of the view.

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> mydata = {
...     'T': {
...         'style': {'color': '#ff0000'},
...         'values': T,
...         'label_position': 0.8
...     },
...     'vol': {'values': np.array([])}
... }
>>> diagram.draw_isolines(fig, ax, 'logph', isoline_data=mydata, x_min=300, x_max=600, y_min=1, y_max=1e2)
>>> plt.tight_layout()
>>> fig.savefig('logph_R290_zoomed_temperature_labels.svg')
_images/logph_R290_zoomed_temperature_labels.svg
_images/logph_R290_zoomed_temperature_labels_darkmode.svg

Note

The placing method of the labels is not fully satisfactory at the moment. If you have ideas, how to place the labels in an improved way, we are looking forward for you suggestions.

Changing the line labels and LaTeX-style units

It is possible to add more than a single label to each isoline with key labels_per_lin, or to only add a label to each n-th isoline with key label_every_nth. We can also disable the LaTeX style unit display with latex_units=False.

>>> diagram.set_isolines_subcritical(-75, 125)  # also resets old isolines
>>> diagram.calc_isolines()
>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> mydata = {
...     'T': {
...         'style': {'color': '#ff0000'},
...         'label_every_nth': 2,  # every second line
...         'labels_per_line': 3  # 3 labels for every line
...     }
... }
>>> diagram.draw_isolines(
...     fig, ax, 'logph', isoline_data=mydata,
...     x_min=0, x_max=750, y_min=1, y_max=5e1, latex_units=False
... )
>>> plt.tight_layout()
>>> fig.savefig('logph_R290_labeling.svg')
_images/logph_R290_labeling.svg
_images/logph_R290_labeling_darkmode.svg

Plotting individual isolines (and isolike lines)

FluProDia offers a method to generate data for individual isolines with a specified starting and a specified ending point. Use the method fluprodia.fluid_property_diagram.FluidPropertyDiagram.calc_individual_isoline() to create datapoints for the isoline. The method returns a dictionary containing the datapoints in numpy arrays using the property name as respective key. Therefore, independent of the diagram you want to draw, you will have all data available. Following, we will draw all available isolines into a Ts and a logph diagram. Each property value must be passed in the diagram’s respective unit system.

>>> data = {
...     'isobaric': {
...         'isoline_property': 'p',
...         'isoline_value': 10,
...         'starting_point_property': 'T',
...         'starting_point_value': -50,
...         'ending_point_property': 'T',
...         'ending_point_value': 150
...     },
...     'isochoric': {
...         'isoline_property': 'vol',
...         'isoline_value': 0.035,
...         'starting_point_property': 'h',
...         'starting_point_value': 250,
...         'ending_point_property': 'T',
...         'ending_point_value': 125
...     },
...     'isothermal': {
...         'isoline_property': 'T',
...         'isoline_value': 50,
...         'starting_point_property': 'Q',
...         'starting_point_value': 0.1,
...         'ending_point_property': 'vol',
...         'ending_point_value': 0.5
...     },
...     'isenthalpic': {
...         'isoline_property': 'h',
...         'isoline_value': 500,
...         'starting_point_property': 'p',
...         'starting_point_value': 95,
...         'ending_point_property': 'p',
...         'ending_point_value': 5
...     },
...     'isentropic': {
...         'isoline_property': 's',
...         'isoline_value': 2500,
...         'starting_point_property': 'p',
...         'starting_point_value': 1,
...         'ending_point_property': 'p',
...         'ending_point_value': 80
...     }
... }

>>> for name, specs in data.items():
...    data[name]['datapoints'] = diagram.calc_individual_isoline(**specs)

With these data, it is possible to plot to your diagram simply by plotting on the diagram.ax object, which is a matplotlib.axes._subplots.AxesSubplot object. Therefore all matplolib plotting functionalities are available. Simply pass the data of the x and y property of your diagram, e.g. to the plot() method.

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> diagram.clear_isolines()  # we remove the old ones and make them new
>>> diagram._setup_isoline_defaults()
>>> iso_T = np.arange(-75, 151, 25)
>>> diagram.set_isolines(T=iso_T)
>>> diagram.calc_isolines()

>>> diagram.draw_isolines(fig, ax, 'logph', isoline_data=mydata, x_min=0, x_max=1000, y_min=1e-1, y_max=1.5e2)
>>> for key, specs in data.items():
...     datapoints = specs['datapoints']
...     _ = ax.plot(specs['datapoints']['h'], specs['datapoints']['p'], label=key)
>>> _ = ax.legend(loc='lower right')
>>> plt.tight_layout()
>>> fig.savefig('logph_R290_isolines.svg')

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> diagram.draw_isolines(fig, ax, 'Ts', x_min=750, x_max=3000, y_min=-50, y_max=150)
>>> for key, specs in data.items():
...     datapoints = specs['datapoints']
...     _ = ax.plot(specs['datapoints']['s'], specs['datapoints']['T'], label=key)
>>> _ = ax.legend(loc='lower right')
>>> plt.tight_layout()
>>> fig.savefig('Ts_R290_isolines.svg')
_images/logph_R290_isolines.svg
_images/logph_R290_isolines_darkmode.svg
_images/Ts_R290_isolines.svg
_images/Ts_R290_isolines_darkmode.svg

Note

Note that the starting_point_property and the ending_point_property do not need to be identical! E.g., you can draw an isobaric line starting at a specific entropy and ending at a specific temperature.

On top of that, e.g. in order to display a pressure loss in a heat exchanger, you can have different values for the (iso)line at the starting and the ending points. The (then former) isoline property will be changed linearly to either change in entropy (for isobars and isotherms) or change in pressure (for all other lines). This functionality is only supposed to display the change in a beautiful way, it does not represent the actual process connecting your starting point with your ending point as this would require perfect knowledge of the process. In order to generate these data, you need to pass the 'isoline_value_end' keyword to the fluprodia.fluid_property_diagram.FluidPropertyDiagram.calc_individual_isoline() method.

>>> data = {
...     'isoline_property': 'p',
...     'isoline_value': 10,
...     'isoline_value_end': 9,
...     'starting_point_property': 'Q',
...     'starting_point_value': 0,
...     'ending_point_property': 'h',
...     'ending_point_value': 750
... }
>>> datapoints = diagram.calc_individual_isoline(**data)
>>> diagram.draw_isolines(fig, ax, 'Ts', x_min=750, x_max=3000, y_min=-50, y_max=150)
>>> for specs in data.values():
...    _ = ax.plot(datapoints['s'], datapoints['T'])
>>> plt.tight_layout()
>>> fig.savefig('Ts_R290_pressure_loss.svg')
_images/Ts_R290_pressure_loss.svg
_images/Ts_R290_pressure_loss_darkmode.svg

Plotting States into the Diagram

For instance, if you want to plot two different states of R290 into your diagram, you could use the scatter() method. If you want to have connected states, you will need the plot() method. In this example, we will plot from a simple heat pump simulation in TESPy [1] (for more information on TESPy see the online documentation) into a logph and a Ts diagram.

_images/logph_diagram_states.svg
_images/logph_diagram_states_darkmode.svg
_images/Ts_diagram_states.svg
_images/Ts_diagram_states_darkmode.svg

You can use the get_plotting_data data method from the tespy.tools module to automatically extract all data for the specified part of the model.

>>> from tespy.components import (
...     Compressor, CycleCloser, SimpleHeatExchanger, Valve
... )
>>> from tespy.connections import Connection
>>> from tespy.networks import Network
>>> from tespy.tools import get_plotting_data


>>> def run_simple_heat_pump_model():
...     nw = Network(iterinfo=False)
...     nw.units.set_defaults(
...         temperature="°C",
...         pressure="bar",
...         enthalpy="kJ/kg",
...         heat="kW",
...         power="kW"
...     )
...
...     cp = Compressor('compressor')
...     cc = CycleCloser('cycle_closer')
...     cd = SimpleHeatExchanger('condenser')
...     va = Valve('expansion valve')
...     ev = SimpleHeatExchanger('evaporator')
...
...     cc_cd = Connection(cc, 'out1', cd, 'in1', label='c1')
...     cd_va = Connection(cd, 'out1', va, 'in1', label='c2')
...     va_ev = Connection(va, 'out1', ev, 'in1', label='c3')
...     ev_cp = Connection(ev, 'out1', cp, 'in1', label='c4')
...     cp_cc = Connection(cp, 'out1', cc, 'in1', label='c5')
...
...     nw.add_conns(cc_cd, cd_va, va_ev, ev_cp, cp_cc)
...
...     cd.set_attr(pr=0.95, Q=-1e3)
...     ev.set_attr(pr=0.9)
...     cp.set_attr(eta_s=0.85)
...
...     cc_cd.set_attr(fluid={'R290': 1})
...     cd_va.set_attr(td_bubble=5, T=60)
...     ev_cp.set_attr(td_dew=5, T=15)
...     nw.solve('design')
...     return nw
>>> nw = run_simple_heat_pump_model()
>>> processes, points = get_plotting_data(nw, "c1")
>>> processes = {
...     key: diagram.calc_individual_isoline(**value)
...     for key, value in processes.items()
...     if value is not None
... }

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> mydata = {
...     'Q': {'values': np.linspace(0, 1, 11)},
...     'T': {
...         'values': np.arange(-25, 150, 25),
...         'style': {'color': '#000000'}
...     }
... }
>>> diagram = FluidPropertyDiagram(fluid="R290")
>>> diagram.set_unit_system(units=nw.units)
>>> diagram.set_isolines_subcritical(T_min=-25, T_max=100)
>>> diagram.set_isolines(T=mydata["T"]["values"], Q=mydata["Q"]["values"])
>>> diagram.calc_isolines()
>>> diagram.draw_isolines(
...     fig, ax, 'logph', isoline_data=mydata,
...     x_min=100, x_max=800, y_min=1e0, y_max=1e2
... )

>>> for label, values in processes.items():
...     _ = ax.plot(values["h"], values["p"], label=label, color="tab:red")
>>> for label, point in points.items():
...     _ = ax.scatter(point["h"], point["p"], label=label, color="tab:red")

>>> plt.tight_layout()
>>> fig.savefig('logph_diagram_states.svg')

>>> fig, ax = plt.subplots(1, figsize=(16, 10))
>>> diagram.draw_isolines(fig, ax, 'Ts', x_min=750, x_max=2500, y_min=-50, y_max=150)

>>> for label, values in processes.items():
...     _ = ax.plot(values["s"], values["T"], label=label, color="tab:red")
>>> for label, point in points.items():
...     _ = ax.scatter(point["s"], point["T"], label=label, color="tab:red")

>>> plt.tight_layout()
>>> fig.savefig('Ts_diagram_states.svg')

Note

The values for plotting must be passed in the diagrams unit system.

Export the underlying data

You can export the underlying data in json format:

>>> diagram.to_json("diagram.json")

Finally, you can also reload a diagram from the data:

>>> diagram = FluidPropertyDiagram.from_json("diagram.json")