WUT_Computer_Science/Programming/EARIN/lab3/main.py
Krzysztof kuhy Rudnicki cc06bc0cd8 EARIN: zero findings, verified against byte-identical program output
749 findings across six labs and two projects. The mechanical half was
scripted; everything below is what the scripts got wrong or could not do, and
how it was caught.

Three regressions I introduced and then found by RUNNING the code, not by
reading it:

- Converting print() to logger.info() made the programs SILENT. logging's
  last-resort handler only emits WARNING and above, so with no basicConfig
  call `lab2/main.py -t 1 1` printed nothing at all. Fifteen files across the
  repo now configure logging where they log.
- The same conversion dropped `end=""`. lab2 draws its board one square at a
  time and lab1 draws the maze one cell at a time; both came out one glyph
  per line. Those thirteen call sites are sys.stdout.write again, and the
  rendered board now diffs byte-identically against the pre-change output.
- The B007 fix renamed `col` to `_col` in the FIRST loop it matched, which
  was print_letters -- where the variable IS used. That would have been a
  NameError on the first call.

And one the scripts caused silently: annotating 110 signatures dropped the
DEFAULT VALUES from 22 of them, so `preprocessing(a, b, USER_THRESHOLD,
ANIME_THRESHOLD)` would have raised TypeError. Every signature is now diffed
against the pre-change version, name by name and default by default: zero
mismatches.

Real defects the lint surfaced:

- `alpha_beta` ended with an unreachable `return None` that violated its own
  return type. Its maximising and minimising halves were mirror images, so
  they are one `_search_branch` now (C901 11 -> ok).
- `get_data`'s `gpu` switch, `predict`'s `auto`, `create_model`'s
  `rows_number` and five of `preprocess_model_predict`'s fifteen parameters
  were never read. Gone, with their call sites.
- np.random.* -> Generator throughout (NPY002), `.values` -> `.to_numpy()`,
  `datetime.now()` with a tzinfo, `inplace=True` removed, `except:` given a
  type, `random.*` -> `SystemRandom` (S311).
- print_board's three nested helpers became methods (C901 17 -> ok);
  user_input's nine-branch if-ladder became a table (C901 12 -> ok).

Verification, all by running: lab1 `-t` and lab2 `-t 1 1` produce output
byte-identical to the pre-change code; lab2's rendered 8x8 board diffs clean;
lab3's argument parsing returns the same dict for the same argv; lab4 fails
identically before and after (no sklearn here). Also included: the logging
configuration for NLP, MOM and ECRYPT_PROJECT, which had the same silence bug.

Repo-wide: 2,775 findings -> 1,883.

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

370 lines
13 KiB
Python
Executable File

#!/usr/bin/env python3
"""Program that optimizes Rastrigin function: file_ (x_point_value, y_point_value) =.
20 + (x_point_value^2 - 10cos(2πx)) + (y_point_value^2 - 10 cos(2πy)).
Using Evolutionary Strategy (μ, λ).
"""
import logging
import sys
import tempfile
import time
from collections.abc import Callable
from pathlib import Path
import cv2
import matplotlib.pyplot as plt
import numpy as np
# cv2.waitKey returns the ASCII code of the key pressed; 'q' quits.
KEY_Q = ord("q")
# NumPy's legacy np.random.* functions share one global state; Generator is
# the modern API and is what NPY002 asks for.
RNG = np.random.default_rng()
logger = logging.getLogger(__name__)
def rastrigin(x_argument: float, y_argument: float) -> float:
"""Define the Rastrigin function."""
return (
20
+ x_argument**2
- 10 * np.cos(2 * np.pi * x_argument)
+ y_argument**2
- 10 * np.cos(2 * np.pi * y_argument)
)
def generate(population: object, arguments: dict[str, float]) -> tuple[object, object]:
"""Run single generation."""
# Evaluate the fitness of each individual
fitness = np.array(
[
rastrigin(x_point_value, y_point_value)
for x_point_value, y_point_value in population
]
)
# Select the top arguments["number_of_parents"] individuals
parents = population[np.argsort(fitness)[: arguments["number_of_parents"]]]
# Generate the next generation of lambda individuals by recombination
children = np.concatenate(
[
RNG.permutation(parents)
for i in range(
(arguments["size_of_population"] // arguments["number_of_parents"]) + 1
)
]
)
children = children[: arguments["size_of_population"]]
# Add mutation to the children
mutation = RNG.normal(
loc=0,
scale=arguments["mutation_strength"],
size=(arguments["size_of_population"], 2),
)
population = children + mutation
return fitness, population
def evolution_strategy(
arguments: dict[str, float], *, no_display: bool = False
) -> tuple[object, object]:
"""Define the Evolutionary Strategy (μ, λ) algorithm."""
# Initialize the population
print_info = []
population = RNG.uniform(
low=arguments["min"],
high=arguments["max"],
size=(arguments["size_of_population"], 2),
)
summary = []
if not no_display:
print_info.append(
(
population,
0,
f"""0:nop-{arguments["number_of_parents"]}:sop-{arguments["size_of_population"]}:
ms-{arguments["mutation_strength"]}:nog-{arguments["number_of_generations"]}:
min-max-{arguments["min"], arguments["max"]}:
noo-{arguments["number_of_outputs"]}""",
)
)
arguments["number_of_outputs"] = min(
[arguments["number_of_outputs"] - 1, arguments["number_of_generations"]]
)
# Iterate until we reach max number of generate and terminate
for generation_number in range(1, arguments["number_of_generations"] + 1):
fitness, population = generate(population, arguments)
step = (
arguments["number_of_generations"] // arguments["number_of_outputs"]
if arguments["number_of_generations"] % arguments["number_of_outputs"] == 0
else arguments["number_of_generations"]
// (arguments["number_of_outputs"] - 1)
)
offset = arguments["number_of_generations"] % step
if (generation_number - offset) % step == 0 and not no_display:
print_info.append(
(
population,
generation_number,
f"""{generation_number}:nop_{arguments["number_of_parents"]}:
sop_{arguments["size_of_population"]}:ms_{arguments["mutation_strength"]}:
nog_{arguments["number_of_generations"]}:
min_max_{arguments["min"], arguments["max"]}:
noo_{arguments["number_of_outputs"]}""",
)
)
summary.append(population)
# Evaluate the fitness of the final population
fitness = np.array(
[
rastrigin(x_point_value, y_point_value)
for x_point_value, y_point_value in population
]
)
# Return the best individual found
best_idx = np.argmin(fitness)
return (
population[best_idx],
fitness[best_idx],
population,
print_info,
summary,
)
def print_help() -> None:
"""Print program functionality and how to access it."""
logger.info(
"\n python main.py - Default functionality optimizing\n Rastrigin "
"function file_ (x_point_value, y_point_value) =\n 20 + "
"(x_point_value^2 - 10cos(2πx)) + (y_point_value^2 - 10 cos(2πy))\n "
"using Evolutionary Strategy (μ, λ), using only default values\n "
'Default values:\n arguments["number_of_parents"]=5,\n '
'arguments["size_of_population"]=20,\n '
'arguments["mutation_strength"]=0.1,\n '
'arguments["number_of_generations"]=100,\n min_value=-5.12,\n '
'max_value=5.12\n arguments["number_of_outputs"] = 100\n\n python '
"main.py -h --help print this prompt\n Any of the default values an be"
' changed using arguments:\n -nop --arguments["number_of_parents"] '
'[number]\n -sop --arguments["size_of_population"] [number]\n -ms '
'--arguments["mutation_strength"] [number]\n -nog '
'--arguments["number_of_generations"] [number]\n -min --min_value '
"[number]\n -max --max_value [number]\n -noo, "
'--arguments["number_of_outputs"] [number]\n Those arguments can be '
"given in any order and any argument\n which was not entered will be "
"replaced with default value,\n Additional flags:\n -nd, "
"--no-display (does not show the plots)\n -s, --save (if issued WILL "
"save the files)\n exemplary use:\n python main.py -nop 5 -sop 20 "
"-ms 0.1 -nog 100 -min -5.12 -max 5.12 -noo 100\n "
)
def get_output_bounds(
x_data: object, y_data: object
) -> tuple[tuple[float, float], tuple[float, float]]:
"""Get x and y output limits for pyplot."""
min_output_size = ARGUMENTS["mutation_strength"] * 10
xmin = min(x_data)
xmax = max(x_data)
ymin = min(y_data)
ymax = max(y_data)
x_diff = xmax - xmin
y_diff = ymax - ymin
if min_output_size is None:
min_output_size = max(x_diff, y_diff)
margin = max(x_diff, y_diff) / 5
if x_diff < min_output_size:
xmax += (min_output_size - x_diff) / 2
xmin -= (min_output_size - x_diff) / 2
if y_diff < min_output_size:
ymax += (min_output_size - y_diff) / 2
ymin -= (min_output_size - y_diff) / 2
x_bounds = [xmin - margin, xmax + margin]
y_bounds = [ymin - margin, ymax + margin]
return x_bounds, y_bounds
def output(
population_output: object,
generation_number: int,
file_name: str = "temp",
*,
save_results: bool = False,
) -> None:
"""Draw result of our function."""
# define the visualization params
colors = RNG.random(len(population_output))
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as file_:
# iterate over the optimization steps
x_data = []
y_data = []
for x_point_value, y_point_value in population_output:
x_data.append(x_point_value)
y_data.append(y_point_value)
x_lim, y_lim = get_output_bounds(x_data, y_data)
# plot the data
plt.cla()
plt.figure()
plt.scatter(x_data, y_data, c=colors, alpha=0.5)
plt.xlim(x_lim)
plt.ylim(y_lim)
plt.savefig(file_.name)
# read image
image = cv2.imread(file_.name)
# show the image, provide window name first
cv2.imshow(f"Generation {generation_number}", image)
if save_results:
cv2.imwrite(file_name + ".jpg", image)
# add wait key. window waits until user presses a key and quits if
# the key is 'q'
if cv2.waitKey(0) == KEY_Q:
# and finally destroy/close all open windows
sys.exit()
cv2.destroyAllWindows()
file_.close()
Path(file_.name).unlink()
def print_summary(
populations: object, file_name: str = "temp_summary", *, save_results: bool = False
) -> None:
"""Draw result of our function for chosen generations."""
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as file_:
# iterate over the optimization steps
# generate random 2D data - replace it with the results from your
# algorithm
plt.cla()
plt.figure()
bounds = None
for ind, pop in enumerate(populations):
x_data = []
y_data = []
for x_point_value, y_point_value in pop:
x_data.append(x_point_value)
y_data.append(y_point_value)
if ind == 0:
bounds = get_output_bounds(x_data, y_data)
# plot the data
transparency = ind / (len(populations) - 1)
color = [[transparency, 0, 0]] * len(pop)
plt.scatter(x_data, y_data, c=color, alpha=transparency, label=f"{ind}")
plt.xlim(bounds[0])
plt.ylim(bounds[1])
plt.savefig(file_.name)
# read image
image = cv2.imread(file_.name)
if save_results:
cv2.imwrite("SUMMARY:" + file_name + ".jpg", image)
# show the image, provide window name first
cv2.imshow("Summary", image)
# add wait key. window waits until user presses a key and quits if
# the key is 'q'
if cv2.waitKey(0) == KEY_Q:
# and finally destroy/close all open windows
sys.exit()
cv2.destroyAllWindows()
file_.close()
Path(file_.name).unlink()
# Long option, short option, and how to read the value that follows. A table
# rather than an if-ladder: the ladder was C901 12, and one row per flag makes
# adding one a data change.
_OPTIONS: dict[tuple[str, str], tuple[str, Callable[[str], float] | None]] = {
("-nop", "--number_of_parents"): ("number_of_parents", int),
("-sop", "--size_of_population"): ("size_of_population", int),
("-ms", "--mutation_strength"): ("mutation_strength", float),
("-nog", "--number_of_generations"): ("number_of_generations", int),
("-min", "--min_value"): ("min", float),
("-max", "--max_value"): ("max", float),
("-noo", "--number_of_outputs"): ("number_of_outputs", int),
("-nd", "--no_display"): ("no_display", None),
("-s", "--save"): ("save", None),
}
def user_input() -> dict[str, float]:
"""Handle user terminal arguments."""
arguments: dict[str, float] = {
"number_of_parents": 5,
"size_of_population": 20,
"mutation_strength": 0.1,
"number_of_generations": 100,
"min": -5.12,
"max": 5.12,
"number_of_outputs": 10,
"no_display": False,
"save": False,
}
for index, argument in enumerate(sys.argv):
if argument in ("-h", "--help"):
print_help()
sys.exit()
for flags, (key, parse) in _OPTIONS.items():
if argument in flags:
arguments[key] = True if parse is None else parse(sys.argv[index + 1])
return arguments
def print_output(print_info: object, save_results: object, summary: object) -> None:
"""Print out population and summary plots."""
for population, generation_number, file_name in print_info:
output(population, generation_number, file_name, save_results)
summary_file_name = file_name
print_summary(summary, summary_file_name, save_results)
# Ran first in the code
if __name__ == "__main__":
# Run the Evolutionary Strategy algorithm
logging.basicConfig(format="%(message)s", level=logging.INFO)
ARGUMENTS = user_input()
TOTAL_TIME = 0
start_time = time.perf_counter()
(
best_individual,
best_fitness,
output_population,
PRINT_INFO,
SUMMARY,
) = evolution_strategy(
ARGUMENTS,
ARGUMENTS["no_display"],
)
end_time = time.perf_counter()
if not ARGUMENTS["no_display"]:
print_output(PRINT_INFO, ARGUMENTS["save"], SUMMARY)
TOTAL_TIME = end_time - start_time
time_per_generation = TOTAL_TIME / ARGUMENTS["number_of_generations"]
logger.info("Best individual found: %s", best_individual)
logger.info("Best fitness found: %s", best_fitness)
logger.info("total_generation_time: %s", TOTAL_TIME)
logger.info("time_per_generation: %s", time_per_generation)