diff --git a/pypuf/experiments/experiment/reliability_based_cmaes.py b/pypuf/experiments/experiment/reliability_based_cmaes.py new file mode 100644 index 00000000..1e4b4480 --- /dev/null +++ b/pypuf/experiments/experiment/reliability_based_cmaes.py @@ -0,0 +1,148 @@ +"""This module provides an experiment class which learns an instance of LTFArray with reliability based CMAES learner. +It is based on the work from G. T. Becker in "The Gap Between Promise and Reality: On the Insecurity of +XOR Arbiter PUFs". The learning algorithm applies Covariance Matrix Adaptation Evolution Strategies from N. Hansen +in "The CMA Evolution Strategy: A Comparing Review". +""" +import numpy as np + +from pypuf.experiments.experiment.base import Experiment +from pypuf.learner.evolution_strategies.reliability_based_cmaes import ReliabilityBasedCMAES +from pypuf.simulation.arbiter_based.ltfarray import LTFArray, NoisyLTFArray +from pypuf import tools + + +class ExperimentReliabilityBasedCMAES(Experiment): + """This class implements an experiment for executing the reliability based CMAES learner for XOR LTF arrays. + It provides all relevant parameters as well as an instance of an LTF array to learn. + Furthermore, the learning results are being logged into csv files. + """ + + def __init__( + self, log_name, + seed_instance, k, n, transform, combiner, noisiness, + seed_challenges, num, reps, + seed_model, pop_size, limit_stag, limit_iter + ): + """Initialize an Experiment using the Reliability based CMAES Learner for modeling LTF Arrays + :param log_name: Log name, Prefix of the name of the experiment log file + :param seed_instance: PRNG seed used to create an LTF array instance to learn + :param k: Width, the number of parallel LTFs in the LTF array + :param n: Length, the number stages within the LTF array + :param transform: Transformation function, the function that modifies the input within the LTF array + :param combiner: Combiner, the function that combines particular chains' outputs within the LTF array + :param noisiness: Noisiness, the relative scale of noise of instance compared to the scale of weights + :param seed_challenges: PRNG seed used to sample challenges + :param num: Challenge number, the number of binary inputs (challenges) for the LTF array + :param reps: Repetitions, the number of evaluations of every challenge (part of training_set) + :param seed_model: PRNG seed used by the CMAES algorithm for sampling solution points + :param pop_size: Population size, the number of sampled points of every CMAES iteration + :param limit_stag: Stagnation limit, the maximal number of stagnating iterations within the CMAES + :param limit_iter: Iteration limit, the maximal number of iterations within the CMAES + """ + super().__init__( + log_name='%s.0x%x_%i_%i_%i_%i_%i' % ( + log_name, + seed_instance, + k, + n, + num, + reps, + pop_size, + ), + ) + # Instance of LTF array to learn + self.seed_instance = seed_instance + self.prng_i = np.random.RandomState(seed=self.seed_instance) + self.k = k + self.n = n + self.transform = transform + self.combiner = combiner + self.noisiness = noisiness + # Training set + self.seed_challenges = seed_challenges + self.prng_c = np.random.RandomState(seed=self.seed_instance) + self.num = num + self.reps = reps + self.training_set = None + # Parameters for CMAES + self.seed_model = seed_model + self.pop_size = pop_size + self.limit_s = limit_stag + self.limit_i = limit_iter + # Basic objects + self.instance = None + self.learner = None + self.model = None + + def run(self): + """Initialize the instance, the training set and the learner + to then run the Reliability based CMAES with the given parameters + """ + self.instance = NoisyLTFArray( + weight_array=LTFArray.normal_weights(self.n, self.k, random_instance=self.prng_i), + transform=self.transform, + combiner=self.combiner, + sigma_noise=NoisyLTFArray.sigma_noise_from_random_weights(self.n, 1, self.noisiness), + random_instance=self.prng_i, + ) + self.training_set = tools.TrainingSet(self.instance, self.num, self.prng_c, self.reps) + self.learner = ReliabilityBasedCMAES( + self.training_set, + self.k, + self.n, + self.transform, + self.combiner, + self.pop_size, + self.limit_s, + self.limit_i, + self.seed_model, + self.progress_logger, + ) + self.model = self.learner.learn() + + def analyze(self): + """Analyze the learned model""" + assert self.model is not None + self.result_logger.info( + '0x%x\t0x%x\t0x%x\t%i\t%i\t%i\t%f\t%i\t%i\t%f\t%s\t%f\t%s\t%i\t%i\t%s', + self.seed_instance, + self.seed_challenges, + self.seed_model, + self.n, + self.k, + self.num, + self.noisiness, + self.reps, + self.pop_size, + 1.0 - tools.approx_dist(self.instance, self.model, min(100000, 2 ** self.n), self.prng_c), + ','.join(map(str, self.calc_individual_accs())), + self.measured_time, + self.learner.stops, + self.learner.num_abortions, + self.learner.num_iterations, + ','.join(map(str, self.model.weight_array.flatten() / np.linalg.norm(self.model.weight_array.flatten()))), + ) + + def calc_individual_accs(self): + """Calculate the accuracies of individual chains of the learned model""" + transform = self.model.transform + combiner = self.model.combiner + accuracies = np.zeros(self.k) + poles = np.zeros(self.k) + for i in range(self.k): + chain_original = LTFArray(self.instance.weight_array[i, np.newaxis, :], transform, combiner) + for j in range(self.k): + chain_model = LTFArray(self.model.weight_array[j, np.newaxis, :], transform, combiner) + accuracy = tools.approx_dist(chain_original, chain_model, min(10000, 2 ** self.n), self.prng_c) + pole = 1 + if accuracy < 0.5: + accuracy = 1.0 - accuracy + pole = -1 + if accuracy > accuracies[i]: + accuracies[i] = accuracy + poles[i] = pole + accuracies *= poles + for i in range(self.k): + if accuracies[i] < 0: + accuracies[i] += 1 + return accuracies diff --git a/pypuf/learner/evolution_strategies/__init__.py b/pypuf/learner/evolution_strategies/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pypuf/learner/evolution_strategies/reliability_based_cmaes.py b/pypuf/learner/evolution_strategies/reliability_based_cmaes.py new file mode 100644 index 00000000..0d8f42e7 --- /dev/null +++ b/pypuf/learner/evolution_strategies/reliability_based_cmaes.py @@ -0,0 +1,241 @@ +"""This module provides a learner exploiting different reliabilities of challenges evaluated several times on an +XOR Arbiter PUF. It is based on the work from G. T. Becker in "The Gap Between Promise and Reality: On the Insecurity +of XOR Arbiter PUFs". The learning algorithm applies Covariance Matrix Adaptation Evolution Strategies from +N. Hansen in "The CMA Evolution Strategy: A Comparing Review". +""" +import sys +import contextlib +import numpy as np +from scipy.special import gamma +from scipy.linalg import norm +import cma + +from pypuf import tools +from pypuf.learner.base import Learner +from pypuf.simulation.arbiter_based.ltfarray import LTFArray + + +class ReliabilityBasedCMAES(Learner): + """This class implements a learner for XOR LTF arrays. Thus, by means of a CMAES algorithm a model + is created similar to the original LTF array. This process is based on the information of reliability + originating from multiple repeatedly evaluated challenges. + """ + + # Constants + CONST_EPSILON = 0.1 + FREQ_ABORTION_CHECK = 50 + FREQ_LOGGING = 1 + THRESHOLD_DIST = 0.25 + + # Semi-constant :-) + approx_challenge_num = 10000 + + def __init__(self, training_set, k, n, transform, combiner, + pop_size, limit_stag, limit_iter, random_seed, logger): + """Initialize a Reliability based CMAES Learner for the specified LTF array + + :param training_set: Training set, a data structure containing repeated challenge response pairs + :param k: Width, the number of parallel LTFs in the LTF array + :param n: Length, the number stages within the LTF array + :param transform: Transformation function, the function that modifies the input within the LTF array + :param combiner: Combiner, the function that combines particular chains' outputs within the LTF array + :param pop_size: Population size, the number of sampled points of every CMAES iteration + :param limit_stag: Stagnation limit, the maximal number of stagnating iterations within the CMAES + :param limit_iter: Iteration limit, the maximal number of iterations within the CMAES + :param random_seed: PRNG seed used by the CMAES algorithm for sampling solution points + :param logger: Logger, the instance that logs detailed information every learning iteration + """ + self.training_set = training_set + self.k = k + self.n = n + self.transform = transform + self.combiner = combiner + self.pop_size = pop_size + self.limit_s = limit_stag + self.limit_i = limit_iter + self.prng = np.random.RandomState(random_seed) + self.chains_learned = np.zeros((self.k, self.n)) + self.num_iterations = 0 + self.stops = '' + self.num_abortions = 0 + self.num_learned = 0 + self.logger = logger + + if 2**n < self.approx_challenge_num: + self.approx_challenge_num = 2 ** n + + def learn(self): + """Compute a model according to the given LTF Array parameters and training set + Note that this function can take long to return + :return: pypuf.simulation.arbiter_based.LTFArray + The computed model. + """ + + def log_state(ellipsoid): + """Log a snapshot of learning variables while running""" + if self.logger is None: + return + self.logger.debug( + '%i\t%f\t%f\t%i\t%i\t%s', + self.num_iterations, + ellipsoid.sigma, + fitness(ellipsoid.mean), + self.num_learned, + self.num_abortions, + ','.join(map(str, list(ellipsoid.mean))), + ) + + # Preparation + epsilon = np.sqrt(self.n) * self.CONST_EPSILON + fitness = self.create_fitness_function( + challenges=self.training_set.challenges, + measured_rels=self.measure_rels(self.training_set.responses), + epsilon=epsilon, + transform=self.transform, + combiner=self.combiner, + ) + normalize = np.sqrt(2) * gamma(self.n / 2) / gamma((self.n - 1) / 2) + mean_start = np.zeros(self.n) + step_size_start = 1 + options = { + 'seed': 0, + 'pop': self.pop_size, + 'maxiter': self.limit_i, + 'tolstagnation': self.limit_s, + } + + # Learn all individual LTF arrays (chains) + with self.avoid_printing(): + while self.num_learned < self.k: + aborted = False + options['seed'] = self.prng.randint(2 ** 32) + is_same_solution = self.create_abortion_function( + chains_learned=self.chains_learned, + num_learned=self.num_learned, + transform=self.transform, + combiner=self.combiner, + threshold=self.THRESHOLD_DIST, + ) + search = cma.CMAEvolutionStrategy(x0=mean_start, sigma0=step_size_start, inopts=options) + counter = 1 + # Learn individual LTF array using abortion if evolutionary search approximates previous a solution + while not search.stop(): + curr_points = search.ask() # Sample new solution points + search.tell(curr_points, [fitness(point) for point in curr_points]) + self.num_iterations += 1 + if counter % self.FREQ_LOGGING == 0: + log_state(search) + counter += 1 + if counter % self.FREQ_ABORTION_CHECK == 0: + if is_same_solution(search.mean): + self.num_abortions += 1 + aborted = True + break + solution = search.result.xbest + + # Include normalized solution, if it is different from previous ones + if not aborted: + self.chains_learned[self.num_learned] = normalize * solution / norm(solution) + self.num_learned += 1 + if self.stops != '': + self.stops += ',' + self.stops += '_'.join(list(search.stop())) + + # Polarize the learned combined LTF array + majority_responses = self.majority_responses(self.training_set.responses) + self.chains_learned = self.polarize_chains( + chains_learned=self.chains_learned, + challenges=self.training_set.challenges, + majority_responses=majority_responses, + transform=self.transform, + combiner=self.combiner, + ) + return LTFArray(self.chains_learned, self.transform, self.combiner) + + @staticmethod + @contextlib.contextmanager + def avoid_printing(): + """Avoid printing on sys.stdout while learning""" + save_stdout = sys.stdout + sys.stdout = open('/dev/null', 'w') + yield + sys.stdout.close() + sys.stdout = save_stdout + + @staticmethod + def create_fitness_function(challenges, measured_rels, epsilon, transform, combiner): + """Return a fitness function on a fixed set of challenges and corresponding reliabilities""" + this = ReliabilityBasedCMAES + + def fitness(individual): + """Return individuals sorted by their correlation coefficient as fitness""" + ltf_array = LTFArray(individual[np.newaxis, :], transform, combiner) + delay_diffs = ltf_array.val(challenges) + reliabilities = np.zeros(np.shape(delay_diffs)) + indices_of_reliable = np.abs(delay_diffs[:]) > epsilon + reliabilities[indices_of_reliable] = 1 + correlation = this.calc_corr(reliabilities, measured_rels) + obj_vals = 1 - (1 + correlation)/2 + return obj_vals + + return fitness + + @staticmethod + def calc_corr(reliabilities, measured_rels): + """Return pearson correlation coefficient between reliability arrays of individual and instance""" + if np.var(reliabilities[:]) == 0: # Avoid dividing by zero + return -1 + else: + return np.corrcoef(reliabilities[:], measured_rels)[0, 1] + + @staticmethod + def create_abortion_function(chains_learned, num_learned, transform, combiner, threshold): + """Return an abortion function on a fixed set of challenges and LTFs""" + this = ReliabilityBasedCMAES + weight_arrays = chains_learned[:num_learned, :] + learned_ltf_arrays = list(this.build_individual_ltf_arrays(weight_arrays, transform, combiner)) + + def is_same_solution(solution): + """Return True, if the current solution mean within CMAES is similar to a previously learned LTF array""" + if num_learned == 0: + return False + new_ltf_array = LTFArray(solution[np.newaxis, :], transform, combiner) + for current_ltf_array in learned_ltf_arrays: + dist = tools.approx_dist(current_ltf_array, new_ltf_array, this.approx_challenge_num) + if dist < threshold or dist > (1 - threshold): + return True + return False + + return is_same_solution + + @staticmethod + def polarize_chains(chains_learned, challenges, majority_responses, transform, combiner): + """Return the correctly polarized combined LTF array""" + model = LTFArray(chains_learned, transform, combiner) + responses_model = model.eval(challenges) + num, _ = np.shape(challenges) + accuracy = np.count_nonzero(responses_model == majority_responses) / num + polarized_chains = chains_learned + if accuracy < 0.5: + polarized_chains[0, :] *= -1 + return polarized_chains + + @staticmethod + def build_individual_ltf_arrays(weight_arrays, transform, combiner): + """Return iterator over LTF arrays created out of every individual""" + pop_size, _ = np.shape(weight_arrays) + for i in range(pop_size): + yield LTFArray(weight_arrays[i, np.newaxis, :], transform, combiner) + + @staticmethod + def majority_responses(responses): + """Return the common responses out of repeated responses""" + return np.sign(np.sum(responses, axis=0)) + + @staticmethod + def measure_rels(responses): + """Return array of measured reliabilities of instance""" + measured_rels = np.abs(np.sum(responses, axis=0)) + if np.var(measured_rels) == 0: + raise Exception('The challenges\' reliabilities evaluated on the instance to learn are to high!') + return measured_rels diff --git a/pypuf/tools.py b/pypuf/tools.py index 663c5f8d..9f854a08 100644 --- a/pypuf/tools.py +++ b/pypuf/tools.py @@ -10,8 +10,10 @@ from random import sample from numpy import abs as np_abs, absolute +from numpy import copy as np_copy from numpy import count_nonzero, array, append, zeros, vstack, mean, prod, ones, dtype, full, shape, copy, int8, \ multiply, empty, average +from numpy import squeeze from numpy import sum as np_sum from numpy.random import RandomState @@ -212,7 +214,7 @@ def transform_challenge_01_to_11(challenge): Same vector in -1,1 notation """ assert_result_type(challenge) - res = copy(challenge) + res = np_copy(challenge) res[res == 1] = -1 res[res == 0] = 1 return res @@ -227,7 +229,7 @@ def transform_challenge_11_to_01(challenge): Same vector in 0,1 notation """ assert_result_type(challenge) - res = copy(challenge) + res = np_copy(challenge) res[res == 1] = 0 res[res == -1] = 1 return res @@ -416,20 +418,31 @@ class TrainingSet(ChallengeResponseSet): Note that this is, strictly speaking, not a set. """ - def __init__(self, instance, N, random_instance=RandomState()): + def __init__(self, instance, N, random_instance=RandomState(), reps=1): """ - :param instance: pypuf.simulation.base.Simulation - Instance which is used to generate responses for random challenges. - :param N: int - Number of desired challenges + :param instance: pypuf.simulation.base.Simulation + Instance which is used to generate responses for random challenges + :param N: int + Number of desired challenges :param random_instance: numpy.random.RandomState - PRNG which is used to draft challenges. + PRNG instance for pseudo random sampling challenges + :param reps: int + Number of repeated evaluations of every challenge on instance (None equals 1) """ self.instance = instance - challenges = sample_inputs(instance.n, N, random_instance=random_instance) + self.N = min(N, 2 ** instance.n) + challenges = sample_inputs(instance.n, self.N, random_instance=random_instance) + responses = zeros((reps, self.N)) + for i in range(reps): + challenges, copy = itertools.tee(challenges) + responses[i, :] = instance.eval(array(list(copy))) + challenges = array(list(challenges)) + if reps == 1: + responses = squeeze(responses, axis=0) + self.reps = reps super().__init__( challenges=challenges, - responses=instance.eval(challenges) + responses=responses, ) diff --git a/requirements.txt b/requirements.txt index c4b10336..46e3b44b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ scipy~=1.2.0 matplotlib~=3.0.0 pandas~=0.24.1 seaborn~=0.9.0 +cma~=2.6 diff --git a/sim_rel_cmaes_attack.py b/sim_rel_cmaes_attack.py new file mode 100644 index 00000000..0c656fc8 --- /dev/null +++ b/sim_rel_cmaes_attack.py @@ -0,0 +1,114 @@ +""" +This module is a command line tool which provides an interface for experiments which are designed to learn an arbiter +PUF LTFarray simulation with the reliability based CMAES learning algorithm. If you want to use this tool you will have +to define nine parameters which define the experiment. +""" +from sys import argv, stderr +import numpy.random as rnd + +from pypuf.simulation.arbiter_based.ltfarray import LTFArray +from pypuf.experiments.experiment.reliability_based_cmaes import ExperimentReliabilityBasedCMAES +from pypuf.experiments.experimenter import Experimenter + + +def main(args): + """This method includes the main functionality of the module it parses the argument vector + and executes the learning attempts on the PUF instances. + """ + if len(args) < 9 or len(args) > 15: + stderr.write('\n***LTF Array Simulator and Reliability based CMAES Learner***\n\n') + stderr.write('Usage:\n') + stderr.write( + 'python sim_rel_cmaes_attack.py n k noisiness num reps pop_size [seed_i] [seed_c] [seed_m] [log_name]\n') + stderr.write(' n: number of bits per Arbiter chain\n') + stderr.write(' k: number of Arbiter chains\n') + stderr.write(' noisiness: proportion of noise scale related to the scale of variability\n') + stderr.write(' num: number of different challenges in the training set\n') + stderr.write(' reps: number of responses for each challenge in the training set\n') + stderr.write(' pop_size: number of solution points sampled per iteration within the CMAES algorithm\n') + stderr.write(' limit_s: max number of iterations with consistent fitness within the CMAES algorithm\n') + stderr.write(' limit_i: max number of overall iterations within the CMAES algorithm\n') + stderr.write(' [log_name]: name of the logfile which contains all results from the experiment.\n' + ' The tool will add a ".log" to log_name. The default is "sim_rel_cmaes.log"\n') + stderr.write(' [instances]: number of repeated initializations of the instance\n') + stderr.write(' [attempts]: number of repeated initializations of the learner for the same instance\n') + stderr.write(' The number of total learning executions is instances times attempts.\n') + stderr.write(' [seed_i]: random seed for creating LTF array instance and simulating its noise\n') + stderr.write(' [seed_c]: random seed for sampling challenges\n') + stderr.write(' [seed_m]: random seed for modelling LTF array instance\n') + quit(1) + + # Use obligatory parameters + n = int(args[1]) + k = int(args[2]) + noisiness = float(args[3]) + num = int(args[4]) + reps = int(args[5]) + pop_size = int(args[6]) + limit_s = int(args[7]) + limit_i = int(args[8]) + + # Initialize or use optional parameters + log_name = 'sim_rel_cmaes' + instances = 1 + attempts = 1 + seed_i = rnd.randint(0, 2 ** 32) + seed_c = rnd.randint(0, 2 ** 32) + seed_m = rnd.randint(0, 2 ** 32) + if len(args) >= 10: + log_name = args[9] + if len(args) >= 11: + instances = int(args[10]) + if len(args) >= 12: + attempts = int(args[11]) + if len(args) >= 13: + seed_i = int(args[12], 0) + if len(args) >= 14: + seed_c = int(args[13], 0) + if len(args) == 15: + seed_m = int(args[14], 0) + + stderr.write('Learning %i time(s) each %i (%i,%i)-XOR Arbiter PUF(s) with %f noisiness, ' + 'using %i different %i times repeated CRPs.\n' + 'There, %i solution points are sampled each iteration of the CMAES algorithm. ' + 'Among other termination criteria, it stops if the fitness stagnates for %i iterations ' + 'or the total number of iterations equals %i.\n' + % (attempts, instances, n, k, noisiness, num, reps, pop_size, limit_s, limit_i)) + stderr.write('The following seeds are used for generating pseudo random numbers.\n') + stderr.write(' seed for instance: 0x%x\n' % seed_i) + stderr.write(' seed for challenges: 0x%x\n' % seed_c) + stderr.write(' seed for model: 0x%x\n' % seed_m) + stderr.write('\n') + + # Create different experiment instances + experiments = [] + for instance in range(instances): + for attempt in range(attempts): + l_name = log_name + if instances > 1 or attempts > 1: + l_name += '_%i_%i' % (instance, attempt) + experiment = ExperimentReliabilityBasedCMAES( + log_name=l_name, + seed_instance=(seed_i + instance) % 2**32, + k=k, + n=n, + transform=LTFArray.transform_id, + combiner=LTFArray.combiner_xor, + noisiness=noisiness, + seed_challenges=(seed_c + instance) % 2**32, + num=num, + reps=reps, + seed_model=(seed_m + attempt) % 2**32, + pop_size=pop_size, + limit_stag=limit_s, + limit_iter=limit_i, + ) + experiments.append(experiment) + + experimenter = Experimenter(log_name, experiments) + # Run the instances + experimenter.run() + + +if __name__ == '__main__': + main(argv) diff --git a/test/test_experiment.py b/test/test_experiment.py index edf23ea6..312c62af 100644 --- a/test/test_experiment.py +++ b/test/test_experiment.py @@ -1,12 +1,19 @@ """This module tests the different experiment classes.""" import unittest -from test.utility import remove_test_logs, logging, get_functions_with_prefix, LOG_PATH +from multiprocessing import Queue, Process + from numpy import around +from numpy import shape +from numpy.random import RandomState from numpy.testing import assert_array_equal -from pypuf.simulation.arbiter_based.ltfarray import LTFArray, NoisyLTFArray + from pypuf.experiments.experiment.logistic_regression import ExperimentLogisticRegression, Parameters as LRParameters from pypuf.experiments.experiment.majority_vote import ExperimentMajorityVoteFindVotes, Parameters as MVParameters from pypuf.experiments.experiment.property_test import ExperimentPropertyTest, Parameters as PTParameters +from pypuf.experiments.experiment.reliability_based_cmaes import ExperimentReliabilityBasedCMAES +from pypuf.experiments.experimenter import log_listener, setup_logger +from pypuf.simulation.arbiter_based.ltfarray import LTFArray, NoisyLTFArray +from test.utility import remove_test_logs, logging, get_functions_with_prefix, LOG_PATH class TestBase(unittest.TestCase): @@ -298,3 +305,77 @@ def create_experiment(N, test_function, ins_gen_function, param_ins_gen): exp_rel = create_experiment(N, test_function, 'create_noisy_ltf_arrays', array_parameter) exp_rel.execute(logger.queue, logger.logger_name) + + +class TestExperimentReliabilityBasedCMAES(TestBase): + """This class tests the reliability based CMAES experiment.""" + def test_run_and_analyze(self): + """This method only runs the experiment.""" + logger_name = 'log' + + # Setup multiprocessing logging + queue = Queue(-1) + listener = Process(target=log_listener, args=(queue, setup_logger, logger_name,)) + listener.start() + + experiment = ExperimentReliabilityBasedCMAES( + log_name=logger_name, + seed_instance=0xbee, + k=2, + n=16, + transform=LTFArray.transform_id, + combiner=LTFArray.combiner_xor, + noisiness=0.1, + seed_challenges=0xbee, + num=2**12, + reps=4, + seed_model=0xbee, + pop_size=16, + limit_stag=100, + limit_iter=1000, + ) + experiment.execute(queue, logger_name) + + queue.put_nowait(None) + listener.join() + + def test_calc_individual_accs(self): + """This method tests the calculation of individual (non-polarized) accuracies of a learned model.""" + exp = ExperimentReliabilityBasedCMAES( + log_name='exp_log', + seed_instance=0x123, + k=2, + n=16, + transform=LTFArray.transform_id, + combiner=LTFArray.combiner_xor, + noisiness=.1, + seed_challenges=0x456, + num=40000, + reps=5, + seed_model=0x789, + pop_size=24, + limit_stag=40, + limit_iter=500 + ) + weight_array_model = LTFArray.normal_weights( + exp.n, exp.k, random_instance=RandomState(exp.seed_model) + ) + exp.model = LTFArray( + combiner=exp.combiner, + transform=exp.transform, + weight_array=weight_array_model + ) + weight_array_instance = LTFArray.normal_weights( + exp.n, exp.k, random_instance=RandomState(exp.seed_instance) + ) + exp.instance = LTFArray( + combiner=exp.combiner, + transform=exp.transform, + weight_array=weight_array_instance + ) + individual_accs = exp.calc_individual_accs() + self.assertIsNotNone(individual_accs) + assert shape(individual_accs) == (exp.k,) + for i in range(exp.k): + assert individual_accs[i] > 0.0 + assert individual_accs[i] <= 1.0 diff --git a/test/test_reliability_based_cmaes.py b/test/test_reliability_based_cmaes.py new file mode 100644 index 00000000..80805904 --- /dev/null +++ b/test/test_reliability_based_cmaes.py @@ -0,0 +1,162 @@ +"""This module tests the reliability based CMAES learner.""" +import unittest +import numpy as np + +from pypuf.simulation.arbiter_based.ltfarray import LTFArray, NoisyLTFArray +from pypuf.learner.evolution_strategies.reliability_based_cmaes import ReliabilityBasedCMAES as Learner +from pypuf import tools + + +class TestReliabilityBasedCMAES(unittest.TestCase): + """This class contains tests for the methods of the reliability based CMAES learner.""" + n = 16 + k = 2 + num = 2**12 + reps = 5 + mu_weight = 0 + sigma_weight = 1 + seed_instance = 0x1 + prng_i = np.random.RandomState(seed_instance) + seed_model = 0x2 + prng_m = np.random.RandomState(seed_model) + seed_challenges = 0x3 + prng_c = np.random.RandomState(seed_challenges) + + weight_array = np.array([ + [.1, .2, .3, .4, .5, .6, .7, .8, -.1, -.2, -.3, -.4, -.5, -.6, -.7, -.83], + [.1, .2, .3, .4, -.5, -.6, -.7, -.8, -.1, -.2, -.3, -.4, .5, .6, .7, .81] + ]) + sigma_noise = NoisyLTFArray.sigma_noise_from_random_weights(n, sigma_weight, noisiness=0.05) + + def setUp(self): + """This method initializes class attributes for ensuring transform and combiner to work properly.""" + self.transform = LTFArray.transform_id + self.combiner = LTFArray.combiner_xor + self.instance = NoisyLTFArray(self.weight_array, self.transform, self.combiner, self.sigma_noise, self.prng_i) + self.training_set = tools.TrainingSet(self.instance, self.num, self.prng_c, self.reps) + + def test_create_fitness_function(self): + """This method tests the creation of a fitness function for the CMAES algorithm.""" + measured_rels = Learner.measure_rels(self.training_set.responses) + epsilon = .5 + fitness = Learner.create_fitness_function( + challenges=self.training_set.challenges, + measured_rels=measured_rels, + epsilon=epsilon, + transform=self.transform, + combiner=self.combiner, + ) + self.assertLessEqual(fitness(self.instance.weight_array[0, :]), 0.3) + + def test_create_abortion_function(self): + """This method tests the creation of an abortion function for the CMAES algorithm.""" + is_same_solution = Learner.create_abortion_function( + chains_learned=self.instance.weight_array, + num_learned=2, + transform=self.transform, + combiner=self.combiner, + threshold=0.25, + ) + weight_array = np.array( + [.8, .8, .8, .8, .5, .5, .5, .5, 1.4, 1.4, 1.4, 1.4, -.7, -.7, -.7, -.33] + ) + self.assertFalse(is_same_solution(weight_array)) + self.assertTrue(is_same_solution(self.instance.weight_array[0, :])) + + def test_learn(self): + """This method tests the learning of an XOR LTF array with the reliability based CMAES learner.""" + pop_size = 12 + limit_stag = 100 + limit_iter = 1000 + logger = None + learner = Learner( + training_set=self.training_set, + k=self.k, + n=self.n, + transform=self.transform, + combiner=self.combiner, + pop_size=pop_size, + limit_stag=limit_stag, + limit_iter=limit_iter, + random_seed=self.seed_model, + logger=logger, + ) + model = learner.learn() + distance = tools.approx_dist(self.instance, model, 10000) + self.assertLessEqual(distance, 0.4) + + def test_calc_corr(self): + """This method tests the calculation of the correlation between two arrays.""" + rels_1 = np.array([0, 1, 2, 1]) + rels_2 = np.array([0, 0, 0, 1]) + rels_3 = np.array([0, 1, 2, 5]) + rels_4 = np.array([1, 1, 1, 1]) + corr_1_2 = Learner.calc_corr(rels_1, rels_2) + corr_1_3 = Learner.calc_corr(rels_1, rels_3) + corr_2_3 = Learner.calc_corr(rels_2, rels_3) + corr_4_1 = Learner.calc_corr(rels_4, rels_1) + self.assertLess(corr_1_2, corr_1_3) + self.assertLess(corr_1_3, corr_2_3) + self.assertEqual(corr_4_1, -1) + + def test_polarize_ltfs(self): + """This method tests the polarization of a learned XOR LTF array consistent with its original counterpart.""" + learned_ltfs = np.array([ + [.5, -1, -.5, 1], + [-1, -1, 1, 1], + ]) + challenges = np.array(list(tools.sample_inputs(n=4, num=8, random_instance=self.prng_c))) + majority_responses = np.array([1, 1, 1, 1, -1, -1, -1, -1]) + polarized_ltf_array = Learner.polarize_chains( + chains_learned=learned_ltfs, + challenges=challenges, + majority_responses=majority_responses, + transform=self.transform, + combiner=self.combiner + ) + self.assertIsNotNone(polarized_ltf_array) + + @unittest.skip + def test_build_ltf_arrays(self): + challenges = tools.sample_inputs(self.n, self.num) + ltf_array_original = LTFArray(self.weight_array, self.transform, self.combiner) + res_original = ltf_array_original.eval(challenges) + weight_arrays = self.weight_array[np.newaxis, :].repeat(2, axis=0) + ltf_arrays = Learner.build_ltf_arrays(weight_arrays, self.transform, self.combiner) + for ltf_array in ltf_arrays: + res = ltf_array.eval(challenges) + np.testing.assert_array_equal(res, res_original) + + def test_build_individual_ltf_arrays(self): + """This method tests the sequentially instantiation of single LTF arrays out of given weight arrays.""" + n = 16 + k = 2 + num = 2**10 + prng = np.random.RandomState(0x4) + challenges = np.array(list(tools.sample_inputs(n, num, prng))) + duplicated_weights = np.array([ + [.1, .2, .3, .4, .5, .6, .7, .8, -.1, -.2, -.3, -.4, -.5, -.6, -.7, -.8], + [.1, .2, .3, .4, .5, .6, .7, .8, -.1, -.2, -.3, -.4, -.5, -.6, -.7, -.8] + ]) + ltf_arrays = Learner.build_individual_ltf_arrays(duplicated_weights, self.transform, self.combiner) + res = np.zeros((k, num)) + for i, ltf_array in enumerate(ltf_arrays): + res[i, :] = ltf_array.eval(challenges) + np.testing.assert_array_equal(res[0, :], res[1, :]) + + responses = np.array([ + [1, 1, 1, 1], + [1, 1, 1, -1], + [1, 1, -1, -1], + [1, -1, -1, -1] + ]) + + def test_majority_responses(self): + """This method tests the calculation of majority responses out of given repeated responses.""" + majority_res = Learner.majority_responses(self.responses) + self.assertEqual(majority_res.all(), np.array([1, 1, 0, -1]).all()) + + def test_measure_rels(self): + """This method tests the calculation of reliabilities out of given repeated responses.""" + rels = Learner.measure_rels(self.responses) + self.assertEqual(rels.all(), np.array([4, 2, 0, 2]).all())