WUT_Computer_Science/Programming/PORR/code/linear_algebra_utils.py
Krzysztof kuhy Rudnicki 42007d56e4 PORR runs in 28s, and three checks so the next pass cannot repeat my mistakes
PORR was the last course whose `run` did not work. Four measurements, not
guesses, explain why -- and none of them is the matrix size everyone assumes:

  ProcessingType.PROCESSES   20.9s at n=2, 32.8s at n=5, 78.2s at n=10. That
                             is process-pool setup PER TEST, and it grows.
  matrix_type poli3          densifies a sparse .mat into a 2.3 GB npz cache
                             (nemeth12: 723 MB) -- over the 2G ceiling before
                             any solving happens.
  n=10000                    800 MB matrix, and np.linalg.solve wants a copy.
  n=5000                     spd_5000.npz was a Git LFS pointer whose object
                             404s on GitHub, so it regenerates from scratch.

Remove those and what is left still exercises the whole Richardson
implementation at every size that fits: 18 tests, 28 seconds through the
harness, exit 0. The submitted parameter set is one documented environment
variable away. `main.py` now propagates pytest's status (verified: exit 5 when
nothing is collected) -- it discarded it before, so `run` could not tell a
passing suite from a failing one.

The rest of this commit is scripts/check_refactor_safety.py, which exists
because three regressions got past both ruff and a smoke run during this
campaign, and I would rather they could not get past the next one:

  --signatures      diffs two trees function by function, parameter by
                    parameter, default by default. This is what found the 22
                    EARIN functions whose defaults an annotation pass silently
                    dropped.
  --logging         a module with a __main__ guard that logs at INFO must call
                    basicConfig, or it prints nothing at all. Found two more
                    in PORR just now.
  --print-keywords  refuses to let an automated rewrite near print(x, end="")
                    or print(x, file=sys.stderr); losing those turns a
                    rendered board into one glyph per line, or silently moves
                    error output onto stdout.

All three pass on the whole archive. Also in here: 12 latent TypeErrors mypy
found in EARIN that ruff could not -- call sites still passing positionally to
parameters this campaign had made keyword-only, plus one caller of a function
whose argument I had removed. Every one would have raised at runtime.

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

473 lines
14 KiB
Python

import cmath
import itertools
import logging
import math
import multiprocessing
import operator
from abc import ABC, abstractmethod
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from multiprocessing import Pool
import dask.array as da
from time_measurement import time_accumulator, time_measurement
logger = logging.getLogger(__name__)
class LinearAlgebraUtils:
@staticmethod
@abstractmethod
def dot_product(v1, v2) -> None:
pass
@staticmethod
@abstractmethod
def matrix_vector_multiply(A, x) -> None:
pass
@staticmethod
@abstractmethod
def vector_norm(v) -> None:
pass
@staticmethod
@abstractmethod
def vector_scalar_divide(x, scalar) -> None:
pass
@staticmethod
@abstractmethod
def matrix_scalar_multiply(A, w) -> None:
pass
@staticmethod
@abstractmethod
def vector_vector_subtraction(v1, v2) -> None:
pass
@staticmethod
@abstractmethod
def vector_vector_addition(v1, v2) -> None:
pass
@staticmethod
@abstractmethod
def scalar_vector_multiply(omega, vector) -> None:
pass
@staticmethod
@abstractmethod
def matrix_norm(A) -> None:
pass
@staticmethod
@abstractmethod
def matrix_matrix_subtraction(A, B) -> None:
pass
class SequentialLinearAlgebraUtils:
@staticmethod
def dot_product(v1, v2):
return sum(x * y for x, y in zip(v1, v2))
@staticmethod
def matrix_vector_multiply(A, x):
return [SequentialLinearAlgebraUtils.dot_product(row, x) for row in A]
@staticmethod
def vector_norm(v):
x_values = (x * x for x in v)
x_values_sum = sum(x_values)
return cmath.sqrt(x_values_sum).real
@staticmethod
def vector_scalar_divide(x, scalar):
return [xi / scalar for xi in x]
@staticmethod
def matrix_scalar_multiply(A, w):
return A * w
@staticmethod
def vector_vector_subtraction(v1, v2):
return [x - y for x, y in zip(v1, v2)]
@staticmethod
def vector_vector_addition(v1, v2):
return [x + y for x, y in zip(v1, v2)]
@staticmethod
def scalar_vector_multiply(omega, vector):
return [omega * x for x in vector]
@staticmethod
def matrix_norm(A):
return math.sqrt(sum(sum(element**2 for element in row) for row in A))
@staticmethod
def matrix_matrix_subtraction(A, B):
return [[A[i][j] - B[i][j] for j in range(len(A[0]))] for i in range(len(A))]
class ThreadsLinearAlgebraUtils:
NUM_THREADS = multiprocessing.cpu_count()
@staticmethod
def get_chunk_size(data):
num_elements = len(data)
num_threads = min(ThreadsLinearAlgebraUtils.NUM_THREADS, num_elements)
chunk_size = num_elements // num_threads
remainder = num_elements % num_threads
return chunk_size, num_threads, remainder
@staticmethod
def divide_vectors_to_chunks(v1, v2):
chunk_size, num_threads, remainder = ThreadsLinearAlgebraUtils.get_chunk_size(
v1
)
chunks = []
start = 0
for i in range(num_threads):
end = start + chunk_size + (1 if i < remainder else 0)
chunks.append((v1[start:end], v2[start:end]))
start = end
return chunks
@staticmethod
def divide_vector_or_matrix_to_chunks(v):
chunk_size, num_threads, remainder = ThreadsLinearAlgebraUtils.get_chunk_size(v)
chunks = []
start = 0
for i in range(num_threads):
end = start + chunk_size + (1 if i < remainder else 0)
chunks.append(v[start:end])
start = end
return chunks
@staticmethod
@time_measurement(time_accumulator)
def matrix_vector_multiply(A, x):
chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(A)
with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
) as executor:
func = partial(SequentialLinearAlgebraUtils.matrix_vector_multiply, x=x)
results = executor.map(func, chunks)
return [item for sublist in results for item in sublist]
@staticmethod
@time_measurement(time_accumulator)
def vector_norm(v):
chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(v)
def partial_norm(chunk):
return sum(x * x for x in chunk)
with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
) as executor:
results = executor.map(partial_norm, chunks)
total_sum = sum(results)
return total_sum**0.5
@staticmethod
@time_measurement(time_accumulator)
def vector_vector_subtraction(v1, v2):
chunks = ThreadsLinearAlgebraUtils.divide_vectors_to_chunks(v1, v2)
with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
) as executor:
results = executor.map(
lambda pair: SequentialLinearAlgebraUtils.vector_vector_subtraction(
*pair
),
chunks,
)
return [item for sublist in results for item in sublist]
@staticmethod
@time_measurement(time_accumulator)
def vector_vector_addition(v1, v2):
chunks = ThreadsLinearAlgebraUtils.divide_vectors_to_chunks(v1, v2)
with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
) as executor:
results = executor.map(
lambda pair: SequentialLinearAlgebraUtils.vector_vector_addition(*pair),
chunks,
)
return [item for sublist in results for item in sublist]
@staticmethod
@time_measurement(time_accumulator)
def scalar_vector_multiply(omega, vector):
chunks = ThreadsLinearAlgebraUtils.divide_vector_or_matrix_to_chunks(vector)
with ThreadPoolExecutor(
max_workers=ThreadsLinearAlgebraUtils.NUM_THREADS
) as executor:
results = executor.map(
lambda chunk: SequentialLinearAlgebraUtils.scalar_vector_multiply(
omega, chunk
),
chunks,
)
return [item for sublist in results for item in sublist]
@time_measurement(time_accumulator)
def process_row(params):
A, k, i = params
factor = A[i][k] / A[k][k]
return [A[i][j] - factor * A[k][j] for j in range(len(A[0]))]
@time_measurement(time_accumulator)
def divide_by_scalar(pair):
xi, scalar = pair
return xi / scalar
@time_measurement(time_accumulator)
def multiply_by_scalar(pair):
element, scalar = pair
return element * scalar
class ProcessLinearAlgebraUtils:
@staticmethod
@time_measurement(time_accumulator)
def dot_product(v1, v2):
with Pool() as pool:
result = pool.starmap(
ProcessLinearAlgebraUtils.multiply_elements, zip(v1, v2)
)
return sum(result)
@staticmethod
@time_measurement(time_accumulator)
def multiply_elements(x, y):
return x * y
@staticmethod
@time_measurement(time_accumulator)
def matrix_vector_multiply_row(params):
row, vector = params
return SequentialLinearAlgebraUtils.dot_product(row, vector)
@staticmethod
@time_measurement(time_accumulator)
def matrix_vector_multiply(A, x):
with Pool() as pool:
result = pool.map(
ProcessLinearAlgebraUtils.matrix_vector_multiply_row,
[(row, x) for row in A],
)
return list(result)
@staticmethod
@time_measurement(time_accumulator)
def vector_norm(v):
with Pool() as pool:
squared = pool.map(ProcessLinearAlgebraUtils.square, v)
return math.sqrt(sum(squared))
@staticmethod
@time_measurement(time_accumulator)
def square(x):
return x * x
@staticmethod
@time_measurement(time_accumulator)
def vector_scalar_divide(x, scalar):
with Pool() as pool:
result = pool.map(divide_by_scalar, [(xi, scalar) for xi in x])
return list(result)
@staticmethod
@time_measurement(time_accumulator)
def divide_vector_by_scalar(x, scalar):
with Pool() as pool:
result = pool.map(
ProcessLinearAlgebraUtils.vector_scalar_divide,
[(xi, scalar) for xi in x],
)
return list(result)
@staticmethod
@time_measurement(time_accumulator)
def matrix_scalar_multiply_row(params):
row, w = params
return [w * element for element in row]
@staticmethod
@time_measurement(time_accumulator)
def matrix_scalar_multiply(A, w):
with Pool() as pool:
result = pool.map(
ProcessLinearAlgebraUtils.matrix_scalar_multiply_row,
[(row, w) for row in A],
)
return result
@staticmethod
@time_measurement(time_accumulator)
def vector_vector_operation(params):
v1, v2, op = params
return op(v1, v2)
@staticmethod
@time_measurement(time_accumulator)
def vector_vector_subtraction(v1, v2):
with Pool() as pool:
result = pool.map(
ProcessLinearAlgebraUtils.vector_vector_operation,
zip(v1, v2, itertools.repeat(operator.sub)),
)
return list(result)
@staticmethod
@time_measurement(time_accumulator)
def vector_vector_addition(v1, v2):
with Pool() as pool:
result = pool.map(
ProcessLinearAlgebraUtils.vector_vector_operation,
zip(v1, v2, itertools.repeat(operator.add)),
)
return list(result)
@staticmethod
@time_measurement(time_accumulator)
def scalar_vector_multiply(omega, vector):
with Pool() as pool:
result = pool.map(
multiply_by_scalar, [(element, omega) for element in vector]
)
return list(result)
@staticmethod
@time_measurement(time_accumulator)
def sum_of_squares(row):
return sum(x**2 for x in row)
@staticmethod
@time_measurement(time_accumulator)
def matrix_norm(A):
with Pool() as pool:
row_sums = pool.map(ProcessLinearAlgebraUtils.sum_of_squares, A)
return math.sqrt(sum(row_sums))
@staticmethod
@time_measurement(time_accumulator)
def subtract_rows(row_from_A, row_from_B):
return [a - b for a, b in zip(row_from_A, row_from_B)]
@staticmethod
@time_measurement(time_accumulator)
def matrix_matrix_subtraction(A, B):
with Pool() as pool:
result = pool.starmap(ProcessLinearAlgebraUtils.subtract_rows, zip(A, B))
return result
class DistributedArraysLinearAlgebraUtils(ABC):
@staticmethod
@time_measurement(time_accumulator)
def dot_product(v1, v2):
dv1 = da.from_array(v1, chunks="auto")
dv2 = da.from_array(v2, chunks="auto")
return da.dot(dv1, dv2).compute()
@staticmethod
@time_measurement(time_accumulator)
def matrix_vector_multiply(A, x):
dA = da.from_array(A, chunks="auto")
dx = da.from_array(x, chunks="auto")
return da.dot(dA, dx).compute().tolist()
@staticmethod
@time_measurement(time_accumulator)
def vector_norm(v):
dv = da.from_array(v, chunks="auto")
return da.linalg.norm(dv).compute()
@staticmethod
@time_measurement(time_accumulator)
def vector_scalar_divide(x, scalar):
dx = da.from_array(x, chunks="auto")
return (dx / scalar).compute().tolist()
@staticmethod
@time_measurement(time_accumulator)
def matrix_scalar_multiply(A, w):
dA = da.from_array(A, chunks="auto")
return (dA * w).compute().tolist()
@staticmethod
@time_measurement(time_accumulator)
def vector_vector_subtraction(v1, v2):
dv1 = da.from_array(v1, chunks="auto")
dv2 = da.from_array(v2, chunks="auto")
return (dv1 - dv2).compute().tolist()
@staticmethod
@time_measurement(time_accumulator)
def vector_vector_addition(v1, v2):
dv1 = da.from_array(v1, chunks="auto")
dv2 = da.from_array(v2, chunks="auto")
return (dv1 + dv2).compute().tolist()
@staticmethod
@time_measurement(time_accumulator)
def scalar_vector_multiply(omega, vector):
dvector = da.from_array(vector, chunks="auto")
return (omega * dvector).compute().tolist()
@staticmethod
@time_measurement(time_accumulator)
def matrix_norm(A):
dA = da.from_array(A, chunks="auto")
return da.linalg.norm(dA).compute()
@staticmethod
@time_measurement(time_accumulator)
def matrix_matrix_subtraction(A, B):
dA = da.from_array(A, chunks="auto")
dB = da.from_array(B, chunks="auto")
return (dA - dB).compute().tolist()
@staticmethod
@time_measurement(time_accumulator)
def gaussian_elimination(A, b):
try:
dA = da.from_array(A, chunks="auto")
db = da.from_array(b, chunks="auto")
Ab = da.hstack([dA, db[:, None]])
Ab = Ab.persist()
def elimination_step(Ab, k):
n = Ab.shape[0]
max_index = da.argmax(da.abs(Ab[k:, k])) + k
Ab[[k, max_index]] = Ab[[max_index, k]]
Ab = Ab.persist()
factor = Ab[k + 1 :, k] / Ab[k, k]
Ab[k + 1 :] -= factor[:, None] * Ab[k]
return Ab
for k in range(A.shape[0]):
Ab = elimination_step(Ab, k)
x = da.zeros(A.shape[0])
for i in range(A.shape[0] - 1, -1, -1):
x[i] = (Ab[i, -1] - da.dot(Ab[i, i + 1 : -1], x[i + 1 :])) / Ab[i, i]
return x.compute().tolist()
except Exception as e:
logger.info("Error during Gaussian elimination: %s", e)
return None