Beispiel #1
0
def test_to_n_dimensional():
    N = state_by_node.shape[-1]
    S = state_by_node.shape[0]
    result = convert.to_n_dimensional(state_by_node)
    for i in range(S):
        state = convert.loli_index2state(i, N)
        assert np.array_equal(result[state], state_by_node[i])
Beispiel #2
0
def test_to_n_dimensional():
    N = state_by_node.shape[-1]
    S = state_by_node.shape[0]
    result = convert.to_n_dimensional(state_by_node)
    for i in range(S):
        state = convert.loli_index2state(i, N)
        assert np.array_equal(result[state], state_by_node[i])
Beispiel #3
0
def convert_holi_tpm_to_loli(holi_tpm):
    # Assumes state by node format
    states, nodes = holi_tpm.shape
    loli_tpm = np.zeros([states, nodes])
    for i in range(states):
        loli_state = loli_index2state(i, nodes)
        holi_tpm_row = state2holi_index(loli_state)
        loli_tpm[i, :] = holi_tpm[holi_tpm_row, :]

    return loli_tpm
Beispiel #4
0
    def loli_tpm(self):
        # TODO: Condition on background elements
        """Yield the State-by-node LOLI tpm for the full system. Not conditioned
           on background elements."""
        number_of_states = 2**len(self)
        number_of_nodes = len(self)
        tpm = np.zeros([number_of_states, number_of_nodes])
        for state_index in range(number_of_states):
            current_state = loli_index2state(state_index, number_of_nodes)
            tpm[state_index] = utils.predict_next_state(self, current_state)

        return tpm
Beispiel #5
0
def pretty_print_tpm(node_tokens, tpm):
    number_of_states, number_of_nodes = tpm.shape
    for state_index in range(number_of_states):
        current_state = loli_index2state(state_index, number_of_nodes)
        next_state = tpm[state_index, :]
        pretty_tokens = format_node_tokens_by_state(node_tokens,
                                                    current_state,
                                                    mode='back')
        pretty_tokens = format_node_tokens_by_state(pretty_tokens,
                                                    next_state,
                                                    mode='fore')
        print(':'.join(pretty_tokens))
Beispiel #6
0
def input_state_permutation(num_nodes, perm):
    """
    Permute input state order according to permutation of nodes given
    """
    # in the permuted tpm the inputs nodes are ordered as in the permutation i.e. 1 2 0 
    input_states = np.array([loli_index2state(s, num_nodes) for s in range(2**num_nodes)])

    # now I have to find the permutation that would convert perm back to 0 1 2
    perm_back = [i[0] for i in sorted(enumerate(perm), key=lambda x:x[1])]
    # now permute columns of input_states using perm_back
    # used transposed cause I haven't figured out how to index columns
    permuted_input_states = input_states.T[np.array(perm_back)].T

    permuted_state_indices = [state2loli_index(permuted_input_states[s]) for s in range(2**num_nodes)]

    return permuted_state_indices
Beispiel #7
0
def test_loli_index2state():
    assert convert.loli_index2state(7, 8) == (1, 1, 1, 0, 0, 0, 0, 0)
    assert convert.loli_index2state(1, 3) == (1, 0, 0)
    assert convert.loli_index2state(8, 4) == (0, 0, 0, 1)
Beispiel #8
0
def plot_3D_constellation(constellation,
                          label_axes=False,
                          label_stars=True,
                          color_code_axes=False,
                          state_fmt='A'):
    """Generate a 3D-plot of a constellation of concepts in cause-effect space.

    Examples:
        >>> big_mip = pyphi.compute.big_mip(sub)
        >>> plot_3D_constellation(big_mip.unpartitioned_constellation)

    Written by Billy Marshall, modified by Graham Findlay.
    Cause-effect space is a high dimensional space, one for each possible past
    and future state of the system (2 ** (n+1) dimensional for a system of
    binary elements). Each concept in the constellation is a point in
    cause-effect space. The size of the point is proportional to the small-phi
    value of the concept. The location on each axis represents the probability
    of the corresponding past / future state in the cause-effect repertoires of
    the concept. Only three dimensions are shown in the plot, the two future
    states and one past state with greatest variance in the repertoire values.

    Args:
        constellation (list(pyphi.models.Concept)): A list of concepts to plot.
    """
    if not constellation:
        return

    sub = constellation[0].subsystem
    n_nodes = sub.size
    n_states = 2**n_nodes
    n_concepts = len(constellation)
    node_labels, sep = fmt.parse_spec(constellation[0], state_fmt)

    # Get an array of cause-effect repertoires, expanded over the system
    cause_repertoires = np.zeros((n_concepts, n_states))
    effect_repertoires = np.zeros((n_concepts, n_states))
    for i, concept in enumerate(constellation):
        cause_repertoires[i] = concept.expand_cause_repertoire().flatten('F')
        effect_repertoires[i] = concept.expand_effect_repertoire().flatten('F')

    # Find the one cause state and two effect states with greatest variance
    cause_variance = np.var(cause_repertoires, 0)
    effect_variance = np.var(effect_repertoires, 0)
    cause_arg = cause_variance.argsort()[-1:]
    effect_arg = effect_variance.argsort()[-2:]

    # Set of points in cause-effect space and their size (phi)
    x = effect_repertoires[:, effect_arg[0]]
    y = effect_repertoires[:, effect_arg[1]]
    z = cause_repertoires[:, cause_arg[0]]
    size = [concept.phi for concept in constellation]

    # Initialize plot
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')

    # Turn off grid background
    ax.axis('off')

    # Draw axes anchor at the origin
    ax1 = (0, 0)
    ax3 = (0, 1)

    # Plan axis colors
    if color_code_axes:
        x_ax_color = y_ax_color = 'green'
        z_ax_color = 'blue'
    else:
        x_ax_color = y_ax_color = z_ax_color = 'black'

    # Draw axes
    ax.plot(ax1, ax1, ax3, z_ax_color, linewidth=2, zorder=0.3)
    ax.plot(ax1, ax3, ax1, y_ax_color, linewidth=2, zorder=0.3)
    ax.plot(ax3, ax1, ax1, x_ax_color, linewidth=2, zorder=0.3)

    # Label axes with states
    if label_axes:
        xs = (1, -0.1, -0.1)
        ys = (-0.1, 1, -0.1)
        zs = (-0.1, -0.1, 1)
        dirs = ('x', 'y', 'z')

        x_ax_state = loli_index2state(effect_arg[0], n_nodes)
        y_ax_state = loli_index2state(effect_arg[1], n_nodes)
        z_ax_state = loli_index2state(cause_arg[0], n_nodes)

        x_ax_label = r'${}^f$'.format(
            fmt.state(x_ax_state, node_labels=node_labels, sep=sep))
        y_ax_label = r'${}^f$'.format(
            fmt.state(y_ax_state, node_labels=node_labels, sep=sep))
        z_ax_label = r'${}^p$'.format(
            fmt.state(z_ax_state, node_labels=node_labels, sep=sep))

        ax_labels = [x_ax_label, y_ax_label, z_ax_label]
        for S, D, X, Y, Z in zip(ax_labels, dirs, xs, ys, zs):
            ax.text(X, Y, Z, S, D)

    # Add appropriately sized concepts
    for i in range(n_concepts):
        ax.scatter(x[i],
                   y[i],
                   z[i],
                   s=size[i] * 1000,
                   marker=u"*",
                   c=u'yellow',
                   zorder=0.1)

    # Set the ticks for the grid on the plot, no tick labels
    ax.set_xticks([0, 0.25, 0.5, 0.75, 1])
    ax.set_yticks([0, 0.25, 0.5, 0.75, 1])
    ax.set_zticks([0, 0.25, 0.5, 0.75, 1])
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.set_zticklabels([])

    # Set the size of the space
    ax.set_zlim(0, 1)
    ax.set_ylim(0, 1)
    ax.set_xlim(0, 1)

    # Default view in elevation and azimuth angle
    ax.view_init(elev=25, azim=0)

    # Make actual axes and ticks invisible - using homemade axes
    ax.w_xaxis.line.set_color((1, 1, 1, 0))
    ax.w_yaxis.line.set_color((1, 1, 1, 0))
    ax.w_zaxis.line.set_color((1, 1, 1, 0))
    ax.tick_params(colors=(1, 1, 1, 0))

    # Label stars with their mechanisms
    if label_stars:
        star_labels = []  # Holds matplotlib annotation objects
        for i in range(n_concepts):
            # Get 2d projection (think "screen coordinates") of the star, since
            # there's no direct way in matplotlib to annotate a 3D point.
            x2d, y2d, _ = proj3d.proj_transform(x[i], y[i], z[i],
                                                ax.get_proj())
            # Get the mechanism's label
            node_labels, _ = fmt.parse_spec(constellation[0], 'A,')
            mech_node_labels = [
                node_labels[x] for x in constellation[i].mechanism
            ]
            mech_label = ','.join(mech_node_labels)
            # plot and save the label
            label = ax.annotate(mech_label,
                                xy=(x2d, y2d),
                                xytext=(10, 10),
                                textcoords='offset points')
            star_labels.append(label)

        # This callback will update the label positions if the user changes
        #   the plot's perspective interactively.
        def update_star_labels(event):
            for i in range(n_concepts):
                x2d, y2d, _ = proj3d.proj_transform(x[i], y[i], z[i],
                                                    ax.get_proj())
                star_labels[i].xy = x2d, y2d
                star_labels[i].update_positions(fig.canvas.renderer)
            fig.canvas.draw()

        # Register the callback
        fig.canvas.mpl_connect('button_release_event', update_star_labels)

    # Show plot
    plt.show()
Beispiel #9
0
def propagation_delay_network():
    """A version of the primary example from the IIT 3.0 paper with
    deterministic COPY gates on each connection. These copy gates essentially
    function as propagation delays on the signal between OR, AND and XOR gates
    from the original system.

    The current and past states of the network are also selected to mimic the
    corresponding states from the IIT 3.0 paper.


    Diagram::

                                   +----------+
                +------------------+ C (COPY) +<----------------+
                v                  +----------+                 |
        +-------+-+                                           +-+-------+
        |         |                +----------+               |         |
        | A (OR)  +--------------->+ B (COPY) +-------------->+ D (XOR) |
        |         |                +----------+               |         |
        +-+-----+-+                                           +-+-----+-+
          |     ^                                               ^     |
          |     |                                               |     |
          |     |   +----------+                 +----------+   |     |
          |     +---+ H (COPY) +<----+     +---->+ F (COPY) +---+     |
          |         +----------+     |     |     +----------+         |
          |                          |     |                          |
          |                        +-+-----+-+                        |
          |         +----------+   |         |   +----------+         |
          +-------->+ I (COPY) +-->| G (AND) |<--+ E (COPY) +<--------+
                    +----------+   |         |   +----------+
                                   +---------+

    Connectivity matrix:

    +---+---+---+---+---+---+---+---+---+---+
    | . | A | B | C | D | E | F | G | H | I |
    +---+---+---+---+---+---+---+---+---+---+
    | A | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
    +---+---+---+---+---+---+---+---+---+---+
    | B | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | C | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | D | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | E | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | F | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | G | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | H | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | I | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+

    States:

    In the IIT 3.0 paper example, the past state of the system has only the XOR
    gate on. For the propagation delay network, this corresponds to a state of
    ``(0, 0, 0, 1, 0, 0, 0, 0, 0)``.

    The current state of the IIT 3.0 example has only the OR gate on. By
    advancing the propagation delay system two time steps, the current state
    ``(1, 0, 0, 0, 0, 0, 0, 0, 0)`` is achieved, with corresponding past state
    ``(0, 0, 1, 0, 1, 0, 0, 0, 0)``.
    """
    num_nodes = 9
    num_states = 2**num_nodes

    tpm = np.zeros((num_states, num_nodes))

    for past_state_index in range(num_states):
        past_state = loli_index2state(past_state_index, num_nodes)
        current_state = [0 for i in range(num_nodes)]
        if (past_state[2] == 1 or past_state[7] == 1):
            current_state[0] = 1
        if (past_state[0] == 1):
            current_state[1] = 1
            current_state[8] = 1
        if (past_state[3] == 1):
            current_state[2] = 1
            current_state[4] = 1
        if (past_state[1] == 1 ^ past_state[5] == 1):
            current_state[3] = 1
        if (past_state[4] == 1 and past_state[8] == 1):
            current_state[6] = 1
        if (past_state[6] == 1):
            current_state[5] = 1
            current_state[7] = 1
        tpm[past_state_index, :] = current_state

    cm = np.array([[0, 1, 0, 0, 0, 0, 0, 0, 1],
                   [0, 0, 0, 1, 0, 0, 0, 0, 0],
                   [1, 0, 0, 0, 0, 0, 0, 0, 0],
                   [0, 0, 1, 0, 1, 0, 0, 0, 0],
                   [0, 0, 0, 0, 0, 0, 1, 0, 0],
                   [0, 0, 0, 1, 0, 0, 0, 0, 0],
                   [0, 0, 0, 0, 0, 1, 0, 1, 0],
                   [1, 0, 0, 0, 0, 0, 0, 0, 0],
                   [0, 0, 0, 0, 0, 0, 1, 0, 0]])

    return Network(tpm, connectivity_matrix=cm)
Beispiel #10
0
def propagation_delay_network():
    """A version of the primary example from the IIT 3.0 paper with
    deterministic COPY gates on each connection. These copy gates essentially
    function as propagation delays on the signal between OR, AND and XOR gates
    from the original system.

    The current and past states of the network are also selected to mimic the
    corresponding states from the IIT 3.0 paper.


    Diagram::

                                   +----------+
                +------------------+ C (COPY) +<----------------+
                v                  +----------+                 |
        +-------+-+                                           +-+-------+
        |         |                +----------+               |         |
        | A (OR)  +--------------->+ B (COPY) +-------------->+ D (XOR) |
        |         |                +----------+               |         |
        +-+-----+-+                                           +-+-----+-+
          |     ^                                               ^     |
          |     |                                               |     |
          |     |   +----------+                 +----------+   |     |
          |     +---+ H (COPY) +<----+     +---->+ F (COPY) +---+     |
          |         +----------+     |     |     +----------+         |
          |                          |     |                          |
          |                        +-+-----+-+                        |
          |         +----------+   |         |   +----------+         |
          +-------->+ I (COPY) +-->| G (AND) |<--+ E (COPY) +<--------+
                    +----------+   |         |   +----------+
                                   +---------+

    Connectivity matrix:

    +---+---+---+---+---+---+---+---+---+---+
    | . | A | B | C | D | E | F | G | H | I |
    +---+---+---+---+---+---+---+---+---+---+
    | A | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
    +---+---+---+---+---+---+---+---+---+---+
    | B | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | C | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | D | 0 | 0 | 1 | 0 | 1 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | E | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | F | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | G | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | H | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+
    | I | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
    +---+---+---+---+---+---+---+---+---+---+

    States:

    In the IIT 3.0 paper example, the past state of the system has only the XOR
    gate on. For the propagation delay network, this corresponds to a state of
    ``(0, 0, 0, 1, 0, 0, 0, 0, 0)``.

    The current state of the IIT 3.0 example has only the OR gate on. By
    advancing the propagation delay system two time steps, the current state
    ``(1, 0, 0, 0, 0, 0, 0, 0, 0)`` is achieved, with corresponding past state
    ``(0, 0, 1, 0, 1, 0, 0, 0, 0)``.
    """
    num_nodes = 9
    num_states = 2**num_nodes

    tpm = np.zeros((num_states, num_nodes))

    for past_state_index in range(num_states):
        past_state = loli_index2state(past_state_index, num_nodes)
        current_state = [0 for i in range(num_nodes)]
        if (past_state[2] == 1 or past_state[7] == 1):
            current_state[0] = 1
        if (past_state[0] == 1):
            current_state[1] = 1
            current_state[8] = 1
        if (past_state[3] == 1):
            current_state[2] = 1
            current_state[4] = 1
        if (past_state[1] == 1 ^ past_state[5] == 1):
            current_state[3] = 1
        if (past_state[4] == 1 and past_state[8] == 1):
            current_state[6] = 1
        if (past_state[6] == 1):
            current_state[5] = 1
            current_state[7] = 1
        tpm[past_state_index, :] = current_state

    cm = np.array([[0, 1, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0, 0, 0],
                   [1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 1, 0, 0, 0, 0],
                   [0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0],
                   [0, 0, 0, 0, 0, 1, 0, 1, 0], [1, 0, 0, 0, 0, 0, 0, 0, 0],
                   [0, 0, 0, 0, 0, 0, 1, 0, 0]])

    return Network(tpm, connectivity_matrix=cm)
Beispiel #11
0
def test_loli_index2state():
    assert convert.loli_index2state(7, 8) == (1, 1, 1, 0, 0, 0, 0, 0)
    assert convert.loli_index2state(1, 3) == (1, 0, 0)
    assert convert.loli_index2state(8, 4) == (0, 0, 0, 1)
Beispiel #12
0
def plot_3D_constellation(constellation, label_axes=False, label_stars=True,
                          color_code_axes=False, state_fmt='A'):
    """Generate a 3D-plot of a constellation of concepts in cause-effect space.

    Examples:
        >>> big_mip = pyphi.compute.big_mip(sub)
        >>> plot_3D_constellation(big_mip.unpartitioned_constellation)

    Written by Billy Marshall, modified by Graham Findlay.
    Cause-effect space is a high dimensional space, one for each possible past
    and future state of the system (2 ** (n+1) dimensional for a system of
    binary elements). Each concept in the constellation is a point in
    cause-effect space. The size of the point is proportional to the small-phi
    value of the concept. The location on each axis represents the probability
    of the corresponding past / future state in the cause-effect repertoires of
    the concept. Only three dimensions are shown in the plot, the two future
    states and one past state with greatest variance in the repertoire values.

    Args:
        constellation (list(pyphi.models.Concept)): A list of concepts to plot.
    """
    if not constellation:
        return

    sub = constellation[0].subsystem
    n_nodes = sub.size
    n_states = 2 ** n_nodes
    n_concepts = len(constellation)
    node_labels, sep = fmt.parse_spec(constellation[0], state_fmt)

    # Get an array of cause-effect repertoires, expanded over the system
    cause_repertoires = np.zeros((n_concepts, n_states))
    effect_repertoires = np.zeros((n_concepts, n_states))
    for i, concept in enumerate(constellation):
        cause_repertoires[i] = concept.expand_cause_repertoire().flatten('F')
        effect_repertoires[i] = concept.expand_effect_repertoire().flatten('F')

    # Find the one cause state and two effect states with greatest variance
    cause_variance = np.var(cause_repertoires, 0)
    effect_variance = np.var(effect_repertoires, 0)
    cause_arg = cause_variance.argsort()[-1:]
    effect_arg = effect_variance.argsort()[-2:]

    # Set of points in cause-effect space and their size (phi)
    x = effect_repertoires[:, effect_arg[0]]
    y = effect_repertoires[:, effect_arg[1]]
    z = cause_repertoires[:, cause_arg[0]]
    size = [concept.phi for concept in constellation]

    # Initialize plot
    fig = plt.figure()
    ax = fig.add_subplot(111, projection='3d')

    # Turn off grid background
    ax.axis('off')

    # Draw axes anchor at the origin
    ax1 = (0, 0)
    ax3 = (0, 1)

    # Plan axis colors
    if color_code_axes:
        x_ax_color = y_ax_color = 'green'
        z_ax_color = 'blue'
    else:
        x_ax_color = y_ax_color = z_ax_color = 'black'

    # Draw axes
    ax.plot(ax1, ax1, ax3, z_ax_color, linewidth=2, zorder=0.3)
    ax.plot(ax1, ax3, ax1, y_ax_color, linewidth=2, zorder=0.3)
    ax.plot(ax3, ax1, ax1, x_ax_color, linewidth=2, zorder=0.3)

    # Label axes with states
    if label_axes:
        xs = (1, -0.1, -0.1)
        ys = (-0.1, 1, -0.1)
        zs = (-0.1, -0.1, 1)
        dirs = ('x', 'y', 'z')

        x_ax_state = loli_index2state(effect_arg[0], n_nodes)
        y_ax_state = loli_index2state(effect_arg[1], n_nodes)
        z_ax_state = loli_index2state(cause_arg[0], n_nodes)

        x_ax_label = r'${}^f$'.format(fmt.state(x_ax_state, node_labels=node_labels, sep=sep))
        y_ax_label = r'${}^f$'.format(fmt.state(y_ax_state, node_labels=node_labels, sep=sep))
        z_ax_label = r'${}^p$'.format(fmt.state(z_ax_state, node_labels=node_labels, sep=sep))

        ax_labels = [x_ax_label, y_ax_label, z_ax_label]
        for S, D, X, Y, Z in zip(ax_labels, dirs, xs, ys, zs):
            ax.text(X, Y, Z, S, D)

    # Add appropriately sized concepts
    for i in range(n_concepts):
        ax.scatter(x[i], y[i], z[i], s=size[i]*1000, marker=u"*", c=u'yellow', zorder=0.1)

    # Set the ticks for the grid on the plot, no tick labels
    ax.set_xticks([0, 0.25, 0.5, 0.75, 1])
    ax.set_yticks([0, 0.25, 0.5, 0.75, 1])
    ax.set_zticks([0, 0.25, 0.5, 0.75, 1])
    ax.set_xticklabels([])
    ax.set_yticklabels([])
    ax.set_zticklabels([])

    # Set the size of the space
    ax.set_zlim(0, 1)
    ax.set_ylim(0, 1)
    ax.set_xlim(0, 1)

    # Default view in elevation and azimuth angle
    ax.view_init(elev=25, azim=0)

    # Make actual axes and ticks invisible - using homemade axes
    ax.w_xaxis.line.set_color((1, 1, 1, 0))
    ax.w_yaxis.line.set_color((1, 1, 1, 0))
    ax.w_zaxis.line.set_color((1, 1, 1, 0))
    ax.tick_params(colors=(1, 1, 1, 0))

    # Label stars with their mechanisms
    if label_stars:
        star_labels = [] # Holds matplotlib annotation objects
        for i in range(n_concepts):
            # Get 2d projection (think "screen coordinates") of the star, since
            # there's no direct way in matplotlib to annotate a 3D point.
            x2d, y2d, _ = proj3d.proj_transform(x[i], y[i], z[i], ax.get_proj())
            # Get the mechanism's label
            node_labels, _ = fmt.parse_spec(constellation[0], 'A,')
            mech_node_labels = [node_labels[x] for x in constellation[i].mechanism]
            mech_label = ','.join(mech_node_labels)
            # plot and save the label
            label = ax.annotate(mech_label, xy=(x2d, y2d), xytext=(10, 10),
                                textcoords='offset points')
            star_labels.append(label)

        # This callback will update the label positions if the user changes
        #   the plot's perspective interactively.
        def update_star_labels(event):
            for i in range(n_concepts):
                x2d, y2d, _ = proj3d.proj_transform(x[i], y[i], z[i], ax.get_proj())
                star_labels[i].xy = x2d, y2d
                star_labels[i].update_positions(fig.canvas.renderer)
            fig.canvas.draw()

        # Register the callback
        fig.canvas.mpl_connect('button_release_event', update_star_labels)

    # Show plot
    plt.show()