mirror of
https://github.com/kuhyx/WUT_Computer_Science.git
synced 2026-11-10 19:47:32 +01:00
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
165 lines
5.6 KiB
Python
Executable File
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()
|