EnthalpyConverter.cc (13494B)
1 // Copyright (C) 2009-2017 Andreas Aschwanden, Ed Bueler and Constantine Khroulev 2 // 3 // This file is part of PISM. 4 // 5 // PISM is free software; you can redistribute it and/or modify it under the 6 // terms of the GNU General Public License as published by the Free Software 7 // Foundation; either version 3 of the License, or (at your option) any later 8 // version. 9 // 10 // PISM is distributed in the hope that it will be useful, but WITHOUT ANY 11 // WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 12 // FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 13 // details. 14 // 15 // You should have received a copy of the GNU General Public License 16 // along with PISM; if not, write to the Free Software 17 // Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 18 19 #include "pism/util/pism_utilities.hh" 20 #include "EnthalpyConverter.hh" 21 #include "pism/util/ConfigInterface.hh" 22 23 #include "pism/util/error_handling.hh" 24 25 namespace pism { 26 27 /*! @class EnthalpyConverter 28 29 Maps from @f$ (H,p) @f$ to @f$ (T,\omega,p) @f$ and back. 30 31 Requirements: 32 33 1. A converter has to implement an invertible map @f$ (H,p) \to (T, 34 \omega, p) @f$ *and* its inverse. Both the forward map and the 35 inverse need to be defined for all permissible values of @f$ 36 (H,p) @f$ and @f$ (T, \omega, p) @f$, respectively. 37 38 2. A converter has to be consistent with laws and parameterizations 39 used elsewhere in the model. This includes models coupled to 40 PISM. 41 42 3. For a fixed volume of liquid (or solid) water and given two 43 energy states @f$ (H_1, p_1) @f$ and @f$ (H_2, p_2) @f$ , let @f$ 44 \Delta U_H @f$ be the difference in internal energy of this 45 volume between the two states *computed using enthalpy*. We require 46 that @f$ \Delta U_T = \Delta U_H @f$ , where @f$ \Delta U_T @f$ 47 is the difference in internal energy *computed using corresponding* 48 @f$ (T_1, \omega_1, p_1) @f$ *and* @f$ (T_2, \omega_2, p_2) @f$. 49 50 4. We assume that ice and water are incompressible, so a change in 51 pressure does no work, and @f$ \Diff{H}{p} = 0 @f$. In addition 52 to this, for cold ice and liquid water @f$ \Diff{T}{p} = 0 @f$. 53 */ 54 55 EnthalpyConverter::EnthalpyConverter(const Config &config) { 56 m_p_air = config.get_number("surface.pressure"); // Pa 57 m_g = config.get_number("constants.standard_gravity"); // m s-2 58 m_beta = config.get_number("constants.ice.beta_Clausius_Clapeyron"); // K Pa-1 59 m_rho_i = config.get_number("constants.ice.density"); // kg m-3 60 m_c_i = config.get_number("constants.ice.specific_heat_capacity"); // J kg-1 K-1 61 m_c_w = config.get_number("constants.fresh_water.specific_heat_capacity"); // J kg-1 K-1 62 m_L = config.get_number("constants.fresh_water.latent_heat_of_fusion"); // J kg-1 63 m_T_melting = config.get_number("constants.fresh_water.melting_point_temperature"); // K 64 m_T_tolerance = config.get_number("enthalpy_converter.relaxed_is_temperate_tolerance"); // K 65 m_T_0 = config.get_number("enthalpy_converter.T_reference"); // K 66 67 m_do_cold_ice_methods = config.get_flag("energy.temperature_based"); 68 } 69 70 EnthalpyConverter::~EnthalpyConverter() { 71 // empty 72 } 73 74 //! Return `true` if ice at `(E, P)` is temperate. 75 //! Determines if E >= E_s(p), that is, if the ice is at the pressure-melting point. 76 bool EnthalpyConverter::is_temperate(double E, double P) const { 77 if (m_do_cold_ice_methods) { 78 return is_temperate_relaxed(E, P); 79 } else { 80 return (E >= enthalpy_cts(P)); 81 } 82 } 83 84 //! A relaxed version of `is_temperate()`. 85 /*! Returns `true` if the pressure melting temperature corresponding to `(E, P)` is within 86 `enthalpy_converter.relaxed_is_temperate_tolerance` from `fresh_water.melting_point_temperature`. 87 */ 88 bool EnthalpyConverter::is_temperate_relaxed(double E, double P) const { 89 return (pressure_adjusted_temperature(E, P) >= m_T_melting - m_T_tolerance); 90 } 91 92 93 void EnthalpyConverter::validate_T_omega_P(double T, double omega, double P) const { 94 #if (Pism_DEBUG==1) 95 const double T_melting = melting_temperature(P); 96 if (T <= 0.0) { 97 throw RuntimeError::formatted(PISM_ERROR_LOCATION, "T = %f <= 0 is not a valid absolute temperature",T); 98 } 99 if ((omega < 0.0 - 1.0e-6) || (1.0 + 1.0e-6 < omega)) { 100 throw RuntimeError::formatted(PISM_ERROR_LOCATION, "water fraction omega=%f not in range [0,1]",omega); 101 } 102 if (T > T_melting + 1.0e-6) { 103 throw RuntimeError::formatted(PISM_ERROR_LOCATION, "T=%f exceeds T_melting=%f; not allowed",T,T_melting); 104 } 105 if ((T < T_melting - 1.0e-6) && (omega > 0.0 + 1.0e-6)) { 106 throw RuntimeError::formatted(PISM_ERROR_LOCATION, "T < T_melting AND omega > 0 is contradictory;" 107 " got T=%f, T_melting=%f, omega=%f", 108 T, T_melting, omega); 109 } 110 #else 111 (void) T; 112 (void) omega; 113 (void) P; 114 #endif 115 } 116 117 void EnthalpyConverter::validate_E_P(double E, double P) const { 118 #if (Pism_DEBUG==1) 119 if (E >= enthalpy_liquid(P)) { 120 throw RuntimeError::formatted(PISM_ERROR_LOCATION, "E=%f J/kg at P=%f Pa equals or exceeds that of liquid water (%f J/kg)", 121 E, P, enthalpy_liquid(P)); 122 } 123 #else 124 (void) E; 125 (void) P; 126 #endif 127 } 128 129 130 //! Get pressure in ice from depth below surface using the hydrostatic assumption. 131 /*! If \f$d\f$ is the depth then 132 \f[ p = p_{\text{air}} + \rho_i g d. \f] 133 Frequently \f$d\f$ is computed from the thickess minus a level in the ice, 134 something like `ice_thickness(i, j) - z[k]`. The input depth to this routine is allowed to 135 be negative, representing a position above the surface of the ice. 136 */ 137 double EnthalpyConverter::pressure(double depth) const { 138 if (depth >= 0.0) { 139 return m_p_air + m_rho_i * m_g * depth; 140 } else { 141 return m_p_air; // at or above surface of ice 142 } 143 } 144 145 //! Compute pressure in a column of ice. Does not check validity of `depth`. 146 void EnthalpyConverter::pressure(const std::vector<double> &depth, 147 unsigned int ks, 148 std::vector<double> &result) const { 149 for (unsigned int k = 0; k <= ks; ++k) { 150 result[k] = m_p_air + m_rho_i * m_g * depth[k]; 151 } 152 } 153 154 //! Get melting temperature from pressure p. 155 /*! 156 \f[ T_m(p) = T_{melting} - \beta p. \f] 157 */ 158 double EnthalpyConverter::melting_temperature(double P) const { 159 return m_T_melting - m_beta * P; 160 } 161 162 163 //! @brief Compute the maximum allowed value of ice enthalpy 164 //! (corresponds to @f$ \omega = 1 @f$). 165 double EnthalpyConverter::enthalpy_liquid(double P) const { 166 return enthalpy_cts(P) + L(melting_temperature(P)); 167 } 168 169 170 //! Get absolute (not pressure-adjusted) ice temperature (K) from enthalpy and pressure. 171 /*! From \ref AschwandenBuelerKhroulevBlatter, 172 \f[ T= T(E,p) = \begin{cases} 173 c_i^{-1} E + T_0, & E < E_s(p), \\ 174 T_m(p), & E_s(p) \le E < E_l(p). 175 \end{cases} \f] 176 177 We do not allow liquid water (%i.e. water fraction \f$\omega=1.0\f$) so we 178 throw an exception if \f$E \ge E_l(p)\f$. 179 */ 180 double EnthalpyConverter::temperature(double E, double P) const { 181 validate_E_P(E, P); 182 183 if (E < enthalpy_cts(P)) { 184 return temperature_cold(E); 185 } else { 186 return melting_temperature(P); 187 } 188 } 189 190 191 //! Get pressure-adjusted ice temperature, in Kelvin, from enthalpy and pressure. 192 /*! 193 The pressure-adjusted temperature is: 194 \f[ T_{pa}(E,p) = T(E,p) - T_m(p) + T_{melting}. \f] 195 */ 196 double EnthalpyConverter::pressure_adjusted_temperature(double E, double P) const { 197 return temperature(E, P) - melting_temperature(P) + m_T_melting; 198 } 199 200 201 //! Get liquid water fraction from enthalpy and pressure. 202 /*! 203 From [@ref AschwandenBuelerKhroulevBlatter], 204 @f[ 205 \omega(E,p) = 206 \begin{cases} 207 0.0, & E \le E_s(p), \\ 208 (E-E_s(p)) / L, & E_s(p) < E < E_l(p). 209 \end{cases} 210 @f] 211 212 We do not allow liquid water (i.e. water fraction @f$ \omega=1.0 @f$). 213 */ 214 double EnthalpyConverter::water_fraction(double E, double P) const { 215 validate_E_P(E, P); 216 217 double E_s = enthalpy_cts(P); 218 if (E <= E_s) { 219 return 0.0; 220 } else { 221 return (E - E_s) / L(melting_temperature(P)); 222 } 223 } 224 225 226 //! Compute enthalpy from absolute temperature, liquid water fraction, and pressure. 227 /*! This is an inverse function to the functions \f$T(E,p)\f$ and 228 \f$\omega(E,p)\f$ [\ref AschwandenBuelerKhroulevBlatter]. It returns: 229 \f[E(T,\omega,p) = 230 \begin{cases} 231 c_i (T - T_0), & T < T_m(p) \quad\text{and}\quad \omega = 0, \\ 232 E_s(p) + \omega L, & T = T_m(p) \quad\text{and}\quad \omega \ge 0. 233 \end{cases} \f] 234 Certain cases are not allowed and throw exceptions: 235 - \f$T<=0\f$ (error code 1) 236 - \f$\omega < 0\f$ or \f$\omega > 1\f$ (error code 2) 237 - \f$T>T_m(p)\f$ (error code 3) 238 - \f$T<T_m(p)\f$ and \f$\omega > 0\f$ (error code 4) 239 These inequalities may be violated in the sixth digit or so, however. 240 */ 241 double EnthalpyConverter::enthalpy(double T, double omega, double P) const { 242 validate_T_omega_P(T, omega, P); 243 244 const double T_melting = melting_temperature(P); 245 246 if (T < T_melting) { 247 return enthalpy_cold(T); 248 } else { 249 return enthalpy_cts(P) + omega * L(T_melting); 250 } 251 } 252 253 254 //! Compute enthalpy more permissively than enthalpy(). 255 /*! Computes enthalpy from absolute temperature, liquid water fraction, and 256 pressure. Use this form of enthalpy() when outside sources (e.g. information 257 from a coupler) might generate a temperature above the pressure melting point or 258 generate cold ice with a positive water fraction. 259 260 Treats temperatures above pressure-melting point as \e at the pressure-melting 261 point. Interprets contradictory case of \f$T < T_m(p)\f$ and \f$\omega > 0\f$ 262 as cold ice, ignoring the water fraction (\f$\omega\f$) value. 263 264 Calls enthalpy(), which validates its inputs. 265 266 Computes: 267 @f[ 268 E = 269 \begin{cases} 270 E(T,0.0,p), & T < T_m(p) \quad \text{and} \quad \omega \ge 0, \\ 271 E(T_m(p),\omega,p), & T \ge T_m(p) \quad \text{and} \quad \omega \ge 0, 272 \end{cases} 273 @f] 274 but ensures @f$ 0 \le \omega \le 1 @f$ in second case. Calls enthalpy() for 275 @f$ E(T,\omega,p) @f$. 276 */ 277 double EnthalpyConverter::enthalpy_permissive(double T, double omega, double P) const { 278 const double T_m = melting_temperature(P); 279 280 if (T < T_m) { 281 return enthalpy(T, 0.0, P); 282 } else { // T >= T_m(P) replaced with T = T_m(P) 283 return enthalpy(T_m, std::max(0.0, std::min(omega, 1.0)), P); 284 } 285 } 286 287 ColdEnthalpyConverter::ColdEnthalpyConverter(const Config &config) 288 : EnthalpyConverter(config) { 289 // turn on the "cold" enthalpy converter mode 290 m_do_cold_ice_methods = true; 291 // set melting temperature to one million Kelvin so that all ice is cold 292 m_T_melting = 1e6; 293 // disable pressure-dependence of the melting temperature by setting Clausius-Clapeyron beta to 294 // zero 295 m_beta = 0.0; 296 } 297 298 ColdEnthalpyConverter::~ColdEnthalpyConverter() { 299 // empty 300 } 301 302 //! Latent heat of fusion of water as a function of pressure melting 303 //! temperature. 304 /*! 305 306 Following a re-interpretation of [@ref 307 AschwandenBuelerKhroulevBlatter], we require that @f$ \Diff{H}{p} = 308 0 @f$: 309 310 @f[ 311 \Diff{H}{p} = \diff{H_w}{p} + \diff{H_w}{p}\Diff{T}{p} 312 @f] 313 314 We assume that water is incompressible, so @f$ \Diff{T}{p} = 0 @f$ 315 and the second term vanishes. 316 317 As for the first term, equation (5) of [@ref 318 AschwandenBuelerKhroulevBlatter] defines @f$ H_w @f$ as follows: 319 320 @f[ 321 H_w = \int_{T_0}^{T_m(p)} C_i(t) dt + L + \int_{T_m(p)}^T C_w(t)dt 322 @f] 323 324 Using the fundamental theorem of Calculus, we get 325 @f[ 326 \diff{H_w}{p} = (C_i(T_m(p)) - C_w(T_m(p))) \diff{T_m(p)}{p} + \diff{L}{p} 327 @f] 328 329 Assuming that @f$ C_i(T) = c_i @f$ and @f$ C_w(T) = c_w @f$ (i.e. specific heat 330 capacities of ice and water do not depend on temperature) and using 331 the Clausius-Clapeyron relation 332 @f[ 333 T_m(p) = T_m(p_{\text{air}}) - \beta p, 334 @f] 335 336 we get 337 @f{align}{ 338 \Diff{H}{p} &= (c_i - c_w)\diff{T_m(p)}{p} + \diff{L}{p}\\ 339 &= \beta(c_w - c_i) + \diff{L}{p}\\ 340 @f} 341 Requiring @f$ \Diff{H}{p} = 0 @f$ implies 342 @f[ 343 \diff{L}{p} = -\beta(c_w - c_i), 344 @f] 345 and so 346 @f{align}{ 347 L(p) &= -\beta p (c_w - c_i) + C\\ 348 &= (T_m(p) - T_m(p_{\text{air}})) (c_w - c_i) + C. 349 @f} 350 351 Letting @f$ p = p_{\text{air}} @f$ we find @f$ C = L(p_\text{air}) = L_0 @f$, so 352 @f[ 353 L(p) = (T_m(p) - T_m(p_{\text{air}})) (c_w - c_i) + L_0, 354 @f] 355 where @f$ L_0 @f$ is the latent heat of fusion of water at atmospheric 356 pressure. 357 358 Therefore a consistent interpretation of [@ref 359 AschwandenBuelerKhroulevBlatter] requires the temperature-dependent 360 approximation of the latent heat of fusion of water given above. 361 362 Note that this form of @f$ L(p) @f$ also follows from Kirchhoff's 363 law of thermochemistry. 364 */ 365 double EnthalpyConverter::L(double T_pm) const { 366 return m_L + (m_c_w - m_c_i) * (T_pm - 273.15); 367 } 368 369 //! Specific heat capacity of ice. 370 double EnthalpyConverter::c() const { 371 return m_c_i; 372 } 373 374 //! Get enthalpy E_s(p) at cold-temperate transition point from pressure p. 375 /*! Returns 376 \f[ E_s(p) = c_i (T_m(p) - T_0), \f] 377 */ 378 double EnthalpyConverter::enthalpy_cts(double P) const { 379 return m_c_i * (melting_temperature(P) - m_T_0); 380 } 381 382 //! Convert temperature into enthalpy (cold case). 383 double EnthalpyConverter::enthalpy_cold(double T) const { 384 return m_c_i * (T - m_T_0); 385 } 386 387 //! Convert enthalpy into temperature (cold case). 388 double EnthalpyConverter::temperature_cold(double E) const { 389 return (E / m_c_i) + m_T_0; 390 } 391 392 } // end of namespace pism