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()
|
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.
|
||||||
|
|||||||
@@ -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
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]
|
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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user