Core solver API: inverse and general solve¶
Keywords: solver · inverse · general solve · linear system
The `solver_basic_xx.py" demonstrate the equation-solving pipeline in G(3,0) float64.
Topics covered:
1 — Multivector inverse via solve() 2 — General solve: A * X = B 3 — Detecting a singular system (solve vs solve_lsq)
Source¶
ga/numerics/solver_basics_01.py
Code¶
# SPDX-License-Identifier: Apache-2.0
# Copyright 2021 Christian Perwass
"""
solver_basics_01.py — Core solver API: inverse and general solve.
The `solver_basic_xx.py" demonstrate the equation-solving pipeline in G(3,0) float64.
Topics covered:
1 — Multivector inverse via solve()
2 — General solve: A * X = B
3 — Detecting a singular system (solve vs solve_lsq)
Keywords: solver, inverse, general solve, linear system
"""
from __future__ import annotations
import numpy as np
from pytanga import MV, Algebra, EProduct, MVMatrix
from pytanga.matrix.convert import from_matrix, to_matrix
from pytanga.matrix.product import product_matrix
from pytanga.solver.solve import solve
def hr(title: str) -> None:
"""Print a title with a horizontal rule."""
print(f"\n{'─' * 60}\n{title}\n{'─' * 60}")
# ---------------------------------------------------------------------------
# Setup
# ---------------------------------------------------------------------------
# If you run this script for the first time, creating the algebra will trigger
# a one-time codegen step that may take a few seconds.
alg = Algebra(3, 0, "float64")
# ---------------------------------------------------------------------------
# Part A — Multivector inverse
# ---------------------------------------------------------------------------
hr("Multivector inverse (two equivalent paths)")
# Use a general (mixed-grade) multivector so the sub-algebra has full depth.
A = alg("0.5 + e1 - 2.0 e2")
print(f"A = {A}")
# Path 1: high-level one-liner — scalar 1.0 is coerced to MV automatically
B1 = solve(A, 1.0)
assert isinstance(B1, MV), "a scalar right-hand side solves to one MV"
print("\nPath 1 — solve(A, 1.0):")
print(f" B = {B1}")
check = A * B1
check.prune()
print(f" A*B = {check} (should be scalar 1)")
# Path 2: explicit step-by-step (educational)
# We want to solve A * B = 1 for B given A.
print("\nPath 2 — explicit step-by-step:")
# Now we create the product matrix.
# If no explicit blade masks are given, this function assumes multivector B may contain
# elements of the whole algebra and uses the non-zero components of A to determine
# the set of blades in the resulting multivector C (for A * B = C).
# The product matrix M is a linear map from the components of B to the components of C.
M = product_matrix(A, product=EProduct.GP)
# print(f"Product matrix M:\n{M}")
print(f" b_mask: {M.b_mask}")
print(f" c_mask: {M.c_mask}")
print(f" M shape: {M.data.shape}")
c_mv: MV = alg("1.0") # The right-hand side of the equation A * B = C
C = to_matrix(
c_mv, mask=M.c_mask
) # Convert C to a column vector with the same blade mask as M's output
if M.data.shape[1] == M.data.shape[2]:
# Solve the linear system M * B = C for B, where B is the unknown multivector.
b_arr = np.linalg.solve(M.data[0, :, :], C.data)
# Convert the solution back to a multivector using the blade mask of B.
B2 = from_matrix(MVMatrix(b_arr, M.b_mask))
assert isinstance(B2, MV), "a single-column matrix yields one MV"
print(f" B = {B2}")
# The two paths use different linear-algebra backends (`solve()` vs LAPACK),
# so their coefficients agree only to floating-point precision — compare
# with a tolerance rather than `==`.
d1 = B1.to_dict()
d2 = B2.to_dict()
keys = sorted(d1)
assert keys == sorted(d2), "paths disagree on the blade set"
assert np.allclose(
[d1[k] for k in keys], [d2[k] for k in keys], rtol=1e-12, atol=1e-12
), "paths disagree!"
print(" ✓ Both paths agree")
else:
print(
f" System is {M.data.shape[1]}x{M.data.shape[2]} — not square with this mask."
)
print(" (Use solve() which auto-derives a square closed sub-algebra mask.)")