99from collections .abc import Mapping
1010from collections import namedtuple
1111from itertools import chain
12- import re
1312
1413import 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
191227class _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
323352class _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
458492class Step :
0 commit comments