Skip to content

Do LC16 and LC6 receive the same inputs?

Introduction

Question

Do LC16 and LC6 receive input from the same presynaptic cell types?

Background

Wu et al. 2016 find that the two types stratify in the same layers of the lobula, and expect the same inputs from it:

Consistent with their similar responses in the imaging experiments, LC6 and LC16 have very similar lobula layer patterns while LC11 has a different arbor stratification indicating that LC11 receives inputs from a different set of medulla cell types than LC6 and LC16.

— Wu et al. 2016, p. 29 · PDF p. 29 · 10.7554/eLife.21022

The same paper cautions that a shared layer does not settle it:

since all lobula layers contain terminals of many neurons, connectivity cannot be inferred based on layer patterns alone, as not all the neuronal types that arborize in a shared layer will be synaptic partners.

— Wu et al. 2016, p. 18 · PDF p. 18 · 10.7554/eLife.21022

The connectome gives the inputs themselves. This notebook compares the input of the two types by the types of their strong input partners (lc_pathways defines input partners and lc_output_strength strong ones), with LC11 as a type that the paper says differs. It uses the right hemisphere only, the better-proofread side.

Expectations

  1. If the two types share their inputs, the Jaccard index of their sets of input partner types is higher than the index of either with LC11. If the layers say nothing about the inputs, the three indices are about equal.
  2. If they share their heaviest inputs, the 3 input types that carry the most synapses to each type overlap. If they do not, the heaviest types differ even where the sets overlap.

Load data

The analysis reads census, which gives each cell's type, sides, which says which hemisphere a cell is in, and proofread_ids, which lists the proofread neurons.

census = load_census()
census
shape: (138_327, 2)
root_idprimary_type
i64str
720575940599457990"T4b"
720575940599763910"T5b"
720575940600623020"Dm3q"
720575940602564320"KCg-m"
720575940602720940"JO-CA"
……
720575940661275009"KCab-p"
720575940661281409"L4"
720575940661285249"LPC2"
720575940661304449"Lawf1"
720575940661333889"CB2303"
sides = load_visual_types().select("root_id", "side")
sides
shape: (95_079, 2)
root_idside
i64str
720575940596125868"right"
720575940597856265"right"
720575940597944841"right"
720575940598267657"right"
720575940599333574"right"
……
720575940661323905"left"
720575940661325697"left"
720575940661327745"right"
720575940661336193"left"
720575940661339777"right"
proofread_ids = load_proofread_ids()
proofread_ids
shape: (139_255, 1)
root_id
i64
720575940599457990
720575940599763910
720575940600623020
720575940602309600
720575940602564320
…
720575940661275009
720575940661281409
720575940661285249
720575940661304449
720575940661333889

The types are restricted to the right hemisphere.

cells = cells_of_types(census, sides, TYPES)
pl.DataFrame(
    {
        "type": list(TYPES),
        "right-hemisphere cells": [len(cells[_t]) for _t in TYPES],
    }
)
shape: (3, 2)
typeright-hemisphere cells
stri64
"LC16"74
"LC6"60
"LC11"61

inputs has one row per connection that ends at one of those cells, with its weight, the type of the receiving cell (post_type), and the type of the sending cell (pre_type), which is null for a cell the census does not name. input_connections is the same table with the two ends swapped (reversed_connections), as in lc_pathways, so that a strength reads the same way for inputs as for outputs.

_scored = [_id for _ids in cells.values() for _id in _ids]
_raw = load_connections_of(_scored, end="post")
inputs = with_cell_columns(
    with_post_type(_raw, cells),
    census.rename({"primary_type": "type"}),
    end="pre",
)
input_connections = with_pre_type(reversed_connections(_raw), cells)
inputs
shape: (87_135, 5)
pre_pt_root_idpost_pt_root_idweightpost_typepre_type
i64i64i64strstr
7205759403796172877205759406052422421"LC16"null
7205759403796359757205759406131685541"LC16"null
7205759403796713037205759406147121151"LC16"null
7205759403796976717205759406241235361"LC16"null
7205759403797364067205759406341448751"LC11"null
……………
7205759406612719377205759406249835311"LC16""MLt1"
7205759406613164817205759406214347891"LC6""Tm5c"
7205759406613169937205759406220147581"LC16""LPLC2"
7205759406613169937205759406278748321"LC16""LPLC2"
7205759406613180177205759406365780141"LC11""LLPC3"

Input by sender type

The synapses that a type receives from the cells of each sender type are

\[ I_T(A) = \sum_{i \in A} \sum_{j \in T} w(i \to j), \]

where \(w(i \to j)\) is the weight of the connection from \(i\) to \(j\). The types have different numbers of cells, so the share \(I_T(A) / \sum_{A'} I_T(A')\) compares them, over the sender types the census names. A cell without a type cannot be matched between the types, so it is left out. input_shares gives \(I_T(A)\) and the share for each type. On three connections onto two types:

def input_shares(inputs: pl.DataFrame) -> pl.DataFrame:
    """The synapses each receiving type gets from each sender type, and
    their share of the type's typed input.

    inputs is DataFrame[post_type, pre_type, weight, ...]; a connection
    with a null pre_type or post_type is left out.
    DataFrame[type, sender_type, synapses, share].
    """
    return (
        inputs.drop_nulls(["pre_type", "post_type"])
        .group_by("post_type", "pre_type")
        .agg(synapses=pl.col("weight").sum())
        .rename({"post_type": "type", "pre_type": "sender_type"})
        .with_columns(
            share=pl.col("synapses") / pl.col("synapses").sum().over("type")
        )
        .sort("type", "synapses", descending=[False, True])
    )
input_shares(
    pl.DataFrame(
        {
            "post_type": ["T", "T", "U"],
            "pre_type": ["A", "B", None],
            "weight": [3, 1, 9],
        }
    )
)
shape: (2, 4)
typesender_typesynapsesshare
strstri64f64
"T""A"30.75
"T""B"10.25
shares = input_shares(inputs)
shares
shape: (1_256, 4)
typesender_typesynapsesshare
strstri64f64
"LC11""T3"274130.168868
"LC11""T2"221560.136484
"LC11""T2a"128990.07946
"LC11""Tm21"109420.067404
"LC11""Tm25"75620.046583
…………
"LC6""CB2316"10.000024
"LC6""LatB"10.000024
"LC6""LTe50"10.000024
"LC6""SLP003"10.000024
"LC6""PLP004"10.000024

The chart shows the sender types with the largest share in either of the two types, largest at the top.

_top = (
    shares.filter(pl.col("type").is_in(PAIR))
    .group_by("sender_type")
    .agg(pl.col("share").max())
    .sort("share", descending=True)
    .head(SHARES_SHOWN)["sender_type"]
    .to_list()
)
alt.Chart(
    shares.filter(
        pl.col("type").is_in(PAIR) & pl.col("sender_type").is_in(_top)
    )
).mark_bar().encode(
    y=alt.Y("sender_type:N", sort=_top, title=None),
    yOffset=alt.YOffset("type:N", sort=list(PAIR)),
    x=alt.X(
        "share:Q",
        axis=alt.Axis(format="%"),
        title="% of the type's typed input synapses",
    ),
    color=type_color(),
    tooltip=[
        "type:N",
        "sender_type:N",
        alt.Tooltip("share:Q", format=".1%"),
    ],
).properties(width=420, height=360)

Overlap of the input types

The strong input partners of a type are the proofread cells that connect to it with a strength of at least STRONG_SYN, without the LC cells among them (labeled_partners and strong_set, as in lc_pathways). Their types, from the census, make the set of input types of the type. A proofread neuron that the census does not name has no type to compare, so it is left out. The Jaccard index \(J(A, B) = |A \cap B| / |A \cup B|\) is 1 for identical sets and 0 for disjoint ones. It counts a type shared at the weakest the same as one shared at the strongest, so the heaviest input types are compared as well.

strong_in = strong_set(
    labeled_partners(input_connections, cells, census, proofread_ids)
)
input_types = sets_by_type(
    strong_in.filter(pl.col("partner_type") != "untyped neuron"),
    "partner_type",
    TYPES,
)
overlap = pairwise_jaccard(input_types)
overlap
shape: (3, 5)
abjaccardsharedunion
strstrf64i64i64
"LC16""LC6"0.2631581038
"LC16""LC11"0.133333860
"LC6""LC11"0.137931858

The heaviest input types of each type, by the synapses it receives from the strong partners of the type (type_mass):

heaviest = {
    _t: type_mass(strong_in)
    .filter(pl.col("type") == _t)
    .head(TOP_INPUTS)["partner_type"]
    .to_list()
    for _t in TYPES
}
pl.DataFrame(
    {
        "rank": list(range(1, TOP_INPUTS + 1)),
        **{
            _t: heaviest[_t] + [None] * (TOP_INPUTS - len(heaviest[_t]))
            for _t in TYPES
        },
    }
)
shape: (3, 4)
rankLC16LC6LC11
i64strstrstr
1"PVLP007""Tm5b""T3"
2"Tm20""PVLP008""T2"
3"Tm37""Li33""Li15"

Discussion

Expectation 1: the index of LC16 and LC6 is 0.26, higher than the index of the pairs with LC11 (LC16/LC11 0.13 and LC6/LC11 0.14). The two types have 25 and 23 input types, and 10 of the 38 types in either are in both (Overlap of the input types).

Expectation 2: 0 of the 3 heaviest input types are shared. The heaviest are PVLP007, Tm20, and Tm37 for LC16 and Tm5b, PVLP008, and Li33 for LC6.

Conclusion. The two types share more input types than either shares with LC11, and 0 of their 3 heaviest are common. The comparison is at the level of cell types, so it cannot say whether the two types receive a shared type in the same columns or from the same cells, and a shared type can still feed them different parts of their dendrites.

Limitations

The input types are those of the strong input partners that are proofread neurons with a type in the census, so a type whose members are mostly unproofread or unnamed is missing from both sets. The weights are the synapse counts of the Codex release. The analysis uses the right hemisphere only.

Open questions

  1. Do LC16 and LC6 share input cells where their dendrites cover the same columns? Test: the Jaccard index of the strong input partners of pairs of LC16 and LC6 cells with nearby addresses (lc_output_clusters), against pairs with distant addresses. Refuted if nearby pairs share no more input cells than distant ones.
  2. Do the two types receive their heaviest inputs at different depths of the lobula? Test: the depth of those synapses on each type's dendrite; it needs the coordinates of the synapses, which the connections table does not hold. Refuted if both types receive them at the same depth.

Terms

  • connection: all the synapses from cell \(i\) to cell \(j\).
  • input partner: a cell that sends a connection onto some cell of a type.
  • input type: the type of an input partner.
  • Jaccard index \(J(A, B) = |A \cap B| / |A \cup B|\): the share of the union of two sets that they have in common.
  • proofread neuron: a segment that people have proofread and joined into a neuron (proofread_ids).
  • share \(I_T(A) / \sum_{A'} I_T(A')\): the fraction of the typed input synapses of a type that come from the sender type \(A\).
  • strength \(s(T \to j)\): the weight of the heaviest single connection from a cell of the type \(T\) onto the partner \(j\); for an input partner, the heaviest connection from it onto a cell of the type.
  • strong partner: a proofread partner that is not an LC cell and has a strength of at least STRONG_SYN.
  • weight \(w(i \to j)\): the number of synapses in the connection from \(i\) to \(j\).