paddle 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- paddle/__init__.py +6 -0
- paddle/crm.py +76 -0
- paddle/data/saturn1d.yaml +88 -0
- paddle/evolve_kinetics.py +40 -0
- paddle/find_init_params.py +72 -0
- paddle/setup_profile.py +283 -0
- paddle/write_profile.py +109 -0
- paddle-1.1.0.dist-info/METADATA +37 -0
- paddle-1.1.0.dist-info/RECORD +11 -0
- paddle-1.1.0.dist-info/WHEEL +4 -0
- paddle-1.1.0.dist-info/entry_points.txt +2 -0
paddle/__init__.py
ADDED
paddle/crm.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import math
|
|
3
|
+
import time
|
|
4
|
+
import kintera
|
|
5
|
+
import numpy as np
|
|
6
|
+
from snapy import (
|
|
7
|
+
index,
|
|
8
|
+
MeshBlockOptions,
|
|
9
|
+
MeshBlock,
|
|
10
|
+
OutputOptions,
|
|
11
|
+
NetcdfOutput,
|
|
12
|
+
)
|
|
13
|
+
from kintera import (
|
|
14
|
+
ThermoOptions,
|
|
15
|
+
ThermoX,
|
|
16
|
+
KineticsOptions,
|
|
17
|
+
Kinetics,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
if __name__ == '__main__':
|
|
21
|
+
# input file
|
|
22
|
+
infile = "earth.yaml"
|
|
23
|
+
device = "cpu"
|
|
24
|
+
|
|
25
|
+
# create meshblock
|
|
26
|
+
op_block = MeshBlockOptions.from_yaml(infile)
|
|
27
|
+
block = MeshBlock(op_block)
|
|
28
|
+
block.to(torch.device(device))
|
|
29
|
+
|
|
30
|
+
# create thermo module
|
|
31
|
+
op_thermo = ThermoOptions.from_yaml(infile)
|
|
32
|
+
thermo_x = ThermoX(op_thermo)
|
|
33
|
+
thermo_x.to(torch.device(device))
|
|
34
|
+
|
|
35
|
+
# create kinetics module
|
|
36
|
+
op_kinet = KineticsOptions.from_yaml(infile)
|
|
37
|
+
kinet = Kinetics(op_kinet)
|
|
38
|
+
kinet.to(torch.device(device))
|
|
39
|
+
|
|
40
|
+
# create output fields
|
|
41
|
+
op_out = OutputOptions().file_basename("earth")
|
|
42
|
+
out2 = NetcdfOutput(op_out.fid(2).variable("prim"))
|
|
43
|
+
out3 = NetcdfOutput(op_out.fid(3).variable("uov"))
|
|
44
|
+
out4 = NetcdfOutput(op_out.fid(4).variable("diag"))
|
|
45
|
+
outs = [out2, out4]
|
|
46
|
+
|
|
47
|
+
# set up initial condition
|
|
48
|
+
w = setup_initial_condition(block, thermo_x)
|
|
49
|
+
print("w = ", w[:,0,0,:])
|
|
50
|
+
|
|
51
|
+
# integration
|
|
52
|
+
current_time = 0.0
|
|
53
|
+
count = 0
|
|
54
|
+
start_time = time.time()
|
|
55
|
+
interior = block.part((0, 0, 0))
|
|
56
|
+
while not block.intg.stop(count, current_time):
|
|
57
|
+
dt = block.max_time_step()
|
|
58
|
+
u = block.buffer("hydro.eos.U")
|
|
59
|
+
|
|
60
|
+
if count % 1 == 0:
|
|
61
|
+
print(f"count = {count}, dt = {dt}, time = {current_time}")
|
|
62
|
+
print("mass = ", u[interior][index.idn].sum())
|
|
63
|
+
|
|
64
|
+
for out in outs:
|
|
65
|
+
out.increment_file_number()
|
|
66
|
+
out.write_output_file(block, current_time)
|
|
67
|
+
out.combine_blocks()
|
|
68
|
+
|
|
69
|
+
for stage in range(len(block.intg.stages)):
|
|
70
|
+
block.forward(dt, stage)
|
|
71
|
+
|
|
72
|
+
# evolve kinetics
|
|
73
|
+
u[index.icy:] += evolve_kinetics(block, kinet, thermo_x)
|
|
74
|
+
|
|
75
|
+
current_time += dt
|
|
76
|
+
count += 1
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Saturn Reference Atmosphere Model
|
|
2
|
+
#
|
|
3
|
+
# Solar abundances relative to H2, enrichments
|
|
4
|
+
#
|
|
5
|
+
# X_He = 0.195, 0.6955
|
|
6
|
+
# X_CH4 = 5.50e-04, 9.4
|
|
7
|
+
# X_H2O = 1.026e-03, 10.0
|
|
8
|
+
# X_NH3 = 1.352e-04, 3.0
|
|
9
|
+
# X_H2S = 3.10e-05, 3.0
|
|
10
|
+
#
|
|
11
|
+
# Converted to mole fractions
|
|
12
|
+
#
|
|
13
|
+
# X_H2O = 8.91e-03
|
|
14
|
+
# X_NH3 = 3.52e-04
|
|
15
|
+
# X_H2S = 8.08e-05
|
|
16
|
+
#
|
|
17
|
+
# Dry air composition
|
|
18
|
+
#
|
|
19
|
+
# 0.86838 * H2 + 0.11778 * He + 4.49e-03 * CH4
|
|
20
|
+
# {H: 1.755, He: 0.118, C: 4.49e-03}
|
|
21
|
+
|
|
22
|
+
reference-state:
|
|
23
|
+
Tref: 0.
|
|
24
|
+
Pref: 1.e5
|
|
25
|
+
|
|
26
|
+
species:
|
|
27
|
+
- name: dry
|
|
28
|
+
composition: {H: 1.755, He: 0.118, C: 4.49e-03}
|
|
29
|
+
cv_R: 2.5
|
|
30
|
+
|
|
31
|
+
- name: H2O
|
|
32
|
+
composition: {H: 2, O: 1}
|
|
33
|
+
cv_R: 2.5
|
|
34
|
+
u0_R: 0.
|
|
35
|
+
|
|
36
|
+
- name: NH3
|
|
37
|
+
composition: {H: 2, O: 1}
|
|
38
|
+
cv_R: 2.5
|
|
39
|
+
u0_R: 0.
|
|
40
|
+
|
|
41
|
+
- name: H2S
|
|
42
|
+
composition: {H: 2, S: 1}
|
|
43
|
+
cv_R: 2.5
|
|
44
|
+
u0_R: 0.
|
|
45
|
+
|
|
46
|
+
- name: H2O(l)
|
|
47
|
+
composition: {H: 2, O: 1}
|
|
48
|
+
cv_R: 9.0
|
|
49
|
+
u0_R: -3430.
|
|
50
|
+
|
|
51
|
+
- name: NH3(s)
|
|
52
|
+
composition: {H: 2, O: 1}
|
|
53
|
+
cv_R: 9.0
|
|
54
|
+
u0_R: -5520.
|
|
55
|
+
|
|
56
|
+
- name: NH4SH(s)
|
|
57
|
+
composition: {N: 1, H: 5, S: 1}
|
|
58
|
+
cv_R: 9.0
|
|
59
|
+
u0_R: -1.2e4
|
|
60
|
+
|
|
61
|
+
geometry:
|
|
62
|
+
type: cartesian
|
|
63
|
+
bounds: {x1min: 0., x1max: 600.e3, x2min: -0.5, x2max: 0.5, x3min: -0.5, x3max: 0.5}
|
|
64
|
+
cells: {nx1: 200, nx2: 1, nx3: 1, nghost: 0}
|
|
65
|
+
|
|
66
|
+
dynamics:
|
|
67
|
+
equation-of-state:
|
|
68
|
+
type: moist-mixture
|
|
69
|
+
max-iter: 20
|
|
70
|
+
ftol: 1.e-6
|
|
71
|
+
|
|
72
|
+
reactions:
|
|
73
|
+
- equation: H2O => H2O(l)
|
|
74
|
+
type: nucleation
|
|
75
|
+
rate-constant: {formula: h2o_ideal}
|
|
76
|
+
|
|
77
|
+
- equation: NH3 => NH3(s)
|
|
78
|
+
type: nucleation
|
|
79
|
+
rate-constant: {formula: nh3_ideal}
|
|
80
|
+
|
|
81
|
+
- equation: NH3 + H2S <=> NH4SH(s)
|
|
82
|
+
type: nucleation
|
|
83
|
+
rate-constant: {formula: nh3_h2s_lewis}
|
|
84
|
+
|
|
85
|
+
boundary-condition:
|
|
86
|
+
external:
|
|
87
|
+
x1-inner: reflecting
|
|
88
|
+
x1-outer: reflecting
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import torch
|
|
2
|
+
import snapy
|
|
3
|
+
import kintera
|
|
4
|
+
|
|
5
|
+
def evolve_kinetics(
|
|
6
|
+
hydro_w: torch.Tensor,
|
|
7
|
+
block: snapy.MeshBlock,
|
|
8
|
+
kinet: kintera.Kinetics,
|
|
9
|
+
thermo_x: kintera.ThermoX) -> torch.Tensor:
|
|
10
|
+
"""
|
|
11
|
+
Evolve the chemical kinetics for one time step using implicit method.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
hydro_w (torch.Tensor): The primitive variables tensor.
|
|
15
|
+
block (snapy.MeshBlock): The mesh block containing the simulation data.
|
|
16
|
+
kinet (kintera.Kinetics): The kinetics module for chemical reactions.
|
|
17
|
+
thermo_x (kintera.ThermoX): The thermodynamics module for computing properties.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
torch.Tensor: The change in mass density due to chemical reactions.
|
|
21
|
+
"""
|
|
22
|
+
eos = block.module("hydro.eos")
|
|
23
|
+
thermo_y = eos.named_modules()["thermo"]
|
|
24
|
+
|
|
25
|
+
temp = eos.compute("W->T", (w,))
|
|
26
|
+
pres = w[index.ipr]
|
|
27
|
+
xfrac = thermo_y.compute("Y->X", (w[ICY:],))
|
|
28
|
+
conc = thermo_x.compute("TPX->V", (temp, pres, xfrac))
|
|
29
|
+
cp_vol = thermo_x.compute("TV->cp", (temp, conc))
|
|
30
|
+
|
|
31
|
+
conc_kinet = kinet.options.narrow_copy(conc, thermo_y.options)
|
|
32
|
+
rate, rc_ddC, rc_ddT = kinet.forward(temp, pres, conc_kinet)
|
|
33
|
+
jac = kinet.jacobian(temp, conc_kinet, cp_vol, rate, rc_ddC, rc_ddT)
|
|
34
|
+
|
|
35
|
+
stoich = kinet.buffer("stoich")
|
|
36
|
+
del_conc = kintera.evolve_implicit(rate, stoich, jac, dt)
|
|
37
|
+
|
|
38
|
+
inv_mu = thermo_y.buffer("inv_mu")
|
|
39
|
+
del_rho = del_conc / inv_mu[1:].view((1, 1, 1, -1))
|
|
40
|
+
return del_rho.permute((3, 0, 1, 2))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from scipy.interpolate import interp1d
|
|
2
|
+
import snapy
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .setup_profile import setup_profile
|
|
6
|
+
|
|
7
|
+
def find_init_params(
|
|
8
|
+
block: snapy.MeshBlock,
|
|
9
|
+
param: dict[str, float],
|
|
10
|
+
*,
|
|
11
|
+
target_T: float=300.,
|
|
12
|
+
target_P: float=1.e5,
|
|
13
|
+
method: str="moist-adiabat",
|
|
14
|
+
max_iter: int=50,
|
|
15
|
+
ftol: float=1.e-2,
|
|
16
|
+
verbose: bool=True):
|
|
17
|
+
"""Find initial parameters that yield desired T and P
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
block (snapy.MeshBlock): The mesh block to set up.
|
|
21
|
+
param (dict[str, float]): Initial guess parameters for the adiabat setup.
|
|
22
|
+
Required keys: Ts, Ps, x<species>, grav.
|
|
23
|
+
target_T (float, optional): Target temperature in Kelvin. Defaults to 300 K.
|
|
24
|
+
target_P (float, optional): Target pressure in Pascals. Defaults to 1e5 Pa.
|
|
25
|
+
method (str, optional): Method for the adiabat setup.
|
|
26
|
+
max_iter (int, optional): Maximum number of iterations. Defaults to 50.
|
|
27
|
+
ftol (float, optional): Tolerance for temperature convergence. Defaults to 1e-2 K.
|
|
28
|
+
verbose (bool, optional): If True, print iteration details. Defaults to True.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
dict: Dictionary containing the found parameters: Ts, Ps, xH2O, xNH3, xH2S.
|
|
32
|
+
"""
|
|
33
|
+
count = 0
|
|
34
|
+
eos = block.hydro.get_eos()
|
|
35
|
+
|
|
36
|
+
while count < max_iter:
|
|
37
|
+
if verbose:
|
|
38
|
+
print(f"Iteration {count+1}: Trying Ts={param['Ts']}\n")
|
|
39
|
+
|
|
40
|
+
# setup profile
|
|
41
|
+
w = setup_profile(block, param, method=method)
|
|
42
|
+
|
|
43
|
+
# calculate temperature
|
|
44
|
+
temp = eos.compute("W->T", (w,)).squeeze()
|
|
45
|
+
|
|
46
|
+
# calculate 1D pressure
|
|
47
|
+
pres = w[snapy.index.ipr,...].squeeze()
|
|
48
|
+
|
|
49
|
+
# temperature function
|
|
50
|
+
t_func = interp1d(
|
|
51
|
+
pres.log().cpu().numpy(),
|
|
52
|
+
temp.log().cpu().numpy(),
|
|
53
|
+
kind="linear",
|
|
54
|
+
fill_value="extrapolate")
|
|
55
|
+
|
|
56
|
+
temp1 = np.exp(t_func(np.log(target_P)))
|
|
57
|
+
if verbose:
|
|
58
|
+
print(f" At P={target_P:.3e} Pa, T={temp1:.3f} K (target {target_T} K)\n")
|
|
59
|
+
if abs(temp1 - target_T) < ftol:
|
|
60
|
+
if verbose:
|
|
61
|
+
print("Converged! Found parameters:")
|
|
62
|
+
for key, val in param.items():
|
|
63
|
+
print(f" {key} = {val}")
|
|
64
|
+
print(f"Matching T = {target_T} K at P = {target_P} Pa")
|
|
65
|
+
return param
|
|
66
|
+
|
|
67
|
+
# adjust Ts using a damped scaling
|
|
68
|
+
param["Ts"] += (target_T - temp1) * 0.9
|
|
69
|
+
count += 1
|
|
70
|
+
|
|
71
|
+
raise RuntimeError("Failed to converge within the maximum number of iterations.")
|
|
72
|
+
|
paddle/setup_profile.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import torch
|
|
4
|
+
import snapy
|
|
5
|
+
import kintera
|
|
6
|
+
|
|
7
|
+
def integrate_neutral(
|
|
8
|
+
thermo_x: kintera.ThermoX,
|
|
9
|
+
temp: torch.Tensor,
|
|
10
|
+
pres: torch.Tensor,
|
|
11
|
+
xfrac: torch.Tensor,
|
|
12
|
+
grav: float,
|
|
13
|
+
dz: float,
|
|
14
|
+
max_iter: int = 100
|
|
15
|
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
16
|
+
"""
|
|
17
|
+
A neutral density profile assumes no cloud and:
|
|
18
|
+
|
|
19
|
+
(1) dP/dz = -rho*g
|
|
20
|
+
(2) d(rho)/dz = ...
|
|
21
|
+
|
|
22
|
+
In discretized form:
|
|
23
|
+
|
|
24
|
+
rho_bar = 0.5 * (rho_old + rho_ad)
|
|
25
|
+
P_new = P_old - rho_bar * g * dz
|
|
26
|
+
"""
|
|
27
|
+
conc = thermo_x.compute("TPX->V", [temp, pres, xfrac])
|
|
28
|
+
rho = thermo_x.compute("V->D", [conc])
|
|
29
|
+
|
|
30
|
+
# make an adiabatic step first
|
|
31
|
+
temp_ad = temp.clone()
|
|
32
|
+
pres_ad = pres.clone()
|
|
33
|
+
xfrac_ad = xfrac.clone()
|
|
34
|
+
|
|
35
|
+
thermo_x.extrapolate_ad(temp_ad, pres_ad, xfrac_ad, grav, dz)
|
|
36
|
+
conc_ad = thermo_x.compute("TPX->V", [temp_ad, pres_ad, xfrac_ad])
|
|
37
|
+
rho_ad = thermo_x.compute("V->D", [conc_ad])
|
|
38
|
+
rho_bar = 0.5 * (rho + rho_ad)
|
|
39
|
+
|
|
40
|
+
pres2 = pres - rho_bar * grav * dz
|
|
41
|
+
|
|
42
|
+
# initial guess
|
|
43
|
+
temp2 = temp_ad.clone()
|
|
44
|
+
count = 0
|
|
45
|
+
while count < max_iter:
|
|
46
|
+
xfrac2 = xfrac_ad.clone()
|
|
47
|
+
|
|
48
|
+
# equilibrate clouds
|
|
49
|
+
thermo_x.forward(temp2, pres2, xfrac2)
|
|
50
|
+
|
|
51
|
+
# drop clouds fractions
|
|
52
|
+
for cid in thermo_x.options.cloud_ids():
|
|
53
|
+
xfrac2[..., cid] = 0.0
|
|
54
|
+
# renormalize mole fractions
|
|
55
|
+
xfrac2 /= xfrac.sum(dim=-1, keepdim=True)
|
|
56
|
+
|
|
57
|
+
conc2 = thermo_x.compute("TPX->V", [temp2, pres2, xfrac2])
|
|
58
|
+
rho2 = thermo_x.compute("V->D", [conc2])
|
|
59
|
+
|
|
60
|
+
if torch.allclose(rho2, rho_ad):
|
|
61
|
+
break
|
|
62
|
+
|
|
63
|
+
temp2 -= temp2 * (rho_ad - rho2) / rho2
|
|
64
|
+
count += 1
|
|
65
|
+
|
|
66
|
+
if count == max_iter:
|
|
67
|
+
raise RuntimeError("neutral density integration did not converge.")
|
|
68
|
+
|
|
69
|
+
return temp2, pres2, xfrac2
|
|
70
|
+
|
|
71
|
+
def integrate_dry_adiabat(
|
|
72
|
+
thermo_x: kintera.ThermoX,
|
|
73
|
+
temp: torch.Tensor,
|
|
74
|
+
pres: torch.Tensor,
|
|
75
|
+
xfrac: torch.Tensor,
|
|
76
|
+
grav: float,
|
|
77
|
+
dz: float,
|
|
78
|
+
max_iter: int = 100
|
|
79
|
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
80
|
+
"""
|
|
81
|
+
A dry adiabatic profile assumes no cloud and:
|
|
82
|
+
|
|
83
|
+
(1) dT/dz = -g/cp
|
|
84
|
+
(2) dP/dz = -rho*g
|
|
85
|
+
|
|
86
|
+
In discretized form:
|
|
87
|
+
|
|
88
|
+
cp_bar = 0.5 * (cp(T_old) + cp(T_new))
|
|
89
|
+
T_new = T_old - g/bar * dz
|
|
90
|
+
rho_bar = 0.5 * (rho_old + rho_new)
|
|
91
|
+
P_new = P_old - rho_bar * g * dz
|
|
92
|
+
|
|
93
|
+
"""
|
|
94
|
+
conc1 = thermo_x.compute("TPX->V", [temp, pres, xfrac])
|
|
95
|
+
cp1 = thermo_x.compute("TV->cp", [temp, conc1]) / conc1.sum(-1)
|
|
96
|
+
rho1 = thermo_x.compute("V->D", [conc1])
|
|
97
|
+
mmw1 = (thermo_x.mu * xfrac).sum(-1)
|
|
98
|
+
|
|
99
|
+
# initial guess
|
|
100
|
+
temp2 = temp - grav * mmw1 * dz / cp1
|
|
101
|
+
pres2 = pres - rho1 * grav * dz
|
|
102
|
+
|
|
103
|
+
count = 0
|
|
104
|
+
while count < max_iter:
|
|
105
|
+
xfrac2 = xfrac.clone()
|
|
106
|
+
|
|
107
|
+
# equilibrate clouds
|
|
108
|
+
thermo_x.forward(temp2, pres2, xfrac2)
|
|
109
|
+
|
|
110
|
+
# drop clouds fractions
|
|
111
|
+
for cid in thermo_x.options.cloud_ids():
|
|
112
|
+
xfrac2[..., cid] = 0.0
|
|
113
|
+
# renormalize mole fractions
|
|
114
|
+
xfrac2 /= xfrac.sum(dim=-1, keepdim=True)
|
|
115
|
+
|
|
116
|
+
conc2 = thermo_x.compute("TPX->V", [temp2, pres2, xfrac2])
|
|
117
|
+
cp2 = thermo_x.compute("TV->cp", [temp2, conc2]) / conc2.sum(-1)
|
|
118
|
+
rho2 = thermo_x.compute("V->D", [conc2])
|
|
119
|
+
mmw2 = (thermo_x.mu * xfrac2).sum(-1)
|
|
120
|
+
|
|
121
|
+
cp_bar = 0.5 * (cp1 / mmw1 + cp2 / mmw2)
|
|
122
|
+
rho_bar = 0.5 * (rho1 + rho2)
|
|
123
|
+
|
|
124
|
+
temp_new = temp - grav * dz / cp_bar
|
|
125
|
+
pres_new = pres - rho_bar * grav * dz
|
|
126
|
+
|
|
127
|
+
if torch.allclose(temp_new, temp2) and torch.allclose(pres_new, pres2):
|
|
128
|
+
break
|
|
129
|
+
|
|
130
|
+
temp2 = temp_new
|
|
131
|
+
pres2 = pres_new
|
|
132
|
+
count += 1
|
|
133
|
+
|
|
134
|
+
if count == max_iter:
|
|
135
|
+
raise RuntimeError("Dry adiabat integration did not converge.")
|
|
136
|
+
|
|
137
|
+
return temp2, pres2, xfrac2
|
|
138
|
+
|
|
139
|
+
def setup_profile(
|
|
140
|
+
block: snapy.MeshBlock,
|
|
141
|
+
param: dict[str, float] = {},
|
|
142
|
+
method: str = "moist-adiabat"
|
|
143
|
+
) -> torch.Tensor:
|
|
144
|
+
"""
|
|
145
|
+
Set up an adiabatic initial condition for the mesh block.
|
|
146
|
+
|
|
147
|
+
This function initializes the primitive variables in the mesh block
|
|
148
|
+
and returns the initialized tensor.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
block (snapy.MeshBlock): The mesh block to set up.
|
|
152
|
+
param (dict[str, float], optional): Parameters for the adiabat setup. Defaults to {}.
|
|
153
|
+
method (str, optional): Method for the adiabat setup. Choose between
|
|
154
|
+
(1) "dry-adiabat"
|
|
155
|
+
(2) "moist-adiabat"
|
|
156
|
+
(3) "isothermal"
|
|
157
|
+
(4) "pseudo-adiabat"
|
|
158
|
+
(5) "neutral"
|
|
159
|
+
Defaults to "moist-adiabat".
|
|
160
|
+
|
|
161
|
+
Required parameters in `param`:
|
|
162
|
+
Ts (float): Surface temperature in Kelvin. Default is 300 K.
|
|
163
|
+
Ps (float): Surface pressure in Pascals. Default is 1e5 Pa.
|
|
164
|
+
x<species> (float): Mole fraction of a specific species (e.g., xH2O for
|
|
165
|
+
water vapor). Default is 0.0.
|
|
166
|
+
grav (float): Gravitational acceleration in m/s^2. Default is 9.8 m/s^2.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
torch.Tensor: The initialized primitive variables tensor.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
# check method
|
|
173
|
+
valid_methods = [
|
|
174
|
+
"dry-adiabat",
|
|
175
|
+
"moist-adiabat",
|
|
176
|
+
"isothermal",
|
|
177
|
+
"pseudo-adiabat",
|
|
178
|
+
"neutral"
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
if method not in valid_methods:
|
|
182
|
+
raise ValueError(f"Invalid method '{method}'. Choose from {valid_methods}.")
|
|
183
|
+
|
|
184
|
+
Ts = param.get("Ts", 300.)
|
|
185
|
+
Ps = param.get("Ps", 1.e5)
|
|
186
|
+
grav = param.get("grav", 9.8)
|
|
187
|
+
Tmin = param.get("Tmin", 0.)
|
|
188
|
+
|
|
189
|
+
# get handles to modules
|
|
190
|
+
coord = block.module("hydro.coord")
|
|
191
|
+
thermo_y = block.module("hydro.eos.thermo")
|
|
192
|
+
|
|
193
|
+
# get coordinates
|
|
194
|
+
x3v, x2v, x1v = torch.meshgrid(
|
|
195
|
+
coord.buffer("x3v"), coord.buffer("x2v"), coord.buffer("x1v"), indexing="ij"
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# handling mole fractions
|
|
199
|
+
thermo_x = kintera.ThermoX(thermo_y.options)
|
|
200
|
+
thermo_x.to(dtype=x1v.dtype, device=x1v.device)
|
|
201
|
+
|
|
202
|
+
# get dimensions
|
|
203
|
+
nc3, nc2, nc1 = x1v.shape
|
|
204
|
+
ny = len(thermo_y.options.species()) - 1
|
|
205
|
+
nvar = 5 + ny
|
|
206
|
+
|
|
207
|
+
w = torch.zeros((nvar, nc3, nc2, nc1),
|
|
208
|
+
dtype=x1v.dtype, device=x1v.device)
|
|
209
|
+
|
|
210
|
+
temp = Ts * torch.ones((nc3, nc2), dtype=w.dtype, device=w.device)
|
|
211
|
+
pres = Ps * torch.ones((nc3, nc2), dtype=w.dtype, device=w.device)
|
|
212
|
+
xfrac = torch.zeros((nc3, nc2, ny + 1), dtype=w.dtype, device=w.device)
|
|
213
|
+
|
|
214
|
+
for name in thermo_y.options.species():
|
|
215
|
+
index = thermo_y.options.species().index(name)
|
|
216
|
+
xfrac[..., index] = param.get(f"x{name}", 0.0)
|
|
217
|
+
|
|
218
|
+
# dry air mole fraction
|
|
219
|
+
xfrac[..., 0] = 1. - xfrac[..., 1:].sum(dim=-1)
|
|
220
|
+
|
|
221
|
+
# start and end indices for the vertical direction
|
|
222
|
+
# excluding ghost cells
|
|
223
|
+
ifirst = coord.ifirst()
|
|
224
|
+
ilast = coord.ilast()
|
|
225
|
+
|
|
226
|
+
# vertical grid distance of the first cell
|
|
227
|
+
dz = coord.buffer("dx1f")[ifirst]
|
|
228
|
+
|
|
229
|
+
# half a grid to cell center
|
|
230
|
+
thermo_x.extrapolate_ad(temp, pres, xfrac, grav, dz / 2.);
|
|
231
|
+
|
|
232
|
+
# adiabatic extrapolation
|
|
233
|
+
if method == "isothermal":
|
|
234
|
+
i_isothermal = ifirst
|
|
235
|
+
ifirst = ilast
|
|
236
|
+
else:
|
|
237
|
+
i_isothermal = ilast
|
|
238
|
+
|
|
239
|
+
for i in range(ifirst, ilast):
|
|
240
|
+
# drop clouds fractions
|
|
241
|
+
if method.split("-")[0] != "moist":
|
|
242
|
+
for cid in thermo_x.options.cloud_ids():
|
|
243
|
+
xfrac[..., cid] = 0.0
|
|
244
|
+
# renormalize mole fractions
|
|
245
|
+
xfrac /= xfrac.sum(dim=-1, keepdim=True)
|
|
246
|
+
conc = thermo_x.compute("TPX->V", [temp, pres, xfrac])
|
|
247
|
+
|
|
248
|
+
w[snapy.index.ipr, ..., i] = pres;
|
|
249
|
+
w[snapy.index.idn, ..., i] = thermo_x.compute("V->D", [conc])
|
|
250
|
+
w[snapy.index.icy:, ...,i] = thermo_x.compute("X->Y", [xfrac])
|
|
251
|
+
|
|
252
|
+
dz = coord.buffer("dx1f")[i]
|
|
253
|
+
if method.split("-")[0] == "dry":
|
|
254
|
+
temp, pres, xfrac = integrate_dry_adiabat(thermo_x, temp, pres, xfrac, grav, dz);
|
|
255
|
+
elif method.split("-")[0] == "neutral":
|
|
256
|
+
temp, pres, xfrac = integrate_neutral(thermo_x, temp, pres, xfrac, grav, dz);
|
|
257
|
+
else:
|
|
258
|
+
thermo_x.extrapolate_ad(temp, pres, xfrac, grav, dz);
|
|
259
|
+
|
|
260
|
+
if torch.any(temp < Tmin):
|
|
261
|
+
i_isothermal = i + 1
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
# isothermal extrapolation
|
|
265
|
+
for i in range(i_isothermal, ilast):
|
|
266
|
+
# drop clouds fractions
|
|
267
|
+
if method.split("-")[0] != "moist":
|
|
268
|
+
for cid in thermo_x.options.cloud_ids():
|
|
269
|
+
xfrac[..., cid] = 0.0
|
|
270
|
+
# renormalize mole fractions
|
|
271
|
+
xfrac /= xfrac.sum(dim=-1, keepdim=True)
|
|
272
|
+
|
|
273
|
+
mu = (thermo_x.mu * xfrac).sum(-1)
|
|
274
|
+
dz = coord.buffer("dx1f")[i]
|
|
275
|
+
pres *= torch.exp(-grav * mu * dz / (kintera.constants.Rgas * temp))
|
|
276
|
+
conc = thermo_x.compute("TPX->V", [temp, pres, xfrac])
|
|
277
|
+
w[snapy.index.ipr, ..., i] = pres
|
|
278
|
+
w[snapy.index.idn, ..., i] = thermo_x.compute("V->D", [conc])
|
|
279
|
+
w[snapy.index.icy:, ..., i] = thermo_x.compute("X->Y", [xfrac])
|
|
280
|
+
|
|
281
|
+
# initialize hydro state
|
|
282
|
+
block.initialize({"hydro_w": w})
|
|
283
|
+
return w
|
paddle/write_profile.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from scipy.interpolate import interp1d
|
|
3
|
+
|
|
4
|
+
import datetime
|
|
5
|
+
import numpy as np
|
|
6
|
+
import torch
|
|
7
|
+
import kintera
|
|
8
|
+
import snapy
|
|
9
|
+
|
|
10
|
+
def write_profile(
|
|
11
|
+
filename: str,
|
|
12
|
+
hydro_w: torch.Tensor,
|
|
13
|
+
block: snapy.MeshBlock,
|
|
14
|
+
ref_pressure: float = 1.e5,
|
|
15
|
+
comment: Optional[str] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""
|
|
18
|
+
Write an atmospheric profile to a text file.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
filename (str): The name of the text file to write.
|
|
22
|
+
hydro_w (torch.Tensor): Primitive variables of the atmosphere profile.
|
|
23
|
+
block (snapy.MeshBlock): The mesh block containing the atmosphere data.
|
|
24
|
+
ref_pressure (float): Reference pressure for height calculation.
|
|
25
|
+
comment (Optional[str]): A comment string to write to the file.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
None
|
|
29
|
+
"""
|
|
30
|
+
# useful modules
|
|
31
|
+
thermo_y = block.module("hydro.eos.thermo")
|
|
32
|
+
coord = block.module("hydro.coord")
|
|
33
|
+
eos = block.hydro.get_eos()
|
|
34
|
+
|
|
35
|
+
# handling mole fraction quantities
|
|
36
|
+
thermo_x = kintera.ThermoX(thermo_y.options)
|
|
37
|
+
thermo_x.to(dtype=hydro_w.dtype, device=hydro_w.device)
|
|
38
|
+
|
|
39
|
+
# check dimensions of hydro_w
|
|
40
|
+
species = thermo_y.options.species()
|
|
41
|
+
if hydro_w.ndim != 4:
|
|
42
|
+
raise ValueError("hydro_w must be a 4D tensor.")
|
|
43
|
+
if hydro_w.shape[0] != 4 + len(species):
|
|
44
|
+
raise ValueError("hydro_w shape does not match number of species.")
|
|
45
|
+
if hydro_w.shape[1] != 1 or hydro_w.shape[2] != 1:
|
|
46
|
+
raise ValueError("hydro_w must have shape (N, 1, 1, L).")
|
|
47
|
+
|
|
48
|
+
# calculate a height grid
|
|
49
|
+
pres = hydro_w[snapy.index.ipr,...].squeeze() / 1.e5 # Pa -> bar
|
|
50
|
+
zlev_func = interp1d(
|
|
51
|
+
pres.log().cpu().numpy(),
|
|
52
|
+
coord.buffer("x1v").cpu().numpy(),
|
|
53
|
+
kind="linear",
|
|
54
|
+
fill_value="extrapolate")
|
|
55
|
+
zref = zlev_func(np.log(ref_pressure / 1.e5))
|
|
56
|
+
zlev = (coord.buffer("x1v") - zref) / 1.e3 # m -> km
|
|
57
|
+
|
|
58
|
+
# calculate temperature
|
|
59
|
+
temp = eos.compute("W->T", (hydro_w,)).squeeze()
|
|
60
|
+
|
|
61
|
+
# calculate mole fractions
|
|
62
|
+
xfrac = thermo_y.compute("Y->X", (hydro_w[snapy.index.icy:,...],)).squeeze()
|
|
63
|
+
|
|
64
|
+
# calculate heat capacity
|
|
65
|
+
conc = thermo_x.compute("TPX->V", (temp, pres * 1.e5, xfrac))
|
|
66
|
+
cpx = thermo_x.compute("TV->cp", (temp, conc)) / conc.sum(-1)
|
|
67
|
+
|
|
68
|
+
# calculate entropy
|
|
69
|
+
ens = thermo_x.compute("TPV->S", (temp, pres * 1.e5, conc)) / conc.sum(-1)
|
|
70
|
+
|
|
71
|
+
with open(filename, "w") as f:
|
|
72
|
+
# write comments
|
|
73
|
+
f.write(f"# File generated by write_profile.py\n")
|
|
74
|
+
f.write(f"# Date generated: {datetime.datetime.now()}\n")
|
|
75
|
+
f.write(f"# Speices units: mole fraction [ppmv]\n")
|
|
76
|
+
f.write(f"# Width of the first column: 6 characters\n")
|
|
77
|
+
f.write(f"# Width of other columns: 13 characters\n")
|
|
78
|
+
f.write(f"# Reference pressure for z = 0: {ref_pressure} Pa\n")
|
|
79
|
+
if comment:
|
|
80
|
+
f.write(f"# {comment}\n\n")
|
|
81
|
+
|
|
82
|
+
# Write header
|
|
83
|
+
headers = []
|
|
84
|
+
headers.append(f"# {'IDX':<4}")
|
|
85
|
+
headers.append(f"{'HGT[km]':>{13}}")
|
|
86
|
+
headers.append(f"{'PRE[bar]':>{13}}")
|
|
87
|
+
headers.append(f"{'TEM[K]':>{13}}")
|
|
88
|
+
for sp in species[1:]:
|
|
89
|
+
headers.append(f"{sp.upper():>{13}}")
|
|
90
|
+
headers.append(f"{'CPX[J,K,mol]':>{13}}")
|
|
91
|
+
headers.append(f"{'ENS[J,K,mol]':>{13}}")
|
|
92
|
+
|
|
93
|
+
f.write("".join(headers) + "\n")
|
|
94
|
+
|
|
95
|
+
# Write data
|
|
96
|
+
num_rows = hydro_w.shape[-1]
|
|
97
|
+
for i in range(num_rows, 0, -1):
|
|
98
|
+
row = []
|
|
99
|
+
row.append(f"{i:<6}")
|
|
100
|
+
row.append(f"{zlev[i-1]:.6g}".rjust(13)) # HGT
|
|
101
|
+
row.append(f"{pres[i-1]:.6g}".rjust(13)) # PRE
|
|
102
|
+
row.append(f"{temp[i-1]:.6g}".rjust(13))
|
|
103
|
+
for s in range(1, len(species)):
|
|
104
|
+
row.append(f"{xfrac[i-1,s]*1.e6:.6g}".rjust(13))
|
|
105
|
+
row.append(f"{cpx[i-1]:.6g}".rjust(13))
|
|
106
|
+
row.append(f"{ens[i-1]:.6g}".rjust(13))
|
|
107
|
+
|
|
108
|
+
f.write("".join(row) + "\n")
|
|
109
|
+
print(f"Atmosphere profile written to {filename}")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: paddle
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Canoe's utility subroutines
|
|
5
|
+
Project-URL: Homepage, https://github.com/chengcli/paddle
|
|
6
|
+
Project-URL: Repository, https://github.com/chengcli/paddle
|
|
7
|
+
Project-URL: Issues, https://github.com/chengcli/paddle/issues
|
|
8
|
+
Author-email: Cheng Li <chengcli@umich.edu>
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: kintera>=1.0.1
|
|
24
|
+
Requires-Dist: snapy>=0.7.0
|
|
25
|
+
Requires-Dist: torch<=2.7.1,>=2.7.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# paddle
|
|
31
|
+
|
|
32
|
+
A minimal, utility subroutines for canoe
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install paddle
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
paddle/__init__.py,sha256=22d5IF6ukQPV4erU7BqEsZs7H2lE1eyaM7T4eOhj9rY,217
|
|
2
|
+
paddle/crm.py,sha256=LXuvOINJE_d6skKu5wU--LVR2CLnI8N5FP5blatM04s,2034
|
|
3
|
+
paddle/evolve_kinetics.py,sha256=OEjp0At_9yD8FRGjSJx0q6aPo8VqrpVD3M2k4sL8xdg,1445
|
|
4
|
+
paddle/find_init_params.py,sha256=iEc-NyBlDlGY6k9wfjYqgqU9hMPklp_7YEjvMnEp57M,2525
|
|
5
|
+
paddle/setup_profile.py,sha256=OYl_mJI9twya6MsC5FeskTrk37FX4YJbyWxJK3WnRsg,8901
|
|
6
|
+
paddle/write_profile.py,sha256=3PECXsvGzvJvZfKMTdm49nNj61ZCgf488KoCL3ayB_s,3892
|
|
7
|
+
paddle/data/saturn1d.yaml,sha256=07JvsjBGg004TdrhLZOjPZ3m0CYlEhB7fz98Iv1dEGE,1635
|
|
8
|
+
paddle-1.1.0.dist-info/METADATA,sha256=aQ8H-b1BQX3ZfJ2IO0P17H8kSAWh7IEhF0Vc4hedoGA,1296
|
|
9
|
+
paddle-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
10
|
+
paddle-1.1.0.dist-info/entry_points.txt,sha256=pDR96GW6ylBZrbFd-tRGthW8qTuYaSLjrEt1LFIEYto,48
|
|
11
|
+
paddle-1.1.0.dist-info/RECORD,,
|