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
This commit is contained in:
Krzysztof Rudnicki 2026-08-28 21:01:13 +02:00
parent 42007d56e4
commit faef20dc45
13 changed files with 720 additions and 407 deletions

View File

@ -1,9 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Load the SemEval corpus: sentences, chunkings and gold alignments.""" """Load the SemEval corpus: sentences, chunkings and gold alignments."""
import sys
import logging import logging
import re import re
import sys
from pathlib import Path from pathlib import Path
import numpy as np import numpy as np

331
Programming/PORR/code/linear_algebra_utils.py Normal file → Executable file
View File

@ -1,121 +1,154 @@
#!/usr/bin/env python3
"""Four linear-algebra backends with the same interface.
Sequential, threads, processes and dask distributed arrays -- comparing them
is what this course is about.
"""
import cmath import cmath
import itertools import itertools
import logging import logging
import math import math
import multiprocessing import multiprocessing
import operator import operator
from abc import ABC, abstractmethod from abc import abstractmethod
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from functools import partial from functools import partial
from multiprocessing import Pool from multiprocessing import Pool
import dask.array as da import dask.array as da
import numpy as np
from time_measurement import time_accumulator, time_measurement from time_measurement import time_accumulator, 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__) logger = logging.getLogger(__name__)
class LinearAlgebraUtils: class LinearAlgebraUtils:
@staticmethod """The interface every linear-algebra backend implements."""
@abstractmethod
def dot_product(v1, v2) -> None:
pass
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def matrix_vector_multiply(A, x) -> None: def dot_product(v1: Vector, v2: Vector) -> None:
pass """Return the dot product of two vectors."""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def vector_norm(v) -> None: def matrix_vector_multiply(matrix: Matrix, x: Vector) -> None:
pass """Return the matrix-vector product."""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def vector_scalar_divide(x, scalar) -> None: def vector_norm(v: Vector) -> None:
pass """Return the Euclidean norm of a vector."""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def matrix_scalar_multiply(A, w) -> None: def vector_scalar_divide(x: Vector, scalar: float) -> None:
pass """Divide every element of a vector by a scalar."""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def vector_vector_subtraction(v1, v2) -> None: def matrix_scalar_multiply(matrix: Matrix, w: float) -> None:
pass """Multiply every element of a matrix by a scalar."""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def vector_vector_addition(v1, v2) -> None: def vector_vector_subtraction(v1: Vector, v2: Vector) -> None:
pass """Subtract one vector from another, elementwise."""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def scalar_vector_multiply(omega, vector) -> None: def vector_vector_addition(v1: Vector, v2: Vector) -> None:
pass """Add two vectors elementwise."""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def matrix_norm(A) -> None: def scalar_vector_multiply(omega: float, vector: Vector) -> None:
pass """Multiply every element of a vector by a scalar."""
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def matrix_matrix_subtraction(A, B) -> None: def matrix_norm(matrix: Matrix) -> None:
pass """Return the Frobenius norm of a matrix."""
@staticmethod
@abstractmethod
def matrix_matrix_subtraction(matrix: Matrix, other_matrix: Matrix) -> None:
"""Subtract one matrix from another, elementwise."""
class SequentialLinearAlgebraUtils: class SequentialLinearAlgebraUtils:
@staticmethod """Linear algebra in plain Python, one core."""
def dot_product(v1, v2):
return sum(x * y for x, y in zip(v1, v2))
@staticmethod @staticmethod
def matrix_vector_multiply(A, x): def dot_product(v1: Vector, v2: Vector) -> float:
return [SequentialLinearAlgebraUtils.dot_product(row, x) for row in A] """Return the dot product of two vectors."""
return sum(x * y for x, y in zip(v1, v2, strict=False))
@staticmethod @staticmethod
def vector_norm(v): def matrix_vector_multiply(matrix: Matrix, x: Vector) -> Vector:
"""Return the matrix-vector product."""
return [SequentialLinearAlgebraUtils.dot_product(row, x) for row in matrix]
@staticmethod
def vector_norm(v: Vector) -> float:
"""Return the Euclidean norm of a vector."""
x_values = (x * x for x in v) x_values = (x * x for x in v)
x_values_sum = sum(x_values) x_values_sum = sum(x_values)
return cmath.sqrt(x_values_sum).real return cmath.sqrt(x_values_sum).real
@staticmethod @staticmethod
def vector_scalar_divide(x, scalar): def vector_scalar_divide(x: Vector, scalar: float) -> Vector:
"""Divide every element of a vector by a scalar."""
return [xi / scalar for xi in x] return [xi / scalar for xi in x]
@staticmethod @staticmethod
def matrix_scalar_multiply(A, w): def matrix_scalar_multiply(matrix: Matrix, w: float) -> Matrix:
return A * w """Multiply every element of a matrix by a scalar."""
return matrix * w
@staticmethod @staticmethod
def vector_vector_subtraction(v1, v2): def vector_vector_subtraction(v1: Vector, v2: Vector) -> Vector:
return [x - y for x, y in zip(v1, v2)] """Subtract one vector from another, elementwise."""
return [x - y for x, y in zip(v1, v2, strict=False)]
@staticmethod @staticmethod
def vector_vector_addition(v1, v2): def vector_vector_addition(v1: Vector, v2: Vector) -> Vector:
return [x + y for x, y in zip(v1, v2)] """Add two vectors elementwise."""
return [x + y for x, y in zip(v1, v2, strict=False)]
@staticmethod @staticmethod
def scalar_vector_multiply(omega, vector): def scalar_vector_multiply(omega: float, vector: Vector) -> Vector:
"""Multiply every element of a vector by a scalar."""
return [omega * x for x in vector] return [omega * x for x in vector]
@staticmethod @staticmethod
def matrix_norm(A): def matrix_norm(matrix: Matrix) -> float:
return math.sqrt(sum(sum(element**2 for element in row) for row in A)) """Return the Frobenius norm of a matrix."""
return math.sqrt(sum(sum(element**2 for element in row) for row in matrix))
@staticmethod @staticmethod
def matrix_matrix_subtraction(A, B): def matrix_matrix_subtraction(matrix: Matrix, other_matrix: Matrix) -> Matrix:
return [[A[i][j] - B[i][j] for j in range(len(A[0]))] for i in range(len(A))] """Subtract one matrix from another, elementwise."""
return [
[matrix[i][j] - other_matrix[i][j] for j in range(len(matrix[0]))]
for i in range(len(matrix))
]
class ThreadsLinearAlgebraUtils: class ThreadsLinearAlgebraUtils:
"""Linear algebra split across threads."""
NUM_THREADS = multiprocessing.cpu_count() NUM_THREADS = multiprocessing.cpu_count()
@staticmethod @staticmethod
def get_chunk_size(data): def get_chunk_size(data: Vector) -> tuple[int, int, int]:
"""Work out the chunk size, thread count and remainder."""
num_elements = len(data) num_elements = len(data)
num_threads = min(ThreadsLinearAlgebraUtils.NUM_THREADS, num_elements) num_threads = min(ThreadsLinearAlgebraUtils.NUM_THREADS, num_elements)
chunk_size = num_elements // num_threads chunk_size = num_elements // num_threads
@ -123,7 +156,8 @@ class ThreadsLinearAlgebraUtils:
return chunk_size, num_threads, remainder return chunk_size, num_threads, remainder
@staticmethod @staticmethod
def divide_vectors_to_chunks(v1, v2): def divide_vectors_to_chunks(v1: Vector, v2: Vector) -> list[tuple[Vector, Vector]]:
"""Split two vectors into matching chunks."""
chunk_size, num_threads, remainder = ThreadsLinearAlgebraUtils.get_chunk_size( chunk_size, num_threads, remainder = ThreadsLinearAlgebraUtils.get_chunk_size(
v1 v1
) )
@ -138,7 +172,8 @@ class ThreadsLinearAlgebraUtils:
return chunks return chunks
@staticmethod @staticmethod
def divide_vector_or_matrix_to_chunks(v): def divide_vector_or_matrix_to_chunks(v: Vector) -> list[Vector]:
"""Split a vector or matrix into chunks."""
chunk_size, num_threads, remainder = ThreadsLinearAlgebraUtils.get_chunk_size(v) chunk_size, num_threads, remainder = ThreadsLinearAlgebraUtils.get_chunk_size(v)
chunks = [] chunks = []
@ -152,8 +187,9 @@ class ThreadsLinearAlgebraUtils:
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_vector_multiply(A, x): def matrix_vector_multiply(matrix: Matrix, x: Vector) -> Vector:
chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(A) """Return the matrix-vector product."""
chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(matrix)
with ThreadPoolExecutor( with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
) as executor: ) as executor:
@ -163,10 +199,12 @@ class ThreadsLinearAlgebraUtils:
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_norm(v): def vector_norm(v: Vector) -> float:
"""Return the Euclidean norm of a vector."""
chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(v) chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(v)
def partial_norm(chunk): def partial_norm(chunk: Vector) -> float:
"""Return the sum of squares of one chunk, for a parallel norm."""
return sum(x * x for x in chunk) return sum(x * x for x in chunk)
with ThreadPoolExecutor( with ThreadPoolExecutor(
@ -178,7 +216,8 @@ class ThreadsLinearAlgebraUtils:
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_vector_subtraction(v1, v2): def vector_vector_subtraction(v1: Vector, v2: Vector) -> Vector:
"""Subtract one vector from another, elementwise."""
chunks = ThreadsLinearAlgebraUtils.divide_vectors_to_chunks(v1, v2) chunks = ThreadsLinearAlgebraUtils.divide_vectors_to_chunks(v1, v2)
with ThreadPoolExecutor( with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
@ -193,7 +232,8 @@ class ThreadsLinearAlgebraUtils:
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_vector_addition(v1, v2): def vector_vector_addition(v1: Vector, v2: Vector) -> Vector:
"""Add two vectors elementwise."""
chunks = ThreadsLinearAlgebraUtils.divide_vectors_to_chunks(v1, v2) chunks = ThreadsLinearAlgebraUtils.divide_vectors_to_chunks(v1, v2)
with ThreadPoolExecutor( with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
@ -206,7 +246,8 @@ class ThreadsLinearAlgebraUtils:
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def scalar_vector_multiply(omega, vector): def scalar_vector_multiply(omega: float, vector: Vector) -> Vector:
"""Multiply every element of a vector by a scalar."""
chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(vector) chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(vector)
with ThreadPoolExecutor( with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
@ -222,77 +263,90 @@ class ThreadsLinearAlgebraUtils:
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def process_row(params): def process_row(params: tuple[object, ...]) -> Vector:
A, k, i = params """Eliminate one row against the pivot row."""
factor = A[i][k] / A[k][k] matrix, k, i = params
return [A[i][j] - factor * A[k][j] for j in range(len(A[0]))] factor = matrix[i][k] / matrix[k][k]
return [matrix[i][j] - factor * matrix[k][j] for j in range(len(matrix[0]))]
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def divide_by_scalar(pair): def divide_by_scalar(pair: tuple[Vector, float]) -> Vector:
"""Divide one chunk of a vector by its scalar."""
xi, scalar = pair xi, scalar = pair
return xi / scalar return xi / scalar
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def multiply_by_scalar(pair): def multiply_by_scalar(pair: tuple[Vector, float]) -> Vector:
"""Multiply one chunk of a vector by its scalar."""
element, scalar = pair element, scalar = pair
return element * scalar return element * scalar
class ProcessLinearAlgebraUtils: class ProcessLinearAlgebraUtils:
"""Linear algebra split across a process pool."""
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def dot_product(v1, v2): def dot_product(v1: Vector, v2: Vector) -> float:
"""Return the dot product of two vectors."""
with Pool() as pool: with Pool() as pool:
result = pool.starmap( result = pool.starmap(
ProcessLinearAlgebraUtils.multiply_elements, zip(v1, v2) ProcessLinearAlgebraUtils.multiply_elements, zip(v1, v2, strict=False)
) )
return sum(result) return sum(result)
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def multiply_elements(x, y): def multiply_elements(x: Vector, y: float) -> float:
"""Return the product of two numbers."""
return x * y return x * y
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_vector_multiply_row(params): def matrix_vector_multiply_row(params: tuple[object, ...]) -> Vector:
"""Return one row of a matrix-vector product."""
row, vector = params row, vector = params
return SequentialLinearAlgebraUtils.dot_product(row, vector) return SequentialLinearAlgebraUtils.dot_product(row, vector)
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_vector_multiply(A, x): def matrix_vector_multiply(matrix: Matrix, x: Vector) -> Vector:
"""Return the matrix-vector product."""
with Pool() as pool: with Pool() as pool:
result = pool.map( result = pool.map(
ProcessLinearAlgebraUtils.matrix_vector_multiply_row, ProcessLinearAlgebraUtils.matrix_vector_multiply_row,
[(row, x) for row in A], [(row, x) for row in matrix],
) )
return list(result) return list(result)
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_norm(v): def vector_norm(v: Vector) -> float:
"""Return the Euclidean norm of a vector."""
with Pool() as pool: with Pool() as pool:
squared = pool.map(ProcessLinearAlgebraUtils.square, v) squared = pool.map(ProcessLinearAlgebraUtils.square, v)
return math.sqrt(sum(squared)) return math.sqrt(sum(squared))
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def square(x): def square(x: Vector) -> float:
"""Return the square of a number."""
return x * x return x * x
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_scalar_divide(x, scalar): def vector_scalar_divide(x: Vector, scalar: float) -> Vector:
"""Divide every element of a vector by a scalar."""
with Pool() as pool: with Pool() as pool:
result = pool.map(divide_by_scalar, [(xi, scalar) for xi in x]) result = pool.map(divide_by_scalar, [(xi, scalar) for xi in x])
return list(result) return list(result)
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def divide_vector_by_scalar(x, scalar): def divide_vector_by_scalar(x: Vector, scalar: float) -> Vector:
"""Divide every element of a vector by a scalar."""
with Pool() as pool: with Pool() as pool:
result = pool.map( result = pool.map(
ProcessLinearAlgebraUtils.vector_scalar_divide, ProcessLinearAlgebraUtils.vector_scalar_divide,
@ -302,49 +356,54 @@ class ProcessLinearAlgebraUtils:
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_scalar_multiply_row(params): def matrix_scalar_multiply_row(params: tuple[object, ...]) -> Vector:
"""Multiply one row of a matrix by a scalar."""
row, w = params row, w = params
return [w * element for element in row] return [w * element for element in row]
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_scalar_multiply(A, w): def matrix_scalar_multiply(matrix: Matrix, w: float) -> Matrix:
"""Multiply every element of a matrix by a scalar."""
with Pool() as pool: with Pool() as pool:
result = pool.map( return pool.map(
ProcessLinearAlgebraUtils.matrix_scalar_multiply_row, ProcessLinearAlgebraUtils.matrix_scalar_multiply_row,
[(row, w) for row in A], [(row, w) for row in matrix],
) )
return result
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_vector_operation(params): def vector_vector_operation(params: tuple[object, ...]) -> Vector:
"""Apply one elementwise operation to a pair of chunks."""
v1, v2, op = params v1, v2, op = params
return op(v1, v2) return op(v1, v2)
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_vector_subtraction(v1, v2): def vector_vector_subtraction(v1: Vector, v2: Vector) -> Vector:
"""Subtract one vector from another, elementwise."""
with Pool() as pool: with Pool() as pool:
result = pool.map( result = pool.map(
ProcessLinearAlgebraUtils.vector_vector_operation, ProcessLinearAlgebraUtils.vector_vector_operation,
zip(v1, v2, itertools.repeat(operator.sub)), zip(v1, v2, itertools.repeat(operator.sub), strict=False),
) )
return list(result) return list(result)
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_vector_addition(v1, v2): def vector_vector_addition(v1: Vector, v2: Vector) -> Vector:
"""Add two vectors elementwise."""
with Pool() as pool: with Pool() as pool:
result = pool.map( result = pool.map(
ProcessLinearAlgebraUtils.vector_vector_operation, ProcessLinearAlgebraUtils.vector_vector_operation,
zip(v1, v2, itertools.repeat(operator.add)), zip(v1, v2, itertools.repeat(operator.add), strict=False),
) )
return list(result) return list(result)
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def scalar_vector_multiply(omega, vector): def scalar_vector_multiply(omega: float, vector: Vector) -> Vector:
"""Multiply every element of a vector by a scalar."""
with Pool() as pool: with Pool() as pool:
result = pool.map( result = pool.map(
multiply_by_scalar, [(element, omega) for element in vector] multiply_by_scalar, [(element, omega) for element in vector]
@ -353,120 +412,142 @@ class ProcessLinearAlgebraUtils:
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def sum_of_squares(row): def sum_of_squares(row: Vector) -> float:
"""Return the sum of the squares of a row."""
return sum(x**2 for x in row) return sum(x**2 for x in row)
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_norm(A): def matrix_norm(matrix: Matrix) -> float:
"""Return the Frobenius norm of a matrix."""
with Pool() as pool: with Pool() as pool:
row_sums = pool.map(ProcessLinearAlgebraUtils.sum_of_squares, A) row_sums = pool.map(ProcessLinearAlgebraUtils.sum_of_squares, matrix)
return math.sqrt(sum(row_sums)) return math.sqrt(sum(row_sums))
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def subtract_rows(row_from_A, row_from_B): def subtract_rows(row_from_matrix: Vector, row_from_other: Vector) -> Vector:
return [a - b for a, b in zip(row_from_A, row_from_B)] """Subtract one row from another, elementwise."""
return [a - b for a, b in zip(row_from_matrix, row_from_other, strict=False)]
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_matrix_subtraction(A, B): def matrix_matrix_subtraction(matrix: Matrix, other_matrix: Matrix) -> Matrix:
"""Subtract one matrix from another, elementwise."""
with Pool() as pool: with Pool() as pool:
result = pool.starmap(ProcessLinearAlgebraUtils.subtract_rows, zip(A, B)) return pool.starmap(
return result ProcessLinearAlgebraUtils.subtract_rows,
zip(matrix, other_matrix, strict=False),
)
class DistributedArraysLinearAlgebraUtils(ABC): class DistributedArraysLinearAlgebraUtils:
"""Linear algebra over dask distributed arrays."""
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def dot_product(v1, v2): def dot_product(v1: Vector, v2: Vector) -> float:
"""Return the dot product of two vectors."""
dv1 = da.from_array(v1, chunks="auto") dv1 = da.from_array(v1, chunks="auto")
dv2 = da.from_array(v2, chunks="auto") dv2 = da.from_array(v2, chunks="auto")
return da.dot(dv1, dv2).compute() return da.dot(dv1, dv2).compute()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_vector_multiply(A, x): def matrix_vector_multiply(matrix: Matrix, x: Vector) -> Vector:
dA = da.from_array(A, chunks="auto") """Return the matrix-vector product."""
dask_matrix = da.from_array(matrix, chunks="auto")
dx = da.from_array(x, chunks="auto") dx = da.from_array(x, chunks="auto")
return da.dot(dA, dx).compute().tolist() return da.dot(dask_matrix, dx).compute().tolist()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_norm(v): def vector_norm(v: Vector) -> float:
"""Return the Euclidean norm of a vector."""
dv = da.from_array(v, chunks="auto") dv = da.from_array(v, chunks="auto")
return da.linalg.norm(dv).compute() return da.linalg.norm(dv).compute()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_scalar_divide(x, scalar): def vector_scalar_divide(x: Vector, scalar: float) -> Vector:
"""Divide every element of a vector by a scalar."""
dx = da.from_array(x, chunks="auto") dx = da.from_array(x, chunks="auto")
return (dx / scalar).compute().tolist() return (dx / scalar).compute().tolist()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_scalar_multiply(A, w): def matrix_scalar_multiply(matrix: Matrix, w: float) -> Matrix:
dA = da.from_array(A, chunks="auto") """Multiply every element of a matrix by a scalar."""
return (dA * w).compute().tolist() dask_matrix = da.from_array(matrix, chunks="auto")
return (dask_matrix * w).compute().tolist()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_vector_subtraction(v1, v2): def vector_vector_subtraction(v1: Vector, v2: Vector) -> Vector:
"""Subtract one vector from another, elementwise."""
dv1 = da.from_array(v1, chunks="auto") dv1 = da.from_array(v1, chunks="auto")
dv2 = da.from_array(v2, chunks="auto") dv2 = da.from_array(v2, chunks="auto")
return (dv1 - dv2).compute().tolist() return (dv1 - dv2).compute().tolist()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def vector_vector_addition(v1, v2): def vector_vector_addition(v1: Vector, v2: Vector) -> Vector:
"""Add two vectors elementwise."""
dv1 = da.from_array(v1, chunks="auto") dv1 = da.from_array(v1, chunks="auto")
dv2 = da.from_array(v2, chunks="auto") dv2 = da.from_array(v2, chunks="auto")
return (dv1 + dv2).compute().tolist() return (dv1 + dv2).compute().tolist()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def scalar_vector_multiply(omega, vector): def scalar_vector_multiply(omega: float, vector: Vector) -> Vector:
"""Multiply every element of a vector by a scalar."""
dvector = da.from_array(vector, chunks="auto") dvector = da.from_array(vector, chunks="auto")
return (omega * dvector).compute().tolist() return (omega * dvector).compute().tolist()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_norm(A): def matrix_norm(matrix: Matrix) -> float:
dA = da.from_array(A, chunks="auto") """Return the Frobenius norm of a matrix."""
return da.linalg.norm(dA).compute() dask_matrix = da.from_array(matrix, chunks="auto")
return da.linalg.norm(dask_matrix).compute()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def matrix_matrix_subtraction(A, B): def matrix_matrix_subtraction(matrix: Matrix, other_matrix: Matrix) -> Matrix:
dA = da.from_array(A, chunks="auto") """Subtract one matrix from another, elementwise."""
dB = da.from_array(B, chunks="auto") dask_matrix = da.from_array(matrix, chunks="auto")
return (dA - dB).compute().tolist() dask_other = da.from_array(other_matrix, chunks="auto")
return (dask_matrix - dask_other).compute().tolist()
@staticmethod @staticmethod
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def gaussian_elimination(A, b): def gaussian_elimination(matrix: Matrix, b: Vector) -> Vector | None:
"""Solve the system by Gaussian elimination."""
try: try:
dA = da.from_array(A, chunks="auto") dask_matrix = da.from_array(matrix, chunks="auto")
db = da.from_array(b, chunks="auto") db = da.from_array(b, chunks="auto")
Ab = da.hstack([dA, db[:, None]]) augmented = da.hstack([dask_matrix, db[:, None]])
Ab = Ab.persist() augmented = augmented.persist()
def elimination_step(Ab, k): def elimination_step(augmented: Matrix, k: int) -> Matrix:
n = Ab.shape[0] """Run one Gaussian elimination step over the augmented matrix."""
max_index = da.argmax(da.abs(Ab[k:, k])) + k max_index = da.argmax(da.abs(augmented[k:, k])) + k
Ab[[k, max_index]] = Ab[[max_index, k]] augmented[[k, max_index]] = augmented[[max_index, k]]
Ab = Ab.persist() augmented = augmented.persist()
factor = Ab[k + 1 :, k] / Ab[k, k] factor = augmented[k + 1 :, k] / augmented[k, k]
Ab[k + 1 :] -= factor[:, None] * Ab[k] augmented[k + 1 :] -= factor[:, None] * augmented[k]
return Ab return augmented
for k in range(A.shape[0]): for k in range(matrix.shape[0]):
Ab = elimination_step(Ab, k) augmented = elimination_step(augmented, k)
x = da.zeros(A.shape[0]) x = da.zeros(matrix.shape[0])
for i in range(A.shape[0] - 1, -1, -1): for i in range(matrix.shape[0] - 1, -1, -1):
x[i] = (Ab[i, -1] - da.dot(Ab[i, i + 1 : -1], x[i + 1 :])) / Ab[i, i] x[i] = (
augmented[i, -1] - da.dot(augmented[i, i + 1 : -1], x[i + 1 :])
) / augmented[i, i]
return x.compute().tolist() return x.compute().tolist()
except Exception as e: except (ValueError, ArithmeticError) as exc:
logger.info("Error during Gaussian elimination: %s", e) # A singular or badly-scaled system; dask surfaces both here.
logger.info("Error during Gaussian elimination: %s", exc)
return None return None

1
Programming/PORR/code/main.py Normal file → Executable file
View File

@ -1,3 +1,4 @@
#!/usr/bin/env python3
"""Entry point for the PORR benchmark suite. """Entry point for the PORR benchmark suite.
Runs ``tests.py``, which compares this project's Richardson implementations Runs ``tests.py``, which compares this project's Richardson implementations

82
Programming/PORR/code/matrix_generator.py Normal file → Executable file
View File

@ -1,53 +1,71 @@
import os #!/usr/bin/env python3
"""Build and cache the SPD, nemeth12 and poli3 test problems."""
from pathlib import Path
import numpy as np import numpy as np
import scipy.io import scipy.io
# Generator rather than the legacy global np.random state (NPY002).
_RNG = np.random.default_rng()
# 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]
class MatrixGenerator: class MatrixGenerator:
"""Build or load the test problems this course measures against."""
@staticmethod @staticmethod
def generate_spd_matrix(n: int) -> np.ndarray: def generate_spd_matrix(n: int) -> np.ndarray:
A = np.random.rand(n, n) """Generate a symmetric positive-definite matrix."""
return np.dot(A, A.T) + n * MatrixGenerator.generate_identity_matrix( matrix = _RNG.random((n, n))
return np.dot(matrix, matrix.T) + n * MatrixGenerator.generate_identity_matrix(
n n
) # Adding n*I ensures positive definiteness ) # Adding n*I ensures positive definiteness
@staticmethod @staticmethod
def generate_identity_matrix(size): def generate_identity_matrix(size: int) -> Matrix:
"""Return an identity matrix of the given size."""
return np.eye(size) return np.eye(size)
@staticmethod @staticmethod
def generate_alternating_vector(size): def generate_alternating_vector(size: int) -> Vector:
"""Return a vector alternating between 1 and 2."""
return np.tile([1, 2], int(np.ceil(size / 2)))[:size] return np.tile([1, 2], int(np.ceil(size / 2)))[:size]
@staticmethod @staticmethod
def get_matrix_from_file(file_path, problem): def get_matrix_from_file(file_path: str, problem: int) -> Matrix:
"""Load a matrix from a MatrixMarket .mat file."""
mat_contents = scipy.io.loadmat(file_path) mat_contents = scipy.io.loadmat(file_path)
problem_record = mat_contents["Problem"][0][0] problem_record = mat_contents["Problem"][0][0]
A = problem_record[problem] matrix = problem_record[problem]
if scipy.sparse.issparse(A): dense = matrix.todense() if scipy.sparse.issparse(matrix) else matrix
A_dense = A.todense() return np.array(dense)
else:
A_dense = A
return np.array(A_dense)
@staticmethod @staticmethod
def generate_matrix_and_vector(type, size=None): def generate_matrix_and_vector(
if type == "spd": kind: str, size: int | None = None
) -> tuple[Matrix, Vector, float, float]:
"""Return the matrix, vector and eigenvalue bounds."""
if kind == "spd":
if size is None: if size is None:
raise ValueError("Size must be provided for SPD matrix generation.") msg = "Size must be provided for SPD matrix generation."
raise ValueError(msg)
try: try:
matrix, vector, lambda_min, lambda_max = MatrixGenerator.load_from_file( matrix, vector, lambda_min, lambda_max = MatrixGenerator.load_from_file(
"spd_" + str(size) + ".npz" "spd_" + str(size) + ".npz"
) )
except FileNotFoundError: except FileNotFoundError:
matrix = MatrixGenerator.generate_spd_matrix(size) matrix = MatrixGenerator.generate_spd_matrix(size)
vector = np.random.uniform(-1, 1, size) vector = _RNG.uniform(-1, 1, size)
lambda_min, lambda_max = MatrixGenerator.calculate_eigenvalues(matrix) lambda_min, lambda_max = MatrixGenerator.calculate_eigenvalues(matrix)
MatrixGenerator.save_to_file( MatrixGenerator.save_to_file(
matrix, vector, lambda_min, lambda_max, "spd_" + str(size) + ".npz" matrix, vector, lambda_min, lambda_max, "spd_" + str(size) + ".npz"
) )
elif type == "nemeth12": elif kind == "nemeth12":
try: try:
matrix, vector, lambda_min, lambda_max = MatrixGenerator.load_from_file( matrix, vector, lambda_min, lambda_max = MatrixGenerator.load_from_file(
"nemeth12.npz" "nemeth12.npz"
@ -60,7 +78,7 @@ class MatrixGenerator:
MatrixGenerator.save_to_file( MatrixGenerator.save_to_file(
matrix, vector, lambda_min, lambda_max, "nemeth12.npz" matrix, vector, lambda_min, lambda_max, "nemeth12.npz"
) )
elif type == "poli3": elif kind == "poli3":
try: try:
matrix, vector, lambda_min, lambda_max = MatrixGenerator.load_from_file( matrix, vector, lambda_min, lambda_max = MatrixGenerator.load_from_file(
"poli3.npz" "poli3.npz"
@ -74,21 +92,28 @@ class MatrixGenerator:
matrix, vector, lambda_min, lambda_max, "poli3.npz" matrix, vector, lambda_min, lambda_max, "poli3.npz"
) )
else: else:
raise ValueError( msg = "Invalid type specified. Choose 'spd', 'nemeth12', or 'poli3'."
"Invalid type specified. Choose 'spd', 'nemeth12', or 'poli3'." raise ValueError(msg)
)
return matrix, vector, lambda_min, lambda_max return matrix, vector, lambda_min, lambda_max
@staticmethod @staticmethod
def calculate_eigenvalues(A): def calculate_eigenvalues(matrix: Matrix) -> tuple[float, float]:
eigenvalues = np.linalg.eigvals(A) """Return the smallest and largest eigenvalue."""
eigenvalues = np.linalg.eigvals(matrix)
lambda_min = np.min(eigenvalues) lambda_min = np.min(eigenvalues)
lambda_max = np.max(eigenvalues) lambda_max = np.max(eigenvalues)
return lambda_min, lambda_max return lambda_min, lambda_max
@staticmethod @staticmethod
def save_to_file(matrix, vector, lambda_min, lambda_max, file_path) -> None: def save_to_file(
matrix: Matrix,
vector: Vector,
lambda_min: float,
lambda_max: float,
file_path: str,
) -> None:
"""Cache a generated problem so the next run can reuse it."""
np.savez( np.savez(
file_path, file_path,
matrix=matrix, matrix=matrix,
@ -97,9 +122,12 @@ class MatrixGenerator:
lambda_max=lambda_max, lambda_max=lambda_max,
) )
def load_from_file(file_path): @staticmethod
if not os.path.exists(file_path): def load_from_file(file_path: str) -> tuple[Matrix, Vector, float, float]:
raise FileNotFoundError(f"The file {file_path} does not exist.") """Load a cached problem."""
if not Path(file_path).exists():
msg = f"The file {file_path} does not exist."
raise FileNotFoundError(msg)
data = np.load(file_path) data = np.load(file_path)
matrix = data["matrix"] matrix = data["matrix"]
vector = data["vector"] vector = data["vector"]

5
Programming/PORR/code/processing_type.py Normal file → Executable file
View File

@ -1,7 +1,12 @@
#!/usr/bin/env python3
"""The enum naming the four processing backends."""
from enum import Enum, auto from enum import Enum, auto
class ProcessingType(Enum): class ProcessingType(Enum):
"""Which backend a run should use."""
SEQUENTIAL = auto() SEQUENTIAL = auto()
THREADS = auto() THREADS = auto()
PROCESSES = auto() PROCESSES = auto()

106
Programming/PORR/code/richardson_method.py Normal file → Executable file
View File

@ -1,79 +1,100 @@
#!/usr/bin/env python3
"""Richardson iteration, parameterised by linear-algebra backend."""
import gc import gc
import logging import logging
import time import time
import linear_algebra_utils as linAlg import linear_algebra_utils as lin_alg
import numpy as np
from processing_type import ProcessingType from processing_type import ProcessingType
from richardson_problem import Problem, Settings
from time_measurement import tests_time, time_accumulator, time_measurement from time_measurement import tests_time, time_accumulator, 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__) logger = logging.getLogger(__name__)
class RichardsonMethod: class RichardsonMethod:
"""Richardson iteration over a pluggable linear-algebra backend."""
@time_measurement(time_accumulator) @time_measurement(time_accumulator)
def __init__( def __init__(
self, self,
method: ProcessingType, method: ProcessingType,
A, problem: Problem,
b, settings: Settings,
lambda_min,
lambda_max,
max_iterations,
size: int,
x0=None,
tol=1e-5,
) -> None: ) -> None:
self.LinAlg = self.assign_LinAlgType(method) """Set up the solver for one problem.
self.A = A
self.b = b `size` used to be a parameter and was never read -- len(b) is the
self.x0 = x0 if x0 is not None else [0.0] * len(b) size, and it is right here.
self.max_iterations = max_iterations """
self.tol = tol self.LinAlg = self.assign_lin_alg_type(method)
self.lambda_min = lambda_min self.matrix = problem.matrix
self.lambda_max = lambda_max self.b = problem.rhs
self.x0 = settings.x0 if settings.x0 is not None else [0.0] * len(problem.rhs)
self.max_iterations = settings.max_iterations
self.tol = settings.tol
self.lambda_min = problem.lambda_min
self.lambda_max = problem.lambda_max
if self.lambda_min < 0: if self.lambda_min < 0:
raise ValueError("Matrix A is not positive semi-definite.") msg = "Matrix A is not positive semi-definite."
raise ValueError(msg)
self.omega = RichardsonMethod.calculate_omega(self.lambda_min, self.lambda_max) self.omega = RichardsonMethod.calculate_omega(self.lambda_min, self.lambda_max)
@staticmethod @staticmethod
def calculate_omega(lambda_min, lambda_max): def calculate_omega(lambda_min: float, lambda_max: float) -> float:
"""Return the optimal Richardson relaxation factor."""
return 2 / (lambda_min + lambda_max) return 2 / (lambda_min + lambda_max)
@staticmethod @staticmethod
def convergence_norm(LinAlgType, A, omega, I) -> bool: def convergence_norm(
wA = LinAlgType.matrix_scalar_multiply(A, omega) lin_alg_type: type, matrix: Matrix, omega: float, identity: Matrix
IMinuswA = LinAlgType.matrix_matrix_subtraction(I, wA) ) -> bool:
return LinAlgType.matrix_norm(IMinuswA) """Report whether the iteration matrix norm is under 1."""
scaled_matrix = lin_alg_type.matrix_scalar_multiply(matrix, omega)
identity_minus_scaled = lin_alg_type.matrix_matrix_subtraction(
identity, scaled_matrix
)
return lin_alg_type.matrix_norm(identity_minus_scaled)
@staticmethod @staticmethod
def assign_LinAlgType(method): def assign_lin_alg_type(method: ProcessingType) -> type:
"""Return the linear-algebra backend for a processing type."""
methods = { methods = {
ProcessingType.SEQUENTIAL: linAlg.SequentialLinearAlgebraUtils, ProcessingType.SEQUENTIAL: lin_alg.SequentialLinearAlgebraUtils,
ProcessingType.THREADS: linAlg.ThreadsLinearAlgebraUtils, ProcessingType.THREADS: lin_alg.ThreadsLinearAlgebraUtils,
ProcessingType.PROCESSES: linAlg.ProcessLinearAlgebraUtils, ProcessingType.PROCESSES: lin_alg.ProcessLinearAlgebraUtils,
ProcessingType.DISTRIBUTED_ARRAYS: linAlg.DistributedArraysLinearAlgebraUtils, ProcessingType.DISTRIBUTED_ARRAYS: (
lin_alg.DistributedArraysLinearAlgebraUtils
),
} }
try: try:
return methods[method] return methods[method]
except KeyError: except KeyError as exc:
raise ValueError( msg = "Unknown method, please use 'SEQUENTIAL', 'THREADS' or 'PROCESSES'."
"Unknown method, please use 'SEQUENTIAL', 'THREADS' or 'PROCESSES'." raise ValueError(msg) from exc
)
def solve(self): def solve(self) -> tuple[Vector, str | None]:
"""Run the Richardson iteration to convergence, or give up."""
gc.disable() gc.disable()
time_accumulator.total_time = 0 time_accumulator.total_time = 0
start = time.perf_counter() start = time.perf_counter()
x = self.x0[:] x = self.x0[:]
for iteration in range(self.max_iterations): for _iteration in range(self.max_iterations):
Ax = self.LinAlg.matrix_vector_multiply(self.A, x) product = self.LinAlg.matrix_vector_multiply(self.matrix, x)
residual = self.LinAlg.vector_vector_subtraction(self.b, Ax) residual = self.LinAlg.vector_vector_subtraction(self.b, product)
x = self.LinAlg.vector_vector_addition( x = self.LinAlg.vector_vector_addition(
x, self.LinAlg.scalar_vector_multiply(self.omega, residual) x, self.LinAlg.scalar_vector_multiply(self.omega, residual)
) )
if linAlg.SequentialLinearAlgebraUtils.vector_norm(residual) < self.tol: if lin_alg.SequentialLinearAlgebraUtils.vector_norm(residual) < self.tol:
break break
end = time.perf_counter() end = time.perf_counter()
@ -81,11 +102,11 @@ class RichardsonMethod:
gc.enable() gc.enable()
match self.LinAlg: match self.LinAlg:
case linAlg.SequentialLinearAlgebraUtils: case lin_alg.SequentialLinearAlgebraUtils:
logger.info( logger.info(
"Total: %ss, Tests time: %ss", total_time, tests_time.total_time "Total: %ss, Tests time: %ss", total_time, tests_time.total_time
) )
case linAlg.ThreadsLinearAlgebraUtils: case lin_alg.ThreadsLinearAlgebraUtils:
sequential_time = total_time - time_accumulator.total_time sequential_time = total_time - time_accumulator.total_time
logger.info( logger.info(
"Total: %ss, Seq: %ss, Parallel (threads): %ss, Tests time: %ss", "Total: %ss, Seq: %ss, Parallel (threads): %ss, Tests time: %ss",
@ -94,7 +115,7 @@ class RichardsonMethod:
time_accumulator.total_time, time_accumulator.total_time,
tests_time.total_time, tests_time.total_time,
) )
case linAlg.ProcessLinearAlgebraUtils: case lin_alg.ProcessLinearAlgebraUtils:
sequential_time = total_time - time_accumulator.total_time sequential_time = total_time - time_accumulator.total_time
logger.info( logger.info(
"Total: %ss, Seq: %ss, Parallel (processes): %ss, Tests time: %ss", "Total: %ss, Seq: %ss, Parallel (processes): %ss, Tests time: %ss",
@ -103,10 +124,11 @@ class RichardsonMethod:
time_accumulator.total_time, time_accumulator.total_time,
tests_time.total_time, tests_time.total_time,
) )
case linAlg.DistributedArraysLinearAlgebraUtils: case lin_alg.DistributedArraysLinearAlgebraUtils:
sequential_time = total_time - time_accumulator.total_time sequential_time = total_time - time_accumulator.total_time
logger.info( logger.info(
"Total: %ss, Seq: %ss, Parallel (distributed arrays): %ss, Tests time: %ss", "Total: %ss, Seq: %ss, Parallel (distributed arrays): %ss, "
"Tests time: %ss",
total_time, total_time,
sequential_time, sequential_time,
time_accumulator.total_time, time_accumulator.total_time,

74
Programming/PORR/code/richardson_mpi.py Normal file → Executable file
View File

@ -1,25 +1,42 @@
#!/usr/bin/env python3
"""Richardson iteration split across MPI ranks."""
import logging import logging
import time import time
import numpy as np import numpy as np
from mpi4py import MPI from mpi4py import MPI
from richardson_problem import Problem, Settings
# 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__) logger = logging.getLogger(__name__)
def richardson_parallel(A, b, lambda_min, lambda_max, tol=1e-5, max_iter=10000): def richardson_parallel(
problem: Problem, settings: Settings | None = None
) -> tuple[Vector, float]:
"""Run Richardson across MPI ranks."""
if settings is None:
settings = Settings(max_iterations=1000)
matrix = problem.matrix
b = problem.rhs
lambda_min = problem.lambda_min
lambda_max = problem.lambda_max
tol = settings.tol
max_iter = settings.max_iterations
comm = MPI.COMM_WORLD comm = MPI.COMM_WORLD
rank = comm.Get_rank() rank = comm.Get_rank()
size = comm.Get_size() size = comm.Get_size()
# Rozmiar macierzy A # Rozmiar macierzy A
n = A.shape[0] n = matrix.shape[0]
# Obliczanie wartości własnych tylko na jednym procesie # Obliczanie wartości własnych tylko na jednym procesie
if rank == 0: omega = 2 / (lambda_min + lambda_max) if rank == 0 else None
omega = 2 / (lambda_min + lambda_max)
else:
omega = None
# Rozgłoszenie omega do wszystkich procesów # Rozgłoszenie omega do wszystkich procesów
omega = comm.bcast(omega, root=0) omega = comm.bcast(omega, root=0)
@ -33,7 +50,7 @@ def richardson_parallel(A, b, lambda_min, lambda_max, tol=1e-5, max_iter=10000):
end_row = start_row + local_rows if rank != size - 1 else n end_row = start_row + local_rows if rank != size - 1 else n
# Przydzielenie lokalnych porcji A i b # Przydzielenie lokalnych porcji A i b
local_A = A[start_row:end_row, :] local_matrix = matrix[start_row:end_row, :]
local_b = b[start_row:end_row] local_b = b[start_row:end_row]
# Lokalny wektor residuum # Lokalny wektor residuum
@ -44,9 +61,9 @@ def richardson_parallel(A, b, lambda_min, lambda_max, tol=1e-5, max_iter=10000):
start_time = time.time() start_time = time.time()
for i in range(max_iter): for _i in range(max_iter):
# Oblicz lokalny residuum r = b - A @ x # Oblicz lokalny residuum r = b - A @ x
local_r[:] = local_b - np.dot(local_A, x) local_r[:] = local_b - np.dot(local_matrix, x)
# Tworzymy tymczasowy wektor o pełnym rozmiarze i kopiujemy lokalne dane # Tworzymy tymczasowy wektor o pełnym rozmiarze i kopiujemy lokalne dane
temp_r = np.zeros_like(b, dtype=np.float64) temp_r = np.zeros_like(b, dtype=np.float64)
@ -69,8 +86,11 @@ def richardson_parallel(A, b, lambda_min, lambda_max, tol=1e-5, max_iter=10000):
return x, execution_time return x, execution_time
def check_solution(A, b, x_approx, tolerance=8e-3): def check_solution(
x_true = np.linalg.solve(A, b) matrix: Matrix, b: Vector, x_approx: Vector, tolerance: float = 0.008
) -> tuple[bool, float]:
"""Report whether an approximate solution is within tolerance."""
x_true = np.linalg.solve(matrix, b)
error = np.linalg.norm(x_true - x_approx) error = np.linalg.norm(x_true - x_approx)
return error < tolerance, error return error < tolerance, error
@ -85,25 +105,25 @@ if __name__ == "__main__":
sizes = [2, 5, 10, 50, 80, 100, 300, 500, 750, 1000, 5000, 10000] sizes = [2, 5, 10, 50, 80, 100, 300, 500, 750, 1000, 5000, 10000]
for i in sizes: for i in sizes:
if rank == 0: if rank == 0:
A, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector( matrix, b, lambda_min, lambda_max = (
"spd", size=i MatrixGenerator.generate_matrix_and_vector("spd", size=i)
) )
else: else:
A = None matrix = None
b = None b = None
lambda_min = None lambda_min = None
lambda_max = None lambda_max = None
A = comm.bcast(A, root=0) matrix = comm.bcast(matrix, root=0)
b = comm.bcast(b, root=0) b = comm.bcast(b, root=0)
# Rozwiązanie przy użyciu zrównoleglonej metody Richardsona # Rozwiązanie przy użyciu zrównoleglonej metody Richardsona
x, time_taken = richardson_parallel(A, b, lambda_min, lambda_max) x, time_taken = richardson_parallel(Problem(matrix, b, lambda_min, lambda_max))
# Sprawdzanie poprawności rozwiązania (na procesie 0) # Sprawdzanie poprawności rozwiązania (na procesie 0)
if rank == 0: if rank == 0:
logger.info("Spd matrix with size %s", i) logger.info("Spd matrix with size %s", i)
is_correct, error = check_solution(A, b, x) is_correct, error = check_solution(matrix, b, x)
logger.info("Czas wykonania [s]: %s", time_taken) logger.info("Czas wykonania [s]: %s", time_taken)
logger.info( logger.info(
"Czy rozwiązanie jest poprawne: %s", "Tak" if is_correct else "Nie" "Czy rozwiązanie jest poprawne: %s", "Tak" if is_correct else "Nie"
@ -111,49 +131,49 @@ if __name__ == "__main__":
logger.info("Błąd rozwiązania: %s", error) logger.info("Błąd rozwiązania: %s", error)
if rank == 0: if rank == 0:
A, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector( matrix, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector(
"nemeth12" "nemeth12"
) )
else: else:
A = None matrix = None
b = None b = None
lambda_min = None lambda_min = None
lambda_max = None lambda_max = None
A = comm.bcast(A, root=0) matrix = comm.bcast(matrix, root=0)
b = comm.bcast(b, root=0) b = comm.bcast(b, root=0)
# Rozwiązanie przy użyciu zrównoleglonej metody Richardsona # Rozwiązanie przy użyciu zrównoleglonej metody Richardsona
x, time_taken = richardson_parallel(A, b, lambda_min, lambda_max) x, time_taken = richardson_parallel(Problem(matrix, b, lambda_min, lambda_max))
# Sprawdzanie poprawności rozwiązania (na procesie 0) # Sprawdzanie poprawności rozwiązania (na procesie 0)
if rank == 0: if rank == 0:
logger.info("Nemeth12 matrix") logger.info("Nemeth12 matrix")
is_correct, error = check_solution(A, b, x) is_correct, error = check_solution(matrix, b, x)
logger.info("Czas wykonania [s]: %s", time_taken) logger.info("Czas wykonania [s]: %s", time_taken)
logger.info("Czy rozwiązanie jest poprawne: %s", "Tak" if is_correct else "Nie") logger.info("Czy rozwiązanie jest poprawne: %s", "Tak" if is_correct else "Nie")
logger.info("Błąd rozwiązania: %s", error) logger.info("Błąd rozwiązania: %s", error)
if rank == 0: if rank == 0:
A, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector( matrix, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector(
"poli3" "poli3"
) )
else: else:
A = None matrix = None
b = None b = None
lambda_min = None lambda_min = None
lambda_max = None lambda_max = None
A = comm.bcast(A, root=0) matrix = comm.bcast(matrix, root=0)
b = comm.bcast(b, root=0) b = comm.bcast(b, root=0)
# Rozwiązanie przy użyciu zrównoleglonej metody Richardsona # Rozwiązanie przy użyciu zrównoleglonej metody Richardsona
x, time_taken = richardson_parallel(A, b, lambda_min, lambda_max) x, time_taken = richardson_parallel(Problem(matrix, b, lambda_min, lambda_max))
# Sprawdzanie poprawności rozwiązania (na procesie 0) # Sprawdzanie poprawności rozwiązania (na procesie 0)
if rank == 0: if rank == 0:
logger.info("Poli3 matrix") logger.info("Poli3 matrix")
is_correct, error = check_solution(A, b, x) is_correct, error = check_solution(matrix, b, x)
logger.info("Czas wykonania [s]: %s", time_taken) logger.info("Czas wykonania [s]: %s", time_taken)
logger.info("Czy rozwiązanie jest poprawne: %s", "Tak" if is_correct else "Nie") logger.info("Czy rozwiązanie jest poprawne: %s", "Tak" if is_correct else "Nie")
logger.info("Błąd rozwiązania: %s", error) logger.info("Błąd rozwiązania: %s", error)

View File

@ -0,0 +1,34 @@
#!/usr/bin/env python3
"""The system to solve, and the settings to solve it with.
Every Richardson backend in this project took the same seven or nine loose
arguments -- matrix, right-hand side, both eigenvalue bounds, iteration cap,
starting guess and tolerance -- which is what put all of them over ruff's
argument limit. They travel together, so they are two dataclasses.
"""
from dataclasses import dataclass
import numpy as np
Matrix = np.ndarray | list[list[float]]
Vector = np.ndarray | list[float]
@dataclass(frozen=True)
class Problem:
"""One linear system, with the eigenvalue bounds Richardson needs."""
matrix: Matrix
rhs: Vector
lambda_min: float
lambda_max: float
@dataclass(frozen=True)
class Settings:
"""How hard to try before giving up."""
max_iterations: int
x0: Vector | None = None
tol: float = 1e-05

122
Programming/PORR/code/tests.py Normal file → Executable file
View File

@ -1,3 +1,6 @@
#!/usr/bin/env python3
"""Compare every Richardson backend against numpy's direct solver."""
import logging import logging
import numpy as np import numpy as np
@ -5,34 +8,48 @@ import pytest
from matrix_generator import MatrixGenerator from matrix_generator import MatrixGenerator
from processing_type import ProcessingType from processing_type import ProcessingType
from richardson_method import RichardsonMethod from richardson_method import RichardsonMethod
from threads_indep import RichardsonMethodThreads from richardson_problem import Problem, Settings
from threads_indep import richardson_method_threads
from time_measurement import tests_time, time_measurement 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__) 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(I, w, A):
difference = I - w * A 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) return np.linalg.norm(difference)
def calculate_eigenvalues(A): def calculate_eigenvalues(matrix: Matrix) -> tuple[float, float]:
eigenvalues = np.linalg.eigvals(A) """Return the smallest and largest eigenvalue."""
eigenvalues = np.linalg.eigvals(matrix)
lambda_min = np.min(eigenvalues) lambda_min = np.min(eigenvalues)
lambda_max = np.max(eigenvalues) lambda_max = np.max(eigenvalues)
return lambda_min, lambda_max return lambda_min, lambda_max
def calcualte_norm_from_matrix_numpy(A, n): def calcualte_norm_from_matrix_numpy(matrix: Matrix, n: int) -> float:
lambda_min, lambda_max = calculate_eigenvalues(A) """Return the iteration-matrix norm for a matrix."""
lambda_min, lambda_max = calculate_eigenvalues(matrix)
omega = 2 / (lambda_min + lambda_max) omega = 2 / (lambda_min + lambda_max)
I = np.eye(n) identity = np.eye(n)
return calculate_norm_numpy(I, omega, A) return calculate_norm_numpy(identity, omega, matrix)
@time_measurement(tests_time) @time_measurement(tests_time)
def solution_lib(A, b): def solution_lib(matrix: Matrix, b: Vector) -> Vector:
return np.linalg.solve(A, b) """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("n", [2, 5, 10, 50, 100, 300, 500, 750, 1000, 5000, 10000])
@ -47,86 +64,99 @@ def solution_lib(A, b):
) )
@pytest.mark.parametrize("matrix_type", ["spd", "nemeth12", "poli3"]) @pytest.mark.parametrize("matrix_type", ["spd", "nemeth12", "poli3"])
def test_richardson_vs_cg( def test_richardson_vs_cg(
n: int, processing_type: ProcessingType, matrix_type: str, capsys n: int,
processing_type: ProcessingType,
matrix_type: str,
capsys: pytest.CaptureFixture[str],
) -> None: ) -> None:
"""Every backend must agree with numpy's direct solver, at every size."""
logger.info("matrix type: %s", matrix_type) logger.info("matrix type: %s", matrix_type)
logger.info("matrix size: %s", n if matrix_type == "spd" else "fixed") logger.info("matrix size: %s", n if matrix_type == "spd" else "fixed")
tolerance = 8e-3 tolerance = 8e-3
max_iterations = 100 max_iterations = 100
if matrix_type in ["nemeth12", "poli3"] and n != 2: if matrix_type in ("nemeth12", "poli3") and n != FIXED_SIZE_N:
pytest.skip( pytest.skip(
"Fixed matrix size for nemeth12 and poli3, skipping redundant runs." "Fixed matrix size for nemeth12 and poli3, skipping redundant runs."
) )
if matrix_type == "spd": if matrix_type == "spd":
A, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector( matrix, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector(
"spd", size=n "spd", size=n
) )
elif matrix_type == "poli3": elif matrix_type == "poli3":
A, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector( matrix, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector(
"poli3" "poli3"
) )
elif matrix_type == "nemeth12": elif matrix_type == "nemeth12":
A, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector( matrix, b, lambda_min, lambda_max = MatrixGenerator.generate_matrix_and_vector(
"nemeth12" "nemeth12"
) )
else: else:
raise ValueError( msg = "Invalid matrix type specified. Choose 'spd', 'poli3', or 'nemeth12'."
"Invalid matrix type specified. Choose 'spd', 'poli3', or 'nemeth12'." raise ValueError(msg)
)
solution_richardson, info_richardson = None, None solution_richardson, info_richardson = None, None
if processing_type != ProcessingType.THREADS: if processing_type != ProcessingType.THREADS:
richardson_solver = RichardsonMethod( richardson_solver = RichardsonMethod(
processing_type, processing_type,
A, Problem(matrix, b, lambda_min, lambda_max),
b, Settings(max_iterations, tol=1e-7),
lambda_min,
lambda_max,
max_iterations,
size=A.shape[0],
tol=1e-7,
) )
with capsys.disabled(): with capsys.disabled():
solution_richardson, info_richardson = richardson_solver.solve() solution_richardson, info_richardson = richardson_solver.solve()
else: else:
with capsys.disabled(): with capsys.disabled():
solution_richardson, info_richardson = RichardsonMethodThreads( solution_richardson, info_richardson = richardson_method_threads(
A, b, lambda_min, lambda_max, max_iterations, tol=1e-7 Problem(matrix, b, lambda_min, lambda_max),
Settings(max_iterations, tol=1e-7),
) )
# Przechwytywanie wyjścia po solve # Przechwytywanie wyjścia po solve
captured = capsys.readouterr() captured = capsys.readouterr()
logger.info("Captured output: %s", captured.out) logger.info("Captured output: %s", captured.out)
solution = solution_lib(A, b) solution = solution_lib(matrix, b)
assert_converged(solution_richardson, info_richardson, solution, tolerance, A, n) assert_converged(
(solution_richardson, info_richardson),
solution,
tolerance,
(matrix, n),
)
def assert_converged( def assert_converged(
solution_richardson, info_richardson, solution, tolerance, A, n result: tuple[Vector, str | None],
solution: Vector,
tolerance: float,
problem: tuple[Matrix, int],
) -> None: ) -> 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": if info_richardson == "Richardson method for those values will NOT converge":
numpy_norm = calcualte_norm_from_matrix_numpy(A, n) numpy_norm = calcualte_norm_from_matrix_numpy(matrix, n)
logger.info( logger.info(
"Numpy norm: %s Richardson norm: %s", numpy_norm, solution_richardson "Numpy norm: %s Richardson norm: %s", numpy_norm, solution_richardson
) )
assert False, "Richardson did not converge" msg = "Richardson did not converge"
else: raise AssertionError(msg)
difference = np.linalg.norm(solution_richardson - solution) difference = np.linalg.norm(solution_richardson - solution)
logger.info("Difference between Richardson and numpy solutions: %s", difference) logger.info("Difference between Richardson and numpy solutions: %s", difference)
if difference < tolerance: if difference < tolerance:
logger.info( logger.info(
"Both Richardson and numpy method converged and calculated correct values." "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)
assert difference < tolerance, (
f"The solutions are different! Difference: {difference:.8f}"
) )
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__": if __name__ == "__main__":

88
Programming/PORR/code/threads_indep.py Normal file → Executable file
View File

@ -1,9 +1,14 @@
#!/usr/bin/env python3
"""Threaded Richardson with njit-compiled kernels."""
import gc import gc
import logging import logging
import time import time
import linear_algebra_utils as linAlg import linear_algebra_utils as lin_alg
import numpy as np
from numba import njit, prange from numba import njit, prange
from richardson_problem import Problem, Settings
from time_measurement import ( from time_measurement import (
longest_threads_time_accumulator, longest_threads_time_accumulator,
tests_time, tests_time,
@ -12,60 +17,89 @@ from time_measurement import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Called with numpy arrays here and with plain lists in the sequential
# backend; the aliases name both.
Matrix = np.ndarray | list[list[float]]
Vector = np.ndarray | list[float]
@njit(parallel=True) @njit(parallel=True)
def numba_matrix_vector_multiply(A, input_x, Ax) -> None: def numba_matrix_vector_multiply(
for i in prange(len(A)): matrix: Matrix, input_x: Vector, product: Vector
) -> None:
"""Matrix-vector product, njit-compiled."""
for i in prange(len(matrix)):
acc = 0.0 acc = 0.0
for j in range(len(input_x)): for j in range(len(input_x)):
acc += A[i][j] * input_x[j] acc += matrix[i][j] * input_x[j]
Ax[i] = acc product[i] = acc
@njit(parallel=True) @njit(parallel=True)
def numba_vector_vector_subtraction(b, Ax, residual) -> None: def numba_vector_vector_subtraction(
b: Vector, product: Vector, residual: Vector
) -> None:
"""Elementwise subtraction, njit-compiled."""
for i in prange(len(b)): for i in prange(len(b)):
residual[i] = b[i] - Ax[i] residual[i] = b[i] - product[i]
@njit(nopython=True) @njit(nopython=True)
def numba_scalar_vector_multiply(omega, vector, result) -> None: def numba_scalar_vector_multiply(omega: float, vector: Vector, result: Vector) -> None:
"""Scalar-vector product, njit-compiled."""
omega_real = omega.real omega_real = omega.real
for i in range(len(vector)): for i in range(len(vector)):
result[i] = omega_real * vector[i] result[i] = omega_real * vector[i]
@njit(parallel=True) @njit(parallel=True)
def numba_vector_vector_addition(input_x, vector, output_x) -> None: def numba_vector_vector_addition(
input_x: Vector, vector: Vector, output_x: Vector
) -> None:
"""Elementwise addition, njit-compiled."""
for i in prange(len(input_x)): for i in prange(len(input_x)):
output_x[i] = input_x[i] + vector[i] output_x[i] = input_x[i] + vector[i]
# Funkcje z dekoratorem
# Funkcje z dekoratorem
@time_measurement_longest(longest_threads_time_accumulator)
def matrix_vector_multiply(A, input_x, Ax) -> None:
numba_matrix_vector_multiply(A, input_x, Ax)
@time_measurement_longest(longest_threads_time_accumulator) @time_measurement_longest(longest_threads_time_accumulator)
def vector_vector_subtraction(b, Ax, residual) -> None: def matrix_vector_multiply(matrix: Matrix, input_x: Vector, product: Vector) -> None:
numba_vector_vector_subtraction(b, Ax, residual) """Return the matrix-vector product."""
numba_matrix_vector_multiply(matrix, input_x, product)
@time_measurement_longest(longest_threads_time_accumulator) @time_measurement_longest(longest_threads_time_accumulator)
def scalar_vector_multiply(omega, vector, result) -> None: def vector_vector_subtraction(b: Vector, product: Vector, residual: Vector) -> None:
"""Subtract one vector from another, elementwise."""
numba_vector_vector_subtraction(b, product, residual)
@time_measurement_longest(longest_threads_time_accumulator)
def scalar_vector_multiply(omega: float, vector: Vector, result: Vector) -> None:
"""Multiply every element of a vector by a scalar."""
numba_scalar_vector_multiply(omega, vector, result) numba_scalar_vector_multiply(omega, vector, result)
@time_measurement_longest(longest_threads_time_accumulator) @time_measurement_longest(longest_threads_time_accumulator)
def vector_vector_addition(input_x, vector, output_x) -> None: def vector_vector_addition(input_x: Vector, vector: Vector, output_x: Vector) -> None:
"""Add two vectors elementwise."""
numba_vector_vector_addition(input_x, vector, output_x) numba_vector_vector_addition(input_x, vector, output_x)
# Metoda Richardson z obsługą wątków
# Metoda Richardson z obsługą wątków
def RichardsonMethodThreads( def richardson_method_threads(
A, b, lambda_min, lambda_max, max_iterations, x0=None, tol=1e-5 problem: Problem, settings: Settings
): ) -> tuple[Vector, str | None]:
"""Run Richardson with one thread per chunk."""
matrix = problem.matrix
b = problem.rhs
lambda_min = problem.lambda_min
lambda_max = problem.lambda_max
max_iterations = settings.max_iterations
x0 = settings.x0
tol = settings.tol
longest_threads_time_accumulator.hard_reset() longest_threads_time_accumulator.hard_reset()
gc.disable() gc.disable()
@ -77,13 +111,13 @@ def RichardsonMethodThreads(
omega = 2 / (lambda_min + lambda_max) omega = 2 / (lambda_min + lambda_max)
n = len(b) n = len(b)
for iteration in range(max_iterations): for _iteration in range(max_iterations):
Ax = [0.0] * n product = [0.0] * n
matrix_vector_multiply(A, x, Ax) matrix_vector_multiply(matrix, x, product)
longest_threads_time_accumulator.save_lap_and_reset() longest_threads_time_accumulator.save_lap_and_reset()
residual = [0.0] * n residual = [0.0] * n
vector_vector_subtraction(b, Ax, residual) vector_vector_subtraction(b, product, residual)
longest_threads_time_accumulator.save_lap_and_reset() longest_threads_time_accumulator.save_lap_and_reset()
change_vector = [0.0] * n change_vector = [0.0] * n
@ -96,7 +130,7 @@ def RichardsonMethodThreads(
x = _x[:] x = _x[:]
if linAlg.SequentialLinearAlgebraUtils.vector_norm(residual) < tol: if lin_alg.SequentialLinearAlgebraUtils.vector_norm(residual) < tol:
break break
end_time = time.perf_counter() end_time = time.perf_counter()

192
Programming/PORR/code/threads_indep_old.py Normal file → Executable file
View File

@ -1,10 +1,16 @@
#!/usr/bin/env python3
"""Threaded Richardson, the earlier index-slicing version."""
import gc import gc
import logging import logging
import multiprocessing import multiprocessing
import time import time
from collections.abc import Callable
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import linear_algebra_utils as linAlg import linear_algebra_utils as lin_alg
import numpy as np
from richardson_problem import Problem, Settings
from time_measurement import ( from time_measurement import (
longest_threads_time_accumulator, longest_threads_time_accumulator,
tests_time, tests_time,
@ -13,30 +19,89 @@ from time_measurement import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Called with numpy arrays here and with plain lists in the sequential
@time_measurement_longest(longest_threads_time_accumulator) # backend; the aliases name both.
def matrix_vector_multiply(A, input_x, start, end, Ax) -> None: Matrix = np.ndarray | list[list[float]]
Ax[start:end] = [sum(x * y for x, y in zip(row, input_x)) for row in A[start:end]] Vector = np.ndarray | list[float]
@time_measurement_longest(longest_threads_time_accumulator) @time_measurement_longest(longest_threads_time_accumulator)
def vector_vector_subtraction(b, Ax, start, end, residual) -> None: def matrix_vector_multiply(
residual[start:end] = [x - y for x, y in zip(b[start:end], Ax[start:end])] matrix: Matrix, input_x: Vector, start: int, end: int, product: Vector
) -> None:
"""Return the matrix-vector product."""
product[start:end] = [
sum(x * y for x, y in zip(row, input_x, strict=False))
for row in matrix[start:end]
]
@time_measurement_longest(longest_threads_time_accumulator) @time_measurement_longest(longest_threads_time_accumulator)
def scalar_vector_multiply(omega, vector, start, end, result) -> None: def vector_vector_subtraction(
b: Vector, product: Vector, start: int, end: int, residual: Vector
) -> None:
"""Subtract one vector from another, elementwise."""
residual[start:end] = [
x - y for x, y in zip(b[start:end], product[start:end], strict=False)
]
@time_measurement_longest(longest_threads_time_accumulator)
def scalar_vector_multiply(
omega: float, vector: Vector, start: int, end: int, result: Vector
) -> None:
"""Multiply every element of a vector by a scalar."""
result[start:end] = [omega * x for x in vector[start:end]] result[start:end] = [omega * x for x in vector[start:end]]
@time_measurement_longest(longest_threads_time_accumulator) @time_measurement_longest(longest_threads_time_accumulator)
def vector_vector_addition(input_x, vector, start, end, output_x) -> None: def vector_vector_addition(
output_x[start:end] = [x + y for x, y in zip(input_x[start:end], vector[start:end])] input_x: Vector, vector: Vector, start: int, end: int, output_x: Vector
) -> None:
"""Add two vectors elementwise."""
output_x[start:end] = [
x + y for x, y in zip(input_x[start:end], vector[start:end], strict=False)
]
def RichardsonMethodThreads( def _run_chunked(
A, b, lambda_min, lambda_max, max_iterations, x0=None, tol=1e-5 executor: ThreadPoolExecutor,
): layout: tuple[int, int, int],
func: Callable[..., None],
args: tuple[object, ...],
) -> None:
"""Split a row range into per-thread chunks, submit them, and wait.
``layout`` is (num_threads, n, chunk_size). The four stages of one
Richardson iteration each did this by hand, which is what made the
iteration too complex and too long for ruff.
"""
num_threads, n, chunk_size = layout
futures = []
for i in range(num_threads):
start = i * chunk_size
end = n if i == num_threads - 1 else (i + 1) * chunk_size
futures.append(executor.submit(func, *args[:-1], start, end, args[-1]))
for future in futures:
future.result()
longest_threads_time_accumulator.save_lap_and_reset()
def richardson_method_threads(
problem: Problem, settings: Settings
) -> tuple[Vector, str | None]:
"""Run Richardson with one thread per chunk.
The unpacking below is deliberate: the body predates Problem/Settings and
reads these as locals throughout.
"""
matrix = problem.matrix
b = problem.rhs
lambda_min = problem.lambda_min
lambda_max = problem.lambda_max
max_iterations = settings.max_iterations
x0 = settings.x0
tol = settings.tol
longest_threads_time_accumulator.hard_reset() longest_threads_time_accumulator.hard_reset()
gc.disable() gc.disable()
@ -48,89 +113,36 @@ def RichardsonMethodThreads(
omega = 2 / (lambda_min + lambda_max) omega = 2 / (lambda_min + lambda_max)
num_threads = multiprocessing.cpu_count() num_threads = multiprocessing.cpu_count()
chunk_size = n // num_threads chunk_size = n // num_threads
layout = (num_threads, n, chunk_size)
with ThreadPoolExecutor( with ThreadPoolExecutor(
max_workers=num_threads max_workers=num_threads
) as executor: # wątki są tworzone raz i nie są niszczone ) as executor: # wątki są tworzone raz i nie są niszczone
for iteration in range(max_iterations): for _iteration in range(max_iterations):
Ax = [0] * len( # holds the result of multiplying A by x
x product = [0] * len(x)
) # tutaj zostanie przypisany wynik z mnożenia macierzy A z wektorem x _run_chunked(executor, layout, matrix_vector_multiply, (matrix, x, product))
futures = [] # holds the result of b - Ax
residual = [0] * len(b)
for i in range(num_threads): _run_chunked(
start = i * chunk_size executor, layout, vector_vector_subtraction, (b, product, residual)
end = n if i == num_threads - 1 else (i + 1) * chunk_size )
futures.append( # holds omega * residual
executor.submit(matrix_vector_multiply, A, x, start, end, Ax) change_vector = [0] * len(residual)
) _run_chunked(
executor,
for future in futures: layout,
future.result() scalar_vector_multiply,
(omega, residual, change_vector),
longest_threads_time_accumulator.save_lap_and_reset() )
residual = [0] * len( # the threads write this iteration's result into _x
b _x = x[:]
) # tutaj zostanie przypisany wynik z vector_vector_subtraction _run_chunked(
futures = [] executor, layout, vector_vector_addition, (x, change_vector, _x)
)
for i in range(num_threads):
start = i * chunk_size
end = n if i == num_threads - 1 else (i + 1) * chunk_size
futures.append(
executor.submit(
vector_vector_subtraction, b, Ax, start, end, residual
)
)
for future in futures:
future.result()
longest_threads_time_accumulator.save_lap_and_reset()
change_vector = [0] * len(
residual
) # zostanie tu przypisany wynik scalar_vector_multiply po pracy wątków
futures = []
for i in range(num_threads):
start = i * chunk_size
end = n if i == num_threads - 1 else (i + 1) * chunk_size
futures.append(
executor.submit(
scalar_vector_multiply,
omega,
residual,
start,
end,
change_vector,
)
)
for future in futures:
future.result()
longest_threads_time_accumulator.save_lap_and_reset()
_x = x[
:
] # do _x zostanie przez wątki przypisany wynik pracy w danej iteracji
futures = []
for i in range(num_threads):
start = i * chunk_size
end = n if i == num_threads - 1 else (i + 1) * chunk_size
futures.append(
executor.submit(
vector_vector_addition, x, change_vector, start, end, _x
)
)
for future in futures:
future.result()
longest_threads_time_accumulator.save_lap_and_reset()
x = _x[:] x = _x[:]
if linAlg.SequentialLinearAlgebraUtils.vector_norm(residual) < tol: if lin_alg.SequentialLinearAlgebraUtils.vector_norm(residual) < tol:
break break
end_time = time.perf_counter() end_time = time.perf_counter()

50
Programming/PORR/code/threads_too_simple.py Normal file → Executable file
View File

@ -1,3 +1,6 @@
#!/usr/bin/env python3
"""Threaded Richardson, the first attempt (one thread per slice)."""
import gc import gc
import logging import logging
import multiprocessing import multiprocessing
@ -6,6 +9,7 @@ import threading
import time import time
import numpy as np import numpy as np
from richardson_problem import Problem, Settings
from time_measurement import ( from time_measurement import (
longest_time_accumulator, longest_time_accumulator,
tests_time, tests_time,
@ -14,17 +18,40 @@ from time_measurement import (
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Called with numpy arrays here and with plain lists in the sequential
# backend; the aliases name both.
Matrix = np.ndarray | list[list[float]]
Vector = np.ndarray | list[float]
@time_measurement_longest(longest_time_accumulator) @time_measurement_longest(longest_time_accumulator)
def RichardsonThread(A, b, x, _x, omega, start, end) -> None: def richardson_thread(
matrix: Matrix,
b: Vector,
vectors: tuple[Vector, Vector],
omega: float,
span: tuple[int, int],
) -> None:
"""Run one thread's slice of a Richardson iteration.
``vectors`` is (x, next_x) and ``span`` is the half-open row range this
thread owns.
"""
x, _x = vectors
start, end = span
for i in range(start, end): for i in range(start, end):
sigma = np.dot(A[i, :], _x) - A[i, i] * _x[i] sigma = np.dot(matrix[i, :], _x) - matrix[i, i] * _x[i]
x[i] = (1 - omega) * _x[i] + omega * (b[i] - sigma) / A[i, i] x[i] = (1 - omega) * _x[i] + omega * (b[i] - sigma) / matrix[i, i]
def RichardsonMethodThreads( def richardson_method_threads(
A, b, lambda_min, lambda_max, max_iterations, x0=None, tol=1e-5 problem: Problem, settings: Settings
): ) -> tuple[Vector, str | None]:
"""Run Richardson with one thread per chunk."""
matrix = problem.matrix
b = problem.rhs
max_iterations = settings.max_iterations
x0 = settings.x0
longest_time_accumulator.total_time = 0 longest_time_accumulator.total_time = 0
longest_time_accumulator.start = sys.float_info.max longest_time_accumulator.start = sys.float_info.max
longest_time_accumulator.end = 0 longest_time_accumulator.end = 0
@ -44,21 +71,18 @@ def RichardsonMethodThreads(
for _ in range(max_iterations): for _ in range(max_iterations):
_x = x[:] _x = x[:]
for i in range(num_threads): for i in range(num_threads):
start = ( # start is an index into A: each thread gets the next multiple of
i * chunk_size # the per-thread chunk size as its starting point.
) # start jest indeksem w A. Wątki otrzymują kolejny punkt startowy będący wielokrotnością rozmiaru porcji na wątek start = i * chunk_size
end = n if i == num_threads - 1 else (i + 1) * chunk_size end = n if i == num_threads - 1 else (i + 1) * chunk_size
thread = threading.Thread( thread = threading.Thread(
target=RichardsonThread, args=(A, b, x, _x, omega, start, end) target=richardson_thread, args=(matrix, b, (x, _x), omega, (start, end))
) )
threads.append(thread) threads.append(thread)
thread.start() thread.start()
for thread in threads: for thread in threads:
thread.join() thread.join()
# break
end_time = time.perf_counter() end_time = time.perf_counter()
gc.enable() gc.enable()
total_time = end_time - start_time total_time = end_time - start_time

40
Programming/PORR/code/time_measurement.py Normal file → Executable file
View File

@ -1,27 +1,39 @@
#!/usr/bin/env python3
"""Decorators that accumulate how long the measured calls take."""
import sys import sys
import time import time
from functools import wraps from collections.abc import Callable
class TimeAccumulator: class TimeAccumulator:
"""Total time spent inside the decorated functions."""
def __init__(self) -> None: def __init__(self) -> None:
"""Set up the object."""
self.total_time = 0 self.total_time = 0
class ComplexTimeAcumulator: class ComplexTimeAcumulator:
"""Per-lap and longest times for the decorated functions."""
def __init__(self) -> None: def __init__(self) -> None:
"""Set up the object."""
self.hard_reset() self.hard_reset()
def hard_reset(self) -> None: def hard_reset(self) -> None:
"""Clear every recorded time."""
self.total_time = 0 self.total_time = 0
self.reset() self.reset()
def reset(self) -> None: def reset(self) -> None:
"""Clear the current lap."""
self.lap_time = 0 self.lap_time = 0
self.start = sys.float_info.max self.start = sys.float_info.max
self.end = 0 self.end = 0
def save_lap_and_reset(self) -> None: def save_lap_and_reset(self) -> None:
"""Store the current lap and start a new one."""
self.total_time += self.lap_time self.total_time += self.lap_time
self.reset() self.reset()
@ -32,10 +44,14 @@ tests_time = TimeAccumulator()
longest_threads_time_accumulator = ComplexTimeAcumulator() longest_threads_time_accumulator = ComplexTimeAcumulator()
def time_measurement(accumulator: TimeAccumulator): def time_measurement(accumulator: TimeAccumulator) -> Callable[..., object]:
def decorator(func): """Decorate a function so its runtime accumulates."""
@wraps(func)
def inner(*args, **kwargs): def decorator(func: Callable[..., object]) -> Callable[..., object]:
"""Wrap the function so its runtime is recorded."""
def inner(*args: object, **kwargs: object) -> object:
"""Call the wrapped function and record how long it took."""
start = time.perf_counter() start = time.perf_counter()
result = func(*args, **kwargs) result = func(*args, **kwargs)
end = time.perf_counter() end = time.perf_counter()
@ -47,10 +63,16 @@ def time_measurement(accumulator: TimeAccumulator):
return decorator return decorator
def time_measurement_longest(accumulator: ComplexTimeAcumulator): def time_measurement_longest(
def decorator(func): accumulator: ComplexTimeAcumulator,
@wraps(func) ) -> Callable[..., object]:
def inner(*args, **kwargs): """Decorate a function so its longest runtime is kept."""
def decorator(func: Callable[..., object]) -> Callable[..., object]:
"""Wrap the function so its runtime is recorded."""
def inner(*args: object, **kwargs: object) -> object:
"""Call the wrapped function and record how long it took."""
start = time.perf_counter() start = time.perf_counter()
result = func(*args, **kwargs) result = func(*args, **kwargs)
end = time.perf_counter() end = time.perf_counter()