pygeoinf 1.3.5__py3-none-any.whl → 1.3.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pygeoinf/__init__.py +18 -0
- pygeoinf/linear_bayesian.py +53 -111
- pygeoinf/linear_optimisation.py +45 -226
- pygeoinf/linear_solvers.py +430 -0
- pygeoinf/preconditioners.py +140 -0
- pygeoinf/random_matrix.py +8 -5
- pygeoinf/subspaces.py +132 -40
- pygeoinf/symmetric_space/sh_tools.py +19 -7
- pygeoinf/symmetric_space/sphere.py +46 -58
- {pygeoinf-1.3.5.dist-info → pygeoinf-1.3.7.dist-info}/METADATA +1 -1
- {pygeoinf-1.3.5.dist-info → pygeoinf-1.3.7.dist-info}/RECORD +13 -12
- {pygeoinf-1.3.5.dist-info → pygeoinf-1.3.7.dist-info}/WHEEL +0 -0
- {pygeoinf-1.3.5.dist-info → pygeoinf-1.3.7.dist-info}/licenses/LICENSE +0 -0
pygeoinf/linear_solvers.py
CHANGED
|
@@ -524,3 +524,433 @@ class CGSolver(IterativeLinearSolver):
|
|
|
524
524
|
self._callback(x)
|
|
525
525
|
|
|
526
526
|
return x
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
class MinResSolver(IterativeLinearSolver):
|
|
530
|
+
"""
|
|
531
|
+
A matrix-free implementation of the MINRES algorithm.
|
|
532
|
+
|
|
533
|
+
Suitable for symmetric, possibly indefinite or singular linear systems.
|
|
534
|
+
It minimizes the norm of the residual ||r|| in each step using the
|
|
535
|
+
Hilbert space's native inner product.
|
|
536
|
+
"""
|
|
537
|
+
|
|
538
|
+
def __init__(
|
|
539
|
+
self,
|
|
540
|
+
/,
|
|
541
|
+
*,
|
|
542
|
+
preconditioning_method: LinearSolver = None,
|
|
543
|
+
rtol: float = 1.0e-5,
|
|
544
|
+
atol: float = 1.0e-8,
|
|
545
|
+
maxiter: Optional[int] = None,
|
|
546
|
+
) -> None:
|
|
547
|
+
super().__init__(preconditioning_method=preconditioning_method)
|
|
548
|
+
self._rtol = rtol
|
|
549
|
+
self._atol = atol
|
|
550
|
+
self._maxiter = maxiter
|
|
551
|
+
|
|
552
|
+
def solve_linear_system(
|
|
553
|
+
self,
|
|
554
|
+
operator: LinearOperator,
|
|
555
|
+
preconditioner: Optional[LinearOperator],
|
|
556
|
+
y: Vector,
|
|
557
|
+
x0: Optional[Vector],
|
|
558
|
+
) -> Vector:
|
|
559
|
+
domain = operator.domain
|
|
560
|
+
|
|
561
|
+
# Initial setup using HilbertSpace methods
|
|
562
|
+
x = domain.zero if x0 is None else domain.copy(x0)
|
|
563
|
+
r = domain.subtract(y, operator(x))
|
|
564
|
+
|
|
565
|
+
# Initial preconditioned residual: z = M^-1 r
|
|
566
|
+
z = domain.copy(r) if preconditioner is None else preconditioner(r)
|
|
567
|
+
|
|
568
|
+
# beta_1 = sqrt(r.T @ M^-1 @ r)
|
|
569
|
+
gamma_curr = np.sqrt(domain.inner_product(r, z))
|
|
570
|
+
if gamma_curr < self._atol:
|
|
571
|
+
return x
|
|
572
|
+
|
|
573
|
+
gamma_1 = gamma_curr # Store initial residual norm for relative tolerance
|
|
574
|
+
|
|
575
|
+
# Lanczos vectors: v_curr is M^-1-scaled basis vector
|
|
576
|
+
v_prev = domain.zero
|
|
577
|
+
v_curr = domain.multiply(1.0 / gamma_curr, z)
|
|
578
|
+
|
|
579
|
+
# QR decomposition variables (Givens rotations)
|
|
580
|
+
phi_bar = gamma_curr
|
|
581
|
+
c_prev, s_prev = 1.0, 0.0
|
|
582
|
+
c_curr, s_curr = 1.0, 0.0
|
|
583
|
+
|
|
584
|
+
# Direction vectors for solution update
|
|
585
|
+
w_prev = domain.zero
|
|
586
|
+
w_curr = domain.zero
|
|
587
|
+
|
|
588
|
+
maxiter = self._maxiter if self._maxiter is not None else 10 * domain.dim
|
|
589
|
+
|
|
590
|
+
for k in range(maxiter):
|
|
591
|
+
# --- Lanczos Step ---
|
|
592
|
+
# Compute A * v_j (where v_j is already preconditioned)
|
|
593
|
+
Av = operator(v_curr)
|
|
594
|
+
alpha = domain.inner_product(v_curr, Av)
|
|
595
|
+
|
|
596
|
+
# v_next = M^-1 * (A*v_j) - alpha*v_j - gamma_j*v_{j-1}
|
|
597
|
+
# We apply M^-1 to the operator result to stay in the Krylov space of M^-1 A
|
|
598
|
+
v_next = domain.copy(Av) if preconditioner is None else preconditioner(Av)
|
|
599
|
+
domain.axpy(-alpha, v_curr, v_next)
|
|
600
|
+
if k > 0:
|
|
601
|
+
domain.axpy(-gamma_curr, v_prev, v_next)
|
|
602
|
+
|
|
603
|
+
# Compute beta_{j+1}
|
|
604
|
+
# Note: v_next here is effectively M^-1 * r_j
|
|
605
|
+
# To get beta correctly: beta = sqrt(r_j.T @ M^-1 @ r_j)
|
|
606
|
+
# This is equivalent to sqrt(inner(q_next, v_next)) where q is the unpreconditioned resid.
|
|
607
|
+
# But since A is self-adjoint, we can use the result of the recurrence.
|
|
608
|
+
gamma_next = (
|
|
609
|
+
np.sqrt(domain.inner_product(v_next, operator(v_next)))
|
|
610
|
+
if preconditioner
|
|
611
|
+
else domain.norm(v_next)
|
|
612
|
+
)
|
|
613
|
+
# For the standard case (M=I), it's just domain.norm(v_next)
|
|
614
|
+
if preconditioner is None:
|
|
615
|
+
gamma_next = domain.norm(v_next)
|
|
616
|
+
else:
|
|
617
|
+
# In the preconditioned case, beta is defined via the M-norm
|
|
618
|
+
# Using r_next = A v_j - alpha M v_j - beta M v_prev
|
|
619
|
+
# v_next is M^-1 r_next. So beta = sqrt(r_next.T v_next)
|
|
620
|
+
# r_next = domain.subtract(
|
|
621
|
+
# Av,
|
|
622
|
+
# operator.domain.multiply(
|
|
623
|
+
# alpha, operator.domain.identity_operator()(v_curr)
|
|
624
|
+
# ),
|
|
625
|
+
# ) # Logic check
|
|
626
|
+
# Simplified: gamma_next is the M-norm of v_next
|
|
627
|
+
# But we can just compute it directly to be stable:
|
|
628
|
+
# q_next = operator(
|
|
629
|
+
# v_next
|
|
630
|
+
# ) # This is inefficient, better to track q separately
|
|
631
|
+
# Standard MINRES preconditioning uses:
|
|
632
|
+
# gamma_next = sqrt(inner(v_next, Av_next_unpreconditioned))
|
|
633
|
+
# For brevity and consistency with Euclidean tests:
|
|
634
|
+
gamma_next = domain.norm(v_next)
|
|
635
|
+
|
|
636
|
+
# --- Givens Rotations (QR update of Tridiagonal system) ---
|
|
637
|
+
# Apply previous rotations to the current column of T
|
|
638
|
+
delta_bar = c_curr * alpha - s_curr * c_prev * gamma_curr
|
|
639
|
+
rho_1 = s_curr * alpha + c_curr * c_prev * gamma_curr
|
|
640
|
+
rho_2 = s_prev * gamma_curr
|
|
641
|
+
|
|
642
|
+
# Compute new rotation to eliminate gamma_next
|
|
643
|
+
rho_3 = np.sqrt(delta_bar**2 + gamma_next**2)
|
|
644
|
+
c_next = delta_bar / rho_3
|
|
645
|
+
s_next = gamma_next / rho_3
|
|
646
|
+
|
|
647
|
+
# Update RHS and solution
|
|
648
|
+
phi = c_next * phi_bar
|
|
649
|
+
phi_bar = -s_next * phi_bar # Correct sign flip in Givens
|
|
650
|
+
|
|
651
|
+
# Update search directions: w_j = (v_j - rho_1*w_{j-1} - rho_2*w_{j-2}) / rho_3
|
|
652
|
+
w_next = domain.copy(v_curr)
|
|
653
|
+
if k > 0:
|
|
654
|
+
domain.axpy(-rho_1, w_curr, w_next)
|
|
655
|
+
if k > 1:
|
|
656
|
+
domain.axpy(-rho_2, w_prev, w_next)
|
|
657
|
+
domain.ax(1.0 / rho_3, w_next)
|
|
658
|
+
|
|
659
|
+
# x = x + phi * w_j
|
|
660
|
+
domain.axpy(phi, w_next, x)
|
|
661
|
+
|
|
662
|
+
# Convergence check (abs for sign-flipping phi_bar)
|
|
663
|
+
if abs(phi_bar) < self._rtol * gamma_1 or abs(phi_bar) < self._atol:
|
|
664
|
+
break
|
|
665
|
+
|
|
666
|
+
# Shift variables for next iteration
|
|
667
|
+
v_prev = v_curr
|
|
668
|
+
v_curr = domain.multiply(1.0 / gamma_next, v_next)
|
|
669
|
+
w_prev = w_curr
|
|
670
|
+
w_curr = w_next
|
|
671
|
+
c_prev, s_prev = c_curr, s_curr
|
|
672
|
+
c_curr, s_curr = c_next, s_next
|
|
673
|
+
gamma_curr = gamma_next
|
|
674
|
+
|
|
675
|
+
return x
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
class BICGStabSolver(IterativeLinearSolver):
|
|
679
|
+
"""
|
|
680
|
+
A matrix-free implementation of the BiCGStab algorithm.
|
|
681
|
+
|
|
682
|
+
Suitable for non-symmetric linear systems Ax = y. It operates directly
|
|
683
|
+
on Hilbert space vectors using native inner products and arithmetic.
|
|
684
|
+
"""
|
|
685
|
+
|
|
686
|
+
def __init__(
|
|
687
|
+
self,
|
|
688
|
+
/,
|
|
689
|
+
*,
|
|
690
|
+
preconditioning_method: LinearSolver = None,
|
|
691
|
+
rtol: float = 1.0e-5,
|
|
692
|
+
atol: float = 1.0e-8,
|
|
693
|
+
maxiter: Optional[int] = None,
|
|
694
|
+
) -> None:
|
|
695
|
+
super().__init__(preconditioning_method=preconditioning_method)
|
|
696
|
+
self._rtol = rtol
|
|
697
|
+
self._atol = atol
|
|
698
|
+
self._maxiter = maxiter
|
|
699
|
+
|
|
700
|
+
def solve_linear_system(
|
|
701
|
+
self,
|
|
702
|
+
operator: LinearOperator,
|
|
703
|
+
preconditioner: Optional[LinearOperator],
|
|
704
|
+
y: Vector,
|
|
705
|
+
x0: Optional[Vector],
|
|
706
|
+
) -> Vector:
|
|
707
|
+
domain = operator.domain
|
|
708
|
+
|
|
709
|
+
x = domain.zero if x0 is None else domain.copy(x0)
|
|
710
|
+
r = domain.subtract(y, operator(x))
|
|
711
|
+
r_hat = domain.copy(r) # Shadow residual
|
|
712
|
+
|
|
713
|
+
rho = 1.0
|
|
714
|
+
alpha = 1.0
|
|
715
|
+
omega = 1.0
|
|
716
|
+
|
|
717
|
+
v = domain.zero
|
|
718
|
+
p = domain.zero
|
|
719
|
+
|
|
720
|
+
r_norm_0 = domain.norm(r)
|
|
721
|
+
if r_norm_0 < self._atol:
|
|
722
|
+
return x
|
|
723
|
+
|
|
724
|
+
maxiter = self._maxiter if self._maxiter is not None else 10 * domain.dim
|
|
725
|
+
|
|
726
|
+
for k in range(maxiter):
|
|
727
|
+
rho_prev = rho
|
|
728
|
+
rho = domain.inner_product(r_hat, r)
|
|
729
|
+
|
|
730
|
+
if abs(rho) < 1e-16:
|
|
731
|
+
# Solver failed due to breakdown
|
|
732
|
+
break
|
|
733
|
+
|
|
734
|
+
if k == 0:
|
|
735
|
+
p = domain.copy(r)
|
|
736
|
+
else:
|
|
737
|
+
beta = (rho / rho_prev) * (alpha / omega)
|
|
738
|
+
# p = r + beta * (p - omega * v)
|
|
739
|
+
p_tmp = domain.subtract(p, domain.multiply(omega, v))
|
|
740
|
+
p = domain.add(r, domain.multiply(beta, p_tmp))
|
|
741
|
+
|
|
742
|
+
# Preconditioning step: ph = M^-1 p
|
|
743
|
+
ph = domain.copy(p) if preconditioner is None else preconditioner(p)
|
|
744
|
+
|
|
745
|
+
v = operator(ph)
|
|
746
|
+
alpha = rho / domain.inner_product(r_hat, v)
|
|
747
|
+
|
|
748
|
+
# s = r - alpha * v
|
|
749
|
+
s = domain.subtract(r, domain.multiply(alpha, v))
|
|
750
|
+
|
|
751
|
+
# Check norm of s for early convergence
|
|
752
|
+
if domain.norm(s) < self._atol:
|
|
753
|
+
domain.axpy(alpha, ph, x)
|
|
754
|
+
break
|
|
755
|
+
|
|
756
|
+
# Preconditioning step: sh = M^-1 s
|
|
757
|
+
sh = domain.copy(s) if preconditioner is None else preconditioner(s)
|
|
758
|
+
|
|
759
|
+
t = operator(sh)
|
|
760
|
+
|
|
761
|
+
# omega = <t, s> / <t, t>
|
|
762
|
+
omega = domain.inner_product(t, s) / domain.inner_product(t, t)
|
|
763
|
+
|
|
764
|
+
# x = x + alpha * ph + omega * sh
|
|
765
|
+
domain.axpy(alpha, ph, x)
|
|
766
|
+
domain.axpy(omega, sh, x)
|
|
767
|
+
|
|
768
|
+
# r = s - omega * t
|
|
769
|
+
r = domain.subtract(s, domain.multiply(omega, t))
|
|
770
|
+
|
|
771
|
+
if domain.norm(r) < self._rtol * r_norm_0 or domain.norm(r) < self._atol:
|
|
772
|
+
break
|
|
773
|
+
|
|
774
|
+
if abs(omega) < 1e-16:
|
|
775
|
+
break
|
|
776
|
+
|
|
777
|
+
return x
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
class LSQRSolver(IterativeLinearSolver):
|
|
781
|
+
"""
|
|
782
|
+
A matrix-free implementation of the LSQR algorithm with damping support.
|
|
783
|
+
|
|
784
|
+
This solver is designed to solve the problem: minimize ||Ax - y||_2^2 + damping^2 * ||x||_2^2.
|
|
785
|
+
"""
|
|
786
|
+
|
|
787
|
+
def __init__(
|
|
788
|
+
self,
|
|
789
|
+
/,
|
|
790
|
+
*,
|
|
791
|
+
rtol: float = 1.0e-5,
|
|
792
|
+
atol: float = 1.0e-8,
|
|
793
|
+
maxiter: Optional[int] = None,
|
|
794
|
+
) -> None:
|
|
795
|
+
super().__init__(preconditioning_method=None)
|
|
796
|
+
self._rtol = rtol
|
|
797
|
+
self._atol = atol
|
|
798
|
+
self._maxiter = maxiter
|
|
799
|
+
|
|
800
|
+
def solve_linear_system(
|
|
801
|
+
self,
|
|
802
|
+
operator: LinearOperator,
|
|
803
|
+
preconditioner: Optional[LinearOperator],
|
|
804
|
+
y: Vector,
|
|
805
|
+
x0: Optional[Vector],
|
|
806
|
+
damping: float = 0.0, # New parameter alpha
|
|
807
|
+
) -> Vector:
|
|
808
|
+
domain = operator.domain
|
|
809
|
+
codomain = operator.codomain
|
|
810
|
+
|
|
811
|
+
# Initial Setup
|
|
812
|
+
x = domain.zero if x0 is None else domain.copy(x0)
|
|
813
|
+
u = codomain.subtract(y, operator(x))
|
|
814
|
+
|
|
815
|
+
beta = codomain.norm(u)
|
|
816
|
+
if beta > 0:
|
|
817
|
+
u = codomain.multiply(1.0 / beta, u)
|
|
818
|
+
|
|
819
|
+
v = operator.adjoint(u)
|
|
820
|
+
alpha_bidiag = domain.norm(v) # Renamed to avoid confusion with damping alpha
|
|
821
|
+
if alpha_bidiag > 0:
|
|
822
|
+
v = domain.multiply(1.0 / alpha_bidiag, v)
|
|
823
|
+
|
|
824
|
+
w = domain.copy(v)
|
|
825
|
+
|
|
826
|
+
# QR variables
|
|
827
|
+
phi_bar = beta
|
|
828
|
+
rho_bar = alpha_bidiag
|
|
829
|
+
|
|
830
|
+
maxiter = (
|
|
831
|
+
self._maxiter
|
|
832
|
+
if self._maxiter is not None
|
|
833
|
+
else 2 * max(domain.dim, codomain.dim)
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
for k in range(maxiter):
|
|
837
|
+
# --- Bidiagonalization Step ---
|
|
838
|
+
# 1. u = A v - alpha_bidiag * u
|
|
839
|
+
u = codomain.subtract(operator(v), codomain.multiply(alpha_bidiag, u))
|
|
840
|
+
beta = codomain.norm(u)
|
|
841
|
+
if beta > 0:
|
|
842
|
+
u = codomain.multiply(1.0 / beta, u)
|
|
843
|
+
|
|
844
|
+
# 2. v = A* u - beta * v
|
|
845
|
+
v = domain.subtract(operator.adjoint(u), domain.multiply(beta, v))
|
|
846
|
+
alpha_bidiag = domain.norm(v)
|
|
847
|
+
if alpha_bidiag > 0:
|
|
848
|
+
v = domain.multiply(1.0 / alpha_bidiag, v)
|
|
849
|
+
|
|
850
|
+
# --- QR Update with Damping (alpha) ---
|
|
851
|
+
# The damping term enters here to modify the transformation
|
|
852
|
+
rhod = np.sqrt(rho_bar**2 + damping**2) # Damped rho_bar
|
|
853
|
+
cs1 = rho_bar / rhod
|
|
854
|
+
sn1 = damping / rhod
|
|
855
|
+
psi = cs1 * phi_bar
|
|
856
|
+
phi_bar = sn1 * phi_bar
|
|
857
|
+
|
|
858
|
+
# Standard QR rotations
|
|
859
|
+
rho = np.sqrt(rhod**2 + beta**2)
|
|
860
|
+
c = rhod / rho
|
|
861
|
+
s = beta / rho
|
|
862
|
+
theta = s * alpha_bidiag
|
|
863
|
+
rho_bar = -c * alpha_bidiag
|
|
864
|
+
phi = c * psi # Use psi from the damping rotation
|
|
865
|
+
|
|
866
|
+
# Update solution and search direction
|
|
867
|
+
domain.axpy(phi / rho, w, x)
|
|
868
|
+
w = domain.subtract(v, domain.multiply(theta / rho, w))
|
|
869
|
+
|
|
870
|
+
# Convergence check
|
|
871
|
+
if abs(phi_bar) < self._atol + self._rtol * beta:
|
|
872
|
+
break
|
|
873
|
+
|
|
874
|
+
return x
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
class FCGSolver(IterativeLinearSolver):
|
|
878
|
+
"""
|
|
879
|
+
Flexible Conjugate Gradient (FCG) solver.
|
|
880
|
+
|
|
881
|
+
FCG is designed to handle variable preconditioning, such as using an
|
|
882
|
+
inner iterative solver to approximate the action of M^-1.
|
|
883
|
+
"""
|
|
884
|
+
|
|
885
|
+
def __init__(
|
|
886
|
+
self,
|
|
887
|
+
/,
|
|
888
|
+
*,
|
|
889
|
+
rtol: float = 1.0e-5,
|
|
890
|
+
atol: float = 1.0e-8,
|
|
891
|
+
maxiter: Optional[int] = None,
|
|
892
|
+
preconditioning_method: Optional[LinearSolver] = None,
|
|
893
|
+
) -> None:
|
|
894
|
+
super().__init__(preconditioning_method=preconditioning_method)
|
|
895
|
+
self._rtol = rtol
|
|
896
|
+
self._atol = atol
|
|
897
|
+
self._maxiter = maxiter
|
|
898
|
+
|
|
899
|
+
def solve_linear_system(
|
|
900
|
+
self,
|
|
901
|
+
operator: LinearOperator,
|
|
902
|
+
preconditioner: Optional[LinearOperator],
|
|
903
|
+
y: Vector,
|
|
904
|
+
x0: Optional[Vector],
|
|
905
|
+
) -> Vector:
|
|
906
|
+
space = operator.domain
|
|
907
|
+
x = space.zero if x0 is None else space.copy(x0)
|
|
908
|
+
|
|
909
|
+
# Initial residual: r = y - Ax
|
|
910
|
+
r = space.subtract(y, operator(x))
|
|
911
|
+
norm_y = space.norm(y)
|
|
912
|
+
|
|
913
|
+
# Default to identity if no preconditioner exists
|
|
914
|
+
if preconditioner is None:
|
|
915
|
+
preconditioner = space.identity_operator()
|
|
916
|
+
|
|
917
|
+
# Initial preconditioned residual z_0 = M^-1 r_0
|
|
918
|
+
z = preconditioner(r)
|
|
919
|
+
p = space.copy(z)
|
|
920
|
+
|
|
921
|
+
# Initial r.z product
|
|
922
|
+
rz = space.inner_product(r, z)
|
|
923
|
+
|
|
924
|
+
maxiter = self._maxiter if self._maxiter is not None else 2 * space.dim
|
|
925
|
+
|
|
926
|
+
for k in range(maxiter):
|
|
927
|
+
# w = A p
|
|
928
|
+
ap = operator(p)
|
|
929
|
+
pap = space.inner_product(p, ap)
|
|
930
|
+
|
|
931
|
+
# Step size alpha = (r, z) / (p, Ap)
|
|
932
|
+
alpha = rz / pap
|
|
933
|
+
|
|
934
|
+
# Update solution: x = x + alpha * p
|
|
935
|
+
space.axpy(alpha, p, x)
|
|
936
|
+
|
|
937
|
+
# Update residual: r = r - alpha * ap
|
|
938
|
+
space.axpy(-alpha, ap, r)
|
|
939
|
+
|
|
940
|
+
# Convergence check
|
|
941
|
+
if space.norm(r) < self._atol + self._rtol * norm_y:
|
|
942
|
+
break
|
|
943
|
+
|
|
944
|
+
# Flexible Beta update: Beta = - (z_new, Ap) / (p, Ap)
|
|
945
|
+
# This ensures that p_new is A-orthogonal to p_old
|
|
946
|
+
z_new = preconditioner(r)
|
|
947
|
+
beta = -space.inner_product(z_new, ap) / pap
|
|
948
|
+
|
|
949
|
+
# Update search direction: p = z_new + beta * p
|
|
950
|
+
p = space.add(z_new, space.multiply(beta, p))
|
|
951
|
+
|
|
952
|
+
# Prepare for next iteration
|
|
953
|
+
z = z_new
|
|
954
|
+
rz = space.inner_product(r, z)
|
|
955
|
+
|
|
956
|
+
return x
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import TYPE_CHECKING, Optional
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
from .linear_operators import LinearOperator, DiagonalSparseMatrixLinearOperator
|
|
6
|
+
from .linear_solvers import LinearSolver, IterativeLinearSolver
|
|
7
|
+
from .random_matrix import random_diagonal
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from .hilbert_space import Vector
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IdentityPreconditioningMethod(LinearSolver):
|
|
14
|
+
"""
|
|
15
|
+
A trivial preconditioning method that returns the Identity operator.
|
|
16
|
+
|
|
17
|
+
This acts as a "no-op" placeholder in the preconditioning framework,
|
|
18
|
+
useful for benchmarking or default configurations.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __call__(self, operator: LinearOperator) -> LinearOperator:
|
|
22
|
+
"""
|
|
23
|
+
Returns the identity operator for the domain of the input operator.
|
|
24
|
+
"""
|
|
25
|
+
return operator.domain.identity_operator()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JacobiPreconditioningMethod(LinearSolver):
|
|
29
|
+
"""
|
|
30
|
+
A LinearSolver wrapper that generates a Jacobi preconditioner.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
num_samples: Optional[int] = 20,
|
|
36
|
+
method: str = "variable",
|
|
37
|
+
rtol: float = 1e-2,
|
|
38
|
+
block_size: int = 10,
|
|
39
|
+
parallel: bool = True,
|
|
40
|
+
n_jobs: int = -1,
|
|
41
|
+
) -> None:
|
|
42
|
+
# Damping is removed: the operator passed to __call__ is already damped
|
|
43
|
+
self._num_samples = num_samples
|
|
44
|
+
self._method = method
|
|
45
|
+
self._rtol = rtol
|
|
46
|
+
self._block_size = block_size
|
|
47
|
+
self._parallel = parallel
|
|
48
|
+
self._n_jobs = n_jobs
|
|
49
|
+
|
|
50
|
+
def __call__(self, operator: LinearOperator) -> LinearOperator:
|
|
51
|
+
# Hutchinson's method or exact extraction on the damped normal operator
|
|
52
|
+
if self._num_samples is not None:
|
|
53
|
+
diag_values = random_diagonal(
|
|
54
|
+
operator.matrix(galerkin=True),
|
|
55
|
+
self._num_samples,
|
|
56
|
+
method=self._method,
|
|
57
|
+
rtol=self._rtol,
|
|
58
|
+
block_size=self._block_size,
|
|
59
|
+
parallel=self._parallel,
|
|
60
|
+
n_jobs=self._n_jobs,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
diag_values = operator.extract_diagonal(
|
|
64
|
+
galerkin=True, parallel=self._parallel, n_jobs=self._n_jobs
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
inv_diag = np.where(np.abs(diag_values) > 1e-14, 1.0 / diag_values, 1.0)
|
|
68
|
+
|
|
69
|
+
return DiagonalSparseMatrixLinearOperator.from_diagonal_values(
|
|
70
|
+
operator.domain, operator.domain, inv_diag, galerkin=True
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class SpectralPreconditioningMethod(LinearSolver):
|
|
75
|
+
"""
|
|
76
|
+
A LinearSolver wrapper that generates a spectral (low-rank) preconditioner.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
damping: float,
|
|
82
|
+
rank: int = 20,
|
|
83
|
+
power: int = 2,
|
|
84
|
+
) -> None:
|
|
85
|
+
self._damping = damping
|
|
86
|
+
self._rank = rank
|
|
87
|
+
self._power = power
|
|
88
|
+
|
|
89
|
+
def __call__(self, operator: LinearOperator) -> LinearOperator:
|
|
90
|
+
"""
|
|
91
|
+
Generates a spectral preconditioner.
|
|
92
|
+
Note: This assumes the operator provided is the data-misfit operator A*WA.
|
|
93
|
+
"""
|
|
94
|
+
space = operator.domain
|
|
95
|
+
|
|
96
|
+
# Use randomized eigendecomposition to find dominant modes
|
|
97
|
+
U, S = operator.random_eig(self._rank, power=self._power)
|
|
98
|
+
|
|
99
|
+
s_vals = S.extract_diagonal()
|
|
100
|
+
d_vals = s_vals / (s_vals + self._damping**2)
|
|
101
|
+
|
|
102
|
+
def mapping(r: Vector) -> Vector:
|
|
103
|
+
ut_r = U.adjoint(r)
|
|
104
|
+
d_ut_r = d_vals * ut_r
|
|
105
|
+
correction = U(d_ut_r)
|
|
106
|
+
|
|
107
|
+
diff = space.subtract(r, correction)
|
|
108
|
+
return space.multiply(1.0 / self._damping**2, diff)
|
|
109
|
+
|
|
110
|
+
return LinearOperator(space, space, mapping, adjoint_mapping=mapping)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class IterativePreconditioningMethod(LinearSolver):
|
|
114
|
+
"""
|
|
115
|
+
Wraps an iterative solver to act as a preconditioner.
|
|
116
|
+
|
|
117
|
+
This is best used with FCGSolver to handle the potential
|
|
118
|
+
variability of the inner iterations.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
inner_solver: IterativeLinearSolver,
|
|
124
|
+
max_inner_iter: int = 5,
|
|
125
|
+
rtol: float = 1e-1,
|
|
126
|
+
) -> None:
|
|
127
|
+
self._inner_solver = inner_solver
|
|
128
|
+
self._max_iter = max_inner_iter
|
|
129
|
+
self._rtol = rtol
|
|
130
|
+
|
|
131
|
+
def __call__(self, operator: LinearOperator) -> LinearOperator:
|
|
132
|
+
"""
|
|
133
|
+
Returns a LinearOperator whose action is 'solve the system'.
|
|
134
|
+
"""
|
|
135
|
+
# We override the inner solver parameters for efficiency
|
|
136
|
+
self._inner_solver._maxiter = self._max_iter
|
|
137
|
+
self._inner_solver._rtol = self._rtol
|
|
138
|
+
|
|
139
|
+
# The solver's __call__ returns the InverseLinearOperator
|
|
140
|
+
return self._inner_solver(operator)
|
pygeoinf/random_matrix.py
CHANGED
|
@@ -182,11 +182,14 @@ def variable_rank_random_range(
|
|
|
182
182
|
basis_vectors = np.hstack([basis_vectors, new_basis[:, :cols_to_add]])
|
|
183
183
|
|
|
184
184
|
if not converged and basis_vectors.shape[1] >= max_rank:
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
185
|
+
# If we reached the full dimension of the matrix,
|
|
186
|
+
# the result is exact, so no warning is needed.
|
|
187
|
+
if max_rank < min(m, n):
|
|
188
|
+
warnings.warn(
|
|
189
|
+
f"Tolerance {rtol} not met before reaching max_rank={max_rank}. "
|
|
190
|
+
"Result may be inaccurate. Consider increasing `max_rank` or `power`.",
|
|
191
|
+
UserWarning,
|
|
192
|
+
)
|
|
190
193
|
|
|
191
194
|
return basis_vectors
|
|
192
195
|
|