1313# limitations under the License.
1414"""Helper for finding safety factor outputs."""
1515import dataclasses
16- import functools
1716
18- import chex
1917import jax
2018from jax import numpy as jnp
2119from torax ._src import array_typing
2422@jax .tree_util .register_dataclass
2523@dataclasses .dataclass (frozen = True )
2624class SafetyFactorFit :
27- """Collection of outputs calculated after each simulation step.
25+ """Collection of statistics of safety factor calculated each simulation step.
2826
2927 Attributes:
3028 rho_q_min: rho_norm at the minimum q.
@@ -53,115 +51,41 @@ class SafetyFactorFit:
5351 rho_q_3_1_second : array_typing .FloatScalar
5452
5553
56- def _sliding_window_of_three (flat_array : jax .Array ) -> jax .Array :
57- """Sliding window equivalent to numpy.lib.stride_tricks.sliding_window."""
58- window_size = 3
59- starts = jnp .arange (len (flat_array ) - window_size + 1 )
60- return jax .vmap (
61- lambda start : jax .lax .dynamic_slice (flat_array , (start ,), (window_size ,))
62- )(starts )
63-
54+ def _linear_intercepts (
55+ rho_norm : jax .Array , q_face : jax .Array , q_target : float
56+ ) -> jax .Array :
57+ """Finds intercepts of q_face with q_target using linear interpolation.
6458
65- def _fit_polynomial_to_intervals_of_three (
66- rho_norm : jax .Array , q_face : jax .Array
67- ) -> tuple [jax .Array , jax .Array , jax .Array ]:
68- """Fit polynomial to every set of three points in given arrays."""
69- q_face_intervals = _sliding_window_of_three (
70- q_face ,
71- )
72- rho_norm_intervals = _sliding_window_of_three (
73- rho_norm ,
74- )
59+ Args:
60+ rho_norm: Array of rho_norm values on face grid.
61+ q_face: Array of q values on face grid.
62+ q_target: q value to find intercepts for.
7563
76- @jax .vmap
77- def batch_polyfit (
78- q_face_interval : jax .Array , rho_norm_interval : jax .Array
79- ) -> jax .Array :
80- """Solve Ax=b to get coefficients for a quadratic polynomial."""
81- chex .assert_shape (q_face_interval , (3 ,))
82- chex .assert_shape (rho_norm_interval , (3 ,))
83- rho_norm_squared = rho_norm_interval ** 2
84- A = jnp .array ([ # pylint: disable=invalid-name
85- [rho_norm_squared [0 ], rho_norm_interval [0 ], 1 ],
86- [rho_norm_squared [1 ], rho_norm_interval [1 ], 1 ],
87- [rho_norm_squared [2 ], rho_norm_interval [2 ], 1 ],
88- ])
89- b = jnp .array ([q_face_interval [0 ], q_face_interval [1 ], q_face_interval [2 ]])
90- coeffs = jnp .linalg .solve (A , b )
91- return coeffs
92-
93- return (
94- batch_polyfit (q_face_intervals , rho_norm_intervals ),
95- rho_norm_intervals ,
96- q_face_intervals ,
97- )
64+ Returns:
65+ Array of rho_norm values of (potential) intercepts in each interval. If no
66+ intercept is found in an interval, -jnp.inf is placed in the array.
67+ """
68+ q_diff = q_face [1 :] - q_face [:- 1 ]
69+ safe_q_diff = jnp .where (q_diff == 0.0 , 1.0 , q_diff )
70+ t = (q_target - q_face [:- 1 ]) / safe_q_diff
9871
72+ # If q_diff is 0 and q_face[:-1] == q_target, we force t = 1.0 (intercept at
73+ # rho_norm[i+1])
74+ t = jnp .where ((q_diff == 0.0 ) & (q_face [:- 1 ] == q_target ), 1.0 , t )
9975
100- @jax .vmap
101- def _minimum_location_value_in_interval (
102- coeffs : jax .Array , rho_norm_interval : jax .Array , q_interval : jax .Array
103- ) -> tuple [jax .Array , jax .Array ]:
104- """Returns the minimum value and location of the fit quadratic in the interval."""
105- min_interval , max_interval = rho_norm_interval [0 ], rho_norm_interval [1 ]
106- q_min_interval , q_max_interval = (
107- q_interval [0 ],
108- q_interval [1 ],
109- )
110- a , b = coeffs [0 ], coeffs [1 ]
111- extremum_location = - b / (2 * a )
112- extremum_in_interval = jnp .greater (
113- extremum_location , min_interval
114- ) & jnp .less (extremum_location , max_interval )
115- extremum_value = jax .lax .cond (
116- extremum_in_interval ,
117- lambda x : jnp .polyval (coeffs , x ),
118- lambda x : jnp .inf ,
119- extremum_location ,
120- )
76+ is_intercept = (t > 0.0 ) & (t <= 1.0 )
77+ # Mask out cases where q_diff == 0 and q_face[:-1] != q_target
78+ is_intercept = is_intercept & ~ ((q_diff == 0.0 ) & (q_face [:- 1 ] != q_target ))
12179
122- interval_minimum_location , interval_minimum_value = jax .lax .cond (
123- jnp .less (q_min_interval , q_max_interval ),
124- lambda : (min_interval , q_min_interval ),
125- lambda : (max_interval , q_max_interval ),
126- )
127- overall_minimum_location , overall_minimum_value = jax .lax .cond (
128- jnp .less (interval_minimum_value , extremum_value ),
129- lambda : (interval_minimum_location , interval_minimum_value ),
130- lambda : (extremum_location , extremum_value ),
131- )
132- return overall_minimum_location , overall_minimum_value
133-
134-
135- def _find_roots_quadratic (coeffs : jax .Array ) -> jax .Array :
136- """Finds the roots of a quadratic equation."""
137- a , b , c = coeffs [0 ], coeffs [1 ], coeffs [2 ]
138- determinant = b ** 2 - 4.0 * a * c
139- roots_exist = jnp .greater (determinant , 0 )
140- plus_root = jax .lax .cond (
141- roots_exist ,
142- lambda : (- b + jnp .sqrt (determinant )) / (2.0 * a ),
143- lambda : - jnp .inf ,
144- )
145- minus_root = jax .lax .cond (
146- roots_exist ,
147- lambda : (- b - jnp .sqrt (determinant )) / (2.0 * a ),
148- lambda : - jnp .inf ,
149- )
150- return jnp .array ([plus_root , minus_root ])
80+ intercepts = rho_norm [:- 1 ] + t * (rho_norm [1 :] - rho_norm [:- 1 ])
15181
82+ all_intercepts = jnp .where (is_intercept , intercepts , - jnp .inf )
15283
153- @functools .partial (jax .vmap , in_axes = (0 , 0 , None ))
154- def _root_in_interval (
155- coeffs : jax .Array , interval : jax .Array , q_surface : float
156- ) -> jax .Array :
157- """Finds roots of quadratic(coeffs)=q_surface in the interval."""
158- intercept_coeffs = coeffs - jnp .array ([0.0 , 0.0 , q_surface ])
159- min_interval , max_interval = interval [0 ], interval [1 ]
160- root_values = _find_roots_quadratic (intercept_coeffs )
161- in_interval = jnp .greater (root_values , min_interval ) & jnp .less (
162- root_values , max_interval
84+ first_point_intercept = jnp .where (
85+ q_face [0 ] == q_target , rho_norm [0 ], - jnp .inf
16386 )
164- return jnp .where (in_interval , root_values , - jnp .inf )
87+
88+ return jnp .concat ([jnp .array ([first_point_intercept ]), all_intercepts ])
16589
16690
16791@jax .jit
@@ -170,33 +94,16 @@ def find_min_q_and_q_surface_intercepts(
17094) -> SafetyFactorFit :
17195 """Finds the minimum q and the q surface intercepts.
17296
173- This method divides the input arrays into groups of three points, fits a
174- quadratic to each interval containing three points. It then uses the fit
175- quadratics to find the minimum q value as well as any intercepts of the q=3/2,
176- q=2/1, and q=3/1 planes.
177-
178- As the quadratic fits could have overlapping definitions where the groups
179- overlap, we choose the quadratic for the domain [rho_norm[i-1], rho_norm[i]]
180- defined using the points (i, i-1, i-2).
181- (There is one exception which is the first group for which the domain for
182- points in [rho_norm[0], rho_norm[2]] are defined using points (0, 1, 2))
97+ This method uses the input arrays to find the minimum q value and its
98+ corresponding rho_norm. It then uses linear interpolation between adjacent
99+ points to find any intercepts of the q=3/2, q=2/1, and q=3/1 planes.
183100
184101 Args:
185102 rho_norm: Array of rho_norm values on face grid.
186103 q_face: Array of q values on face grid.
187104
188105 Returns:
189- rho_q_min: rho_norm at the minimum q.
190- q_min: minimum q value.
191- outermost_rho_q_3_2: Array of two outermost rho_norm values that intercept
192- the q=3/2 plane. If fewer than two intercepts are found, entries will be set
193- to -inf.
194- outermost_rho_q_2_1: Array of 2 outermost rho_norm values that intercept the
195- q=2/1 plane. If fewer than two intercepts are found, entries will be set to
196- -inf.
197- outermost_rho_q_3_1: Array of 2 outermost rho_norm values that intercept the
198- q=3/1 plane. If fewer than two intercepts are found, entries will be set to
199- -inf.
106+ Safety factor statistics.
200107 """
201108 if len (q_face ) != len (rho_norm ):
202109 raise ValueError (
@@ -209,36 +116,21 @@ def find_min_q_and_q_surface_intercepts(
209116 sorted_indices = jnp .argsort (rho_norm )
210117 rho_norm = rho_norm [sorted_indices ]
211118 q_face = q_face [sorted_indices ]
212- # Fit polynomial to every set of three points in given arrays.
213- poly_coeffs , rho_norm_3 , q_face_3 = _fit_polynomial_to_intervals_of_three (
214- rho_norm , q_face
215- )
216- # To bridge across the entire span of rho_norm, and avoid overlapping
217- # interval domains, we define the first group from points (0, 2), and all
218- # subsequent groups from (i+1, i+2), from i=1 until i=len(rho_norm)-1.
219- first_rho_norm = jnp .expand_dims (
220- jnp .array ([rho_norm [0 ], rho_norm [2 ]]), axis = 0
221- )
222- first_q_face = jnp .expand_dims (jnp .array ([q_face [0 ], q_face [2 ]]), axis = 0 )
223- rho_norms = jnp .concat ([first_rho_norm , rho_norm_3 [1 :, 1 :]], axis = 0 )
224- q_faces = jnp .concat ([first_q_face , q_face_3 [1 :, 1 :]], axis = 0 )
225119
226- # Find the minimum q value and its location for each interval.
227- rho_q_min_intervals , q_min_intervals = _minimum_location_value_in_interval (
228- poly_coeffs , rho_norms , q_faces
229- )
230- # Find the minimum q value and its location out of all intervals.
231- arg_q_min = jnp .argmin (q_min_intervals )
232- rho_q_min = rho_q_min_intervals [arg_q_min ]
233- q_min = q_min_intervals [arg_q_min ]
120+ # Find the minimum q value and its location.
121+ idx_min = jnp .argmin (q_face )
122+ q_min = q_face [idx_min ]
123+ rho_q_min = rho_norm [idx_min ]
234124
235125 # Find the outermost rho_norm values that intercept the q=3/2, q=2/1, and
236126 # q=3/1 planes. If none are found, fill from the left with -jnp.inf.
237- rho_q_3_2 = _root_in_interval ( poly_coeffs , rho_norms , 1.5 ). flatten ( )
127+ rho_q_3_2 = _linear_intercepts ( rho_norm , q_face , 1.5 )
238128 outermost_rho_q_3_2 = rho_q_3_2 [jnp .argsort (rho_q_3_2 )[- 2 :]]
239- rho_q_2_1 = _root_in_interval (poly_coeffs , rho_norms , 2.0 ).flatten ()
129+
130+ rho_q_2_1 = _linear_intercepts (rho_norm , q_face , 2.0 )
240131 outermost_rho_q_2_1 = rho_q_2_1 [jnp .argsort (rho_q_2_1 )[- 2 :]]
241- rho_q_3_1 = _root_in_interval (poly_coeffs , rho_norms , 3.0 ).flatten ()
132+
133+ rho_q_3_1 = _linear_intercepts (rho_norm , q_face , 3.0 )
242134 outermost_rho_q_3_1 = rho_q_3_1 [jnp .argsort (rho_q_3_1 )[- 2 :]]
243135
244136 return SafetyFactorFit (
0 commit comments