Skip to content

Commit 4d01b7d

Browse files
committed
Simplify code
1 parent 98a79f9 commit 4d01b7d

5 files changed

Lines changed: 114 additions & 223 deletions

File tree

ramanujantools/asymptotics/growth_rate.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import sympy as sp
4+
from sympy.abc import t, n
45

56

67
class GrowthRate:
@@ -179,3 +180,44 @@ def simplify(self) -> GrowthRate:
179180
polynomial_degree=sp.simplify(self.polynomial_degree),
180181
log_power=self.log_power,
181182
)
183+
184+
@classmethod
185+
def from_taylor_coefficients(
186+
cls, coeffs: list[sp.Expr], p: int, log_power: int = 0, factorial_power: int = 0
187+
) -> GrowthRate:
188+
"""
189+
Calculates the exact asymptotic bounds of a formal product by extracting
190+
the sub-exponential and polynomial degrees via a logarithmic Maclaurin expansion.
191+
"""
192+
exp_base = sp.cancel(sp.expand(coeffs[0]))
193+
if exp_base == sp.S.Zero:
194+
return cls(exp_base=sp.S.Zero)
195+
196+
precision = len(coeffs)
197+
198+
x = sum(
199+
(coeffs[k] / exp_base) * (t**k) for k in range(1, min(precision, p + 1))
200+
)
201+
202+
# Maclaurin series of ln(1+x) up to O(t^(p+1))
203+
log_series = sp.expand(
204+
sum(((-1) ** (j + 1) / sp.Rational(j)) * (x**j) for j in range(1, p + 1))
205+
)
206+
sub_exp, poly_deg = sp.S.Zero, sp.S.Zero
207+
208+
for k in range(1, p + 1):
209+
c_k = sp.cancel(sp.expand(log_series.coeff(t, k)))
210+
if c_k != sp.S.Zero:
211+
if k < p:
212+
power = 1 - sp.Rational(k, p)
213+
sub_exp += (c_k / power) * (n**power)
214+
else:
215+
poly_deg = c_k
216+
217+
return cls(
218+
exp_base=exp_base,
219+
sub_exp=sub_exp,
220+
polynomial_degree=poly_deg,
221+
log_power=log_power,
222+
factorial_power=factorial_power,
223+
)

ramanujantools/asymptotics/reducer.py

Lines changed: 44 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -49,62 +49,6 @@ def __init__(
4949
self._is_reduced = False
5050
self.children = []
5151

52-
@classmethod
53-
def from_matrix(
54-
cls, matrix: Matrix, precision: int = 5, force: bool = False
55-
) -> "Reducer":
56-
if not matrix.is_square():
57-
raise ValueError("Input matrix must be square.")
58-
59-
free_syms = list(matrix.free_symbols)
60-
if len(free_syms) > 1:
61-
raise ValueError("Input matrix must depend on at most one variable.")
62-
63-
p = 1
64-
factorial_power = max(matrix.degrees(n))
65-
66-
normalized_matrix = matrix / (n**factorial_power)
67-
68-
degrees = [d for d in normalized_matrix.degrees(n) if d != -sp.oo]
69-
S = max(degrees) - min(degrees) if degrees else 1
70-
poincare_bound = S * 2 + 1
71-
72-
negative_bound = (
73-
-min(
74-
[
75-
factorial_power
76-
for factorial_power in normalized_matrix.degrees(n)
77-
if factorial_power > -sp.oo
78-
],
79-
default=0,
80-
)
81-
* p
82-
+ 1
83-
)
84-
85-
required_precision = max(poincare_bound, negative_bound)
86-
if not force and precision < required_precision:
87-
raise PrecisionExhaustedError(
88-
"Encountered a silent rational Taylor truncation."
89-
)
90-
91-
logger.debug(f"FROM_MATRIX: Expanding Taylor Series to {precision=} ...")
92-
series = SeriesMatrix.from_matrix(normalized_matrix, n, p, precision)
93-
94-
return cls(
95-
series=series,
96-
factorial_power=factorial_power,
97-
precision=precision,
98-
p=p,
99-
)
100-
101-
def _unramified_target(self, ramified_target: int | sp.Expr) -> int:
102-
"""
103-
Converts a local ramified precision requirement back to the
104-
global unramified scale for the top-level backoff loop.
105-
"""
106-
return int(sp.ceiling(ramified_target / self.p))
107-
10852
@staticmethod
10953
def _solve_sylvester(A: Matrix, B: Matrix, C: Matrix) -> Matrix:
11054
"""Solves the Sylvester equation: A*X - X*B = C for X using Kronecker flattening."""
@@ -426,70 +370,36 @@ def _check_cfm_validity(self, grid: list[list["GrowthRate"]]) -> None:
426370
def asymptotic_growth(self) -> list[GrowthRate]:
427371
"""
428372
Extracts the raw, unmapped asymptotic components of the internal canonical basis.
429-
Returns a list of strongly-typed GrowthRate objects.
430373
"""
431374
if not self._is_reduced:
432375
self.reduce()
433376

434377
if self.children:
435-
for i, child in enumerate(self.children):
436-
logger.debug(
437-
f"CFM: Child {i} has precision {child.S_total.precision=}. "
438-
f"Is it identity? {child.S_total.coeffs[0].is_Identity}"
439-
)
440378
return [sol for child in self.children for sol in child.asymptotic_growth()]
441379

442380
growths, log_power = [], 0
443381

444382
for i in range(self.dim):
445-
exp_base = sp.cancel(sp.expand(self.M.coeffs[0][i, i]))
383+
exp_base = self.M.coeffs[0][i, i]
446384
self._check_eigenvalue_blindness(exp_base)
447385

448-
is_jordan_link = False
449-
if i > 0 and exp_base == sp.cancel(
450-
sp.expand(self.M.coeffs[0][i - 1, i - 1])
451-
):
386+
if i > 0 and exp_base == self.M.coeffs[0][i - 1, i - 1]:
452387
is_jordan_link = any(
453-
sp.cancel(sp.expand(self.M.coeffs[k][i - 1, i])) != sp.S.Zero
388+
self.M.coeffs[k][i - 1, i] != sp.S.Zero
454389
for k in range(self.precision)
455390
)
456-
log_power = log_power + 1 if is_jordan_link else 0
457-
458-
x = sp.S.Zero
459-
max_k = min(self.precision, self.p + 1)
460-
for k in range(1, max_k):
461-
x += (self.M.coeffs[k][i, i] / exp_base) * (t**k)
462-
463-
log_series = sp.S.Zero
464-
for j in range(1, self.p + 1):
465-
log_series += ((-1) ** (j + 1) / sp.Rational(j)) * (x**j)
466-
467-
log_series = sp.expand(log_series)
468-
469-
sub_exp, polynomial_degree = sp.S.Zero, sp.S.Zero
470-
471-
for k in range(1, self.p + 1):
472-
c_k = log_series.coeff(t, k)
473-
if c_k == sp.S.Zero:
474-
continue
475-
476-
c_k = sp.cancel(sp.expand(c_k))
477-
478-
if k < self.p:
479-
power = 1 - sp.Rational(k, self.p)
480-
sub_exp += (c_k / power) * (n**power)
481-
elif k == self.p:
482-
polynomial_degree = c_k
391+
log_power = log_power + 1 if is_jordan_link else 0
392+
else:
393+
log_power = 0
483394

484-
logger.debug(
485-
f"GROWTH: Variable {i=}, {k=}, {c_k=}, {exp_base=}, {x=}, {log_series=}, {polynomial_degree=}"
486-
)
395+
# Isolate the diagonal Taylor coefficients for this variable
396+
diag_coeffs = [self.M.coeffs[k][i, i] for k in range(self.precision)]
487397

398+
# Let GrowthRate parse its own math!
488399
growths.append(
489-
GrowthRate(
490-
exp_base=exp_base,
491-
sub_exp=sub_exp,
492-
polynomial_degree=polynomial_degree,
400+
GrowthRate.from_taylor_coefficients(
401+
coeffs=diag_coeffs,
402+
p=self.p,
493403
log_power=log_power,
494404
factorial_power=self.factorial_power,
495405
)
@@ -524,58 +434,47 @@ def canonical_growth_matrix(self) -> list[list[GrowthRate]]:
524434
if not self._is_reduced:
525435
self.reduce()
526436

527-
# 1. Base Grid Construction (Recursive Block Diagonal or Leaf Nodes)
437+
base_grid = [[GrowthRate() for _ in range(self.dim)] for _ in range(self.dim)]
528438
if self.children:
529-
base_grid = [
530-
[GrowthRate() for _ in range(self.dim)] for _ in range(self.dim)
531-
]
532439
offset = 0
533440
for child in self.children:
534-
c_grid = child.canonical_growth_matrix() # RECURSIVE CALL
535-
c_dim = child.dim
536-
for r in range(c_dim):
537-
for c in range(c_dim):
538-
base_grid[offset + r][offset + c] = c_grid[r][c]
539-
offset += c_dim
441+
c_grid = child.canonical_growth_matrix()
442+
for r, row in enumerate(c_grid):
443+
for c, val in enumerate(row):
444+
base_grid[offset + r][offset + c] = val
445+
offset += child.dim
540446
else:
541-
growths = self.asymptotic_growth()
542-
base_grid = [
543-
[GrowthRate() if r != c else growths[r] for c in range(self.dim)]
544-
for r in range(self.dim)
447+
for i, growth in enumerate(self.asymptotic_growth()):
448+
base_grid[i][i] = growth
449+
450+
shift_matrix = [
451+
[GrowthRate() for _ in range(self.dim)] for _ in range(self.dim)
452+
]
453+
for r in range(self.dim):
454+
for c in range(self.dim):
455+
for k, mat in enumerate(self.S_total.coeffs):
456+
if mat[r, c] != sp.S.Zero:
457+
shift_matrix[r][c] = GrowthRate(
458+
exp_base=sp.S.One,
459+
polynomial_degree=-sp.Rational(k, self.p),
460+
)
461+
break
462+
463+
final_grid = [
464+
[
465+
sum(
466+
(shift_matrix[row][k] * base_grid[k][col] for k in range(self.dim)),
467+
start=GrowthRate(),
468+
)
469+
for col in range(self.dim)
545470
]
546-
547-
# 2. Map the assembled block through the local gauge transformation S_total
548-
final_grid = []
549-
for row in range(self.dim):
550-
final_row = []
551-
for col in range(self.dim):
552-
cell_growth = GrowthRate()
553-
554-
for k_idx in range(self.dim):
555-
base_cell = base_grid[k_idx][col]
556-
if base_cell.exp_base == sp.S.Zero:
557-
continue
558-
559-
# Find the leading shift in S_total for this mapping
560-
for series_k in range(self.S_total.precision):
561-
coeff = self.S_total.coeffs[series_k][row, k_idx]
562-
if coeff != sp.S.Zero:
563-
shift_growth = GrowthRate(
564-
exp_base=sp.S.One,
565-
polynomial_degree=-sp.Rational(series_k, self.p),
566-
)
567-
cell_growth += shift_growth * base_cell
568-
break
569-
570-
final_row.append(cell_growth)
571-
final_grid.append(final_row)
471+
for row in range(self.dim)
472+
]
572473

573474
sorted_indices = sorted(
574475
range(self.dim), key=lambda c: final_grid[-1][c], reverse=True
575476
)
576-
577-
for i in range(self.dim):
578-
final_grid[i] = [final_grid[i][c] for c in sorted_indices]
477+
final_grid = [[row[c] for c in sorted_indices] for row in final_grid]
579478

580479
self._check_cfm_validity(final_grid)
581480
return final_grid

ramanujantools/asymptotics/series_matrix.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,7 @@ def __mul__(self, other: "SeriesMatrix") -> "SeriesMatrix":
7575

7676
new_coeffs[i + j] += self.coeffs[i] * other.coeffs[j]
7777

78-
new_coeffs = [
79-
c.applyfunc(lambda x: sp.cancel(sp.expand(x))) for c in new_coeffs
80-
]
81-
82-
return SeriesMatrix(new_coeffs, p=self.p, precision=out_precision)
78+
return SeriesMatrix(new_coeffs, p=self.p, precision=out_precision).simplify()
8379

8480
def inverse(self) -> SeriesMatrix:
8581
r"""
@@ -289,13 +285,13 @@ def shear_coboundary(
289285
for k in range(output_precision):
290286
target_power = k - h
291287
if target_power in power_dict:
292-
# Deflate immediately upon shifting.
293-
# Bounding this loop skips sp.cancel on dead terms!
294-
new_coeffs.append(power_dict[target_power].applyfunc(sp.cancel))
288+
new_coeffs.append(power_dict[target_power])
295289
else:
296290
new_coeffs.append(Matrix.zeros(*self.shape))
297291

298-
return SeriesMatrix(new_coeffs, p=self.p, precision=output_precision), h
292+
return SeriesMatrix(
293+
new_coeffs, p=self.p, precision=output_precision
294+
).simplify(), h
299295

300296
def shift_leading_eigenvalue(self, lambda_val: sp.Expr) -> SeriesMatrix:
301297
"""
@@ -338,6 +334,14 @@ def get_first_non_scalar_index(self) -> int | None:
338334

339335
return None
340336

337+
def simplify(self) -> SeriesMatrix:
338+
def crush(x):
339+
return sp.cancel(sp.expand(x))
340+
341+
new_coeffs = [c.applyfunc(crush) for c in self.coeffs]
342+
343+
return type(self)(new_coeffs, p=self.p, precision=self.precision)
344+
341345
@classmethod
342346
def from_matrix(
343347
cls, matrix: Matrix, var: sp.Symbol, p: int, precision: int

0 commit comments

Comments
 (0)