diff --git a/docs/PINDragon_102015.pdf b/docs/PINDragon_102015.pdf new file mode 100644 index 0000000..7a93412 Binary files /dev/null and b/docs/PINDragon_102015.pdf differ diff --git a/notebooks/tests.ipynb b/notebooks/tests.ipynb new file mode 100644 index 0000000..3563ac2 --- /dev/null +++ b/notebooks/tests.ipynb @@ -0,0 +1,122 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2024-12-10T14:54:42.059468Z", + "start_time": "2024-12-10T14:54:42.057421Z" + } + }, + "source": [ + "from src.keypad import Keypad\n", + "from src.utils import total_shuffle_states, total_valid_nkode_states" + ], + "outputs": [], + "execution_count": 3 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-12-10T14:54:43.388607Z", + "start_time": "2024-12-10T14:54:43.373898Z" + } + }, + "cell_type": "code", + "source": [ + "p = 4 # properties_per_key\n", + "k = 3 # number_of_keys\n", + "keypad = Keypad.new_keypad(k, p)\n", + "print(keypad.keypad)\n", + "keypad.partial_shuffle()\n", + "print(keypad.keypad)\n", + "keypad.partial_shuffle()\n", + "print(keypad.keypad)\n" + ], + "id": "dd4b3cb6405a56e0", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[ 8 1 2 3]\n", + " [ 4 5 6 11]\n", + " [ 0 9 10 7]]\n" + ] + }, + { + "ename": "UnboundLocalError", + "evalue": "cannot access local variable 'shuffled_keypad' where it is not associated with a value", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mUnboundLocalError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[4], line 5\u001B[0m\n\u001B[1;32m 3\u001B[0m keypad \u001B[38;5;241m=\u001B[39m Keypad\u001B[38;5;241m.\u001B[39mnew_keypad(k, p)\n\u001B[1;32m 4\u001B[0m \u001B[38;5;28mprint\u001B[39m(keypad\u001B[38;5;241m.\u001B[39mkeypad)\n\u001B[0;32m----> 5\u001B[0m keypad\u001B[38;5;241m.\u001B[39mpartial_shuffle()\n\u001B[1;32m 6\u001B[0m \u001B[38;5;28mprint\u001B[39m(keypad\u001B[38;5;241m.\u001B[39mkeypad)\n\u001B[1;32m 7\u001B[0m keypad\u001B[38;5;241m.\u001B[39mpartial_shuffle()\n", + "File \u001B[0;32m~/repos/nkode-analysis/src/keypad.py:32\u001B[0m, in \u001B[0;36mKeypad.partial_shuffle\u001B[0;34m(self)\u001B[0m\n\u001B[1;32m 28\u001B[0m perm_indices \u001B[38;5;241m=\u001B[39m np\u001B[38;5;241m.\u001B[39mrandom\u001B[38;5;241m.\u001B[39mpermutation(\u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mk)\n\u001B[1;32m 30\u001B[0m shuffled_sets \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mkeypad[perm_indices, :][:, column_subset]\n\u001B[0;32m---> 32\u001B[0m \u001B[38;5;28;01mwhile\u001B[39;00m shuffled_keypad \u001B[38;5;129;01min\u001B[39;00m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mkeypad_cache:\n\u001B[1;32m 33\u001B[0m shuffled_keypad \u001B[38;5;241m=\u001B[39m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mkeypad[perm_indices, :][:, column_subset]\n\u001B[1;32m 35\u001B[0m \u001B[38;5;28mself\u001B[39m\u001B[38;5;241m.\u001B[39mkeypad_cache\u001B[38;5;241m.\u001B[39mappend(shuffled_keypad)\n", + "\u001B[0;31mUnboundLocalError\u001B[0m: cannot access local variable 'shuffled_keypad' where it is not associated with a value" + ] + } + ], + "execution_count": 4 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2024-12-10T14:52:52.461529Z", + "start_time": "2024-12-09T21:51:54.241796Z" + } + }, + "cell_type": "code", + "source": [ + "print(total_shuffle_states(k,p))\n", + "print(total_valid_nkode_states(k,p))" + ], + "id": "6e031aca38204895", + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'k' is not defined", + "output_type": "error", + "traceback": [ + "\u001B[0;31m---------------------------------------------------------------------------\u001B[0m", + "\u001B[0;31mNameError\u001B[0m Traceback (most recent call last)", + "Cell \u001B[0;32mIn[2], line 1\u001B[0m\n\u001B[0;32m----> 1\u001B[0m \u001B[38;5;28mprint\u001B[39m(total_shuffle_states(k,p))\n\u001B[1;32m 2\u001B[0m \u001B[38;5;28mprint\u001B[39m(total_valid_nkode_states(k,p))\n", + "\u001B[0;31mNameError\u001B[0m: name 'k' is not defined" + ] + } + ], + "execution_count": 2 + }, + { + "metadata": {}, + "cell_type": "code", + "outputs": [], + "execution_count": null, + "source": "", + "id": "df3525c6e2bdaa8d" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/requirements.txt b/requirements.txt index e69de29..249cfb6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1 @@ +numpy==2.1.3 \ No newline at end of file diff --git a/src/keypad.py b/src/keypad.py new file mode 100644 index 0000000..65e7901 --- /dev/null +++ b/src/keypad.py @@ -0,0 +1,69 @@ +from dataclasses import dataclass + +import numpy as np + +@dataclass +class Keypad: + keypad: np.ndarray + k: int # number of keys + p: int # properties per key + keypad_cache: list + max_cache_size: int = 100 + + @staticmethod + def new_keypad(k: int, p: int): + total_properties = k * p + array = np.arange(total_properties) + # Reshape into a 3x4 matrix + keypad = array.reshape(k, p) + set_view = keypad.T + + for set_idx in set_view: + np.random.shuffle(set_idx) + + return Keypad(keypad=set_view.T, k=k, p=p, keypad_cache=[]) + + def partial_shuffle(self): + shuffled_sets = self._shuffle() + sorted_set = shuffled_sets[np.argsort(shuffled_sets[:, 0])] + + while sorted_set in self.keypad_cache: + shuffled_sets = self._shuffle() + sorted_set = shuffled_sets[np.argsort(shuffled_sets[:, 0])] + + self.keypad_cache.append(sorted_set) + self.keypad_cache = self.keypad_cache[:self.max_cache_size] + + self.keypad = shuffled_sets + + def _shuffle(self) -> np.ndarray: + column_permutation = np.random.permutation(self.p) + column_subset = column_permutation[:self.p // 2] + perm_indices = np.random.permutation(self.k) + shuffled_sets = self.keypad.copy() + shuffled_sets[:, column_subset] = shuffled_sets[perm_indices, :][:, column_subset] + return shuffled_sets + + def key_entry(self, target_passcode: list[int]) -> list[int]: + """ + Given target_values, return the row indices they are in. + Assert that each element is >= 0 and < self.k * self.p. + """ + # Convert the list to a NumPy array for vectorized checks + vals = np.array(target_passcode) + + # Validate that each value is within the valid range + if np.any((vals < 0) | (vals >= self.k * self.p)): + raise ValueError("One or more values are out of the valid range.") + + # Flatten the keypad to a 1D array + flat = self.keypad.flatten() + + # Create an inverse mapping from value -> row index + inv_index = np.empty(self.k * self.p, dtype=int) + # Each value v is at position i in 'flat', so row = i // p + for i, v in enumerate(flat): + inv_index[v] = i // self.p + + # Use the inverse mapping to get row indices for all target values + return inv_index[vals].tolist() diff --git a/src/utils.py b/src/utils.py new file mode 100644 index 0000000..7e6d642 --- /dev/null +++ b/src/utils.py @@ -0,0 +1,7 @@ +from math import factorial, comb + +def total_valid_nkode_states(k: int, p: int) -> int: + return factorial(k) ** (p-1) + +def total_shuffle_states(k: int, p: int) -> int: + return comb(p, p // 2) * factorial(k) diff --git a/tests/test_keypad.py b/tests/test_keypad.py new file mode 100644 index 0000000..6b1d3e7 --- /dev/null +++ b/tests/test_keypad.py @@ -0,0 +1,23 @@ +import numpy as np + +from src.keypad import Keypad + +def test_keypad(): + keypad = Keypad( + keypad=np.array([ + [8, 9, 10, 11], + [0, 5, 2, 3], + [4, 1, 6,7] + ]), k= 3, p=4, keypad_cache=[]) + + assert keypad.key_entry([8, 5, 6, 11]) == [0,1,2,0] + +def test_shuffle(): + p = 4 # properties_per_key + k = 3 # number_of_keys + keypad = Keypad.new_keypad(k, p) + print(keypad.keypad) + keypad.partial_shuffle() + print(keypad.keypad) + keypad.partial_shuffle() + print(keypad.keypad) \ No newline at end of file