dapper.mods.QG
Quasi-geostraphic 2D flow. Described in detail by bib.sakov2008deterministic
.
Adapted from Pavel Sakov's enkf-matlab package.
More info:
governing_eqn.png
demo.py
- ψ (psi) is the stream function (i.e. surface elevation)
- Doubling time "between 25 and 50"
- Note Sakov's trick of increasing RKH2 from 2.0e-12 to 2.0e-11 to stabilize
the ensemble integration, which may be necessary for EnKF's with small N.
See example in
counillon2009
.
1"""Quasi-geostraphic 2D flow. Described in detail by `bib.sakov2008deterministic`. 2 3Adapted from Pavel Sakov's enkf-matlab package. 4 5More info: 6 7- `governing_eqn.png` 8- `demo.py` 9- ψ (psi) is the stream function (i.e. surface elevation) 10- Doubling time "between 25 and 50" 11- Note Sakov's trick of increasing RKH2 from 2.0e-12 to 2.0e-11 to stabilize 12 the ensemble integration, which may be necessary for EnKF's with small N. 13 See example in `counillon2009`. 14""" 15 16import sys 17from pathlib import Path 18 19import matplotlib as mpl 20import numpy as np 21 22import dapper.mods as modelling 23import dapper.tools.liveplotting as LP 24 25######################### 26# Model 27######################### 28default_prms = dict( 29 # These parameters may be interesting to change. 30 dtout=5.0, # dt for output to DAPPER. 31 dt=1.25, # dt used internally by Fortran. CFL = 2.0 32 RKB=0, # bottom friction 33 RKH=0, # horizontal friction 34 RKH2=2.0e-12, # horizontal friction, biharmonic 35 F=1600, # Froud number 36 R=1.0e-5, # ≈ Rossby number 37 scheme="'rk4'", # One of (2ndorder, rk4, dp5) 38 # Do not change the following: 39 tend=0, # Only used by standalone QG 40 verbose=0, # Turn off 41 rstart=0, # Restart: switch 42 restartfname="''", # Restart: read file 43 outfname="''", # Restart: write file 44) 45 46 47class model_config: 48 """Define model. 49 50 Helps ensure consistency between prms file (that Fortran module reads) 51 and Python calls to step(), for example for dt. 52 """ 53 54 def __init__(self, name, prms, mp=True): 55 """Use `prms={}` to get the default configuration.""" 56 # Insert prms. Assert key is present in defaults. 57 D = default_prms.copy() 58 for key in prms: 59 assert key in D 60 D[key] = prms[key] 61 62 # Fortran code does not adjust its dt to divide dtout. 63 # Nor is it worth implementing -- just assert: 64 assert D["dtout"] % D["dt"] == 0, "Must be integer multiple" 65 66 self.prms = D 67 self.mp = mp 68 self.name = name 69 self.fname = Path(__file__).parent / "f90" / f"prms_{name}.txt" 70 71 # Create string 72 text = [f" {key.ljust(20)!s} = {D[key]!s}" for key in D] 73 text = ( 74 f"""! Parameter namelist ("{name}") generated via Python 75 ¶meters\n""" 76 + "\n".join(text) 77 + """\n/\n""" 78 ) 79 80 # Write string to file 81 with open(self.fname, "w") as f: 82 f.write(text) 83 84 @property 85 def f90(self): 86 try: 87 from .f90.py_mod import interface_mod 88 89 return interface_mod 90 except ImportError as error: 91 error.msg = error.msg + ( 92 "\nHave you compiled the (Fortran) model?\n" 93 f"See README in {__name__.replace('.', '/')}/f90" 94 ) 95 raise 96 97 def step_1(self, x0, t, dt): 98 """Step a single state vector.""" 99 # Coz fortran.step() reads dt (dtout) from prms file: 100 assert self.prms["dtout"] == dt 101 # Coz Fortran is typed. 102 assert isinstance(t, float) 103 # QG is autonomous (⇒ t should not matter), but Fortran doesn't like nan/inf. 104 assert np.isfinite(t) 105 # Copy coz Fortran will modify in-place. 106 psi = py2f(x0.copy()) 107 # Call Fortran model. 108 self.f90.step(t, psi, self.fname) 109 # Flattening 110 x = f2py(psi) 111 return x 112 113 def step(self, E, t, dt): 114 """Vector and 2D-array (ens) input, with multiproc for ens case.""" 115 if E.ndim == 1: 116 return self.step_1(E, t, dt) 117 if E.ndim == 2: 118 if self.mp: # PARALLELIZED: 119 # Note: the relative overhead for parallelization decreases 120 # as the ratio dtout/dt increases. 121 # But the overhead is already negligible with a ratio of 4. 122 import dapper.tools.multiproc as multiproc 123 124 with multiproc.Pool(self.mp) as pool: 125 E = pool.map(lambda x: self.step_1(x, t=t, dt=dt), E) 126 E = np.array(E) 127 else: # NON-PARALLELIZED: 128 for n, x in enumerate(E): 129 E[n] = self.step_1(x, t, dt) 130 return E 131 132 133######################### 134# Domain management 135######################### 136# Domain size "hardcoded" in f90/parameters.f90. 137 138# "Physical" domain length -- copied from f90/parameters.f90. 139# In my tests, only square domains generate any dynamics of interest. 140NX1 = 2 141NY1 = 2 142# Resolution level -- copied MREFIN from parameters.f90 143res = 7 144# Grid lengths. 145nx = NX1 * 2 ** (res - 1) + 1 # (axis=1) 146ny = NY1 * 2 ** (res - 1) + 1 # (axis=0) 147# Actually, the BCs are psi = nabla psi = nabla^2 psi = 0, 148# => psi should always be zero on the boundries. 149# => it'd be safer to rm boundries from the DA state vector, 150# yielding ndim(state)=(nx-2)*(ny-2), but this is not done here. 151 152# Fortran model (e.g. f90/interface.f90) requires orientation: X[ix,iy]. 153shape = (nx, ny) 154# Passing arrays to/from Fortran requries that flags['F_CONTIGUOUS']==True. 155order = "F" 156 157 158def py2f(x): 159 return x.reshape(shape, order=order) 160 161 162def f2py(X): 163 return X.flatten(order=order) 164 165 166# However, FOR PRINTING/PLOTTING PURPOSES, the y-axis should be vertical 167# [imshow(mat) uses the same orientation as print(mat)]. 168def square(x): 169 return x.reshape(shape[::-1]) 170 171 172def ind2sub(ind): 173 return np.unravel_index(ind, shape[::-1]) 174 175 176######################### 177# Free run 178######################### 179def gen_sample(model, nSamples, SpinUp, Spacing): 180 simulator = modelling.with_recursion(model.step, prog="Simulating") 181 K = SpinUp + nSamples * Spacing 182 Nx = np.prod(shape) # total state length 183 sample = simulator(np.zeros(Nx), K, 0.0, model.prms["dtout"]) 184 return sample[SpinUp::Spacing] 185 186 187sample_filename = modelling.rc.dirs.samples / "QG_samples.npz" 188if (not sample_filename.is_file()) and ("pdoc" not in sys.modules): 189 print( 190 "Did not find sample file", 191 sample_filename, 192 "for experiment initialization. Generating...", 193 ) 194 sample = gen_sample(model_config("sample_generation", {}), 400, 700, 10) 195 np.savez(sample_filename, sample=sample) 196 197 198######################### 199# Liveplotting 200######################### 201cm = mpl.colors.ListedColormap(0.85 * mpl.cm.jet(np.arange(256))) 202center = nx * int(ny / 2) + int(0.5 * nx) 203 204 205def LP_setup(jj): 206 return [ 207 (1, LP.spatial2d(square, ind2sub, jj, cm)), 208 (0, LP.spectral_errors), 209 (0, LP.sliding_marginals(dims=center + np.arange(4))), 210 ]
default_prms =
{'dtout': 5.0, 'dt': 1.25, 'RKB': 0, 'RKH': 0, 'RKH2': 2e-12, 'F': 1600, 'R': 1e-05, 'scheme': "'rk4'", 'tend': 0, 'verbose': 0, 'rstart': 0, 'restartfname': "''", 'outfname': "''"}
class
model_config:
48class model_config: 49 """Define model. 50 51 Helps ensure consistency between prms file (that Fortran module reads) 52 and Python calls to step(), for example for dt. 53 """ 54 55 def __init__(self, name, prms, mp=True): 56 """Use `prms={}` to get the default configuration.""" 57 # Insert prms. Assert key is present in defaults. 58 D = default_prms.copy() 59 for key in prms: 60 assert key in D 61 D[key] = prms[key] 62 63 # Fortran code does not adjust its dt to divide dtout. 64 # Nor is it worth implementing -- just assert: 65 assert D["dtout"] % D["dt"] == 0, "Must be integer multiple" 66 67 self.prms = D 68 self.mp = mp 69 self.name = name 70 self.fname = Path(__file__).parent / "f90" / f"prms_{name}.txt" 71 72 # Create string 73 text = [f" {key.ljust(20)!s} = {D[key]!s}" for key in D] 74 text = ( 75 f"""! Parameter namelist ("{name}") generated via Python 76 ¶meters\n""" 77 + "\n".join(text) 78 + """\n/\n""" 79 ) 80 81 # Write string to file 82 with open(self.fname, "w") as f: 83 f.write(text) 84 85 @property 86 def f90(self): 87 try: 88 from .f90.py_mod import interface_mod 89 90 return interface_mod 91 except ImportError as error: 92 error.msg = error.msg + ( 93 "\nHave you compiled the (Fortran) model?\n" 94 f"See README in {__name__.replace('.', '/')}/f90" 95 ) 96 raise 97 98 def step_1(self, x0, t, dt): 99 """Step a single state vector.""" 100 # Coz fortran.step() reads dt (dtout) from prms file: 101 assert self.prms["dtout"] == dt 102 # Coz Fortran is typed. 103 assert isinstance(t, float) 104 # QG is autonomous (⇒ t should not matter), but Fortran doesn't like nan/inf. 105 assert np.isfinite(t) 106 # Copy coz Fortran will modify in-place. 107 psi = py2f(x0.copy()) 108 # Call Fortran model. 109 self.f90.step(t, psi, self.fname) 110 # Flattening 111 x = f2py(psi) 112 return x 113 114 def step(self, E, t, dt): 115 """Vector and 2D-array (ens) input, with multiproc for ens case.""" 116 if E.ndim == 1: 117 return self.step_1(E, t, dt) 118 if E.ndim == 2: 119 if self.mp: # PARALLELIZED: 120 # Note: the relative overhead for parallelization decreases 121 # as the ratio dtout/dt increases. 122 # But the overhead is already negligible with a ratio of 4. 123 import dapper.tools.multiproc as multiproc 124 125 with multiproc.Pool(self.mp) as pool: 126 E = pool.map(lambda x: self.step_1(x, t=t, dt=dt), E) 127 E = np.array(E) 128 else: # NON-PARALLELIZED: 129 for n, x in enumerate(E): 130 E[n] = self.step_1(x, t, dt) 131 return E
Define model.
Helps ensure consistency between prms file (that Fortran module reads) and Python calls to step(), for example for dt.
model_config(name, prms, mp=True)
55 def __init__(self, name, prms, mp=True): 56 """Use `prms={}` to get the default configuration.""" 57 # Insert prms. Assert key is present in defaults. 58 D = default_prms.copy() 59 for key in prms: 60 assert key in D 61 D[key] = prms[key] 62 63 # Fortran code does not adjust its dt to divide dtout. 64 # Nor is it worth implementing -- just assert: 65 assert D["dtout"] % D["dt"] == 0, "Must be integer multiple" 66 67 self.prms = D 68 self.mp = mp 69 self.name = name 70 self.fname = Path(__file__).parent / "f90" / f"prms_{name}.txt" 71 72 # Create string 73 text = [f" {key.ljust(20)!s} = {D[key]!s}" for key in D] 74 text = ( 75 f"""! Parameter namelist ("{name}") generated via Python 76 ¶meters\n""" 77 + "\n".join(text) 78 + """\n/\n""" 79 ) 80 81 # Write string to file 82 with open(self.fname, "w") as f: 83 f.write(text)
Use prms={}
to get the default configuration.
def
step_1(self, x0, t, dt):
98 def step_1(self, x0, t, dt): 99 """Step a single state vector.""" 100 # Coz fortran.step() reads dt (dtout) from prms file: 101 assert self.prms["dtout"] == dt 102 # Coz Fortran is typed. 103 assert isinstance(t, float) 104 # QG is autonomous (⇒ t should not matter), but Fortran doesn't like nan/inf. 105 assert np.isfinite(t) 106 # Copy coz Fortran will modify in-place. 107 psi = py2f(x0.copy()) 108 # Call Fortran model. 109 self.f90.step(t, psi, self.fname) 110 # Flattening 111 x = f2py(psi) 112 return x
Step a single state vector.
def
step(self, E, t, dt):
114 def step(self, E, t, dt): 115 """Vector and 2D-array (ens) input, with multiproc for ens case.""" 116 if E.ndim == 1: 117 return self.step_1(E, t, dt) 118 if E.ndim == 2: 119 if self.mp: # PARALLELIZED: 120 # Note: the relative overhead for parallelization decreases 121 # as the ratio dtout/dt increases. 122 # But the overhead is already negligible with a ratio of 4. 123 import dapper.tools.multiproc as multiproc 124 125 with multiproc.Pool(self.mp) as pool: 126 E = pool.map(lambda x: self.step_1(x, t=t, dt=dt), E) 127 E = np.array(E) 128 else: # NON-PARALLELIZED: 129 for n, x in enumerate(E): 130 E[n] = self.step_1(x, t, dt) 131 return E
Vector and 2D-array (ens) input, with multiproc for ens case.
NX1 =
2
NY1 =
2
res =
7
nx =
129
ny =
129
shape =
(129, 129)
order =
'F'
def
py2f(x):
def
f2py(X):
def
square(x):
def
ind2sub(ind):
def
gen_sample(model, nSamples, SpinUp, Spacing):
180def gen_sample(model, nSamples, SpinUp, Spacing): 181 simulator = modelling.with_recursion(model.step, prog="Simulating") 182 K = SpinUp + nSamples * Spacing 183 Nx = np.prod(shape) # total state length 184 sample = simulator(np.zeros(Nx), K, 0.0, model.prms["dtout"]) 185 return sample[SpinUp::Spacing]
sample_filename =
PosixPath('/home/runner/dpr_data/samples/QG_samples.npz')
cm =
<matplotlib.colors.ListedColormap object>
center =
8320
def
LP_setup(jj):