From 90ce78f17d040501b6ed29598c1e0e47cda3d542 Mon Sep 17 00:00:00 2001 From: Jan-Niclas Loosen Date: Fri, 24 Oct 2025 20:58:31 +0200 Subject: [PATCH] Dice simulator --- .gitignore | 3 +- Uebung-01/decomposition.py | 14 +++ Uebung-01/perseverance.py | 16 ++- Uebung-02/dice.cpp | 60 +++++++++ Uebung-02/dice.py | 36 ++++++ Uebung-02/minsearch.py | 27 ++++ lib/Natives.py | 159 ++++++++++++++++++++++++ lib/__pycache__/Natives.cpython-313.pyc | Bin 0 -> 11690 bytes requirements.txt | 3 + 9 files changed, 312 insertions(+), 6 deletions(-) create mode 100644 Uebung-01/decomposition.py create mode 100644 Uebung-02/dice.cpp create mode 100644 Uebung-02/dice.py create mode 100644 Uebung-02/minsearch.py create mode 100644 lib/Natives.py create mode 100644 lib/__pycache__/Natives.cpython-313.pyc create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index ecbfcaa..6275ad3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -.venv \ No newline at end of file +.venv +*.build \ No newline at end of file diff --git a/Uebung-01/decomposition.py b/Uebung-01/decomposition.py new file mode 100644 index 0000000..a043c44 --- /dev/null +++ b/Uebung-01/decomposition.py @@ -0,0 +1,14 @@ +import math + +def decomp(x: int, base: list[int]) -> dict[int, int]: + base = sorted(base, reverse=True) + + coef = {} + for b in base: + c = math.floor(x / b) + coef[b] = c + x -= b * c + + return coef + +print(decomp(25, [1, 5, 10, 25])) \ No newline at end of file diff --git a/Uebung-01/perseverance.py b/Uebung-01/perseverance.py index 3023f5c..e4ea599 100644 --- a/Uebung-01/perseverance.py +++ b/Uebung-01/perseverance.py @@ -10,28 +10,34 @@ max_y = len(data) max_x = len(data[0]) start = (max_y-1, max_x-1) -# Cache with size of n points -cache = [[None for _ in range(max_x)] for _ in range(max_y)] +# Table with size of data +table = [[None for _ in range(max_x)] for _ in range(max_y)] def find_path(y, x): + # Return bad value for positions which are out of bounds if not (0 <= y < max_y and 0 <= x < max_x): return (), float('-inf') - if cache[y][x] is not None: - return cache[y][x] + # Table look up + if table[y][x] is not None: + return table[y][x] + # End of the recursion is the starting position if y == 0 and x == 0: res = (((y, x),), data[y][x]) + # Calculate defined recursion on all other existing positions else: west_path, west_val = find_path(y, x-1) north_path, north_val = find_path(y-1, x) + # Choose most valuable path to the considered position if west_val > north_val: res = (west_path + ((y, x),), west_val + data[y][x]) else: res = (north_path + ((y, x),), north_val + data[y][x]) - cache[y][x] = res + # Store most valuable path in the table and return + table[y][x] = res return res path, val = find_path(*start) diff --git a/Uebung-02/dice.cpp b/Uebung-02/dice.cpp new file mode 100644 index 0000000..9b78cb4 --- /dev/null +++ b/Uebung-02/dice.cpp @@ -0,0 +1,60 @@ +#include +#include +#include +#include +#include +#include +#include + +class Dice { + std::vector sides; + std::vector probs; + std::mt19937 gen; + std::discrete_distribution<> dist; + +public: + size_t accepted = 0; + size_t rejected = 0; + + Dice(const std::vector& sides, const std::vector& probs) + : sides(sides), probs(probs), gen(std::random_device{}()), dist(probs.begin(), probs.end()) {} + + std::string fair_dice() { + while (true) { + std::vector res; + for (size_t i = 0; i < sides.size(); ++i) + res.push_back(sides[dist(gen)]); + std::set uniq(res.begin(), res.end()); + if (uniq.size() == res.size()) { + ++accepted; + return res[0]; + } + ++rejected; + } + } + + std::map throw_dice(int n) { + std::map res; + for (auto& s : sides) res[s] = 0; + for (int i = 0; i < n; ++i) + res[fair_dice()]++; + return res; + } + + std::pair stats() const { return {accepted, rejected}; } + + void reset_stats() { accepted = rejected = 0; } +}; + +namespace py = pybind11; + +PYBIND11_MODULE(dice, m) { + py::class_(m, "Dice") + .def(py::init&, const std::vector&>()) + .def("fair_dice", &Dice::fair_dice) + .def("throw_dice", &Dice::throw_dice) + .def("stats", &Dice::stats) + .def("reset_stats", &Dice::reset_stats) + .def_readwrite("accepted", &Dice::accepted) + .def_readwrite("rejected", &Dice::rejected); +} diff --git a/Uebung-02/dice.py b/Uebung-02/dice.py new file mode 100644 index 0000000..4db4e73 --- /dev/null +++ b/Uebung-02/dice.py @@ -0,0 +1,36 @@ +from lib.Natives import NativeLoader + +def throw_dice_cpp(n: int, sides: list, probs: list): + loader = NativeLoader() + dice_mod = loader.cpp("dice") + Dice = dice_mod.Dice + d = Dice(sides, probs) + d.reset_stats() + res = d.throw_dice(n) + accepted, rejected = d.stats() + return res, accepted, rejected + + +dice_sides = ['1', '2', '3', '4', '5', '6'] +dice_props = [0.1, 0.1, 0.1, 0.1, 0.1, 0.5] + +coin_sides = ['Head', 'Number'] +coin_head = [0.1, 0.5] + +rep = 100000 + +# --- Dice test --- +res_cpp, acc_dice, rej_dice = throw_dice_cpp(rep, dice_sides, dice_props) +total = sum(res_cpp.values()) +probs_estimated = {k: v / total for k, v in res_cpp.items()} +print("C++ Dice result (relative):", probs_estimated) +print(f"Accepted: {acc_dice/(acc_dice+rej_dice)*100:.2f}%, " + f"Rejected: {rej_dice/(acc_dice+rej_dice)*100:.2f}%\n") + +# --- Coin test --- +res_coin, acc_coin, rej_coin = throw_dice_cpp(rep, coin_sides, coin_head) +total_coin = sum(res_coin.values()) +probs_coin = {k: v / total_coin for k, v in res_coin.items()} +print("C++ Coin result (relative):", probs_coin) +print(f"Accepted: {acc_coin/(acc_coin+rej_coin)*100:.2f}%, " + f"Rejected: {rej_coin/(acc_coin+rej_coin)*100:.2f}%") diff --git a/Uebung-02/minsearch.py b/Uebung-02/minsearch.py new file mode 100644 index 0000000..bb81092 --- /dev/null +++ b/Uebung-02/minsearch.py @@ -0,0 +1,27 @@ +import random + +numsTest = [1, 2, 3, 4, 5] + +def search_min(nums): + if len(nums) == 1: + return nums[0] + + num = nums[0] + nums.remove(num) + + # n-1 recursions + rec_num = search_min(nums) + + if rec_num < num: + return rec_num + else: + # n-1 times called in worst case + smaller = True + for n in nums: + if n <= num: + smaller = False + print('Hit') + assert smaller == True + return num + +print(search_min(numsTest)) \ No newline at end of file diff --git a/lib/Natives.py b/lib/Natives.py new file mode 100644 index 0000000..a9ca3cf --- /dev/null +++ b/lib/Natives.py @@ -0,0 +1,159 @@ +import ctypes +import pathlib +import subprocess +import hashlib +import json +import platform +import sys +import importlib.util +import sysconfig +import shutil +from datetime import datetime +from typing import Callable, Any, Tuple + +class LazyCall: + def __init__(self, func: Callable[..., Any], args: Tuple[Any, ...]) -> None: + self.func = func + self.args = args + + # Calls C function lazily and returns string + def to_str(self) -> str: + self.func.restype = ctypes.c_char_p + res = self.func(*self.__convert_args()) + return res.decode() if res else "" + + # Calls C function lazily and returns int + def to_int(self) -> int: + self.func.restype = ctypes.c_int + return self.func(*self.__convert_args()) + + # Calls C function lazily and returns float + def to_float(self) -> float: + self.func.restype = ctypes.c_double + return self.func(*self.__convert_args()) + + # Convert arguments to C types + def __convert_args(self): + out = [] + for a in self.args: + if isinstance(a, str): + out.append(ctypes.c_char_p(a.encode())) + elif isinstance(a, int): + out.append(ctypes.c_int(a)) + elif isinstance(a, float): + out.append(ctypes.c_double(a)) + else: + out.append(a) + return out + + +class FunctionWrapper: + def __init__(self, lib: ctypes.CDLL, name: str) -> None: + self._lib = lib + self._name = name + + # Prepares lazy function calls + def __call__(self, *args) -> LazyCall: + func = getattr(self._lib, self._name) + return LazyCall(func, args) + + +class LibWrapper: + def __init__(self, lib: ctypes.CDLL) -> None: + self._lib = lib + + # Exposes all C functions within the lib + def __getattr__(self, name: str) -> FunctionWrapper: + return FunctionWrapper(self._lib, name) + + +class NativeLoader: + def __init__(self) -> None: + self.build_dir = pathlib.Path(".build") + self.build_dir.mkdir(exist_ok=True) + self.ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") or sysconfig.get_config_var("SO") + + # Loads a Cpp class + def cpp(self, path: str): + src = pathlib.Path(path).with_suffix(".cpp") + if not src.exists(): + raise FileNotFoundError(src) + out = self.build_dir / f"{src.stem}_cpp" + + if self.__needs_rebuild(src, out): + target = self.__build_cpp(src, out) + else: + target = str(out) + ".so" + + spec = importlib.util.spec_from_file_location(src.stem, target) + mod = importlib.util.module_from_spec(spec) + sys.modules[src.stem] = mod + spec.loader.exec_module(mod) + return mod + + # Loads a C lib + def c(self, path: str) -> LibWrapper: + src = pathlib.Path(path).with_suffix(".c") + if not src.exists(): + raise FileNotFoundError(src) + out = self.build_dir / f"{src.stem}_c.so" + if self.__needs_rebuild(src, out): + self.__build_c(src, out) + lib = ctypes.CDLL(str(out)) + return LibWrapper(lib) + + @staticmethod + def __create_hash(path: pathlib.Path) -> str: + h = hashlib.sha256() + with open(path, "rb") as f: + for chunk in iter(lambda: f.read(8192), b""): + h.update(chunk) + return h.hexdigest() + + @staticmethod + def __info_path(target: pathlib.Path) -> pathlib.Path: + return target.with_suffix(target.suffix + ".buildinfo") + + @staticmethod + def __load_info(info_path: pathlib.Path): + if info_path.exists(): + with open(info_path, "r") as f: + return json.load(f) + return None + + def __write_info(self, info_path: pathlib.Path, src: pathlib.Path, out: pathlib.Path) -> None: + data = { + "os": platform.system(), + "build_date": datetime.now().isoformat(), + "src": str(src), + "out": str(out), + "src_hash": self.__create_hash(src), + } + with open(info_path, "w") as f: + json.dump(data, f, indent=2) + + def __needs_rebuild(self, src: pathlib.Path, out: pathlib.Path) -> bool: + info = self.__load_info(self.__info_path(out)) + final_out = pathlib.Path(str(out) + ".so") + if not info or not final_out.exists(): + return True + try: + return info.get("src_hash") != self.__create_hash(src) + except FileNotFoundError: + return True + + def __build_cpp(self, src: pathlib.Path, out: pathlib.Path) -> str: + includes = (subprocess.check_output(["python3", "-m", "pybind11", "--includes"]).decode().strip().split()) + + built_temp = str(out) + self.ext_suffix + final_path = str(out) + ".so" + + subprocess.run(["c++", "-O3", "-shared", "-fPIC", "-std=c++17", *includes, str(src), "-o", built_temp], check=True) + + shutil.move(built_temp, final_path) + self.__write_info(self.__info_path(out), src, out) + return final_path + + def __build_c(self, src: pathlib.Path, out: pathlib.Path) -> None: + subprocess.run(["gcc", "-O3", "-shared", "-fPIC", "-std=c11", str(src), "-o", str(out)], check=True) + self.__write_info(self.__info_path(out), src, out) diff --git a/lib/__pycache__/Natives.cpython-313.pyc b/lib/__pycache__/Natives.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe469c61e00dee6de40793fd1382542a7fd40495 GIT binary patch literal 11690 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.12 +setuptools>=70.0 +wheel>=0.44