Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
6 changes: 4 additions & 2 deletions .github/workflows/pace_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ on:
- main # ... when merging into the main branch

jobs:
pace_main_tests:
uses: NOAA-GFDL/pace/.github/workflows/main_unit_tests.yaml@develop
pace_unit_tests:
# TODO: Revert tests back to GFDL (not jjuyeonkim) after merging
# https://github.com/NOAA-GFDL/NDSL/issues/64
uses: jjuyeonkim/pace/.github/workflows/main_unit_tests.yaml@20250908_namelist_2
with:
component_trigger: true
component_name: pyFV3
4 changes: 3 additions & 1 deletion .github/workflows/pyshield_tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ on:

jobs:
pyshield_translate_tests:
uses: NOAA-GFDL/PySHiELD/.github/workflows/translate.yaml@develop
# TODO: Revert tests back to GFDL (not jjuyeonkim) after merging
# https://github.com/NOAA-GFDL/NDSL/issues/64
uses: jjuyeonkim/PySHiELD/.github/workflows/translate.yaml@20250908_namelist_2
with:
component_trigger: true
component_name: pyFV3
4 changes: 3 additions & 1 deletion .github/workflows/translate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ jobs:
uses: actions/checkout@v5
with:
submodules: 'recursive'
repository: noaa-gfdl/pyFV3
# TODO: Revert tests back to GFDL (not jjuyeonkim) after merging https://github.com/NOAA-GFDL/NDSL/issues/64
repository: jjuyeonkim/pyFV3
ref: 20250908_namelist_2
path: pyFV3

- name: Checkout hash that triggered CI
Expand Down
269 changes: 97 additions & 172 deletions pyfv3/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@
import f90nml
import yaml

from ndsl.namelist import Namelist, NamelistDefaults
from ndsl import Namelist


DEFAULT_INT = 0
DEFAULT_STR = ""
DEFAULT_FLOAT = 0.0
DEFAULT_BOOL = False
DEFAULT_NML_GROUPS = (
"main_nml",
"coupler_nml",
"fv_core_nml",
) # TODO Double-check for any additional groups


@dataclasses.dataclass(frozen=True)
Expand Down Expand Up @@ -195,86 +200,60 @@ class DynamicalCoreConfig:
vtdm4: float = DEFAULT_FLOAT
z_tracer: bool = DEFAULT_BOOL
do_qa: bool = DEFAULT_BOOL
layout: Tuple[int, int] = NamelistDefaults.layout
grid_type: int = NamelistDefaults.grid_type
u_max: float = NamelistDefaults.u_max # max windspeed for dp config
do_f3d: bool = NamelistDefaults.do_f3d
inline_q: bool = NamelistDefaults.inline_q
do_skeb: bool = NamelistDefaults.do_skeb # save dissipation estimate
use_logp: bool = NamelistDefaults.use_logp
moist_phys: bool = NamelistDefaults.moist_phys
check_negative: bool = NamelistDefaults.check_negative
layout: Tuple[int, int] = (1, 1)
grid_type: int = 0
u_max: float = 350.0 # max windspeed for dp config

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this (and the other floats) be type hinted as an NDSL defined Float, or is it sufficient for it to be a Python native float?

@jjuyeonkim jjuyeonkim Sep 12, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good point. I think eventually they should be changed to Float.

However, I wanted to focus on the Namelist split/refactor for these PRs. If more people think that it should be part of this PR, I'll be overruled and will change it.

do_f3d: bool = False
inline_q: bool = False
do_skeb: bool = False # save dissipation estimate
use_logp: bool = False
moist_phys: bool = True
check_negative: bool = False
# gfdl_cloud_microphys.F90
tau_r2g: float = NamelistDefaults.tau_r2g # rain freezing during fast_sat
tau_smlt: float = NamelistDefaults.tau_smlt # snow melting
tau_g2r: float = NamelistDefaults.tau_g2r # graupel melting to rain
tau_imlt: float = NamelistDefaults.tau_imlt # cloud ice melting
tau_i2s: float = NamelistDefaults.tau_i2s # cloud ice to snow auto - conversion
tau_l2r: float = NamelistDefaults.tau_l2r # cloud water to rain auto - conversion
tau_g2v: float = NamelistDefaults.tau_g2v # graupel sublimation
tau_v2g: float = (
NamelistDefaults.tau_v2g
) # graupel deposition -- make it a slow process
sat_adj0: float = (
NamelistDefaults.sat_adj0
) # adjustment factor (0: no 1: full) during fast_sat_adj
tau_r2g: float = 900.0 # rain freezing during fast_sat
tau_smlt: float = 900.0 # snow melting
tau_g2r: float = 600.0 # graupel melting to rain
tau_imlt: float = 600.0 # cloud ice melting
tau_i2s: float = 1000.0 # cloud ice to snow auto - conversion
tau_l2r: float = 900.0 # cloud water to rain auto - conversion
tau_g2v: float = 1200.0 # graupel sublimation
tau_v2g: float = 21600.0 # graupel deposition -- make it a slow process
sat_adj0: float = 0.90 # adjustment factor (0: no 1: full) during fast_sat_adj
ql_gen: float = (
1.0e-3 # max new cloud water during remapping step if fast_sat_adj = .t.
)
ql_mlt: float = (
NamelistDefaults.ql_mlt
) # max value of cloud water allowed from melted cloud ice
qs_mlt: float = NamelistDefaults.qs_mlt # max cloud water due to snow melt
ql0_max: float = (
NamelistDefaults.ql0_max
) # max cloud water value (auto converted to rain)
t_sub: float = NamelistDefaults.t_sub # min temp for sublimation of cloud ice
qi_gen: float = (
NamelistDefaults.qi_gen
) # max cloud ice generation during remapping step
qi_lim: float = (
NamelistDefaults.qi_lim
) # cloud ice limiter to prevent large ice build up
qi0_max: float = NamelistDefaults.qi0_max # max cloud ice value (by other sources)
rad_snow: bool = (
NamelistDefaults.rad_snow
) # consider snow in cloud fraction calculation
rad_rain: bool = (
NamelistDefaults.rad_rain
) # consider rain in cloud fraction calculation
rad_graupel: bool = (
NamelistDefaults.rad_graupel
) # consider graupel in cloud fraction calculation
tintqs: bool = (
NamelistDefaults.tintqs
) # use temperature in the saturation mixing in PDF
dw_ocean: float = NamelistDefaults.dw_ocean # base value for ocean
dw_land: float = (
NamelistDefaults.dw_land
) # base value for subgrid deviation / variability over land
ql_mlt: float = 2.0e-3 # max value of cloud water allowed from melted cloud ice
qs_mlt: float = 1.0e-6 # max cloud water due to snow melt
ql0_max: float = 2.0e-3 # max cloud water value (auto converted to rain)
t_sub: float = 184.0 # min temp for sublimation of cloud ice
qi_gen: float = 1.82e-6 # max cloud ice generation during remapping step
qi_lim: float = 1.0 # cloud ice limiter to prevent large ice build up
qi0_max: float = 1.0e-4 # max cloud ice value (by other sources)
rad_snow: bool = True # consider snow in cloud fraction calculation
rad_rain: bool = True # consider rain in cloud fraction calculation
rad_graupel: bool = True # consider graupel in cloud fraction calculation
tintqs: bool = False # use temperature in the saturation mixing in PDF
dw_ocean: float = 0.10 # base value for ocean
dw_land: float = 0.15 # base value for subgrid deviation / variability over land
# cloud scheme 0 - ?
# 1: old fvgfs gfdl) mp implementation
# 2: binary cloud scheme (0 / 1)
icloud_f: int = NamelistDefaults.icloud_f
cld_min: float = NamelistDefaults.cld_min # !< minimum cloud fraction
tau_l2v: float = (
NamelistDefaults.tau_l2v
) # cloud water to water vapor (evaporation)
tau_v2l: float = (
NamelistDefaults.tau_v2l
) # water vapor to cloud water (condensation)
c2l_ord: int = NamelistDefaults.c2l_ord
regional: bool = NamelistDefaults.regional
m_split: int = NamelistDefaults.m_split
convert_ke: bool = NamelistDefaults.convert_ke
breed_vortex_inline: bool = NamelistDefaults.breed_vortex_inline
use_old_omega: bool = NamelistDefaults.use_old_omega
rf_fast: bool = NamelistDefaults.rf_fast
adiabatic: bool = NamelistDefaults.adiabatic
nf_omega: int = NamelistDefaults.nf_omega
fv_sg_adj: int = NamelistDefaults.fv_sg_adj
n_sponge: int = NamelistDefaults.n_sponge
sw_dynamics: bool = False # TODO: Change to NamelistDefaults.sw_dynamics
icloud_f: int = 0
cld_min: float = 0.05 # !< minimum cloud fraction
tau_l2v: float = 300.0 # cloud water to water vapor (evaporation)
tau_v2l: float = 90.0 # water vapor to cloud water (condensation)
c2l_ord: int = 4
regional: bool = False
m_split: int = 0
convert_ke: bool = False
breed_vortex_inline: bool = False
use_old_omega: bool = True
rf_fast: bool = False
adiabatic: bool = False
nf_omega: int = 1
fv_sg_adj: int = -1
n_sponge: int = 1
sw_dynamics: bool = False # shallow water conditions
namelist_override: Optional[str] = None

def __post_init__(self):
Expand All @@ -291,125 +270,65 @@ def __post_init__(self):
if self.grid_type > 3:
self.nf_omega = 0

# TODO: Consider removing from_namelist or from_f90nml. Duplicates.
@classmethod
def from_f90nml(cls, f90_namelist: f90nml.Namelist) -> "DynamicalCoreConfig":
namelist = Namelist.from_f90nml(f90_namelist)
return cls.from_namelist(namelist)
def from_namelist(
cls, namelist: Namelist, target_groups=None
) -> "DynamicalCoreConfig":
"""This creates a DynamicalCoreConfig using the input namelist.
Duplicate of from_f90nml.
"""
return cls.from_f90nml(namelist, target_groups=target_groups)

@classmethod
def from_namelist(cls, namelist: Namelist) -> "DynamicalCoreConfig":
return cls(
dt_atmos=namelist.dt_atmos,
a_imp=namelist.a_imp,
beta=namelist.beta,
consv_te=namelist.consv_te,
d2_bg=namelist.d2_bg,
d2_bg_k1=namelist.d2_bg_k1,
d2_bg_k2=namelist.d2_bg_k2,
d4_bg=namelist.d4_bg,
d_con=namelist.d_con,
d_ext=namelist.d_ext,
dddmp=namelist.dddmp,
delt_max=namelist.delt_max,
do_sat_adj=namelist.do_sat_adj,
do_vort_damp=namelist.do_vort_damp,
fill=namelist.fill,
hord_dp=namelist.hord_dp,
hord_mt=namelist.hord_mt,
hord_tm=namelist.hord_tm,
hord_tr=namelist.hord_tr,
hord_vt=namelist.hord_vt,
hydrostatic=namelist.hydrostatic,
k_split=namelist.k_split,
ke_bg=namelist.ke_bg,
kord_mt=namelist.kord_mt,
kord_tm=namelist.kord_tm,
kord_tr=namelist.kord_tr,
kord_wz=namelist.kord_wz,
n_split=namelist.n_split,
nord=namelist.nord,
npx=namelist.npx,
npy=namelist.npy,
npz=namelist.npz,
ntiles=namelist.ntiles,
nwat=namelist.nwat,
p_fac=namelist.p_fac,
rf_cutoff=namelist.rf_cutoff,
tau=namelist.tau,
vtdm4=namelist.vtdm4,
z_tracer=namelist.z_tracer,
do_qa=namelist.do_qa,
layout=namelist.layout,
grid_type=namelist.grid_type,
u_max=namelist.u_max,
do_f3d=namelist.do_f3d,
inline_q=namelist.inline_q,
do_skeb=namelist.do_skeb,
check_negative=namelist.check_negative,
tau_r2g=namelist.tau_r2g,
tau_smlt=namelist.tau_smlt,
tau_g2r=namelist.tau_g2r,
tau_imlt=namelist.tau_imlt,
tau_i2s=namelist.tau_i2s,
tau_l2r=namelist.tau_l2r,
tau_g2v=namelist.tau_g2v,
tau_v2g=namelist.tau_v2g,
sat_adj0=namelist.sat_adj0,
ql_gen=namelist.ql_gen,
ql_mlt=namelist.ql_mlt,
qs_mlt=namelist.qs_mlt,
ql0_max=namelist.ql0_max,
t_sub=namelist.t_sub,
qi_gen=namelist.qi_gen,
qi_lim=namelist.qi_lim,
qi0_max=namelist.qi0_max,
rad_snow=namelist.rad_snow,
rad_rain=namelist.rad_rain,
rad_graupel=namelist.rad_graupel,
tintqs=namelist.tintqs,
dw_ocean=namelist.dw_ocean,
dw_land=namelist.dw_land,
icloud_f=namelist.icloud_f,
cld_min=namelist.cld_min,
tau_l2v=namelist.tau_l2v,
tau_v2l=namelist.tau_v2l,
c2l_ord=namelist.c2l_ord,
regional=namelist.regional,
m_split=namelist.m_split,
convert_ke=namelist.convert_ke,
breed_vortex_inline=namelist.breed_vortex_inline,
use_old_omega=namelist.use_old_omega,
rf_fast=namelist.rf_fast,
adiabatic=namelist.adiabatic,
nf_omega=namelist.nf_omega,
fv_sg_adj=namelist.fv_sg_adj,
n_sponge=namelist.n_sponge,
)
def from_f90nml(
cls, namelist: f90nml.Namelist, target_groups=None
) -> "DynamicalCoreConfig":
"""This creates a DynamicalCoreConfig using the input namelist.
Args:
namelist
target_groups - If None, then the DEFAULT_NML_GROUPS will be used to populate the dataclass fields.
"""
namelist = Namelist(namelist)
if target_groups is None:
groups = DEFAULT_NML_GROUPS
else:
groups = target_groups

namelist_dict = namelist.namelist_groups_to_flatish_dict(groups)
namelist_dict = {
key: value
for key, value in namelist_dict.items()
if key in cls.__dataclass_fields__ # type: ignore
}
return cls(**namelist_dict)

@classmethod
def from_yaml(cls, yaml_config: str) -> "DynamicalCoreConfig":
def from_yaml_dict(cls, yaml_dict: dict) -> "DynamicalCoreConfig":
"""TOODO
Args:
yaml_dict: dict, as if read from yaml safe_load
"""
config = cls()
with open(yaml_config, "r") as f:
raw_config = yaml.safe_load(f)
flat_config: dict = {}
timestep = timedelta(seconds=raw_config["dt_atmos"])
timestep = timedelta(seconds=yaml_dict["dt_atmos"])
runtime = {
"days": 0.0,
"hours": 0.0,
"minutes": 0.0,
"seconds": 0.0,
}
for key in runtime.keys():
if key in raw_config.keys():
runtime[key] = raw_config[key]
if key in yaml_dict.keys():
runtime[key] = yaml_dict[key]

total_time = timedelta(
days=runtime["days"],
hours=runtime["hours"],
minutes=runtime["minutes"],
seconds=runtime["seconds"],
)
for key, value in raw_config.items():
for key, value in yaml_dict.items():
if isinstance(value, dict):
for subkey, subvalue in value.items():
if subkey in config.__annotations__.keys():
Expand All @@ -435,6 +354,12 @@ def from_yaml(cls, yaml_config: str) -> "DynamicalCoreConfig":
config.n_steps = floor(total_time.total_seconds() / timestep.total_seconds())
return config

@classmethod
Comment thread
fmalatino marked this conversation as resolved.
def from_yaml(cls, yaml_config: str) -> "DynamicalCoreConfig":
with open(yaml_config, "r") as f:
raw_config = yaml.safe_load(f)
return cls.from_yaml_dict(raw_config)

@property
def do_dry_convective_adjustment(self) -> bool:
return self.fv_sg_adj > 0
Expand Down
2 changes: 1 addition & 1 deletion pyfv3/testing/translate_fvdynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def __init__(
stencil_factory: StencilFactory,
):
super().__init__(grid, stencil_factory)
self.namelist = DynamicalCoreConfig.from_namelist(namelist)
self.config = DynamicalCoreConfig.from_f90nml(namelist)


class TranslateFVDynamics(ParallelTranslateBaseSlicing):
Expand Down
13 changes: 12 additions & 1 deletion pyfv3/wrappers/geos_wrapper.py

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FlorianDeconinck @romanc I'm proposing a change to the wrapper with the Namelists refactoring. Please let me know if you have feedback on this.

Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,19 @@ def __init__(
self.perf_collector = PerformanceCollector("GEOS wrapper", comm)

self.backend = backend

# TODO: The namelist argument is expected to contain a hierarchy
# consistent with what pace expects from a yaml configuration file
# (rather than an input.nml configuration file).
# The test_geos_wrapper in test_init_from_geos.py takes that yaml
# hierarchy and uses it to initialize a f90nml.Namelist.
# This seems a bit confusing. Because of this, the dycore_config
# is being initialized using "from_yaml_dict", even though it's
# it uses a "namelist". This should eventually be cleared up, but I'm
# leaving it as is because I'm not sure if it's important elsewhere.
self.namelist = namelist
self.dycore_config = pyfv3.DynamicalCoreConfig.from_f90nml(self.namelist)
self.dycore_config = pyfv3.DynamicalCoreConfig.from_yaml_dict(self.namelist)

self.dycore_config.dt_atmos = bdt
assert self.dycore_config.dt_atmos != 0

Expand Down
Loading
Loading