From 25837c90bbdb3e0bf44472f961e1be8a12abdbcd Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Fri, 20 Feb 2015 15:36:29 -0600 Subject: [PATCH 01/19] Copyedit Mac OS X installation guide --- INSTALLATION.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/INSTALLATION.md b/INSTALLATION.md index d7954aee8..8631ca45b 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -41,10 +41,9 @@ another, so that they don't interact in unexpected ways. Please see [this guide](http://docs.python-guide.org/en/latest/dev/virtualenvs/) for more information. -To do this, you must install `virtualenv` and `virtualenvwrapper`, a [tool for -manipulating virtual -environments](http://virtualenvwrapper.readthedocs.org/en/latest/). Both of -those tools are available on [PyPI](https://pypi.python.org/pypi), the Python +To do this, you must install `virtualenvwrapper`, a [tool for manipulating +virtual environments](http://virtualenvwrapper.readthedocs.org/en/latest/). +This tool is available on [PyPI](https://pypi.python.org/pypi), the Python package index, and can be installed with `pip`, the command-line utility for installing and managing Python packages (`pip` was installed automatically with the brewed Python): @@ -73,8 +72,8 @@ development project directories, and the location of the script installed with this package, respectively. **Note:** The location of the script can be found by running `which virtualenvwrapper.sh`. -The filepath after the equals sign on second line will different for everyone, -but here is an example: +The filepath after the equals sign on the second line will different for +everyone, but here is an example: ```bash export WORKON_HOME=$HOME/.virtualenvs @@ -93,7 +92,7 @@ virtual environment, like so: mkvirtualenv -p `which python3` ``` -The `` -p `which python3 ``\` option ensures that when the virtual environment +The option `` -p `which python3 ``\` ensures that when the virtual environment is activated, the commands `python` and `pip` will refer to their Python 3 counterparts. @@ -124,6 +123,5 @@ import pyphi ``` Please see the documentation for some -[examples](http://pythonhosted.org/pyphi/#usage-and-examples) and information -on how to [configure](http://pythonhosted.org/pyphi/#configuration-optional) -it. +[examples](http://pythonhosted.org/pyphi/#examples) and information on how to +[configure](http://pythonhosted.org/pyphi/#configuration) it. From 31b3373d88fc8c97965c9692fa259f4357a41352 Mon Sep 17 00:00:00 2001 From: William Marshall Date: Fri, 20 Feb 2015 16:58:50 -0600 Subject: [PATCH 02/19] short circuit parallel computing --- pyphi/compute.py | 60 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index 8cd719add..8528aa15e 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -11,7 +11,7 @@ import functools from time import time import numpy as np -from joblib import Parallel, delayed +import multiprocessing from scipy.sparse.csgraph import connected_components from scipy.sparse import csr_matrix @@ -264,6 +264,15 @@ def _evaluate_partition(uncut_subsystem, partition, # Choose minimal unidirectional cut. return min(forward_mip, backward_mip) +# Wrapper for _evaluate_partition for parallel processing +def _eval_wrapper(in_queue, out_queue, subsystem, unpartitioned_constellation): + while True: + partition = in_queue.get() + if partition == None: + break + new_mip = _evaluate_partition(subsystem, partition, unpartitioned_constellation) + out_queue.put(new_mip) + out_queue.put(None) # TODO document big_mip @memory.cache(ignore=["subsystem"]) @@ -317,21 +326,50 @@ def time_annotated(big_mip, small_phi_time=0.0): unpartitioned_constellation = constellation(subsystem) small_phi_time = time() - small_phi_start log.debug("Found unpartitioned constellation.") - + min_mip = _null_mip(subsystem) + min_mip.phi = float('inf') if config.PARALLEL_CUT_EVALUATION: # Parallel loop over all partitions, using the specified number of # cores. - mip_candidates = Parallel(n_jobs=(config.NUMBER_OF_CORES), - verbose=config.PARALLEL_VERBOSITY)( - delayed(_evaluate_partition)(subsystem, partition, - unpartitioned_constellation) - for partition in bipartitions) - return time_annotated(min(mip_candidates), small_phi_time) + in_queue = multiprocessing.Queue() + out_queue = multiprocessing.Queue() + if config.NUMBER_OF_CORES < 0: + number_of_processes = multiprocessing.cpu_count()+config.NUMBER_OF_CORES+1 + elif config.NUMBER_OF_CORES <= multiprocessing.cpu_count(): + number_of_processes = config.NUMBER_OF_CORES + else: + raise ValueError( + 'Invalid number of cores, value may not be 0, and must be less' + 'than the number of cores ({} for this ' + 'system).'.format(multiprocessing.cpu_count())) + processes = [multiprocessing.Process(target = _eval_wrapper, + args = (in_queue, out_queue, subsystem, + unpartitioned_constellation)) + for i in range(number_of_processes)] + for partition in bipartitions: + in_queue.put(partition) + for i in range(number_of_processes): + in_queue.put(None) + for i in range(number_of_processes): + processes[i].start() + while True: + new_mip = out_queue.get() + if new_mip == None: + number_of_processes -= 1 + if number_of_processes == 0: + break + elif utils.phi_eq(new_mip.phi, 0): + min_mip = new_mip + for process in processes: + process.terminate() + break + else: + if (new_mip < min_mip): + min_mip = new_mip + result = time_annotated(min_mip, small_phi_time) else: # Sequentially loop over all partitions, holding only two BigMips in # memory at once. - min_mip = _null_mip(subsystem) - min_mip.phi = float('inf') for i, partition in enumerate(bipartitions): new_mip = _evaluate_partition( subsystem, partition, unpartitioned_constellation) @@ -342,7 +380,7 @@ def time_annotated(big_mip, small_phi_time=0.0): # Short-circuit as soon as we find a MIP with effectively 0 phi. if not min_mip: break - return time_annotated(min_mip, small_phi_time) + result = time_annotated(min_mip, small_phi_time) log.info("Finished calculating big-phi data for {}.".format(subsystem)) log.debug("RESULT: \n" + str(result)) From 96217305199ba580b62da99a35d71c3cce714bd3 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Sun, 22 Feb 2015 16:35:13 -0600 Subject: [PATCH 03/19] Use @property getters/setters in Network --- pyphi/network.py | 131 +++++++++++++++++++++++++++++++------------ pyphi/validate.py | 61 ++++++++++---------- test/test_network.py | 10 ++-- 3 files changed, 134 insertions(+), 68 deletions(-) diff --git a/pyphi/network.py b/pyphi/network.py index 0648591da..47f19edb0 100644 --- a/pyphi/network.py +++ b/pyphi/network.py @@ -8,7 +8,7 @@ """ import numpy as np -from . import validate, utils, json, convert +from . import validate, utils, json, convert, config # TODO!!! raise error if user tries to change TPM or CM, double-check and document @@ -68,8 +68,74 @@ class Network: The number of possible states of the network. """ + # TODO make tpm also optional when implementing logical network definition def __init__(self, tpm, current_state, past_state, connectivity_matrix=None, perturb_vector=None): + self.tpm = tpm + + self._size = self.tpm.shape[-1] + # TODO extend to nonbinary nodes + self._num_states = 2 ** self.size + self._node_indices = tuple(range(self.size)) + + self._current_state = tuple(current_state) + self._past_state = tuple(past_state) + self.perturb_vector = perturb_vector + self.connectivity_matrix = connectivity_matrix + + # Validate the entire network. + validate.network(self) + + @property + def size(self): + return self._size + + @property + def num_states(self): + return self._num_states + + @property + def node_indices(self): + return self._node_indices + + @property + def current_state(self): + return self._current_state + + @current_state.setter + def current_state(self, current_state): + # Cast current state to a tuple so it can be hashed and properly used + # as np.array indices. + current_state = tuple(current_state) + # Validate it. + validate.current_state_length(current_state, self.size) + if config.VALIDATE_NETWORK_STATE: + validate.state_reachable(self.past_state, current_state, self.tpm) + validate.state_reachable_from(self.past_state, current_state, + self.tpm) + self._current_state = current_state + + @property + def past_state(self): + return self._past_state + + @past_state.setter + def past_state(self, past_state): + # Cast past state to a tuple so it can be hashed and properly used + # as np.array indices. + past_state = tuple(past_state) + # Validate it. + validate.past_state_length(past_state, self.size) + if config.VALIDATE_NETWORK_STATE: + validate.state_reachable_from(past_state, self.current_state, self.tpm) + self._past_state = past_state + + @property + def tpm(self): + return self._tpm + + @tpm.setter + def tpm(self, tpm): # Cast TPM to np.array. tpm = np.array(tpm) # Validate TPM. @@ -79,53 +145,47 @@ def __init__(self, tpm, current_state, past_state, # Convert to N-D state-by-node if we were given a square state-by-state # TPM. Otherwise, force conversion to N-D format. if tpm.ndim == 2 and tpm.shape[0] == tpm.shape[1]: - tpm = convert.state_by_state2state_by_node(tpm) + self._tpm = convert.state_by_state2state_by_node(tpm) else: - tpm = convert.to_n_dimensional(tpm) + self._tpm = convert.to_n_dimensional(tpm) + # Make the underlying attribute immutable. + self._tpm.flags.writeable = False + # Update hash. + self._tpm_hash = utils.np_hash(self.tpm) - self.tpm = tpm - # Get the number of nodes in the network. - self.size = tpm.shape[-1] - self.node_indices = tuple(range(self.size)) + @property + def connectivity_matrix(self): + return self._connectivity_matrix + @connectivity_matrix.setter + def connectivity_matrix(self, cm): # Get the connectivity matrix. - if connectivity_matrix is not None: - connectivity_matrix = np.array(connectivity_matrix) + if cm is not None: + self._connectivity_matrix = np.array(cm) else: # If none was provided, assume all are connected. - connectivity_matrix = np.ones((self.size, self.size)) + self._connectivity_matrix = np.ones((self.size, self.size)) + # Make the underlying attribute immutable. + self._connectivity_matrix.flags.writeable = False + # Update hash. + self._cm_hash = utils.np_hash(self.connectivity_matrix) - # TODO make tpm also optional when implementing logical network - # definition + @property + def perturb_vector(self): + return self._perturb_vector + @perturb_vector.setter + def perturb_vector(self, perturb_vector): # Get pertubation vector. if perturb_vector is not None: - perturb_vector = np.array(perturb_vector) + self._perturb_vector = np.array(perturb_vector) else: # If none was provided, assume maximum-entropy. - perturb_vector = np.ones(self.size) / 2 - - self.perturb_vector = perturb_vector - self.connectivity_matrix = connectivity_matrix - # Coerce current and past state to tuples so they can be properly used - # as np.array indices. - self.current_state = tuple(current_state) - self.past_state = tuple(past_state) - # Make the TPM, pertubation vector and connectivity matrix immutable - # (for hashing). - self.tpm.flags.writeable = False - self.connectivity_matrix.flags.writeable = False - self.perturb_vector.flags.writeable = False - + self._perturb_vector = np.ones(self.size) / 2 + # Make the underlying attribute immutable. + self._perturb_vector.flags.writeable = False + # Update hash. self._pv_hash = utils.np_hash(self.perturb_vector) - self._tpm_hash = utils.np_hash(self.tpm) - self._cm_hash = utils.np_hash(self.connectivity_matrix) - - # TODO extend to nonbinary nodes - self.num_states = 2 ** self.size - - # Validate the entire network. - validate.network(self) def __repr__(self): return ("Network(" + ", ".join([repr(self.tpm), @@ -158,6 +218,7 @@ def __ne__(self, other): return not self.__eq__(other) def __hash__(self): + # TODO: hash only once? return hash((self._tpm_hash, self.current_state, self.past_state, self._cm_hash, self._pv_hash)) diff --git a/pyphi/validate.py b/pyphi/validate.py index ae5d106ad..525950cb2 100644 --- a/pyphi/validate.py +++ b/pyphi/validate.py @@ -85,51 +85,54 @@ def connectivity_matrix(cm): # TODO test -def _state_reachable(current_state, tpm): +def state_reachable(past_state, current_state, tpm): """Return whether a state can be reached according to the given TPM.""" # If there is a row `r` in the TPM such that all entries of `r - state` are # between -1 and 1, then the given state has a nonzero probability of being # reached from some state. test = tpm - np.array(current_state) - return np.any(np.logical_and(-1 < test, test < 1).all(-1)) + if not np.any(np.logical_and(-1 < test, test < 1).all(-1)): + raise StateUnreachableError( + current_state, past_state, tpm, + 'The current state cannot be reached from the past state ' + 'according to the given TPM.') # TODO test -def _state_reachable_from(past_state, current_state, tpm): +def state_reachable_from(past_state, current_state, tpm): """Return whether a state is reachable from the given past state.""" test = tpm[tuple(past_state)] - np.array(current_state) - return np.all(np.logical_and(-1 < test, test < 1)) + if not np.all(np.logical_and(-1 < test, test < 1)): + raise StateUnreachableError( + current_state, past_state, tpm, + "The current state is unreachable according to the given TPM.") + + +def _state_length(state, size, name): + if len(state) != size: + raise ValueError('Invalid {} state: there must be one entry per ' + 'node in the network; this state has {} entries, but ' + 'there are {} nodes.'.format(len(state), size, name)) + return True + + +def current_state_length(state, size): + return _state_length(state, size, 'current') + + +def past_state_length(state, size): + return _state_length(state, size, 'past') # TODO test def state(network): """Validate a network's current and past state.""" - current_state, past_state = network.current_state, network.past_state - tpm = network.tpm - # Check that the current and past states are the right size. - invalid_state = False - if len(current_state) != network.size: - invalid_state = ('current', len(network.current_state)) - if len(past_state) != network.size: - invalid_state = ('past', len(network.past_state)) - if invalid_state: - raise ValueError("Invalid {} state: there must be one entry per node " - "in the network; this state has {} entries, but " - "there are {} nodes.".format(invalid_state[0], - invalid_state[1], - network.size)) + current_state_length(network.current_state, network.size) + past_state_length(network.past_state, network.size) if config.VALIDATE_NETWORK_STATE: - # Check that the current state is reachable from some state. - if not _state_reachable(current_state, tpm): - raise StateUnreachableError( - current_state, past_state, tpm, - "The current state is unreachable according to the given TPM.") - # Check that the current state is reachable from the given past state. - if not _state_reachable_from(past_state, current_state, tpm): - raise StateUnreachableError( - current_state, past_state, tpm, - "The current state cannot be reached from the past state " - "according to the given TPM.") + state_reachable(network.past_state, network.current_state, network.tpm) + state_reachable_from(network.past_state, network.current_state, + network.tpm) return True diff --git a/test/test_network.py b/test/test_network.py index 2b581985f..35319baf4 100644 --- a/test/test_network.py +++ b/test/test_network.py @@ -30,11 +30,13 @@ def test_network_init_validation(network): state = (0, 1, 0) Network(tpm, state, past_state) with pytest.raises(ValueError): - state = (0, 1) - Network(network.tpm, state, network.past_state) + # Wrong current state length + current_state = (0, 1) + Network(network.tpm, current_state, network.past_state) with pytest.raises(ValueError): - state = (0, 1) - Network(network.tpm, network.current_state, state) + # Wrong past state length + past_state = (0, 1) + Network(network.tpm, network.current_state, past_state) def test_repr(standard): From 10acdf7dc16bb83444b50df4ae169fc9f609e831 Mon Sep 17 00:00:00 2001 From: William Marshall Date: Mon, 23 Feb 2015 09:43:21 -0600 Subject: [PATCH 04/19] prune away input and output nodes when computing main complex --- pyphi/compute.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index 8cd719add..7bbb04d11 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -326,7 +326,7 @@ def time_annotated(big_mip, small_phi_time=0.0): delayed(_evaluate_partition)(subsystem, partition, unpartitioned_constellation) for partition in bipartitions) - return time_annotated(min(mip_candidates), small_phi_time) + result = time_annotated(min(mip_candidates), small_phi_time) else: # Sequentially loop over all partitions, holding only two BigMips in # memory at once. @@ -342,7 +342,7 @@ def time_annotated(big_mip, small_phi_time=0.0): # Short-circuit as soon as we find a MIP with effectively 0 phi. if not min_mip: break - return time_annotated(min_mip, small_phi_time) + result = time_annotated(min_mip, small_phi_time) log.info("Finished calculating big-phi data for {}.".format(subsystem)) log.debug("RESULT: \n" + str(result)) @@ -385,12 +385,14 @@ def main_complex(network): log.debug("RESULT: \n" + str(result)) return result - def subsystems(network): """Return a generator of all possible subsystems of a network. - This is the just powerset of the network's set of nodes.""" - for subset in utils.powerset(range(network.size)): + This is the just powerset of the network's non-input and non-ouput + nodes.""" + for subset in utils.powerset(np.where(np.logical_and( + np.sum(network.connectivity_matrix, 0) > 0, + np.sum(network.connectivity_matrix, 1) > 0))[0]): yield Subsystem(subset, network) From 3228dfb666bc2b8a1865d929be689d66ecf72fbb Mon Sep 17 00:00:00 2001 From: William Marshall Date: Mon, 23 Feb 2015 10:18:57 -0600 Subject: [PATCH 05/19] clean up parallel short-circuit --- pyphi/compute.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index 8528aa15e..bdd672b26 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -331,8 +331,6 @@ def time_annotated(big_mip, small_phi_time=0.0): if config.PARALLEL_CUT_EVALUATION: # Parallel loop over all partitions, using the specified number of # cores. - in_queue = multiprocessing.Queue() - out_queue = multiprocessing.Queue() if config.NUMBER_OF_CORES < 0: number_of_processes = multiprocessing.cpu_count()+config.NUMBER_OF_CORES+1 elif config.NUMBER_OF_CORES <= multiprocessing.cpu_count(): @@ -342,16 +340,24 @@ def time_annotated(big_mip, small_phi_time=0.0): 'Invalid number of cores, value may not be 0, and must be less' 'than the number of cores ({} for this ' 'system).'.format(multiprocessing.cpu_count())) - processes = [multiprocessing.Process(target = _eval_wrapper, - args = (in_queue, out_queue, subsystem, - unpartitioned_constellation)) - for i in range(number_of_processes)] + # Define input and output queues to allow short-circuit if a cut + # if found with Phi = 0. Load the input queue with all possible + # cuts and a 'poison pill' for each process. + in_queue = multiprocessing.Queue() + out_queue = multiprocessing.Queue() for partition in bipartitions: in_queue.put(partition) for i in range(number_of_processes): in_queue.put(None) + # Initialize the processes and start them + processes = [multiprocessing.Process(target = _eval_wrapper, + args = (in_queue, out_queue, subsystem, + unpartitioned_constellation)) + for i in range(number_of_processes)] for i in range(number_of_processes): processes[i].start() + # Continue to process output queue until all processes have completed, + # or a 'poison pill' has been returned while True: new_mip = out_queue.get() if new_mip == None: From a1615d1f457323a9ffecac2f1c4a64a6fd746ef8 Mon Sep 17 00:00:00 2001 From: William Marshall Date: Mon, 23 Feb 2015 13:29:46 -0600 Subject: [PATCH 06/19] adjust test package for parallel short circuit --- pyphi/compute.py | 2 -- test/test_big_phi.py | 22 ++++++++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index c966e72b8..1fd1f31ea 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -25,7 +25,6 @@ # Create a logger for this module. log = logging.getLogger(__name__) - def concept(subsystem, mechanism): """Return the concept specified by the a mechanism within a subsytem. @@ -222,7 +221,6 @@ def _single_node_mip(subsystem): else: return _null_mip(subsystem) - def _evaluate_partition(uncut_subsystem, partition, unpartitioned_constellation): log.debug("Evaluating partition {}...".format(partition)) diff --git a/test/test_big_phi.py b/test/test_big_phi.py index 72480415d..465bb7ca2 100644 --- a/test/test_big_phi.py +++ b/test/test_big_phi.py @@ -159,6 +159,18 @@ 'cut': models.Cut(severed=(0, 2), intact=(1, 3)) } +micro_answer2 = { + 'phi': 0.97441, + 'unpartitioned_small_phis': { + (0,): 0.175, + (1,): 0.175, + (2,): 0.175, + (3,): 0.175, + (0, 1): 0.34811, + (2, 3): 0.34811, + }, + 'cut': models.Cut(severed=(1, 2), intact=(0, 3)) +} macro_answer = { 'phi': 0.86905, @@ -194,7 +206,7 @@ def check_partitioned_small_phis(answer, partitioned_constellation): PRECISION) -def check_mip(mip, answer): +def check_mip(mip, answer, answer2=None): # Check big phi value. np.testing.assert_almost_equal(mip.phi, answer['phi'], PRECISION) # Check small phis of unpartitioned constellation. @@ -205,7 +217,10 @@ def check_mip(mip, answer): check_partitioned_small_phis(answer, mip.partitioned_constellation) # Check cut. if 'cut' in answer: - assert mip.cut == answer['cut'] + if answer2 == None: + assert mip.cut == answer['cut'] + else: + assert mip.cut == answer['cut'] or answer2['cut'] # Tests @@ -430,7 +445,7 @@ def test_big_mip_micro_parallel(micro_s, flushcache, restore_fs_cache): config.PARALLEL_CUT_EVALUATION = True mip = compute.big_mip(micro_s) - check_mip(mip, micro_answer) + check_mip(mip, micro_answer, micro_answer2) config.PARALLEL_CUT_EVALUATION = initial @@ -443,7 +458,6 @@ def test_big_mip_micro_sequential(micro_s, flushcache, restore_fs_cache): mip = compute.big_mip(micro_s) check_mip(mip, micro_answer) - config.PARALLEL_CUT_EVALUATION = initial From 87b3381fa88c7895bb94d9c5b531fcfb982b9ad6 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 14:43:12 -0600 Subject: [PATCH 07/19] Clean up code style --- pyphi/compute.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index 1fd1f31ea..5d119a7db 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -25,6 +25,7 @@ # Create a logger for this module. log = logging.getLogger(__name__) + def concept(subsystem, mechanism): """Return the concept specified by the a mechanism within a subsytem. @@ -46,6 +47,7 @@ def concept(subsystem, mechanism): :mod:`pyphi.constants`. """ start = time() + def time_annotated(concept): concept.time = time() - start return concept @@ -198,7 +200,7 @@ def conceptual_information(subsystem): # TODO document def _null_mip(subsystem): - """Returns a BigMip with zero phi and empty constellations. + """Returns a BigMip with zero Phi and empty constellations. This is the MIP associated with a reducible subsystem.""" return BigMip(subsystem=subsystem, cut_subsystem=subsystem, @@ -221,6 +223,7 @@ def _single_node_mip(subsystem): else: return _null_mip(subsystem) + def _evaluate_partition(uncut_subsystem, partition, unpartitioned_constellation): log.debug("Evaluating partition {}...".format(partition)) @@ -262,13 +265,15 @@ def _evaluate_partition(uncut_subsystem, partition, # Choose minimal unidirectional cut. return min(forward_mip, backward_mip) -# Wrapper for _evaluate_partition for parallel processing + +# Wrapper for _evaluate_partition for parallel processing. def _eval_wrapper(in_queue, out_queue, subsystem, unpartitioned_constellation): while True: partition = in_queue.get() - if partition == None: + if partition is None: break - new_mip = _evaluate_partition(subsystem, partition, unpartitioned_constellation) + new_mip = _evaluate_partition(subsystem, partition, + unpartitioned_constellation) out_queue.put(new_mip) out_queue.put(None) From 1ecacd4448a2f61764771a4a149fff269a875f82 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 14:44:35 -0600 Subject: [PATCH 08/19] Refactor _big_mip Split the parallel/sequential mip-finding into separate functions. --- pyphi/compute.py | 131 ++++++++++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 57 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index 5d119a7db..20e026a2f 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -277,6 +277,75 @@ def _eval_wrapper(in_queue, out_queue, subsystem, unpartitioned_constellation): out_queue.put(new_mip) out_queue.put(None) + +def _find_mip_parallel(subsystem, bipartitions, unpartitioned_constellation, + min_mip): + """Parallel loop over all partitions, using the specified number of + cores.""" + if config.NUMBER_OF_CORES < 0: + number_of_processes = (multiprocessing.cpu_count() + + config.NUMBER_OF_CORES + 1) + elif config.NUMBER_OF_CORES <= multiprocessing.cpu_count(): + number_of_processes = config.NUMBER_OF_CORES + else: + raise ValueError( + 'Invalid number of cores; value may not be 0, and must be less' + 'than the number of cores ({} for this ' + 'system).'.format(multiprocessing.cpu_count())) + # Define input and output queues to allow short-circuit if a cut if found + # with zero Phi. Load the input queue with all possible cuts and a 'poison + # pill' for each process. + in_queue = multiprocessing.Queue() + out_queue = multiprocessing.Queue() + for partition in bipartitions: + in_queue.put(partition) + for i in range(number_of_processes): + in_queue.put(None) + # Initialize the processes and start them. + processes = [ + multiprocessing.Process(target=_eval_wrapper, + args=(in_queue, out_queue, subsystem, + unpartitioned_constellation)) + for i in range(number_of_processes) + ] + for i in range(number_of_processes): + processes[i].start() + # Continue to process output queue until all processes have completed, or a + # 'poison pill' has been returned. + while True: + new_mip = out_queue.get() + if new_mip is None: + number_of_processes -= 1 + if number_of_processes == 0: + break + elif utils.phi_eq(new_mip.phi, 0): + min_mip = new_mip + for process in processes: + process.terminate() + break + else: + if new_mip < min_mip: + min_mip = new_mip + return min_mip + + +def _find_mip_sequential(subsystem, bipartitions, unpartitioned_constellation, + min_mip): + """Sequentially loop over all partitions, holding only two BigMips in + memory at once.""" + for i, partition in enumerate(bipartitions): + new_mip = _evaluate_partition( + subsystem, partition, unpartitioned_constellation) + log.debug("Finished {} of {} partitions.".format( + i + 1, len(bipartitions))) + if new_mip < min_mip: + min_mip = new_mip + # Short-circuit as soon as we find a MIP with effectively 0 phi. + if not min_mip: + break + return min_mip + + # TODO document big_mip @memory.cache(ignore=["subsystem"]) def _big_mip(cache_key, subsystem): @@ -332,64 +401,12 @@ def time_annotated(big_mip, small_phi_time=0.0): min_mip = _null_mip(subsystem) min_mip.phi = float('inf') if config.PARALLEL_CUT_EVALUATION: - # Parallel loop over all partitions, using the specified number of - # cores. - if config.NUMBER_OF_CORES < 0: - number_of_processes = multiprocessing.cpu_count()+config.NUMBER_OF_CORES+1 - elif config.NUMBER_OF_CORES <= multiprocessing.cpu_count(): - number_of_processes = config.NUMBER_OF_CORES - else: - raise ValueError( - 'Invalid number of cores, value may not be 0, and must be less' - 'than the number of cores ({} for this ' - 'system).'.format(multiprocessing.cpu_count())) - # Define input and output queues to allow short-circuit if a cut - # if found with Phi = 0. Load the input queue with all possible - # cuts and a 'poison pill' for each process. - in_queue = multiprocessing.Queue() - out_queue = multiprocessing.Queue() - for partition in bipartitions: - in_queue.put(partition) - for i in range(number_of_processes): - in_queue.put(None) - # Initialize the processes and start them - processes = [multiprocessing.Process(target = _eval_wrapper, - args = (in_queue, out_queue, subsystem, - unpartitioned_constellation)) - for i in range(number_of_processes)] - for i in range(number_of_processes): - processes[i].start() - # Continue to process output queue until all processes have completed, - # or a 'poison pill' has been returned - while True: - new_mip = out_queue.get() - if new_mip == None: - number_of_processes -= 1 - if number_of_processes == 0: - break - elif utils.phi_eq(new_mip.phi, 0): - min_mip = new_mip - for process in processes: - process.terminate() - break - else: - if (new_mip < min_mip): - min_mip = new_mip - result = time_annotated(min_mip, small_phi_time) + min_mip = _find_mip_parallel(subsystem, bipartitions, + unpartitioned_constellation, min_mip) else: - # Sequentially loop over all partitions, holding only two BigMips in - # memory at once. - for i, partition in enumerate(bipartitions): - new_mip = _evaluate_partition( - subsystem, partition, unpartitioned_constellation) - log.debug("Finished {} of {} partitions.".format( - i + 1, len(bipartitions))) - if new_mip < min_mip: - min_mip = new_mip - # Short-circuit as soon as we find a MIP with effectively 0 phi. - if not min_mip: - break - result = time_annotated(min_mip, small_phi_time) + min_mip = _find_mip_sequential(subsystem, bipartitions, + unpartitioned_constellation, min_mip) + result = time_annotated(min_mip, small_phi_time) log.info("Finished calculating big-phi data for {}.".format(subsystem)) log.debug("RESULT: \n" + str(result)) From 203503c284070376da85e5ebe62c52ea473352d2 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 14:45:33 -0600 Subject: [PATCH 09/19] Make null-mip constellations tuples, not lists --- pyphi/compute.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index 20e026a2f..f62f38ef0 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -205,7 +205,7 @@ def _null_mip(subsystem): This is the MIP associated with a reducible subsystem.""" return BigMip(subsystem=subsystem, cut_subsystem=subsystem, phi=0.0, - unpartitioned_constellation=[], partitioned_constellation=[]) + unpartitioned_constellation=(), partitioned_constellation=()) def _single_node_mip(subsystem): From 0132228c94d2773acff082897d7fd3e02efefaaa Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 14:46:01 -0600 Subject: [PATCH 10/19] Make single-node-mip constellations tuples --- pyphi/compute.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index f62f38ef0..41c586f54 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -216,8 +216,8 @@ def _single_node_mip(subsystem): # TODO return the actual concept return BigMip( phi=0.5, - unpartitioned_constellation=None, - partitioned_constellation=None, + unpartitioned_constellation=(), + partitioned_constellation=(), subsystem=subsystem, cut_subsystem=subsystem) else: From 1b92a8924723ff949d3accf4991828bf53c79db7 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 14:51:32 -0600 Subject: [PATCH 11/19] Generalize check_mip to arbitrary number of valid cuts --- test/test_big_phi.py | 30 ++++++++++-------------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/test/test_big_phi.py b/test/test_big_phi.py index 465bb7ca2..ff3f09d03 100644 --- a/test/test_big_phi.py +++ b/test/test_big_phi.py @@ -156,20 +156,10 @@ (0, 1): 0.34811, (2, 3): 0.34811, }, - 'cut': models.Cut(severed=(0, 2), intact=(1, 3)) -} - -micro_answer2 = { - 'phi': 0.97441, - 'unpartitioned_small_phis': { - (0,): 0.175, - (1,): 0.175, - (2,): 0.175, - (3,): 0.175, - (0, 1): 0.34811, - (2, 3): 0.34811, - }, - 'cut': models.Cut(severed=(1, 2), intact=(0, 3)) + 'cuts': [ + models.Cut(severed=(0, 2), intact=(1, 3)), + models.Cut(severed=(1, 2), intact=(0, 3)), + ] } macro_answer = { @@ -206,7 +196,7 @@ def check_partitioned_small_phis(answer, partitioned_constellation): PRECISION) -def check_mip(mip, answer, answer2=None): +def check_mip(mip, answer): # Check big phi value. np.testing.assert_almost_equal(mip.phi, answer['phi'], PRECISION) # Check small phis of unpartitioned constellation. @@ -217,10 +207,9 @@ def check_mip(mip, answer, answer2=None): check_partitioned_small_phis(answer, mip.partitioned_constellation) # Check cut. if 'cut' in answer: - if answer2 == None: - assert mip.cut == answer['cut'] - else: - assert mip.cut == answer['cut'] or answer2['cut'] + assert mip.cut == answer['cut'] + elif 'cuts' in answer: + assert mip.cut in answer['cuts'] # Tests @@ -445,7 +434,7 @@ def test_big_mip_micro_parallel(micro_s, flushcache, restore_fs_cache): config.PARALLEL_CUT_EVALUATION = True mip = compute.big_mip(micro_s) - check_mip(mip, micro_answer, micro_answer2) + check_mip(mip, micro_answer) config.PARALLEL_CUT_EVALUATION = initial @@ -458,6 +447,7 @@ def test_big_mip_micro_sequential(micro_s, flushcache, restore_fs_cache): mip = compute.big_mip(micro_s) check_mip(mip, micro_answer) + config.PARALLEL_CUT_EVALUATION = initial From 464e2741d92350e40946cd01af8dbc5915c0d091 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 15:39:37 -0600 Subject: [PATCH 12/19] Use utils.phi_eq where possible --- pyphi/models.py | 6 +++--- pyphi/subsystem.py | 5 ++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/pyphi/models.py b/pyphi/models.py index a2d5872a2..efa97ac72 100644 --- a/pyphi/models.py +++ b/pyphi/models.py @@ -224,7 +224,7 @@ def __eq__(self, other): def __bool__(self): """A Mip is truthy if it is not reducible; i.e. if it has a significant amount of |small_phi|.""" - return self.phi > constants.EPSILON + return not utils.phi_eq(self.phi, 0) def __hash__(self): return hash((self.phi, self.direction, self.mechanism, self.purview, @@ -414,7 +414,7 @@ def __str__(self): def __bool__(self): """A Concept is truthy if it is not reducible; i.e. if it has a significant amount of |big_phi|.""" - return self.phi > constants.EPSILON + return not utils.phi_eq(self.phi, 0) def eq_repertoires(self, other): """Return whether this concept has the same cause and effect @@ -545,7 +545,7 @@ def __eq__(self, other): def __bool__(self): """A BigMip is truthy if it is not reducible; i.e. if it has a significant amount of |big_phi|.""" - return self.phi >= constants.EPSILON + return not utils.phi_eq(self.phi, 0) def __hash__(self): return hash((self.phi, self.unpartitioned_constellation, diff --git a/pyphi/subsystem.py b/pyphi/subsystem.py index cfa112324..850f31e4d 100644 --- a/pyphi/subsystem.py +++ b/pyphi/subsystem.py @@ -575,7 +575,7 @@ def find_mip(self, direction, mechanism, purview): partitioned_repertoire) # Return immediately if mechanism is reducible. - if phi < constants.EPSILON: + if utils.phi_eq(phi, 0): return Mip(direction=direction, mechanism=mechanism, purview=purview, @@ -590,7 +590,7 @@ def find_mip(self, direction, mechanism, purview): len(purview) > len(mip.purview))): phi_min = phi # TODO Use properties here to infer mechanism and purview from - # partition yet access them with .mechanism and .partition + # partition yet access them with .mechanism and .purview mip = Mip(direction=direction, mechanism=mechanism, purview=purview, @@ -598,7 +598,6 @@ def find_mip(self, direction, mechanism, purview): unpartitioned_repertoire=unpartitioned_repertoire, partitioned_repertoire=partitioned_repertoire, phi=phi) - return mip # TODO Don't use these internally From 54e039ea8f7a498282ed3099d56a913e45a58c32 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 15:40:24 -0600 Subject: [PATCH 13/19] Use <= EPSILON in utils.phi_eq, not < --- pyphi/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyphi/utils.py b/pyphi/utils.py index 167c47421..2144756d5 100644 --- a/pyphi/utils.py +++ b/pyphi/utils.py @@ -98,7 +98,7 @@ def np_hash(a): def phi_eq(x, y): """Compare two phi values up to |PRECISION|.""" - return abs(x - y) < constants.EPSILON + return abs(x - y) <= constants.EPSILON # see http://stackoverflow.com/questions/16003217 From 9a52fc57f0af16dfac7a60ae4c5fed688f0e9337 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 16:12:48 -0600 Subject: [PATCH 14/19] Remove newlines below ..note:: --- docs/examples/basic.rst | 2 -- docs/examples/conventions.rst | 3 --- pyphi/examples.py | 1 - 3 files changed, 6 deletions(-) diff --git a/docs/examples/basic.rst b/docs/examples/basic.rst index 3f58a79ef..3b3acb829 100644 --- a/docs/examples/basic.rst +++ b/docs/examples/basic.rst @@ -67,8 +67,6 @@ The documentation for :mod:`pyphi.models` contains description of these structures. .. note:: - The network and subsystem discussed here are returned by the :func:`pyphi.examples.basic_network` and :func:`pyphi.examples.basic_subsystem` functions. - diff --git a/docs/examples/conventions.rst b/docs/examples/conventions.rst index 5b2646359..78e95afec 100644 --- a/docs/examples/conventions.rst +++ b/docs/examples/conventions.rst @@ -57,20 +57,17 @@ Low Index nodes. The other convention, where the highest-index node varies the fastest, is similarly called **HOLI**. .. note:: - The rationale for this choice of convention is that the **LOLI** mapping is stable under changes in the number of nodes, in the sense that the same bit always corresponds to the same node index. The **HOLI** mapping does not have this property. .. note:: - This applies to only situations where decimal indices are encoding states. Whenever a network state is represented as a list or tuple, we use the only sensible convention: the |ith| element gives the state of the |ith| node. .. note:: - There are various conversion functions available for converting between TPMs, states, and indices using different conventions: see the :mod:`pyphi.convert` module. diff --git a/pyphi/examples.py b/pyphi/examples.py index 91a2fca8f..c874ae6e0 100644 --- a/pyphi/examples.py +++ b/pyphi/examples.py @@ -64,7 +64,6 @@ def basic_network(): +---+---+---+---+ .. note:: - |CM[i][j] = 1| means that node |i| is connected to node |j|. """ tpm = np.array([ From df5f86bcd0f8d400ed1ab2052c456095e0637613 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 18:41:57 -0600 Subject: [PATCH 15/19] Remove network equality check in Concept.eq_repertoires --- pyphi/models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/pyphi/models.py b/pyphi/models.py index efa97ac72..9ceef018e 100644 --- a/pyphi/models.py +++ b/pyphi/models.py @@ -418,10 +418,13 @@ def __bool__(self): def eq_repertoires(self, other): """Return whether this concept has the same cause and effect - repertoires as another.""" - if self.subsystem.network != other.subsystem.network: - raise Exception("Can't compare repertoires of concepts from " - "different networks.") + repertoires as another. + + .. warning:: + This only checks if the cause and effect repertoires are equal as + arrays; mechanisms, purviews, or even the nodes that node indices + refer to, might be different. + """ return ( np.array_equal(self.cause.repertoire, other.cause.repertoire) and np.array_equal(self.effect.repertoire, other.effect.repertoire)) From c6cd8a2e5ba4b4cc8c4a2c4f9be09fb7884cd331 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 18:47:21 -0600 Subject: [PATCH 16/19] Add possible_main_complexes - Leave compute.subsystems as the generator for the full powerset. - Refactor the logic in finding possible main complexes to be more readable. - Add some documentation. - Rearrange methods. --- pyphi/compute.py | 51 ++++++++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index 41c586f54..19cefd4c7 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -437,26 +437,25 @@ def big_phi(subsystem): return big_mip(subsystem).phi -def main_complex(network): - """Return the main complex of the network.""" - if not isinstance(network, Network): - raise ValueError( - """Input must be a Network (perhaps you passed a Subsystem - instead?)""") - log.info("Calculating main complex for " + str(network) + "...") - result = max(complexes(network)) - log.info("Finished calculating main complex for" + str(network) + ".") - log.debug("RESULT: \n" + str(result)) - return result +def possible_main_complexes(network): + """"Return a generator of the subsystems of a network that could be a main + complex. + + This is the just powerset of the nodes that have at least one input and + output (nodes with no inputs or no outputs cannot be part of a main + complex, because they do not have a causal link with the rest of the + subsystem in the past or future, respectively).""" + inputs = np.sum(network.connectivity_matrix, 1) + outputs = np.sum(network.connectivity_matrix, 0) + nodes_have_inputs_and_outputs = np.logical_and(inputs > 0, outputs > 0) + causally_significant_nodes = np.where(nodes_have_inputs_and_outputs)[0] + for subset in utils.powerset(causally_significant_nodes): + yield Subsystem(subset, network) -def subsystems(network): - """Return a generator of all possible subsystems of a network. - This is the just powerset of the network's non-input and non-ouput - nodes.""" - for subset in utils.powerset(np.where(np.logical_and( - np.sum(network.connectivity_matrix, 0) > 0, - np.sum(network.connectivity_matrix, 1) > 0))[0]): +def subsystems(network): + """Return a generator of all possible subsystems of a network.""" + for subset in utils.powerset(network.node_indices): yield Subsystem(subset, network) @@ -469,4 +468,18 @@ def complexes(network): raise ValueError( """Input must be a Network (perhaps you passed a Subsystem instead?)""") - return (big_mip(subsystem) for subsystem in subsystems(network)) + return (big_mip(subsystem) for subsystem in + possible_main_complexes(network)) + + +def main_complex(network): + """Return the main complex of the network.""" + if not isinstance(network, Network): + raise ValueError( + """Input must be a Network (perhaps you passed a Subsystem + instead?)""") + log.info("Calculating main complex for " + str(network) + "...") + result = max(complexes(network)) + log.info("Finished calculating main complex for" + str(network) + ".") + log.debug("RESULT: \n" + str(result)) + return result From 65ccb4520320516757161ff450132759a50628b4 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 18:52:43 -0600 Subject: [PATCH 17/19] Return only irreducible complexes - Add an all_complexes function for the BigMips of every powerset. --- pyphi/compute.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pyphi/compute.py b/pyphi/compute.py index 19cefd4c7..987ddb47e 100644 --- a/pyphi/compute.py +++ b/pyphi/compute.py @@ -460,16 +460,24 @@ def subsystems(network): def complexes(network): - """Return a generator for all complexes of the network. + """Return a generator for all irreducible complexes of the network.""" + if not isinstance(network, Network): + raise ValueError( + """Input must be a Network (perhaps you passed a Subsystem + instead?)""") + return tuple(filter(None, (big_mip(subsystem) for subsystem in + possible_main_complexes(network)))) + - This includes reducible, zero-phi complexes (which are not, strictly - speaking, complexes at all).""" +def all_complexes(network): + """Return a generator for all complexes of the network, including + reducible, zero-phi complexes (which are not, strictly speaking, complexes + at all).""" if not isinstance(network, Network): raise ValueError( """Input must be a Network (perhaps you passed a Subsystem instead?)""") - return (big_mip(subsystem) for subsystem in - possible_main_complexes(network)) + return (big_mip(subsystem) for subsystem in subsystems(network)) def main_complex(network): From 76bce09425620a4b7f509d5e4373ef64fc3de0f4 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 18:55:59 -0600 Subject: [PATCH 18/19] Update complex testing --- test/test_big_phi.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/test_big_phi.py b/test/test_big_phi.py index ff3f09d03..936381e5b 100644 --- a/test/test_big_phi.py +++ b/test/test_big_phi.py @@ -324,10 +324,16 @@ def test_big_mip_noised_example_parallel(s_noised, flushcache, config.PARALLEL_CUT_EVALUATION, config.NUMBER_OF_CORES = initial -# TODO!! add more assertions for the smaller subsystems def test_complexes_standard(standard, flushcache, restore_fs_cache): flushcache() complexes = list(compute.complexes(standard)) + check_mip(complexes[2], standard_answer) + + +# TODO!! add more assertions for the smaller subsystems +def test_all_complexes_standard(standard, flushcache, restore_fs_cache): + flushcache() + complexes = list(compute.all_complexes(standard)) check_mip(complexes[7], standard_answer) From 922f62a6b98195dac61f58ef9531e1a8b792b737 Mon Sep 17 00:00:00 2001 From: Will Mayner Date: Mon, 23 Feb 2015 18:57:47 -0600 Subject: [PATCH 19/19] Bump version --- pyphi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyphi/__init__.py b/pyphi/__init__.py index 7eacc3077..d002f861c 100644 --- a/pyphi/__init__.py +++ b/pyphi/__init__.py @@ -57,7 +57,7 @@ """ __title__ = 'pyphi' -__version__ = '0.3.8' +__version__ = '0.4.0' __description__ = 'Python library for computing integrated information.', __author__ = 'Will Mayner' __author_email__ = 'wmayner@gmail.com'