Skip to content

Commit 1ba41c3

Browse files
Infrastructure for 3D RBC (#593)
* Added basic infrastructure for 3D RBC * Added tests for processing of 3D RBC * Added test for interpolation of initial conditions * Fix linting * Added test for vertical profile computation in RBC * Added some documentation
1 parent 42ddf1a commit 1ba41c3

File tree

10 files changed

+751
-6
lines changed

10 files changed

+751
-6
lines changed

pySDC/implementations/problem_classes/RayleighBenard3D.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,3 +462,20 @@ def get_frequency_spectrum(self, u):
462462
spectrum[..., index_global] += _spectrum[..., index_local]
463463

464464
return xp.array(unique_k_all), spectrum
465+
466+
def get_vertical_profiles(self, u, components):
467+
if self.spectral_space:
468+
u_hat = u.copy()
469+
else:
470+
u_hat = self.transform(u)
471+
472+
_u_hat = self.axes[-1].itransform(u_hat, axes=(-1,))
473+
474+
avgs = {}
475+
for c in components:
476+
i = self.index(c)
477+
avg = self.xp.ascontiguousarray(_u_hat[i, 0, 0, :].real) / self.axes[0].N / self.axes[1].N
478+
self.comm.Bcast(avg, root=0)
479+
avgs[c] = avg
480+
481+
return avgs

pySDC/implementations/sweeper_classes/Runge_Kutta.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,21 @@ class IMEXEuler(RungeKuttaIMEX):
510510
matrix_explicit = ForwardEuler.matrix
511511

512512

513+
class IMEXEulerStifflyAccurate(RungeKuttaIMEX):
514+
"""
515+
This implements u = fI^-1(u0 + fE(u0)) rather than u = fI^-1(u0) + fE(u0) + u0.
516+
This implementation is slightly inefficient with two stages, but the last stage is the solution, making it stiffly
517+
accurate and suitable for some DAEs.
518+
"""
519+
520+
nodes = np.array([0, 1])
521+
weights = np.array([0, 1])
522+
weights_explicit = np.array([1, 0])
523+
524+
matrix = np.array([[0, 0], [0, 1]])
525+
matrix_explicit = np.array([[0, 0], [1, 0]])
526+
527+
513528
class CrankNicolson(RungeKutta):
514529
"""
515530
Implicit Runge-Kutta method of second order, A-stable.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import pickle
2+
import matplotlib.pyplot as plt
3+
import numpy as np
4+
from scipy import integrate
5+
from pySDC.helpers.plot_helper import figsize_by_journal, setup_mpl
6+
7+
setup_mpl()
8+
9+
10+
def get_pySDC_data(res=-1, dt=-1, config_name='RBC3DG4', base_path='data/RBC_time_averaged'):
11+
path = f'{base_path}/{config_name}-res{res}-dt{dt:.0e}.pickle'
12+
with open(path, 'rb') as file:
13+
data = pickle.load(file)
14+
15+
return data
16+
17+
18+
def interpolate_NuV_to_reference_times(data, reference_data, order=12):
19+
from qmat.lagrange import getSparseInterpolationMatrix
20+
21+
t_in = np.array(data['t'])
22+
t_out = np.array([me for me in reference_data['t'] if me <= max(t_in)])
23+
24+
interpolation_matrix = getSparseInterpolationMatrix(t_in, t_out, order=order)
25+
return interpolation_matrix @ t_in, interpolation_matrix @ data['Nu']['V']
26+
27+
28+
def plot_Nu(res, dts, config_name, ref, ax, title): # pragma: no cover
29+
ax.plot(ref['t'], ref['Nu']['V'], color='black', ls='--')
30+
ax.set_title(title)
31+
Nu_ref = np.array(ref['Nu']['V'])
32+
33+
for dt in dts:
34+
data = get_pySDC_data(res=res, dt=dt, config_name=config_name)
35+
t_i, Nu_i = interpolate_NuV_to_reference_times(data, ref)
36+
ax.plot(t_i, Nu_i, label=rf'$\Delta t={{{dt}}}$')
37+
38+
error = np.abs(Nu_ref[: Nu_i.shape[0]] - Nu_i) / np.abs(Nu_ref[: Nu_i.shape[0]])
39+
40+
# compute mean Nu
41+
mask = np.logical_and(t_i >= 100, t_i <= 200)
42+
Nu_mean = np.mean(Nu_i[mask])
43+
Nu_std = np.std(Nu_i[mask])
44+
45+
last_line = ax.get_lines()[-1]
46+
if any(error > 1e-2):
47+
deviates = min(t_i[error > 1e-2])
48+
ax.axvline(deviates, color=last_line.get_color(), ls=':')
49+
print(f'{title} dt={dt} Nu={Nu_mean:.3f}+={Nu_std:.3f}, deviates more than 1% from t={deviates:.2f}')
50+
else:
51+
print(f'{title} dt={dt} Nu={Nu_mean:.3f}+={Nu_std:.3f}')
52+
ax.legend(frameon=True, loc='upper left')
53+
54+
55+
def plot_Nu_over_time_Ra1e5(): # pragma: no cover
56+
Nu_fig, Nu_axs = plt.subplots(4, 1, sharex=True, sharey=True, figsize=figsize_by_journal('Nature_CS', 1, 1.4))
57+
58+
res = 32
59+
60+
ref_data = get_pySDC_data(res=res, dt=0.01, config_name='RBC3DG4R4Ra1e5')
61+
62+
plot_Nu(32, [0.06, 0.04, 0.02], 'RBC3DG4R4SDC34Ra1e5', ref_data, Nu_axs[0], 'SDC34')
63+
plot_Nu(32, [0.06, 0.05, 0.02, 0.01], 'RBC3DG4R4SDC23Ra1e5', ref_data, Nu_axs[1], 'SDC23')
64+
plot_Nu(32, [0.05, 0.04, 0.02, 0.01, 0.005], 'RBC3DG4R4RKRa1e5', ref_data, Nu_axs[2], 'RK443')
65+
plot_Nu(32, [0.02, 0.01, 0.005], 'RBC3DG4R4EulerRa1e5', ref_data, Nu_axs[3], 'RK111')
66+
67+
Nu_axs[-1].set_xlabel('$t$')
68+
Nu_axs[-1].set_ylabel('$Nu$')
69+
70+
Nu_fig.tight_layout()
71+
Nu_fig.savefig('./plots/Nu_over_time_Ra1e5.pdf', bbox_inches='tight')
72+
73+
74+
if __name__ == '__main__':
75+
76+
plot_Nu_over_time_Ra1e5()
77+
78+
plt.show()
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
from pySDC.projects.GPU.configs.base_config import get_config
2+
from pySDC.projects.GPU.run_experiment import parse_args
3+
from pySDC.helpers.fieldsIO import FieldsIO
4+
import matplotlib.pyplot as plt
5+
from tqdm import tqdm
6+
from mpi4py import MPI
7+
import numpy as np
8+
import pickle
9+
import os
10+
11+
12+
def process_RBC3D_data(base_path='./data/RBC_time_averaged', plot=True, args=None, config=None):
13+
# prepare problem instance
14+
args = args if args else parse_args()
15+
comm = MPI.COMM_WORLD
16+
config = config if config else get_config(args)
17+
desc = config.get_description(**args)
18+
P = desc['problem_class'](
19+
**{
20+
**desc['problem_params'],
21+
'spectral_space': False,
22+
'comm': comm,
23+
'Dirichlet_recombination': False,
24+
'left_preconditioner': False,
25+
}
26+
)
27+
P.setUpFieldsIO()
28+
zInt = P.axes[-1].get_integration_weights()
29+
xp = P.xp
30+
31+
# prepare paths
32+
os.makedirs(base_path, exist_ok=True)
33+
fname = config.get_file_name()
34+
fname_trim = fname[fname.index('RBC3D') : fname.index('.pySDC')]
35+
path = f'{base_path}/{fname_trim}.pickle'
36+
37+
# open simulation data
38+
data = FieldsIO.fromFile(fname)
39+
40+
# prepare arrays to store data in
41+
Nu = {
42+
'V': [],
43+
'b': [],
44+
't': [],
45+
'thermal': [],
46+
'kinetic': [],
47+
}
48+
t = []
49+
profiles = {key: [] for key in ['T', 'u', 'v', 'w']}
50+
rms_profiles = {key: [] for key in profiles.keys()}
51+
spectrum = []
52+
spectrum_all = []
53+
54+
# try to load time averaged values
55+
u_mean_profile = P.u_exact()
56+
if os.path.isfile(path):
57+
with open(path, 'rb') as file:
58+
avg_data = pickle.load(file)
59+
if comm.rank == 0:
60+
print(f'Read data from file {path!r}')
61+
for key in profiles.keys():
62+
if f'profile_{key}' in avg_data.keys():
63+
u_mean_profile[P.index(key)] = avg_data[f'profile_{key}'][P.local_slice(False)[-1]]
64+
elif comm.rank == 0:
65+
print('No mean profiles available yet. Please rerun script after completion to get correct RMS profiles')
66+
67+
# prepare progress bar
68+
indeces = range(args['restart_idx'], data.nFields)
69+
if P.comm.rank == 0:
70+
indeces = tqdm(indeces)
71+
72+
# loop through all data points and compute stuff
73+
for i in indeces:
74+
_t, u = data.readField(i)
75+
76+
# Nusselt numbers
77+
_Nu = P.compute_Nusselt_numbers(u)
78+
if any(me > 1e3 for me in _Nu.values()):
79+
continue
80+
81+
for key in Nu.keys():
82+
Nu[key].append(_Nu[key])
83+
84+
t.append(_t)
85+
86+
# profiles
87+
_profiles = P.get_vertical_profiles(u, list(profiles.keys()))
88+
_rms_profiles = P.get_vertical_profiles((u - u_mean_profile) ** 2, list(profiles.keys()))
89+
for key in profiles.keys():
90+
profiles[key].append(_profiles[key])
91+
rms_profiles[key].append(_rms_profiles[key])
92+
93+
# spectrum
94+
k, s = P.get_frequency_spectrum(u)
95+
s_mean = zInt @ P.axes[-1].transform(s[0], axes=(0,))
96+
spectrum.append(s_mean)
97+
spectrum_all.append(s)
98+
99+
# make a plot of the results
100+
t = xp.array(t)
101+
z = P.axes[-1].get_1dgrid()
102+
103+
if config.converged == 0:
104+
print('Warning: no convergence has been set for this configuration!')
105+
106+
fig, axs = plt.subplots(1, 4, figsize=(18, 4))
107+
for key in Nu.keys():
108+
axs[0].plot(t, Nu[key], label=f'$Nu_{{{key}}}$')
109+
if config.converged > 0:
110+
axs[0].axvline(config.converged, color='black')
111+
axs[0].set_ylabel('$Nu$')
112+
axs[0].set_xlabel('$t$')
113+
axs[0].legend(frameon=False)
114+
115+
# compute differences in Nusselt numbers
116+
avg_Nu = {}
117+
std_Nu = {}
118+
for key in Nu.keys():
119+
_Nu = [Nu[key][i] for i in range(len(Nu[key])) if t[i] > config.converged]
120+
avg_Nu[key] = xp.mean(_Nu)
121+
std_Nu[key] = xp.std(_Nu)
122+
123+
rel_error = {
124+
key: abs(avg_Nu[key] - avg_Nu['V']) / avg_Nu['V']
125+
for key in [
126+
't',
127+
'b',
128+
'thermal',
129+
'kinetic',
130+
]
131+
}
132+
if comm.rank == 0:
133+
print(
134+
f'With Ra={P.Rayleigh:.0e} got Nu={avg_Nu["V"]:.2f}+-{std_Nu["V"]:.2f} with errors: Top {rel_error["t"]:.2e}, bottom: {rel_error["b"]:.2e}, thermal: {rel_error["thermal"]:.2e}, kinetic: {rel_error["kinetic"]:.2e}'
135+
)
136+
137+
# compute average profiles
138+
avg_profiles = {}
139+
for key, values in profiles.items():
140+
values_from_convergence = [values[i] for i in range(len(values)) if t[i] >= config.converged]
141+
142+
avg_profiles[key] = xp.mean(values_from_convergence, axis=0)
143+
144+
avg_rms_profiles = {}
145+
for key, values in rms_profiles.items():
146+
values_from_convergence = [values[i] for i in range(len(values)) if t[i] >= config.converged]
147+
avg_rms_profiles[key] = xp.sqrt(xp.mean(values_from_convergence, axis=0))
148+
149+
# average T
150+
avg_T = avg_profiles['T']
151+
axs[1].axvline(0.5, color='black')
152+
axs[1].plot(avg_T, z)
153+
axs[1].set_xlabel('$T$')
154+
axs[1].set_ylabel('$z$')
155+
156+
# rms profiles
157+
avg_T = avg_rms_profiles['T']
158+
max_idx = xp.argmax(avg_T)
159+
res_in_boundary_layer = max_idx if max_idx < len(z) / 2 else len(z) - max_idx
160+
boundary_layer = z[max_idx] if max_idx > len(z) / 2 else P.axes[-1].L - z[max_idx]
161+
if comm.rank == 0:
162+
print(
163+
f'Thermal boundary layer of thickness {boundary_layer:.2f} is resolved with {res_in_boundary_layer} points'
164+
)
165+
axs[2].axhline(z[max_idx], color='black')
166+
axs[2].plot(avg_T, z)
167+
axs[2].scatter(avg_T, z)
168+
axs[2].set_xlabel(r'$T_\text{rms}$')
169+
axs[2].set_ylabel('$z$')
170+
171+
# spectrum
172+
_s = xp.array(spectrum)
173+
avg_spectrum = xp.mean(_s[t >= config.converged], axis=0)
174+
axs[3].loglog(k[avg_spectrum > 1e-15], avg_spectrum[avg_spectrum > 1e-15])
175+
axs[3].set_xlabel('$k$')
176+
axs[3].set_ylabel(r'$\|\hat{u}_x\|$')
177+
178+
if P.comm.rank == 0:
179+
write_data = {
180+
't': t,
181+
'Nu': Nu,
182+
'avg_Nu': avg_Nu,
183+
'std_Nu': std_Nu,
184+
'z': P.axes[-1].get_1dgrid(),
185+
'k': k,
186+
'spectrum': spectrum_all,
187+
'avg_spectrum': avg_spectrum,
188+
'boundary_layer_thickness': boundary_layer,
189+
'res_in_boundary_layer': res_in_boundary_layer,
190+
}
191+
for key, values in avg_profiles.items():
192+
write_data[f'profile_{key}'] = values
193+
for key, values in avg_rms_profiles.items():
194+
write_data[f'rms_profile_{key}'] = values
195+
196+
with open(path, 'wb') as file:
197+
pickle.dump(write_data, file)
198+
print(f'Wrote data to file {path!r}')
199+
200+
if plot:
201+
fig.tight_layout()
202+
fig.savefig(f'{base_path}/{fname_trim}.pdf')
203+
plt.show()
204+
return path
205+
206+
207+
if __name__ == '__main__':
208+
process_RBC3D_data()

0 commit comments

Comments
 (0)