Jump to content

Thermodynamics/Property tables and charts/CoolProp/Saturated water - Pressure table/Code

"""Reproduce thermodynamic data.

Requirements: Python 3 and CoolProp 8.0.0.
The CSV values are intentionally not rounded or formatted for presentation.
"""

import csv
import sys

import CoolProp
from CoolProp.CoolProp import PhaseSI, PropsSI, set_reference_state

# Ensure unit symbols and property names are portable when output is redirected.
if hasattr(sys.stdout, "reconfigure"):
    sys.stdout.reconfigure(encoding="utf-8", newline="")

REQUIRED_COOLPROP_VERSION = '8.0.0'
FLUID = 'Water'
FLUID_REF = f"HEOS::{FLUID}"
REFERENCE_CONVENTION = 'DEF'
STATE = 'Saturated'
AXIS = 'Pressure'
PRESSURE_AXIS = 'Pressure'
SATURATED = 'Saturated'
PRESSURES_MPA = [0.001,
 0.0015,
 0.002,
 0.0025,
 0.003,
 0.004,
 0.005,
 0.0075,
 0.01,
 0.015,
 0.02,
 0.025,
 0.03,
 0.04,
 0.05,
 0.075,
 0.1,
 0.101325,
 0.125,
 0.15,
 0.17500000000000002,
 0.2,
 0.225,
 0.25,
 0.275,
 0.3,
 0.325,
 0.35000000000000003,
 0.375,
 0.4,
 0.45,
 0.5,
 0.55,
 0.6,
 0.65,
 0.7000000000000001,
 0.75,
 0.8,
 0.85,
 0.9,
 0.9500000000000001,
 1.0,
 1.1,
 1.2,
 1.3,
 1.4000000000000001,
 1.5,
 1.75,
 2.0,
 2.25,
 2.5,
 3.0,
 3.5,
 4.0,
 5.0,
 6.0,
 7.0,
 8.0,
 9.0,
 10.0,
 11.0,
 12.0,
 13.0,
 14.0,
 15.0,
 16.0,
 17.0,
 18.0,
 19.0,
 20.0,
 21.0,
 22.0,
 22.064]
TEMPERATURES_C = []
COLUMN_KEYS = ['p_mpa',
 't_c',
 'v_f',
 'v_g',
 'u_f',
 'u_fg',
 'u_g',
 'h_f',
 'h_fg',
 'h_g',
 's_f',
 's_fg',
 's_g']
COLUMN_UNITS = {'p_mpa': 'kPa',
 't_c': '°C',
 'v_f': 'm³/kg',
 'v_g': 'm³/kg',
 'u_f': 'kJ/kg',
 'u_fg': 'kJ/kg',
 'u_g': 'kJ/kg',
 'h_f': 'kJ/kg',
 'h_fg': 'kJ/kg',
 'h_g': 'kJ/kg',
 's_f': 'kJ/(kg·K)',
 's_fg': 'kJ/(kg·K)',
 's_g': 'kJ/(kg·K)'}
COLUMN_GROUPS = {'p_mpa': 'pressure',
 't_c': 'temperature',
 'v_f': 'specific_volume',
 'v_g': 'specific_volume',
 'u_f': 'internal_energy',
 'u_fg': 'internal_energy',
 'u_g': 'internal_energy',
 'h_f': 'enthalpy',
 'h_fg': 'enthalpy',
 'h_g': 'enthalpy',
 's_f': 'entropy',
 's_fg': 'entropy',
 's_g': 'entropy'}
DEFAULT_COLUMN_UNITS = {'p_mpa': 'MPa',
 't_c': '°C',
 'v_f': 'm³/kg',
 'v_g': 'm³/kg',
 'u_f': 'kJ/kg',
 'u_fg': 'kJ/kg',
 'u_g': 'kJ/kg',
 'h_f': 'kJ/kg',
 'h_fg': 'kJ/kg',
 'h_g': 'kJ/kg',
 's_f': 'kJ/(kg·K)',
 's_fg': 'kJ/(kg·K)',
 's_g': 'kJ/(kg·K)'}
CSV_HEADERS = ['Saturation pressure, p_sat (kPa)',
 'Saturation temperature, T_sat (°C)',
 'Liquid specific volume, v_f (m³/kg)',
 'Vapor specific volume, v_g (m³/kg)',
 'Liquid internal energy, u_f (kJ/kg)',
 'Internal energy of vaporization, u_fg (kJ/kg)',
 'Vapor internal energy, u_g (kJ/kg)',
 'Liquid enthalpy, h_f (kJ/kg)',
 'Enthalpy of vaporization, h_fg (kJ/kg)',
 'Vapor enthalpy, h_g (kJ/kg)',
 'Liquid entropy, s_f (kJ/(kg·K))',
 'Entropy of vaporization, s_fg (kJ/(kg·K))',
 'Vapor entropy, s_g (kJ/(kg·K))']


if CoolProp.__version__ != REQUIRED_COOLPROP_VERSION:
    raise RuntimeError(
        f"CoolProp {REQUIRED_COOLPROP_VERSION} is required; "
        f"found {CoolProp.__version__}."
    )

set_reference_state(FLUID, REFERENCE_CONVENTION)


def convert_value(value, key):
    """Convert a calculated value from the tool's base unit to its selected unit."""
    unit = COLUMN_UNITS[key]
    default = DEFAULT_COLUMN_UNITS[key]
    group = COLUMN_GROUPS[key]
    if unit == default or unit == "—" or not isinstance(value, (int, float)):
        return value
    if group == "pressure":
        base_mpa = value / 10.0 if key == "p_bar" else value / 1000.0 if key == "p_kpa" else value
        return {
            "MPa": base_mpa,
            "kPa": base_mpa * 1000.0,
            "bar": base_mpa * 10.0,
            "Pa": base_mpa * 1_000_000.0,
            "psi": base_mpa * 145.03773773,
        }[unit]
    if group == "temperature":
        return {
            "°C": value,
            "K": value + 273.15,
            "°F": value * 9.0 / 5.0 + 32.0,
            "°R": (value + 273.15) * 9.0 / 5.0,
        }[unit]
    if group == "specific_volume":
        return value * 16.01846337
    if group == "density":
        return value * 0.06242796058
    if group in {"internal_energy", "enthalpy"}:
        return value * (1000.0 if unit == "J/kg" else 0.429922614)
    if group in {"entropy", "heat_capacity"}:
        return value * (1000.0 if unit == "J/(kg·K)" else 0.238845897)
    if group == "acoustic":
        return value * 3.280839895
    if key == "viscosity":
        return value / (1_000_000.0 if unit == "Pa·s" else 1000.0)
    if key == "conductivity":
        return value * 0.577789318
    return value


def normalize_negative_zero(value):
    """Return unsigned zero without rounding any nonzero CSV value."""
    if isinstance(value, float) and value == 0.0:
        return 0.0
    return value


def saturation_values(independent_value):
    input_key = "P" if AXIS == PRESSURE_AXIS else "T"
    input_value = (
        independent_value * 1_000_000.0
        if AXIS == PRESSURE_AXIS
        else independent_value + 273.15
    )
    if AXIS == PRESSURE_AXIS:
        critical_pressure = PropsSI("pcrit", FLUID_REF)
        if 0.0 < input_value - critical_pressure <= 1e-3:
            input_value = critical_pressure
    else:
        critical_temperature = PropsSI("Tcrit", FLUID_REF)
        if 0.0 < input_value - critical_temperature <= 1e-6:
            input_value = critical_temperature
    pressure = PropsSI("P", input_key, input_value, "Q", 0, FLUID_REF)
    temperature = PropsSI("T", input_key, input_value, "Q", 0, FLUID_REF)

    def prop(output, quality, scale=1.0):
        return PropsSI(output, input_key, input_value, "Q", quality, FLUID_REF) / scale

    density_liquid = prop("Dmass", 0)
    density_vapor = prop("Dmass", 1)
    internal_energy_liquid = prop("Umass", 0, 1000.0)
    internal_energy_vapor = prop("Umass", 1, 1000.0)
    enthalpy_liquid = prop("Hmass", 0, 1000.0)
    enthalpy_vapor = prop("Hmass", 1, 1000.0)
    entropy_liquid = prop("Smass", 0, 1000.0)
    entropy_vapor = prop("Smass", 1, 1000.0)
    return {
        "p_mpa": pressure / 1_000_000.0,
        "p_kpa": pressure / 1_000.0,
        "p_bar": pressure / 100_000.0,
        "t_c": temperature - 273.15,
        "v_f": 1.0 / density_liquid,
        "v_g": 1.0 / density_vapor,
        "rho_f": density_liquid,
        "rho_g": density_vapor,
        "u_f": internal_energy_liquid,
        "u_g": internal_energy_vapor,
        "u_fg": internal_energy_vapor - internal_energy_liquid,
        "h_f": enthalpy_liquid,
        "h_g": enthalpy_vapor,
        "h_fg": enthalpy_vapor - enthalpy_liquid,
        "s_f": entropy_liquid,
        "s_g": entropy_vapor,
        "s_fg": entropy_vapor - entropy_liquid,
        "cp_f": prop("Cpmass", 0, 1000.0),
        "cp_g": prop("Cpmass", 1, 1000.0),
    }


def single_phase_values(pressure_mpa, temperature_c):
    pressure = pressure_mpa * 1_000_000.0
    temperature = temperature_c + 273.15
    phase = PhaseSI("P", pressure, "T", temperature, FLUID_REF)

    def prop(output, scale=1.0):
        return PropsSI(output, "P", pressure, "T", temperature, FLUID_REF) / scale

    density = prop("Dmass")
    return {
        "p_mpa": pressure_mpa,
        "p_kpa": pressure_mpa * 1000.0,
        "p_bar": pressure_mpa * 10.0,
        "t_c": temperature_c,
        "phase": phase,
        "v": 1.0 / density,
        "rho": density,
        "u": prop("Umass", 1000.0),
        "h": prop("Hmass", 1000.0),
        "s": prop("Smass", 1000.0),
        "cp": prop("Cpmass", 1000.0),
        "cv": prop("Cvmass", 1000.0),
        "speed_sound": prop("speed_of_sound"),
        "viscosity": prop("viscosity") * 1_000_000.0,
        "conductivity": prop("conductivity"),
        "prandtl": prop("Prandtl"),
    }


def requested_phase(phase):
    return (
        (STATE == "Subcooled liquid" and phase == "liquid")
        or (STATE == "Superheated vapor" and phase == "gas")
    )


def rows():
    if STATE == SATURATED:
        independent_values = PRESSURES_MPA if AXIS == PRESSURE_AXIS else TEMPERATURES_C
        for independent_value in independent_values:
            yield saturation_values(independent_value)
        return

    for pressure_mpa in PRESSURES_MPA:
        for temperature_c in TEMPERATURES_C:
            try:
                values = single_phase_values(pressure_mpa, temperature_c)
            except ValueError:
                continue
            if requested_phase(values["phase"]):
                yield values


writer = csv.writer(sys.stdout, lineterminator="\n")
writer.writerow(CSV_HEADERS)
for values in rows():
    writer.writerow([
        normalize_negative_zero(convert_value(values[key], key))
        for key in COLUMN_KEYS
    ])