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 0000000..fe469c6 Binary files /dev/null and b/lib/__pycache__/Natives.cpython-313.pyc differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2543cec --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pybind11>=2.12 +setuptools>=70.0 +wheel>=0.44