From 7c9eb9d134a022cd6fca970e727b66cc7273ea9f Mon Sep 17 00:00:00 2001 From: Jorrit Wronski Date: Tue, 23 Jun 2015 17:34:26 +0200 Subject: [PATCH] It is of course more complicated than I thought --- wrappers/Python/CoolProp/Plots/Common.py | 372 ++++++++++++++--------- wrappers/Python/CoolProp/Plots/Plots.py | 10 - 2 files changed, 221 insertions(+), 161 deletions(-) diff --git a/wrappers/Python/CoolProp/Plots/Common.py b/wrappers/Python/CoolProp/Plots/Common.py index 45a5a3bc..a048a195 100644 --- a/wrappers/Python/CoolProp/Plots/Common.py +++ b/wrappers/Python/CoolProp/Plots/Common.py @@ -3,10 +3,13 @@ from __future__ import print_function, unicode_literals import matplotlib -import numpy +import numpy as np import CoolProp.CoolProp as CP from abc import ABCMeta +from CoolProp import AbstractState +import CoolProp +import warnings class BaseQuantity(object): @@ -131,177 +134,244 @@ class EURunits(KSIunits): self.P.mul_SI=1e-5 self.P.unit=r'bar' self.T.add_SI=-273.15 - self.T.unit=r'C' + self.T.unit=ur'\u00B0 C' +class IsoLine(object): + """An object that holds the functions to calculate a line of + a constant property in the dimensions of a property plot. This + class only uses SI units.""" + + # A list of supported plot + PLOTS = ['TS','PH','HS','PS','PD','TD','PT','PU'] + + # Normally we calculate a sweep in x-dimensions, but + # sometimes a sweep in y-dimensions is better. + XY_SWITCH = { + 'D': { 'TS':True , 'PH':True , 'HS':False, 'PS':True , 'PD':False, 'TD':False, 'PT':False, 'PU':False}, + 'H': { 'TS':False, 'PH':False, 'HS':False, 'PS':False, 'PD':False, 'TD':False, 'PT':False, 'PU':False}, + 'P': { 'TS':False, 'PH':False, 'HS':False, 'PS':False, 'PD':False, 'TD':False, 'PT':False, 'PU':False}, + 'S': { 'TS':False, 'PH':False, 'HS':False, 'PS':False, 'PD':False, 'TD':False, 'PT':False, 'PU':False}, + 'T': { 'TS':False, 'PH':False, 'HS':False, 'PS':False, 'PD':False, 'TD':False, 'PT':False, 'PU':False}, + 'U': { 'TS':False, 'PH':False, 'HS':False, 'PS':False, 'PD':False, 'TD':False, 'PT':False, 'PU':False}, + } + + @classmethod + def get_update_pair(cls,i,x,y): + """Processes the values for the isoproperty and the graph dimensions + to figure which should be used as inputs to the state update. Returns + a tuple with the indices for the update call and the property constant. + For an isobar in a Ts-diagram it returns the default order and the + correct constant for the update pair: + get_update_pair('P','S','T') -> (0,1,2,CoolProp.PSmass_INPUTS) + other values require switching and swapping + get_update_pair('S','P','H') -> (1,0,2,CoolProp.PSmass_INPUTS) + """ + + #ii = CP.get_parameter_index(i) + + # Figure out if x or y-dimension should be used + switch = cls.XY_SWITCH[i] + if switch is None: + raise ValueError("This isoline cannot be calculated!") + elif switch is False: + oo = CP.get_parameter_index(i) + tt = CP.get_parameter_index(x) + second = None + third = 2 + elif switch is True: + oo = CP.get_parameter_index(i) + tt = CP.get_parameter_index(y) + second = 2 + third = None + else: + raise ValueError("Unknown error!") + + pair, out1, _ = CP.generate_update_pair(oo,0.0,tt,1.0) + if out1==0.0: # Correct order + first = 0 + if second is None: + second = 1 + else: + third = 1 + else: # Wrong order + first = 1 + if second is None: + second = 0 + else: + third = 0 + return first,second,third,pair -if __name__ == "__main__": - for syst in [SIunits(), KSIunits(), EURunits()]: - print(syst.P.label) - print(syst.P.to_SI(20)) - print(syst.T.label) - print(syst.T.to_SI(20)) + @property + def x(self): return self._x + @x.setter + def x(self, value): self._x = np.array(value) + @property + def y(self): return self._y + @y.setter + def y(self, value): self._y = np.array(value) + @property + def line_type(self): return self._line_type + @line_type.setter + def line_type(self, value): self._line_type = str(value).upper() + @property + def graph_type(self): return self._graph_type + @graph_type.setter + def graph_type(self, value): self._graph_type = str(value).upper() + @property + def value(self): return self._value + @value.setter + def value(self, value): self._value = float(value) + + +class IsoLineCalculator(object): + def __init__(self, state): + self.DEBUG = False + # direct geometry + self.X = None # + self.Y = None # + self.type = None # + self.value = None # + self.unit = None # + self.opts = None # -SMALL = 1E-5 - class BasePlot(object): + """The base class for all plots. It can be instantiated itself, but provides many + general facilities to be used in the different plots. """ + __metaclass__ = ABCMeta + # Define the iteration keys + PROPERTIES = { + 'D': 'density', + 'H': 'specific enthalpy', + 'P': 'pressure', + 'S': 'specific entropy', + 'T': 'temperature', + 'U': 'specific internal energy' + } - #TODO: Simplify / Consolidate dictionary maps - AXIS_LABELS = {'KSI': {'T': ["Temperature", r"[K]"], - 'P': ["Pressure", r"[kPa]"], - 'S': ["Entropy", r"[kJ/kg/K]"], - 'H': ["Enthalpy", r"[kJ/kg]"], - 'U': ["Internal Energy", r"[kJ/kg]"], - 'D': ["Density", r"[kg/m$^3$]"] - }, - 'SI': {'T': ["Temperature", r"[K]"], - 'P': ["Pressure", r"[Pa]"], - 'S': ["Entropy", r"[J/kg/K]"], - 'H': ["Enthalpy", r"[J/kg]"], - 'U': ["Internal Energy", r"[J/kg]"], - 'D': ["Density", r"[kg/m$^3$]"] - } - } - - COLOR_MAP = {'T': 'Darkred', - 'P': 'DarkCyan', - 'H': 'DarkGreen', - 'D': 'DarkBlue', - 'S': 'DarkOrange', - 'Q': 'black'} - - #: Scale factors to multiply SI units by in order to obtain kSI units - KSI_SCALE_FACTOR = {'T' : 1.0, - 'P' : 0.001, - 'H' : 0.001, - 'U' : 0.001, - 'D' : 1, - 'S' : 0.001, - 'Q' : 1.0} - - SYMBOL_MAP_KSI = {'T' : [r'$T = ', r'$ K'], - 'P' : [r'$p = ', r'$ kPa'], - 'H' : [r'$h = ', r'$ kJ/kg'], - 'U' : [r'$h = ', r'$ kJ/kg'], - 'D' : [r'$\rho = ', r'$ kg/m$^3$'], - 'S' : [r'$s = ', r'$ kJ/kg-K'], - 'Q' : [r'$x = ', r'$']} - - SYMBOL_MAP_SI = {'T' : [r'$T = ', r'$ K'], - 'P' : [r'$p = ', r'$ Pa'], - 'H' : [r'$h = ', r'$ J/kg'], - 'U' : [r'$h = ', r'$ J/kg'], - 'D' : [r'$\rho = ', r'$ kg/m$^3$'], - 'S' : [r'$s = ', r'$ J/kg-K'], - 'Q' : [r'$x = ', r'$']} - - LINE_IDS = {'TS': ['P', 'D'], #'H'], - 'PH': ['S', 'T', 'D'], - 'HS': ['P'], #'T', 'D'], - 'PS': ['H', 'T', 'D'], - 'PD': ['T', 'S', 'H'], - 'TD': ['P'], #'S', 'H'], - 'PT': ['D', 'P', 'S'], - 'PU': []} + # A list of supported plot + PLOTS = ['TS','PH','HS','PS','PD','TD','PT','PU'] + + # Define the unit systems + UNIT_SYSTEMS = { + 'SI' : SIunits(), + 'KSI': KSIunits(), + 'EUR': EURunits() + } + + LINE_COLORS = { + 'T': 'Darkred', + 'P': 'DarkCyan', + 'H': 'DarkGreen', + 'D': 'DarkBlue', + 'S': 'DarkOrange', + 'Q': 'black' + } def __init__(self, fluid_ref, graph_type, unit_system = 'KSI', **kwargs): - if not isinstance(graph_type, str): - raise TypeError("Invalid graph_type input, expected a string") - - graph_type = graph_type.upper() - if len(graph_type) >= 2 and graph_type[1:len(graph_type)] == 'RHO': - graph_type = graph_type[0] + graph_type[1:len(graph_type)] - - if graph_type.upper() not in self.LINE_IDS.keys(): - raise ValueError(''.join(["You have to specify the kind of ", - "plot, use one of", - str(self.LINE_IDS.keys())])) - - self.graph_drawn = False - self.fluid_ref = fluid_ref - self.graph_type = graph_type.upper() - self.unit_system = unit_system -# if unit_system == 'KSI': -# self.unit_system = KSIunits() -# elif unit_system == 'EUR': -# self.unit_system = EURunits() -# else: -# self.unit_system = SIunits() - + # Process the fluid and set self._state + if isinstance(fluid_ref, str): + # TODO: Fix the backend extraction etc + fluid_def = fluid_ref.split('::') + if len(fluid_def)==2: + backend = fluid_def[0] + fluid = fluid_def[1] + elif len(fluid_def)==1: + backend = "HEOS" + fluid = fluid_def[0] + else: + raise ValueError("This is not a valid fluid_ref string: {0:s}".format(str(fluid_ref))) + self._state = AbstractState(backend, fluid) + elif isinstance(fluid_ref, AbstractState): + self._state = fluid_ref + else: + raise TypeError("Invalid fluid_ref input, expected a string or an abstract state instance") + + # Process the graph_type and set self._x_type and self._y_type + graph_type = graph_type.upper() + graph_type = graph_type.replace(r'RHO',r'D') + if graph_type in self.PLOTS: + self._y_type = graph_type[0] + self._x_type = graph_type[1] + else: + raise ValueError("Invalid graph_type input, expected a string from {0:s}".format(str(self.PLOTS))) + + # Process the unit_system and set self._system + unit_system = unit_system.upper() + if unit_system in self.UNIT_SYSTEMS: + self._system = self.UNIT_SYSTEMS[unit_system] + else: + raise ValueError("Invalid unit_system input, expected a string from {0:s}".format(str(self.UNIT_SYSTEMS.keys()))) + + self._axis = kwargs.get('axis', matplotlib.pyplot.gca()) + self.small = kwargs.get('small', 1e-5) - self.axis = kwargs.get('axis', None) - if self.axis is None: - self.axis = matplotlib.pyplot.gca() - + self._colors = self.LINE_COLORS.copy() + colors = kwargs.get('colors', None) + if colors is not None: + self._colors.update(colors) + + self._graph_drawn = False + + @property + def small(self): return self._small + @small.setter + def small(self, value): + self._T_small = self._state.trivial_keyed_output(CoolProp.iT_critical)*value + self._P_small = self._state.trivial_keyed_output(CoolProp.iP_critical)*value + self._small = value def __sat_bounds(self, kind, smin=None, smax=None): - """ - Generates limits for the saturation line in either T or p determined + """Generates limits for the saturation line in either T or p determined by 'kind'. If xmin or xmax are provided, values will be checked - against the allowable range for the EOS and an error might be - generated. + against the allowable range for the EOS and a warning might be + generated. Returns a tuple containing (xmin, xmax)""" - Returns a tuple containing (xmin, xmax) - """ + # TODO: REFPROP backend does not have ptriple. + T_triple = self._state.trivial_keyed_output(CoolProp.iT_triple) + T_min = self._state.trivial_keyed_output(CoolProp.iT_min) + self._state.update(CoolProp.QT_INPUTS, 0, max([T_triple,T_min])+self._T_small) + kind = kind.upper() if kind == 'P': - name = 'pressure' - min_key = 'ptriple' + fluid_min = self._state.keyed_output(CoolProp.iP) + fluid_max = self._state.trivial_keyed_output(CoolProp.iP_critical)-self._P_small elif kind == 'T': - name = 'temperature' - min_key = 'Tmin' - - fluid_min = CP.PropsSI(self.fluid_ref, min_key) - fluid_crit = CP.PropsSI(self.fluid_ref, ''.join([kind, 'crit'])) - - if smin is None: - smin = fluid_min + SMALL - elif smin > fluid_crit: - raise ValueError(''.join(['Minimum ', name, - ' cannot be greater than fluid critical ', - name, '.'])) - - if smax is None: - smax = fluid_crit - SMALL - elif smax > fluid_crit: - raise ValueError(''.join(['Maximum ', name, - ' cannot be greater than fluid critical ', - name, '.'])) - - smin = max(smin, fluid_min + SMALL) - smax = min(smax, fluid_crit - SMALL) - - return (smin, smax) - - def _get_fluid_data(self, req_prop, - prop1_name, prop1_vals, - prop2_name, prop2_vals): - """ - Calculates lines for constant iName (iVal) over an interval of xName - (xVal). Returns (x[],y[]) - a tuple of arrays containing the values - in x and y dimensions. - """ - if len(prop1_vals) != len(prop2_vals): - raise ValueError(''.join(['We need the same number of x value ', - 'arrays as iso quantities.'])) - - y_vals = [] - x_vals = [] - - # Calculate the values in SI units - for i, p1_val in enumerate(prop1_vals): - x_vals.append(prop2_vals[i]) - y_vals.append(CP.PropsSI(req_prop, - prop1_name, [p1_val]*len(prop2_vals[i]), # Convert to an iterable the same size as second input array - prop2_name, prop2_vals[i], - self.fluid_ref)) - return numpy.array([x_vals, y_vals]) + fluid_min = self._state.keyed_output(CoolProp.iT) + fluid_max = self._state.trivial_keyed_output(CoolProp.iT_critical)-self._T_small + else: + raise ValueError("Saturation boundaries have to be defined in T or P, but not in {0:s}".format(str(kind))) + + if smin is not None: + if fluid_min < smin < fluid_max: + sat_min = smin + else: + warnings.warn( + "Your minimum {0:s} has been ignored, {1:f} is not between {2:f} and {3:f}".format(self.PROPERTIES[kind],smin,fluid_min,fluid_max), + UserWarning) + sat_min = fluid_min + else: + sat_min = fluid_min + + if smax is not None: + if fluid_min < smax < fluid_max: + sat_max = smax + else: + warnings.warn( + "Your maximum {0:s} has been ignored, {1:f} is not between {2:f} and {3:f}".format(self.PROPERTIES[kind],smax,fluid_min,fluid_max), + UserWarning) + sat_max = fluid_max + else: + sat_max = fluid_max + return (sat_min, sat_max) + + def _get_sat_lines(self, kind='T', smin=None, smax=None, num=500, x=[0., 1.]): """ @@ -309,7 +379,7 @@ class BasePlot(object): You can specify if you need evenly spaced entries in either pressure or temperature by supplying kind='p' and kind='T' (default), respectively. - Limits can be set with kmin (default: minimum from EOS) and + Limits can be set with kmin (default: triple point or EOS minimum) and kmax (default: critical value). Returns lines[] - a 2D array of dicts containing 'x' and 'y' coordinates for bubble and dew line. Additionally, the dict holds diff --git a/wrappers/Python/CoolProp/Plots/Plots.py b/wrappers/Python/CoolProp/Plots/Plots.py index 1e1a6a07..a4709a3f 100644 --- a/wrappers/Python/CoolProp/Plots/Plots.py +++ b/wrappers/Python/CoolProp/Plots/Plots.py @@ -11,17 +11,7 @@ from .Common import BasePlot from scipy import interpolate from scipy.spatial.kdtree import KDTree -class IsoLine(object): - def __init__(self): - self.DEBUG = False - # direct geometry - self.X = None # - self.Y = None # - self.type = None # - self.value = None # - self.unit = None # - self.opts = None # def InlineLabel(xv,yv,x = None, y= None, axis = None, fig = None):