Skip to content

Figures for the LC pathways

The figures of the pathway question (lc_pathways), for the right-hemisphere cells of the types of interest. A partner is a strong partner (strong_set of lc_output_strength), and the figures take their partner tables from lc_pathways.

census = load_census()
sides = load_visual_types().select("root_id", "side")
proofread_ids = load_proofread_ids()
cells = cells_of_types(census, sides, TYPES_OF_INTEREST)
_scored = [_id for _ids in cells.values() for _id in _ids]
connections = with_pre_type(load_connections_of(_scored), cells)
input_connections = with_pre_type(
    reversed_connections(load_connections_of(_scored, end="post")), cells
)
strong_out = strong_set(
    labeled_partners(connections, cells, census, proofread_ids)
)
strong_in = strong_set(
    labeled_partners(input_connections, cells, census, proofread_ids)
)

Partner overlap

Four heatmaps of the Jaccard index of the strong partners of two types, for the type and the cell level, outputs and inputs. 1 is the same set and 0 is disjoint. The cell-level output panel is Dr. Chiappe's convergence test. jaccard_matrix turns the pairs into a symmetric matrix with 1 on the diagonal.

def jaccard_matrix(pairs: pl.DataFrame, types: Sequence[str]) -> np.ndarray:
    """The Jaccard index of every pair of types as a symmetric matrix
    with 1 on the diagonal, in the order of types.

    pairs is DataFrame[a, b, jaccard], one row per pair.
    """
    position = {t: n for n, t in enumerate(types)}
    matrix = np.eye(len(types))
    for row in pairs.iter_rows(named=True):
        i, j = position[row["a"]], position[row["b"]]
        matrix[i, j] = matrix[j, i] = row["jaccard"]
    return matrix
def jaccard_heatmaps(
    panels: dict[str, np.ndarray], types: Sequence[str]
) -> Figure:
    """A 2 x 2 figure of titled Jaccard matrices, shaded from 0 to 1."""
    fig, axes = plt.subplots(2, 2, figsize=(9, 8))
    for ax, (title, matrix) in zip(axes.ravel(), panels.items(), strict=True):
        image = ax.imshow(matrix, vmin=0, vmax=1, cmap="Greys")
        ax.set_xticks(range(len(types)), types, rotation=45, ha="right")
        ax.set_yticks(range(len(types)), types)
        ax.set_title(title, fontsize=11)
        for i in range(len(types)):
            for j in range(len(types)):
                # Dark text on light cells and white on dark ones;
                # 0.55 is where the shading crosses mid-gray.
                ink = "white" if matrix[i, j] >= 0.55 else "black"
                ax.text(
                    j,
                    i,
                    f"{matrix[i, j]:.2f}",
                    ha="center",
                    va="center",
                    color=ink,
                    fontsize=9,
                )
        fig.colorbar(image, ax=ax, fraction=0.046)
    return fig
_named = pl.col("partner_type") != "untyped neuron"
_panels = {
    "Output partners, type": jaccard_matrix(
        pairwise_jaccard(
            sets_by_type(
                strong_out.filter(_named),
                "partner_type",
                TYPES_OF_INTEREST,
            )
        ),
        TYPES_OF_INTEREST,
    ),
    "Input partners, type": jaccard_matrix(
        pairwise_jaccard(
            sets_by_type(
                strong_in.filter(_named),
                "partner_type",
                TYPES_OF_INTEREST,
            )
        ),
        TYPES_OF_INTEREST,
    ),
    "Output partners, cell": jaccard_matrix(
        pairwise_jaccard(
            sets_by_type(strong_out, "partner", TYPES_OF_INTEREST)
        ),
        TYPES_OF_INTEREST,
    ),
    "Input partners, cell": jaccard_matrix(
        pairwise_jaccard(
            sets_by_type(strong_in, "partner", TYPES_OF_INTEREST)
        ),
        TYPES_OF_INTEREST,
    ),
}
_fig = jaccard_heatmaps(_panels, TYPES_OF_INTEREST)
_fig.savefig(figure_path("lc4_partner_jaccard.png"), dpi=150)
_fig

Top partner types

Bars instead of a network: each LC type's heaviest output (top row) and input (bottom row) partner types, by the synapses it exchanges with their strong partners. top_partners_figure draws them in the color of the LC type.

def top_partners_figure(
    out_mass: pl.DataFrame,
    in_mass: pl.DataFrame,
    types: Sequence[str],
    *,
    shown: int = TOP_PARTNERS_SHOWN,
) -> Figure:
    """Horizontal bars of the heaviest partner types of each type, the
    outputs in the top row and the inputs in the bottom one.

    out_mass and in_mass are DataFrame[type, partner_type, synapses],
    as type_mass returns them; shown is the number of bars per panel.
    """
    fig, axes = plt.subplots(2, len(types), figsize=(14, 8))
    for col, t in enumerate(types):
        for row, (mass, direction) in enumerate(
            [(out_mass, "out"), (in_mass, "in")]
        ):
            ax = axes[row][col]
            top = (
                mass.filter(pl.col("type") == t)
                .sort("synapses", descending=True)
                .head(shown)
                .sort("synapses")
            )
            ax.barh(top["partner_type"], top["synapses"], color=TYPE_COLORS[t])
            ax.set_title(f"{t} {direction}", color=TYPE_COLORS[t])
            ax.tick_params(labelsize=8)
            ax.set_xlabel("synapses")
    return fig
_fig = top_partners_figure(
    type_mass(strong_out), type_mass(strong_in), TYPES_OF_INTEREST
)
_fig.savefig(figure_path("lc4_top_partners.png"), dpi=150)
_fig

Network

The LC types, each type's NET_TOP_PER_TYPE heaviest partner types (outputs and inputs together), and the lateral connections between the LC types. Partner-to-partner edges are left out on purpose. An edge's width is its synapse count; one color leaves an LC type, another enters one, and gray is lateral. network_edges picks the edges.

def network_edges(
    out_mass: pl.DataFrame,
    in_mass: pl.DataFrame,
    lateral: pl.DataFrame,
    *,
    top_per_type: int = NET_TOP_PER_TYPE,
    min_synapses: int = NET_MIN_SYN,
) -> pl.DataFrame:
    """The edges of the network: the lateral ones, and those to the
    heaviest partner types of each type.

    out_mass and in_mass are DataFrame[type, partner_type, synapses],
    as type_mass returns them, and lateral is DataFrame[pre_type,
    post_type, synapses], as lateral_synapses returns it.
    A partner type is kept when it is among the top_per_type of its LC
    type by its synapses in both directions together; an edge needs at
    least min_synapses.
    DataFrame[src, dst, synapses, kind]; kind is "out", "in", or
    "lateral".
    """
    outputs = out_mass.select(
        src="type", dst="partner_type", synapses="synapses", kind=pl.lit("out")
    )
    inputs = in_mass.select(
        src="partner_type", dst="type", synapses="synapses", kind=pl.lit("in")
    )
    both = pl.concat(
        [
            outputs.with_columns(type=pl.col("src"), partner=pl.col("dst")),
            inputs.with_columns(type=pl.col("dst"), partner=pl.col("src")),
        ]
    )
    top = (
        both.group_by("type", "partner")
        .agg(pl.col("synapses").sum())
        .sort("synapses", descending=True)
        .group_by("type", maintain_order=True)
        .head(top_per_type)
        .select("type", "partner")
    )
    kept = both.join(top, on=["type", "partner"]).select(
        "src", "dst", "synapses", "kind"
    )
    lateral_edges = lateral.filter(
        pl.col("pre_type") != pl.col("post_type")
    ).select(
        src="pre_type",
        dst="post_type",
        synapses="synapses",
        kind=pl.lit("lateral"),
    )
    return pl.concat([lateral_edges, kept]).filter(
        pl.col("synapses") >= min_synapses
    )
net_edges = network_edges(
    type_mass(strong_out),
    type_mass(strong_in),
    lateral_synapses(
        with_post_type(connections, cells), TYPES_OF_INTEREST
    ),
)
net_edges
shape: (39, 4)
srcdstsynapseskind
strstri64str
"LC10a""LC9"561"lateral"
"LC9""LC10a"2853"lateral"
"LC9""LC11"295"lateral"
"LC9""LC16"93"lateral"
"LC11""LC10a"596"lateral"
…………
"Tm20""LC16"2892"in"
"PVLP004""LC9"13513"in"
"LT56""LC9"3002"in"
"PVLP012""LC9"2674"in"
"PVLP070""LC9"374"in"

network_figure draws them as an interactive network: LC types in their colors, partner types in gray.

def network_figure(edges: pl.DataFrame, types: Sequence[str]) -> Network:
    """A pyvis Network of the edges, with the LC types in TYPE_COLORS,
    the partner types in gray, and each edge in the color of its kind.

    edges is DataFrame[src, dst, synapses, kind].
    """
    net = Network(
        height="560px",
        width="100%",
        directed=True,
        cdn_resources="in_line",
    )
    net.barnes_hut(gravity=-12000, spring_length=220, central_gravity=0.25)
    for name in types:
        net.add_node(
            name, label=name, color=TYPE_COLORS[name], size=40, borderWidth=2
        )
    partners = (set(edges["src"]) | set(edges["dst"])) - set(types)
    for name in sorted(partners):
        net.add_node(name, label=name, color=CONTEXT_COLOR, size=16)
    colors = {
        "out": DIRECTION_COLORS["out"],
        "in": DIRECTION_COLORS["in"],
        "lateral": CONTEXT_COLOR,
    }
    for row in edges.iter_rows(named=True):
        # The width in pixels grows by one for every 300 synapses and is
        # clamped, so weak edges stay visible and strong ones do not
        # swamp the plot.
        width = max(1.5, min(8.0, row["synapses"] / 300))
        net.add_edge(
            row["src"],
            row["dst"],
            width=width,
            color=colors[row["kind"]],
            title=f"{row['kind']} {row['synapses']} synapses",
        )
    return net
_net = network_figure(net_edges, TYPES_OF_INTEREST)
_net.write_html(
    str(figure_path("lc4_network.html")),
    open_browser=False,
    notebook=False,
)
mo.iframe(_net.generate_html(), height="600px")