implement and test evilkode
This commit is contained in:
174
notebooks/evilkode.ipynb
Normal file
174
notebooks/evilkode.ipynb
Normal file
File diff suppressed because one or more lines are too long
90
src/benchmark.py
Normal file
90
src/benchmark.py
Normal 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
49
src/evilkode.py
Normal 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")
|
||||
@@ -27,15 +27,16 @@ class Keypad:
|
||||
shuffled_sets = self._shuffle()
|
||||
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()
|
||||
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 = shuffled_sets
|
||||
|
||||
|
||||
def _shuffle(self) -> np.ndarray:
|
||||
column_permutation = np.random.permutation(self.p)
|
||||
column_subset = column_permutation[:self.p // 2]
|
||||
@@ -44,6 +45,10 @@ class Keypad:
|
||||
shuffled_sets[:, column_subset] = shuffled_sets[perm_indices, :][:, column_subset]
|
||||
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]:
|
||||
"""
|
||||
Given target_values, return the row indices they are in.
|
||||
|
||||
@@ -4,4 +4,4 @@ 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)
|
||||
return comb((p-1), (p-1) // 2) * factorial(k)
|
||||
|
||||
45
tests/test_evilkode.py
Normal file
45
tests/test_evilkode.py
Normal 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
|
||||
@@ -12,12 +12,20 @@ def test_keypad():
|
||||
|
||||
assert keypad.key_entry([8, 5, 6, 11]) == [0,1,2,0]
|
||||
|
||||
def test_shuffle():
|
||||
def test_partial_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()
|
||||
|
||||
|
||||
def test_full_shuffle():
|
||||
p = 4 # properties_per_key
|
||||
k = 3 # number_of_keys
|
||||
keypad = Keypad.new_keypad(k, p)
|
||||
print(keypad.keypad)
|
||||
keypad.full_shuffle()
|
||||
print(keypad.keypad)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user