77from autoarray .structures .triangles .abstract import HEIGHT_FACTOR
88from autoarray .structures .triangles .abstract import AbstractTriangles
99from autoarray .structures .triangles .array import ArrayTriangles
10+ from autoarray .structures .triangles .array import Step0Layout
1011
1112
1213def _lattice_coordinates (
@@ -83,24 +84,118 @@ def static_vertex_table(
8384 axis = 1 ,
8485 )
8586
87+ keys = _vertex_keys (coordinates )
88+
89+ _ , first , inverse = np .unique (keys , axis = 0 , return_index = True , return_inverse = True )
90+
91+ vertices = np .ascontiguousarray (triangles .reshape (- 1 , 2 )[first ], dtype = np .float64 )
92+ indices = np .ascontiguousarray (inverse .reshape (- 1 , 3 ))
93+
94+ vertices .setflags (write = False )
95+ indices .setflags (write = False )
96+
97+ return vertices , indices
98+
99+
100+ def _vertex_keys (coordinates : np .ndarray ) -> np .ndarray :
101+ """
102+ The exact integer lattice key of every ``(3N)`` vertex slot, ordered as
103+ `static_vertex_table` orders them (see its docstring).
104+ """
105+ flip = np .where ((coordinates [:, 0 ] + coordinates [:, 1 ]) % 2 != 0 , - 1 , 1 )[:, None ]
86106 offsets = np .array ([[0 , 1 ], [1 , - 1 ], [- 1 , - 1 ]])
87- keys = np .stack (
107+ return np .stack (
88108 (
89109 coordinates [:, None , 0 ] + flip * offsets [None , :, 0 ],
90110 2 * coordinates [:, None , 1 ] + flip * offsets [None , :, 1 ],
91111 ),
92112 axis = - 1 ,
93113 ).reshape (- 1 , 2 )
94114
95- _ , first , inverse = np .unique (keys , axis = 0 , return_index = True , return_inverse = True )
96115
97- vertices = np .ascontiguousarray (triangles .reshape (- 1 , 2 )[first ], dtype = np .float64 )
98- indices = np .ascontiguousarray (inverse .reshape (- 1 , 3 ))
116+ @lru_cache (maxsize = 32 )
117+ def static_lattice_layout (
118+ y_min : float , y_max : float , x_min : float , x_max : float , scale : float
119+ ) -> Step0Layout :
120+ """
121+ The closed-form layout of `static_vertex_table`'s index map, so the step-0 containment can
122+ read each triangle's vertices by strided slicing instead of a gather (PyAutoArray#579).
123+
124+ The lattice's triangles are row-major, ``n_rows x n_cols``. The vertex table is sorted on the
125+ integer key ``(ky, kx)``; key rows alternate between two widths ``W0, W1`` (equal when the
126+ lattice has an odd number of columns), so the ``(V, 2)`` table -- padded by one row when the
127+ number of key rows is odd -- is a ``(n_pairs, W0 + W1, 2)`` grid of row pairs. Splitting the
128+ triangles into the four parity classes of their (row, column) index fixes each class's flip
129+ and the parity of every vertex's key row, so vertex ``k`` of the class's triangle ``(a, b)``
130+ is grid entry ``(p0 + a, c0 + b)``: one contiguous 2-D slice per class and vertex.
131+
132+ The layout is derived from, and checked element-wise against, the actual index map in NumPy:
133+ if any class did not follow the strided pattern, ``grid`` is ``None`` and the structured
134+ route is not used for this geometry. Built once per geometry (cached) and made only of Python
135+ ints, so it is hashable pytree aux data.
136+ """
137+ vertices , indices = static_vertex_table (y_min , y_max , x_min , x_max , scale )
138+ coordinates = _lattice_coordinates (y_min , y_max , x_min , x_max , scale )
99139
100- vertices .setflags (write = False )
101- indices .setflags (write = False )
140+ ys = np .unique (coordinates [:, 0 ])
141+ xs = np .unique (coordinates [:, 1 ])
142+ n_rows , n_cols = ys .shape [0 ], xs .shape [0 ]
102143
103- return vertices , indices
144+ grid_coordinates = np .stack (np .meshgrid (ys , xs , indexing = "ij" ), axis = - 1 )
145+ if coordinates .shape [0 ] != n_rows * n_cols or not np .array_equal (
146+ coordinates , grid_coordinates .reshape (- 1 , 2 )
147+ ):
148+ return Step0Layout (n_rows = 0 , n_cols = 0 , grid = None )
149+
150+ unique_keys = np .unique (_vertex_keys (coordinates ), axis = 0 )
151+ _ , row_counts = np .unique (unique_keys [:, 0 ], return_counts = True )
152+ R = row_counts .shape [0 ]
153+ W0 = int (row_counts [0 ])
154+ W1 = int (row_counts [1 ]) if R > 1 else W0
155+ if (
156+ unique_keys .shape [0 ] != vertices .shape [0 ]
157+ or np .any (row_counts [0 ::2 ] != W0 )
158+ or np .any (row_counts [1 ::2 ] != W1 )
159+ ):
160+ return Step0Layout (n_rows = n_rows , n_cols = n_cols , grid = None )
161+
162+ # Key rows come in pairs of widths (W0, W1). Viewing the table as (n_pairs, W0 + W1, 2) --
163+ # padded by one row when R is odd -- puts vertex (row r, column c) at
164+ # (r // 2, (r % 2) * W0 + c).
165+ pair_width = W0 + W1
166+ n_pairs = (R + 1 ) // 2
167+ row_start = np .concatenate (([0 ], np .cumsum (row_counts )[:- 1 ]))
168+ vertex_key_row = np .repeat (np .arange (R ), row_counts )
169+ vertex_key_col = np .arange (vertices .shape [0 ]) - row_start [vertex_key_row ]
170+ vertex_pair = (vertex_key_row // 2 )[indices ].reshape (n_rows , n_cols , 3 )
171+ vertex_offset = ((vertex_key_row % 2 ) * W0 + vertex_key_col )[indices ].reshape (
172+ n_rows , n_cols , 3
173+ )
174+
175+ corners = []
176+ for pi in (0 , 1 ):
177+ for pj in (0 , 1 ):
178+ corner = []
179+ for k in range (3 ):
180+ class_pair = vertex_pair [pi ::2 , pj ::2 , k ]
181+ class_offset = vertex_offset [pi ::2 , pj ::2 , k ]
182+ na , nb = class_pair .shape
183+ p0 , c0 = int (class_pair [0 , 0 ]), int (class_offset [0 , 0 ])
184+ expected_pair = p0 + np .arange (na )[:, None ] + 0 * class_pair
185+ expected_offset = c0 + np .arange (nb )[None , :] + 0 * class_offset
186+ if not (
187+ np .array_equal (class_pair , expected_pair )
188+ and np .array_equal (class_offset , expected_offset )
189+ ):
190+ return Step0Layout (n_rows = n_rows , n_cols = n_cols , grid = None )
191+ corner .append ((p0 , c0 , int (na ), int (nb )))
192+ corners .append (tuple (corner ))
193+
194+ return Step0Layout (
195+ n_rows = n_rows ,
196+ n_cols = n_cols ,
197+ grid = (n_pairs , pair_width , int (n_pairs * pair_width - vertices .shape [0 ]), tuple (corners )),
198+ )
104199
105200
106201class CoordinateArrayTriangles (AbstractTriangles , ABC ):
@@ -113,6 +208,7 @@ def __init__(
113208 y_offset : float = 0.0 ,
114209 flipped : bool = False ,
115210 vertex_table : Optional [Tuple [np .ndarray , np .ndarray ]] = None ,
211+ step0_layout : Optional [Step0Layout ] = None ,
116212 ):
117213 """
118214 Represents a set of triangles by integer coordinates.
@@ -134,11 +230,17 @@ def __init__(
134230 sets it. Derived lattices (`for_indexes`, `up_sample`, `neighborhood`) do not inherit
135231 it, and it is not part of the pytree (`tree_flatten`), so an unflattened copy falls
136232 back to the flat table -- which is still correct, only unshared.
233+ step0_layout
234+ The `static_lattice_layout` of ``vertex_table``, set with it by
235+ `for_limits_and_scale(..., static_vertices=True)` and passed by `with_vertices` to the
236+ `ArrayTriangles` it returns, whose `containing_indices` then avoids the ``(N, 3, 2)``
237+ gather. Dropped exactly where ``vertex_table`` is.
137238 """
138239 import jax .numpy as jnp
139240
140241 self .coordinates = coordinates
141242 self .vertex_table = vertex_table
243+ self .step0_layout = step0_layout if vertex_table is not None else None
142244 self .side_length = side_length
143245 self .flipped = flipped
144246
@@ -191,17 +293,25 @@ def for_limits_and_scale(
191293 import jax .numpy as jnp
192294
193295 vertex_table = None
296+ step0_layout = None
194297 if static_vertices :
195- vertex_table = static_vertex_table (
196- float (y_min ), float (y_max ), float (x_min ), float (x_max ), float (scale )
298+ geometry = (
299+ float (y_min ),
300+ float (y_max ),
301+ float (x_min ),
302+ float (x_max ),
303+ float (scale ),
197304 )
305+ vertex_table = static_vertex_table (* geometry )
306+ step0_layout = static_lattice_layout (* geometry )
198307
199308 return cls (
200309 coordinates = jnp .array (
201310 _lattice_coordinates (y_min , y_max , x_min , x_max , scale )
202311 ),
203312 side_length = scale ,
204313 vertex_table = vertex_table ,
314+ step0_layout = step0_layout ,
205315 )
206316
207317 def tree_flatten (self ):
@@ -437,6 +547,7 @@ def with_vertices(self, vertices: np.ndarray) -> ArrayTriangles:
437547 return ArrayTriangles (
438548 indices = self .indices ,
439549 vertices = vertices ,
550+ step0_layout = self .step0_layout ,
440551 )
441552
442553 def for_indexes (self , indexes : np .ndarray ) -> "CoordinateArrayTriangles" :
0 commit comments