Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NDSL] QSat port #994

Merged
merged 25 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b30ae8d
SaturationVaporPressureTable code first pass
FlorianDeconinck Aug 14, 2024
cde8399
QSat_Ice_scalar_exact
FlorianDeconinck Aug 14, 2024
dd15d29
Fix calculation. Table generates OK.
FlorianDeconinck Aug 14, 2024
e6b597c
Fix Murphy and CAM
FlorianDeconinck Aug 14, 2024
5e4e464
Rename estimate_table -> table
FlorianDeconinck Aug 15, 2024
b80185c
Functional QSat module. Can handle optional RAMP/PASCALS/DQSAT inputs…
CharlesKrop Aug 28, 2024
1065cff
More clean up, first attempt at QSat function
CharlesKrop Aug 28, 2024
76b6ec5
Working QSat function. QSat class can still be called outside of sten…
CharlesKrop Aug 28, 2024
fdbf8aa
Minor change for improved DQSAT functionality
CharlesKrop Sep 3, 2024
035f1a9
Minor change for improved DQSAT functionality
CharlesKrop Sep 3, 2024
d826f35
Two new functions: QSat_Float_Liquid and QSat_Float_Ice. These two fu…
CharlesKrop Sep 3, 2024
5aa9b48
Tiny little bugfix fro QSat_Float_Liquid and QSat_Float_Ice
CharlesKrop Sep 5, 2024
b4998d6
Fixed indexing error in QSat_Float_Liquid/Ice, and inserted a workaro…
CharlesKrop Sep 6, 2024
5cc9dd6
Code cleanup, ready to be pulled back into main branch
CharlesKrop Sep 10, 2024
d9d13ed
Few more cleanups, modification to translate test to test sat tables
CharlesKrop Sep 10, 2024
b4d35f9
QSat verifies if tables are read in from netcdf (this is the current …
CharlesKrop Sep 11, 2024
a93a35e
Merge branch 'dsl/develop' into dsl/QSat
Sep 12, 2024
1c96c31
Fix run tests
Sep 12, 2024
e3c636e
Functional QSat module. Table calculations are still incorrect due to…
CharlesKrop Sep 12, 2024
534dcc9
Forgot to apply fix to other QSat functions. All functions now work p…
CharlesKrop Sep 12, 2024
6bd9006
Linting
Sep 13, 2024
4c2609e
Refactor table_method to an enum
Sep 13, 2024
c138b46
Clean up translate test
Sep 13, 2024
8e5b5cc
Revert run_tests.sh to common one
Sep 13, 2024
ece972f
Clean up debug fields for table
Sep 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ test_data/
.translate-errors/
.vscode
test_data/
sandbox/
*.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Saturation formulations

## In Fortran

### GEOS_QSatX

The code lists 3 ways, with each a specific set of formulation.

- `GEOS_QsatLqu` & `GEOS_QsatIce`:
- Latest code, carrying for every subsequent code the "exact" formulations for the different saturations schemes
- Can run with in exact mode or in table mode
- `Qsat` & `DQsat` are the "traditional" called, which are flagged as deprecated in docs but still in use
- Can run in exact mode (and ping back to GEOS_QSatLqu/Ice) or in table mode

The table (ESINIT) is a constant computation that leverages the GEOS_QsatLqu/GEOS_QSatIce to freeze results in increment
of 0.1 Pa (per documenttion)

A lot of the complexity of the code is due to micro-optimization to re-use inlined code instead of using function calls.

### MAPL_EQSatX

TBD

## Our implementation

### Estimated Saturation Table

:warning: This inplements the GEOS_QSatX only

The class `SaturationVaporPressureTable` in `table.py` computes on-demand the table of estimated saturation based on 0.1 Pa
increments. The `get_table` function in conjection with the `SaturationFormulation` in `fomulation.py` returns the correct table
to be sampled across a few fields.
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from ndsl.dsl.typing import Float
import numpy as np

# Generic constants to be moved to pyMoist global constant file
MAPL_TICE = Float(273.16) # K
MAPL_AIRMW = Float(28.965)
MAPL_H2OMW = Float(18.015) # kg/Kmole

# Saturation specific constants
TMINTBL = Float(150.0)
TMAXTBL = Float(333.0)
TMINLQU = MAPL_TICE - Float(40.0)
DEGSUBS = np.int32(100)
DELTA_T = Float(1.0) / DEGSUBS
TABLESIZE = np.int32(TMAXTBL - TMINTBL) * DEGSUBS + 1
TMIX = Float(-20.0)
ESFAC = MAPL_H2OMW / MAPL_AIRMW
ERFAC = DEGSUBS / ESFAC

MAX_MIXING_RATIO = Float(1.0)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import enum


class SaturationFormulation(enum.Enum):
"""
The choice of saturation vapor pressure formulation is a compile-time
option. Three choices are currently supported: The CAM formulation,
Murphy and Koop (2005, QJRMS), and Staars formulation from NSIPP-1.
"""

Staars = 1
CAM = 2
MurphyAndKoop = 3
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
from ndsl import StencilFactory, QuantityFactory, orchestrate
from ndsl.dsl.typing import Float, FloatField, Int, IntField
from ndsl.constants import X_DIM, Y_DIM, Z_DIM
from gt4py.cartesian.gtscript import computation, interval, PARALLEL
import gt4py.cartesian.gtscript as gtscript
import copy
from typing import Optional
from pyMoist.saturation.formulation import SaturationFormulation
from pyMoist.saturation.table import get_table
from pyMoist.saturation.constants import (
TMIX,
TMINTBL,
TMAXTBL,
TMINLQU,
MAPL_TICE,
DEGSUBS,
MAX_MIXING_RATIO,
ESFAC,
ERFAC,
TABLESIZE,
)

# FloatField with extra dimension initialized to handle table data
# This is a temporary solution. This solution creates a KxTABLESIZE array
# At some point a proper solution will be implemented, enabling a 1xTABLESIZE array
# Allocation is currently fixed to TABLESIZE constant. Fortran has some examples of
# flexible table sixes (larger than TABLESIZE, with increased granulatiry).
# Current implementation does not allow for flexible table sizes
# so if needed this will have to be implemented in another way.
FloatField_Extra_Dim = gtscript.Field[gtscript.K, (Float, (int(TABLESIZE)))]

# The following two functions, QSat_Float_Liquid and QSat_Float_Ice are,
# together, a non-equivalent replacement for QSat_Float.
# QSat_Float interpolates a smooth transition between liquid water and ice
# (based on the parameter TMIX), while the Liquic/Ice versions have a
# hard transition at zero C.
@gtscript.function
def QSat_Float_Liquid(
esw: FloatField_Extra_Dim,
estlqu: Float,
TL: Float,
PL: Float = -999.,
DQ_trigger: bool = False,
):
# qsatlqu.code with UTBL = True
if TL <= TMINLQU:
QS=estlqu
if DQ_trigger: DDQ = 0.
elif TL >= TMAXTBL:
QS=esw[0][TABLESIZE-1]
if DQ_trigger: DDQ = 0.
else:
TT = (TL - TMINTBL) * DEGSUBS + 1
IT = int(TT)
IT_PLUS_1 = IT + 1 # dace backend does not allow for [IT + 1] indexing because of cast to int
DDQ = esw[0][IT_PLUS_1] - esw[0][IT]
QS = ((TT - IT) * DDQ + esw[0][IT])

if PL != -999.:
if PL > QS:
DD = (ESFAC / (PL - (1. - ESFAC) * QS))
QS = QS * DD
if DQ_trigger: DQ = DDQ * ERFAC * PL * DD * DD
else:
QS = MAX_MIXING_RATIO
if DQ_trigger: DQ = 0.0
else:
if DQ_trigger: DQ = DDQ

return QS, DQ

@gtscript.function
def QSat_Float_Ice(
ese: FloatField_Extra_Dim,
estfrz: Float,
TL: Float,
PL: Float = -999.,
DQ_trigger: bool = False,
):
# qsatice.code with UTBL = True
if TL <= TMINTBL:
QS=ese[0][0]
if DQ_trigger: DDQ = 0.0
elif TL >= MAPL_TICE:
QS=estfrz
if DQ_trigger: DDQ = 0.0
else:
TT = (TL - TMINTBL) * DEGSUBS + 1
IT = int(TT)
IT_PLUS_1 = IT + 1 # dace backend does not allow for [IT + 1] indexing because of cast to int
DDQ = ese[0][IT_PLUS_1] - ese[0][IT]
QS = ((TT - IT) * DDQ + ese[0][IT])

if PL != -999.:
if PL > QS:
DD = (ESFAC / (PL - (1.0 - ESFAC) * QS))
QS = QS * DD
if DQ_trigger: DQ = DDQ * ERFAC * PL * DD * DD
else:
QS = MAX_MIXING_RATIO
if DQ_trigger: DQ = 0.0
else:
if DQ_trigger: DQ = DDQ

return QS, DQ

# Function version of QSat_table
@gtscript.function
def QSat_Float(
ese: FloatField_Extra_Dim,
esx: FloatField_Extra_Dim,
T: Float,
PL: Float,
RAMP: Float = -999.,
PASCALS_trigger: bool = False,
RAMP_trigger: bool = False,
DQSAT_trigger: bool = False,
):
if RAMP_trigger:
URAMP = -abs(RAMP)
else:
URAMP = TMIX

if PASCALS_trigger:
PP = PL
else:
PP = PL*100.


if T <= TMINTBL:
TI = TMINTBL
elif T >= TMAXTBL-.001:
TI = TMAXTBL-.001
else:
TI = T

TI = (TI - TMINTBL)*DEGSUBS+1
IT = int(TI)
IT_PLUS_1 = IT + 1 # dace backend does not allow for [IT + 1] indexing because of cast to int

if URAMP==TMIX:
DQ = esx[0][IT_PLUS_1] - esx[0][IT] # should be esx[0][IT + 1] - esx[0][IT]
QSAT = (TI-IT)*DQ + esx[0][IT]
else:
DQ = ese[0][IT_PLUS_1] - ese[0][IT] # should be ese[0][IT + 1] - ese[0][IT]
QSAT = (TI-IT)*DQ + ese[0][IT]

if PP <= QSAT:
QSAT = MAX_MIXING_RATIO
if DQSAT_trigger: DQSAT = 0.0
else: DQSAT = -999.
else:
DD = 1.0/(PP - (1.0-ESFAC)*QSAT)
QSAT = ESFAC*QSAT*DD
if DQSAT_trigger: DQSAT = ESFAC*DQ*DEGSUBS*PP*(DD*DD)
else: DQSAT = -999.

return QSAT, DQSAT


# Stencils implement QSAT0 function from GEOS_Utilities.F90
def QSat_FloatField(
ese: FloatField_Extra_Dim,
esx: FloatField_Extra_Dim,
T: FloatField,
PL: FloatField,
QSAT: FloatField,
DQSAT: FloatField,
RAMP: FloatField,
PASCALS_trigger: bool,
RAMP_trigger: bool,
DQSAT_trigger: bool,
):
with computation(PARALLEL), interval(...):
if RAMP_trigger:
URAMP = -abs(RAMP)
else:
URAMP = TMIX

if PASCALS_trigger:
PP = PL
else:
PP = PL*100.


if T <= TMINTBL:
TI = TMINTBL
elif T >= TMAXTBL-.001:
TI = TMAXTBL-.001
else:
TI = T

TI = (TI - TMINTBL)*DEGSUBS+1
IT = int(TI)

if URAMP==TMIX:
DQ = esx[0][IT] - esx[0][IT]
QSAT = (TI-IT)*DQ + esx[0][IT]
else:
DQ = ese[0][IT] - ese[0][IT]
QSAT = (TI-IT)*DQ + ese[0][IT]

if PP <= QSAT:
QSAT = MAX_MIXING_RATIO
if DQSAT_trigger: DQSAT = 0.0
else:
DD = 1.0/(PP - (1.0-ESFAC)*QSAT)
QSAT = ESFAC*QSAT*DD
if DQSAT_trigger: DQSAT = ESFAC*DQ*DEGSUBS*PP*(DD*DD)


class QSat:
"""Traditional satuation specific humidity.
In Fortran: GEOS_Utilities:QSat

Uses various formulations of the saturation vapor pressure to compute the saturation specific
humidity for temperature T and pressure PL.

The function get_table called in __init__ creates tables using exact calculations (equivalent
to UTBL=False in Fortran). get_table can be called again for further exact computations.
QSat_FloatField computes QSat for an entire field using table lookups
(equivalent to UTBL=True in Fortran).

For temperatures <= TMIX (-20C)
the calculation is done over ice; for temperatures >= ZEROC (0C) the calculation
is done over liquid water; and in between these values,
it interpolates linearly between the two.

The optional RAMP is the width of this
ice/water ramp (i.e., TMIX = ZEROC-RAMP); its default is 20.

If PASCALS is true, PL is
assumed to be in Pa; if false or not present, it is assumed to be in mb.
"""

def __init__(
self,
stencil_factory: StencilFactory,
quantity_factory: QuantityFactory,
formulation: SaturationFormulation = SaturationFormulation.Staars,
) -> None:

self.table = get_table(formulation)

self.extra_dim_quantity_factory = self.make_extra_dim_quantity_factory(
quantity_factory
)

self.ese = self.extra_dim_quantity_factory.zeros([Z_DIM, "table_axis"], "n/a")
self.esw = self.extra_dim_quantity_factory.zeros([Z_DIM, "table_axis"], "n/a")
self.esx = self.extra_dim_quantity_factory.zeros([Z_DIM, "table_axis"], "n/a")
self.ese.view[:] = self.table.ese
self.esw.view[:] = self.table.esw
self.esx.view[:] = self.table.esx

self._RAMP = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")
self._DQSAT = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")

self.QSat = quantity_factory.zeros([X_DIM, Y_DIM, Z_DIM], "n/a")

orchestrate(obj=self, config=stencil_factory.config.dace_config)
self._QSat_FloatField = stencil_factory.from_dims_halo(
func=QSat_FloatField,
compute_dims=[X_DIM, Y_DIM, Z_DIM],
)


@staticmethod
def make_extra_dim_quantity_factory(ijk_quantity_factory: QuantityFactory):
extra_dim_quantity_factory = copy.deepcopy(ijk_quantity_factory)
extra_dim_quantity_factory.set_extra_dim_lengths(
**{
"table_axis": TABLESIZE,
}
)
return extra_dim_quantity_factory

def __call__(
self,
T: FloatField,
PL: FloatField,
RAMP: Optional[FloatField] = None,
PASCALS: bool = False,
DQSAT: bool = False,
use_table_lookup: bool = True,
):

if RAMP is None:
RAMP = self._RAMP
RAMP_trigger = False
else:
RAMP_trigger = True

if use_table_lookup:
self._QSat_FloatField(self.ese, self.esw, self.esx, T, PL, self.QSat, self._DQSAT,
RAMP, PASCALS, RAMP_trigger, DQSAT)

if not use_table_lookup:
raise NotImplementedError(
"Saturation calculation: exact formulation not available, only table look up"
)
Loading