PISM, A Parallel Ice Sheet Model  stable v2.1-1-g6902d5502 committed by Ed Bueler on 2023-12-20 08:38:27 -0800
DEBMSimplePointwise.cc
Go to the documentation of this file.
1 // Copyright (C) 2009--2023 PISM Authors
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 <algorithm>
20 #include <cassert>
21 #include <cmath>
22 
23 #include "pism/coupler/surface/DEBMSimplePointwise.hh"
24 #include "pism/util/ConfigInterface.hh"
25 #include "pism/util/Context.hh"
26 #include "pism/util/Time.hh"
27 
28 /*!
29  * This class implements dEBM-simple, the simple diurnal energy balance model described in
30  *
31  * M. Zeitz, R. Reese, J. Beckmann, U. Krebs-Kanzow, and R. Winkelmann, “Impact of the
32  * melt–albedo feedback on the future evolution of the Greenland Ice Sheet with
33  * PISM-dEBM-simple,” The Cryosphere, vol. 15, Art. no. 12, Dec. 2021.
34  *
35  * See also
36  *
37  * U. Krebs-Kanzow, P. Gierz, and G. Lohmann, “Brief communication: An ice surface melt
38  * scheme including the diurnal cycle of solar radiation,” The Cryosphere, vol. 12, Art.
39  * no. 12, Dec. 2018.
40  *
41  * and chapter 2 of
42  *
43  * K. N. Liou, Introduction to Atmospheric Radiation. Elsevier Science & Technology Books, 2002.
44  *
45  */
46 namespace pism {
47 namespace surface {
48 
49 namespace details {
50 // Disable clang-tidy warnings about "magic numbers":
51 // NOLINTBEGIN(readability-magic-numbers)
52 
53 /*!
54  * The integrand in equation 6 of
55  *
56  * R. Calov and R. Greve, “A semi-analytical solution for the positive degree-day model
57  * with stochastic temperature variations,” Journal of Glaciology, vol. 51, Art. no. 172,
58  * 2005.
59  *
60  * @param[in] sigma standard deviation of daily variation of near-surface air temperature (Kelvin)
61  * @param[in] temperature near-surface air temperature in "degrees Kelvin above the melting point"
62  */
63 static double CalovGreveIntegrand(double sigma, double temperature) {
64 
65  if (sigma == 0) {
66  return std::max(temperature, 0.0);
67  }
68 
69  double Z = temperature / (sqrt(2.0) * sigma);
70  return (sigma / sqrt(2.0 * M_PI)) * exp(-Z * Z) + (temperature / 2.0) * erfc(-Z);
71 }
72 
73 /*!
74  * The hour angle (radians) at which the sun reaches the solar angle `phi`
75  *
76  * Implements equation 11 in Krebs-Kanzow et al solved for h_phi.
77  *
78  * Equation 2 in Zeitz et al should be equivalent but misses "acos(...)".
79  *
80  * The return value is in the range [0, pi].
81  *
82  * @param[in] phi angle (radians)
83  * @param[in] latitude latitude (radians)
84  * @param[in] declination solar declination angle (radians)
85  */
86 static double hour_angle(double phi, double latitude, double declination) {
87  double cos_h_phi = ((sin(phi) - sin(latitude) * sin(declination)) /
88  (cos(latitude) * cos(declination)));
89  return acos(pism::clip(cos_h_phi, -1.0, 1.0));
90 }
91 
92 /*!
93  * Solar longitude (radians) at current time in the year.
94  *
95  * @param[in] year_fraction year fraction (between 0 and 1)
96  * @param[in] eccentricity eccentricity of the earth’s orbit (no units)
97  * @param[in] perihelion_longitude perihelion longitude (radians)
98  *
99  * Implements equation A2 in Zeitz et al.
100  */
101 static double solar_longitude(double year_fraction,
102  double eccentricity,
103  double perihelion_longitude) {
104 
105  // Shortcuts to make formulas below easier to read:
106  double E = eccentricity;
107  double E2 = E * E;
108  double E3 = E * E * E;
109  double L_p = perihelion_longitude;
110 
111  // Note: lambda = 0 at March equinox (80th day of the year)
112  const double equinox_day_number = 80.0;
113  double delta_lambda = 2.0 * M_PI * (year_fraction - equinox_day_number / 365.0);
114  double beta = sqrt(1.0 - E2);
115 
116  double lambda_m = (-2.0 * ((E / 2.0 + E3 / 8.0) * (1.0 + beta) * sin(-L_p) -
117  E2 / 4.0 * (1.0 / 2.0 + beta) * sin(-2.0 * L_p) +
118  E3 / 8.0 * (1.0 / 3.0 + beta) * sin(-3.0 * L_p)) +
119  delta_lambda);
120 
121  return (lambda_m +
122  (2.0 * E - E3 / 4.0) * sin(lambda_m - L_p) +
123  (5.0 / 4.0) * E2 * sin(2.0 * (lambda_m - L_p)) +
124  (13.0 / 12.0) * E3 * sin(3.0 * (lambda_m - L_p)));
125 }
126 
127 /*!
128  * The unit-less factor scaling top of atmosphere insolation according to the Earth's
129  * distance from the Sun.
130  *
131  * The returned value is `(d_bar / d)^2`, where `d_bar` is the average distance from the
132  * Earth to the Sun and `d` is the *current* distance at a given time.
133  *
134  * Implements equation 2.2.9 from Liou (2002).
135  *
136  * Liou states: "Note that the factor (a/r)^2 never departs from the unity by more than
137  * 3.5%." (`a/r` in Liou is equivalent to `d_bar/d` here.)
138  */
139 static double distance_factor_present_day(double year_fraction) {
140  // These coefficients come from Table 2.2 in Liou 2002
141  double
142  a0 = 1.000110,
143  a1 = 0.034221,
144  a2 = 0.000719,
145  b0 = 0.,
146  b1 = 0.001280,
147  b2 = 0.000077;
148 
149  double t = 2. * M_PI * year_fraction;
150 
151  return (a0 + b0 +
152  a1 * cos(t) + b1 * sin(t) +
153  a2 * cos(2. * t) + b2 * sin(2. * t));
154 }
155 
156 /*!
157  * The unit-less factor scaling top of atmosphere insolation according to the Earth's
158  * distance from the Sun. This is the "paleo" version used when the trigonometric
159  * expansion (equation 2.2.9 in Liou 2002) is not valid.
160  *
161  * Implements equation A1 in Zeitz et al.
162  *
163  * See also equation 2.2.5 from Liou (2002).
164  */
165 static double distance_factor_paleo(double eccentricity,
166  double perihelion_longitude,
167  double solar_longitude) {
168  double E = eccentricity;
169 
170  if (E == 1.0) {
171  // protect from division by zero
173  "invalid eccentricity value: 1.0");
174  }
175 
176  return pow((1.0 + E * cos(solar_longitude - perihelion_longitude)) / (1.0 - E * E), 2);
177 }
178 
179 /*!
180  * Solar declination (radian)
181  *
182  * Implements equation 2.2.10 from Liou (2002)
183  */
184 static double solar_declination_present_day(double year_fraction) {
185  // These coefficients come from Table 2.2 in Liou 2002
186  double
187  a0 = 0.006918,
188  a1 = -0.399912,
189  a2 = -0.006758,
190  a3 = -0.002697,
191  b0 = 0.,
192  b1 = 0.070257,
193  b2 = 0.000907,
194  b3 = 0.000148;
195 
196  double t = 2. * M_PI * year_fraction;
197 
198  return (a0 + b0 +
199  a1 * cos(t) + b1 * sin(t) +
200  a2 * cos(2. * t) + b2 * sin(2. * t) +
201  a3 * cos(3. * t) + b3 * sin(3. * t));
202 }
203 
204 /*!
205  * Solar declination (radians). This is the "paleo" version used when
206  * the trigonometric expansion (equation 2.2.10 in Liou 2002) is not valid.
207  *
208  * The return value is in the range [-pi/2, pi/2].
209  *
210  * Implements equation in the text just above equation A1 in Zeitz et al.
211  *
212  * See also equation 2.2.4 of Liou (2002).
213  */
214 static double solar_declination_paleo(double obliquity,
215  double solar_longitude) {
216  return asin(sin(obliquity * sin(solar_longitude)));
217 }
218 
219 /*!
220  * Average top of atmosphere insolation (rate) during the daily melt period, in W/m^2.
221  *
222  * This should be equation 5 in Zeitz et al or equation 12 in Krebs-Kanzow et al, but both
223  * of these miss a factor of Delta_t (day length in seconds) in the numerator.
224  *
225  * To confirm this, see the derivation of equation 2.2.21 in Liou and note that
226  *
227  * omega = 2 * pi (radian/day)
228  *
229  * or
230  *
231  * omega = (2 * pi / 86400) (radian/second).
232  *
233  * The correct equation should say
234  *
235  * S_Phi = A * B^2 * (h_phi * sin(phi) * sin(delta) + cos(phi) * cos(delta) * sin(h_phi)),
236  *
237  * where
238  *
239  * A = (S0 * Delta_t) / (Delta_t_Phi * pi),
240  * B = d_bar / d.
241  *
242  * Note that we do not know Delta_t_phi but we can use equation 2 in Zeitz et al (or
243  * equation 11 in Krebs-Kanzow et al) to get
244  *
245  * Delta_t_phi = h_phi * Delta_t / pi.
246  *
247  * This gives
248  *
249  * S_Phi = C * B^2 * (h_phi * sin(phi) * sin(delta) + cos(phi) * cos(delta) * sin(h_phi))
250  *
251  * with
252  *
253  * C = (S0 * Delta_t * pi) / (h_phi * Delta_t * pi)
254  *
255  * or
256  *
257  * C = S0 / h_phi.
258  *
259  * @param[in] solar constant solar constant, W/m^2
260  * @param[in] distance_factor square of the ratio of the mean sun-earth distance to the current sun-earth distance (no units)
261  * @param[in] hour_angle hour angle (radians) when the sun reaches the critical angle Phi
262  * @param[in] latitude latitude (radians)
263  * @param[in] declination declination (radians)
264  *
265  */
266 static double insolation(double solar_constant,
267  double distance_factor,
268  double hour_angle,
269  double latitude,
270  double declination) {
271  if (hour_angle == 0) {
272  return 0.0;
273  }
274 
275  return ((solar_constant / hour_angle) * distance_factor *
276  (hour_angle * sin(latitude) * sin(declination) +
277  cos(latitude) * cos(declination) * sin(hour_angle)));
278 }
279 
280 // NOLINTEND(readability-magic-numbers)
281 } // end of namespace details
282 
284  snow_depth = 0.0;
285  melt = 0.0;
286  runoff = 0.0;
287  smb = 0.0;
288 }
289 
291  temperature_melt = 0.0;
292  insolation_melt = 0.0;
293  offset_melt = 0.0;
294  total_melt = 0.0;
295 }
296 
298 
299  const Config &config = *ctx.config();
300 
301  m_time = ctx.time();
302 
303  m_L = config.get_number("constants.fresh_water.latent_heat_of_fusion");
304  m_albedo_min = config.get_number("surface.debm_simple.albedo_min");
305  m_albedo_ocean = config.get_number("surface.debm_simple.albedo_ocean");
306  m_albedo_slope = config.get_number("surface.debm_simple.albedo_slope");
307  m_albedo_max = config.get_number("surface.debm_simple.albedo_max");
308  m_melt_threshold_temp = config.get_number("surface.debm_simple.melting_threshold_temp");
309  m_melt_c1 = config.get_number("surface.debm_simple.c1");
310  m_melt_c2 = config.get_number("surface.debm_simple.c2");
311  m_constant_eccentricity = config.get_number("surface.debm_simple.paleo.eccentricity");
312  m_constant_obliquity = config.get_number("surface.debm_simple.paleo.obliquity", "radian");
313  m_constant_perihelion_longitude = config.get_number("surface.debm_simple.paleo.perihelion_longitude", "radian");
314  m_paleo = config.get_flag("surface.debm_simple.paleo.enabled");
315  m_phi = config.get_number("surface.debm_simple.phi", "radian");
316  m_positive_threshold_temperature = config.get_number("surface.debm_simple.positive_threshold_temp");
317  m_refreeze_fraction = config.get_number("surface.debm_simple.refreeze");
318  m_refreeze_ice_melt = config.get_flag("surface.debm_simple.refreeze_ice_melt");
319  m_solar_constant = config.get_number("surface.debm_simple.solar_constant");
320  m_transmissivity_intercept = config.get_number("surface.debm_simple.tau_a_intercept");
321  m_transmissivity_slope = config.get_number("surface.debm_simple.tau_a_slope");
322 
323  m_ice_density = config.get_number("constants.ice.density");
324  m_water_density = config.get_number("constants.fresh_water.density");
325 
326  assert(m_albedo_slope < 0.0);
327  assert(m_ice_density > 0.0);
328 
329  std::string paleo_file = config.get_string("surface.debm_simple.paleo.file");
330 
331  if (not paleo_file.empty()) {
332  m_use_paleo_file = true;
333 
334  m_eccentricity.reset(new ScalarForcing(ctx, "surface.debm_simple.paleo", "eccentricity", "1",
335  "1", "eccentricity of the earth"));
336 
337  m_obliquity.reset(new ScalarForcing(ctx, "surface.debm_simple.paleo", "obliquity", "radian",
338  "degree", "obliquity of the earth"));
339 
341  new ScalarForcing(ctx, "surface.debm_simple.paleo", "perihelion_longitude", "radian",
342  "degree", "longitude of the perihelion relative to the vernal equinox"));
343  } else {
344  m_use_paleo_file = false;
345  }
346 }
347 
348 /*! Albedo parameterized as a function of the melt rate
349  *
350  * See equation 7 in Zeitz et al.
351  *
352  * @param[in] melt_rate melt rate (meters (liquid water equivalent) per second)
353  * @param[in] cell_type cell type mask (used to exclude ice free areas)
354  */
355 double DEBMSimplePointwise::albedo(double melt_rate, MaskValue cell_type) const {
356  if (cell_type == MASK_ICE_FREE_OCEAN) {
357  return m_albedo_ocean;
358  }
359 
360  assert(melt_rate >= 0.0);
361 
362  return std::max(m_albedo_max + m_albedo_slope * melt_rate * m_ice_density, //
363  m_albedo_min);
364 }
365 
366 /*! Atmosphere transmissivity (no units; acts as a scaling factor)
367  *
368  * See appendix A2 in Zeitz et al 2021.
369  *
370  * @param[in] elevation elevation above the geoid (meters)
371  */
372 double DEBMSimplePointwise::atmosphere_transmissivity(double elevation) const {
374 }
375 
377  double declination = 0.0;
378  double distance_factor = 0.0;
379 
380  double year_fraction = m_time->year_fraction(time);
381  if (m_paleo) {
382  // eccentricity and perihelion longitude are needed by both declination and distance_factor
383  double eccentricity = this->eccentricity(time);
384  double perihelion_longitude = this->perihelion_longitude(time);
385 
386  double solar_longitude = details::solar_longitude(year_fraction,
387  eccentricity,
389 
391 
395  } else {
396  declination = details::solar_declination_present_day(year_fraction);
397  distance_factor = details::distance_factor_present_day(year_fraction);
398  }
399 
400  return {declination, distance_factor};
401 }
402 
403 
404 /*!
405  * Compute top of atmosphere insolation to report as a diagnostic quantity.
406  *
407  * Do not use this in the model itself: doing so will make it slower because that way we'd
408  * end up computing hour_angle more than once.
409  */
410 double DEBMSimplePointwise::insolation(double declination,
411  double distance_factor,
412  double latitude_degrees) const {
413  const double degrees_to_radians = M_PI / 180.0;
414  double latitude_rad = latitude_degrees * degrees_to_radians;
415 
416  double h_phi = details::hour_angle(m_phi, latitude_rad, declination);
417 
419  distance_factor,
420  h_phi,
421  latitude_rad,
422  declination);
423 }
424 
425 /* Melt amount (in m water equivalent) and its components over the time step `dt`
426  *
427  * Implements equation (1) in Zeitz et al.
428  *
429  * See also the equation (6) in Krebs-Kanzow et al.
430  *
431  * @param[in] time current time (seconds)
432  * @param[in] dt time step length (seconds)
433  * @param[in] T_std_deviation standard deviation of the near-surface air temperature (Kelvin)
434  * @param[in] T near-surface air temperature (Kelvin)
435  * @param[in] surface_elevation surface elevation (meters)
436  * @param[in] latitude latitude (degrees north)
437  * @param[in] albedo current albedo (fraction)
438  */
440  double distance_factor,
441  double dt,
442  double T_std_deviation,
443  double T,
444  double surface_elevation,
445  double latitude,
446  double albedo) const {
447  assert(dt > 0.0);
448 
449  const double degrees_to_radians = M_PI / 180.0;
450  double latitude_rad = latitude * degrees_to_radians;
451 
452  double transmissivity = atmosphere_transmissivity(surface_elevation);
453  double h_phi = details::hour_angle(m_phi, latitude_rad, declination);
455  distance_factor,
456  h_phi,
457  latitude_rad,
458  declination);
459 
460  double Teff = details::CalovGreveIntegrand(T_std_deviation,
462  const double eps = 1.0e-4;
463  if (Teff < eps) {
464  Teff = 0;
465  }
466 
467  // Note that in the line below we replace "Delta_t_Phi / Delta_t" with "h_Phi / pi". See
468  // equations 1 and 2 in Zeitz et al.
469  double A = dt * (h_phi / M_PI / (m_water_density * m_L));
470 
471  DEBMSimpleMelt result;
472 
473  result.insolation_melt = A * (transmissivity * (1.0 - albedo) * insolation);
474  result.temperature_melt = A * m_melt_c1 * Teff;
475  result.offset_melt = A * m_melt_c2;
476 
477  double total_melt = (result.insolation_melt + result.temperature_melt +
478  result.offset_melt);
479  // this model should not produce negative melt rates
480  result.total_melt = std::max(total_melt, 0.0);
481 
482  if (T < m_melt_threshold_temp) {
483  result.total_melt = 0.0;
484  }
485 
486  return result;
487 }
488 
489 /*! @brief Compute the surface mass balance at a location from the amount of melted snow
490  * and the solid accumulation amount in a time interval.
491  *
492  * - a fraction of the melted snow and ice refreezes, conceptualized
493  * as superimposed ice
494  */
496  double max_melt,
497  double old_snow_depth,
498  double accumulation) const {
499  Changes result;
500 
501  double
502  snow_depth = old_snow_depth,
503  snow_melted = 0.0,
504  ice_melted = 0.0;
505 
506  assert(ice_thickness >= 0);
507 
508  // snow depth cannot exceed total ice_thickness
509  snow_depth = std::min(snow_depth, ice_thickness);
510 
511  assert(snow_depth >= 0);
512 
513  snow_depth += accumulation;
514 
515  if (max_melt <= 0.0) { // The "no melt" case.
516  snow_melted = 0.0;
517  ice_melted = 0.0;
518  } else if (max_melt <= snow_depth) {
519  // Some of the snow melted and some is left; in any case, all of the energy available
520  // for melt was used up in melting snow.
521  snow_melted = max_melt;
522  ice_melted = 0.0;
523  } else {
524  // All (snow_depth meters) of snow melted. Excess melt is available to melt ice.
525  snow_melted = snow_depth;
526  ice_melted = std::min(max_melt - snow_melted, ice_thickness);
527  }
528 
529  double ice_created_by_refreeze = m_refreeze_fraction * snow_melted;
530  if (m_refreeze_ice_melt) {
531  ice_created_by_refreeze += m_refreeze_fraction * ice_melted;
532  }
533 
534  snow_depth = std::max(snow_depth - snow_melted, 0.0);
535 
536  double total_melt = (snow_melted + ice_melted);
537  double runoff = total_melt - ice_created_by_refreeze;
538  double smb = accumulation - runoff;
539 
540  result.snow_depth = snow_depth - old_snow_depth;
541  result.melt = total_melt;
542  result.runoff = runoff;
543  result.smb = ice_thickness + smb >= 0 ? smb : -ice_thickness;
544 
545  assert(ice_thickness + result.smb >= 0);
546 
547  return result;
548 }
549 
550 /*!
551  * Eccentricity of the Earth’s orbit (no units).
552  */
553 double DEBMSimplePointwise::eccentricity(double time) const {
554  if (m_use_paleo_file) {
555  return m_eccentricity->value(time);
556  }
558 }
559 
560 /*!
561  * Returns the obliquity of the ecliptic in radians.
562  */
563 double DEBMSimplePointwise::obliquity(double time) const {
564  if (m_use_paleo_file) {
565  return m_obliquity->value(time);
566  }
567  return m_constant_obliquity;
568 }
569 
570 /*!
571  * Returns the longitude of the perihelion in radians.
572  */
573 double DEBMSimplePointwise::perihelion_longitude(double time) const {
574  if (m_use_paleo_file) {
575  return remainder(m_perihelion_longitude->value(time), 2.0 * M_PI);
576  }
578 }
579 
580 } // end of namespace surface
581 } // end of namespace pism
double get_number(const std::string &name, UseFlag flag=REMEMBER_THIS_USE) const
std::string get_string(const std::string &name, UseFlag flag=REMEMBER_THIS_USE) const
bool get_flag(const std::string &name, UseFlag flag=REMEMBER_THIS_USE) const
A class for storing and accessing PISM configuration flags and parameters.
std::shared_ptr< const Config > config() const
Definition: Context.cc:105
std::shared_ptr< const Time > time() const
Definition: Context.cc:117
static RuntimeError formatted(const ErrorLocation &location, const char format[],...) __attribute__((format(printf
build a RuntimeError with a formatted message
double albedo(double melt_rate, MaskValue cell_type) const
double insolation(double declination, double distance_factor, double latitude_degrees) const
double m_transmissivity_slope
slope used in the linear parameterization of transmissivity
std::unique_ptr< ScalarForcing > m_eccentricity
std::unique_ptr< ScalarForcing > m_obliquity
double m_refreeze_fraction
refreeze fraction
double m_positive_threshold_temperature
threshold temperature for the computation of temperature-driven melt
double eccentricity(double time) const
DEBMSimpleMelt melt(double declination, double distance_factor, double dt, double T_std_deviation, double T, double surface_elevation, double lat, double albedo) const
double m_albedo_slope
slope used in the linear parameterization of the albedo as a function of melt
bool m_refreeze_ice_melt
refreeze melted ice
double perihelion_longitude(double time) const
double m_solar_constant
the solar constant
Changes step(double ice_thickness, double max_melt, double snow_depth, double accumulation) const
Compute the surface mass balance at a location from the amount of melted snow and the solid accumulat...
double m_phi
minimum solar elevation angle above which melt is possible
double atmosphere_transmissivity(double elevation) const
std::unique_ptr< ScalarForcing > m_perihelion_longitude
std::shared_ptr< const Time > m_time
OrbitalParameters orbital_parameters(double time) const
#define PISM_ERROR_LOCATION
const double phi
Definition: exactTestK.c:41
static const double b0
Definition: exactTestL.cc:41
double max(const array::Scalar &input)
Finds maximum over all the values in an array::Scalar object. Ignores ghosts.
Definition: Scalar.cc:165
double min(const array::Scalar &input)
Finds minimum over all the values in an array::Scalar object. Ignores ghosts.
Definition: Scalar.cc:193
static double solar_declination_paleo(double obliquity, double solar_longitude)
static double solar_longitude(double year_fraction, double eccentricity, double perihelion_longitude)
static double hour_angle(double phi, double latitude, double declination)
static double CalovGreveIntegrand(double sigma, double temperature)
static double distance_factor_present_day(double year_fraction)
static double solar_declination_present_day(double year_fraction)
static double distance_factor_paleo(double eccentricity, double perihelion_longitude, double solar_longitude)
static double insolation(double solar_constant, double distance_factor, double hour_angle, double latitude, double declination)
T clip(T x, T a, T b)
MaskValue
Definition: Mask.hh:30
@ MASK_ICE_FREE_OCEAN
Definition: Mask.hh:35