Source code for aerosandbox.atmosphere.thermodynamics.gas

import aerosandbox.numpy as np
from aerosandbox.tools.string_formatting import eng_string
import copy
from typing import Union

[docs]universal_gas_constant = 8.31432 # J/(mol*K); universal gas constant
[docs]class PerfectGas: """ Provides a class for an ideal, calorically perfect gas. Specifically, this gas: * Has PV = nRT (ideal) * Has constant heat capacities C_V, C_P (independent of temperature and pressure). * Is in thermodynamic equilibrium * Is not chemically reacting * Has internal energy and enthalpy purely as functions of temperature """ def __init__(self, pressure: Union[float, np.ndarray] = 101325, temperature: Union[float, np.ndarray] = 273.15 + 15, specific_heat_constant_pressure: float = 1006, specific_heat_constant_volume: float = 717, molecular_mass: float = 28.9644e-3, effective_collision_diameter: float = 0.365e-9, ): """ Args: pressure: Pressure of the gas, in Pascals temperature: Temperature of the gas, in Kelvin specific_heat_constant_pressure: Specific heat at constant pressure, also known as C_p. In J/kg-K. specific_heat_constant_volume: Specific heat at constant volume, also known as C_v. In J/kg-K. molecular_mass: Molecular mass of the gas, in kg/mol effective_collision_diameter: Effective collision diameter of a molecule, in meters. """ self.pressure = pressure self.temperature = temperature self.specific_heat_constant_pressure = specific_heat_constant_pressure self.specific_heat_constant_volume = specific_heat_constant_volume self.molecular_mass = molecular_mass self.effective_collision_diameter = effective_collision_diameter
[docs] def __repr__(self) -> str: f = lambda s, u: eng_string(s, unit=u, format="%.6g") return f"Gas (P = {f(self.pressure, 'Pa')}, T = {self.temperature:.6g} K, ρ = {self.density:.6g} kg/m^3, Pv^gamma = {self.pressure * self.specific_volume ** self.ratio_of_specific_heats: .6g})"
@property
[docs] def density(self): return self.pressure / (self.temperature * self.specific_gas_constant)
@property
[docs] def speed_of_sound(self): return (self.ratio_of_specific_heats * self.specific_gas_constant * self.temperature) ** 0.5
@property
[docs] def specific_gas_constant(self): return universal_gas_constant / self.molecular_mass
@property
[docs] def ratio_of_specific_heats(self): return self.specific_heat_constant_pressure / self.specific_heat_constant_volume
[docs] def specific_enthalpy_change(self, start_temperature, end_temperature): """ Returns the change in specific enthalpy that would occur from a given temperature change via a thermodynamic process. Args: start_temperature: Starting temperature [K] end_temperature: Ending temperature [K] Returns: The change in specific enthalpy, in J/kg. """ return self.specific_heat_constant_pressure * (end_temperature - start_temperature)
[docs] def specific_internal_energy_change(self, start_temperature, end_temperature): """ Returns the change in specific internal energy that would occur from a given temperature change via a thermodynamic process. Args: start_temperature: Starting temperature [K] end_temperature: Ending temperature [K] Returns: The change in specific internal energy, in J/kg. """ return self.specific_heat_constant_volume * (end_temperature - start_temperature)
@property
[docs] def specific_volume(self): """ Gives the specific volume, often denoted `v`. (Note the lowercase; "V" is often the volume of a specific amount of gas, and this presents a potential point of confusion.) """ return 1 / self.density
@property
[docs] def specific_enthalpy(self): """ Gives the specific enthalpy, often denoted `h`. Enthalpy here is in units of J/kg. """ return self.specific_enthalpy_change(start_temperature=0, end_temperature=self.temperature)
@property
[docs] def specific_internal_energy(self): """ Gives the specific internal energy, often denoted `u`. Internal energy here is in units of J/kg. """ return self.specific_internal_energy_change(start_temperature=0, end_temperature=self.temperature)
[docs] def process(self, process: str = "isentropic", new_pressure: float = None, new_temperature: float = None, new_density: float = None, enthalpy_addition_at_constant_pressure: float = None, enthalpy_addition_at_constant_volume: float = None, polytropic_n: float = None, inplace=False ) -> "PerfectGas": """ Puts this gas under a thermodynamic process. Equations here: https://en.wikipedia.org/wiki/Ideal_gas_law Args: process: Type of process. One of: * "isobaric" * "isochoric" * "isothermal" * "isentropic" * "polytropic" The `process` must be specified. You must specifiy exactly one of the following arguments: * `new_pressure`: the new pressure after the process [Pa]. * `new_temperature`: the new temperature after the process [K] * `new_density`: the new density after the process [kg/m^3] * `enthalpy_addition_at_constant_pressure`: [J/kg] * `enthalpy_addition_at_constant_volume`: [J/kg] polytropic_n: If you specified the process type to be "polytropic", you must provide the polytropic index `n` to be used here. (Reminder: PV^n = constant) inplace: Specifies whether to return the result in-place or to allocate a new PerfectGas object in memory for the result. Returns: If `inplace` is False (default), returns a new PerfectGas object that represents the gas after the change. If `inplace` is True, nothing is returned. """ pressure_specified = new_pressure is not None temperature_specified = new_temperature is not None density_specified = new_density is not None enthalpy_at_pressure_specified = enthalpy_addition_at_constant_pressure is not None enthalpy_at_volume_specified = enthalpy_addition_at_constant_volume is not None number_of_conditions_specified = ( pressure_specified + temperature_specified + density_specified + enthalpy_at_pressure_specified + enthalpy_at_volume_specified ) if number_of_conditions_specified != 1: raise ValueError("You must specify exactly one of the following arguments:\n" + "\n".join([ "\t* `new_pressure`", "\t* `new_temperature`", "\t* `new_density`", "\t* `enthalpy_addition_at_constant_pressure`", "\t* `enthalpy_addition_at_constant_volume`", ])) if enthalpy_at_pressure_specified: new_temperature = self.temperature + enthalpy_addition_at_constant_pressure / self.specific_heat_constant_pressure elif enthalpy_at_volume_specified: new_temperature = self.temperature + enthalpy_addition_at_constant_volume / self.specific_heat_constant_volume if pressure_specified: P_ratio = new_pressure / self.pressure elif temperature_specified: T_ratio = new_temperature / self.temperature elif density_specified: V_ratio = 1 / (new_density / self.density) if process == "isobaric": new_pressure = self.pressure if pressure_specified: raise ValueError("Can't specify pressure change for an isobaric process!") elif density_specified: new_temperature = self.temperature * V_ratio elif temperature_specified: pass elif process == "isochoric": if pressure_specified: new_temperature = self.temperature * P_ratio elif density_specified: raise ValueError("Can't specify density change for an isochoric process!") elif temperature_specified: new_pressure = self.pressure * T_ratio elif process == "isothermal": new_temperature = self.temperature if pressure_specified: pass elif density_specified: new_pressure = self.pressure / V_ratio elif temperature_specified: raise ValueError("Can't specify temperature change for an isothermal process!") elif process == "isentropic": gam = self.ratio_of_specific_heats if pressure_specified: new_temperature = self.temperature * P_ratio ** ((gam - 1) / gam) elif density_specified: new_pressure = self.pressure * V_ratio ** -gam new_temperature = self.temperature * V_ratio ** (1 - gam) elif temperature_specified: new_pressure = self.pressure * T_ratio ** (gam / (gam - 1)) elif process == "polytropic": if polytropic_n is None: raise ValueError("If the process is polytropic, then the polytropic index `n` must be specified.") n = polytropic_n if pressure_specified: new_temperature = self.temperature * P_ratio ** ((n - 1) / n) elif density_specified: new_pressure = self.pressure * V_ratio ** -n new_temperature = self.temperature * V_ratio ** (1 - n) elif temperature_specified: new_pressure = self.pressure * T_ratio ** (n / (n - 1)) elif process == "isenthalpic": raise NotImplementedError() else: raise ValueError("Bad value of `process`!") if inplace: self.pressure = new_pressure self.temperature = new_temperature else: return PerfectGas( pressure=new_pressure, temperature=new_temperature, specific_heat_constant_pressure=self.specific_heat_constant_pressure, specific_heat_constant_volume=self.specific_heat_constant_volume, molecular_mass=self.molecular_mass, effective_collision_diameter=self.effective_collision_diameter )
if __name__ == '__main__': ### Carnot
[docs] g = []
g.append(PerfectGas(pressure=100e3, temperature=300)) g.append(g[-1].process("isothermal", new_density=0.5)) g.append(g[-1].process("isentropic", new_density=0.25)) g.append(g[-1].process("isothermal", new_density=0.58)) g.append(g[-1].process("isentropic", new_temperature=300)) for i in range(len(g)): print(f"After Process {i}: {g[i]}")