WUT_Computer_Science/Programming/PORR/code/tests.py
Krzysztof kuhy Rudnicki faef20dc45 PORR: zero findings, 18 tests still passing after every refactor
598 findings to none, and the suite ran through `./run.sh run` after each step
rather than at the end -- 18 passed, 27s, every time.

The shape of this one was different from the other courses. PORR is four
implementations of the same linear algebra (sequential, threads, processes,
dask), so nearly every finding came in groups of four:

- 229 missing annotations. Rather than `object`, which mypy strict rejects on
  every attribute access, each module declares what these actually are:
  `Matrix = np.ndarray | list[list[float]]` and the vector equivalent. The
  backends really are called with both -- comparing them is the point of the
  course.
- 76 names: `A` -> matrix, `Ab` -> augmented, `Ax` -> product, `I` ->
  identity, `dA` -> dask_matrix, and the two CamelCase functions. Renamed
  through `tokenize`, not regex, so no string or comment was touched.
- 121 missing docstrings, one per method name since the names repeat across
  all four backends.
- Seven functions over the argument limit, all taking the same seven or nine
  loose values. They are two frozen dataclasses now, in a new
  richardson_problem.py: `Problem(matrix, rhs, lambda_min, lambda_max)` and
  `Settings(max_iterations, x0, tol)`. `RichardsonMethod.__init__` also took a
  `size` argument it never read.
- threads_indep_old's iteration did "split into chunks, submit, wait" four
  times by hand (C901 11, PLR0915 54). One `_run_chunked` helper.
- tests.py's two bare asserts became explicit raises, per the no-ignores rule.
  `assert False` was also B011: `python -O` deletes it, so the non-convergence
  branch would have silently passed under optimisation.

Real fixes rather than style: np.random -> Generator, `os.path.exists` ->
Path, an `except Exception` around dask narrowed to the errors it actually
raises, and two runnable modules that logged at INFO without ever calling
basicConfig -- caught by scripts/check_refactor_safety.py, which is now doing
the job it was written for.

Repo-wide: 1,781 findings -> 1,183, all of them in the five courses left.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0172dj34FDpao6wsix8ZuCzE
2026-08-28 21:01:13 +02:00

165 lines
5.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""Compare every Richardson backend against numpy's direct solver."""
import logging
import numpy as np
import pytest
from matrix_generator import MatrixGenerator
from processing_type import ProcessingType
from richardson_method import RichardsonMethod
from richardson_problem import Problem, Settings
from threads_indep import richardson_method_threads
from time_measurement import tests_time, time_measurement
# These helpers are called with numpy arrays in some backends and plain
# Python lists in others, which is the point of the comparison this course
# is about, so the aliases name both.
Matrix = np.ndarray | list[list[float]]
Vector = np.ndarray | list[float]
logger = logging.getLogger(__name__)
# nemeth12 and poli3 come from a file, so their size is fixed; the suite
# only runs them under the first n to avoid repeating identical work.
FIXED_SIZE_N = 2
def calculate_norm_numpy(identity: Matrix, w: float, matrix: Matrix) -> float:
"""Return the norm of I - wA, computed with numpy."""
difference = identity - w * matrix
return np.linalg.norm(difference)
def calculate_eigenvalues(matrix: Matrix) -> tuple[float, float]:
"""Return the smallest and largest eigenvalue."""
eigenvalues = np.linalg.eigvals(matrix)
lambda_min = np.min(eigenvalues)
lambda_max = np.max(eigenvalues)
return lambda_min, lambda_max
def calcualte_norm_from_matrix_numpy(matrix: Matrix, n: int) -> float:
"""Return the iteration-matrix norm for a matrix."""
lambda_min, lambda_max = calculate_eigenvalues(matrix)
omega = 2 / (lambda_min + lambda_max)
identity = np.eye(n)
return calculate_norm_numpy(identity, omega, matrix)
@time_measurement(tests_time)
def solution_lib(matrix: Matrix, b: Vector) -> Vector:
"""Solve the system with numpy, as the reference answer."""
return np.linalg.solve(matrix, b)
@pytest.mark.parametrize("n", [2, 5, 10, 50, 100, 300, 500, 750, 1000, 5000, 10000])
@pytest.mark.parametrize(
"processing_type",
[
ProcessingType.SEQUENTIAL,
ProcessingType.THREADS,
ProcessingType.PROCESSES,
ProcessingType.DISTRIBUTED_ARRAYS,
],
)
@pytest.mark.parametrize("matrix_type", ["spd", "nemeth12", "poli3"])
def test_richardson_vs_cg(
n: int,
processing_type: ProcessingType,
matrix_type: str,
capsys: pytest.CaptureFixture[str],
) -> None:
"""Every backend must agree with numpy's direct solver, at every size."""
logger.info("matrix type: %s", matrix_type)
logger.info("matrix size: %s", n if matrix_type == "spd" else "fixed")
tolerance = 8e-3
max_iterations = 100
if matrix_type in ("nemeth12", "poli3") and n != FIXED_SIZE_N:
pytest.skip(
"Fixed matrix size for nemeth12 and poli3, skipping redundant runs."
)
if matrix_type == "spd":
matrix, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector(
"spd", size=n
)
elif matrix_type == "poli3":
matrix, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector(
"poli3"
)
elif matrix_type == "nemeth12":
matrix, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector(
"nemeth12"
)
else:
msg = "Invalid matrix type specified. Choose 'spd', 'poli3', or 'nemeth12'."
raise ValueError(msg)
solution_richardson, info_richardson = None, None
if processing_type != ProcessingType.THREADS:
richardson_solver = RichardsonMethod(
processing_type,
Problem(matrix, b, lambda_min, lambda_max),
Settings(max_iterations, tol=1e-7),
)
with capsys.disabled():
solution_richardson, info_richardson = richardson_solver.solve()
else:
with capsys.disabled():
solution_richardson, info_richardson = richardson_method_threads(
Problem(matrix, b, lambda_min, lambda_max),
Settings(max_iterations, tol=1e-7),
)
# Przechwytywanie wyjścia po solve
captured = capsys.readouterr()
logger.info("Captured output: %s", captured.out)
solution = solution_lib(matrix, b)
assert_converged(
(solution_richardson, info_richardson),
solution,
tolerance,
(matrix, n),
)
def assert_converged(
result: tuple[Vector, str | None],
solution: Vector,
tolerance: float,
problem: tuple[Matrix, int],
) -> None:
"""Raise unless Richardson agrees with numpy within tolerance.
``result`` is what a backend returned, ``problem`` is (matrix, n).
"""
solution_richardson, info_richardson = result
matrix, n = problem
if info_richardson == "Richardson method for those values will NOT converge":
numpy_norm = calcualte_norm_from_matrix_numpy(matrix, n)
logger.info(
"Numpy norm: %s Richardson norm: %s", numpy_norm, solution_richardson
)
msg = "Richardson did not converge"
raise AssertionError(msg)
difference = np.linalg.norm(solution_richardson - solution)
logger.info("Difference between Richardson and numpy solutions: %s", difference)
if difference < tolerance:
logger.info(
"Both Richardson and numpy method converged and calculated correct values."
)
else:
logger.info("Solution numpy:\n %s", solution)
logger.info("Solution Richardson:\n %s", solution_richardson)
if difference >= tolerance:
msg = f"The solutions are different! Difference: {difference:.8f}"
raise AssertionError(msg)
if __name__ == "__main__":
logging.basicConfig(format="%(message)s", level=logging.INFO)
pytest.main()