44from warnings import warn
55
66
7- def rz_to_ab (rz , mesh ):
7+ class OutOfDomainError (ValueError ):
8+ pass
9+
10+
11+ def _rz_to_ab (rz , mesh ):
12+ """
13+ This functions finds the position (R-z) in the mesh and
14+ returns the index and the relative coordinates [0,1] in that cell.
15+ It uses a newton iteration.
16+ """
817 ij = mesh .find_cell (rz )
9- assert ij >= 0 , "We left the domain"
18+ if ij < 0 :
19+ raise OutOfDomainError ()
1020 _ , nz = mesh .shape
1121 i , j = ij // nz , ij % nz
1222 ABCD = mesh .grid [i : i + 2 , j : j + 2 ]
@@ -17,10 +27,12 @@ def rz_to_ab(rz, mesh):
1727 rz0 = rz - A
1828
1929 def fun (albe ):
30+ "The forwards function"
2031 al , be = albe
2132 return rz0 - a * al - b * be - c * al * be
2233
2334 def J (albe ):
35+ "The jacobian"
2436 al , be = albe
2537 return np .array ([- a - c * be , - b - c * al ])
2638
@@ -33,7 +45,12 @@ def J(albe):
3345 return albe , ij
3446
3547
36- def ab_to_rz (ab , ij , mesh ):
48+ def _ab_to_rz (ab , ij , mesh ):
49+ """
50+ Calculate the position in cartesian R-z coordinates
51+ given the relative position in the grid cell (ab) and
52+ the grid indices ij.
53+ """
3754 _ , nz = mesh .shape
3855 i , j = ij // nz , ij % nz
3956 A = mesh .grid [i , j ]
@@ -44,18 +61,30 @@ def ab_to_rz(ab, ij, mesh):
4461 return A + al * a + be * b + al * be * c
4562
4663
47- def setup_mesh (x , y ):
48- def per (d ):
64+ def _setup_mesh (x , y ):
65+ """
66+ Setup the mesh and store some additional info.
67+
68+ The fci-mesh is assumed to periodic in z - but eudist does not
69+ handle this so we need to copy the first corners around.
70+ """
71+
72+ def make_periodic (d ):
73+ "The grid should be periodic in z"
4974 return np .concatenate ((d , d [:, :1 ]), axis = 1 )
5075
5176 assert x .dims == y .dims
5277 assert x .dims == ("x" , "z" )
53- x = per (x .data )
54- y = per (y .data )
55- return mymesh (x , y )
78+ x = make_periodic (x .data )
79+ y = make_periodic (y .data )
80+ return _MyMesh (x , y )
5681
5782
58- class mymesh (eudist .PolyMesh ):
83+ class _MyMesh (eudist .PolyMesh ):
84+ """
85+ Like the PolyMesh but with extra data
86+ """
87+
5988 def __init__ (self , x , y ):
6089 super ().__init__ ()
6190 self .r = x
@@ -65,20 +94,56 @@ def __init__(self, x, y):
6594
6695
6796class Tracer :
97+ """
98+ Use an EMC3-like tracing. This relies on the grid containing a
99+ tracing to the next slice. The current position in RZ coordinates
100+ is converted to the relative position in the grid, and then the
101+ reverse is done for the end of the "flux tube" defined by corners
102+ of the cells.
103+ """
104+
68105 def __init__ (self , ds , direction = "forward" ):
106+ """
107+ ds: xr.Dataset
108+ a dataset with the needed FCI data from zoidberg.
109+
110+ direction: str
111+ "forward" or "backward"
112+ """
69113 meshes = []
70114 for yi in range (len (ds .y )):
71115 dsi = ds .isel (y = yi )
72116 meshes .append (
73117 [
74- setup_mesh (dsi [f"{ pre } R" ], dsi [f"{ pre } Z" ])
118+ _setup_mesh (dsi [f"{ pre } R" ], dsi [f"{ pre } Z" ])
75119 for pre in ["" , f"{ direction } _" ]
76120 ]
77121 + [yi ]
78122 )
79123 self .meshes = meshes
80124
81125 def poincare (self , rz , yind = 0 , num = 100 , early_exit = "warn" ):
126+ """
127+ rz : array-like with 2 values
128+ The RZ position where to start tracing
129+ yind : int
130+ The y-index of the slice where to start tracing.
131+ num : int
132+ Number of rounds to trace for
133+ early_exit : str
134+ How to handle if we leave the domain before doing `num` rounds.
135+ The possible values are:
136+ "ignore" : do nothing
137+ "warn": raise a warning
138+ "plot" : try to plot the grid and where we leave
139+ "raise" : Raise the exception for handling by the caller
140+
141+ Returns
142+ -------
143+ np.array
144+ An array of shape (num, 2) or less then num if the tracing leaves the domain.
145+ It contains the r-z coordinates at the y-index where tracing started.
146+ """
82147 rz = np .array (rz )
83148 assert rz .shape == (2 ,)
84149 thismeshes = self .meshes [yind :] + self .meshes [:yind ]
@@ -88,7 +153,7 @@ def poincare(self, rz, yind=0, num=100, early_exit="warn"):
88153 for i in range (1 , num ):
89154 for d , meshes in enumerate (thismeshes ):
90155 try :
91- abij = rz_to_ab (rz , meshes [0 ])
156+ abij = _rz_to_ab (rz , meshes [0 ])
92157 except AssertionError as e :
93158 if early_exit == "warn" :
94159 warn (f"early exit in iteration { i } because `{ e } `" )
@@ -105,11 +170,13 @@ def poincare(self, rz, yind=0, num=100, early_exit="warn"):
105170 elif early_exit == "raise" :
106171 raise
107172 else :
108- assert (
109- early_exit == "ignore"
110- ), f'early_exit needs to be one of ["warn", "plot", "raise", ignore"] but got `{ early_exit } `'
173+ assert early_exit == "ignore" , (
174+ "early_exit needs to be one of "
175+ + '["warn", "plot", "raise", ignore"] '
176+ + f"but got `{ early_exit } `"
177+ )
111178 return out [:i ]
112- rz = ab_to_rz (* abij , meshes [1 ])
179+ rz = _ab_to_rz (* abij , meshes [1 ])
113180 last = meshes
114181 out [i ] = rz
115182 return out
0 commit comments