Skip to content

Commit ff99561

Browse files
committed
_Geometry handles walls and centers
Previously, grid coordinates were mixed up instead of separated into walls and centers of finite volumes.
1 parent a054632 commit ff99561

File tree

5 files changed

+194
-143
lines changed

5 files changed

+194
-143
lines changed

stagpy/_step.py

Lines changed: 133 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
from collections.abc import Mapping
1010
from collections import namedtuple
1111
from itertools import chain
12-
import re
1312

1413
import numpy as np
1514

@@ -22,97 +21,146 @@ class _Geometry:
2221
2322
It is deduced from the information in the header of binary field files
2423
output by StagYY.
25-
26-
Attributes:
27-
nxtot, nytot, nztot, nttot, nptot, nrtot, nbtot (int): number of grid
28-
point in the various directions. Note that nxtot==nttot,
29-
nytot==nptot, nztot==nrtot.
30-
x_coord, y_coord, z_coord, t_coord, p_coord, r_coord (numpy.array):
31-
positions of cell centers in the various directions. Note that
32-
x_coord==t_coord, y_coord==p_coord, z_coord==r_coord.
33-
x_mesh, y_mesh, z_mesh, t_mesh, p_mesh, r_mesh (numpy.array):
34-
mesh in cartesian and curvilinear frames. The last three are
35-
not defined if the geometry is cartesian.
3624
"""
3725

38-
_regexes = (re.compile(r'^n([xyztprb])tot$'), # ntot
39-
re.compile(r'^([xyztpr])_coord$'), # coord
40-
re.compile(r'^([xyz])_mesh$'), # cartesian mesh
41-
re.compile(r'^([tpr])_mesh$')) # curvilinear mesh
42-
43-
def __init__(self, header, par):
26+
def __init__(self, header, step):
4427
self._header = header
45-
self._par = par
46-
self._coords = None
47-
self._cart_meshes = None
48-
self._curv_meshes = None
28+
self._step = step
4929
self._shape = {'sph': False, 'cyl': False, 'axi': False,
5030
'ntot': list(header['nts']) + [header['ntb']]}
5131
self._init_shape()
5232

53-
self._coords = [header['e1_coord'],
54-
header['e2_coord'],
55-
header['e3_coord']]
56-
57-
# instead of adding horizontal rows, should construct two grids:
58-
# - center of cells coordinates (the current one);
59-
# - vertices coordinates on which vector fields are determined,
60-
# which geometrically contains one more row.
61-
62-
# add theta, phi / x, y row to have a continuous field
63-
if not self.twod_yz:
64-
self._coords[0] = np.append(
65-
self.t_coord,
66-
self.t_coord[-1] + self.t_coord[1] - self.t_coord[0])
67-
if not self.twod_xz:
68-
self._coords[1] = np.append(
69-
self.p_coord,
70-
self.p_coord[-1] + self.p_coord[1] - self.p_coord[0])
71-
72-
if self.cartesian:
73-
self._cart_meshes = np.meshgrid(self.x_coord, self.y_coord,
74-
self.z_coord, indexing='ij')
75-
self._curv_meshes = (None, None, None)
33+
def _scale_radius_mo(self, radius):
34+
"""Rescale radius for MO runs."""
35+
if self._step.sdat.par['magma_oceans_in']['magma_oceans_mode']:
36+
return self._header['mo_thick_sol'] * (
37+
radius + self._header['mo_lambda'])
38+
return radius
39+
40+
@crop
41+
def nttot(self):
42+
"""Number of grid point along the x/theta direction."""
43+
return self._shape['ntot'][0]
44+
45+
@crop
46+
def nptot(self):
47+
"""Number of grid point along the y/phi direction."""
48+
return self._shape['ntot'][1]
49+
50+
@crop
51+
def nrtot(self):
52+
"""Number of grid point along the z/r direction."""
53+
return self._shape['ntot'][2]
54+
55+
@crop
56+
def nbtot(self):
57+
"""Number of blocks."""
58+
return self._shape['ntot'][3]
59+
60+
nxtot = nttot
61+
nytot = nptot
62+
nztot = nrtot
63+
64+
@crop
65+
def r_walls(self):
66+
"""Position of FV walls along the z/r direction."""
67+
rgeom = self._header.get("rgeom")
68+
if rgeom is not None:
69+
walls = rgeom[:, 0] + self.rcmb
70+
else:
71+
walls = self._header["e3_coord"] + self.rcmb
72+
walls.append(self._step.rprofs.bounds[1])
73+
return self._scale_radius_mo(walls)
74+
75+
@crop
76+
def r_centers(self):
77+
"""Position of FV centers along the z/r direction."""
78+
rgeom = self._header.get("rgeom")
79+
if rgeom is not None:
80+
walls = rgeom[:-1, 1] + self.rcmb
7681
else:
77-
if self.twod_yz:
78-
self._coords[0] = np.array(np.pi / 2)
79-
elif self.twod_xz:
80-
self._coords[1] = np.array(0)
81-
self._coords[2] += self.rcmb
82-
if par['magma_oceans_in']['magma_oceans_mode']:
83-
self._coords[2] += header['mo_lambda']
84-
self._coords[2] *= header['mo_thick_sol']
85-
t_mesh, p_mesh, r_mesh = np.meshgrid(
86-
self.t_coord, self.p_coord, self.r_coord, indexing='ij')
87-
# compute cartesian coordinates
88-
# z along rotation axis at theta=0
89-
# x at th=90, phi=0
90-
# y at th=90, phi=90
91-
x_mesh = r_mesh * np.cos(p_mesh) * np.sin(t_mesh)
92-
y_mesh = r_mesh * np.sin(p_mesh) * np.sin(t_mesh)
93-
z_mesh = r_mesh * np.cos(t_mesh)
94-
self._cart_meshes = (x_mesh, y_mesh, z_mesh)
95-
self._curv_meshes = (t_mesh, p_mesh, r_mesh)
82+
walls = self._step.rprofs.centers
83+
return self._scale_radius_mo(walls)
84+
85+
@crop
86+
def t_walls(self):
87+
"""Position of FV walls along x/theta."""
88+
if self.threed or self.twod_xz:
89+
if self.yinyang:
90+
tmin, tmax = -np.pi / 4, np.pi / 4
91+
elif self.curvilinear:
92+
# should take theta_position/theta_center into account
93+
tmin = 0
94+
tmax = min(np.pi,
95+
self._step.sdat.par['geometry']['aspect_ratio'][0])
96+
else:
97+
tmin = 0
98+
tmax = self._step.sdat.par['geometry']['aspect_ratio'][0]
99+
return np.linspace(tmin, tmax, self.nttot + 1)
100+
# twoD YZ
101+
center = np.pi / 2 if self.curvilinear else 0
102+
d_t = (self.p_walls[1] - self.p_walls[0]) / 2
103+
return np.array([center - d_t, center + d_t])
104+
105+
@crop
106+
def t_centers(self):
107+
"""Position of FV centers along x/theta."""
108+
return (self.t_walls[:-1] + self.t_walls[1:]) / 2
109+
110+
@crop
111+
def p_walls(self):
112+
"""Position of FV walls along y/phi."""
113+
if self.threed or self.twod_yz:
114+
if self.yinyang:
115+
pmin, pmax = -3 * np.pi / 4, 3 * np.pi / 4
116+
elif self.curvilinear:
117+
pmin = 0
118+
pmax = min(2 * np.pi,
119+
self._step.sdat.par['geometry']['aspect_ratio'][1])
120+
else:
121+
pmin = 0
122+
pmax = self._step.sdat.par['geometry']['aspect_ratio'][1]
123+
return np.linspace(pmin, pmax, self.nptot + 1)
124+
# twoD YZ
125+
d_p = (self.t_walls[1] - self.t_walls[0]) / 2
126+
return np.array([-d_p, d_p])
127+
128+
@crop
129+
def p_centers(self):
130+
"""Position of FV centers along y/phi."""
131+
return (self.p_walls[:-1] + self.p_walls[1:]) / 2
132+
133+
z_walls = r_walls
134+
z_centers = r_centers
135+
x_walls = t_walls
136+
x_centers = t_centers
137+
y_walls = p_walls
138+
y_centers = p_centers
96139

97140
def _init_shape(self):
98141
"""Determine shape of geometry."""
99-
shape = self._par['geometry']['shape'].lower()
142+
shape = self._step.sdat.par['geometry']['shape'].lower()
100143
aspect = self._header['aspect']
101-
if self.rcmb is not None and self.rcmb >= 0:
144+
if self._header['rcmb'] is not None and self._header['rcmb'] >= 0:
102145
# curvilinear
103146
self._shape['cyl'] = self.twod_xz and (shape == 'cylindrical' or
104147
aspect[0] >= np.pi)
105148
self._shape['sph'] = not self._shape['cyl']
106-
elif self.rcmb is None:
107-
self._header['rcmb'] = self._par['geometry']['r_cmb']
108-
if self.rcmb >= 0:
149+
elif self._header['rcmb'] is None:
150+
self._header['rcmb'] = self._step.sdat.par['geometry']['r_cmb']
151+
if self._header['rcmb'] >= 0:
109152
if self.twod_xz and shape == 'cylindrical':
110153
self._shape['cyl'] = True
111154
elif shape == 'spherical':
112155
self._shape['sph'] = True
113156
self._shape['axi'] = self.cartesian and self.twod_xz and \
114157
shape == 'axisymmetric'
115158

159+
@crop
160+
def rcmb(self):
161+
"""Radius of CMB, 0 in cartesian geometry."""
162+
return max(self._header["rcmb"], 0)
163+
116164
@property
117165
def cartesian(self):
118166
"""Whether the grid is in cartesian geometry."""
@@ -166,26 +214,14 @@ def at_z(self, zval):
166214
"""
167215
if self.curvilinear:
168216
zval += self.rcmb
169-
return np.argmin(np.abs(self.z_coord - zval))
217+
return np.argmin(np.abs(self.z_centers - zval))
170218

171219
def at_r(self, rval):
172220
"""Return ir closest to given rval position.
173221
174222
If called in cartesian geometry, this is equivalent to :meth:`at_z`.
175223
"""
176-
return np.argmin(np.abs(self.r_coord - rval))
177-
178-
def __getattr__(self, attr):
179-
# provide nDtot, D_coord, D_mesh and nbtot
180-
# with D = x, y, z or t, p, r
181-
for reg, dat in zip(self._regexes, (self._shape['ntot'],
182-
self._coords,
183-
self._cart_meshes,
184-
self._curv_meshes)):
185-
match = reg.match(attr)
186-
if match is not None:
187-
return dat['xtypzrb'.index(match.group(1)) // 2]
188-
return self._header[attr]
224+
return np.argmin(np.abs(self.r_centers - rval))
189225

190226

191227
class _Fields(Mapping):
@@ -226,13 +262,6 @@ def __getitem__(self, name):
226262
header, fields = parsed_data
227263
self._cropped__header = header
228264
for fld_name, fld in zip(fld_names, fields):
229-
if self._header['xyp'] == 0:
230-
if not self.geom.twod_yz:
231-
newline = (fld[:1, ...] + fld[-1:, ...]) / 2
232-
fld = np.concatenate((fld, newline), axis=0)
233-
if not self.geom.twod_xz:
234-
newline = (fld[:, :1, ...] + fld[:, -1:, ...]) / 2
235-
fld = np.concatenate((fld, newline), axis=1)
236265
self._set(fld_name, fld)
237266
return self._data[name]
238267

@@ -317,7 +346,7 @@ def geom(self):
317346
None if not available for this time step.
318347
"""
319348
if self._header is not None:
320-
return _Geometry(self._header, self.step.sdat.par)
349+
return _Geometry(self._header, self.step)
321350

322351

323352
class _Tracers:
@@ -430,12 +459,15 @@ def centers(self):
430459
def walls(self):
431460
"""Radial position of cell walls."""
432461
rbot, rtop = self.bounds
433-
centers = self.centers
434-
# assume walls are mid-way between T-nodes
435-
# could be T-nodes at center between walls
436-
walls = (centers[:-1] + centers[1:]) / 2
437-
walls = np.insert(walls, 0, rbot)
438-
walls = np.append(walls, rtop)
462+
try:
463+
walls = self.step.fields.geom.r_walls
464+
except error.StagpyError:
465+
# assume walls are mid-way between T-nodes
466+
# could be T-nodes at center between walls
467+
centers = self.centers
468+
walls = (centers[:-1] + centers[1:]) / 2
469+
walls = np.insert(walls, 0, rbot)
470+
walls = np.append(walls, rtop)
439471
return walls
440472

441473
@crop
@@ -451,8 +483,10 @@ def bounds(self):
451483
rcmb = step.sdat.par['geometry']['r_cmb']
452484
if step.sdat.par['geometry']['shape'].lower() == 'cartesian':
453485
rcmb = 0
454-
rcmb = max(rcmb, 0)
455-
return rcmb, rcmb + 1
486+
rbot = max(rcmb, 0)
487+
thickness = (step.sdat.scales.length
488+
if step.sdat.par['switches']['dimensional_units'] else 1)
489+
return rbot, rbot + thickness
456490

457491

458492
class Step:

0 commit comments

Comments
 (0)