pygeoinf 1.3.6__py3-none-any.whl → 1.3.8__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 +23 -0
- pygeoinf/linear_optimisation.py +45 -226
- pygeoinf/linear_solvers.py +430 -0
- pygeoinf/plot.py +178 -116
- pygeoinf/preconditioners.py +140 -0
- pygeoinf/random_matrix.py +8 -5
- pygeoinf/symmetric_space/sh_tools.py +1 -1
- {pygeoinf-1.3.6.dist-info → pygeoinf-1.3.8.dist-info}/METADATA +1 -1
- {pygeoinf-1.3.6.dist-info → pygeoinf-1.3.8.dist-info}/RECORD +11 -10
- {pygeoinf-1.3.6.dist-info → pygeoinf-1.3.8.dist-info}/WHEEL +0 -0
- {pygeoinf-1.3.6.dist-info → pygeoinf-1.3.8.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
|