Source code for ThermoPyle

"""
.. module:: ThermoPyle
    :platform: Unix, Windows
    :synopsis: Uses CoolProp to access thermodynamic data about Fluids.

.. moduleauthor:: Nick Meyer <nmeyer5435@gmail.com>
"""

import json
from copy import deepcopy
from itertools import permutations
from random import randint
from typing import Any, Callable, Dict, List, Text, Tuple, TypeVar, Union

import arrow
import CoolProp.CoolProp as CP
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pyrsistent as pyr
from matplotlib import cm, mlab
from mpl_toolkits.mplot3d import Axes3D
from toolz.curried import curry, get, itemmap, pipe

T = TypeVar("ThermoFluid")
C = TypeVar("CSVFluid")

UNITS = {
    "P": "Pa",
    "T": "K",
    "S": "J/kg/K",
    "G": "J/kg",
    "U": "J/kg",
    "D": "kg/m^3",
    "VISCOSITY": "Pa*s",
    "H": "J/kg",
    "PHASE": "",
}


@curry
def apply(f, x):
    return f(x)


[docs]class ThermoFluid: """ A class that will contain the data requested. Call the class with these arguments Parameters: fluid (str): CoolProp Fluid type to use xvar (str): x variable yvar (str): y variable zvar (str): z var (generated by code), normally S, U numPoints (Union[List[int],int]): number of Points for linspaces colorMap (str): What color map to use Attributes: xvar (str): What variable is on the x-axis yvar (str): What variable is on the y-axis zvar (str): What variable is on the z-axis, also, what variable is generated by CoolProp colorMap (str): What ColorMap Matplotlib uses to make its plot vars (List[Text]): a list of xvar, yvar, zvar fluid (str): What fluid is being modeled data (pandas.DataFrame): The data for the given fluid units (List[Text]): the units for xvar, yvar, zvar, in that order. numPoints (List[int]): the x-resolution and y-resolution, in that order. M (float): the molar mass of the fluid for the system. meta (pyrsistent._pmap.PMap): a PMap containing metadata related to the current fluid. """ def __init__(self, fluid: str="Water", xvar: str="T", yvar: str="P", zvar: str="S", numPoints: Union[List[int], int]=[217, 217], colorMap: str="viridis"): self.fluid = fluid if type(numPoints) is int: self.numPoints = [numPoints, numPoints] else: self.numPoints = [numPoints[0], numPoints[1]] self.colorMap = colorMap self.xvar = xvar self.yvar = yvar self.zvar = zvar self.vars = [self.xvar, self.yvar, self.zvar] self.M = CP.PropsSI("M", self.fluid) # Linear interpolation between tmin and tmax with NUM_POINTS number of points, delta = tmax-min/NUM_POINTS if self.xvar in ["P", "T"]: xspace = np.linspace( CP.PropsSI(self.xvar + "MIN", self.fluid) + 0.1, CP.PropsSI(self.xvar + "MAX", self.fluid) - 0.1, self.numPoints[0] + 1) elif self.xvar in ["D"] and self.fluid.lower() == "water": xspace = np.linspace(0.01, 1200.01, self.numPoints[0] + 1) elif self.xvar in ["U"]: xspace = np.linspace(9000.0, 6000000.0, self.numPoints[0] + 1) # Linear interpolation between pmin and pmax with NUM_POINTS number of # points, delta = max-min/NUM_POINTS if self.yvar in ["P", "T"]: yspace = np.linspace( CP.PropsSI(self.yvar + "MIN", self.fluid) + 0.1, CP.PropsSI(self.yvar + "MAX", self.fluid) - 0.1, self.numPoints[1] + 1) elif self.yvar in ["D"] and self.fluid.lower() == "water": yspace = np.linspace(0.01, 1200.01, self.numPoints[1] + 1) elif self.yvar in ["U"]: yspace = np.linspace(9000.0, 6000000.0, self.numPoints[1] + 1) # Create a empty list for storing data # Then make our data. data = np.empty((0, 3), dtype=float) for x in xspace: for y in yspace: data = np.append(data, [[ x, y, CP.PropsSI(self.zvar, self.xvar, x, self.yvar, y, self.fluid) ]], axis=0) # Create Pandas Frame of Data self.data = pd.DataFrame(data, columns=self.vars) self.make_units() self.clean() self.make_meta()
[docs] def make_units(self) -> None: """(Re)make the units list""" self.units = {var: get(str(var), UNITS, "UnknownVar") for var in self.data.columns.tolist()}
[docs] def make_meta(self) -> None: """ (Re)make the metadata object """ self.make_units() self.meta = pyr.pmap({ "date": str(arrow.now("US/Central").format("YYYY-MM-DD @ HH:mm:ss")), "fluid": self.fluid, "xvar": self.xvar, "yvar": self.yvar, "zvar": self.zvar, "vars": list(self.vars), "numPoints": self.numPoints, "colorMap": self.colorMap, "units": self.units })
[docs] def refresh(self) -> None: """ Refreshes the object, remakes meta, cleans data, remakes units. """ self.vars = list(set(self.data.columns.tolist())) self.make_units() self.clean() self.make_meta()
[docs] def add_column(self, variables: Union[List[Text], Text]) -> None: """ Adds a column to the dataframe Paramaters: variable (Union[List[Text],Text]): What variable(s) to add """ if type(variables) is not list: variables = [variables] for var in variables: try: assert var not in self.vars except AssertionError: raise ValueError( "Cannot add column {0}: already in frame".format(var)) self.vars += variables newcols = {var: lambda state: CP.PropsSI(var, self.xvar, state[self.xvar], self.yvar, state[self.yvar], self.fluid) for var in variables} buffer = {key: [] for key in newcols} for index, row in self.data.iterrows(): buffer = itemmap(lambda tup: ( tup[0], tup[1] + [apply(get(tup[0], newcols), row)]), buffer) for key in newcols: self.data[key] = pd.Series(buffer[key], index=self.data.index) self.make_units() self.make_meta()
[docs] def clean(self) -> None: """Re-cleans data""" if "P" in self.vars: self.data = self.data[self.data["P"] >= CP.PropsSI("PMIN", self.fluid) + 1.0] if "S" in self.vars: self.data = self.data[self.data["S"] > 0.0] if "T" in self.vars: self.data = self.data[self.data["T"] >= (CP.PropsSI("TMIN", self.fluid) + 1.0)] if "U" in self.vars: self.data = self.data[self.data["U"] >= 0.0]
[docs] def write_data(self, path: str="./data/", filename: str="", mode: str="default") -> None: """ Does what it says on the tin. Makes a CSV and JSON files and saves them to path given. Parameters: path (str): path where file should be saved filename (str): what to name the file mode (str): How to name the file: - **default**: ``FluidName_X-xpoints_Y-ypoints_Z``; - **custom**: fully custom name; - **dual**: default + custom. """ try: assert mode in {"dual", "custom", "default"} except AssertionError: raise ValueError( "Mode must be one of \"dual\", \"custom\", or \"default\". {0} provided".format(mode)) if filename == "" and mode in {"dual", "custom"}: raise TypeError( "When supplying {0} mode, filename is required; None given".format(mode)) if mode != "custom": default_string = self.fluid + "_" + "_".join([str(varname) + "-" + str(point) for ( varname, point) in zip([self.xvar, self.yvar], self.numPoints)] + [self.zvar]) if mode == "custom": middle_string = filename elif mode == "default": middle_string = default_string elif mode == "dual": middle_string = default_string + "_" + filename self.make_meta() self.data.to_csv(path + middle_string + ".csv", mode="w+", index=False) with open(path + middle_string + ".json", mode="w+", encoding="utf-8") as f: json.dump(dict(self.meta), f)
[docs] def copy(self) -> T: """ Returns a copy of itself """ return deepcopy(self)
[docs]class CSVFluid: """ A class that will help work with ThermoFluid data contained in CSV/JSON files. Parameters: pathToFile (str): The name of the files you want to use, NOT the path. Must have both a json and a CSV file for this purpose. Attributes: data (pandas.DataFrame): The data as read by pandas for the given fluid columns (List[Text]): What variable is on the y-axis zvar (str): What variable is on the z-axis, also, what variable is generated by CoolProp colorMap (str): What ColorMap Matplotlib uses to make its plot vars (List[Text]): a list of xvar, yvar, zvar fluid (str): What fluid is being modeled units (List[Text]): the units for xvar, yvar, zvar, in that order. numPoints (List[int]): the x-resolution and y-resolution, in that order. M (float): the molar mass of the fluid for the system. meta (pyrsistent._pmap.PMap): a PMap containing metadata related to the current fluid. """ def __init__(self, pathToFile: str): with open(pathToFile + ".json", mode="r+") as jf: self.meta = pyr.pmap(json.loads(jf.read())) with open(pathToFile + ".csv", mode="r+") as cf: self.data = pd.read_csv(cf) self.colorMap = self.meta["colorMap"] self.xvar = self.meta["xvar"] self.yvar = self.meta["yvar"] self.zvar = self.meta["zvar"] self.vars = list(self.meta["vars"]) self.fluid = self.meta["fluid"] self.colorMap = self.meta["colorMap"] self.numPoints = self.meta["numPoints"] self.units = self.meta["units"]
[docs] def make_units(self) -> None: """(Re)make the units list""" self.units = {var: get(str(var), UNITS, "UnknownVar") for var in self.data.columns.tolist()}
[docs] def make_meta(self) -> None: """ (Re)make the metadata object """ self.make_units() self.meta = pyr.pmap({ "date": str(arrow.now("US/Central").format("YYYY-MM-DD @ HH:mm:ss")), "fluid": self.fluid, "xvar": self.xvar, "yvar": self.yvar, "zvar": self.zvar, "vars": list(self.vars), "numPoints": self.numPoints, "colorMap": self.colorMap, "units": self.units})
[docs] def add_column(self, variables: Union[List[Text], Text]) -> None: """ Adds a column to the dataframe Paramaters: variable (Union[List[Text],Text]): What variable(s) to add """ if type(variables) is not list: variables = [variables] for var in variables: try: assert var not in self.vars except AssertionError: raise ValueError( "Cannot add column {0}: already in frame".format(var)) self.vars += variables newcols = {var: lambda state: CP.PropsSI( var, self.xvar, state[self.xvar], self.yvar, state[self.yvar], self.fluid) for var in variables} buffer = {key: [] for key in newcols} for index, row in self.data.iterrows(): buffer = itemmap(lambda tup: ( tup[0], tup[1] + [apply(get(tup[0], newcols), row)]), buffer) for key in newcols: self.data[key] = pd.Series(buffer[key], index=self.data.index) self.make_units() self.make_meta()
[docs] def refresh(self) -> None: """ Refreshes the object, remakes meta, cleans data, remakes units. """ self.vars = set(self.data.columns.tolist()) self.make_units() self.clean() self.make_meta()
[docs] def changeOrder(self, order: List[Text]) -> None: """ Changes order of the columns: Parameters: order (List[Text]): is a permutation of length 3 on vars. """ try: assert set(order).issubset(set(self.vars)) except AssertionError: raise ValueError("All entries order MUST be in vars") try: assert order in permutations(self.vars, 3) except AssertionError: raise ValueError( "Order of columns must be a permutation of columns of data") self.data = self.data[list( order) + [var for var in set(self.vars) if var not in set(order)]] self.xvar = order[0] self.yvar = order[1] self.zvar = order[2]
[docs] def copy(self) -> C: """ Returns a copy of itself """ return deepcopy(self)
[docs] def write_data(self, path: str="./data/", filename: str="", mode: str="default") -> None: """ Does what it says on the tin. Makes a CSV and JSON files and saves them to path given. Parameters: path (str): path where file should be saved filename (str): what to name the file mode (str): How to name the file: - **default**: ``FluidName_X-xpoints_Y-ypoints_Z``; - **custom**: fully custom name; - **dual**: default + custom. """ try: assert mode in {"dual", "custom", "default"} except AssertionError: raise ValueError( "Mode must be one of \"dual\", \"custom\", or \"default\". {0} provided".format(mode)) if filename == "" and mode in {"dual", "custom"}: raise TypeError( "When supplying {0} mode, filename is required; None given".format(mode)) if mode != "custom": default_string = self.fluid + "_" + "_".join([str(varname) + "-" + str(point) for ( varname, point) in zip([self.xvar, self.yvar], self.numPoints)] + [self.zvar]) if mode == "custom": middle_string = filename elif mode == "default": middle_string = default_string elif mode == "dual": middle_string = default_string + "_" + filename self.make_meta() self.data.to_csv(path + middle_string + ".csv", mode="w+", index=False) with open(path + middle_string + ".json", mode="w+", encoding="utf-8") as f: json.dump(dict(self.meta), f)
[docs]def fluid_plot(fluid: Union[CSVFluid, ThermoFluid], xvar: Text="", yvar: Text="", zvar: Text="", coloring: Text="") -> None: """ Does what it says on the tin. Makes a 3D Scatter Plot of the dataframe. Parameters: fluid (Union[CSVFluid, ThermoFluid]): Which fluid object to make a plot for. xvar (Text): What variable to put on the x-axis yvar (Text): What variable to put on the y-axis zvar (Text): What variable to put on the z-axis coloring (Text): What variable to color by. Default is zvar. """ if not coloring: coloring = fluid.zvar for var in [xvar, yvar, zvar, coloring]: if not var: if var == xvar: xvar = fluid.xvar if var == yvar: yvar = fluid.yvar if var == zvar: zvar = fluid.zvar if var == coloring: coloring == fluid.zvar try: assert var in fluid.vars except AssertionError: ValueError("variable {0} is not a valid variable.".format(var)) # Plotting: fig = plt.figure(randint(10**4, 10**5)) # we want 3D plots ax = fig.add_subplot(111, projection="3d") # Plot the data ax.scatter( fluid.data[xvar], fluid.data[yvar], fluid.data[zvar], c=fluid.data[coloring], cmap=fluid.colorMap, edgecolors="none") # Set the Labels ax.set_xlabel("{0} [{1}]".format(xvar, fluid.units[xvar])) ax.set_ylabel("{0} [{1}]".format(yvar, fluid.units[yvar])) ax.set_zlabel("{0} [{1}]".format(zvar, fluid.units[zvar])) ax.set_title("{0} and {1} vs {2} of {3}".format( xvar, yvar, zvar, fluid.fluid)) plt.show(fig)
[docs]def rescale(oldrange: List[Union[float, int]], newrange: List[Union[float, int]]) -> Callable[[Union[float, int]], Union[float, int]]: """ Creates a function that transforms a single variable from oldrange to newrange. Use it with map or Pandas.DataFrame.apply Parameters: oldrange (List[Union[float, int]]): The old range of the data, [min, max] newrange (List[Union[float, int]]): The new range of the data, [min, max] """ def scaler(x: float)->float: """ scales input according to y = {slope}*(x-{old_min})+{new_min} Paramaters: x (float): A value to scales Returns: y (float): the scaled version of x """ return (newrange[1] - newrange[0]) / (oldrange[1] - oldrange[0]) * (x - oldrange[0]) + newrange[0] scaler.__doc__ = scaler.__doc__.format(slope=((newrange[1] - newrange[0]) / ( oldrange[1] - oldrange[0])), old_min=oldrange[0], new_min=newrange[0]) return scaler
[docs]def fluid_contour_plot(fluid: Union[CSVFluid, ThermoFluid], xvar: Text="T", yvar: Text="P", contour: Text="U") -> None: """ Does what it says on the tin. Makes a Contour Plot of the Fluid. Paramaters: fluid (Union[CSVFluid, ThermoFluid]): What fluid object to get data from. xvar (Text): What is our x-variable yvar (Text): What is our y-variable? contour (Text): What contour to plot. Must be a variable in fluid.vars """ try: assert contour in set(fluid.vars) except AssertionError: ValueError("contour needs to be a variable of fluid") for var in [xvar, yvar]: try: assert var in set(fluid.vars) except AssertionError: ValueError("Variable not valid.") matplotlib.rcParams["xtick.direction"] = "out" matplotlib.rcParams["ytick.direction"] = "out" fig = plt.figure(randint(1, 10**4)) fignum = fig.number xx = pd.Series(fluid.data[xvar]).tolist() yy = pd.Series(fluid.data[yvar]).tolist() zz = pd.Series(fluid.data[contour]).tolist() xi = np.linspace(min(xx), max(xx), 217) yi = np.linspace(min(yy), max(yy), 217) Z = mlab.griddata(xx, yy, zz, xi, yi, interp='linear') X, Y = np.meshgrid(xi, yi) CS = plt.contour(X, Y, Z, cmap=fluid.colorMap) fmt = {x: "{:.4g}".format( x) + " " + fluid.units[contour] for x in CS.levels} plt.clabel(CS, fontsize=10, inline=1, fmt=fmt) plt.xlabel("{0} [{1}]".format(xvar, fluid.units[xvar])) plt.ylabel("{0} [{1}]".format(yvar, fluid.units[yvar])) plt.title("{0} and {1} vs {2} of {3}".format( xvar, yvar, contour, fluid.fluid)) plt.show(fig)