Commit e64a2661 authored by Chanelle Lee's avatar Chanelle Lee
Browse files

Adapted best of n simulation to a no comparison simulation. UNTESTED

parent 60a7b896
cmake_minimum_required(VERSION 2.8.12)
project(noComparisonSimulation)
# Set source directory
set(SOURCE_DIR "src/noComparisonSimulation")
#Tell CMake that headers are also in SOURCE_DIR
include_directories(${SOURCE_DIR})
set(SOURCES "${SOURCE_DIR}/agentFunctions.cpp")
set(SOURCES "${SOURCE_DIR}/evidenceFunctions.cpp")
# Generate python module
find_package(pybind11 REQUIRED)
pybind11_add_module(agentFunctions ${SOURCES} "${SOURCE_DIR}/bindings.cpp")
pybind11_add_module(evidenceFunctions ${SOURCES} "${SOURCE_DIR}/bindings.cpp")
"""
Copied from https://github.com/pybind/cmake_example
"""
import os
import re
import sys
import platform
import subprocess
from setuptools import setup, find_packages, Extension
from setuptools.command.build_ext import build_ext
from distutils.version import LooseVersion
class CMakeExtension(Extension):
def __init__(self, name, sourcedir=''):
Extension.__init__(self, name, sources=[])
self.sourcedir = os.path.abspath(sourcedir)
class CMakeBuild(build_ext):
def run(self):
try:
out = subprocess.check_output(['cmake', '--version'])
except OSError:
raise RuntimeError("CMake must be installed to build the following extensions: " +
", ".join(e.name for e in self.extensions))
if platform.system() == "Windows":
cmake_version = LooseVersion(re.search(r'version\s*([\d.]+)', out.decode()).group(1))
if cmake_version < '3.1.0':
raise RuntimeError("CMake >= 3.1.0 is required on Windows")
for ext in self.extensions:
self.build_extension(ext)
def build_extension(self, ext):
extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name)))
cmake_args = ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=' + extdir,
'-DPYTHON_EXECUTABLE=' + sys.executable]
cfg = 'Debug' if self.debug else 'Release'
build_args = ['--config', cfg]
if platform.system() == "Windows":
cmake_args += ['-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}'.format(cfg.upper(), extdir)]
if sys.maxsize > 2**32:
cmake_args += ['-A', 'x64']
build_args += ['--', '/m']
else:
cmake_args += ['-DCMAKE_BUILD_TYPE=' + cfg]
build_args += ['--', '-j2']
env = os.environ.copy()
env['CXXFLAGS'] = '{} -DVERSION_INFO=\\"{}\\"'.format(env.get('CXXFLAGS', ''),
self.distribution.get_version())
if not os.path.exists(self.build_temp):
os.makedirs(self.build_temp)
subprocess.check_call(['cmake', ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env)
subprocess.check_call(['cmake', '--build', '.'] + build_args, cwd=self.build_temp)
print()
setup(name='noComparisonSimulation',
version='0.0.1',
author='Chanelle Lee',
description='A module which calculates simple aggregation for a population',
long_description='',
# tell setuptools to look for any packages under 'src'
packages=find_packages('src'),
package_dir={'': 'src'},
ext_modules=[CMakeExtension('noComparisonSimulation/agentFunctions'),
CMakeExtension('noComparisonSimulation/evidenceFunctions')],
cmdclass=dict(build_ext=CMakeBuild),
zip_safe=False,
)
import numpy as np
from operator import attrgetter
from collections import Counter
# C module written by CLL
from noComparisonSimulation import agentFunctions as af
class Agent(object):
"""
Agent Class
This class holds the individual agents of the population and controls low
level functionality, such as individual aggregation, belief updating,
picking options, neighbour checking and evidence updating.
N.B. This represents an agent in the Best-of-n negative updating scenario
"""
def __init__(self, idNum, belief, distrust, w, numOptions):
"""
Initialising method for Agent Class
Parameters
----------
idNum : int
indentifier for the agent, location in initial population agent
list
belief : numpy array
flat array of floats signifying agents' belief in each option, with
belief in option i correlating to belief[i]. Should always sum to 1
distrust : float
Between [0,0.5] signifying weighting for evidential updating
(Called alpha in theory)
w : float
Between [0,1] weighting for agents in SProdOp
numOptions : int
Number of options to choose between
"""
self.idNum = idNum
self._belief = belief
self._distrust = distrust
self._w = w
self._numOptions = numOptions
self.reached_decision = False
def setBelief(self, newBelief):
"""
Sets agent's belief
N.B. Usage is only for removing the overhead during aggregation of
every agent calculating the same sum individually.
"""
if abs(np.sum(newBelief) - 1.0) < 0.0001:
self._belief = newBelief
else:
raise ValueError('Can not set belief to, ', newBelief)
def getBelief(self):
"""Returns the agent's belief"""
return self._belief
def updateOnEvidence(self, evidenceID):
"""
Updates belief value based on the new piece of evidence received
"""
self._belief = af.update_on_evidence(evidenceID,
self._distrust,
self._belief)
def checkNeighbourConsistent(self, neighbour):
"""
Check whether the agents hold completely opposing beliefs, i.e. they
both have maximal beliefs for different options. This can cause an
error in the aggregation operator. Returns False if this is the case,
True otherwise.
Parameters
----------
neighbour : instance of the agent class
other agent for comparison
"""
return af.check_neighbour_consistent(self._belief,
neighbour.getBelief())
def aggregate(self, neighbourhood):
"""
Checks which agents in the neighbourhood are consistent with the agent
and then performs aggregation using chosen pooling operator.
Parameter
--------
neighbourhood : list
list of agent class instances
"""
beliefs = [n.getBelief() for n in neighbourhood]
poolSize = len(neighbourhood)
if af.check_neighbourhood_consistent(self._numOptions,
poolSize,
beliefs):
# Change here for different operator
try:
retBelief = af.aggregate_MProdOp(self._numOptions,
poolSize,
beliefs)
return retBelief
except RuntimeError:
raise ZeroDivisionError('Denominator zero in beliefs', beliefs)
else:
# All neighbours inconsistent so no aggregation
return None
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include <vector>
#include <math.h>
#include "agentFunctions.hpp"
py::array_t<double> update_on_evidence(int evidence_id, double distrust,
py::array_t<double> belief)
{
if (belief.ndim() != 1)
{
throw std::invalid_argument("Expected 1-D array for belief");
}
/* read in the input array buffer info*/
py::buffer_info belief_buf = belief.request();
double *belief_ptr = (double *) belief_buf.ptr;
/* Find the agent's belief in the given piece of evidence */
double b_evidence = belief_ptr[evidence_id];
/* Evidential updating is undefined for inconsistent cases */
if ((distrust == 0) && (b_minSite == 1))
{
return belief;
}
else
{
/* allocate the output array and buffer*/
py::array_t<double> result = py::array_t<double>(belief_buf.size);
py::buffer_info results_buf = result.request();
double *result_ptr = (double *) results_buf.ptr;
/* Calculate the denominator */
double denominator = (1-distrust)*b_evidence + distrust*(1-b_evidence);
if (denominator == 0)
{
throw std::runtime_error("Division by zero condition");
}
for (int i = 0; i < belief_buf.size; ++i)
{
if (i == evidence_id)
{
result_ptr[i] = ((1-distrust) * belief_ptr[i]) / denominator;
}
else
{
result_ptr[i] = (distrust * belief_ptr[i]) / denominator;
}
}
return result;
}
}
py::array_t<double> aggregate_MProdOp(int num_options, int pool_size,
py::array_t<double> beliefs)
{
if (beliefs.ndim() != 2)
{
throw std::invalid_argument("Expected 2-D array for belief");
}
/* read in the input arrays buffer info*/
py::buffer_info beliefs_buf = beliefs.request();
double *beliefs_ptr = (double *) beliefs_buf.ptr;
if (beliefs_buf.shape[0]!=pool_size || beliefs_buf.shape[1]!=num_options)
{
throw std::invalid_argument("Beliefs array is incorrect shape");
}
/* allocate the output array and buffer */
py::array_t<double> result = py::array_t<double>(num_options);
py::buffer_info results_buf = result.request();
double *result_ptr = (double *) results_buf.ptr;
// Calculate the L1-norm denominator
double denominator = 0;
double xj_prod_arr[num_options];
for (int i = 0; i < num_options; ++i)
{
double xj_prod = 1;
for (int j = 0; j < pool_size; ++j)
{
xj_prod *= beliefs_ptr[j*num_options + i];
}
xj_prod_arr[i] = xj_prod;
denominator += xj_prod;
}
if (denominator == 0)
{
throw std::runtime_error("Division by zero condition");
}
//Calculate numerator by Hadamard product
for (int i = 0; i < num_options; ++i)
{
result_ptr[i] = xj_prod_arr[i] / denominator;
}
return result;
}
int check_neighbour_consistent(py::array_t<double> belief1,
py::array_t<double> belief2)
{
if (belief1.ndim() != 1 || belief2.ndim() != 1)
{
throw std::invalid_argument("Expected 1-D array for belief");
}
/* read in the input arrays buffer info*/
py::buffer_info belief1_buf = belief1.request();
double *belief1_ptr = (double *) belief1_buf.ptr;
py::buffer_info belief2_buf = belief2.request();
double *belief2_ptr = (double *) belief2_buf.ptr;
if (belief1_buf.size != belief2_buf.size)
{
throw std::invalid_argument("Input shapes must match");
}
int belief1_arg = -1;
for (int i = 0; i < belief1_buf.size; ++i)
{
if (belief1_ptr[i] == 1.0)
{
belief1_arg = i;
break;
}
}
int belief2_arg = -1;
for (int i = 0; i < belief2_buf.size; ++i)
{
if (belief2_ptr[i] == 1.0)
{
belief2_arg = i;
break;
}
}
if ((belief1_arg>=0) && (belief2_arg>=0) && (belief1_arg!=belief2_arg))
{
return 0;
}
else
{
return 1;
}
}
int check_neighbourhood_consistent(int num_options, int pool_size,
py::array_t<double> beliefs)
{
if (beliefs.ndim() != 2)
{
throw std::invalid_argument("Expected 2-D array for belief");
}
/* read in the input arrays buffer info*/
py::buffer_info beliefs_buf = beliefs.request();
double *beliefs_ptr = (double *) beliefs_buf.ptr;
if (beliefs_buf.shape[0]!=pool_size || beliefs_buf.shape[1]!=num_options)
{
throw std::invalid_argument("Beliefs array is incorrect shape");
}
for (int i = 0; i < num_options; ++i)
{
double xj_prod = 1;
for (int j = 0; j < pool_size; ++j)
{
xj_prod *= beliefs_ptr[j*num_options + i];
}
if (xj_prod == 0)
{
return 0;
}
}
return 1;
}
#ifndef AGENT_FUNCS
#define AGENT_FUNCS
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include <vector>
namespace py = pybind11;
/*! Updates a given belief negative bayesian updating
\param evidence_id : integer denoting option to update /p belief on
\param distrust : a double representing weighting for the bayesian updating
\param belief : a pybind11 numpy array to be updated
*/
py::array_t<double> update_on_evidence(int evidence_id, double distrust,
py::array_t<double> belief);
/*! Aggregates set of agent beliefs using the MProdOP
\param num_options : an integer signifying the number of options
\param pool_size : an integer signifying the number of agents aggregating
\param beliefs : a pybind11 numpy nested array containing the beliefs of the agents to be aggregated.
*/
py::array_t<double> aggregate_MProdOp(int num_options, int pool_size,
py::array_t<double> beliefs);
/*! Compares two agents to check that their beliefs are consistent. i.e. they do not believe with total certainty in two different options.
\param belief1 : a pybind11 numpy array containing agent belief
\param belief2 : another pybind11 numpy array containing agent belief
*/
int check_neighbour_consistent(py::array_t<double> belief1,
py::array_t<double> belief2);
/*! Checks a neighbourhood of beliefs to ensure that there is at least one
option with which all agents have some belief.
\param num_options : an integer signifying the number of options
\param pool_size : an integer signifying the size of the neighbourhood
\param beliefs : a pybind11 numpy array containing neighbourhood beliefs
*/
int check_neighbourhood_consistent(int num_options, int pool_size,
py::array_t<double> beliefs);
#endif
#include <pybind11/pybind11.h>
#include "agentFunctions.hpp"
#include "evidenceFunctions.hpp"
PYBIND11_MODULE(agentFunctions, m){
m.doc() = "Functions for improving agent performance";
m.def("update_on_evidence", &update_on_evidence,
"Updates agent's belief based on negative evidence");
m.def("aggregate_MProdOp", &aggregate_MProdOp,
"Aggregated agent beliefs using MProdOP");
m.def("check_neighbour_consistent", &check_neighbour_consistent,
"Check for consistent beliefs between neighbours");
m.def("check_neighbourhood_consistent", &check_neighbourhood_consistent,
"Checks neighbourhood only contains consistent beliefs");
}
PYBIND11_MODULE(evidenceFunctions, m){
m.doc() = "Functions for improving evidence performance";
m.def("pick_option", &pick_option, "Picks an option from a weighted distribution")
}
from numpy.random import choice
from operator import attrgetter
# C module written by CLL
from noComparisonSimulation import evidenceFunctions as ef
class Evidence(object):
"""
Evidence Class
Initialises and contains all the information about the options
of the trial. It controls evidence rate and interactions with the options.
Can be thought of as the environment and sensors combined.
Attributes
----------
options : list
contains all the option instances
"""
def __init__(self, numOptions, evidenceRate, optionWeights):
"""
Initialising method for the Evidence class
Parameters
----------
"""
self.numOptions = numOptions
self.evidenceRate = evidenceRate
self.optionWeights = optionWeights
def receiveEvidenceBool(self):
"""Returns a boolean for receiving evidence"""
return choice([True, False], 1, p=[self.evidenceRate,
1 - self.evidenceRate])[0]
def retWeightedRandomOption(self):
"""Returns an option at random from a weighted distribution"""
evidenceID = ef.pick_option(self.numOptions, self.optionWeights)
return evidenceID
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include <vector>
#include <math.h>
#include "evidenceFunction.hpp"
int pick_option(int num_options, py::array_t<double> weights){
if (weights.ndim() != 1)
{
throw std::invalid_argument("Expected 1-D array for weights");
}
py::buffer_info weights_buf = weights.request();
double *weights_ptr = (double *) weights_buf.ptr;
int totalWeight = 0; // this stores sum of weights of all elements before current
int selected = 0; // currently selected element
for (int i = 0; i < num_options; ++i)
{
// Have to turn weights into a integer and do this by raising by 10^5
// this should be a suitable level of accuracy.
int weight = 100000 * weights_ptr[i]; // weight of current element
if ((totalWeight + weight) != 0)
{
int r = rand() % (totalWeight + weight); // random value
if (r >= totalWeight)
{ // probability of this is weight/(totalWeight+weight)
selected = i; // it is the probability of discarding last selected element and selecting current one instead
}
totalWeight += weight; // increase weight sum
}
}
selected; // when iterations end, selected is some element of sequence.
}
return selected;
}
#ifndef EV_FUNCS
#define EV_FUNCS
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/numpy.h>
#include <vector>
namespace py = pybind11;
/*! Pick an option from a possible n using a weighted random distribution
\param num_options : an integer signifying the number of options to be chosen from
\param weights : a pybind11 numpy array with the weights for the options
*/
int pick_option(int num_option, py::array_t<double> weights);
# parameters.py
import numpy as np
def params(num_options, er, wr, k, pop_size, total_its, alpha):
"""
Returns parameters as a dictionary
Parameters
----------
num_options : int
number of options
eR : float
evidence rate - chance of agents receiving evidence each iteration
wR : float
weight for pooling operator
k : int
number of agents for pooling
popSize : int
population size
tIts : int
total number of iterations for which the simulation runs
alpha : float
distrust or alpha in the evidential updating operator. Agents' distrust
of the evidence source.
w : float
SProdOp parameter
"""
params = {'numOptions': int(num_options),
'totalIts': int(total_its),
'popSize': int(pop_size),
'distrust': float(alpha),
'k': int(k),
'eR': float(er),
'w': float(wr),
}
return params