1- """Bounded elementary-operation Smith reduction used by trusted producers."""
1+ """Smith normal decomposition used by trusted producers.
2+
3+ The canonical Smith diagonal and divisibility chain are mathematical
4+ invariants. The unimodular transformations ``U`` and ``V`` and every
5+ representative derived from them are deterministic for the pinned SymPy
6+ version but are **not** byte-identical across backend versions: compatibility
7+ is semantic (``D = U A V``, unimodularity, canonical diagonal) rather than
8+ representational.
9+ """
210
311from __future__ import annotations
412
@@ -173,42 +181,24 @@ def inverse_unimodular(matrix: Matrix) -> Matrix:
173181 return [row [size :] for row in augmented ]
174182
175183
176- def _swap_rows (matrix : Matrix , left : int , right : int ) -> None :
177- matrix [left ], matrix [right ] = matrix [right ], matrix [left ]
178-
179-
180- def _swap_columns (matrix : Matrix , left : int , right : int ) -> None :
181- for row in matrix :
182- row [left ], row [right ] = row [right ], row [left ]
183-
184-
185- def _add_row_multiple (
186- matrix : Matrix , target : int , source : int , multiplier : int
187- ) -> None :
188- matrix [target ] = [
189- value + multiplier * other
190- for value , other in zip (matrix [target ], matrix [source ], strict = True )
191- ]
192-
193-
194- def _add_column_multiple (
195- matrix : Matrix , target : int , source : int , multiplier : int
196- ) -> None :
197- for row in matrix :
198- row [target ] += multiplier * row [source ]
199-
200-
201- def _negate_row (matrix : Matrix , row : int ) -> None :
202- matrix [row ] = [- value for value in matrix [row ]]
203-
204-
205184def smith_reduce (
206185 source : Matrix ,
207186 * ,
208187 row_count : int | None = None ,
209188 column_count : int | None = None ,
210189) -> SmithReduction :
211- """Return a canonical Smith diagonal and explicit elementary transformations."""
190+ """Return a canonical Smith diagonal and explicit unimodular transformations.
191+
192+ Delegates the decomposition to SymPy's ``smith_normal_decomp`` over ``ZZ``
193+ and converts the result to native integer matrices. The canonical diagonal
194+ and invariant factors are mathematical invariants; ``U`` and ``V`` are
195+ deterministic for the pinned SymPy version but may differ from other
196+ backends. Fail-closed checks verify ``D = U A V``, unimodularity, and the
197+ positive divisibility chain before returning.
198+ """
199+
200+ import sympy
201+ from sympy .matrices .normalforms import smith_normal_decomp
212202
213203 rows = len (source ) if row_count is None else row_count
214204 columns = (
@@ -217,99 +207,51 @@ def smith_reduce(
217207 if len (source ) != rows or any (len (row ) != columns for row in source ):
218208 raise ValueError ("source entries do not match the declared matrix shape" )
219209 original = [row [:] for row in source ]
220- matrix = [row [:] for row in source ]
221- left = identity_matrix (rows )
222- right = identity_matrix (columns )
223- diagonal_count = min (rows , columns )
224210
225- for pivot in range (diagonal_count ):
226- selected = min (
227- (
228- (abs (matrix [row ][column ]), row , column )
229- for row in range (pivot , rows )
230- for column in range (pivot , columns )
231- if matrix [row ][column ] != 0
232- ),
233- default = None ,
234- )
235- if selected is None :
236- break
237- _ , selected_row , selected_column = selected
238- if selected_row != pivot :
239- _swap_rows (matrix , pivot , selected_row )
240- _swap_rows (left , pivot , selected_row )
241- if selected_column != pivot :
242- _swap_columns (matrix , pivot , selected_column )
243- _swap_columns (right , pivot , selected_column )
244-
245- while True :
246- changed = False
247- for row in range (pivot + 1 , rows ):
248- while matrix [row ][pivot ] != 0 :
249- quotient = matrix [row ][pivot ] // matrix [pivot ][pivot ]
250- _add_row_multiple (matrix , row , pivot , - quotient )
251- _add_row_multiple (left , row , pivot , - quotient )
252- if matrix [row ][pivot ] != 0 and abs (matrix [row ][pivot ]) < abs (
253- matrix [pivot ][pivot ]
254- ):
255- _swap_rows (matrix , row , pivot )
256- _swap_rows (left , row , pivot )
257- changed = True
258- for column in range (pivot + 1 , columns ):
259- while matrix [pivot ][column ] != 0 :
260- quotient = matrix [pivot ][column ] // matrix [pivot ][pivot ]
261- _add_column_multiple (matrix , column , pivot , - quotient )
262- _add_column_multiple (right , column , pivot , - quotient )
263- if matrix [pivot ][column ] != 0 and abs (matrix [pivot ][column ]) < abs (
264- matrix [pivot ][pivot ]
265- ):
266- _swap_columns (matrix , column , pivot )
267- _swap_columns (right , column , pivot )
268- changed = True
269- offender = next (
270- (
271- (row , column )
272- for row in range (pivot + 1 , rows )
273- for column in range (pivot + 1 , columns )
274- if matrix [row ][column ] % matrix [pivot ][pivot ]
275- ),
276- None ,
277- )
278- if offender is not None :
279- _add_row_multiple (matrix , pivot , offender [0 ], 1 )
280- _add_row_multiple (left , pivot , offender [0 ], 1 )
281- changed = True
282- continue
283- if not changed :
284- break
285- if all (matrix [row ][pivot ] == 0 for row in range (pivot + 1 , rows )) and all (
286- matrix [pivot ][column ] == 0 for column in range (pivot + 1 , columns )
287- ):
288- break
289- if matrix [pivot ][pivot ] < 0 :
290- _negate_row (matrix , pivot )
291- _negate_row (left , pivot )
211+ # smith_normal_decomp accepts a plain SymPy Matrix and handles every shape,
212+ # including 0xm and nx0 matrices, returning identity transformations for
213+ # the empty side.
214+ if rows and columns :
215+ sympy_source = sympy .Matrix ([[int (value ) for value in row ] for row in original ])
216+ else :
217+ sympy_source = sympy .Matrix (rows , columns , [])
218+
219+ diagonal , left , right = smith_normal_decomp (sympy_source , domain = sympy .ZZ )
292220
221+ diagonal_matrix = [
222+ [int (diagonal [row , column ]) for column in range (columns )] for row in range (rows )
223+ ]
224+ left_matrix = [
225+ [int (left [row , column ]) for column in range (rows )] for row in range (rows )
226+ ]
227+ right_matrix = [
228+ [int (right [row , column ]) for column in range (columns )] for row in range (columns )
229+ ]
230+
231+ diagonal_count = min (rows , columns )
293232 factors = tuple (
294- matrix [index ][index ]
233+ diagonal_matrix [index ][index ]
295234 for index in range (diagonal_count )
296- if matrix [index ][index ] != 0
235+ if diagonal_matrix [index ][index ] != 0
297236 )
298237 if any (value <= 0 for value in factors ) or any (
299238 right_factor % left_factor for left_factor , right_factor in pairwise (factors )
300239 ):
301240 raise ArithmeticError ("Smith reduction did not produce a canonical diagonal" )
302- if matrix_multiply (matrix_multiply (left , original ), right ) != matrix :
241+ if (
242+ matrix_multiply (matrix_multiply (left_matrix , original ), right_matrix )
243+ != diagonal_matrix
244+ ):
303245 raise ArithmeticError ("Smith transformations do not bind the source" )
304- left_determinant = determinant (left )
305- right_determinant = determinant (right )
246+ left_determinant = determinant (left_matrix )
247+ right_determinant = determinant (right_matrix )
306248 if abs (left_determinant ) != 1 or abs (right_determinant ) != 1 :
307249 raise ArithmeticError ("Smith transformations are not unimodular" )
308250 return SmithReduction (
309251 source = original ,
310- diagonal = matrix ,
311- left = left ,
312- right = right ,
252+ diagonal = diagonal_matrix ,
253+ left = left_matrix ,
254+ right = right_matrix ,
313255 rank = len (factors ),
314256 invariant_factors = factors ,
315257 left_determinant = left_determinant ,
0 commit comments