"""
.. 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 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 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)