nd_phi_id/measures.py
+31
Lines changed: 31 additions & 0 deletions
Original file line number	Diff line number	Diff line change
@@ -0,0 +1,31 @@
import scipy
import numpy as np
from itertools import product
from collections import Counter
def local_entropy_mvn(x, mu, cov):
    return -np.log(scipy.stats.multivariate_normal.pdf(x, mu, cov, allow_singular=True))
def local_entropy_discrete(x):
    
    if x.ndim == 1:
        x = x[None, :]
    n_dim, n_samp = x.shape
    combs = list(product([n for n in range(np.max(x + 1))], repeat=n_dim))
    distri = list(zip(*x.tolist()))
    c = Counter(distri)
    p = np.array([c.get(comb, 0) for comb in combs]) / n_samp
    entropy_dict = {comb: -np.log2(p_) for comb, p_ in zip(combs, p)}
    return np.array([entropy_dict[comb] for comb in distri])
def double_redundancy_mmi(mutual_information, mi_means, atom):
    atom_mis = mi_means[atom[0],:] [:,atom[1]]
    (i1,i2) = np.unravel_index(np.argmin(atom_mis), shape=(len(atom[0]),len(atom[1])))
    return mutual_information[atom[0][i1],atom[1][i2],:]
def double_redundancy_ccs():
    return
‎nd_phi_id/phi_id_calculate.py
+118
Lines changed: 118 additions & 0 deletions
Original file line number	Diff line number	Diff line change
@@ -0,0 +1,118 @@
import numpy as np
import itertools
import time
from measures import double_redundancy_mmi, local_entropy_discrete, local_entropy_mvn
from phi_id_lattice import generate_phi_id
def calculate_entropy(data, sources, targets, kind="gaussian"):
    
    if kind == "gaussian":
        data_mu = np.mean(data, axis=1) 
        data_cov = np.cov(data)
        def _h(idx):
            return local_entropy_mvn(data[idx].T, data_mu[idx], data_cov[np.ix_(idx, idx)]) if idx else 0.0
    else:
        def _h(idx):
            return local_entropy_discrete(data[idx, :]) if idx else 0.0
    h = np.empty((len(sources), len(targets), 249))
    for i in range(len(sources)):
        for j in range(len(targets)):
            h[i,j,:] = _h(sources[i]+targets[j])
    return h
def calculate_mutual_information(h):
    return (-h + h[0,:,:]) + h[:,[0],:]
def calculate_double_redundancy(mutual_information, atoms, double_redundancy="MMI"):
    if double_redundancy == "MMI":
        double_redundancy_function = double_redundancy_mmi
    elif double_redundancy == "CCS":
        print("Only MMI Double Redundancy Function has been implemented")
        #TODO: Implement generalised CCS double redundancy
        #double_redundancy_function = double_redundancy_ccs
    else:
        print("Only MMI Double Redundancy Function has been implemented")
    mi_means = np.mean(mutual_information, axis=2)
    return np.array([double_redundancy_function(mutual_information, mi_means, atom) for atom in atoms])
def calculate_information_atoms(double_redundancy, matrix):
    return np.linalg.solve(matrix, np.mean(double_redundancy, axis=1))
def phi_id(src, trg, lattice=None, kind="gaussian", double_redundancy="MMI", max_source_atom=None, max_target_atom=None):
    
    if lattice is None:  
        lattice = generate_phi_id(dim=src.shape[0], max_source_atom=max_source_atom, max_target_atom=max_target_atom)
    data = np.concatenate([src, trg])
    if kind == "gaussian":
        data = data / np.std(data, axis=1, ddof=1, keepdims=True) 
    elif kind == "discrete":
        #TODO: Implement discretisation for n values
        print("Discretisation not yet implemented - Discrete data will fail if not drawn from (0,1,...,(dim-1))")
    entropy = calculate_entropy(data, lattice["sources"], lattice["targets"], kind)
    mutual_information = calculate_mutual_information(entropy)
    double_redundancy_value  = calculate_double_redundancy(mutual_information, lattice["atoms"], double_redundancy)
    phi_id_atom_values = calculate_information_atoms(double_redundancy_value, lattice["matrix"])       
    return phi_id_atom_values
def temporal_phi_id(base_data, tau=1, lattice=None, kind="gaussian", double_redundancy="MMI", max_source_atom=None, max_target_atom=None):
    src = base_data[:,:-tau]
    trg = base_data[:,tau:]
    
    return phi_id(src, trg, lattice=lattice, kind=kind, double_redundancy=double_redundancy, max_source_atom=max_source_atom, max_target_atom=max_target_atom)
def phi_id_combinations(base_data, dim=2, tau=1, kind="gaussian", double_redundancy="MMI"):
    
    lattice = generate_phi_id(dim=dim)
    rtr = np.zeros((base_data.shape[0],)*dim)
    sts = np.zeros((base_data.shape[0],)*dim)
    tdmi = np.zeros((base_data.shape[0],)*dim)
    
    t = time.process_time()
    prev_index = (0,)*dim
    for index in itertools.combinations_with_replacement(range(base_data.shape[0]), dim):
        phi_id_atom_values = temporal_phi_id(base_data[index,:], tau, lattice, kind, double_redundancy)
        #TODO: Save other atoms:
        # All the info to construct full matrices is computed: 
        # e.g. rtr[i,j] = rtr[j,i]
        # But some requre permutation e.g. u1tu2[i,j] = u2tu1[j,i] 
        # Still need to figure out a general form of this for non-symmetrical n-dimensional atoms
        for perm in itertools.permutations(index):
            rtr[perm] = np.mean(phi_id_atom_values[0])
            sts[perm] = np.mean(phi_id_atom_values[-1])
            tdmi[perm] = np.sum(np.mean(phi_id_atom_values, axis=0))
        if (dim > 2 and index[-3] > prev_index[-3]):
            np.savetxt("results/rtr/rtr_{}".format(prev_index[:-2]), rtr[prev_index[:-2]], delimiter=',')
            np.savetxt("results/sts/sts_{}".format(prev_index[:-2]), sts[prev_index[:-2]], delimiter=',')
            np.savetxt("results/tdmi/tdmi_{}".format(prev_index[:-2]), tdmi[prev_index[:-2]], delimiter=',')
            
            print(prev_index)
            print(time.process_time() - t)
            t = time.process_time()
            prev_index = index
    if dim == 2:
        np.savetxt("results/rtr/rtr", rtr, delimiter=',')
        np.savetxt("results/sts/sts", sts, delimiter=',')
        np.savetxt("results/tdmi/tdmi", tdmi, delimiter=',')
    return rtr, sts, tdmi
#data = np.genfromtxt("test_data.csv", delimiter=",")
#phi_id_combinations(data, dim=3)
‎nd_phi_id/phi_id_lattice.py
+38
Lines changed: 38 additions & 0 deletions
Original file line number	Diff line number	Diff line change
@@ -0,0 +1,38 @@
import numpy as np
import itertools
from pid_lattice import generate_pid
def generate_phi_id(dim=2, max_source_atom=None, max_target_atom=None):
    # Generate source and target PID lattices
    if max_source_atom is None and max_target_atom is None:
        sources, source_atoms, source_matrix = generate_pid(dim)
        targets = [[id+dim for id in source] for source in sources]
        target_atoms = source_atoms
        target_matrix = source_matrix
    else:
        #TODO: Modify generate_pid to generate both rather than calling twice    
        sources, source_atoms, source_matrix = generate_pid(dim, max_atom=max_source_atom)
        targets, target_atoms, target_matrix = generate_pid(dim, max_atom=max_target_atom)
        targets = [[id+dim for id in source] for source in sources]
    # Construct Phi-ID atoms as product of source and target atoms
    phi_id_atoms = list(itertools.product(source_atoms, target_atoms))
    # Construct Phi-ID partial order matrix from source and target PID matrices
    phi_id_matrix = np.zeros((len(phi_id_atoms), len(phi_id_atoms)))
    for i in range(len(phi_id_atoms)):
        for j in range(i+1):
            phi_id_matrix[i][j] = source_matrix[i//len(target_atoms)][j//len(target_atoms)] and target_matrix[i%len(target_atoms)][j%len(target_atoms)]
    return {
        "matrix": phi_id_matrix,
        "atoms": phi_id_atoms,
        "sources": sources,
        "targets": targets
    }
#generate_phi_id(dim=3)
#generate_phi_id(dim=4, max_source_atom=[{0,1},{2},{3}], max_target_atom=[{0,1,2},{3}])
‎nd_phi_id/pid_lattice.py
+60
Lines changed: 60 additions & 0 deletions
Original file line number	Diff line number	Diff line change
@@ -0,0 +1,60 @@
import numpy as np
from itertools import combinations, chain, product
import llist
#TODO: Precompute and save rather than calling this
def generate_pid(dim=3, max_atom=None):
    
    # Get all composite sources
    s = set(range(dim))
    sources = chain.from_iterable(combinations(s, r) for r in range(dim+1))
    # Remove sources which are a superset of any source in max_atom 
    if max_atom is not None:
        sources = filter(lambda source: not any(max_atom_source.issubset(source) and not max_atom_source == set(source) for max_atom_source in max_atom), sources)
    sources = list(list(source) for source in sources)
    # Compute PID ordering on sources
    contains = [[set(b).issubset(a) for a in sources] for b in sources] 
    # Initialise atoms with single sources
    atoms = llist.dllist([[i] for i in range(len(sources))])
    # Build all PID sources
    max_atom_indices = []
    atom_1 = atoms.first
    while not atom_1.value == atoms[-1]:
        atom_1 = atom_1.next
        atom_2 = atoms.first
        while not atom_2.value == atom_1.value:
            atom_2 = atom_2.next
            if not any(contains[s2][atom_1.value[0]] for s2 in atom_2.value):
                joint_atom = llist.dllistnode(atom_2.value + atom_1.value) 
                atoms.insertnodebefore(joint_atom, atom_2)
        
        if max_atom is not None:
            if set(sources[atom_1.value[0]]) in max_atom:
                max_atom_indices += atom_1.value
    # Filter all remaining atoms not less than max_atom
    if max_atom is not None:
        atoms = list(filter(lambda atom: all(any(contains[source][max_atom_source] for source in atom) for max_atom_source in max_atom_indices), atoms))[1:]
    else:
        atoms = list(atoms)[1:]
    # Compute PID partial order for individual sources
    source_contains = np.fromfunction(np.vectorize(lambda i, j: any(contains[atom_source][i] for atom_source in atoms[j])), shape=(len(sources), len(atoms)), dtype=int)
    # Construct complete PID partial order (A1 < A2 iff A1 < S for all S in A2) 
    partial_order_matrix = np.array([np.logical_and.reduce(source_contains[atom,:]) for atom in atoms]).astype(float)
    return sources, atoms, partial_order_matrix
#generate_pid(dim=2)
#generate_pid(dim=3)
#generate_pid(dim=4)
#generate_pid(dim=5)
#generate_pid(dim=6, max_atom=[{0,1,2},{3},{4,5}])
