implement keypad

This commit is contained in:
2024-12-10 11:05:43 -06:00
parent 775e6f9239
commit 1cdfc9b927
6 changed files with 222 additions and 0 deletions

BIN
docs/PINDragon_102015.pdf Normal file

Binary file not shown.

122
notebooks/tests.ipynb Normal file
View File

@@ -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
}

View File

@@ -0,0 +1 @@
numpy==2.1.3

69
src/keypad.py Normal file
View File

@@ -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()

7
src/utils.py Normal file
View File

@@ -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)

23
tests/test_keypad.py Normal file
View File

@@ -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)