implement and test evilkode

This commit is contained in:
2024-12-11 10:15:18 -06:00
parent 1cdfc9b927
commit 80283a576e
7 changed files with 377 additions and 6 deletions

174
notebooks/evilkode.ipynb Normal file

File diff suppressed because one or more lines are too long

90
src/benchmark.py Normal file
View File

@@ -0,0 +1,90 @@
from src.evilkode import Observation, Evilkode
from src.keypad import Keypad
import random
from dataclasses import dataclass
from statistics import mean, variance
from enum import Enum
@dataclass
class Benchmark:
mean: int
variance: int
runs: list[int]
class ShuffleTypes(Enum):
FULL_SHUFFLE = "FULL_SHUFFLE"
PARTIAL_SHUFFLE = "PARTIAL_SHUFFLE"
def observations(number_of_keys, properties_per_key, passcode_len, shuffle_type: ShuffleTypes = ShuffleTypes.PARTIAL_SHUFFLE ):
k = number_of_keys
p = properties_per_key
n = passcode_len
nkode = [random.randint(0, k*p-1) for _ in range(n)]
keypad = Keypad.new_keypad(k, p)
def obs_gen():
for _ in range(100): # finite number of yields
yield Observation(
keypad=keypad.keypad.copy(),
key_selection=keypad.key_entry(target_passcode=nkode)
)
match shuffle_type:
case ShuffleTypes.FULL_SHUFFLE:
keypad.full_shuffle()
case ShuffleTypes.PARTIAL_SHUFFLE:
keypad.partial_shuffle()
case _:
raise Exception(f"no shuffle type {shuffle_type}")
return obs_gen()
def partial_shuffle_benchmark(
number_of_keys: int,
properties_per_key: int,
passcode_len: int,
max_tries_before_lockout: int,
run_count: int,
) -> Benchmark:
runs = []
for _ in range(run_count):
evilkode = Evilkode(
observations=observations(number_of_keys, properties_per_key, passcode_len),
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
max_tries_before_lockout=max_tries_before_lockout
)
evilout = evilkode.run()
runs.append(evilout.iterations)
return Benchmark(
mean=mean(runs),
variance=variance(runs),
runs=runs
)
def full_shuffle_benchmark(
number_of_keys: int,
properties_per_key: int,
passcode_len: int,
max_tries_before_lockout: int,
run_count: int,
) -> Benchmark:
runs = []
for _ in range(run_count):
evilkode = Evilkode(
observations=observations(number_of_keys, properties_per_key, passcode_len, shuffle_type=ShuffleTypes.FULL_SHUFFLE),
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
max_tries_before_lockout=max_tries_before_lockout
)
evilout = evilkode.run()
runs.append(evilout.iterations)
return Benchmark(
mean=mean(runs),
variance=variance(runs),
runs=runs
)

49
src/evilkode.py Normal file
View File

@@ -0,0 +1,49 @@
import math
from dataclasses import dataclass
from itertools import chain
from typing import Iterator
@dataclass
class Observation:
keypad: list[list[int]]
key_selection: list[int]
@property
def property_list(self) -> list[set[int]]:
return [set(self.keypad[idx]) for idx in self.key_selection]
@dataclass
class EvilOutput:
possible_nkodes: list[list[int]]
iterations: int
@property
def number_of_possible_nkode(self):
return math.prod([len(el) for el in self.possible_nkodes])
@dataclass
class Evilkode:
observations: Iterator[Observation]
passcode_len: int
number_of_keys: int
properties_per_key: int
max_tries_before_lockout: int = 5
possible_nkode = None
def initialize(self):
possible_values = set(range(self.number_of_keys * self.properties_per_key))
self.possible_nkode = [possible_values.copy() for _ in range(self.passcode_len)]
def run(self) -> EvilOutput:
self.initialize()
for idx, obs in enumerate(self.observations):
if math.prod([len(el) for el in self.possible_nkode]) <= self.max_tries_before_lockout:
return EvilOutput(possible_nkodes=[list(el) for el in self.possible_nkode], iterations=idx+1)
for jdx, props in enumerate(obs.property_list):
self.possible_nkode[jdx] = props.intersection(self.possible_nkode[jdx])
raise Exception("error in Evilkode, observations stopped yielding")

View File

@@ -27,15 +27,16 @@ class Keypad:
shuffled_sets = self._shuffle() shuffled_sets = self._shuffle()
sorted_set = shuffled_sets[np.argsort(shuffled_sets[:, 0])] sorted_set = shuffled_sets[np.argsort(shuffled_sets[:, 0])]
while sorted_set in self.keypad_cache: while str(sorted_set) in self.keypad_cache:
shuffled_sets = self._shuffle() shuffled_sets = self._shuffle()
sorted_set = shuffled_sets[np.argsort(shuffled_sets[:, 0])] sorted_set = shuffled_sets[np.argsort(shuffled_sets[:, 0])]
self.keypad_cache.append(sorted_set) self.keypad_cache.append(str(sorted_set))
self.keypad_cache = self.keypad_cache[:self.max_cache_size] self.keypad_cache = self.keypad_cache[:self.max_cache_size]
self.keypad = shuffled_sets self.keypad = shuffled_sets
def _shuffle(self) -> np.ndarray: def _shuffle(self) -> np.ndarray:
column_permutation = np.random.permutation(self.p) column_permutation = np.random.permutation(self.p)
column_subset = column_permutation[:self.p // 2] column_subset = column_permutation[:self.p // 2]
@@ -44,6 +45,10 @@ class Keypad:
shuffled_sets[:, column_subset] = shuffled_sets[perm_indices, :][:, column_subset] shuffled_sets[:, column_subset] = shuffled_sets[perm_indices, :][:, column_subset]
return shuffled_sets return shuffled_sets
def full_shuffle(self):
shuffled_matrix = np.array([np.random.permutation(row) for row in self.keypad.T])
self.keypad = shuffled_matrix.T
def key_entry(self, target_passcode: list[int]) -> list[int]: def key_entry(self, target_passcode: list[int]) -> list[int]:
""" """
Given target_values, return the row indices they are in. Given target_values, return the row indices they are in.

View File

@@ -4,4 +4,4 @@ def total_valid_nkode_states(k: int, p: int) -> int:
return factorial(k) ** (p-1) return factorial(k) ** (p-1)
def total_shuffle_states(k: int, p: int) -> int: def total_shuffle_states(k: int, p: int) -> int:
return comb(p, p // 2) * factorial(k) return comb((p-1), (p-1) // 2) * factorial(k)

45
tests/test_evilkode.py Normal file
View File

@@ -0,0 +1,45 @@
import random
import pytest
from src.evilkode import Evilkode, Observation
from src.keypad import Keypad
@pytest.fixture
def observations(number_of_keys, properties_per_key, passcode_len):
k = number_of_keys
p = properties_per_key
n = passcode_len
nkode = [random.randint(0, k*p-1) for _ in range(n)]
keypad = Keypad.new_keypad(k, p)
def obs_gen():
for _ in range(100): # finite number of yields
yield Observation(
keypad=keypad.keypad.copy(),
key_selection=keypad.key_entry(target_passcode=nkode)
)
keypad.partial_shuffle()
return obs_gen()
@pytest.mark.parametrize(
"number_of_keys, properties_per_key, passcode_len",
[
(5, 3, 4), # Test case 1
(10, 5, 6), # Test case 2
(8, 4, 5), # Test case 3
]
)
def test_evilkode(number_of_keys, properties_per_key, passcode_len, observations):
evilkode = Evilkode(
observations=observations,
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
)
evilout = evilkode.run()
assert evilout.iterations > 1

View File

@@ -12,12 +12,20 @@ def test_keypad():
assert keypad.key_entry([8, 5, 6, 11]) == [0,1,2,0] assert keypad.key_entry([8, 5, 6, 11]) == [0,1,2,0]
def test_shuffle(): def test_partial_shuffle():
p = 4 # properties_per_key p = 4 # properties_per_key
k = 3 # number_of_keys k = 3 # number_of_keys
keypad = Keypad.new_keypad(k, p) keypad = Keypad.new_keypad(k, p)
print(keypad.keypad) print(keypad.keypad)
keypad.partial_shuffle() keypad.partial_shuffle()
print(keypad.keypad) print(keypad.keypad)
keypad.partial_shuffle()
def test_full_shuffle():
p = 4 # properties_per_key
k = 3 # number_of_keys
keypad = Keypad.new_keypad(k, p)
print(keypad.keypad) print(keypad.keypad)
keypad.full_shuffle()
print(keypad.keypad)