├── .gitignore ├── .travis.yml ├── CoolPlot ├── Calc │ └── __init__.py ├── Plot │ ├── Common.py │ ├── ConsistencyPlots.py │ ├── Plots.py │ ├── PsychChart.py │ ├── PsychScript.py │ ├── SimpleCycles.py │ ├── SimpleCyclesCompression.py │ ├── SimpleCyclesExpansion.py │ ├── Tests.py │ ├── __init__.py │ ├── psy.py │ └── psyrc ├── Util │ ├── EnhancedState.py │ ├── Quantities.py │ ├── Units.py │ └── __init__.py ├── __init__.py ├── __version__.py ├── core.py └── helpers.py ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── docs ├── HumidAir.rst ├── Makefile ├── conf.py ├── index.rst └── make.bat ├── environment.yml ├── mains ├── 00_plot.py ├── all_fluids.py └── test_plots.py ├── make.bat ├── plots └── _base.py ├── requirements.txt ├── requirements_dev.txt ├── setup.py └── tests ├── __init__.py ├── context.py ├── test_advanced.py ├── test_basic.py └── test_units.py /.gitignore: -------------------------------------------------------------------------------- 1 | # General OS files 2 | **.DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # IPython Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # dotenv 82 | .env 83 | 84 | # virtualenv 85 | .venv/ 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | *.npy 95 | *.pkl 96 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Disable sudo to speed up the build 2 | sudo: false 3 | 4 | # Set the build language to Python 5 | language: python 6 | 7 | # Set the python version 8 | python: 3.6 9 | 10 | # Install the pip dependencies 11 | install: 12 | - make init 13 | - make install 14 | - pip install codecov coveralls 15 | 16 | # Run the unit test 17 | script: 18 | - make test 19 | #- make docs 20 | #- coverage run tests.py 21 | 22 | after_success: 23 | - codecov 24 | - coveralls 25 | 26 | # Push the results back to codecov 27 | #after_success: 28 | #- codecov 29 | 30 | deploy: 31 | provider: pypi 32 | username: "__token__" 33 | password: $PYPI_TOKEN 34 | #distributions: "sdist bdist_wheel" 35 | on: 36 | tags: true -------------------------------------------------------------------------------- /CoolPlot/Calc/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def ray_tracing(x, y, poly): 4 | """Classical ray tracing function to determine 5 | whether a point is inside a polygon or not""" 6 | n = len(poly) 7 | inside = False 8 | p2x = 0.0 9 | p2y = 0.0 10 | xints = 0.0 11 | p1x,p1y = poly[0] 12 | for i in range(n + 1): 13 | p2x,p2y = poly[i % n] 14 | if y > min(p1y,p2y): 15 | if y <= max(p1y,p2y): 16 | if x <= max(p1x,p2x): 17 | if p1y != p2y: 18 | xints = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x 19 | if p1x == p2x or x <= xints: 20 | inside = not inside 21 | p1x,p1y = p2x,p2y 22 | 23 | return inside 24 | -------------------------------------------------------------------------------- /CoolPlot/Plot/Common.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division 3 | 4 | import matplotlib 5 | import matplotlib.pyplot as plt 6 | 7 | import numpy as np 8 | from abc import ABCMeta 9 | from six import with_metaclass 10 | import warnings 11 | 12 | import CoolProp 13 | from CoolProp import AbstractState 14 | from CoolProp import CoolProp as CP 15 | from CoolProp.CoolProp import PropsSI, extract_backend, extract_fractions, PyCriticalState 16 | 17 | from ..Util.Units import SIunits, KSIunits, EURunits 18 | from ..Util import is_string, _get_index 19 | from ..Util.EnhancedState import process_fluid_state, EnhancedState 20 | 21 | 22 | def interpolate_values_1d(x, y, x_points=None, kind='linear'): 23 | try: 24 | from scipy.interpolate.interpolate import interp1d 25 | if x_points is None: 26 | return interp1d(x, y, kind=kind)(x[np.isfinite(x)]) 27 | else: 28 | return interp1d(x, y, kind=kind)(x_points) 29 | except ImportError: 30 | if kind != 'linear': 31 | warnings.warn( 32 | "You requested a non-linear interpolation, but SciPy is not available. Falling back to linear interpolation.", 33 | UserWarning) 34 | if x_points is None: 35 | return np.interp((x[np.isfinite(x)]), x, y) 36 | else: 37 | return np.interp(x_points, x, y) 38 | 39 | 40 | 41 | 42 | class Base2DObject(with_metaclass(ABCMeta), object): 43 | """A container for shared settings and constants for the 44 | isolines and the property plots.""" 45 | 46 | # A list of supported plot 47 | TS = CoolProp.iT * 10 + CoolProp.iSmass 48 | PH = CoolProp.iP * 10 + CoolProp.iHmass 49 | HS = CoolProp.iHmass * 10 + CoolProp.iSmass 50 | PS = CoolProp.iP * 10 + CoolProp.iSmass 51 | PD = CoolProp.iP * 10 + CoolProp.iDmass 52 | TD = CoolProp.iT * 10 + CoolProp.iDmass 53 | PT = CoolProp.iP * 10 + CoolProp.iT 54 | PU = CoolProp.iP * 10 + CoolProp.iUmass 55 | 56 | PLOTS = { 57 | 'TS': TS, 58 | 'PH': PH, 59 | 'HS': HS, 60 | 'PS': PS, 61 | 'PD': PD, 62 | 'TD': TD, 63 | 'PT': PT, 64 | } 65 | 66 | PLOTS_INV = {v: k for k, v in PLOTS.items()} 67 | 68 | # # A list of supported plot 69 | # @property 70 | # def TS(self): return type(self).TS 71 | # @property 72 | # def PH(self): return CoolProp.iP*10 + CoolProp.iHmass 73 | # @property 74 | # def HS(self): return CoolProp.iHmass*10 + CoolProp.iSmass 75 | # @property 76 | # def PS(self): return CoolProp.iP*10 + CoolProp.iSmass 77 | # @property 78 | # def PD(self): return CoolProp.iP*10 + CoolProp.iDmass 79 | # @property 80 | # def TD(self): return CoolProp.iT*10 + CoolProp.iDmass 81 | # @property 82 | # def PT(self): return CoolProp.iP*10 + CoolProp.iT 83 | # @property 84 | # def PU(self): return CoolProp.iP*10 + CoolProp.iUmass 85 | 86 | def __init__(self, x_type, y_type, state=None, small=None, **kwargs): 87 | self._x_index = _get_index(x_type) 88 | self._y_index = _get_index(y_type) 89 | 90 | if small is not None: self._small = small 91 | else: self._small = 1e-7 92 | 93 | if state is not None: self.state = state 94 | else: self._state = None 95 | 96 | # A list of supported plot 97 | @property 98 | def x_index(self): return self._x_index 99 | 100 | @property 101 | def y_index(self): return self._y_index 102 | 103 | @property 104 | def critical_state(self): 105 | if self.state is not None: 106 | return self.state.critical_state 107 | return None 108 | 109 | @property 110 | def state(self): return self._state 111 | 112 | @state.setter 113 | def state(self, value): 114 | self._state = process_fluid_state(value) 115 | self._T_small = self.critical_state.keyed_output(CoolProp.iT) * self._small 116 | self._P_small = self.critical_state.keyed_output(CoolProp.iP) * self._small 117 | 118 | def _get_sat_bounds(self, kind, smin=None, smax=None): 119 | """Generates limits for the saturation line in either T or p determined 120 | by 'kind'. If smin or smax are provided, values will be checked 121 | against the allowable range for the EOS and a warning might be 122 | generated. Returns a tuple containing (xmin, xmax)""" 123 | 124 | # TODO: REFPROP backend does not have ptriple. 125 | T_triple = self.state.trivial_keyed_output(CoolProp.iT_triple) 126 | try: 127 | T_min = self.state.trivial_keyed_output(CoolProp.iT_min) 128 | except: 129 | T_min = T_triple 130 | self.state.update(CoolProp.QT_INPUTS, 0, max([T_triple, T_min]) + self._T_small) 131 | kind = _get_index(kind) 132 | if kind == CoolProp.iP: 133 | fluid_min = self.state.keyed_output(CoolProp.iP) + self._P_small 134 | fluid_max = self.critical_state.keyed_output(CoolProp.iP) - self._P_small 135 | elif kind == CoolProp.iT: 136 | fluid_min = self.state.keyed_output(CoolProp.iT) + self._T_small 137 | fluid_max = self.critical_state.keyed_output(CoolProp.iT) - self._T_small 138 | else: 139 | raise ValueError("Saturation boundaries have to be defined in T or P, but not in {0:s}".format(str(kind))) 140 | 141 | if smin is not None: 142 | if fluid_min < smin < fluid_max: 143 | sat_min = smin 144 | else: 145 | warnings.warn( 146 | "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), 147 | UserWarning) 148 | sat_min = fluid_min 149 | else: 150 | sat_min = fluid_min 151 | 152 | if smax is not None: 153 | if fluid_min < smax < fluid_max: 154 | sat_max = smax 155 | else: 156 | warnings.warn( 157 | "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), 158 | UserWarning) 159 | sat_max = fluid_max 160 | else: 161 | sat_max = fluid_max 162 | 163 | return sat_min, sat_max 164 | 165 | 166 | class IsoLine(Base2DObject): 167 | """An object that holds the functions to calculate a line of 168 | a constant property in the dimensions of a property plot. This 169 | class only uses SI units.""" 170 | 171 | # Normally we calculate a sweep in x-dimensions, but 172 | # sometimes a sweep in y-dimensions is better. 173 | XY_SWITCH = { 174 | CoolProp.iDmass: {Base2DObject.TS: True, Base2DObject.PH: True, Base2DObject.HS: False, Base2DObject.PS: True, Base2DObject.PD: None, Base2DObject.TD: None, Base2DObject.PT: False}, 175 | CoolProp.iHmass: {Base2DObject.TS: False, Base2DObject.PH: None, Base2DObject.HS: None, Base2DObject.PS: True, Base2DObject.PD: True, Base2DObject.TD: False, Base2DObject.PT: False}, 176 | CoolProp.iP: {Base2DObject.TS: False, Base2DObject.PH: None, Base2DObject.HS: False, Base2DObject.PS: None, Base2DObject.PD: None, Base2DObject.TD: False, Base2DObject.PT: None}, 177 | CoolProp.iSmass: {Base2DObject.TS: None, Base2DObject.PH: True, Base2DObject.HS: None, Base2DObject.PS: None, Base2DObject.PD: True, Base2DObject.TD: False, Base2DObject.PT: True}, 178 | CoolProp.iT: {Base2DObject.TS: None, Base2DObject.PH: True, Base2DObject.HS: False, Base2DObject.PS: False, Base2DObject.PD: False, Base2DObject.TD: None, Base2DObject.PT: None}, 179 | CoolProp.iQ: {Base2DObject.TS: True, Base2DObject.PH: True, Base2DObject.HS: True, Base2DObject.PS: True, Base2DObject.PD: True, Base2DObject.TD: True, Base2DObject.PT: False} 180 | } 181 | 182 | # Abort interpolation if there are not enough 183 | # valid entries. 184 | VALID_REQ = 5.0 / 100.0 185 | 186 | def __init__(self, i_index, x_index, y_index, value=0.0, state=None): 187 | super(IsoLine, self).__init__(x_index, y_index, state) 188 | self._i_index = _get_index(i_index) 189 | if value is not None: self.value = value 190 | else: self._value = None 191 | self._x = None 192 | self._y = None 193 | 194 | @property 195 | def i_index(self): return self._i_index 196 | 197 | @property 198 | def value(self): return self._value 199 | 200 | @value.setter 201 | def value(self, value): self._value = float(value) 202 | 203 | @property 204 | def x(self): return self._x 205 | 206 | @x.setter 207 | def x(self, value): self._x = np.array(value) 208 | 209 | @property 210 | def y(self): return self._y 211 | 212 | @y.setter 213 | def y(self, value): self._y = np.array(value) 214 | 215 | def get_update_pair(self): 216 | """Processes the values for the isoproperty and the graph dimensions 217 | to figure which should be used as inputs to the state update. Returns 218 | a tuple with the indices for the update call and the property constant. 219 | For an isobar in a Ts-diagram it returns the default order and the 220 | correct constant for the update pair: 221 | get_update_pair(CoolProp.iP,CoolProp.iSmass,CoolProp.iT) -> (0,1,2,CoolProp.PSmass_INPUTS) 222 | other values require switching and swapping. 223 | """ 224 | # Figure out if x or y-dimension should be used 225 | switch = self.XY_SWITCH[self.i_index][self.y_index * 10 + self.x_index] 226 | 227 | if switch is None: 228 | raise ValueError("This isoline cannot be calculated!") 229 | elif switch is False: 230 | pair, out1, _ = CP.generate_update_pair(self.i_index, 0.0, self.x_index, 1.0) 231 | elif switch is True: 232 | pair, out1, _ = CP.generate_update_pair(self.i_index, 0.0, self.y_index, 1.0) 233 | else: 234 | raise ValueError("Unknown error!") 235 | 236 | if out1 == 0.0: # Correct order 237 | swap = False 238 | else: # Wrong order 239 | swap = True 240 | 241 | if not switch and not swap: 242 | return 0, 1, 2, pair 243 | elif switch and not swap: 244 | return 0, 2, 1, pair 245 | elif not switch and swap: 246 | return 1, 0, 2, pair 247 | elif switch and swap: 248 | return 1, 2, 0, pair 249 | else: 250 | raise ValueError("Check the code, this should not happen!") 251 | 252 | def calc_sat_range(self, Trange=None, Prange=None, num=200): 253 | if Trange is not None: 254 | two = np.array(Trange) 255 | one = np.resize(np.array(self.value), two.shape) 256 | pair = CoolProp.QT_INPUTS 257 | elif Prange is not None: 258 | one = np.array(Prange) 259 | two = np.resize(np.array(self.value), one.shape) 260 | pair = CoolProp.PQ_INPUTS 261 | else: 262 | T_lo, T_hi = self._get_sat_bounds(CoolProp.iT) 263 | two = np.linspace(T_lo, T_hi, num) 264 | one = np.resize(np.array(self.value), two.shape) 265 | pair = CoolProp.QT_INPUTS 266 | 267 | Tcrit = self.critical_state.keyed_output(CoolProp.iT) 268 | Pcrit = self.critical_state.keyed_output(CoolProp.iP) 269 | Dcrit = self.critical_state.keyed_output(CoolProp.iDmass) 270 | try: 271 | #self.state.update(CoolProp.DmassT_INPUTS, Dcrit, Tcrit) 272 | #xcrit = self.state.keyed_output(self._x_index) 273 | #ycrit = self.state.keyed_output(self._y_index) 274 | xcrit = self.critical_state.keyed_output(self._x_index) 275 | ycrit = self.critical_state.keyed_output(self._y_index) 276 | except: 277 | warnings.warn( 278 | "An error occurred for the critical inputs, skipping it.", 279 | UserWarning) 280 | xcrit = np.NaN 281 | ycrit = np.NaN 282 | 283 | X = np.empty_like(one) 284 | Y = np.empty_like(one) 285 | 286 | err = False 287 | for index, _ in np.ndenumerate(one): 288 | try: 289 | self.state.update(pair, one[index], two[index]) 290 | X[index] = self.state.keyed_output(self._x_index) 291 | Y[index] = self.state.keyed_output(self._y_index) 292 | except Exception as e: 293 | if (pair == CoolProp.QT_INPUTS and abs(two[index] - Tcrit) < 1e0) or \ 294 | (pair == CoolProp.PQ_INPUTS and abs(one[index] - Pcrit) < 1e2): 295 | X[index] = xcrit 296 | Y[index] = ycrit 297 | warnings.warn( 298 | "An error occurred for near critical inputs {0:f}, {1:f} with index {2:s}: {3:s}".format(one[index], two[index], str(index), str(e)), 299 | UserWarning) 300 | pass 301 | 302 | warnings.warn( 303 | "An error occurred for inputs {0:f}, {1:f} with index {2:s}: {3:s}".format(one[index], two[index], str(index), str(e)), 304 | UserWarning) 305 | X[index] = np.NaN 306 | Y[index] = np.NaN 307 | err = True 308 | self.x = X; self.y = Y 309 | return 310 | 311 | def calc_range(self, xvals=None, yvals=None): 312 | 313 | if self.i_index == CoolProp.iQ: 314 | warnings.warn( 315 | "Please use \"calc_sat_range\" to calculate saturation and isoquality lines. Input ranges are discarded.", 316 | UserWarning) 317 | if xvals is not None: self.calc_sat_range(num=xvals.size) 318 | elif yvals is not None: self.calc_sat_range(num=yvals.size) 319 | else: self.calc_sat_range() 320 | return 321 | 322 | ipos, xpos, ypos, pair = self.get_update_pair() 323 | 324 | order = [ipos, xpos, ypos] 325 | idxs = [v for (_, v) in sorted(zip(order, [self.i_index, self.x_index, self.y_index]))] 326 | vals = [v for (_, v) in sorted(zip(order, [np.array(self.value), xvals, yvals]))] 327 | if vals[0] is None or vals[1] is None: 328 | raise ValueError("One required input is missing, make sure to supply the correct xvals ({0:s}) or yvals ({1:s}).".format(str(xvals), str(yvals))) 329 | 330 | if vals[0].size > vals[1].size: 331 | vals[1] = np.resize(vals[1], vals[0].shape) 332 | elif vals[0].size < vals[1].size: 333 | vals[0] = np.resize(vals[0], vals[1].shape) 334 | 335 | vals[2] = np.empty_like(vals[0]) 336 | err = False 337 | guesses = CoolProp.CoolProp.PyGuessesStructure() 338 | # Only use the guesses for selected inputs 339 | if pair == CoolProp.HmolarP_INPUTS \ 340 | or pair == CoolProp.HmassP_INPUTS: 341 | # or pair == CoolProp.HmassSmass_INPUTS \ 342 | # or pair == CoolProp.HmolarSmolar_INPUTS \ 343 | # or pair == CoolProp.PSmass_INPUTS \ 344 | # or pair == CoolProp.PSmolar_INPUTS: 345 | use_guesses = True 346 | else: 347 | use_guesses = False 348 | for index, _ in np.ndenumerate(vals[0]): 349 | try: 350 | if use_guesses: 351 | if np.isfinite(guesses.rhomolar): 352 | self.state.update_with_guesses(pair, vals[0][index], vals[1][index], guesses) 353 | else: 354 | self.state.update(pair, vals[0][index], vals[1][index]) 355 | guesses.rhomolar = self.state.rhomolar() 356 | guesses.T = self.state.T() 357 | else: 358 | self.state.update(pair, vals[0][index], vals[1][index]) 359 | vals[2][index] = self.state.keyed_output(idxs[2]) 360 | except Exception as e: 361 | warnings.warn( 362 | "An error occurred for inputs {0:f}, {1:f} with index {2:s}: {3:s}".format(vals[0][index], vals[1][index], str(index), str(e)), 363 | UserWarning) 364 | vals[2][index] = np.NaN 365 | guesses.rhomolar = np.NaN 366 | guesses.T = np.NaN 367 | err = True 368 | 369 | for i, v in enumerate(idxs): 370 | if v == self.x_index: self.x = vals[i] 371 | if v == self.y_index: self.y = vals[i] 372 | 373 | def sanitize_data(self): 374 | """Fill the series via interpolation""" 375 | validx = None; validy = None 376 | countx = None; county = None 377 | if self.x is not None: 378 | validx = np.isfinite(self.x) 379 | countx = float(self.x.size) 380 | else: 381 | raise ValueError("The x-axis is not populated, calculate values before you interpolate.") 382 | if self.y is not None: 383 | validy = np.isfinite(self.y) 384 | county = float(self.y.size) 385 | else: 386 | raise ValueError("The y-axis is not populated, calculate values before you interpolate.") 387 | 388 | if min([np.sum(validx) / countx, np.sum(validy) / county]) < self.VALID_REQ: 389 | warnings.warn( 390 | "Poor data quality, there are not enough valid entries for x ({0:f}/{1:f}) or y ({2:f}/{3:f}).".format(np.sum(validx), countx, np.sum(validy), county), 391 | UserWarning) 392 | # TODO: use filter and cubic splines! 393 | #filter = np.logical_and(np.isfinite(self.x),np.isfinite(self.y)) 394 | if np.sum(validy) > np.sum(validx): 395 | self.x = interpolate_values_1d(self.y, self.x, x_points=self.y[validy]) 396 | self.y = self.y[validy] 397 | else: 398 | self.y = interpolate_values_1d(self.x, self.y, x_points=self.x[validx]) 399 | self.x = self.x[validx] 400 | 401 | 402 | class BasePlot(Base2DObject): 403 | """The base class for all plots. It can be instantiated itself, but provides many 404 | general facilities to be used in the different plots. """ 405 | 406 | # Define the iteration keys 407 | PROPERTIES = { 408 | CoolProp.iDmass: 'density', 409 | CoolProp.iHmass: 'specific enthalpy', 410 | CoolProp.iP: 'pressure', 411 | CoolProp.iSmass: 'specific entropy', 412 | CoolProp.iT: 'temperature', 413 | CoolProp.iUmass: 'specific internal energy' 414 | } 415 | 416 | # Define the unit systems 417 | UNIT_SYSTEMS = { 418 | 'SI': SIunits(), 419 | 'KSI': KSIunits(), 420 | 'EUR': EURunits() 421 | } 422 | 423 | LINE_PROPS = { 424 | CoolProp.iT: dict(color='Darkred', lw=0.25), 425 | CoolProp.iP: dict(color='DarkCyan', lw=0.25), 426 | CoolProp.iHmass: dict(color='DarkGreen', lw=0.25), 427 | CoolProp.iDmass: dict(color='DarkBlue', lw=0.25), 428 | CoolProp.iSmass: dict(color='DarkOrange', lw=0.25), 429 | CoolProp.iQ: dict(color='black', lw=0.25) 430 | } 431 | 432 | ID_FACTOR = 10.0 # Values below this number are interpreted as factors 433 | HI_FACTOR = 2.25 # Upper default limits: HI_FACTOR*T_crit and HI_FACTOR*p_crit 434 | LO_FACTOR = 1.01 # Lower default limits: LO_FACTOR*T_triple and LO_FACTOR*p_triple 435 | 436 | TP_LIMITS = { 437 | 'NONE': [None, None, None, None], 438 | 'DEF': [LO_FACTOR, HI_FACTOR, LO_FACTOR, HI_FACTOR], 439 | 'ACHP': [173.15, 493.15, 0.25e5, HI_FACTOR], 440 | 'ORC': [273.15, 673.15, 0.25e5, HI_FACTOR] 441 | } 442 | 443 | def __init__(self, fluid_ref, graph_type, unit_system='KSI', tp_limits='DEF', **kwargs): 444 | 445 | # Process the graph_type and set self._x_type and self._y_type 446 | graph_type = graph_type.upper() 447 | graph_type = graph_type.replace(r'RHO', r'D') 448 | if graph_type not in Base2DObject.PLOTS: 449 | raise ValueError("Invalid graph_type input, expected a string from {0:s}".format(str(self.PLOTS))) 450 | 451 | # Process the unit_system and set self._system 452 | self.system = unit_system 453 | # Process the plotting range based on T and p 454 | self.limits = tp_limits 455 | # Other properties 456 | #self.figure = kwargs.pop('figure', plt.figure(tight_layout=True)) 457 | 458 | self.figure = kwargs.get('figure', matplotlib.figure.Figure(tight_layout=True)) 459 | if 'axis' in kwargs and 'axes' not in kwargs: 460 | kwargs['axes'] = kwargs['axis'] 461 | self.axes = kwargs.get('axes', self.figure.add_subplot(111)) 462 | self.props = kwargs.get('props', None) 463 | 464 | # call the base class 465 | state = process_fluid_state(fluid_ref) 466 | Base2DObject.__init__(self, graph_type[1], graph_type[0], state, **kwargs) 467 | 468 | @property 469 | def system(self): return self._system 470 | 471 | @system.setter 472 | def system(self, value): 473 | value = value.upper() 474 | if value in self.UNIT_SYSTEMS: self._system = self.UNIT_SYSTEMS[value] 475 | else: raise ValueError("Invalid input, expected a string from {0:s}".format(str(self.UNIT_SYSTEMS.keys()))) 476 | 477 | @property 478 | def limits(self): 479 | """Returns [Tmin,Tmax,pmin,pmax] as value or factors""" 480 | return self._limits 481 | 482 | @limits.setter 483 | def limits(self, value): 484 | if is_string(value): 485 | value = value.upper() 486 | if value in self.TP_LIMITS: 487 | self._limits = self.TP_LIMITS[value] 488 | elif len(value) == 4: 489 | self._limits = value 490 | else: 491 | raise ValueError("Invalid input, expected a list with 4 items or a string from {0:s}".format(str(self.TP_LIMITS.keys()))) 492 | 493 | @property 494 | def figure(self): return self._figure 495 | 496 | @figure.setter 497 | def figure(self, value): self._figure = value 498 | 499 | @property 500 | def axis(self): 501 | warnings.warn("You use the deprecated property \"axis\", please use \"axes\" instead", DeprecationWarning) 502 | return self._axes 503 | 504 | @axis.setter 505 | def axis(self, value): 506 | warnings.warn("You use the deprecated property \"axis\", please use \"axes\" instead", DeprecationWarning) 507 | self._axes = value 508 | 509 | @property 510 | def axes(self): return self._axes 511 | 512 | @axes.setter 513 | def axes(self, value): self._axes = value 514 | 515 | @property 516 | def props(self): return self._props 517 | 518 | @props.setter 519 | def props(self, value): 520 | self._props = self.LINE_PROPS.copy() 521 | if value is not None: 522 | self._props.update(value) 523 | 524 | def __sat_bounds(self, kind, smin=None, smax=None): 525 | warnings.warn( 526 | "You called the deprecated function \"__sat_bounds\", \ 527 | consider replacing it with \"_get_sat_bounds\".", 528 | DeprecationWarning) 529 | return self._get_sat_bounds(kind, smin, smax) 530 | 531 | def _get_iso_label(self, isoline, unit=True): 532 | if self._system is not None: 533 | dim = self._system[isoline.i_index] 534 | return str(r"$" + dim.symbol + "=" + str(dim.from_SI(isoline.value)) + "$ " + dim.unit if unit else "$").strip() 535 | return str(isoline.value).strip() 536 | 537 | # def _get_phase_envelope(self): 538 | # 539 | #HEOS = CoolProp.AbstractState("HEOS", fluid) 540 | # HEOS.build_phase_envelope("") 541 | #PED = HEOS.get_phase_envelope_data() 542 | #plt.plot(PED.T, np.log(PED.p)) 543 | # plt.show() 544 | 545 | def _plot_default_annotations(self): 546 | # def filter_fluid_ref(fluid_ref): 547 | # fluid_ref_string = fluid_ref 548 | # if fluid_ref.startswith('REFPROP-MIX'): 549 | # end = 0 550 | # fluid_ref_string = '' 551 | # while fluid_ref.find('[', end + 1) != -1: 552 | # start = fluid_ref.find('&', end + 1) 553 | # if end == 0: 554 | # start = fluid_ref.find(':', end + 1) 555 | # end = fluid_ref.find('[', end + 1) 556 | # fluid_ref_string = ' '.join([fluid_ref_string, 557 | # fluid_ref[start+1:end], '+']) 558 | # fluid_ref_string = fluid_ref_string[0:len(fluid_ref_string)-2] 559 | # return fluid_ref_string 560 | # 561 | # if len(self.graph_type) == 2: 562 | # y_axis_id = self.graph_type[0] 563 | # x_axis_id = self.graph_type[1] 564 | # else: 565 | # y_axis_id = self.graph_type[0] 566 | # x_axis_id = self.graph_type[1:len(self.graph_type)] 567 | # 568 | # tl_str = "%s - %s Graph for %s" 569 | # if not self.axes.get_title(): 570 | # self.axes.set_title(tl_str % (self.AXIS_LABELS[self.unit_system][y_axis_id][0], 571 | # self.AXIS_LABELS[self.unit_system][x_axis_id][0], 572 | # filter_fluid_ref(self.fluid_ref))) 573 | if self._x_index in [CoolProp.iDmass, CoolProp.iP]: 574 | self.axes.set_xscale('log') 575 | if self._y_index in [CoolProp.iDmass, CoolProp.iP]: 576 | self.axes.set_yscale('log') 577 | 578 | if not self.axes.get_xlabel(): 579 | dim = self._system[self._x_index] 580 | self.xlabel((dim.label + u" $" + dim.symbol + u"$ / " + dim.unit).strip()) 581 | if not self.axes.get_ylabel(): 582 | dim = self._system[self._y_index] 583 | self.ylabel((dim.label + u" $" + dim.symbol + u"$ / " + dim.unit).strip()) 584 | 585 | def title(self, title): 586 | self.axes.set_title(title) 587 | 588 | def xlabel(self, xlabel): 589 | self.axes.set_xlabel(xlabel) 590 | 591 | def ylabel(self, ylabel): 592 | self.axes.set_ylabel(ylabel) 593 | 594 | def grid(self, b=None, **kwargs): 595 | g_map = {'on': True, 'off': False} 596 | if b is not None: 597 | b = g_map[b.lower()] 598 | if not kwargs: # len=0 599 | self.axes.grid(b) 600 | else: 601 | self.axes.grid(kwargs) 602 | 603 | def set_Tp_limits(self, limits): 604 | """Set the limits for the graphs in temperature and pressure, based on 605 | the active units: [Tmin, Tmax, pmin, pmax]""" 606 | dim = self._system[CoolProp.iT] 607 | limits[0] = dim.to_SI(limits[0]) 608 | limits[1] = dim.to_SI(limits[1]) 609 | dim = self._system[CoolProp.iP] 610 | limits[2] = dim.to_SI(limits[2]) 611 | limits[3] = dim.to_SI(limits[3]) 612 | self.limits = limits 613 | 614 | def get_Tp_limits(self): 615 | """Get the limits for the graphs in temperature and pressure, based on 616 | the active units: [Tmin, Tmax, pmin, pmax]""" 617 | limits = self._get_Tp_limits() 618 | dim = self._system[CoolProp.iT] 619 | limits[0] = dim.from_SI(limits[0]) 620 | limits[1] = dim.from_SI(limits[1]) 621 | dim = self._system[CoolProp.iP] 622 | limits[2] = dim.from_SI(limits[2]) 623 | limits[3] = dim.from_SI(limits[3]) 624 | return limits 625 | 626 | def _get_Tp_limits(self): 627 | """Get the limits for the graphs in temperature and pressure, based on 628 | SI units: [Tmin, Tmax, pmin, pmax]""" 629 | T_lo, T_hi, P_lo, P_hi = self.limits 630 | Ts_lo, Ts_hi = self._get_sat_bounds(CoolProp.iT) 631 | Ps_lo, Ps_hi = self._get_sat_bounds(CoolProp.iP) 632 | 633 | if T_lo is None: T_lo = 0.0 634 | elif T_lo < self.ID_FACTOR: T_lo *= Ts_lo 635 | if T_hi is None: T_hi = 1e6 636 | elif T_hi < self.ID_FACTOR: T_hi *= Ts_hi 637 | if P_lo is None: P_lo = 0.0 638 | elif P_lo < self.ID_FACTOR: P_lo *= Ps_lo 639 | if P_hi is None: P_hi = 1e10 640 | elif P_hi < self.ID_FACTOR: P_hi *= Ps_hi 641 | 642 | try: T_lo = np.nanmax([T_lo, self.state.trivial_keyed_output(CoolProp.iT_min)]) 643 | except: pass 644 | try: T_hi = np.nanmin([T_hi, self.state.trivial_keyed_output(CoolProp.iT_max)]) 645 | except: pass 646 | try: P_lo = np.nanmax([P_lo, self.state.trivial_keyed_output(CoolProp.iP_min)]) 647 | except: pass 648 | try: P_hi = np.nanmin([P_hi, self.state.trivial_keyed_output(CoolProp.iP_max)]) 649 | except: pass 650 | 651 | return [T_lo, T_hi, P_lo, P_hi] 652 | 653 | def set_axis_limits(self, limits): 654 | """Set the limits of the internal axes object based on the active units, 655 | takes [xmin, xmax, ymin, ymax]""" 656 | self.axes.set_xlim([limits[0], limits[1]]) 657 | self.axes.set_ylim([limits[2], limits[3]]) 658 | 659 | def set_axis_limits_SI(self, limits): 660 | """Set the limits of the internal axes object based on SI units, 661 | takes [xmin, xmax, ymin, ymax]""" 662 | dim = self._system[self._x_index] 663 | self.axes.set_xlim([dim.from_SI(limits[0]), dim.from_SI(limits[1])]) 664 | dim = self._system[self._y_index] 665 | self.axes.set_ylim([dim.from_SI(limits[2]), dim.from_SI(limits[3])]) 666 | 667 | def get_axis_limits(self, x_index=None, y_index=None): 668 | """Returns the previously set limits or generates them and 669 | converts the default values to the selected unit system. 670 | Returns a list containing [xmin, xmax, ymin, ymax]""" 671 | if x_index is None: x_index = self._x_index 672 | if y_index is None: y_index = self._y_index 673 | 674 | if x_index != self.x_index or y_index != self.y_index or \ 675 | self.axes.get_autoscalex_on() or self.axes.get_autoscaley_on(): 676 | # One of them is not set or we work on a different set of axes 677 | T_lo, T_hi, P_lo, P_hi = self._get_Tp_limits() 678 | 679 | X = [0.0] * 4; Y = [0.0] * 4 680 | i = -1 681 | for T in [T_lo, T_hi]: 682 | for P in [P_lo, P_hi]: 683 | i += 1 684 | try: 685 | self.state.update(CoolProp.PT_INPUTS, P, T) 686 | # TODO: include a check for P and T? 687 | X[i] = self.state.keyed_output(x_index) 688 | Y[i] = self.state.keyed_output(y_index) 689 | except: 690 | X[i] = np.nan; Y[i] = np.nan 691 | # Figure out what to update 692 | dim = self._system[x_index] 693 | x_lim = [dim.from_SI(np.nanmin(X)), dim.from_SI(np.nanmax(X))] 694 | dim = self._system[y_index] 695 | y_lim = [dim.from_SI(np.nanmin(Y)), dim.from_SI(np.nanmax(Y))] 696 | # Either update the axes limits or get them 697 | if x_index == self._x_index: 698 | if self.axes.get_autoscalex_on(): 699 | self.axes.set_xlim(x_lim) 700 | else: 701 | x_lim = self.axes.get_xlim() 702 | if y_index == self._y_index: 703 | if self.axes.get_autoscaley_on(): 704 | self.axes.set_ylim(y_lim) 705 | else: 706 | y_lim = self.axes.get_ylim() 707 | else: # We only asked for the real axes limits and they are set already 708 | x_lim = self.axes.get_xlim() 709 | y_lim = self.axes.get_ylim() 710 | 711 | return [x_lim[0], x_lim[1], y_lim[0], y_lim[1]] 712 | 713 | def get_axis_limits_SI(self, x_index=None, y_index=None): 714 | """Get the limits of the internal axes object in SI units 715 | Returns a list containing [xmin, xmax, ymin, ymax]""" 716 | if x_index is None: x_index = self._x_index 717 | if y_index is None: y_index = self._y_index 718 | limits = self.get_axis_limits(x_index, y_index) 719 | dim = self._system[x_index] 720 | limits[0] = dim.to_SI(limits[0]) 721 | limits[1] = dim.to_SI(limits[1]) 722 | dim = self._system[y_index] 723 | limits[2] = dim.to_SI(limits[2]) 724 | limits[3] = dim.to_SI(limits[3]) 725 | return limits 726 | 727 | @staticmethod 728 | def generate_ranges(itype, imin, imax, num): 729 | """Generate a range for a certain property""" 730 | if itype in [CoolProp.iP, CoolProp.iDmass]: 731 | return np.logspace(np.log2(imin), np.log2(imax), num=num, base=2.) 732 | return np.linspace(imin, imax, num=num) 733 | 734 | def _get_conversion_data(self): 735 | [Axmin, Axmax, Aymin, Aymax] = self.get_axis_limits_SI() 736 | DELTAX_axis = Axmax - Axmin 737 | DELTAY_axis = Aymax - Aymin 738 | width = self.figure.get_figwidth() 739 | height = self.figure.get_figheight() 740 | pos = self.axes.get_position().get_points() 741 | [[Fxmin, Fymin], [Fxmax, Fymax]] = pos 742 | DELTAX_fig = width * (Fxmax - Fxmin) 743 | DELTAY_fig = height * (Fymax - Fymin) 744 | return [[Axmin, Axmax, Aymin, Aymax, Fxmin, Fxmax, Fymin, Fymax], [DELTAX_axis, DELTAY_axis, DELTAX_fig, DELTAY_fig]] 745 | 746 | def _to_pixel_coords(self, xv, yv): 747 | [[Axmin, Axmax, Aymin, Aymax, Fxmin, Fxmax, Fymin, Fymax], [DELTAX_axis, DELTAY_axis, DELTAX_fig, DELTAY_fig]] = self._get_conversion_data() 748 | # Convert coords to pixels 749 | x = (xv - Axmin) / DELTAX_axis * DELTAX_fig + Fxmin 750 | y = (yv - Aymin) / DELTAY_axis * DELTAY_fig + Fymin 751 | return x, y 752 | 753 | def _to_data_coords(self, xv, yv): 754 | [[Axmin, Axmax, Aymin, Aymax, Fxmin, Fxmax, Fymin, Fymax], [DELTAX_axis, DELTAY_axis, DELTAX_fig, DELTAY_fig]] = self._get_conversion_data() 755 | # Convert back to measurements 756 | x = (xv - Fxmin) / DELTAX_fig * DELTAX_axis + Axmin 757 | y = (yv - Fymin) / DELTAY_fig * DELTAY_axis + Aymin 758 | return x, y 759 | 760 | @staticmethod 761 | def get_x_y_dydx(xv, yv, x): 762 | """Get x and y coordinates and the linear interpolation derivative""" 763 | # Old implementation: 764 | # Get the rotation angle 765 | #f = interp1d(xv, yv) 766 | #y = f(x) 767 | #h = 0.00001*x 768 | #dy_dx = (f(x+h)-f(x-h))/(2*h) 769 | # return x,y,dy_dx 770 | if len(xv) == len(yv) and len(yv) > 1: # assure same length 771 | if len(xv) == len(yv) and len(yv) == 2: # only two points 772 | if np.min(xv) < x < np.max(xv): 773 | dx = xv[1] - xv[0] 774 | dy = yv[1] - yv[0] 775 | dydx = dy / dx 776 | y = yv[0] + dydx * (x - xv[0]) 777 | return x, y, dydx 778 | else: 779 | raise ValueError("Your coordinate has to be between the input values.") 780 | else: 781 | limit = 1e-10 # avoid hitting a point directly 782 | diff = np.array(xv) - x # get differences 783 | index = np.argmin(diff * diff) # nearest neighbour 784 | if (xv[index] < x < xv[index + 1] # nearest below, positive inclination 785 | or xv[index] > x > xv[index + 1]): # nearest above, negative inclination 786 | if diff[index] < limit: 787 | index = [index - 1, index + 1] 788 | else: 789 | index = [index, index + 1] 790 | elif (xv[index - 1] < x < xv[index] # nearest above, positive inclination 791 | or xv[index - 1] > x > xv[index]): # nearest below, negative inclination 792 | if diff[index] < limit: 793 | index = [index - 1, index + 1] 794 | else: 795 | index = [index - 1, index] 796 | xvnew = xv[index] 797 | yvnew = yv[index] 798 | return BasePlot.get_x_y_dydx(xvnew, yvnew, x) # Allow for a single recursion 799 | else: 800 | raise ValueError("You have to provide the same amount of x- and y-pairs with at least two entries each.") 801 | 802 | def _inline_label(self, xv, yv, x=None, y=None): 803 | """ 804 | This will give the coordinates and rotation required to align a label with 805 | a line on a plot in SI units. 806 | """ 807 | if y is None and x is not None: 808 | trash = 0 809 | (xv, yv) = self._to_pixel_coords(xv, yv) 810 | # x is provided but y isn't 811 | (x, trash) = self._to_pixel_coords(x, trash) 812 | 813 | # Get the rotation angle and y-value 814 | x, y, dy_dx = BasePlot.get_x_y_dydx(xv, yv, x) 815 | rot = np.arctan(dy_dx) / np.pi * 180. 816 | 817 | elif x is None and y is not None: 818 | # y is provided, but x isn't 819 | _xv = xv[::-1] 820 | _yv = yv[::-1] 821 | # Find x by interpolation 822 | x = interpolate_values_1d(yv, xv, x_points=y) 823 | trash = 0 824 | (xv, yv) = self._to_pixel_coords(xv, yv) 825 | (x, trash) = self._to_pixel_coords(x, trash) 826 | 827 | # Get the rotation angle and y-value 828 | x, y, dy_dx = BasePlot.get_x_y_dydx(xv, yv, x) 829 | rot = np.arctan(dy_dx) / np.pi * 180. 830 | (x, y) = self._to_data_coords(x, y) 831 | return (x, y, rot) 832 | 833 | def inline_label(self, xv, yv, x=None, y=None): 834 | """ 835 | This will give the coordinates and rotation required to align a label with 836 | a line on a plot in axis units. 837 | """ 838 | dimx = self._system[self._x_index] 839 | xv = dimx.to_SI(xv) 840 | if x is not None: x = dimx.to_SI(x) 841 | dimy = self._system[self._y_index] 842 | yv = dimy.to_SI(yv) 843 | if y is not None: y = dimy.to_SI(y) 844 | (x, y, rot) = self._inline_label(xv, yv, x, y) 845 | x = dimx.from_SI(x) 846 | y = dimy.from_SI(y) 847 | return (x, y, rot) 848 | 849 | def plot_SI(self, _x, _y, *args, **kwargs): 850 | dimx = self._system[self._x_index] 851 | x = dimx.from_SI(_x) 852 | dimy = self._system[self._y_index] 853 | y = dimy.from_SI(_y) 854 | return self.axes.plot(x, y, *args, **kwargs) 855 | 856 | def show(self): 857 | plt.show() 858 | 859 | def savefig(self, *args, **kwargs): 860 | self.figure.savefig(*args, **kwargs) 861 | 862 | 863 | if __name__ == "__main__": 864 | for sys in [SIunits(), KSIunits(), EURunits()]: 865 | print(sys.H.label) 866 | print(sys.H.to_SI(20)) 867 | print(sys.P.label) 868 | print(sys.P.to_SI(20)) 869 | 870 | # i_index, x_index, y_index, value=None, state=None) 871 | iso = IsoLine('T', 'H', 'P') 872 | print(iso.get_update_pair()) 873 | 874 | state = AbstractState("HEOS", "water") 875 | iso = IsoLine('T', 'H', 'P', 300.0, state) 876 | hr = PropsSI("H", "T", [290, 310], "P", [1e5, 1e5], "water") 877 | pr = np.linspace(0.9e5, 1.1e5, 3) 878 | iso.calc_range(hr, pr) 879 | print(iso.x, iso.y) 880 | 881 | iso = IsoLine('Q', 'H', 'P', 0.0, state) 882 | iso.calc_range(hr, pr); print(iso.x, iso.y) 883 | iso = IsoLine('Q', 'H', 'P', 1.0, state) 884 | iso.calc_range(hr, pr); print(iso.x, iso.y) 885 | 886 | # bp = BasePlot(fluid_ref, graph_type, unit_system = 'KSI', **kwargs): 887 | bp = BasePlot('n-Pentane', 'PH', unit_system='EUR') 888 | # print(bp._get_sat_bounds('P')) 889 | # print(bp._get_iso_label(iso)) 890 | print(bp.get_axis_limits()) 891 | 892 | # get_update_pair(CoolProp.iP,CoolProp.iSmass,CoolProp.iT) -> (0,1,2,CoolProp.PSmass_INPUTS) 893 | # other values require switching and swapping 894 | # get_update_pair(CoolProp.iSmass,CoolProp.iP,CoolProp.iHmass) -> (1,0,2,CoolProp.PSmass_INPUTS) 895 | -------------------------------------------------------------------------------- /CoolPlot/Plot/ConsistencyPlots.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import 3 | 4 | 5 | import matplotlib.pyplot as plt 6 | import numpy as np 7 | import time, timeit 8 | import six 9 | import pandas 10 | import CoolProp as CP 11 | 12 | CP.CoolProp.set_debug_level(00) 13 | from matplotlib.backends.backend_pdf import PdfPages 14 | 15 | all_solvers = ['PT', 'DmolarT', 'HmolarP', 'PSmolar', 'SmolarT', 'PUmolar', 'DmolarP', 'DmolarHmolar', 'DmolarSmolar', 'DmolarUmolar', 'HmolarSmolar', 'HmolarT', 'TUmolar', 'SmolarUmolar', 'HmolarUmolar'] 16 | not_implemented_solvers = ['SmolarUmolar', 'HmolarUmolar', 'TUmolar', 'HmolarT'] 17 | no_two_phase_solvers = ['PT'] 18 | 19 | implemented_solvers = [pair for pair in all_solvers if pair not in not_implemented_solvers] 20 | 21 | param_labels = dict(Hmolar='Enthalpy [J/mol]/1000', 22 | Smolar='Entropy [J/mol/K]/1000', 23 | Umolar='Int. Ener. [J/mol]/1000', 24 | T='Temperature [K]', 25 | Dmolar='Density [mol/m3]/1000', 26 | P='Pressure [Pa]/1000') 27 | 28 | 29 | def split_pair(pair): 30 | for key in ['Dmolar', 'Hmolar', 'Smolar', 'P', 'T', 'Umolar']: 31 | if pair.startswith(key): 32 | return key, pair.replace(key, '') 33 | 34 | 35 | def split_pair_xy(pair): 36 | if pair == 'HmolarP': 37 | return 'Hmolar', 'P' 38 | elif pair == 'PSmolar': 39 | return 'Smolar', 'P' 40 | elif pair == 'PUmolar': 41 | return 'Umolar', 'P' 42 | elif pair == 'PT': 43 | return 'T', 'P' 44 | elif pair == 'DmolarT': 45 | return 'Dmolar', 'T' 46 | elif pair == 'SmolarT': 47 | return 'Smolar', 'T' 48 | elif pair == 'TUmolar': 49 | return 'Umolar', 'T' 50 | elif pair == 'HmolarT': 51 | return 'Hmolar', 'T' 52 | elif pair == 'DmolarP': 53 | return 'Dmolar', 'P' 54 | elif pair == 'DmolarHmolar': 55 | return 'Dmolar', 'Hmolar' 56 | elif pair == 'DmolarSmolar': 57 | return 'Dmolar', 'Smolar' 58 | elif pair == 'DmolarUmolar': 59 | return 'Dmolar', 'Umolar' 60 | elif pair == 'HmolarSmolar': 61 | return 'Smolar', 'Hmolar' 62 | elif pair == 'SmolarUmolar': 63 | return 'Smolar', 'Umolar' 64 | elif pair == 'HmolarUmolar': 65 | return 'Hmolar', 'Umolar' 66 | else: 67 | raise ValueError(pair) 68 | 69 | 70 | DEBUG_LEVEL = 1 71 | 72 | 73 | def myprint(level, *args, **kwargs): 74 | if level > DEBUG_LEVEL: 75 | print(*args, **kwargs) 76 | 77 | 78 | class ConsistencyFigure(object): 79 | def __init__(self, fluid, figsize=(15, 23), backend='HEOS', additional_skips=[], mole_fractions=None, p_limits_1phase=None, T_limits_1phase=None, NT_1phase=40, Np_1phase=40, NT_2phase=20, NQ_2phase=20): 80 | 81 | self.fluid = fluid 82 | self.backend = backend 83 | print('***********************************************************************************') 84 | print('*************** ' + backend + '::' + fluid + ' ************************') 85 | print('***********************************************************************************') 86 | self.fig, self.axes = plt.subplots(nrows=5, ncols=3, figsize=figsize) 87 | self.pairs = all_solvers 88 | pairs_generator = iter(self.pairs) 89 | 90 | states = [CP.AbstractState(backend, fluid) for _ in range(3)] 91 | if mole_fractions is not None: 92 | for state in states: 93 | state.set_mole_fractions(mole_fractions) 94 | self.axes_list = [] 95 | for row in self.axes: 96 | for ax in row: 97 | pair = six.next(pairs_generator) 98 | kwargs = dict(p_limits_1phase=p_limits_1phase, T_limits_1phase=T_limits_1phase, NT_1phase=NT_1phase, Np_1phase=Np_1phase, 99 | NT_2phase=NT_2phase, NQ_2phase=NQ_2phase) 100 | self.axes_list.append(ConsistencyAxis(ax, self, pair, self.fluid, self.backend, *states, **kwargs)) 101 | ax.set_title(pair) 102 | 103 | self.calc_saturation_curves() 104 | self.plot_saturation_curves() 105 | 106 | self.calc_Tmax_curve() 107 | self.plot_Tmax_curve() 108 | 109 | self.calc_melting_curve() 110 | self.plot_melting_curve() 111 | 112 | self.tight_layout() 113 | 114 | self.fig.subplots_adjust(top=0.95) 115 | self.fig.suptitle('Consistency plots for ' + self.fluid, size=14) 116 | 117 | errors = [] 118 | for i, (ax, pair) in enumerate(zip(self.axes_list, self.pairs)): 119 | if pair not in not_implemented_solvers and pair not in additional_skips: 120 | errors.append(ax.consistency_check_singlephase()) 121 | if pair not in no_two_phase_solvers: 122 | ax.consistency_check_twophase() 123 | else: 124 | ax.cross_out_axis() 125 | 126 | self.errors = pandas.concat(errors, sort=True) 127 | 128 | def calc_saturation_curves(self): 129 | """ 130 | Calculate all the saturation curves in one shot using the state class to save computational time 131 | """ 132 | HEOS = CP.AbstractState(self.backend, self.fluid) 133 | self.dictL, self.dictV = {}, {} 134 | for Q, dic in zip([0, 1], [self.dictL, self.dictV]): 135 | rhomolar, smolar, hmolar, T, p, umolar = [], [], [], [], [], [] 136 | for _T in np.logspace(np.log10(HEOS.keyed_output(CP.iT_triple)), np.log10(HEOS.keyed_output(CP.iT_critical)), 500): 137 | try: 138 | HEOS.update(CP.QT_INPUTS, Q, _T) 139 | if (HEOS.p() < 0): raise ValueError('P is negative:' + str(HEOS.p())) 140 | HEOS.T(), HEOS.p(), HEOS.rhomolar(), HEOS.hmolar(), HEOS.smolar() 141 | HEOS.umolar() 142 | 143 | T.append(HEOS.T()) 144 | p.append(HEOS.p()) 145 | rhomolar.append(HEOS.rhomolar()) 146 | hmolar.append(HEOS.hmolar()) 147 | smolar.append(HEOS.smolar()) 148 | umolar.append(HEOS.umolar()) 149 | except ValueError as VE: 150 | myprint(1, 'satT error:', VE, '; T:', '{T:0.16g}'.format(T=_T), 'T/Tc:', _T / HEOS.keyed_output(CP.iT_critical)) 151 | 152 | dic.update(dict(T=np.array(T), 153 | P=np.array(p), 154 | Dmolar=np.array(rhomolar), 155 | Hmolar=np.array(hmolar), 156 | Smolar=np.array(smolar), 157 | Umolar=np.array(umolar))) 158 | 159 | def plot_saturation_curves(self): 160 | for ax in self.axes_list: 161 | ax.label_axes() 162 | ax.plot_saturation_curves() 163 | 164 | def calc_Tmax_curve(self): 165 | HEOS = CP.AbstractState(self.backend, self.fluid) 166 | rhomolar, smolar, hmolar, T, p, umolar = [], [], [], [], [], [] 167 | 168 | for _p in np.logspace(np.log10(HEOS.keyed_output(CP.iP_min) * 1.01), np.log10(HEOS.keyed_output(CP.iP_max)), 300): 169 | try: 170 | HEOS.update(CP.PT_INPUTS, _p, HEOS.keyed_output(CP.iT_max)) 171 | except ValueError as VE: 172 | myprint(1, 'Tmax', _p, VE) 173 | continue 174 | 175 | try: 176 | T.append(HEOS.T()) 177 | p.append(HEOS.p()) 178 | rhomolar.append(HEOS.rhomolar()) 179 | hmolar.append(HEOS.hmolar()) 180 | smolar.append(HEOS.smolar()) 181 | umolar.append(HEOS.umolar()) 182 | except ValueError as VE: 183 | myprint(1, 'Tmax access', VE) 184 | 185 | self.Tmax = dict(T=np.array(T), 186 | P=np.array(p), 187 | Dmolar=np.array(rhomolar), 188 | Hmolar=np.array(hmolar), 189 | Smolar=np.array(smolar), 190 | Umolar=np.array(umolar)) 191 | 192 | def plot_Tmax_curve(self): 193 | for ax in self.axes_list: 194 | ax.plot_Tmax_curve() 195 | 196 | def calc_melting_curve(self): 197 | state = CP.AbstractState('HEOS', self.fluid) 198 | rhomolar, smolar, hmolar, T, p, umolar = [], [], [], [], [], [] 199 | 200 | # Melting line if it has it 201 | if state.has_melting_line(): 202 | pmelt_min = max(state.melting_line(CP.iP_min, -1, -1), state.keyed_output(CP.iP_triple)) * 1.01 203 | pmelt_max = min(state.melting_line(CP.iP_max, -1, -1), state.keyed_output(CP.iP_max)) * 0.99 204 | 205 | for _p in np.logspace(np.log10(pmelt_min), np.log10(pmelt_max), 100): 206 | try: 207 | Tm = state.melting_line(CP.iT, CP.iP, _p) 208 | state.update(CP.PT_INPUTS, _p, Tm) 209 | T.append(state.T()) 210 | p.append(state.p()) 211 | rhomolar.append(state.rhomolar()) 212 | hmolar.append(state.hmolar()) 213 | smolar.append(state.smolar()) 214 | umolar.append(state.umolar()) 215 | except ValueError as VE: 216 | myprint(1, 'melting', VE) 217 | 218 | self.melt = dict(T=np.array(T), 219 | P=np.array(p), 220 | Dmolar=np.array(rhomolar), 221 | Hmolar=np.array(hmolar), 222 | Smolar=np.array(smolar), 223 | Umolar=np.array(umolar)) 224 | 225 | def plot_melting_curve(self): 226 | for ax in self.axes_list: 227 | ax.plot_melting_curve() 228 | 229 | def tight_layout(self): 230 | self.fig.tight_layout() 231 | 232 | def add_to_pdf(self, pdf): 233 | """ Add this figure to the pdf instance """ 234 | pdf.savefig(self.fig) 235 | 236 | def savefig(self, fname, **kwargs): 237 | self.fig.savefig(fname, **kwargs) 238 | 239 | 240 | class ConsistencyAxis(object): 241 | def __init__(self, axis, fig, pair, fluid, backend, state1, state2, state3, 242 | p_limits_1phase=None, T_limits_1phase=None, NT_1phase=40, Np_1phase=40, 243 | NT_2phase=20, NQ_2phase=20 244 | ): 245 | self.ax = axis 246 | self.fig = fig 247 | self.pair = pair 248 | self.fluid = fluid 249 | self.backend = backend 250 | self.state = state1 251 | self.state_PT = state2 252 | self.state_QT = state3 253 | self.p_limits_1phase = p_limits_1phase 254 | self.T_limits_1phase = T_limits_1phase 255 | self.NT_1phase = NT_1phase 256 | self.Np_1phase = Np_1phase 257 | self.NQ_2phase = NQ_2phase 258 | self.NT_2phase = NT_2phase 259 | # self.saturation_curves() 260 | 261 | def label_axes(self): 262 | """ Label the axes for the given pair """ 263 | xparam, yparam = split_pair_xy(self.pair) 264 | self.ax.set_xlabel(param_labels[xparam]) 265 | self.ax.set_ylabel(param_labels[yparam]) 266 | 267 | if xparam in ['P', 'Dmolar']: 268 | self.ax.set_xscale('log') 269 | if yparam in ['P', 'Dmolar']: 270 | self.ax.set_yscale('log') 271 | 272 | def plot_saturation_curves(self): 273 | xparam, yparam = split_pair_xy(self.pair) 274 | xL = self.to_axis_units(xparam, self.fig.dictL[xparam]) 275 | yL = self.to_axis_units(yparam, self.fig.dictL[yparam]) 276 | xV = self.to_axis_units(xparam, self.fig.dictV[xparam]) 277 | yV = self.to_axis_units(yparam, self.fig.dictV[yparam]) 278 | self.ax.plot(xL, yL, 'k', lw=1) 279 | self.ax.plot(xV, yV, 'k', lw=1) 280 | 281 | def plot_Tmax_curve(self): 282 | xparam, yparam = split_pair_xy(self.pair) 283 | x = self.to_axis_units(xparam, self.fig.Tmax[xparam]) 284 | y = self.to_axis_units(yparam, self.fig.Tmax[yparam]) 285 | self.ax.plot(x, y, 'r', lw=1) 286 | 287 | def plot_melting_curve(self): 288 | xparam, yparam = split_pair_xy(self.pair) 289 | x = self.to_axis_units(xparam, self.fig.melt[xparam]) 290 | y = self.to_axis_units(yparam, self.fig.melt[yparam]) 291 | self.ax.plot(x, y, 'b', lw=1) 292 | 293 | def to_axis_units(self, label, vals): 294 | """ Convert to the units used in the plot """ 295 | if label in ['Hmolar', 'Smolar', 'Umolar', 'Dmolar', 'P']: 296 | return vals / 1000 297 | elif label in ['T']: 298 | return vals 299 | else: 300 | raise ValueError(label) 301 | 302 | def consistency_check_singlephase(self): 303 | 304 | tic = time.time() 305 | 306 | # Update the state given the desired set of inputs 307 | param1, param2 = split_pair(self.pair) 308 | key1 = getattr(CP, 'i' + param1) 309 | key2 = getattr(CP, 'i' + param2) 310 | pairkey = getattr(CP, self.pair + '_INPUTS') 311 | 312 | # Get the keys and indices and values for the inputs needed 313 | xparam, yparam = split_pair_xy(self.pair) 314 | xkey = getattr(CP, 'i' + xparam) 315 | ykey = getattr(CP, 'i' + yparam) 316 | 317 | data = [] 318 | 319 | if self.p_limits_1phase is not None: 320 | # User-specified limits were provided, use them 321 | p_min, p_max = self.p_limits_1phase 322 | else: 323 | # No user-specified limits were provided, use the defaults 324 | p_min = self.state.keyed_output(CP.iP_min) * 1.01 325 | p_max = self.state.keyed_output(CP.iP_max) 326 | 327 | for p in np.logspace(np.log10(p_min), np.log10(p_max), self.Np_1phase): 328 | 329 | if self.T_limits_1phase is None: 330 | # No user-specified limits were provided, using the defaults 331 | Tmin = self.state.keyed_output(CP.iT_triple) 332 | if self.state.has_melting_line(): 333 | try: 334 | pmelt_min = self.state.melting_line(CP.iP_min, -1, -1) 335 | if p < pmelt_min: 336 | T0 = Tmin 337 | else: 338 | T0 = self.state.melting_line(CP.iT, CP.iP, p) 339 | except Exception as E: 340 | T0 = Tmin + 1.1 341 | data.append(dict(err=str(E), type="melting", input=p)) 342 | myprint(1, 'MeltingLine:', E) 343 | else: 344 | T0 = Tmin + 1.1 345 | Tvec = np.linspace(T0, self.state.keyed_output(CP.iT_max), self.NT_1phase) 346 | else: 347 | # Use the provided limits for T 348 | Tvec = np.linspace(self.T_limits_1phase[0], self.T_limits_1phase[1], self.NT_1phase) 349 | 350 | for T in Tvec: 351 | 352 | try: 353 | # Update the state using PT inputs in order to calculate all the remaining inputs 354 | self.state_PT.update(CP.PT_INPUTS, p, T) 355 | except ValueError as VE: 356 | data.append(dict(err=str(VE), cls="EXCEPTION", type="update", in1="P", val1=p, in2="T", val2=T)) 357 | myprint(1, 'consistency', VE) 358 | continue 359 | 360 | _exception = False 361 | tic2 = timeit.default_timer() 362 | try: 363 | val1, val2 = self.state_PT.keyed_output(key1), self.state_PT.keyed_output(key2) 364 | self.state.update(pairkey, val1, val2) 365 | toc2 = timeit.default_timer() 366 | except ValueError as VE: 367 | data.append(dict(err=str(VE), cls="EXCEPTION", type="update", in1=param1, val1=val1, in2=param2, val2=val2)) 368 | myprint(1, 'update(1p)', self.pair, 'P', p, 'T', T, 'D', self.state_PT.keyed_output(CP.iDmolar), '{0:18.16g}, {1:18.16g}'.format(self.state_PT.keyed_output(key1), self.state_PT.keyed_output(key2)), VE) 369 | _exception = True 370 | 371 | x = self.to_axis_units(xparam, self.state_PT.keyed_output(xkey)) 372 | y = self.to_axis_units(yparam, self.state_PT.keyed_output(ykey)) 373 | 374 | if not _exception: 375 | # Check the error on the density 376 | if abs(self.state_PT.rhomolar() / self.state.rhomolar() - 1) < 1e-3 and abs(self.state_PT.p() / self.state.p() - 1) < 1e-3 and abs(self.state_PT.T() - self.state.T()) < 1e-3: 377 | data.append(dict(cls="GOOD", x=x, y=y, elapsed=toc2 - tic2)) 378 | if 'REFPROP' not in self.backend: 379 | if self.state_PT.phase() != self.state.phase(): 380 | myprint(1, 'bad phase', self.pair, '{0:18.16g}, {1:18.16g}'.format(self.state_PT.keyed_output(key1), self.state_PT.keyed_output(key2)), self.state.phase(), 'instead of', self.state_PT.phase()) 381 | else: 382 | data.append(dict(cls="INCONSISTENT", type="update", in1=param1, val1=val1, in2=param2, val2=val2, x=x, y=y)) 383 | myprint(1, 'bad', self.pair, '{0:18.16g}, {1:18.16g}'.format(self.state_PT.keyed_output(key1), self.state_PT.keyed_output(key2)), 'T:', self.state_PT.T(), 'Drho:', abs(self.state_PT.rhomolar() / self.state.rhomolar() - 1), abs(self.state_PT.p() / self.state.p() - 1), 'DT:', abs(self.state_PT.T() - self.state.T())) 384 | 385 | toc = time.time() 386 | df = pandas.DataFrame(data) 387 | bad = df[df.cls == 'INCONSISTENT'] 388 | good = df[df.cls == 'GOOD'] 389 | slowgood = good[good.elapsed > 0.01] 390 | excep = df[df.cls == 'EXCEPTION'] 391 | badphase = df[df.cls == 'BAD_PHASE'] 392 | 393 | self.ax.plot(bad.x, bad.y, 'r+', ms=3) 394 | self.ax.plot(good.x, good.y, 'k.', ms=1) 395 | self.ax.plot(excep.x, excep.y, 'rx', ms=3) 396 | self.ax.plot(slowgood.x, slowgood.y, 'b*', ms=6) 397 | self.ax.plot(badphase.x, badphase.y, 'o', ms=3, mfc='none') 398 | 399 | print('1-phase took ' + str(toc - tic) + ' s for ' + self.pair) 400 | 401 | if self.pair == 'HmolarSmolar': 402 | # plt.plot(good.elapsed) 403 | # plt.title(self.pair) 404 | # plt.show() 405 | 406 | good.to_excel('times_water.xlsx') 407 | return df[df.cls != 'GOOD'] 408 | 409 | def consistency_check_twophase(self): 410 | 411 | tic = time.time() 412 | state = self.state 413 | 414 | try: 415 | if state.fluid_param_string('pure') == 'false': 416 | print("Not a pure-fluid, skipping two-phase evaluation") 417 | return 418 | except: 419 | pass 420 | 421 | # Update the state given the desired set of inputs 422 | param1, param2 = split_pair(self.pair) 423 | key1 = getattr(CP, 'i' + param1) 424 | key2 = getattr(CP, 'i' + param2) 425 | pairkey = getattr(CP, self.pair + '_INPUTS') 426 | 427 | # Get the keys and indices and values for the inputs needed 428 | xparam, yparam = split_pair_xy(self.pair) 429 | xkey = getattr(CP, 'i' + xparam) 430 | ykey = getattr(CP, 'i' + yparam) 431 | 432 | data = [] 433 | for q in np.linspace(0, 1, self.NQ_2phase): 434 | 435 | Tmin = state.keyed_output(CP.iT_triple) + 1 436 | 437 | for T in np.linspace(Tmin, state.keyed_output(CP.iT_critical) - 1, self.NT_2phase): 438 | 439 | try: 440 | # Update the state using QT inputs in order to calculate all the remaining inputs 441 | self.state_QT.update(CP.QT_INPUTS, q, T) 442 | except ValueError as VE: 443 | data.append(dict(err=str(VE), cls="EXCEPTION", type="update", in1="Q", val1=q, in2="T", val2=T)) 444 | myprint(1, 'consistency', VE) 445 | continue 446 | 447 | _exception = False 448 | try: 449 | val1, val2 = self.state_QT.keyed_output(key1), self.state_QT.keyed_output(key2) 450 | state.update(pairkey, val1, val2) 451 | except ValueError as VE: 452 | data.append(dict(err=str(VE), cls="EXCEPTION", type="update", in1=param1, val1=val1, in2=param2, val2=val2)) 453 | myprint(1, 'update_QT', T, q) 454 | myprint(1, 'update', param1, self.state_QT.keyed_output(key1), param2, self.state_QT.keyed_output(key2), VE) 455 | _exception = True 456 | 457 | x = self.to_axis_units(xparam, self.state_QT.keyed_output(xkey)) 458 | y = self.to_axis_units(yparam, self.state_QT.keyed_output(ykey)) 459 | 460 | if not _exception: 461 | # Check the error on the density 462 | if abs(self.state_QT.rhomolar() / self.state.rhomolar() - 1) < 1e-3 and abs(self.state_QT.p() / self.state.p() - 1) < 1e-3 and abs(self.state_QT.T() - self.state.T()) < 1e-3: 463 | data.append(dict(cls="GOOD", x=x, y=y)) 464 | if 'REFPROP' not in self.backend: 465 | if self.state_QT.phase() != self.state.phase(): 466 | myprint(1, 'bad phase (2phase)', self.pair, '{0:18.16g}, {1:18.16g}'.format(self.state_QT.keyed_output(key1), self.state_QT.keyed_output(key2)), self.state.phase(), 'instead of', self.state_QT.phase()) 467 | 468 | else: 469 | myprint(1, 'Q', q) 470 | myprint(1, 'bad(2phase)', self.pair, '{0:18.16g}, {1:18.16g}'.format(self.state_QT.keyed_output(key1), self.state_QT.keyed_output(key2)), 'pnew:', self.state.p(), 'pold:', self.state_QT.p(), 'Tnew:', self.state.T(), 'T:', self.state_QT.T(), 'Drho:', abs(self.state_QT.rhomolar() / self.state.rhomolar() - 1), 'DP', abs(self.state_QT.p() / self.state.p() - 1), 'DT:', abs(self.state_QT.T() - self.state.T())) 471 | data.append(dict(cls="INCONSISTENT", type="update", in1=param1, val1=val1, in2=param2, val2=val2, x=x, y=y)) 472 | 473 | toc = time.time() 474 | df = pandas.DataFrame(data) 475 | bad = df[df.cls == 'INCONSISTENT'] 476 | good = df[df.cls == 'GOOD'] 477 | excep = df[df.cls == 'EXCEPTION'] 478 | badphase = df[df.cls == 'BAD_PHASE'] 479 | 480 | self.ax.plot(bad.x, bad.y, 'r+', ms=3) 481 | self.ax.plot(good.x, good.y, 'k.', ms=1) 482 | self.ax.plot(excep.x, excep.y, 'rx', ms=3) 483 | self.ax.plot(badphase.x, badphase.y, 'o', ms=3, mfc='none') 484 | print('2-phase took ' + str(toc - tic) + ' s for ' + self.pair) 485 | 486 | def cross_out_axis(self): 487 | xlims = self.ax.get_xlim() 488 | ylims = self.ax.get_ylim() 489 | self.ax.plot([xlims[0], xlims[1]], [ylims[0], ylims[1]], lw=3, c='r') 490 | self.ax.plot([xlims[0], xlims[1]], [ylims[1], ylims[0]], lw=3, c='r') 491 | 492 | xparam, yparam = split_pair_xy(self.pair) 493 | x = 0.5 * xlims[0] + 0.5 * xlims[1] 494 | y = 0.5 * ylims[0] + 0.5 * ylims[1] 495 | if xparam in ['P', 'Dmolar']: 496 | x = (xlims[0] * xlims[1])**0.5 497 | if yparam in ['P', 'Dmolar']: 498 | y = (ylims[0] * ylims[1])**0.5 499 | 500 | self.ax.text(x, y, 'Not\nImplemented', ha='center', va='center', bbox=dict(fc='white')) 501 | 502 | 503 | if __name__ == '__main__': 504 | PVT = PdfPages('Consistency.pdf') 505 | CP.CoolProp.set_debug_level(0) 506 | open('timelog.txt', 'w') 507 | with open('timelog.txt', 'a+', buffering=1) as fp: 508 | for fluid in ['METHANOL']: # CP.__fluids__: 509 | tic = timeit.default_timer() 510 | skips = ['DmolarHmolar', 'DmolarSmolar', 'DmolarUmolar', 'HmolarSmolar'] 511 | skips = [] 512 | ff = ConsistencyFigure(fluid, backend='HEOS', additional_skips=skips) # , NT_1phase = 10, Np_1phase = 10, NT_2phase = 100, NQ_2phase = 0) 513 | ff.errors.to_excel('Errors' + fluid + '.xlsx') 514 | toc = timeit.default_timer() 515 | print('Time to build:', toc - tic, 'seconds') 516 | ff.add_to_pdf(PVT) 517 | ff.savefig(fluid + '.png') 518 | ff.savefig(fluid + '.pdf') 519 | plt.close() 520 | fp.write('Time to build: {0} seconds for {1}\n'.format(toc - tic, fluid)) 521 | del ff 522 | PVT.close() 523 | -------------------------------------------------------------------------------- /CoolPlot/Plot/Plots.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division 3 | 4 | import numpy as np 5 | import warnings 6 | 7 | import CoolProp 8 | 9 | from .Common import IsoLine, BasePlot, interpolate_values_1d 10 | from .SimpleCycles import StateContainer 11 | 12 | 13 | class PropertyPlot(BasePlot): 14 | def __init__(self, fluid_name, graph_type, **kwargs): 15 | """ 16 | Create graph for the specified fluid properties 17 | 18 | Parameters 19 | ---------- 20 | fluid_name : string or AbstractState 21 | The name of the fluid to be plotted or a state instance 22 | graph_type : string 23 | The graph type to be plotted, like \"PH\" or \"TS\" 24 | axis : :func:`matplotlib.pyplot.gca()`, Optional 25 | The current axis system to be plotted to. 26 | Default: create a new axis system 27 | fig : :func:`matplotlib.pyplot.figure()`, Optional 28 | The current figure to be plotted to. 29 | Default: create a new figure 30 | unit_system : string, ['EUR','KSI','SI'] 31 | Select the units used for the plotting. 'EUR' is bar, kJ, C; 'KSI' is kPa, kJ, K; 'SI' is Pa, J, K 32 | tp_limits : string, ['NONE','DEF','ACHP','ORC'] 33 | Select the limits in T and p. 34 | reciprocal_density : bool 35 | NOT IMPLEMENTED: If True, 1/rho will be plotted instead of rho 36 | 37 | Examples 38 | -------- 39 | >>> from CoolProp.Plots import PropertyPlot 40 | >>> plot = PropertyPlot('HEOS::Water', 'TS') 41 | >>> plot.calc_isolines() 42 | >>> plot.show() 43 | 44 | >>> import CoolProp 45 | >>> from CoolProp.Plots import PropertyPlot 46 | >>> plot = PropertyPlot('HEOS::R134a', 'PH', unit_system='EUR', tp_limits='ACHP') 47 | >>> plot.calc_isolines(CoolProp.iQ, num=11) 48 | >>> plot.calc_isolines(CoolProp.iT, num=25) 49 | >>> plot.calc_isolines(CoolProp.iSmass, num=15) 50 | >>> plot.show() 51 | 52 | >>> import CoolProp 53 | >>> from CoolProp.Plots import PropertyPlot 54 | >>> plot = PropertyPlot('HEOS::R245fa', 'TS', unit_system='EUR', tp_limits='ORC') 55 | >>> plot.calc_isolines(CoolProp.iQ, num=11) 56 | >>> plot.calc_isolines(CoolProp.iP, iso_range=[1,50], num=10, rounding=True) 57 | >>> plot.draw() 58 | >>> plot.isolines.clear() 59 | >>> plot.props[CoolProp.iP]['color'] = 'green' 60 | >>> plot.props[CoolProp.iP]['lw'] = '0.5' 61 | >>> plot.calc_isolines(CoolProp.iP, iso_range=[1,50], num=10, rounding=False) 62 | >>> plot.show() 63 | 64 | .. note:: 65 | 66 | See the online documentation for a list of the available fluids and 67 | graph types 68 | """ 69 | super(PropertyPlot, self).__init__(fluid_name, graph_type, **kwargs) 70 | self._isolines = {} 71 | #self._plines = {} 72 | #self._ppoints = {} 73 | self.get_axis_limits() 74 | self._plot_default_annotations() 75 | 76 | @property 77 | def isolines(self): return self._isolines 78 | # @property 79 | #def plines(self): return self._plines 80 | # @property 81 | #def ppoints(self): return self._ppoints 82 | 83 | def show(self): 84 | self.draw() 85 | super(PropertyPlot, self).show() 86 | 87 | def savefig(self, *args, **kwargs): 88 | self.draw() 89 | super(PropertyPlot, self).savefig(*args, **kwargs) 90 | 91 | def _plotRound(self, values): 92 | """ 93 | A function round an array-like object while maintaining the 94 | amount of entries. This is needed for the isolines since we 95 | want the labels to look pretty (=rounding), but we do not 96 | know the spacing of the lines. A fixed number of digits after 97 | rounding might lead to reduced array size. 98 | """ 99 | inVal = np.unique(np.sort(np.array(values))) 100 | output = inVal[1:] * 0.0 101 | digits = -1 102 | limit = 10 103 | lim = inVal * 0.0 + 10 104 | # remove less from the numbers until same length, 105 | # more than 10 significant digits does not really 106 | # make sense, does it? 107 | while len(inVal) > len(output) and digits < limit: 108 | digits += 1 109 | val = (np.around(np.log10(np.abs(inVal))) * -1) + digits + 1 110 | val = np.where(val < lim, val, lim) 111 | val = np.where(val > -lim, val, -lim) 112 | output = np.zeros(inVal.shape) 113 | for i in range(len(inVal)): 114 | output[i] = np.around(inVal[i], decimals=int(val[i])) 115 | output = np.unique(output) 116 | return output 117 | 118 | def calc_isolines(self, iso_type=None, iso_range=None, num=15, rounding=False, points=250): 119 | """Calculate lines with constant values of type 'iso_type' in terms of x and y as 120 | defined by the plot object. 'iso_range' either is a collection of values or 121 | simply the minimum and maximum value between which 'num' lines get calculated. 122 | The 'rounding' parameter can be used to generate prettier labels if needed. 123 | """ 124 | 125 | if iso_type is None or iso_type == 'all': 126 | for i_type in IsoLine.XY_SWITCH: 127 | if IsoLine.XY_SWITCH[i_type].get(self.y_index * 10 + self.x_index, None) is not None: 128 | self.calc_isolines(i_type, None, num, rounding, points) 129 | return 130 | 131 | if iso_range is None: 132 | if iso_type is CoolProp.iQ: 133 | iso_range = [0.0, 1.0] 134 | else: 135 | limits = self.get_axis_limits(iso_type, CoolProp.iT) 136 | iso_range = [limits[0], limits[1]] 137 | 138 | if len(iso_range) <= 1 and num != 1: 139 | raise ValueError('You have to provide two values for the iso_range, {0} is not valid.'.format(iso_range)) 140 | 141 | if len(iso_range) == 2 and (num is None or num < 2): 142 | raise ValueError('Please specify the number of isoline you want e.g. num=10.') 143 | 144 | iso_range = np.sort(np.unique(iso_range)) 145 | # Generate iso ranges 146 | if len(iso_range) == 2: 147 | iso_range = self.generate_ranges(iso_type, iso_range[0], iso_range[1], num) 148 | if rounding: 149 | iso_range = self._plotRound(iso_range) 150 | 151 | # Limits are already in SI units 152 | limits = self.get_axis_limits_SI() 153 | 154 | ixrange = self.generate_ranges(self._x_index, limits[0], limits[1], points) 155 | iyrange = self.generate_ranges(self._y_index, limits[2], limits[3], points) 156 | 157 | dim = self._system[iso_type] 158 | 159 | lines = self.isolines.get(iso_type, []) 160 | for i in range(num): 161 | lines.append(IsoLine(iso_type, self._x_index, self._y_index, value=dim.to_SI(iso_range[i]), state=self._state)) 162 | lines[-1].calc_range(ixrange, iyrange) 163 | lines[-1].sanitize_data() 164 | self.isolines[iso_type] = lines 165 | return 166 | 167 | def draw_isolines(self): 168 | dimx = self._system[self._x_index] 169 | dimy = self._system[self._y_index] 170 | 171 | sat_props = self.props[CoolProp.iQ].copy() 172 | if 'lw' in sat_props: sat_props['lw'] *= 2.0 173 | else: sat_props['lw'] = 1.0 174 | if 'alpha' in sat_props: min([sat_props['alpha'] * 2.0, 1.0]) 175 | else: sat_props['alpha'] = 1.0 176 | 177 | for i in self.isolines: 178 | props = self.props[i] 179 | dew = None; bub = None 180 | xcrit = None; ycrit = None 181 | if i == CoolProp.iQ: 182 | for line in self.isolines[i]: 183 | if line.value == 0.0: bub = line 184 | elif line.value == 1.0: dew = line 185 | if dew is not None and bub is not None: 186 | xmin, xmax, ymin, ymax = self.get_axis_limits() 187 | xmin = dimx.to_SI(xmin) 188 | xmax = dimx.to_SI(xmax) 189 | ymin = dimy.to_SI(ymin) 190 | ymax = dimy.to_SI(ymax) 191 | dx = xmax - xmin 192 | dy = ymax - ymin 193 | dew_filter = np.logical_and(np.isfinite(dew.x), np.isfinite(dew.y)) 194 | #dew_filter = np.logical_and(dew_filter,dew.x>dew.x[-1]) 195 | stp = min([dew_filter.size, 10]) 196 | dew_filter[0:-stp] = False 197 | bub_filter = np.logical_and(np.isfinite(bub.x), np.isfinite(bub.y)) 198 | 199 | if self._x_index == CoolProp.iP or self._x_index == CoolProp.iDmass: 200 | filter_x = lambda x: np.log10(x) 201 | else: 202 | filter_x = lambda x: x 203 | if self._y_index == CoolProp.iP or self._y_index == CoolProp.iDmass: 204 | filter_y = lambda y: np.log10(y) 205 | else: 206 | filter_y = lambda y: y 207 | 208 | if ( # (filter_x(dew.x[dew_filter][-1])-filter_x(bub.x[bub_filter][-1])) > 0.010*filter_x(dx) and 209 | (filter_x(dew.x[dew_filter][-1]) - filter_x(bub.x[bub_filter][-1])) < 0.050 * filter_x(dx) or 210 | (filter_y(dew.y[dew_filter][-1]) - filter_y(bub.y[bub_filter][-1])) < 0.010 * filter_y(dy)): 211 | x = np.linspace(bub.x[bub_filter][-1], dew.x[dew_filter][-1], 11) 212 | y = interpolate_values_1d( 213 | np.append(bub.x[bub_filter], dew.x[dew_filter][::-1]), 214 | np.append(bub.y[bub_filter], dew.y[dew_filter][::-1]), 215 | x_points=x, 216 | kind='cubic') 217 | self.axis.plot(dimx.from_SI(x), dimy.from_SI(y), **sat_props) 218 | warnings.warn("Detected an incomplete phase envelope, fixing it numerically.") 219 | xcrit = x[5]; ycrit = y[5] 220 | #Tcrit = self.state.trivial_keyed_output(CoolProp.iT_critical) 221 | #Dcrit = self.state.trivial_keyed_output(CoolProp.irhomass_critical) 222 | # try: 223 | # self.state.update(CoolProp.DmassT_INPUTS, Dcrit, Tcrit) 224 | # xcrit = self.state.keyed_output(self._x_index) 225 | # ycrit = self.state.keyed_output(self._y_index) 226 | # except: 227 | # xcrit = x[5]; ycrit = y[5] 228 | # pass 229 | #self.axis.plot(dimx.from_SI(np.array([bub.x[bub_filter][-1], dew.x[dew_filter][-1]])),dimy.from_SI(np.array([bub.y[bub_filter][-1], dew.y[dew_filter][-1]])),'o') 230 | for line in self.isolines[i]: 231 | if line.i_index == CoolProp.iQ: 232 | if line.value == 0.0 or line.value == 1.0: 233 | self.axis.plot(dimx.from_SI(line.x), dimy.from_SI(line.y), **sat_props) 234 | else: 235 | if xcrit is not None and ycrit is not None: 236 | self.axis.plot(dimx.from_SI(np.append(line.x, xcrit)), dimy.from_SI(np.append(line.y, ycrit)), **props) 237 | # try: 238 | # x = np.append(line.x,[xcrit]) 239 | # y = np.append(line.y,[ycrit]) 240 | # fltr = np.logical_and(np.isfinite(x),np.isfinite(y)) 241 | # f = interp1d(x[fltr][-3:],y[fltr][-3:],kind='linear') # could also be quadratic 242 | # x = np.linspace(x[fltr][-2], x[fltr][-1], 5) 243 | # y = f(x) 244 | # #f = interp1d(y[fltr][-5:],x[fltr][-5:],kind='cubic') 245 | # #y = np.linspace(y[fltr][-2], y[fltr][-1], 5) 246 | # #x = f(y) 247 | # self.axis.plot(dimx.from_SI(np.append(line.x,x)),dimy.from_SI(np.append(line.y,y)),**props) 248 | # except: 249 | # self.axis.plot(dimx.from_SI(np.append(line.x,xcrit)),dimy.from_SI(np.append(line.y,ycrit)),**props) 250 | # pass 251 | else: 252 | self.axis.plot(dimx.from_SI(line.x), dimy.from_SI(line.y), **props) 253 | 254 | def draw(self): 255 | self.get_axis_limits() 256 | self.draw_isolines() 257 | 258 | # def label_isolines(self, dx=0.075, dy=0.100): 259 | # [xmin, xmax, ymin, ymax] = self.get_axis_limits() 260 | # for i in self.isolines: 261 | # for line in self.isolines[i]: 262 | # if self.get_x_y_dydx(xv, yv, x) 263 | 264 | def draw_process(self, statecontainer, points=None, line_opts=None): 265 | """ Draw process or cycle from x and y values in axis units 266 | 267 | Parameters 268 | ---------- 269 | statecontainer : CoolProp.Plots.SimpleCycles.StateContainer() 270 | A state container object that contains all the information required to draw the process. 271 | Note that points that appear several times get added to a special of highlighted points. 272 | line_opts : dict 273 | Line options (please see :func:`matplotlib.pyplot.plot`), optional 274 | Use this parameter to pass a label for the legend. 275 | 276 | Examples 277 | -------- 278 | >>> import CoolProp 279 | >>> from CoolProp.Plots import PropertyPlot 280 | >>> pp = PropertyPlot('HEOS::Water', 'TS', unit_system='EUR') 281 | >>> pp.calc_isolines(CoolProp.iP ) 282 | >>> pp.calc_isolines(CoolProp.iHmass ) 283 | >>> pp.calc_isolines(CoolProp.iQ, num=11) 284 | >>> cycle = SimpleRankineCycle('HEOS::Water', 'TS', unit_system='EUR') 285 | >>> T0 = 300 286 | >>> pp.state.update(CoolProp.QT_INPUTS,0.0,T0+15) 287 | >>> p0 = pp.state.keyed_output(CoolProp.iP) 288 | >>> T2 = 700 289 | >>> pp.state.update(CoolProp.QT_INPUTS,1.0,T2-150) 290 | >>> p2 = pp.state.keyed_output(CoolProp.iP) 291 | >>> cycle.simple_solve(T0, p0, T2, p2, 0.7, 0.8, SI=True) 292 | >>> cycle.steps = 50 293 | >>> sc = cycle.get_state_changes() 294 | >>> pp.draw_process(sc) 295 | >>> # The same calculation can be carried out in another unit system: 296 | >>> cycle.simple_solve(T0-273.15-10, p0/1e5, T2-273.15+50, p2/1e5-5, 0.7, 0.8, SI=False) 297 | >>> sc2 = cycle.get_state_changes() 298 | >>> pp.draw_process(sc2, line_opts={'color':'blue', 'lw':1.5}) 299 | >>> pp.show() 300 | 301 | """ 302 | warnings.warn("You called the function \"draw_process\", which is not tested.", UserWarning) 303 | 304 | # Default values 305 | line_opts = line_opts or {'color': 'r', 'lw': 1.5} 306 | 307 | dimx = self.system[self.x_index] 308 | dimy = self.system[self.y_index] 309 | 310 | marker = line_opts.pop('marker', 'o') 311 | style = line_opts.pop('linestyle', 'solid') 312 | style = line_opts.pop('ls', style) 313 | 314 | if points is None: points = StateContainer() 315 | 316 | xdata = [] 317 | ydata = [] 318 | old = statecontainer[len(statecontainer) - 1] 319 | for i in statecontainer: 320 | point = statecontainer[i] 321 | if point == old: 322 | points.append(point) 323 | old = point 324 | continue 325 | xdata.append(point[self.x_index]) 326 | ydata.append(point[self.y_index]) 327 | old = point 328 | xdata = dimx.from_SI(np.asarray(xdata)) 329 | ydata = dimy.from_SI(np.asarray(ydata)) 330 | self.axis.plot(xdata, ydata, marker='None', linestyle=style, **line_opts) 331 | 332 | xdata = np.empty(len(points)) 333 | ydata = np.empty(len(points)) 334 | for i in points: 335 | point = points[i] 336 | xdata[i] = point[self.x_index] 337 | ydata[i] = point[self.y_index] 338 | xdata = dimx.from_SI(np.asarray(xdata)) 339 | ydata = dimy.from_SI(np.asarray(ydata)) 340 | line_opts['label'] = '' 341 | self.axis.plot(xdata, ydata, marker=marker, linestyle='None', **line_opts) 342 | 343 | 344 | def InlineLabel(xv, yv, x=None, y=None, axis=None, fig=None): 345 | warnings.warn("You called the deprecated function \"InlineLabel\", use \"BasePlot.inline_label\".", DeprecationWarning) 346 | plot = PropertyPlot("water", "TS", figure=fig, axis=axis) 347 | return plot.inline_label(xv, yv, x, y) 348 | 349 | 350 | class PropsPlot(PropertyPlot): 351 | def __init__(self, fluid_name, graph_type, units='KSI', reciprocal_density=False, **kwargs): 352 | super(PropsPlot, self).__init__(fluid_name, graph_type, unit_system=units, reciprocal_density=reciprocal_density, **kwargs) 353 | warnings.warn("You called the deprecated class \"PropsPlot\", use \"PropertyPlot\".", DeprecationWarning) 354 | 355 | 356 | if __name__ == "__main__": 357 | plot = PropertyPlot('HEOS::n-Pentane', 'PD', unit_system='EUR') # , reciprocal_density=True) 358 | plot.calc_isolines(CoolProp.iT) 359 | plot.calc_isolines(CoolProp.iQ, num=11) 360 | # plot.calc_isolines(CoolProp.iSmass) 361 | # plot.calc_isolines(CoolProp.iHmass) 362 | plot.show() 363 | -------------------------------------------------------------------------------- /CoolPlot/Plot/PsychChart.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import 3 | 4 | """ 5 | This file implements a psychrometric chart for air at 1 atm 6 | """ 7 | 8 | from CoolProp.HumidAirProp import HAPropsSI 9 | from .Plots import InlineLabel 10 | import matplotlib, numpy, textwrap 11 | 12 | import_template = ( 13 | """ 14 | # This file was auto-generated by the PsychChart.py script in wrappers/Python/CoolProp/Plots 15 | 16 | if __name__=='__main__': 17 | import numpy, matplotlib 18 | from CoolProp.HumidAirProp import HAPropsSI 19 | from CoolProp.Plots.Plots import InlineLabel 20 | 21 | p = 101325 22 | Tdb = numpy.linspace(-10,60,100)+273.15 23 | 24 | # Make the figure and the axes 25 | fig=matplotlib.pyplot.figure(figsize=(10,8)) 26 | ax=fig.add_axes((0.1,0.1,0.85,0.85)) 27 | """ 28 | ) 29 | 30 | closure_template = ( 31 | """ 32 | matplotlib.pyplot.show() 33 | """ 34 | ) 35 | 36 | Tdb = numpy.linspace(-10, 60, 100) + 273.15 37 | p = 101325 38 | 39 | 40 | def indented_segment(s): 41 | return '\n'.join([' ' + line for line in textwrap.dedent(s).split('\n')]) 42 | 43 | 44 | class PlotFormatting(object): 45 | 46 | def plot(self, ax): 47 | ax.set_xlim(Tdb[0] - 273.15, Tdb[-1] - 273.15) 48 | ax.set_ylim(0, 0.03) 49 | ax.set_xlabel(r"$T_{db}$ [$^{\circ}$C]") 50 | ax.set_ylabel(r"$W$ ($m_{w}/m_{da}$) [-]") 51 | 52 | def __str__(self): 53 | return indented_segment(""" 54 | ax.set_xlim(Tdb[0]-273.15,Tdb[-1]-273.15) 55 | ax.set_ylim(0,0.03) 56 | ax.set_xlabel(r"$T_{db}$ [$^{\circ}$C]") 57 | ax.set_ylabel(r"$W$ ($m_{w}/m_{da}$) [-]") 58 | """) 59 | 60 | 61 | class SaturationLine(object): 62 | 63 | def plot(self, ax): 64 | w = [HAPropsSI('W', 'T', T, 'P', p, 'R', 1.0) for T in Tdb] 65 | ax.plot(Tdb - 273.15, w, lw=2) 66 | 67 | def __str__(self): 68 | return indented_segment(""" 69 | # Saturation line 70 | w = [HAPropsSI('W','T',T,'P',p,'R',1.0) for T in Tdb] 71 | ax.plot(Tdb-273.15,w,lw=2) 72 | """ 73 | ) 74 | 75 | 76 | class HumidityLabels(object): 77 | def __init__(self, RH_values, h): 78 | self.RH_values = RH_values 79 | self.h = h 80 | 81 | def plot(self, ax): 82 | xv = Tdb # [K] 83 | for RH in self.RH_values: 84 | yv = [HAPropsSI('W', 'T', T, 'P', p, 'R', RH) for T in Tdb] 85 | y = HAPropsSI('W', 'P', p, 'H', self.h, 'R', RH) 86 | T_K, w, rot = InlineLabel(xv, yv, y=y, axis=ax) 87 | string = r'$\phi$=' + '{s:0.0f}'.format(s=RH * 100) + '%' 88 | # Make a temporary label to get its bounding box 89 | bbox_opts = dict(boxstyle='square,pad=0.0', fc='white', ec='None', alpha=0.5) 90 | ax.text(T_K - 273.15, w, string, rotation=rot, ha='center', va='center', bbox=bbox_opts) 91 | 92 | def __str__(self): 93 | return indented_segment(""" 94 | xv = Tdb #[K] 95 | for RH in {RHValues:s}: 96 | yv = [HAPropsSI('W','T',T,'P',p,'R',RH) for T in Tdb] 97 | y = HAPropsSI('W','P',p,'H',{h:f},'R',RH) 98 | T_K,w,rot = InlineLabel(xv, yv, y=y, axis = ax) 99 | string = r'$\phi$='+{s:s}+'%' 100 | bbox_opts = dict(boxstyle='square,pad=0.0',fc='white',ec='None',alpha = 0.5) 101 | ax.text(T_K-273.15,w,string,rotation = rot,ha ='center',va='center',bbox=bbox_opts) 102 | """.format(h=self.h, RHValues=str(self.RH_values), s="'{s:0.0f}'.format(s=RH*100)") 103 | ) 104 | 105 | 106 | class HumidityLines(object): 107 | 108 | def __init__(self, RH_values): 109 | self.RH_values = RH_values 110 | 111 | def plot(self, ax): 112 | for RH in self.RH_values: 113 | w = [HAPropsSI('W', 'T', T, 'P', p, 'R', RH) for T in Tdb] 114 | ax.plot(Tdb - 273.15, w, 'r', lw=1) 115 | 116 | def __str__(self): 117 | return indented_segment(""" 118 | # Humidity lines 119 | RHValues = {RHValues:s} 120 | for RH in RHValues: 121 | w = [HAPropsSI('W','T',T,'P',p,'R',RH) for T in Tdb] 122 | ax.plot(Tdb-273.15,w,'r',lw=1) 123 | """.format(RHValues=str(self.RH_values)) 124 | ) 125 | 126 | 127 | class EnthalpyLines(object): 128 | 129 | def __init__(self, H_values): 130 | self.H_values = H_values 131 | 132 | def plot(self, ax): 133 | for H in self.H_values: 134 | # Line goes from saturation to zero humidity ratio for this enthalpy 135 | T1 = HAPropsSI('T', 'H', H, 'P', p, 'R', 1.0) - 273.15 136 | T0 = HAPropsSI('T', 'H', H, 'P', p, 'R', 0.0) - 273.15 137 | w1 = HAPropsSI('W', 'H', H, 'P', p, 'R', 1.0) 138 | w0 = HAPropsSI('W', 'H', H, 'P', p, 'R', 0.0) 139 | ax.plot(numpy.r_[T1, T0], numpy.r_[w1, w0], 'r', lw=1) 140 | 141 | def __str__(self): 142 | return indented_segment(""" 143 | # Humidity lines 144 | for H in {HValues:s}: 145 | #Line goes from saturation to zero humidity ratio for this enthalpy 146 | T1 = HAPropsSI('T','H',H,'P',p,'R',1.0)-273.15 147 | T0 = HAPropsSI('T','H',H,'P',p,'R',0.0)-273.15 148 | w1 = HAPropsSI('W','H',H,'P',p,'R',1.0) 149 | w0 = HAPropsSI('W','H',H,'P',p,'R',0.0) 150 | ax.plot(numpy.r_[T1,T0],numpy.r_[w1,w0],'r',lw=1) 151 | """.format(HValues=str(self.H_values)) 152 | ) 153 | 154 | 155 | if __name__ == '__main__': 156 | 157 | and_plot = False 158 | if and_plot: 159 | fig = matplotlib.pyplot.figure(figsize=(10, 8)) 160 | ax = fig.add_axes((0.1, 0.1, 0.85, 0.85)) 161 | ax.set_xlim(Tdb[0] - 273.15, Tdb[-1] - 273.15) 162 | ax.set_ylim(0, 0.03) 163 | ax.set_xlabel(r"Dry bulb temperature [$^{\circ}$C]") 164 | ax.set_ylabel(r"Humidity ratio ($m_{water}/m_{dry\ air}$) [-]") 165 | 166 | SL = SaturationLine() 167 | if and_plot: SL.plot(ax) 168 | 169 | RHL = HumidityLines([0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]) 170 | if and_plot: RHL.plot(ax) 171 | 172 | RHLabels = HumidityLabels([0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9], h=65000) 173 | if and_plot: RHLabels.plot(ax) 174 | 175 | HL = EnthalpyLines(range(-20000, 100000, 10000)) 176 | if and_plot: HL.plot(ax) 177 | 178 | PF = PlotFormatting() 179 | if and_plot: PF.plot(ax) 180 | 181 | if and_plot: matplotlib.pyplot.show() 182 | 183 | with open('PsychScript.py', 'w') as fp: 184 | for chunk in [import_template, SL, RHL, HL, PF, RHLabels, closure_template]: 185 | fp.write(str(chunk).encode('ascii')) 186 | 187 | execfile('PsychScript.py') 188 | -------------------------------------------------------------------------------- /CoolPlot/Plot/PsychScript.py: -------------------------------------------------------------------------------- 1 | 2 | # This file was auto-generated by the PsychChart.py script in wrappers/Python/CoolProp/Plots 3 | 4 | if __name__ == '__main__': 5 | import numpy, matplotlib 6 | from CoolProp.HumidAirProp import HAPropsSI 7 | from CoolPlot.Plot.Plots import InlineLabel 8 | 9 | p = 101325 10 | Tdb = numpy.linspace(-10, 60, 100) + 273.15 11 | 12 | # Make the figure and the axes 13 | fig = matplotlib.pyplot.figure(figsize=(10, 8)) 14 | ax = fig.add_axes((0.1, 0.1, 0.85, 0.85)) 15 | 16 | # Saturation line 17 | w = [HAPropsSI('W', 'T', T, 'P', p, 'R', 1.0) for T in Tdb] 18 | ax.plot(Tdb - 273.15, w, lw=2) 19 | 20 | # Humidity lines 21 | RHValues = [0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9] 22 | for RH in RHValues: 23 | w = [HAPropsSI('W', 'T', T, 'P', p, 'R', RH) for T in Tdb] 24 | ax.plot(Tdb - 273.15, w, 'r', lw=1) 25 | 26 | # Humidity lines 27 | for H in [-20000, -10000, 0, 10000, 20000, 30000, 40000, 50000, 60000, 70000, 80000, 90000]: 28 | # Line goes from saturation to zero humidity ratio for this enthalpy 29 | T1 = HAPropsSI('T', 'H', H, 'P', p, 'R', 1.0) - 273.15 30 | T0 = HAPropsSI('T', 'H', H, 'P', p, 'R', 0.0) - 273.15 31 | w1 = HAPropsSI('W', 'H', H, 'P', p, 'R', 1.0) 32 | w0 = HAPropsSI('W', 'H', H, 'P', p, 'R', 0.0) 33 | ax.plot(numpy.r_[T1, T0], numpy.r_[w1, w0], 'r', lw=1) 34 | 35 | ax.set_xlim(Tdb[0] - 273.15, Tdb[-1] - 273.15) 36 | ax.set_ylim(0, 0.03) 37 | ax.set_xlabel(r"$T_{db}$ [$^{\circ}$C]") 38 | ax.set_ylabel(r"$W$ ($m_{w}/m_{da}$) [-]") 39 | 40 | xv = Tdb # [K] 41 | for RH in [0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]: 42 | yv = [HAPropsSI('W', 'T', T, 'P', p, 'R', RH) for T in Tdb] 43 | y = HAPropsSI('W', 'P', p, 'H', 65000.000000, 'R', RH) 44 | T_K, w, rot = InlineLabel(xv, yv, y=y, axis=ax) 45 | string = r'$\phi$=' + '{s:0.0f}'.format(s=RH * 100) + '%' 46 | bbox_opts = dict(boxstyle='square,pad=0.0', fc='white', ec='None', alpha=0.5) 47 | ax.text(T_K - 273.15, w, string, rotation=rot, ha='center', va='center', bbox=bbox_opts) 48 | 49 | matplotlib.pyplot.show() 50 | -------------------------------------------------------------------------------- /CoolPlot/Plot/SimpleCycles.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import 3 | 4 | 5 | import matplotlib, warnings 6 | import numpy as np 7 | 8 | 9 | import CoolProp 10 | from CoolProp.CoolProp import PropsSI 11 | from CoolProp.Plots.Common import BasePlot, PropertyDict, SIunits 12 | 13 | 14 | def SimpleCycle(Ref, Te, Tc, DTsh, DTsc, eta_a, Ts_Ph='Ph', **kwargs): 15 | """ 16 | This function plots a simple four-component cycle, on the current axis, or that given by the optional parameter *axis* 17 | 18 | Required parameters: 19 | 20 | * Ref : A string for the refrigerant 21 | * Te : Evap Temperature in K 22 | * Tc : Condensing Temperature in K 23 | * DTsh : Evaporator outlet superheat in K 24 | * DTsc : Condenser outlet subcooling in K 25 | * eta_a : Adiabatic efficiency of compressor (no units) in range [0,1] 26 | 27 | Optional parameters: 28 | 29 | * Ts_Ph : 'Ts' for a Temperature-Entropy plot, 'Ph' for a Pressure-Enthalpy 30 | * axis : An axis to use instead of the active axis 31 | * skipPlot : If True, won't actually plot anything, just print COP 32 | 33 | """ 34 | warnings.warn("This function has been deprecated. Please consider converting it to an object inheriting from \"BaseCycle\".", DeprecationWarning) 35 | 36 | for i in kwargs: 37 | warnings.warn("This function has been deprecated, your input \"{0}: {1}\" will be ignored".format(i, kwargs[i]), DeprecationWarning) 38 | 39 | from CoolProp.Plots import SimpleCompressionCycle 40 | cycle = SimpleCompressionCycle(fluid_ref=Ref, graph_type=Ts_Ph) 41 | cycle.simple_solve_dt(Te, Tc, DTsh, DTsc, eta_a, SI=True) 42 | print(cycle.COP_cooling(), cycle.COP_heating()) 43 | 44 | 45 | def TwoStage(Ref, Q, Te, Tc, DTsh, DTsc, eta_oi, f_p, Tsat_ic, DTsh_ic, Ts_Ph='Ph', prints=False, skipPlot=False, axis=None, **kwargs): 46 | """ 47 | This function plots a two-stage cycle, on the current axis, or that given by the optional parameter *axis* 48 | 49 | Required parameters: 50 | 51 | * Ref : Refrigerant [string] 52 | * Q : Cooling capacity [W] 53 | * Te : Evap Temperature [K] 54 | * Tc : Condensing Temperature [K] 55 | * DTsh : Evaporator outlet superheat [K] 56 | * DTsc : Condenser outlet subcooling [K] 57 | * eta_oi : Adiabatic efficiency of compressor (no units) in range [0,1] 58 | * f_p : fraction of compressor power lost as ambient heat transfer in range [0,1] 59 | * Tsat_ic : Saturation temperature corresponding to intermediate pressure [K] 60 | * DTsh_ic : Superheating at outlet of intermediate stage [K] 61 | 62 | Optional parameters: 63 | 64 | * Ts_Ph : 'Ts' for a Temperature-Entropy plot, 'Ph' for a Pressure-Enthalpy 65 | * prints : True to print out some values 66 | * axis : An axis to use instead of the active axis 67 | * skipPlot : If True, won't actually plot anything, just print COP 68 | 69 | """ 70 | 71 | warnings.warn("This function has been deprecated. PLease consider converting it to an object inheriting from \"BaseCycle\".", DeprecationWarning) 72 | 73 | T = np.zeros((8)) 74 | h = np.zeros_like(T) 75 | p = np.zeros_like(T) 76 | s = np.zeros_like(T) 77 | rho = np.zeros_like(T) 78 | T[0] = np.NAN 79 | s[0] = np.NAN 80 | T[1] = Te + DTsh 81 | pe = PropsSI('P', 'T', Te, 'Q', 1.0, Ref) 82 | pc = PropsSI('P', 'T', Tc, 'Q', 1.0, Ref) 83 | pic = PropsSI('P', 'T', Tsat_ic, 'Q', 1.0, Ref) 84 | Tbubble_c = PropsSI('T', 'P', pc, 'Q', 0, Ref) 85 | Tbubble_e = PropsSI('T', 'P', pe, 'Q', 0, Ref) 86 | 87 | h[1] = PropsSI('H', 'T', T[1], 'P', pe, Ref) 88 | s[1] = PropsSI('S', 'T', T[1], 'P', pe, Ref) 89 | rho[1] = PropsSI('D', 'T', T[1], 'P', pe, Ref) 90 | T[5] = Tbubble_c - DTsc 91 | h[5] = PropsSI('H', 'T', T[5], 'P', pc, Ref) 92 | s[5] = PropsSI('S', 'T', T[5], 'P', pc, Ref) 93 | rho[5] = PropsSI('D', 'T', T[5], 'P', pc, Ref) 94 | mdot = Q / (h[1] - h[5]) 95 | 96 | rho1 = PropsSI('D', 'T', T[1], 'P', pe, Ref) 97 | h2s = PropsSI('H', 'S', s[1], 'P', pic, Ref) 98 | Wdot1 = mdot * (h2s - h[1]) / eta_oi 99 | h[2] = h[1] + (1 - f_p) * Wdot1 / mdot 100 | T[2] = PropsSI('T', 'H', h[2], 'P', pic, Ref) 101 | s[2] = PropsSI('S', 'T', T[2], 'P', pic, Ref) 102 | rho[2] = PropsSI('D', 'T', T[2], 'P', pic, Ref) 103 | T[3] = 288 104 | p[3] = pic 105 | h[3] = PropsSI('H', 'T', T[3], 'P', pic, Ref) 106 | s[3] = PropsSI('S', 'T', T[3], 'P', pic, Ref) 107 | rho[3] = PropsSI('D', 'T', T[3], 'P', pic, Ref) 108 | rho3 = PropsSI('D', 'T', T[3], 'P', pic, Ref) 109 | h4s = PropsSI('H', 'T', s[3], 'P', pc, Ref) 110 | Wdot2 = mdot * (h4s - h[3]) / eta_oi 111 | h[4] = h[3] + (1 - f_p) * Wdot2 / mdot 112 | T[4] = PropsSI('T', 'H', h[4], 'P', pc, Ref) 113 | s[4] = PropsSI('S', 'T', T[4], 'P', pc, Ref) 114 | rho[4] = PropsSI('D', 'T', T[4], 'P', pc, Ref) 115 | 116 | sbubble_e = PropsSI('S', 'T', Tbubble_e, 'Q', 0, Ref) 117 | sbubble_c = PropsSI('S', 'T', Tbubble_c, 'Q', 0, Ref) 118 | sdew_e = PropsSI('S', 'T', Te, 'Q', 1, Ref) 119 | sdew_c = PropsSI('S', 'T', Tc, 'Q', 1, Ref) 120 | 121 | hsatL = PropsSI('H', 'T', Tbubble_e, 'Q', 0, Ref) 122 | hsatV = PropsSI('H', 'T', Te, 'Q', 1, Ref) 123 | ssatL = PropsSI('S', 'T', Tbubble_e, 'Q', 0, Ref) 124 | ssatV = PropsSI('S', 'T', Te, 'Q', 1, Ref) 125 | vsatL = 1 / PropsSI('D', 'T', Tbubble_e, 'Q', 0, Ref) 126 | vsatV = 1 / PropsSI('D', 'T', Te, 'Q', 1, Ref) 127 | x = (h[5] - hsatL) / (hsatV - hsatL) 128 | s[6] = x * ssatV + (1 - x) * ssatL 129 | T[6] = x * Te + (1 - x) * Tbubble_e 130 | rho[6] = 1.0 / (x * vsatV + (1 - x) * vsatL) 131 | 132 | h[6] = h[5] 133 | h[7] = h[1] 134 | s[7] = s[1] 135 | T[7] = T[1] 136 | p = [np.nan, pe, pic, pic, pc, pc, pe, pe] 137 | COP = Q / (Wdot1 + Wdot2) 138 | RE = h[1] - h[6] 139 | 140 | if prints == True: 141 | print('x5:', x) 142 | print('COP:', COP) 143 | print('COPH', (Q + Wdot1 + Wdot2) / (Wdot1 + Wdot2)) 144 | print(T[2] - 273.15, T[4] - 273.15, p[2] / p[1], p[4] / p[3]) 145 | print(mdot, mdot * (h[4] - h[5]), pic) 146 | print('Vdot1', mdot / rho1, 'Vdisp', mdot / rho1 / (3500 / 60.) * 1e6 / 0.7) 147 | print('Vdot2', mdot / rho3, 'Vdisp', mdot / rho3 / (3500 / 60.) * 1e6 / 0.7) 148 | print(mdot * (h[4] - h[5]), Tc - 273.15) 149 | for i in range(1, len(T) - 1): 150 | print('%d & %g & %g & %g & %g & %g \\\\' % (i, T[i] - 273.15, p[i], h[i], s[i], rho[i])) 151 | else: 152 | print(Tsat_ic, COP) 153 | 154 | if skipPlot == False: 155 | if axis == None: 156 | ax = matplotlib.pyplot.gca() 157 | else: 158 | ax = axis 159 | if Ts_Ph in ['ph', 'Ph']: 160 | ax.plot(h, p) 161 | elif Ts_Ph in ['Ts', 'ts']: 162 | s_copy = s.copy() 163 | T_copy = T.copy() 164 | for i in range(1, len(s) - 1): 165 | ax.plot(s[i], T[i], 'bo', mfc='b', mec='b') 166 | dT = [0, -5, 5, -20, 5, 5, 5] 167 | ds = [0, 0.05, 0, 0, 0, 0, 0] 168 | ax.text(s[i] + ds[i], T[i] + dT[i], str(i)) 169 | 170 | s = list(s) 171 | T = list(T) 172 | s.insert(7, sdew_e) 173 | T.insert(7, Te) 174 | s.insert(5, sbubble_c) 175 | T.insert(5, Tbubble_c) 176 | s.insert(5, sdew_c) 177 | T.insert(5, Tc) 178 | 179 | ax.plot(s, T) 180 | s = s_copy 181 | T = T_copy 182 | else: 183 | raise TypeError('Type of Ts_Ph invalid') 184 | return COP 185 | 186 | 187 | def EconomizedCycle(Ref, Qin, Te, Tc, DTsh, DTsc, eta_oi, f_p, Ti, Ts_Ph='Ts', skipPlot=False, axis=None, **kwargs): 188 | """ 189 | This function plots an economized cycle, on the current axis, or that given by the optional parameter *axis* 190 | 191 | Required parameters: 192 | 193 | * Ref : Refrigerant [string] 194 | * Qin : Cooling capacity [W] 195 | * Te : Evap Temperature [K] 196 | * Tc : Condensing Temperature [K] 197 | * DTsh : Evaporator outlet superheat [K] 198 | * DTsc : Condenser outlet subcooling [K] 199 | * eta_oi : Adiabatic efficiency of compressor (no units) in range [0,1] 200 | * f_p : fraction of compressor power lost as ambient heat transfer in range [0,1] 201 | * Ti : Saturation temperature corresponding to intermediate pressure [K] 202 | 203 | Optional parameters: 204 | 205 | * Ts_Ph : 'Ts' for a Temperature-Entropy plot, 'Ph' for a Pressure-Enthalpy 206 | * axis : An axis to use instead of the active axis 207 | * skipPlot : If True, won't actually plot anything, just print COP 208 | 209 | """ 210 | 211 | warnings.warn("This function has been deprecated. Please consider converting it to an object inheriting from \"BaseCycle\".", DeprecationWarning) 212 | from scipy.optimize import newton 213 | 214 | m = 1 215 | 216 | T = np.zeros((11)) 217 | h = np.zeros_like(T) 218 | p = np.zeros_like(T) 219 | s = np.zeros_like(T) 220 | rho = np.zeros_like(T) 221 | 222 | T[0] = np.NAN 223 | s[0] = np.NAN 224 | T[1] = Te + DTsh 225 | pe = PropsSI('P', 'T', Te, 'Q', 1.0, Ref) 226 | pc = PropsSI('P', 'T', Tc, 'Q', 1.0, Ref) 227 | pi = PropsSI('P', 'T', Ti, 'Q', 1.0, Ref) 228 | p[1] = pe 229 | h[1] = PropsSI('H', 'T', T[1], 'P', pe, Ref) 230 | s[1] = PropsSI('S', 'T', T[1], 'P', pe, Ref) 231 | rho[1] = PropsSI('D', 'T', T[1], 'P', pe, Ref) 232 | h2s = PropsSI('H', 'S', s[1], 'P', pi, Ref) 233 | wdot1 = (h2s - h[1]) / eta_oi 234 | h[2] = h[1] + (1 - f_p[0]) * wdot1 235 | p[2] = pi 236 | # T[2]=T_hp(Ref,h[2],pi,T2s) 237 | T[2] = PropsSI('T', 'H', h[2], 'P', pi, Ref) 238 | 239 | s[2] = PropsSI('S', 'T', T[2], 'P', pi, Ref) 240 | rho[2] = PropsSI('D', 'T', T[2], 'P', pi, Ref) 241 | 242 | T[5] = Tc - DTsc 243 | h[5] = PropsSI('H', 'T', T[5], 'P', pc, Ref) 244 | s[5] = PropsSI('S', 'T', T[5], 'P', pc, Ref) 245 | rho[5] = PropsSI('D', 'T', T[5], 'P', pc, Ref) 246 | 247 | p[5] = pc 248 | p[6] = pi 249 | h[6] = h[5] 250 | 251 | p[7] = pi 252 | p[8] = pi 253 | p[6] = pi 254 | T[7] = Ti 255 | h[7] = PropsSI('H', 'T', Ti, 'Q', 1, Ref) 256 | s[7] = PropsSI('S', 'T', Ti, 'Q', 1, Ref) 257 | rho[7] = PropsSI('D', 'T', Ti, 'Q', 1, Ref) 258 | T[8] = Ti 259 | h[8] = PropsSI('H', 'T', Ti, 'Q', 0, Ref) 260 | s[8] = PropsSI('S', 'T', Ti, 'Q', 0, Ref) 261 | rho[8] = PropsSI('D', 'T', Ti, 'Q', 0, Ref) 262 | x6 = (h[6] - h[8]) / (h[7] - h[8]) # Vapor Quality 263 | s[6] = s[7] * x6 + s[8] * (1 - x6) 264 | rho[6] = 1.0 / (x6 / rho[7] + (1 - x6) / rho[8]) 265 | T[6] = Ti 266 | 267 | # Injection mass flow rate 268 | x = m * (h[6] - h[8]) / (h[7] - h[6]) 269 | 270 | p[3] = pi 271 | h[3] = (m * h[2] + x * h[7]) / (m + x) 272 | # T[3]=T_hp(Ref,h[3],pi,T[2]) 273 | T[3] = PropsSI('T', 'H', h[3], 'P', pi, Ref) 274 | s[3] = PropsSI('S', 'T', T[3], 'P', pi, Ref) 275 | rho[3] = PropsSI('D', 'T', T[3], 'P', pi, Ref) 276 | T4s = newton(lambda T: PropsSI('S', 'T', T, 'P', pc, Ref) - s[3], T[2] + 30) 277 | h4s = PropsSI('H', 'T', T4s, 'P', pc, Ref) 278 | p[4] = pc 279 | wdot2 = (h4s - h[3]) / eta_oi 280 | h[4] = h[3] + (1 - f_p[1]) * wdot2 281 | # T[4]=T_hp(Ref,h[4],pc,T4s) 282 | T[4] = PropsSI('T', 'H', h[4], 'P', pc, Ref) 283 | s[4] = PropsSI('S', 'T', T[4], 'P', pc, Ref) 284 | rho[4] = PropsSI('D', 'T', T[4], 'P', pc, Ref) 285 | 286 | p[9] = pe 287 | h[9] = h[8] 288 | T[9] = Te 289 | hsatL_e = PropsSI('H', 'T', Te, 'Q', 0, Ref) 290 | hsatV_e = PropsSI('H', 'T', Te, 'Q', 1, Ref) 291 | ssatL_e = PropsSI('S', 'T', Te, 'Q', 0, Ref) 292 | ssatV_e = PropsSI('S', 'T', Te, 'Q', 1, Ref) 293 | vsatL_e = 1 / PropsSI('D', 'T', Te, 'Q', 0, Ref) 294 | vsatV_e = 1 / PropsSI('D', 'T', Te, 'Q', 1, Ref) 295 | x9 = (h[9] - hsatL_e) / (hsatV_e - hsatL_e) # Vapor Quality 296 | s[9] = ssatV_e * x9 + ssatL_e * (1 - x9) 297 | rho[9] = 1.0 / (x9 * vsatV_e + (1 - x9) * vsatL_e) 298 | 299 | s[10] = s[1] 300 | T[10] = T[1] 301 | h[10] = h[1] 302 | p[10] = p[1] 303 | 304 | Tbubble_e = Te 305 | Tbubble_c = Tc 306 | sbubble_e = PropsSI('S', 'T', Tbubble_e, 'Q', 0, Ref) 307 | sbubble_c = PropsSI('S', 'T', Tbubble_c, 'Q', 0, Ref) 308 | sdew_e = PropsSI('S', 'T', Te, 'Q', 1, Ref) 309 | sdew_c = PropsSI('S', 'T', Tc, 'Q', 1, Ref) 310 | 311 | Wdot1 = m * wdot1 312 | Wdot2 = (m + x) * wdot2 313 | if skipPlot == False: 314 | if axis == None: 315 | ax = matplotlib.pyplot.gca() 316 | else: 317 | ax = axis 318 | if Ts_Ph in ['ph', 'Ph']: 319 | ax.plot(h, p) 320 | ax.set_yscale('log') 321 | elif Ts_Ph in ['Ts', 'ts']: 322 | ax.plot(np.r_[s[7], s[3]], np.r_[T[7], T[3]], 'b') 323 | s_copy = s.copy() 324 | T_copy = T.copy() 325 | dT = [0, -5, 5, -12, 5, 12, -12, 0, 0, 0] 326 | ds = [0, 0.05, 0.05, 0, 0.05, 0, 0.0, 0.05, -0.05, -0.05] 327 | for i in range(1, len(s) - 1): 328 | ax.plot(s[i], T[i], 'bo', mfc='b', mec='b') 329 | ax.text(s[i] + ds[i], T[i] + dT[i], str(i), ha='center', va='center') 330 | 331 | s = list(s) 332 | T = list(T) 333 | s.insert(10, sdew_e) 334 | T.insert(10, Te) 335 | s.insert(5, sbubble_c) 336 | T.insert(5, Tbubble_c) 337 | s.insert(5, sdew_c) 338 | T.insert(5, Tc) 339 | ax.plot(s, T, 'b') 340 | 341 | s = s_copy 342 | T = T_copy 343 | else: 344 | raise TypeError('Type of Ts_Ph invalid') 345 | 346 | COP = m * (h[1] - h[9]) / (m * (h[2] - h[1]) + (m + x) * (h[4] - h[3])) 347 | for i in range(1, len(T) - 1): 348 | print('%d & %g & %g & %g & %g & %g \\\\' % (i, T[i] - 273.15, p[i], h[i], s[i], rho[i])) 349 | print(x, m * (h[1] - h[9]), (m * (h[2] - h[1]) + (m + x) * (h[4] - h[3])), COP) 350 | mdot = Qin / (h[1] - h[9]) 351 | mdot_inj = x * mdot 352 | print('x9', x9,) 353 | print('Qcond', (mdot + mdot_inj) * (h[4] - h[5]), 'T4', T[4] - 273.15) 354 | print(mdot, mdot + mdot_inj) 355 | f = 3500 / 60. 356 | eta_v = 0.7 357 | print('Vdisp1: ', mdot / (rho[1] * f * eta_v) * 1e6, 'cm^3') 358 | print('Vdisp2: ', (mdot + mdot_inj) / (rho[1] * f * eta_v) * 1e6, 'cm^3') 359 | return COP 360 | 361 | # class SimpleCycle(object): 362 | # """A class that calculates a simple thermodynamic cycle""" 363 | # def __init__(self, *args, **kwargs): 364 | # object.__init__(self, *args, **kwargs) 365 | # (states, steps, fluid): 366 | 367 | # Parameters 368 | # ---------- 369 | # x_type : int, str 370 | # Either a letter or an integer that specifies the property type for the x-axis 371 | # y_type : int, str 372 | # Either a letter or an integer that specifies the property type for the y-axis 373 | # states : list 374 | # A collection of state points that follows a fixed scheme defined 375 | # in the implementing subclass. 376 | # fluid_ref : str, CoolProp.AbstractState 377 | # The fluid property provider, either a subclass of CoolProp.AbstractState 378 | # or a string that can be used to generate a CoolProp.AbstractState instance 379 | # via :func:`Common.process_fluid_state`. 380 | # steps : int 381 | # The number of steps used for going from one state to another 382 | # 383 | # for more properties, see :class:`CoolProp.Plots.Common.Base2DObject`. 384 | 385 | # # See http://stackoverflow.com/questions/1061283/lt-instead-of-cmp 386 | # class ComparableMixin: 387 | # """A mixin class that implements all comparing mathods except for __lt__""" 388 | # def __eq__(self, other): 389 | # return not self B: return 1 432 | elif A < B: return -1 433 | elif A == B: return 0 434 | else: raise ValueError("Comparison failed.") 435 | 436 | def __eq__(self, other): 437 | for i in self: 438 | if not self.__prop_compare(other, i) == 0: 439 | return False 440 | return True 441 | 442 | def __hash__(self): 443 | return hash(repr(self)) 444 | 445 | 446 | class StateContainer(object): 447 | """A collection of values for the main properties, built to mixin with :class:`CoolProp.Plots.Common.PropertyDict` 448 | 449 | Examples 450 | -------- 451 | This container has overloaded accessor methods. Just pick your own flavour 452 | or mix the styles as you like: 453 | 454 | >>> from __future__ import print_function 455 | >>> import CoolProp 456 | >>> from CoolProp.Plots.SimpleCycles import StateContainer 457 | >>> T0 = 300.000; p0 = 200000.000; h0 = 112745.749; s0 = 393.035 458 | >>> cycle_states = StateContainer() 459 | >>> cycle_states[0,'H'] = h0 460 | >>> cycle_states[0]['S'] = s0 461 | >>> cycle_states[0][CoolProp.iP] = p0 462 | >>> cycle_states[0,CoolProp.iT] = T0 463 | >>> cycle_states[1,"T"] = 300.064 464 | >>> print(cycle_states) 465 | Stored State Points: 466 | state T (K) p (Pa) d (kg/m3) h (J/kg) s (J/kg/K) 467 | 0 300.000 200000.000 - 112745.749 393.035 468 | 1 300.064 - - - - 469 | 470 | """ 471 | 472 | def __init__(self, unit_system=SIunits()): 473 | self._points = {} 474 | self._units = unit_system 475 | 476 | @property 477 | def points(self): return self._points 478 | 479 | @points.setter 480 | def points(self, value): self._points = value 481 | 482 | @property 483 | def units(self): return self._units 484 | 485 | @units.setter 486 | def units(self, value): self._units = value 487 | 488 | def get_point(self, index, SI=True): 489 | if SI: 490 | state = self[index] 491 | else: 492 | state = self[index] 493 | for i in state: 494 | state[i] = self.units[i].from_SI(state[i]) 495 | return state 496 | 497 | def set_point(self, index, value, SI=True): 498 | if SI: 499 | self._points[index] = value 500 | else: 501 | for i in value: 502 | self._points[index][i] = self.units[i].to_SI(value[i]) 503 | 504 | def _list_like(self, value): 505 | """Try to detect a list-like structure excluding strings""" 506 | return (not hasattr(value, "strip") and 507 | (hasattr(value, "__getitem__") or 508 | hasattr(value, "__iter__"))) 509 | # return is_sequence(value) # use from pandas.core.common import is_sequence 510 | 511 | def __len__(self): 512 | """Some cheating to get the correct behaviour""" 513 | return len(self._points) 514 | 515 | def __iter__(self): 516 | """Make sure we iterate in the righ order""" 517 | for key in sorted(self._points): 518 | yield key 519 | 520 | def __getitem__(self, index): 521 | """Another tweak that changes the default access path""" 522 | if self._list_like(index): 523 | len_var = len(index) 524 | if len_var == 0: 525 | raise IndexError("Received empty index.") 526 | elif len_var == 1: 527 | return self._points[index[0]] 528 | elif len_var == 2: 529 | return self._points[index[0]][index[1]] 530 | else: 531 | raise IndexError("Received too long index.") 532 | return self._points[index] 533 | 534 | def __setitem__(self, index, value): 535 | """Another tweak that changes the default access path""" 536 | if self._list_like(index): 537 | len_var = len(index) 538 | if len_var == 0: 539 | raise IndexError("Received empty index.") 540 | elif len_var == 1: 541 | self._points[index[0]] = value 542 | elif len_var == 2: 543 | # safeguard against empty entries 544 | if index[0] not in self._points: 545 | self._points[index[0]] = StatePoint() 546 | self._points[index[0]][index[1]] = value 547 | else: 548 | raise IndexError("Received too long index.") 549 | else: 550 | self._points[index] = value 551 | 552 | def __str__(self): 553 | out = "Stored State Points:\n" 554 | keys = True 555 | for i in self._points: 556 | if keys: 557 | row = [u"{0:>5s}".format("state")] 558 | for j in self._points[i]: 559 | label = u"{0:s} ({1:s})".format(self.units[j].symbol, self.units[j].unit) 560 | row.append(u"{0:>11s}".format(label)) 561 | out = out + u" ".join(row) + "\n" 562 | keys = False 563 | row = [u"{0:>5s}".format(str(i))] 564 | for j in self._points[i]: 565 | try: 566 | row.append(u"{0:11.3f}".format(self.units[j].from_SI(self._points[i][j]))) 567 | except: 568 | row.append(u"{0:>11s}".format("-")) 569 | out = out + u" ".join(row) + "\n" 570 | return out 571 | 572 | def append(self, new): 573 | i = 0 + self.__len__() 574 | for j in new: 575 | self[i, j] = new[j] 576 | return self 577 | 578 | def extend(self, new): 579 | i = 0 + self.__len__() 580 | for j in new: 581 | for k in new[j]: 582 | self[i, k] = new[j][k] 583 | i = i + 1 584 | return self 585 | 586 | @property 587 | def D(self): return np.array([self._points[k].D for k in self]) 588 | 589 | @property 590 | def H(self): return np.array([self._points[k].H for k in self]) 591 | 592 | @property 593 | def P(self): return np.array([self._points[k].P for k in self]) 594 | 595 | @property 596 | def S(self): return np.array([self._points[k].S for k in self]) 597 | 598 | @property 599 | def T(self): return np.array([self._points[k].T for k in self]) 600 | 601 | @property 602 | def U(self): return np.array([self._points[k].U for k in self]) 603 | 604 | @property 605 | def Q(self): return np.array([self._points[k].Q for k in self]) 606 | 607 | 608 | class BaseCycle(BasePlot): 609 | """A simple thermodynamic cycle, should not be used on its own.""" 610 | 611 | # Define the iteration keys 612 | PROPERTIES = { 613 | CoolProp.iDmass: 'density', 614 | CoolProp.iHmass: 'specific enthalpy', 615 | CoolProp.iP: 'pressure', 616 | CoolProp.iSmass: 'specific entropy', 617 | CoolProp.iT: 'temperature' 618 | } 619 | 620 | STATECOUNT = 0 621 | """A list of accepted numbers of states""" 622 | 623 | STATECHANGE = None 624 | """A list of lists of tuples that defines how the state transitions 625 | behave for the corresponding entry in BaseCycle.STATECOUNT""" 626 | 627 | def __init__(self, fluid_ref, graph_type, unit_system='EUR', **kwargs): 628 | """Initialises a simple cycle calculator 629 | 630 | Parameters 631 | ---------- 632 | fluid_ref : str, CoolProp.AbstractState 633 | The fluid property provider, either a subclass of CoolProp.AbstractState 634 | or a string that can be used to generate a CoolProp.AbstractState instance 635 | via :func:`Common.process_fluid_state`. 636 | graph_type : string 637 | The graph type to be plotted, like \"PH\" or \"TS\" 638 | unit_system : string, ['EUR','KSI','SI'] 639 | Select the units used for the plotting. 'EUR' is bar, kJ, C; 'KSI' is kPa, kJ, K; 'SI' is Pa, J, K 640 | 641 | for more properties, see :class:`CoolProp.Plots.Common.BasePlot`. 642 | """ 643 | self._cycle_states = StateContainer() 644 | self._steps = 2 645 | BasePlot.__init__(self, fluid_ref, graph_type, unit_system, **kwargs) 646 | 647 | @property 648 | def cycle_states(self): return self._cycle_states 649 | 650 | @cycle_states.setter 651 | def cycle_states(self, value): 652 | if len(value) != self.STATECOUNT: 653 | raise ValueError("Your number of states ({0:d}) is not in the list of allowed state counts: {1:s}.".format(len(value), str(self.STATECOUNT))) 654 | self._cycle_states = value 655 | 656 | @property 657 | def steps(self): return self._steps 658 | 659 | @steps.setter 660 | def steps(self, value): self._steps = int(max([value, 2])) 661 | 662 | @BasePlot.system.setter 663 | def system(self, value): 664 | if value in self.UNIT_SYSTEMS: 665 | self._system = self.UNIT_SYSTEMS[value] 666 | elif isinstance(value, PropertyDict): 667 | self._system = value 668 | else: 669 | raise ValueError("Invalid unit_system input \"{0:s}\", expected a string from {1:s}".format(str(value), str(self.UNIT_SYSTEMS.keys()))) 670 | self._cycle_states.units = self._system 671 | 672 | def valid_states(self): 673 | """Check the formats of BaseCycle.STATECOUNT and BaseCycle.STATECHANGE""" 674 | if len(self.STATECHANGE) != self.STATECOUNT: 675 | raise ValueError("Invalid number of states and or state change operations") 676 | return True 677 | 678 | def fill_states(self, objs=None): 679 | """Try to populate all fields in the state objects""" 680 | 681 | if objs is None: 682 | objs = self._cycle_states 683 | local = True 684 | else: 685 | local = False 686 | 687 | for i in objs: 688 | full = True 689 | for j in objs[i]: 690 | if objs[i][j] is None: 691 | full = False 692 | if full: continue 693 | if (objs[i][CoolProp.iDmass] is not None and 694 | objs[i][CoolProp.iT] is not None): 695 | self._state.update(CoolProp.DmassT_INPUTS, objs[i][CoolProp.iDmass], objs[i][CoolProp.iT]) 696 | elif (objs[i][CoolProp.iP] is not None and 697 | objs[i][CoolProp.iHmass] is not None): 698 | self._state.update(CoolProp.HmassP_INPUTS, objs[i][CoolProp.iHmass], objs[i][CoolProp.iP]) 699 | elif (objs[i][CoolProp.iP] is not None and 700 | objs[i][CoolProp.iSmass] is not None): 701 | self._state.update(CoolProp.PSmass_INPUTS, objs[i][CoolProp.iP], objs[i][CoolProp.iSmass]) 702 | else: 703 | warnings.warn("Please fill the state[{0:s}] manually.".format(str(i))) 704 | continue 705 | for j in objs[i]: 706 | if objs[i][j] is None: 707 | objs[i][j] = self._state.keyed_output(j) 708 | 709 | if local: self._cycle_states = objs 710 | return objs 711 | 712 | def state_change(self, in1, in2, start, ty1='lin', ty2='lin'): 713 | """Calculates a state change defined by the properties in1 and in2 714 | 715 | Uses self.states[start] and self.states[start+1] (or self.states[0]) to define 716 | the process and interpolates between the values. 717 | 718 | Parameters 719 | ---------- 720 | in1 : int 721 | The index of the first defined property. 722 | in2 : int 723 | The index of the second defined property. 724 | start : int 725 | The index of the start state. 726 | ty1 : str 727 | The key that defines the type of state change for in1, lin or log. 728 | ty2 : str 729 | The key that defines the type of state change for in2, lin or log. 730 | 731 | Returns 732 | ------- 733 | scalar or array_like 734 | a list of the length of self.steps+1 that describes the process. It includes start and end state. 735 | """ 736 | self.fill_states() 737 | end = start + 1 738 | if end >= len(self.cycle_states): end -= len(self.cycle_states) 739 | start = self.cycle_states[start] 740 | end = self.cycle_states[end] 741 | # 742 | val = [] 743 | inv = [in1, in2] 744 | typ = [ty1, ty2] 745 | for i, v in enumerate(inv): 746 | if typ[i] == 'lin': 747 | val.append(np.linspace(start[v], end[v], self.steps)) 748 | elif typ[i] == 'log': 749 | val.append(np.logspace(np.log10(start[v]), np.log10(end[v]), self.steps)) 750 | else: 751 | raise ValueError("Unknown range generator {0:s}".format(str(typ[i]))) 752 | 753 | sc = StateContainer(self._system) 754 | for i, _ in enumerate(val[0]): 755 | sc[i, inv[0]] = val[0][i] 756 | sc[i, inv[1]] = val[1][i] 757 | 758 | return self.fill_states(sc) 759 | 760 | def get_state_change(self, index): 761 | return self.STATECHANGE[index](self) 762 | 763 | def get_state_changes(self): 764 | sc = self.get_state_change(0) 765 | for i in range(1, self.STATECOUNT): 766 | sc.extend(self.get_state_change(i)) 767 | return sc 768 | -------------------------------------------------------------------------------- /CoolPlot/Plot/SimpleCyclesCompression.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import 3 | 4 | import numpy as np 5 | 6 | import CoolProp 7 | from .Common import process_fluid_state 8 | from .SimpleCycles import BaseCycle, StateContainer 9 | 10 | 11 | class BaseCompressionCycle(BaseCycle): 12 | """A thermodynamic cycle for vapour compression processes. 13 | 14 | Defines the basic properties and methods to unify access to 15 | compression cycle-related quantities. 16 | """ 17 | 18 | def __init__(self, fluid_ref='HEOS::Water', graph_type='PH', **kwargs): 19 | """see :class:`CoolProp.Plots.SimpleCycles.BaseCycle` for details.""" 20 | BaseCycle.__init__(self, fluid_ref, graph_type, **kwargs) 21 | 22 | def eta_carnot_heating(self): 23 | """Carnot efficiency 24 | 25 | Calculates the Carnot efficiency for a heating process, :math:`\eta_c = \frac{T_h}{T_h-T_c}`. 26 | 27 | Returns 28 | ------- 29 | float 30 | """ 31 | Tvector = self._cycle_states.T 32 | return np.max(Tvector) / (np.max(Tvector) - np.min(Tvector)) 33 | 34 | def eta_carnot_cooling(self): 35 | """Carnot efficiency 36 | 37 | Calculates the Carnot efficiency for a cooling process, :math:`\eta_c = \frac{T_c}{T_h-T_c}`. 38 | 39 | Returns 40 | ------- 41 | float 42 | """ 43 | Tvector = self._cycle_states.T 44 | return np.min(Tvector) / (np.max(Tvector) - np.min(Tvector)) 45 | 46 | 47 | class SimpleCompressionCycle(BaseCompressionCycle): 48 | """A simple vapour compression cycle""" 49 | STATECOUNT = 4 50 | STATECHANGE = [ 51 | lambda inp: BaseCycle.state_change(inp, 'S', 'P', 0, ty1='log', ty2='log'), # Compression process 52 | lambda inp: BaseCycle.state_change(inp, 'H', 'P', 1, ty1='lin', ty2='lin'), # Heat removal 53 | lambda inp: BaseCycle.state_change(inp, 'H', 'P', 2, ty1='log', ty2='log'), # Expansion 54 | lambda inp: BaseCycle.state_change(inp, 'H', 'P', 3, ty1='lin', ty2='lin') # Heat addition 55 | ] 56 | 57 | def __init__(self, fluid_ref='HEOS::Water', graph_type='PH', **kwargs): 58 | """see :class:`CoolProp.Plots.SimpleCyclesCompression.BaseCompressionCycle` for details.""" 59 | BaseCompressionCycle.__init__(self, fluid_ref, graph_type, **kwargs) 60 | 61 | def simple_solve(self, T0, p0, T2, p2, eta_com, fluid=None, SI=True): 62 | """" 63 | A simple vapour compression cycle calculation 64 | 65 | Parameters 66 | ---------- 67 | T0 : float 68 | The evaporated fluid, before the compressor 69 | p0 : float 70 | The evaporated fluid, before the compressor 71 | T2 : float 72 | The condensed fluid, before the expansion valve 73 | p2 : float 74 | The condensed fluid, before the expansion valve 75 | eta_com : float 76 | Isentropic compressor efficiency 77 | 78 | Examples 79 | -------- 80 | >>> import CoolProp 81 | >>> from CoolProp.Plots import PropertyPlot 82 | >>> from CoolProp.Plots import SimpleCompressionCycle 83 | >>> pp = PropertyPlot('HEOS::R134a', 'PH', unit_system='EUR') 84 | >>> pp.calc_isolines(CoolProp.iQ, num=11) 85 | >>> cycle = SimpleCompressionCycle('HEOS::R134a', 'PH', unit_system='EUR') 86 | >>> T0 = 280 87 | >>> pp.state.update(CoolProp.QT_INPUTS,0.0,T0-15) 88 | >>> p0 = pp.state.keyed_output(CoolProp.iP) 89 | >>> T2 = 310 90 | >>> pp.state.update(CoolProp.QT_INPUTS,1.0,T2+10) 91 | >>> p2 = pp.state.keyed_output(CoolProp.iP) 92 | >>> cycle.simple_solve(T0, p0, T2, p2, 0.7, SI=True) 93 | >>> cycle.steps = 50 94 | >>> sc = cycle.get_state_changes() 95 | >>> import matplotlib.pyplot as plt 96 | >>> plt.close(cycle.figure) 97 | >>> pp.draw_process(sc) 98 | 99 | """ 100 | if fluid is not None: 101 | self.state = process_fluid_state(fluid) 102 | if self._state is None: 103 | raise ValueError("You have to specify a fluid before you can calculate.") 104 | 105 | cycle_states = StateContainer(unit_system=self._system) 106 | 107 | if not SI: 108 | conv_t = self._system[CoolProp.iT].to_SI 109 | conv_p = self._system[CoolProp.iP].to_SI 110 | T0 = conv_t(T0) 111 | p0 = conv_p(p0) 112 | T2 = conv_t(T2) 113 | p2 = conv_p(p2) 114 | 115 | # Gas from evaporator 116 | self.state.update(CoolProp.PT_INPUTS, p0, T0) 117 | h0 = self.state.hmass() 118 | s0 = self.state.smass() 119 | # Just a showcase for the different accessor methods 120 | cycle_states[0, 'H'] = h0 121 | cycle_states[0]['S'] = s0 122 | cycle_states[0][CoolProp.iP] = p0 123 | cycle_states[0, CoolProp.iT] = T0 124 | 125 | # Pressurised vapour 126 | p1 = p2 127 | self.state.update(CoolProp.PSmass_INPUTS, p1, s0) 128 | h1 = h0 + (self.state.hmass() - h0) / eta_com 129 | self.state.update(CoolProp.HmassP_INPUTS, h1, p1) 130 | s1 = self.state.smass() 131 | T1 = self.state.T() 132 | cycle_states[1, 'H'] = h1 133 | cycle_states[1, 'S'] = s1 134 | cycle_states[1, 'P'] = p1 135 | cycle_states[1, 'T'] = T1 136 | 137 | # Condensed vapour 138 | self.state.update(CoolProp.PT_INPUTS, p2, T2) 139 | h2 = self.state.hmass() 140 | s2 = self.state.smass() 141 | cycle_states[2, 'H'] = h2 142 | cycle_states[2, 'S'] = s2 143 | cycle_states[2, 'P'] = p2 144 | cycle_states[2, 'T'] = T2 145 | 146 | # Expanded fluid, 2-phase 147 | p3 = p0 148 | h3 = h2 149 | self.state.update(CoolProp.HmassP_INPUTS, h3, p3) 150 | s3 = self.state.smass() 151 | T3 = self.state.T() 152 | cycle_states[3, 'H'] = h3 153 | cycle_states[3, 'S'] = s3 154 | cycle_states[3, 'P'] = p3 155 | cycle_states[3, 'T'] = T3 156 | 157 | w_net = h0 - h1 158 | q_evap = h0 - h3 159 | 160 | self.cycle_states = cycle_states 161 | self.fill_states() 162 | 163 | def simple_solve_dt(self, Te, Tc, dT_sh, dT_sc, eta_com, fluid=None, SI=True): 164 | """" 165 | A simple vapour compression cycle calculation based on 166 | superheat, subcooling and temperatures. 167 | 168 | Parameters 169 | ---------- 170 | Te : float 171 | The evaporation temperature 172 | Tc : float 173 | The condensation temperature 174 | dT_sh : float 175 | The superheat after the evaporator 176 | dT_sc : float 177 | The subcooling after the condenser 178 | eta_com : float 179 | Isentropic compressor efficiency 180 | 181 | Examples 182 | -------- 183 | >>> import CoolProp 184 | >>> from CoolProp.Plots import PropertyPlot 185 | >>> from CoolProp.Plots import SimpleCompressionCycle 186 | >>> pp = PropertyPlot('HEOS::R134a', 'PH', unit_system='EUR') 187 | >>> pp.calc_isolines(CoolProp.iQ, num=11) 188 | >>> cycle = SimpleCompressionCycle('HEOS::R134a', 'PH', unit_system='EUR') 189 | >>> Te = 265 190 | >>> Tc = 300 191 | >>> cycle.simple_solve_dt(Te, Tc, 10, 15, 0.7, SI=True) 192 | >>> cycle.steps = 50 193 | >>> sc = cycle.get_state_changes() 194 | >>> import matplotlib.pyplot as plt 195 | >>> plt.close(cycle.figure) 196 | >>> pp.draw_process(sc) 197 | """ 198 | if fluid is not None: 199 | self.state = process_fluid_state(fluid) 200 | if self._state is None: 201 | raise ValueError("You have to specify a fluid before you can calculate.") 202 | 203 | if not SI: 204 | conv_t = self._system[CoolProp.iT].to_SI 205 | Te = conv_p(Te) 206 | Tc = conv_p(Tc) 207 | 208 | # Get the saturation conditions 209 | self.state.update(CoolProp.QT_INPUTS, 1.0, Te) 210 | p0 = self.state.p() 211 | self.state.update(CoolProp.QT_INPUTS, 0.0, Tc) 212 | p2 = self.state.p() 213 | 214 | T0 = Te + dT_sh 215 | T2 = Tc - dT_sc 216 | 217 | self.simple_solve(T0, p0, T2, p2, eta_com, fluid=None, SI=True) 218 | 219 | def COP_heating(self): 220 | """COP for a heating process 221 | 222 | Calculates the coefficient of performance for a heating process, :math:`COP_h = \frac{q_{con}}{w_{comp}}`. 223 | 224 | Returns 225 | ------- 226 | float 227 | """ 228 | return (self.cycle_states[1, 'H'] - self.cycle_states[2, 'H']) / (self.cycle_states[1, 'H'] - self.cycle_states[0, 'H']) 229 | 230 | def COP_cooling(self): 231 | """COP for a cooling process 232 | 233 | Calculates the coefficient of performance for a cooling process, :math:`COP_c = \frac{q_{eva}}{w_{comp}}`. 234 | 235 | Returns 236 | ------- 237 | float 238 | """ 239 | return (self.cycle_states[0, 'H'] - self.cycle_states[3, 'H']) / (self.cycle_states[1, 'H'] - self.cycle_states[0, 'H']) 240 | -------------------------------------------------------------------------------- /CoolPlot/Plot/SimpleCyclesExpansion.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import 3 | 4 | import numpy as np 5 | 6 | import CoolProp 7 | from .Common import process_fluid_state 8 | from .SimpleCycles import BaseCycle, StateContainer 9 | 10 | 11 | class BasePowerCycle(BaseCycle): 12 | """A thermodynamic cycle for power producing processes. 13 | 14 | Defines the basic properties and methods to unify access to 15 | power cycle-related quantities. 16 | """ 17 | 18 | def __init__(self, fluid_ref='HEOS::Water', graph_type='TS', **kwargs): 19 | """see :class:`CoolProp.Plots.SimpleCycles.BaseCycle` for details.""" 20 | BaseCycle.__init__(self, fluid_ref, graph_type, **kwargs) 21 | 22 | def eta_carnot(self): 23 | """Carnot efficiency 24 | 25 | Calculates the Carnot efficiency for the specified process, :math:`\eta_c = 1 - \frac{T_c}{T_h}`. 26 | 27 | Returns 28 | ------- 29 | float 30 | """ 31 | Tvector = self._cycle_states.T 32 | return 1. - np.min(Tvector) / np.max(Tvector) 33 | 34 | def eta_thermal(self): 35 | """Thermal efficiency 36 | 37 | The thermal efficiency for the specified process(es), :math:`\eta_{th} = \frac{\dot{W}_{exp} - \dot{W}_{pum}}{\dot{Q}_{in}}`. 38 | 39 | Returns 40 | ------- 41 | float 42 | """ 43 | raise NotImplementedError("Implement it in the subclass.") 44 | 45 | 46 | class SimpleRankineCycle(BasePowerCycle): 47 | """A simple Rankine cycle *without* regeneration""" 48 | STATECOUNT = 4 49 | STATECHANGE = [ 50 | lambda inp: BaseCycle.state_change(inp, 'S', 'P', 0, ty1='log', ty2='log'), # Pumping process 51 | lambda inp: BaseCycle.state_change(inp, 'H', 'P', 1, ty1='lin', ty2='lin'), # Heat addition 52 | lambda inp: BaseCycle.state_change(inp, 'H', 'P', 2, ty1='log', ty2='log'), # Expansion 53 | lambda inp: BaseCycle.state_change(inp, 'H', 'P', 3, ty1='lin', ty2='lin') # Heat removal 54 | ] 55 | 56 | def __init__(self, fluid_ref='HEOS::Water', graph_type='TS', **kwargs): 57 | """see :class:`CoolProp.Plots.SimpleCycles.BasePowerCycle` for details.""" 58 | BasePowerCycle.__init__(self, fluid_ref, graph_type, **kwargs) 59 | 60 | def simple_solve(self, T0, p0, T2, p2, eta_exp, eta_pum, fluid=None, SI=True): 61 | """" 62 | A simple Rankine cycle calculation 63 | 64 | Parameters 65 | ---------- 66 | T0 : float 67 | The coldest point, before the pump 68 | p0 : float 69 | The lowest pressure, before the pump 70 | T2 : float 71 | The hottest point, before the expander 72 | p2 : float 73 | The highest pressure, before the expander 74 | eta_exp : float 75 | Isentropic expander efficiency 76 | eta_pum : float 77 | Isentropic pump efficiency 78 | 79 | Examples 80 | -------- 81 | >>> import CoolProp 82 | >>> from CoolProp.Plots import PropertyPlot 83 | >>> from CoolProp.Plots import SimpleRankineCycle 84 | >>> pp = PropertyPlot('HEOS::Water', 'TS', unit_system='EUR') 85 | >>> pp.calc_isolines(CoolProp.iQ, num=11) 86 | >>> cycle = SimpleRankineCycle('HEOS::Water', 'TS', unit_system='EUR') 87 | >>> T0 = 300 88 | >>> pp.state.update(CoolProp.QT_INPUTS,0.0,T0+15) 89 | >>> p0 = pp.state.keyed_output(CoolProp.iP) 90 | >>> T2 = 700 91 | >>> pp.state.update(CoolProp.QT_INPUTS,1.0,T2-150) 92 | >>> p2 = pp.state.keyed_output(CoolProp.iP) 93 | >>> cycle.simple_solve(T0, p0, T2, p2, 0.7, 0.8, SI=True) 94 | >>> cycle.steps = 50 95 | >>> sc = cycle.get_state_changes() 96 | >>> import matplotlib.pyplot as plt 97 | >>> plt.close(cycle.figure) 98 | >>> pp.draw_process(sc) 99 | 100 | """ 101 | if fluid is not None: self.state = process_fluid_state(fluid) 102 | if self._state is None: 103 | raise ValueError("You have to specify a fluid before you can calculate.") 104 | 105 | cycle_states = StateContainer(unit_system=self._system) 106 | 107 | if not SI: 108 | Tc = self._system[CoolProp.iT].to_SI 109 | pc = self._system[CoolProp.iP].to_SI 110 | T0 = Tc(T0) 111 | p0 = pc(p0) 112 | T2 = Tc(T2) 113 | p2 = pc(p2) 114 | 115 | # Subcooled liquid 116 | self.state.update(CoolProp.PT_INPUTS, p0, T0) 117 | h0 = self.state.hmass() 118 | s0 = self.state.smass() 119 | # Just a showcase for the different accessor methods 120 | cycle_states[0, 'H'] = h0 121 | cycle_states[0]['S'] = s0 122 | cycle_states[0][CoolProp.iP] = p0 123 | cycle_states[0, CoolProp.iT] = T0 124 | 125 | # Pressurised liquid 126 | p1 = p2 127 | self.state.update(CoolProp.PSmass_INPUTS, p1, s0) 128 | h1 = h0 + (self.state.hmass() - h0) / eta_pum 129 | self.state.update(CoolProp.HmassP_INPUTS, h1, p1) 130 | s1 = self.state.smass() 131 | T1 = self.state.T() 132 | cycle_states[1, 'H'] = h1 133 | cycle_states[1, 'S'] = s1 134 | cycle_states[1, 'P'] = p1 135 | cycle_states[1, 'T'] = T1 136 | 137 | # Evaporated vapour 138 | self.state.update(CoolProp.PT_INPUTS, p2, T2) 139 | h2 = self.state.hmass() 140 | s2 = self.state.smass() 141 | cycle_states[2, 'H'] = h2 142 | cycle_states[2, 'S'] = s2 143 | cycle_states[2, 'P'] = p2 144 | cycle_states[2, 'T'] = T2 145 | 146 | # Expanded gas 147 | p3 = p0 148 | self.state.update(CoolProp.PSmass_INPUTS, p3, s2) 149 | h3 = h2 - eta_exp * (h2 - self.state.hmass()) 150 | self.state.update(CoolProp.HmassP_INPUTS, h3, p3) 151 | s3 = self.state.smass() 152 | T3 = self.state.T() 153 | cycle_states[3, 'H'] = h3 154 | cycle_states[3, 'S'] = s3 155 | cycle_states[3, 'P'] = p3 156 | cycle_states[3, 'T'] = T3 157 | 158 | w_net = h2 - h3 159 | q_boiler = h2 - h1 160 | eta_th = w_net / q_boiler 161 | 162 | self.cycle_states = cycle_states 163 | self.fill_states() 164 | 165 | def eta_thermal(self): 166 | """Thermal efficiency 167 | 168 | The thermal efficiency for the specified process(es), :math:`\eta_{th} = \frac{\dot{W}_{exp} - \dot{W}_{pum}}{\dot{Q}_{in}}`. 169 | 170 | Returns 171 | ------- 172 | float 173 | """ 174 | w_net = self.cycle_states[2].H - self.cycle_states[3].H - (self.cycle_states[1].H - self.cycle_states[0].H) 175 | q_boiler = self.cycle_states[2].H - self.cycle_states[1].H 176 | return w_net / q_boiler 177 | -------------------------------------------------------------------------------- /CoolPlot/Plot/Tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import 3 | 4 | from CoolProp.Plots import PropertyPlot # TODO: Change to absolute import 5 | 6 | 7 | def main(): 8 | fluid_ref = 'n-Pentane' 9 | for plot_type in ['Ts']: # ['pt', 'ph', 'ps', 'ts', 'pt', 'prho', 'trho']: 10 | plt = PropertyPlot(fluid_ref, plot_type) 11 | plt.set_axis_limits([-0.5 * 1e3, 1.5 * 1e3, 300, 530]) 12 | plt.draw_isolines('Q', [0.1, 0.9]) 13 | plt.draw_isolines('P', [100 * 1e3, 2000 * 1e3]) 14 | plt.draw_isolines('D', [2, 600]) 15 | plt.show() 16 | 17 | 18 | if __name__ == "__main__": 19 | main() 20 | -------------------------------------------------------------------------------- /CoolPlot/Plot/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import 3 | 4 | # Bring some functions into the Plots namespace for code concision, 5 | # but be careful not to clutter the namespace with too many 6 | # classes and functions. 7 | 8 | # Plotting objects and functions 9 | from .Plots import PropertyPlot 10 | from .Common import IsoLine 11 | 12 | # Cycle calculation and drawing 13 | from .SimpleCycles import StateContainer 14 | from .SimpleCyclesExpansion import SimpleRankineCycle 15 | from .SimpleCyclesCompression import SimpleCompressionCycle 16 | 17 | # Old and deprecated objects 18 | from .SimpleCycles import SimpleCycle, TwoStage, EconomizedCycle 19 | -------------------------------------------------------------------------------- /CoolPlot/Plot/psy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import os 5 | import cPickle 6 | from ConfigParser import ConfigParser 7 | 8 | import numpy as np 9 | 10 | from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg 11 | from pylab import Figure 12 | 13 | from CoolProp.HumidAirProp import HAProps, HAProps_Aux 14 | 15 | from PyQt4.QtGui import (QDialog, QGridLayout, QProgressBar, QLabel, 16 | QDialogButtonBox, QPushButton, QFileDialog, QApplication) 17 | 18 | Preferences = ConfigParser() 19 | config_path = os.path.join(os.path.dirname(__file__), "psyrc") 20 | Preferences.read(config_path) 21 | P = Preferences.getfloat("General", "P") 22 | 23 | 24 | def _Pbar(Z): 25 | """ 26 | ASHRAE Fundamentals Handbook pag 1.1 eq. 3 27 | input: 28 | Z: altitude, m 29 | return 30 | standard atmosphere barometric pressure, Pa 31 | """ 32 | return 101325. * (1 - 2.25577e-5 * Z)**5.256 33 | 34 | 35 | class PsychroPlot(FigureCanvasQTAgg): 36 | """ 37 | Plot widget for psychrometric chart 38 | Add custom margins 39 | Define a pointer to text state properties, to remove and redraw 40 | """ 41 | 42 | def __init__(self, parent=None, width=15, height=5, dpi=100): 43 | self.fig = Figure(figsize=(width, height), dpi=dpi) 44 | FigureCanvasQTAgg.__init__(self, self.fig) 45 | self.setParent(parent) 46 | self.axes2D = self.fig.add_subplot(111) 47 | FigureCanvasQTAgg.updateGeometry(self) 48 | self.axes2D.figure.subplots_adjust(left=0.01, right=0.92, 49 | bottom=0.05, top=0.98) 50 | self.notes = [] 51 | 52 | def plot(self, *args, **kwargs): 53 | self.axes2D.plot(*args, **kwargs) 54 | 55 | def config(self): 56 | self.axes2D.set_autoscale_on(False) 57 | self.axes2D.set_xlabel(u"Tdb, ºC") 58 | self.axes2D.set_ylabel("Absolute humidity, kg/kg") 59 | self.axes2D.yaxis.set_ticks_position("right") 60 | self.axes2D.yaxis.set_label_position("right") 61 | 62 | tmin = Preferences.getfloat("Psychr", "isotdbStart") - 273.15 63 | tmax = Preferences.getfloat("Psychr", "isotdbEnd") - 273.15 64 | 65 | self.axes2D.set_xlim(tmin, tmax) 66 | self.axes2D.set_ylim(0, 0.04) 67 | 68 | def showPointData(self, state): 69 | self.clearPointData() 70 | 71 | yi = 0.99 72 | keys = "tdb", "tdp", "twb", "HR", "w", "h", "v", "rho" 73 | units = u"ºC", u"ºC", u"ºC", "%", "kgw/kgda", "kJ/kg", u"m³/kg", u"kg/m³" 74 | for key, txt in zip(keys, units): 75 | self.notes.append(self.axes2D.annotate( 76 | "%s: %0.4f %s" % (key, state.__getattribute__(key), txt), (0.01, yi), 77 | xycoords='axes fraction', size="small", va="center")) 78 | yi -= 0.025 79 | self.draw() 80 | 81 | def clearPointData(self): 82 | while self.notes: 83 | anotation = self.notes.pop() 84 | anotation.remove() 85 | self.draw() 86 | 87 | 88 | class PsyCoolprop(object): 89 | """ 90 | Psychrometric state using coolprop library 91 | 92 | kwargs definition parameters: 93 | P: Pressure, Pa 94 | z: altitude, m 95 | 96 | tdp: dew-point temperature 97 | tdb: dry-bulb temperature 98 | twb: web-bulb temperature 99 | w: Humidity Ratio [kg water/kg dry air] 100 | HR: Relative humidity 101 | h: Mixture enthalpy 102 | v: Mixture specified volume 103 | 104 | P: mandatory input for barometric pressure, z is an alternate pressure input 105 | it needs other two input parameters: 106 | 0 - tdb, w 107 | 1 - tdb, HR 108 | 2 - tdb, twb 109 | 3 - tdb, tdp 110 | 4 - tdp, HR 111 | 5 - tdp, twb 112 | 6 - twb, w 113 | """ 114 | kwargs = {"z": 0.0, 115 | "P": 0.0, 116 | 117 | "tdb": 0.0, 118 | "tdb": 0.0, 119 | "twb": 0.0, 120 | "w": None, 121 | "HR": None, 122 | "h": None, 123 | "v": 0.0} 124 | status = 0 125 | msg = "Unknown variables" 126 | 127 | def __init__(self, **kwargs): 128 | self.kwargs = self.__class__.kwargs.copy() 129 | self.__call__(**kwargs) 130 | 131 | def __call__(self, **kwargs): 132 | self.kwargs.update(kwargs) 133 | 134 | if self.calculable: 135 | self.status = 1 136 | self.calculo() 137 | self.msg = "Solved" 138 | 139 | @property 140 | def calculable(self): 141 | tdp = self.kwargs.get("tdp", 0) 142 | tdb = self.kwargs.get("tdb", 0) 143 | twb = self.kwargs.get("twb", 0) 144 | w = self.kwargs.get("w", None) 145 | HR = self.kwargs.get("HR", None) 146 | h = self.kwargs.get("h", None) 147 | v = self.kwargs.get("v", 0) 148 | 149 | self._mode = 0 150 | if tdb and w is not None: 151 | self._mode = ("Tdb", "W") 152 | elif tdb and HR is not None: 153 | self._mode = ("Tdb", "RH") 154 | elif tdb and twb: 155 | self._mode = ("Tdb", "Twb") 156 | elif tdb and tdp: 157 | self._mode = ("Tdb", "Tdp") 158 | elif tdp and HR is not None: 159 | self._mode = ("Tdp", "RH") 160 | 161 | return bool(self._mode) 162 | 163 | def calculo(self): 164 | tdp, tdb, twb, P, Pvs, Pv, ws, w, HR, v, h = self._lib() 165 | self.tdp = tdp - 273.15 166 | self.tdb = tdb - 273.15 167 | self.twb = twb - 273.15 168 | self.P = P 169 | self.Pvs = Pvs 170 | self.Pv = Pv 171 | self.ws = ws 172 | self.w = w 173 | self.HR = HR 174 | self.mu = w / ws * 100 175 | self.v = v 176 | self.rho = 1 / v 177 | self.h = h 178 | self.Xa = 1 / (1 + self.w / 0.62198) 179 | self.Xw = 1 - self.Xa 180 | 181 | def args(self): 182 | # Correct coolprop custom namespace versus pychemqt namespace 183 | if "Tdb" in self._mode: 184 | self.kwargs["Tdb"] = self.kwargs["tdb"] 185 | if "Twb" in self._mode: 186 | self.kwargs["Twb"] = self.kwargs["twb"] 187 | if "Tdp" in self._mode: 188 | self.kwargs["Tdp"] = self.kwargs["tdp"] 189 | if "RH" in self._mode: 190 | self.kwargs["RH"] = self.kwargs["HR"] 191 | if "W" in self._mode: 192 | self.kwargs["W"] = self.kwargs["w"] 193 | 194 | var1 = self.kwargs[self._mode[0]] 195 | var2 = self.kwargs[self._mode[1]] 196 | 197 | # units conversion to coolprop expected unit: 198 | # HR in 0-1, H in kJ/kg, S in kJ/kgK 199 | if "RH" in self._mode[0]: 200 | var1 /= 100. 201 | if "RH" in self._mode[1]: 202 | var2 /= 100. 203 | 204 | args = ("P", self._P_kPa, self._mode[0], var1, self._mode[1], var2) 205 | return args 206 | 207 | def _P(self): 208 | """Barometric pressure calculation, Pa""" 209 | if self.kwargs["P"]: 210 | P = self.kwargs["P"] 211 | elif self.kwargs["z"]: 212 | P = _Pbar(self.kwargs["z"]) 213 | else: 214 | P = 101325. 215 | return P 216 | 217 | @property 218 | def _P_kPa(self): 219 | """Property for ease access to pressure in kPa""" 220 | P = self._P() 221 | return P / 1000. 222 | 223 | def _lib(self): 224 | args = self.args() 225 | P = self._P() 226 | 227 | if "Tdb" in self._mode: 228 | tdb = self.kwargs["Tdb"] 229 | else: 230 | tdb = HAProps("Tdb", *args) 231 | tdp = HAProps("Tdp", *args) 232 | twb = HAProps("Twb", *args) 233 | w = HAProps("W", *args) 234 | HR = HAProps("RH", *args) * 100 235 | Pvs = HAProps_Aux("p_ws", tdb, self._P_kPa, w)[0] * 1000 236 | Pv = Pvs * HR / 100 237 | ws = HAProps("W", "P", self._P_kPa, "Tdb", tdb, "RH", 1) 238 | v = HAProps("V", *args) 239 | h = HAProps("H", *args) 240 | 241 | return tdp, tdb, twb, P, Pvs, Pv, ws, w, HR, v, h 242 | 243 | @classmethod 244 | def calculatePlot(cls, parent): 245 | """Function to calculate points in chart""" 246 | 247 | data = {} 248 | P = Preferences.getfloat("General", "P") 249 | P_kPa = P / 1000 250 | t = cls.LineList("isotdb", Preferences) 251 | 252 | # Saturation line 253 | Hs = [] 254 | for tdb in t: 255 | Hs.append(HAProps("W", "P", P_kPa, "Tdb", tdb, "RH", 1)) 256 | parent.progressBar.setValue(5 * len(Hs) / len(t)) 257 | data["t"] = t 258 | data["Hs"] = Hs 259 | 260 | # left limit of isow lines 261 | H = cls.LineList("isow", Preferences) 262 | th = [] 263 | for w in H: 264 | if w: 265 | tdp = HAProps("Tdp", "P", 101.325, "W", w, "RH", 1) 266 | th.append(tdp - 273.15) 267 | else: 268 | tmin = Preferences.getfloat("Psychr", "isotdbStart") 269 | th.append(tmin - 273.15) 270 | data["H"] = H 271 | data["th"] = th 272 | 273 | # Humidity ratio lines 274 | hr = cls.LineList("isohr", Preferences) 275 | Hr = {} 276 | cont = 0 277 | for i in hr: 278 | Hr[i] = [] 279 | for tdb in t: 280 | Hr[i].append(HAProps("W", "P", P_kPa, "Tdb", tdb, "RH", i / 100.)) 281 | cont += 1 282 | parent.progressBar.setValue(5 + 10 * cont / len(hr) / len(Hs)) 283 | data["Hr"] = Hr 284 | 285 | # Twb 286 | lines = cls.LineList("isotwb", Preferences) 287 | Twb = {} 288 | cont = 0 289 | for T in lines: 290 | ws = HAProps("W", "P", P_kPa, "RH", 1, "Tdb", T) 291 | H = [ws, 0] 292 | Tw = [T - 273.15, HAProps("Tdb", "P", P_kPa, "Twb", T, "RH", 0) - 273.15] 293 | cont += 1 294 | parent.progressBar.setValue(15 + 75 * cont / len(lines)) 295 | Twb[T] = (H, Tw) 296 | data["Twb"] = Twb 297 | 298 | # v 299 | lines = cls.LineList("isochor", Preferences) 300 | V = {} 301 | rh = np.arange(1, -0.05, -0.05) 302 | for cont, v in enumerate(lines): 303 | w = [] 304 | Td = [] 305 | for r in rh: 306 | w.append(HAProps("W", "P", P_kPa, "RH", r, "V", v)) 307 | Td.append(HAProps("Tdb", "P", P_kPa, "RH", r, "V", v) - 273.15) 308 | parent.progressBar.setValue(90 + 10 * cont / len(lines)) 309 | V[v] = (Td, w) 310 | data["v"] = V 311 | 312 | return data 313 | 314 | @staticmethod 315 | def LineList(name, Preferences): 316 | """Return a list with the values of isoline name to plot""" 317 | if Preferences.getboolean("Psychr", name + "Custom"): 318 | t = [] 319 | for i in Preferences.get("Psychr", name + 'List').split(','): 320 | if i: 321 | t.append(float(i)) 322 | else: 323 | start = Preferences.getfloat("Psychr", name + "Start") 324 | end = Preferences.getfloat("Psychr", name + "End") 325 | step = Preferences.getfloat("Psychr", name + "Step") 326 | t = np.arange(start, end, step) 327 | return t 328 | 329 | 330 | class UI_Psychrometry(QDialog): 331 | """Psychrometric charts tool""" 332 | 333 | def __init__(self, parent=None): 334 | super(UI_Psychrometry, self).__init__(parent) 335 | self.showMaximized() 336 | self.setWindowTitle("Psychrometric chart") 337 | 338 | layout = QGridLayout(self) 339 | self.diagrama2D = PsychroPlot(self, dpi=90) 340 | self.diagrama2D.fig.canvas.mpl_connect('motion_notify_event', self.scroll) 341 | layout.addWidget(self.diagrama2D, 1, 1, 1, 2) 342 | self.progressBar = QProgressBar() 343 | self.progressBar.setVisible(False) 344 | layout.addWidget(self.progressBar, 2, 1) 345 | self.status = QLabel() 346 | layout.addWidget(self.status, 2, 1) 347 | 348 | self.buttonBox = QDialogButtonBox(QDialogButtonBox.Close) 349 | butonPNG = QPushButton("Save as PNG") 350 | self.buttonBox.addButton(butonPNG, QDialogButtonBox.AcceptRole) 351 | self.buttonBox.rejected.connect(self.reject) 352 | self.buttonBox.accepted.connect(self.savePNG) 353 | layout.addWidget(self.buttonBox, 2, 2) 354 | 355 | self.plot() 356 | 357 | def savePNG(self): 358 | """Save chart image to png file""" 359 | fname = unicode(QFileDialog.getSaveFileName( 360 | self, "Save chart to file", 361 | "./", "Portable Network Graphics (*.png)")) 362 | self.diagrama2D.fig.savefig(fname, facecolor='#eeeeee') 363 | 364 | def drawlabel(self, name, Preferences, t, W, label, unit): 365 | """ 366 | Draw annotation for isolines 367 | name: name of isoline 368 | Preferences: Configparse instance of pychemqt preferences 369 | t: x array of line 370 | W: y array of line 371 | label: text value to draw 372 | unit: text units to draw 373 | """ 374 | if Preferences.getboolean("Psychr", name + "label"): 375 | tmin = Preferences.getfloat("Psychr", "isotdbStart") - 273.15 376 | tmax = Preferences.getfloat("Psychr", "isotdbEnd") - 273.15 377 | x = tmax - tmin 378 | wmin = Preferences.getfloat("Psychr", "isowStart") 379 | wmax = Preferences.getfloat("Psychr", "isowEnd") 380 | y = wmax - wmin 381 | 382 | i = 0 383 | for ti, wi in zip(t, W): 384 | if tmin <= ti <= tmax and wmin <= wi <= wmax: 385 | i += 1 386 | label = str(label) 387 | if Preferences.getboolean("Psychr", name + "units"): 388 | label += unit 389 | pos = Preferences.getfloat("Psychr", name + "position") 390 | p = int(i * pos / 100 - 1) 391 | rot = np.arctan((W[p] - W[p - 1]) / y / (t[p] - t[p - 1]) * x) * 360.0 / 2.0 / np.pi 392 | self.diagrama2D.axes2D.annotate(label, (t[p], W[p]), 393 | rotation=rot, size="small", ha="center", va="center") 394 | 395 | def plot(self): 396 | """Plot chart""" 397 | Preferences = ConfigParser() 398 | Preferences.read("psyrc") 399 | 400 | self.diagrama2D.axes2D.clear() 401 | self.diagrama2D.config() 402 | filename = "%i.pkl" % P 403 | if os.path.isfile(filename): 404 | with open(filename, "r") as archivo: 405 | data = cPickle.load(archivo) 406 | self.status.setText("Loading cached data...") 407 | QApplication.processEvents() 408 | else: 409 | self.progressBar.setVisible(True) 410 | self.status.setText("Calculating data, be patient...") 411 | QApplication.processEvents() 412 | data = PsyCoolprop.calculatePlot(self) 413 | cPickle.dump(data, open(filename, "w")) 414 | self.progressBar.setVisible(False) 415 | self.status.setText("Plotting...") 416 | QApplication.processEvents() 417 | 418 | tmax = Preferences.getfloat("Psychr", "isotdbEnd") - 273.15 419 | 420 | t = [ti - 273.15 for ti in data["t"]] 421 | Hs = data["Hs"] 422 | format = {} 423 | format["ls"] = Preferences.get("Psychr", "saturationlineStyle") 424 | format["lw"] = Preferences.getfloat("Psychr", "saturationlineWidth") 425 | format["color"] = Preferences.get("Psychr", "saturationColor") 426 | format["marker"] = Preferences.get("Psychr", "saturationmarker") 427 | format["markersize"] = 3 428 | self.diagrama2D.plot(t, Hs, **format) 429 | 430 | format = {} 431 | format["ls"] = Preferences.get("Psychr", "isotdblineStyle") 432 | format["lw"] = Preferences.getfloat("Psychr", "isotdblineWidth") 433 | format["color"] = Preferences.get("Psychr", "isotdbColor") 434 | format["marker"] = Preferences.get("Psychr", "isotdbmarker") 435 | format["markersize"] = 3 436 | for i, T in enumerate(t): 437 | self.diagrama2D.plot([T, T], [0, Hs[i]], **format) 438 | 439 | H = data["H"] 440 | th = data["th"] 441 | format = {} 442 | format["ls"] = Preferences.get("Psychr", "isowlineStyle") 443 | format["lw"] = Preferences.getfloat("Psychr", "isowlineWidth") 444 | format["color"] = Preferences.get("Psychr", "isowColor") 445 | format["marker"] = Preferences.get("Psychr", "isowmarker") 446 | format["markersize"] = 3 447 | for i, H in enumerate(H): 448 | self.diagrama2D.plot([th[i], tmax], [H, H], **format) 449 | 450 | format = {} 451 | format["ls"] = Preferences.get("Psychr", "isohrlineStyle") 452 | format["lw"] = Preferences.getfloat("Psychr", "isohrlineWidth") 453 | format["color"] = Preferences.get("Psychr", "isohrColor") 454 | format["marker"] = Preferences.get("Psychr", "isohrmarker") 455 | format["markersize"] = 3 456 | for Hr, H0 in data["Hr"].iteritems(): 457 | self.diagrama2D.plot(t, H0, **format) 458 | self.drawlabel("isohr", Preferences, t, H0, Hr, "%") 459 | 460 | format = {} 461 | format["ls"] = Preferences.get("Psychr", "isotwblineStyle") 462 | format["lw"] = Preferences.getfloat("Psychr", "isotwblineWidth") 463 | format["color"] = Preferences.get("Psychr", "isotwbColor") 464 | format["marker"] = Preferences.get("Psychr", "isotwbmarker") 465 | format["markersize"] = 3 466 | for T, (H, Tw) in data["Twb"].iteritems(): 467 | self.diagrama2D.plot(Tw, H, **format) 468 | value = T - 273.15 469 | txt = u"ºC" 470 | self.drawlabel("isotwb", Preferences, Tw, H, value, txt) 471 | 472 | format = {} 473 | format["ls"] = Preferences.get("Psychr", "isochorlineStyle") 474 | format["lw"] = Preferences.getfloat("Psychr", "isochorlineWidth") 475 | format["color"] = Preferences.get("Psychr", "isochorColor") 476 | format["marker"] = Preferences.get("Psychr", "isochormarker") 477 | format["markersize"] = 3 478 | for v, (Td, H) in data["v"].iteritems(): 479 | self.diagrama2D.plot(Td, H, **format) 480 | value = v 481 | txt = u"m³/kg" 482 | self.drawlabel("isochor", Preferences, Td, H, value, txt) 483 | 484 | self.diagrama2D.draw() 485 | self.status.setText("P = %i Pa" % P) 486 | 487 | def scroll(self, event): 488 | """Update graph annotate when mouse scroll over chart""" 489 | if event.xdata and event.ydata: 490 | punto = self.createState(event.xdata, event.ydata) 491 | if event.ydata < punto.ws: 492 | self.diagrama2D.showPointData(punto) 493 | else: 494 | self.diagrama2D.clearPointData() 495 | 496 | def createState(self, x, y): 497 | """Create psychrometric state from click or mouse position""" 498 | tdb = x + 273.15 499 | punto = PsyCoolprop(P=P, tdb=tdb, w=y) 500 | return punto 501 | 502 | 503 | if __name__ == "__main__": 504 | import sys 505 | app = QApplication(sys.argv) 506 | aireHumedo = UI_Psychrometry() 507 | aireHumedo.show() 508 | sys.exit(app.exec_()) 509 | -------------------------------------------------------------------------------- /CoolPlot/Plot/psyrc: -------------------------------------------------------------------------------- 1 | [General] 2 | P = 101325 3 | 4 | [Psychr] 5 | virial = True 6 | coolprop = True 7 | refprop = False 8 | saturationcolor = #000000 9 | saturationlinewidth = 1.0 10 | saturationlinestyle = - 11 | saturationmarker = None 12 | isotdbstart = 274.0 13 | isotdbend = 330.0 14 | isotdbstep = 1.0 15 | isotdbcustom = False 16 | isotdblist = 17 | isotdbcolor = #000000 18 | isotdblinewidth = 0.5 19 | isotdblinestyle = : 20 | isotdbmarker = None 21 | isotdblabel = False 22 | isotdbunits = False 23 | isotdbposition = 50 24 | isowstart = 0.0 25 | isowend = 0.04 26 | isowstep = 0.001 27 | isowcustom = False 28 | isowlist = 29 | isowcolor = #000000 30 | isowlinewidth = 0.5 31 | isowlinestyle = : 32 | isowmarker = None 33 | isowlabel = False 34 | isowunits = False 35 | isowposition = 50 36 | isohrstart = 10.0 37 | isohrend = 100.0 38 | isohrstep = 10.0 39 | isohrcustom = False 40 | isohrlist = 41 | isohrcolor = #000000 42 | isohrlinewidth = 0.5 43 | isohrlinestyle = -- 44 | isohrmarker = None 45 | isohrlabel = True 46 | isohrunits = True 47 | isohrposition = 85 48 | isotwbstart = 250.0 49 | isotwbend = 320.0 50 | isotwbstep = 1.0 51 | isotwbcustom = False 52 | isotwblist = 53 | isotwbcolor = #aa0000 54 | isotwblinewidth = 0.8 55 | isotwblinestyle = : 56 | isotwbmarker = None 57 | isotwblabel = False 58 | isotwbunits = False 59 | isotwbposition = 99 60 | isochorstart = 0.8 61 | isochorend = 1.0 62 | isochorstep = 0.01 63 | isochorcustom = False 64 | isochorlist = 65 | isochorcolor = #00aa00 66 | isochorlinewidth = 0.8 67 | isochorlinestyle = : 68 | isochormarker = None 69 | isochorlabel = True 70 | isochorunits = False 71 | isochorposition = 90 72 | chart = True 73 | 74 | -------------------------------------------------------------------------------- /CoolPlot/Util/EnhancedState.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division 3 | from enum import Enum 4 | from typing import Union 5 | 6 | import numpy as np 7 | 8 | from . import is_string 9 | 10 | import CoolProp 11 | from CoolProp import CoolProp as CP 12 | 13 | 14 | def get_critical_state(state: CoolProp.AbstractState) -> CoolProp.AbstractState: 15 | """Create a new state instance and update it with the critical point data 16 | 17 | Parameters 18 | ---------- 19 | state : CoolProp.AbstractState 20 | 21 | Returns 22 | ------- 23 | CoolProp.AbstractState 24 | """ 25 | crit_state = CP.PyCriticalState() 26 | crit_state.T = np.nan 27 | crit_state.p = np.nan 28 | crit_state.rhomolar = np.nan 29 | crit_state.stable = False 30 | try: 31 | crit_state.T = state.T_critical() 32 | crit_state.p = state.p_critical() 33 | crit_state.rhomolar = state.rhomolar_critical() 34 | crit_state.stable = True 35 | except: 36 | try: 37 | for crit_state_tmp in state.all_critical_points(): 38 | if crit_state_tmp.stable and (crit_state_tmp.T > crit_state.T or not np.isfinite(crit_state.T)): 39 | crit_state.T = crit_state_tmp.T 40 | crit_state.p = crit_state_tmp.p 41 | crit_state.rhomolar = crit_state_tmp.rhomolar 42 | crit_state.stable = crit_state_tmp.stable 43 | except: 44 | raise ValueError("Could not calculate the critical point data.") 45 | new_state = CoolProp.AbstractState(state.backend_name(), '&'.join(state.fluid_names())) 46 | masses = state.get_mass_fractions() 47 | if len(masses) > 1: 48 | new_state.set_mass_fractions(masses) 49 | # Uses mass fraction to work with incompressible fluids 50 | # try: new_state.build_phase_envelope("dummy") 51 | # except: pass 52 | # TODO: Remove this hack ASAP 53 | # Avoid problems with https://github.com/CoolProp/CoolProp/issues/1962 54 | BE = state.backend_name() 55 | FL = '&'.join(state.fluid_names()) 56 | if BE == "HelmholtzEOSBackend" and FL == "Ammonia": 57 | crit_state.T *= 1.001 58 | msg = "" 59 | if np.isfinite(crit_state.rhomolar) and np.isfinite(crit_state.T): 60 | try: 61 | new_state.specify_phase(CoolProp.iphase_critical_point) 62 | new_state.update(CoolProp.DmolarT_INPUTS, crit_state.rhomolar, crit_state.T) 63 | return new_state 64 | except Exception as e: 65 | msg += str(e) + " - " 66 | pass 67 | try: 68 | new_state.update(CoolProp.DmolarT_INPUTS, crit_state.rhomolar, crit_state.T) 69 | return new_state 70 | except Exception as e: 71 | msg += str(e) + " - " 72 | pass 73 | if np.isfinite(crit_state.p) and np.isfinite(crit_state.T): 74 | try: 75 | new_state.specify_phase(CoolProp.iphase_critical_point) 76 | new_state.update(CoolProp.PT_INPUTS, crit_state.p, crit_state.T) 77 | return new_state 78 | except Exception as e: 79 | msg += str(e) + " - " 80 | pass 81 | try: 82 | new_state.update(CoolProp.PT_INPUTS, crit_state.p, crit_state.T) 83 | return new_state 84 | except Exception as e: 85 | msg += str(e) + " - " 86 | pass 87 | raise ValueError("Could not calculate the critical point data. " + msg) 88 | 89 | 90 | class EnhancedState(CoolProp.AbstractState): 91 | 92 | def __init__(self, backend: str, fluid: str): 93 | CoolProp.AbstractState.__init__(backend, fluid) 94 | self._critical_state = None 95 | self._T_critical = None 96 | self._rhomass_critical = None 97 | self._rhomolar_critical = None 98 | self._p_critical = None 99 | self._smass_critical = None 100 | self._hmass_critical = None 101 | self._small = 1e-7 102 | 103 | @classmethod 104 | def from_abstract_state(cls, state: CoolProp.AbstractState): 105 | new_state = cls(state.backend_name(), '&'.join(state.fluid_names())) 106 | # Uses mass fraction to work with incompressible fluids 107 | masses = state.get_mass_fractions() 108 | if len(masses) > 1: 109 | new_state.set_mass_fractions(masses) 110 | try: 111 | rho = state.rhomolar() 112 | T = state.T() 113 | new_state.update(CoolProp.DmolarT_INPUTS, rho, T) 114 | except: 115 | pass 116 | return new_state 117 | 118 | @property 119 | def critical_state(self): 120 | if self._critical_state is None: 121 | tmp = CoolProp.AbstractState(self.backend_name(), '&'.join(self.fluid_names())) 122 | self._critical_state = get_critical_state(tmp) 123 | return self._critical_state 124 | 125 | #@property 126 | def T_critical(self): 127 | if self._T_critical is None: 128 | self._T_critical = self.critical_state.keyed_output(CoolProp.iT) 129 | return self._T_critical 130 | 131 | #@property 132 | def rhomass_critical(self): 133 | if self._rhomass_critical is None: 134 | self._rhomass_critical = self.critical_state.keyed_output(CoolProp.iDmass) 135 | return self._rhomass_critical 136 | 137 | #@property 138 | def rhomolar_critical(self): 139 | if self._rhomolar_critical is None: 140 | self._rhomolar_critical = self.critical_state.keyed_output(CoolProp.iDmolar) 141 | return self._rhomolar_critical 142 | 143 | #@property 144 | def rho_critical(self): 145 | return self.rhomass_critical() 146 | 147 | #@property 148 | def p_critical(self): 149 | if self._p_critical is None: 150 | self._p_critical = self.critical_state.keyed_output(CoolProp.iP) 151 | return self._p_critical 152 | 153 | #@property 154 | def s_critical(self): 155 | if self._smass_critical is None: 156 | self._smass_critical = self.critical_state.keyed_output(CoolProp.iSmass) 157 | return self._smass_critical 158 | 159 | #@property 160 | def h_critical(self): 161 | if self._hmass_critical is None: 162 | self._hmass_critical = self.critical_state.keyed_output(CoolProp.iHmass) 163 | return self._hmass_critical 164 | 165 | #@property 166 | #def delta_p_small(self): 167 | # return self.p_critical * self._small 168 | 169 | #@property 170 | #def delta_T_small(self): 171 | # return self.T_critical * self._small 172 | 173 | 174 | class FractionType(Enum): 175 | MOLE = 1 176 | MASS = 2 177 | VOLU = 3 178 | 179 | 180 | def process_fluid_state(fluid_ref: Union[str, CoolProp.AbstractState, EnhancedState], fraction: FractionType = FractionType.MOLE) -> EnhancedState: 181 | """Check input for state object or fluid string 182 | 183 | Parameters 184 | ---------- 185 | fluid_ref : str, CoolProp.AbstractState, EnhancedState 186 | fractions : FractionType, switch to set mass, volu or mole fractions 187 | 188 | Returns 189 | ------- 190 | EnhancedState 191 | """ 192 | if isinstance(fluid_ref, EnhancedState): 193 | return fluid_ref 194 | elif isinstance(fluid_ref, CoolProp.AbstractState): 195 | return EnhancedState.from_abstract_state(fluid_ref) 196 | elif is_string(fluid_ref): 197 | backend, fluids = CP.extract_backend(fluid_ref) 198 | fluids, fractions = CP.extract_fractions(fluids) 199 | state = EnhancedState(backend, '&'.join(fluids)) 200 | if len(fluids) > 1 and len(fluids) == len(fractions): 201 | if fraction == FractionType.MOLE: state.set_mole_fractions(fractions) 202 | elif fraction == FractionType.MASS: state.set_mass_fractions(fractions) 203 | elif fraction == FractionType.VOLU: state.set_volu_fractions(fractions) 204 | else: raise ValueError("Unknown composition type received.") 205 | return state 206 | raise TypeError("Invalid fluid_ref input, expected a string or an AbstractState instance.") 207 | -------------------------------------------------------------------------------- /CoolPlot/Util/Quantities.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division 3 | 4 | from abc import ABCMeta 5 | from six import with_metaclass 6 | import warnings 7 | import numpy as np 8 | 9 | import CoolProp 10 | 11 | from . import is_string, _get_index 12 | 13 | class BaseQuantity(object): 14 | """A very basic property that can convert an input to and from a 15 | given unit system, note that the conversion from SI units starts 16 | with a multiplication. If you need to remove an offset, use the 17 | off_SI property. 18 | Examples with temperature: 19 | celsius = BaseQuantity(add_SI=-273.15) 20 | fahrenheit = BaseQuantity(add_SI=32.0, mul_SI=1.8, off_SI=-273.15) 21 | Examples with pressure: 22 | bar = BaseQuantity(mul_SI=1e-5) 23 | psi = BaseQuantity(mul_SI=0.000145037738) 24 | """ 25 | 26 | def __init__(self, add_SI=0.0, mul_SI=1.0, off_SI=0.0): 27 | self._add_SI = add_SI 28 | self._mul_SI = mul_SI 29 | self._off_SI = off_SI 30 | 31 | @property 32 | def add_SI(self): return self._add_SI 33 | 34 | @add_SI.setter 35 | def add_SI(self, value): self._add_SI = value 36 | 37 | @property 38 | def mul_SI(self): return self._mul_SI 39 | 40 | @mul_SI.setter 41 | def mul_SI(self, value): self._mul_SI = value 42 | 43 | @property 44 | def off_SI(self): return self._off_SI 45 | 46 | @off_SI.setter 47 | def off_SI(self, value): self._off_SI = value 48 | 49 | def from_SI(self, value): return ((np.asanyarray(value) + self.off_SI) * self.mul_SI) + self.add_SI 50 | 51 | def to_SI(self, value): return (np.asanyarray(value) - self.add_SI) / self.mul_SI - self.off_SI 52 | 53 | 54 | class BaseDimension(BaseQuantity): 55 | """A dimension is a class that extends the BaseQuantity and adds a 56 | label, a symbol and a unit label. 57 | """ 58 | 59 | def __init__(self, add_SI=0.0, mul_SI=1.0, off_SI=0.0, label='', symbol='', unit=''): 60 | self._label = label 61 | self._symbol = symbol 62 | self._unit = unit 63 | super(BaseDimension, self).__init__(add_SI=add_SI, mul_SI=mul_SI, off_SI=off_SI) 64 | 65 | @property 66 | def label(self): return self._label 67 | 68 | @label.setter 69 | def label(self, value): self._label = value 70 | 71 | @property 72 | def symbol(self): return self._symbol 73 | 74 | @symbol.setter 75 | def symbol(self, value): self._symbol = value 76 | 77 | @property 78 | def unit(self): return self._unit 79 | 80 | @unit.setter 81 | def unit(self, value): self._unit = value 82 | 83 | 84 | class PropertyDict(with_metaclass(ABCMeta), object): 85 | """A collection of dimensions for all the required quantities""" 86 | 87 | def __init__(self): 88 | self._D = None 89 | self._H = None 90 | self._P = None 91 | self._S = None 92 | self._T = None 93 | self._U = None 94 | self._Q = None 95 | 96 | @property 97 | def D(self): return self._D 98 | 99 | @D.setter 100 | def D(self, value): self._D = value 101 | 102 | @property 103 | def H(self): return self._H 104 | 105 | @H.setter 106 | def H(self, value): self._H = value 107 | 108 | @property 109 | def P(self): return self._P 110 | 111 | @P.setter 112 | def P(self, value): self._P = value 113 | 114 | @property 115 | def S(self): return self._S 116 | 117 | @S.setter 118 | def S(self, value): self._S = value 119 | 120 | @property 121 | def T(self): return self._T 122 | 123 | @T.setter 124 | def T(self, value): self._T = value 125 | 126 | @property 127 | def U(self): return self._U 128 | 129 | @U.setter 130 | def U(self, value): self._U = value 131 | 132 | @property 133 | def Q(self): return self._Q 134 | 135 | @Q.setter 136 | def Q(self, value): self._Q = value 137 | 138 | @property 139 | def dimensions(self): 140 | return { 141 | CoolProp.iDmass: self._D, 142 | CoolProp.iHmass: self._H, 143 | CoolProp.iP: self._P, 144 | CoolProp.iSmass: self._S, 145 | CoolProp.iT: self._T, 146 | CoolProp.iUmass: self._U, 147 | CoolProp.iQ: self._Q 148 | } 149 | 150 | def __getitem__(self, index): 151 | """Allow for property access via square brackets""" 152 | idx = _get_index(index) 153 | if idx == CoolProp.iDmass: return self.D 154 | elif idx == CoolProp.iHmass: return self.H 155 | elif idx == CoolProp.iP: return self.P 156 | elif idx == CoolProp.iSmass: return self.S 157 | elif idx == CoolProp.iT: return self.T 158 | elif idx == CoolProp.iUmass: return self.U 159 | elif idx == CoolProp.iQ: return self.Q 160 | else: raise IndexError("Unknown index \"{0:s}\".".format(str(index))) 161 | 162 | def __setitem__(self, index, value): 163 | """Allow for property access via square brackets""" 164 | idx = _get_index(index) 165 | if idx == CoolProp.iDmass: self.D = value 166 | elif idx == CoolProp.iHmass: self.H = value 167 | elif idx == CoolProp.iP: self.P = value 168 | elif idx == CoolProp.iSmass: self.S = value 169 | elif idx == CoolProp.iT: self.T = value 170 | elif idx == CoolProp.iUmass: self.U = value 171 | elif idx == CoolProp.iQ: self.Q = value 172 | else: raise IndexError("Unknown index \"{0:s}\".".format(str(index))) 173 | -------------------------------------------------------------------------------- /CoolPlot/Util/Units.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division 3 | 4 | from .Quantities import PropertyDict, BaseDimension 5 | 6 | 7 | class SIunits(PropertyDict): 8 | def __init__(self): 9 | self._D = BaseDimension(add_SI=0.0, mul_SI=1.0, off_SI=0.0, label='Density', symbol=u'd', unit=u'kg/m3') 10 | self._H = BaseDimension(add_SI=0.0, mul_SI=1.0, off_SI=0.0, label='Specific Enthalpy', symbol=u'h', unit=u'J/kg') 11 | self._P = BaseDimension(add_SI=0.0, mul_SI=1.0, off_SI=0.0, label='Pressure', symbol=u'p', unit=u'Pa') 12 | self._S = BaseDimension(add_SI=0.0, mul_SI=1.0, off_SI=0.0, label='Specific Entropy', symbol=u's', unit=u'J/kg/K') 13 | self._T = BaseDimension(add_SI=0.0, mul_SI=1.0, off_SI=0.0, label='Temperature', symbol=u'T', unit=u'K') 14 | self._U = BaseDimension(add_SI=0.0, mul_SI=1.0, off_SI=0.0, label='Specific Internal Energy', symbol=u'u', unit=u'J/kg') 15 | self._Q = BaseDimension(add_SI=0.0, mul_SI=1.0, off_SI=0.0, label='Vapour Quality', symbol=u'x', unit=u'') 16 | 17 | 18 | class KSIunits(SIunits): 19 | def __init__(self): 20 | super(KSIunits, self).__init__() 21 | self.H.mul_SI = 1e-3 22 | self.H.unit = u'kJ/kg' 23 | self.P.mul_SI = 1e-3 24 | self.P.unit = u'kPa' 25 | self.S.mul_SI = 1e-3 26 | self.S.unit = u'kJ/kg/K' 27 | self.U.mul_SI = 1e-3 28 | self.U.unit = u'kJ/kg' 29 | 30 | 31 | class EURunits(KSIunits): 32 | def __init__(self): 33 | super(EURunits, self).__init__() 34 | self.P.mul_SI = 1e-5 35 | self.P.unit = u'bar' 36 | self.T.add_SI = -273.15 37 | self.T.unit = u'deg C' 38 | 39 | 40 | def get_unit_system_cls(): 41 | return [SIunits, KSIunits, EURunits] 42 | -------------------------------------------------------------------------------- /CoolPlot/Util/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import CoolProp 4 | 5 | def is_string(in_obj): 6 | try: 7 | return isinstance(in_obj, basestring) 8 | except NameError: 9 | return isinstance(in_obj, str) 10 | # except: 11 | # return False 12 | 13 | def _get_index(prop): 14 | if is_string(prop): 15 | return CoolProp.CoolProp.get_parameter_index(prop) 16 | elif isinstance(prop, int): 17 | return prop 18 | else: 19 | raise ValueError("Invalid input, expected a string or an int, not {0:s}.".format(str(prop))) -------------------------------------------------------------------------------- /CoolPlot/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import * 2 | -------------------------------------------------------------------------------- /CoolPlot/__version__.py: -------------------------------------------------------------------------------- 1 | # CoolPlot version information 2 | VERSION = (0, 1, 5) 3 | 4 | __version__ = '.'.join(map(str, VERSION)) 5 | -------------------------------------------------------------------------------- /CoolPlot/core.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import helpers 3 | 4 | def get_hmm(): 5 | """Get a thought.""" 6 | return 'hmmm...' 7 | 8 | 9 | def hmm(): 10 | """Contemplation...""" 11 | if helpers.get_answer(): 12 | print(get_hmm()) -------------------------------------------------------------------------------- /CoolPlot/helpers.py: -------------------------------------------------------------------------------- 1 | def get_answer(): 2 | """Get an answer.""" 3 | return True 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2014-2019 Jorrit Wronski and the other developers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md LICENSE 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for CoolPlot 2 | # 3 | 4 | .PHONY: init test coverage install docs build upload 5 | 6 | help: 7 | @echo "Please use \`make ' where is one of" 8 | @echo " init to install development packages" 9 | @echo " test to run the tests for this package" 10 | @echo " install to install the package locally" 11 | @echo " docs to create the documentation" 12 | @echo " build shorthand notation for init, install, test, docs" 13 | @echo " upload to upload wheels to PyPI" 14 | 15 | init: 16 | pip install -r requirements.txt 17 | pip install -r requirements_dev.txt 18 | 19 | test: 20 | nosetests -v --with-coverage --cover-package=CoolPlot 21 | 22 | install: 23 | python setup.py install 24 | 25 | docs: 26 | cd docs && make html 27 | 28 | build: init install test docs 29 | @echo "Build completed." 30 | 31 | upload: 32 | python setup.py upload 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![GitHub License](https://img.shields.io/github/license/coolprop/coolplot.svg) 3 | ![Coverage - Codecov](https://img.shields.io/codecov/c/gh/CoolProp/CoolPlot.svg) 4 | ![Coverage - Coveralls](https://img.shields.io/coveralls/github/CoolProp/CoolPlot.svg) 5 | ![PyPI - Version](https://img.shields.io/pypi/v/coolplot.svg) 6 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/coolplot.svg) 7 | 8 | 9 | CoolPlot 10 | ======== 11 | 12 | This repository contain code for a Python module to create property plots 13 | for engineering thermodynamics applications using [CoolProp]. 14 | 15 | It focusses on refrigeration and heat pumping applications and the enthalpy- 16 | pressure diagrams (logp,h) are considered to be more mature than others. 17 | However, entropy-temperature diagrams (T,s) have also been implemented and 18 | should work for most fluids. 19 | 20 | We are still in the process of separating the calculations from the plotting 21 | routines - expect some breaking changes until v1.0.0 is released. The code 22 | has been forked from the main [CoolProp repository] at v6.3.0 and the history 23 | was preserved, thus the old commits. 24 | 25 | Pull requests are encouraged! 26 | 27 | 28 | Installation 29 | ------------ 30 | 31 | The project gets published on [PyPi] as `coolplot` and you can install it 32 | using pip 33 | 34 | ```bash 35 | pip install coolplot 36 | ``` 37 | 38 | 39 | License 40 | ------- 41 | 42 | This Python package is released under the terms of the MIT license. 43 | 44 | 45 | [CoolProp]: http://coolprop.sourceforge.net/ 46 | [CoolProp repository]: https://github.com/CoolProp/CoolProp 47 | [PyPi]: https://docs.python.org/3/distutils/packageindex.html 48 | -------------------------------------------------------------------------------- /docs/HumidAir.rst: -------------------------------------------------------------------------------- 1 | .. _Humid-Air: 2 | 3 | Humid Air Properties 4 | ******************** 5 | 6 | .. contents:: :depth: 2 7 | 8 | Examples 9 | -------- 10 | 11 | .. jupyter-execute:: 12 | 13 | name = 'world' 14 | print('hello ' + name + '!') 15 | 16 | 17 | .. jupyter-execute:: 18 | 19 | import numpy as np 20 | from matplotlib import pyplot 21 | %matplotlib inline 22 | 23 | x = np.linspace(0, 2 * np.pi) 24 | 25 | pyplot.plot(x, np.sin(x) / x) 26 | pyplot.plot(x, np.cos(x)) 27 | pyplot.grid() 28 | 29 | 30 | 31 | Psychrometric Chart 32 | ------------------- 33 | 34 | .. plot:: 35 | 36 | import numpy as np 37 | import CoolProp.CoolProp as CP 38 | import matplotlib.pyplot as plt 39 | 40 | fig, ax = plt.subplots(1,1,figsize=(10, 8)) 41 | Tdbvec = np.linspace(-30, 55)+273.15 42 | 43 | # Lines of constant relative humidity 44 | for RH in np.arange(0.1, 1, 0.1): 45 | W = CP.HAPropsSI("W","R",RH,"P",101325,"T",Tdbvec) 46 | plt.plot(Tdbvec-273.15, W, color='k', lw = 0.5) 47 | 48 | # Saturation curve 49 | W = CP.HAPropsSI("W","R",1,"P",101325,"T",Tdbvec) 50 | plt.plot(Tdbvec-273.15, W, color='k', lw=1.5) 51 | 52 | # Lines of constant Vda 53 | for Vda in np.arange(0.69, 0.961, 0.01): 54 | R = np.linspace(0,1) 55 | W = CP.HAPropsSI("W","R",R,"P",101325,"Vda",Vda) 56 | Tdb = CP.HAPropsSI("Tdb","R",R,"P",101325,"Vda",Vda) 57 | plt.plot(Tdb-273.15, W, color='b', lw=1.5 if abs(Vda % 0.05) < 0.001 else 0.5) 58 | 59 | # Lines of constant wetbulb 60 | for Twb_C in np.arange(-16, 33, 2): 61 | if Twb_C == 0: 62 | continue 63 | R = np.linspace(0.0, 1) 64 | print(Twb_C) 65 | Tdb = CP.HAPropsSI("Tdb","R",R,"P",101325,"Twb",Twb_C+273.15) 66 | W = CP.HAPropsSI("W","R",R,"P",101325,"Tdb",Tdb) 67 | plt.plot(Tdb-273.15, W, color='r', lw=1.5 if abs(Twb_C % 10) < 0.001 else 0.5) 68 | 69 | plt.xlabel(r'Dry bulb temperature $T_{\rm db}$ ($^{\circ}$ C)') 70 | plt.ylabel(r'Humidity Ratio $W$ (kg/kg)') 71 | plt.ylim(0, 0.030) 72 | plt.xlim(-30, 55) 73 | # plt.show() 74 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/sample.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sample.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/sample" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sample" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # sample documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Apr 16 21:22:43 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os, datetime 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['matplotlib.sphinxext.plot_directive', 29 | 'sphinx.ext.mathjax', 30 | 'sphinx.ext.autodoc', 31 | 'sphinx.ext.doctest', 32 | 'sphinx.ext.inheritance_diagram', 33 | 'numpydoc', 34 | 'nbsphinx'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix of source filenames. 40 | source_suffix = '.rst' 41 | 42 | # The encoding of source files. 43 | #source_encoding = 'utf-8-sig' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | 49 | 50 | d = datetime.datetime.today() 51 | here = os.path.abspath(os.path.dirname(__file__)) 52 | 53 | about = {} 54 | with open(os.path.join(here, '..', 'CoolPlot', '__version__.py')) as f: 55 | exec(f.read(), about) 56 | 57 | 58 | 59 | # General information about the project. 60 | project = u'CoolPlot' 61 | copyright = u'2014-{0}, CoolPlot developers'.format(d.year) 62 | 63 | # The version info for the project you're documenting, acts as replacement for 64 | # |version| and |release|, also used in various other places throughout the 65 | # built documents. 66 | # 67 | # The short X.Y version. 68 | version = 'v' + about['__version__'] 69 | # The full version, including alpha/beta/rc tags. 70 | release = 'v' + about['__version__'] 71 | 72 | # The language for content autogenerated by Sphinx. Refer to documentation 73 | # for a list of supported languages. 74 | #language = None 75 | 76 | # There are two options for replacing |today|: either, you set today to some 77 | # non-false value, then it is used: 78 | #today = '' 79 | # Else, today_fmt is used as the format for a strftime call. 80 | #today_fmt = '%B %d, %Y' 81 | 82 | # List of patterns, relative to source directory, that match files and 83 | # directories to ignore when looking for source files. 84 | exclude_patterns = ['_build'] 85 | 86 | # The reST default role (used for this markup: `text`) to use for all documents. 87 | #default_role = None 88 | 89 | # If true, '()' will be appended to :func: etc. cross-reference text. 90 | #add_function_parentheses = True 91 | 92 | # If true, the current module name will be prepended to all description 93 | # unit titles (such as .. function::). 94 | #add_module_names = True 95 | 96 | # If true, sectionauthor and moduleauthor directives will be shown in the 97 | # output. They are ignored by default. 98 | #show_authors = False 99 | 100 | # The name of the Pygments (syntax highlighting) style to use. 101 | pygments_style = 'sphinx' 102 | 103 | # A list of ignored prefixes for module index sorting. 104 | #modindex_common_prefix = [] 105 | 106 | 107 | # -- Options for nbsphinx --------------------------------------------------- 108 | 109 | exclude_patterns += ['**.ipynb_checkpoints'] 110 | # List of arguments to be passed to the kernel that executes the notebooks: 111 | nbsphinx_execute_arguments = [ 112 | "--InlineBackend.figure_formats={'svg', 'pdf'}", 113 | "--InlineBackend.rc={'figure.dpi': 96}", 114 | ] 115 | 116 | 117 | # -- Options for HTML output --------------------------------------------------- 118 | 119 | # The theme to use for HTML and HTML Help pages. See the documentation for 120 | # a list of builtin themes. 121 | html_theme = 'default' 122 | 123 | # Theme options are theme-specific and customize the look and feel of a theme 124 | # further. For a list of options available for each theme, see the 125 | # documentation. 126 | #html_theme_options = {} 127 | 128 | # Add any paths that contain custom themes here, relative to this directory. 129 | #html_theme_path = [] 130 | 131 | # The name for this set of Sphinx documents. If None, it defaults to 132 | # " v documentation". 133 | #html_title = None 134 | 135 | # A shorter title for the navigation bar. Default is the same as html_title. 136 | #html_short_title = None 137 | 138 | # The name of an image file (relative to this directory) to place at the top 139 | # of the sidebar. 140 | #html_logo = None 141 | 142 | # The name of an image file (within the static path) to use as favicon of the 143 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 144 | # pixels large. 145 | #html_favicon = None 146 | 147 | # Add any paths that contain custom static files (such as style sheets) here, 148 | # relative to this directory. They are copied after the builtin static files, 149 | # so a file named "default.css" will overwrite the builtin "default.css". 150 | html_static_path = ['_static'] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 153 | # using the given strftime format. 154 | #html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | #html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | #html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | #html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | #html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | #html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | #html_file_suffix = None 192 | 193 | # Output file base name for HTML help builder. 194 | htmlhelp_basename = 'sampledoc' 195 | 196 | 197 | # -- Options for LaTeX output -------------------------------------------------- 198 | 199 | latex_elements = { 200 | # The paper size ('letterpaper' or 'a4paper'). 201 | #'papersize': 'letterpaper', 202 | 203 | # The font size ('10pt', '11pt' or '12pt'). 204 | #'pointsize': '10pt', 205 | 206 | # Additional stuff for the LaTeX preamble. 207 | #'preamble': '', 208 | } 209 | 210 | # Grouping the document tree into LaTeX files. List of tuples 211 | # (source start file, target name, title, author, documentclass [howto/manual]). 212 | latex_documents = [ 213 | #('index', 'sample.tex', u'sample Documentation', 214 | # u'Kenneth Reitz', 'manual'), 215 | ] 216 | 217 | # The name of an image file (relative to this directory) to place at the top of 218 | # the title page. 219 | #latex_logo = None 220 | 221 | # For "manual" documents, if this is true, then toplevel headings are parts, 222 | # not chapters. 223 | #latex_use_parts = False 224 | 225 | # If true, show page references after internal links. 226 | #latex_show_pagerefs = False 227 | 228 | # If true, show URL addresses after external links. 229 | #latex_show_urls = False 230 | 231 | # Documents to append as an appendix to all manuals. 232 | #latex_appendices = [] 233 | 234 | # If false, no module index is generated. 235 | #latex_domain_indices = True 236 | 237 | 238 | # -- Options for manual page output -------------------------------------------- 239 | 240 | # One entry per manual page. List of tuples 241 | # (source start file, name, description, authors, manual section). 242 | man_pages = [ 243 | #('index', 'sample', u'sample Documentation', 244 | # [u'Kenneth Reitz'], 1) 245 | ] 246 | 247 | # If true, show URL addresses after external links. 248 | #man_show_urls = False 249 | 250 | 251 | # -- Options for Texinfo output ------------------------------------------------ 252 | 253 | # Grouping the document tree into Texinfo files. List of tuples 254 | # (source start file, target name, title, author, 255 | # dir menu entry, description, category) 256 | texinfo_documents = [ 257 | #('index', 'sample', u'sample Documentation', 258 | # u'Kenneth Reitz', 'sample', 'One line description of project.', 259 | # 'Miscellaneous'), 260 | ] 261 | 262 | # Documents to append as an appendix to all manuals. 263 | #texinfo_appendices = [] 264 | 265 | # If false, no module index is generated. 266 | #texinfo_domain_indices = True 267 | 268 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 269 | #texinfo_show_urls = 'footnote' 270 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. You can adapt this file completely to your liking, but it should at least 2 | contain the root `toctree` directive. 3 | 4 | Welcome to CoolPlot's documentation! 5 | ==================================== 6 | 7 | Contents: 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | HumidAir 13 | 14 | 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | 24 | 25 | Mixture Syntax 26 | ============== 27 | 28 | You can also specify mixtures straight away and pass the mole fractions as part of the 29 | fluid string. 30 | 31 | .. plot:: 32 | :include-source: 33 | 34 | import matplotlib.pyplot as plt 35 | from CoolPlot.Plot.Plots import PropertyPlot 36 | fig = plt.figure() 37 | plot = PropertyPlot("REFPROP::ISOBUTAN[0.8]&PROPANE[0.2]", 'PH', unit_system='EUR', tp_limits='ACHP', figure=fig) 38 | plot.calc_isolines() 39 | plot.show() 40 | 41 | If you would like to specify the mass fractions instead, you have to construct the state 42 | object separately and pass it to the plot object instead of a string. 43 | 44 | .. plot:: 45 | :include-source: 46 | 47 | import CoolProp 48 | state = CoolProp.AbstractState("REFPROP", "ISOBUTAN&PROPANE") 49 | state.set_mass_fractions([0.8,0.2]) 50 | import matplotlib.pyplot as plt 51 | from CoolPlot.Plot.Plots import PropertyPlot 52 | fig = plt.figure() 53 | plot = PropertyPlot(state, 'TS', unit_system='EUR', tp_limits='ACHP', figure=fig) 54 | plot.calc_isolines() 55 | plot.show() 56 | 57 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\sample.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sample.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: CoolPlot 2 | dependencies: 3 | - numpy 4 | - matplotlib 5 | - nose 6 | - coverage 7 | - sphinx 8 | - numpydoc 9 | - twine 10 | - pip 11 | - pip: 12 | - coolprop 13 | - nbsphinx 14 | -------------------------------------------------------------------------------- /mains/00_plot.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import os 4 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 5 | 6 | from CoolPlot.Plot import PropertyPlot 7 | plot = PropertyPlot('R290', 'ph') 8 | plot.calc_isolines() 9 | plot.show() 10 | -------------------------------------------------------------------------------- /mains/all_fluids.py: -------------------------------------------------------------------------------- 1 | import CoolProp 2 | from CoolProp.Plots import PropertyPlot 3 | 4 | fluids = CoolProp.CoolProp.FluidsList() 5 | 6 | for i, fluid in enumerate(fluids): 7 | plot = PropertyPlot(fluid, 'ph') 8 | plot.calc_isolines() 9 | plot.show() 10 | 11 | if i > 3: 12 | break 13 | -------------------------------------------------------------------------------- /mains/test_plots.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | 4 | 5 | def test_back_compatibility(): 6 | fluid_ref = 'R290' 7 | 8 | def Ts_plot_tests(): 9 | from CoolProp.Plots import Ts 10 | Ts(fluid_ref, show=False) 11 | 12 | from matplotlib import pyplot 13 | fig = pyplot.figure(2) 14 | ax = fig.gca() 15 | Ts(fluid_ref, show=False, axis=ax) 16 | plt.close() 17 | Ts(fluid_ref, show=False, Tmin=200, Tmax=300) 18 | plt.close() 19 | 20 | def Ph_plot_tests(): 21 | from CoolProp.Plots import Ph 22 | Ph(fluid_ref, show=False) 23 | 24 | from matplotlib import pyplot 25 | fig = pyplot.figure(2) 26 | ax = fig.gca() 27 | Ph(fluid_ref, show=False, axis=ax) 28 | plt.close() 29 | Ph(fluid_ref, show=False, Tmin=200, Tmax=300) 30 | plt.close() 31 | 32 | def PT_plot_tests(): 33 | from CoolProp.Plots import PT 34 | PT(fluid_ref, show=False) 35 | 36 | from matplotlib import pyplot 37 | fig = pyplot.figure(2) 38 | ax = fig.gca() 39 | PT(fluid_ref, show=False, axis=ax) 40 | plt.close() 41 | PT(fluid_ref, show=False, Tmin=200, Tmax=300) 42 | plt.close() 43 | 44 | def Ps_plot_tests(): 45 | from CoolProp.Plots import Ps 46 | Ps(fluid_ref, show=False) 47 | 48 | from matplotlib import pyplot 49 | fig = pyplot.figure(2) 50 | ax = fig.gca() 51 | Ps(fluid_ref, show=False, axis=ax) 52 | plt.close() 53 | Ps(fluid_ref, show=False, Tmin=200, Tmax=300) 54 | plt.close() 55 | 56 | def Prho_plot_tests(): 57 | from CoolProp.Plots import Prho 58 | Prho(fluid_ref, show=False) 59 | 60 | from matplotlib import pyplot 61 | fig = pyplot.figure(2) 62 | ax = fig.gca() 63 | Prho(fluid_ref, show=False, axis=ax) 64 | plt.close() 65 | Prho(fluid_ref, show=False, Tmin=200, Tmax=300) 66 | plt.close() 67 | 68 | def Trho_plot_tests(): 69 | from CoolProp.Plots import Trho 70 | Trho(fluid_ref, show=False) 71 | 72 | from matplotlib import pyplot 73 | fig = pyplot.figure(2) 74 | ax = fig.gca() 75 | Trho(fluid_ref, show=False, axis=ax) 76 | plt.close() 77 | Trho(fluid_ref, show=False, Tmin=200, Tmax=300) 78 | plt.close() 79 | 80 | def hs_plot_tests(): 81 | from CoolProp.Plots import hs 82 | hs(fluid_ref, show=False) 83 | 84 | from matplotlib import pyplot 85 | fig = pyplot.figure(2) 86 | ax = fig.gca() 87 | hs(fluid_ref, show=False, axis=ax) 88 | plt.close() 89 | hs(fluid_ref, show=False, Tmin=200, Tmax=300) 90 | plt.close() 91 | 92 | def Isolines_plot_tests(): 93 | from matplotlib import pyplot 94 | from CoolProp.Plots import Ts, drawIsoLines 95 | ax = Ts(fluid_ref) 96 | #ax.set_xlim([-0.5, 1.5]) 97 | #ax.set_ylim([300, 530]) 98 | quality = drawIsoLines(fluid_ref, 'Ts', 'Q', [0.3, 0.5, 0.7, 0.8], axis=ax) 99 | isobars = drawIsoLines(fluid_ref, 'Ts', 'P', [100, 2000], num=5, axis=ax) 100 | isochores = drawIsoLines(fluid_ref, 'Ts', 'D', [2, 600], num=7, axis=ax) 101 | pyplot.close() 102 | 103 | Ts_plot_tests() 104 | Ph_plot_tests() 105 | Ps_plot_tests() 106 | PT_plot_tests() 107 | Prho_plot_tests() 108 | Trho_plot_tests() 109 | hs_plot_tests() 110 | Isolines_plot_tests() 111 | 112 | 113 | def test_new_code(): 114 | fluid_ref = 'Water' 115 | 116 | def Ts_plot_tests(): 117 | from CoolProp.Plots import PropsPlot 118 | PP = PropsPlot(fluid_ref, 'Ts') 119 | plt.close() 120 | 121 | def Ph_plot_tests(): 122 | from CoolProp.Plots import PropsPlot 123 | PP = PropsPlot(fluid_ref, 'Ph') 124 | plt.close() 125 | 126 | def Isolines_plot_tests(): 127 | from CoolProp.Plots import PropsPlot 128 | PP = PropsPlot(fluid_ref, 'Ts') 129 | #plt.set_axis_limits([-0.5, 1.5, 300, 530]) 130 | PP.draw_isolines('Q', [0.3, 0.5, 0.7, 0.8]) 131 | PP.draw_isolines('P', [100, 2000], num=5) 132 | PP.draw_isolines('D', [2, 600], num=7) 133 | plt.close() 134 | 135 | def Graph_annotations(): 136 | from CoolProp.Plots import PropsPlot, IsoLines 137 | PP = PropsPlot(fluid_ref, 'Ts') 138 | PP.draw_isolines('Q', [0.3, 0.5, 0.7, 0.8]) 139 | PP.draw_isolines('P', [100, 2000], num=5) 140 | PP.draw_isolines('D', [2, 600], num=7) 141 | plt.title('New Title') 142 | PP.xlabel('New x label') 143 | PP.ylabel('New y label') 144 | PP = IsoLines(fluid_ref, 'Ts', 'P') 145 | PP.draw_isolines([100, 2000], num=5) 146 | plt.close() 147 | 148 | def Mixture(): 149 | from CoolProp.Plots import PropsPlot 150 | PP = PropsPlot('REFPROP-MIX:R32[0.47319469]&R125[0.2051091]&R134a[0.32169621]', 'TD') 151 | PP._plot_default_annotations() 152 | plt.close() 153 | 154 | Ts_plot_tests() 155 | Ph_plot_tests() 156 | Isolines_plot_tests() 157 | Graph_annotations() 158 | Mixture() 159 | 160 | 161 | if __name__ == '__main__': 162 | import nose 163 | nose.runmodule() 164 | -------------------------------------------------------------------------------- /make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for CoolPlot 4 | 5 | if "%1" == "" goto help 6 | 7 | if "%1" == "help" ( 8 | :help 9 | echo.Please use `make ^` where ^ is one of 10 | echo. init to install development packages 11 | echo. test to run the tests for this package 12 | echo. install to install the package locally 13 | echo. docs to create the documentation 14 | echo. build shorthand notation for init, install, test, docs 15 | echo. upload to upload wheels to PyPI 16 | goto end 17 | ) 18 | 19 | :: if "%1" == "init" ( 20 | :: for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 21 | :: del /q /s %BUILDDIR%\* 22 | :: goto end 23 | :: ) 24 | 25 | if "%1" == "init" ( 26 | pip install -r requirements.txt 27 | pip install -r requirements_dev.txt 28 | goto end 29 | ) 30 | 31 | if "%1" == "test" ( 32 | nosetests -v --with-coverage --cover-package=CoolPlot 33 | goto end 34 | ) 35 | 36 | if "%1" == "install" ( 37 | python setup.py install 38 | goto end 39 | ) 40 | 41 | if "%1" == "docs" ( 42 | cd docs && make html 43 | goto end 44 | ) 45 | 46 | if "%1" == "build" ( 47 | CALL make.bat init 48 | CALL make.bat install 49 | CALL make.bat test 50 | CALL make.bat docs 51 | @echo "Build completed." 52 | goto end 53 | ) 54 | 55 | if "%1" == "upload" ( 56 | python setup.py upload 57 | goto end 58 | ) 59 | 60 | :end 61 | -------------------------------------------------------------------------------- /plots/_base.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import print_function, division, absolute_import 3 | 4 | import os 5 | import sys 6 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 7 | 8 | import CoolProp 9 | from CoolPlot.Plot.Plots import PropertyPlot 10 | 11 | class iso_props(dict): 12 | 13 | def __init__(self, *args, **kwargs): 14 | res = super().__init__(*args, **kwargs) 15 | self['iso_type']=None 16 | self['iso_range']=None 17 | self['num']=15 18 | self['rounding']=False 19 | self['points']=250 20 | return 21 | 22 | 23 | def create_ph_plot(fluid : str, props : list = []): 24 | plot = PropertyPlot(fluid, 'ph', unit_system='EUR', tp_limits='ACHP') 25 | for prop in props: 26 | plot.calc_isolines(**prop) 27 | plot.show() 28 | input("Created ph-plot for {0}, press Enter to continue...".format(fluid)) 29 | return plot 30 | 31 | 32 | if __name__ == "__main__": 33 | plot = PropertyPlot('REFPROP::R455A.mix', 'ph', unit_system='EUR', tp_limits='ACHP') 34 | plot.calc_isolines(CoolProp.iQ, num=11) 35 | plot.calc_isolines(CoolProp.iT, num=25) 36 | plot.calc_isolines(CoolProp.iSmass, num=15) 37 | plot.savefig('R455A.png') 38 | plot.show() 39 | 40 | input("Press Enter to continue...") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | coolprop 4 | -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | coolprop 4 | nose 5 | coverage 6 | sphinx 7 | numpydoc 8 | nbsphinx 9 | twine 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Note: To use the 'upload' functionality of this file, you must: 5 | # $ pipenv install twine --dev 6 | 7 | import io 8 | import os 9 | import sys 10 | from shutil import rmtree 11 | 12 | from setuptools import find_packages, setup, Command 13 | 14 | # Package meta-data. 15 | NAME = 'CoolPlot' 16 | DESCRIPTION = 'Create property plots for engineering thermodynamics using CoolProp as data source.' 17 | URL = 'https://github.com/CoolProp/CoolPlot' 18 | EMAIL = 'coolplot@jorrit.org' 19 | AUTHOR = 'Jorrit Wronski' 20 | REQUIRES_PYTHON = '>=3.6.0' 21 | #VERSION = '0.1.1' 22 | VERSION = False 23 | 24 | here = os.path.abspath(os.path.dirname(__file__)) 25 | 26 | # What packages are required for this module to be executed? 27 | # Load the requirements from a file 28 | with open(os.path.join(here, 'requirements.txt')) as f: 29 | REQUIRED = f.read().splitlines() 30 | 31 | # What packages are optional? 32 | EXTRAS = { 33 | # 'fancy feature': ['django'], 34 | } 35 | 36 | # The rest you shouldn't have to touch too much :) 37 | # ------------------------------------------------ 38 | # Except, perhaps the License and Trove Classifiers! 39 | # If you do change the License, remember to change the Trove Classifier for that! 40 | 41 | # Import the README and use it as the long-description. 42 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file! 43 | try: 44 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f: 45 | long_description = '\n' + f.read() 46 | except FileNotFoundError: 47 | long_description = DESCRIPTION 48 | 49 | # Load the package's __version__.py module as a dictionary. 50 | about = {} 51 | if not VERSION: 52 | #project_slug = NAME.lower().replace("-", "_").replace(" ", "_") 53 | #with open(os.path.join(here, project_slug, '__version__.py')) as f: 54 | with open(os.path.join(here, NAME, '__version__.py')) as f: 55 | exec(f.read(), about) 56 | else: 57 | about['__version__'] = VERSION 58 | 59 | 60 | class UploadCommand(Command): 61 | """Support setup.py upload.""" 62 | 63 | description = 'Build and publish the package.' 64 | user_options = [] 65 | 66 | @staticmethod 67 | def status(s): 68 | """Prints things in bold.""" 69 | print('\033[1m{0}\033[0m'.format(s)) 70 | 71 | def initialize_options(self): 72 | pass 73 | 74 | def finalize_options(self): 75 | pass 76 | 77 | def run(self): 78 | try: 79 | self.status('Removing previous builds...') 80 | rmtree(os.path.join(here, 'dist')) 81 | except OSError: 82 | pass 83 | 84 | self.status('Building Source and Wheel (universal) distribution...') 85 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable)) 86 | 87 | #self.status('Uploading the package to the PyPI test repository using Twine...') 88 | #os.system('twine upload --verbose --repository-url https://test.pypi.org/legacy/ dist/*') 89 | 90 | self.status('Uploading the package to the PyPI repository using Twine...') 91 | os.system('twine upload dist/*') 92 | 93 | self.status('Pushing git tags…') 94 | os.system('git tag v{0}'.format(about['__version__'])) 95 | os.system('git push --tags') 96 | 97 | sys.exit() 98 | 99 | 100 | # Where the magic happens: 101 | setup( 102 | name=NAME, 103 | version=about['__version__'], 104 | description=DESCRIPTION, 105 | long_description=long_description, 106 | long_description_content_type='text/markdown', 107 | author=AUTHOR, 108 | author_email=EMAIL, 109 | python_requires=REQUIRES_PYTHON, 110 | url=URL, 111 | packages=find_packages(exclude=["tests", "*.tests", "*.tests.*", "tests.*"]), 112 | # If your package is a single module, use this instead of 'packages': 113 | # py_modules=['mypackage'], 114 | 115 | # entry_points={ 116 | # 'console_scripts': ['mycli=mymodule:cli'], 117 | # }, 118 | install_requires=REQUIRED, 119 | extras_require=EXTRAS, 120 | include_package_data=True, 121 | license='MIT', 122 | classifiers=[ 123 | # Trove classifiers 124 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers 125 | 'License :: OSI Approved :: MIT License', 126 | 'Programming Language :: Python', 127 | 'Programming Language :: Python :: 3', 128 | 'Programming Language :: Python :: 3.6', 129 | 'Programming Language :: Python :: Implementation :: CPython', 130 | 'Programming Language :: Python :: Implementation :: PyPy' 131 | ], 132 | # $ setup.py publish support. 133 | cmdclass={ 134 | 'upload': UploadCommand, 135 | }, 136 | ) 137 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CoolProp/CoolPlot/0967e243934c0970ef830b43befeabacc688cdfd/tests/__init__.py -------------------------------------------------------------------------------- /tests/context.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | import os 5 | sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) 6 | 7 | import CoolPlot 8 | -------------------------------------------------------------------------------- /tests/test_advanced.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .context import CoolPlot 4 | 5 | import unittest 6 | 7 | 8 | class AdvancedTestSuite(unittest.TestCase): 9 | """Advanced test cases.""" 10 | 11 | def test_thoughts(self): 12 | self.assertIsNone(CoolPlot.hmm()) 13 | 14 | 15 | if __name__ == '__main__': 16 | unittest.main() 17 | -------------------------------------------------------------------------------- /tests/test_basic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .context import CoolPlot 4 | 5 | import unittest 6 | 7 | 8 | class BasicTestSuite(unittest.TestCase): 9 | """Basic test cases.""" 10 | 11 | def test_absolute_truth_and_meaning(self): 12 | assert True 13 | 14 | 15 | if __name__ == '__main__': 16 | unittest.main() -------------------------------------------------------------------------------- /tests/test_units.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import unittest 4 | 5 | try: 6 | from .context import CoolPlot 7 | import CoolPlot.Util.Units 8 | except ImportError as ie: 9 | print(str(ie)) 10 | 11 | #from CoolPlot.Util.Units import SIunits, KSIunits, EURunits 12 | 13 | 14 | class UnitSystemTests(unittest.TestCase): 15 | """Tests for the unit systems""" 16 | 17 | def _systems_to_test(self): 18 | return CoolPlot.Util.Units.get_unit_system_cls() 19 | 20 | def test_round_trip(self): 21 | #systems = [us() for us in self._systems_to_test()] 22 | systems = self._systems_to_test() 23 | for _us in systems: 24 | us = _us() 25 | for dim in us.dimensions.keys(): 26 | raw_value = 23.0 27 | si_value = us[dim].to_SI(raw_value) 28 | us_value = us[dim].from_SI(si_value) 29 | self.assertAlmostEqual(raw_value, us_value) 30 | 31 | if __name__ == '__main__': 32 | unittest.main() 33 | #tst = UnitSystemTests() 34 | #tst.run() 35 | 36 | 37 | 38 | 39 | #if __name__ == '__main__': 40 | 41 | # for from in [SIunits(), KSIunits(), EURunits()]: 42 | 43 | # print(sys.H.label) 44 | # print(sys.H.to_SI(20)) 45 | # print(sys.P.label) 46 | # print(sys.P.to_SI(20)) 47 | 48 | # # i_index, x_index, y_index, value=None, state=None) 49 | # iso = IsoLine('T', 'H', 'P') 50 | # print(iso.get_update_pair()) 51 | 52 | # state = AbstractState("HEOS", "water") 53 | # iso = IsoLine('T', 'H', 'P', 300.0, state) 54 | # hr = PropsSI("H", "T", [290, 310], "P", [1e5, 1e5], "water") 55 | # pr = np.linspace(0.9e5, 1.1e5, 3) 56 | # iso.calc_range(hr, pr) 57 | # print(iso.x, iso.y) 58 | 59 | # iso = IsoLine('Q', 'H', 'P', 0.0, state) 60 | # iso.calc_range(hr, pr); print(iso.x, iso.y) 61 | # iso = IsoLine('Q', 'H', 'P', 1.0, state) 62 | # iso.calc_range(hr, pr); print(iso.x, iso.y) 63 | 64 | # # bp = BasePlot(fluid_ref, graph_type, unit_system = 'KSI', **kwargs): 65 | # bp = BasePlot('n-Pentane', 'PH', unit_system='EUR') 66 | # # print(bp._get_sat_bounds('P')) 67 | # # print(bp._get_iso_label(iso)) 68 | # print(bp.get_axis_limits()) 69 | 70 | # # get_update_pair(CoolProp.iP,CoolProp.iSmass,CoolProp.iT) -> (0,1,2,CoolProp.PSmass_INPUTS) 71 | # # other values require switching and swapping 72 | # # get_update_pair(CoolProp.iSmass,CoolProp.iP,CoolProp.iHmass) -> (1,0,2,CoolProp.PSmass_INPUTS) --------------------------------------------------------------------------------