Skip to content

IronSheep16/Teal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Teal

Timelapse Express Analysis Library

PyPI Tests DOI

Teal

A Python library for analyzing cell lineage data from timelapse microscopy, with a focus on plant tissue development. Meant to be used in combination with MorphoGraphX (Pierre Barbier de Reuille et al., 2015), but having an otherwise connected cell mesh is sufficient for usage.


Features

  • One Graph, One Truth: Every cell is a node carrying its attributes, its timepoint and the mesh labels it came from — no parallel dataframes to keep in sync
  • Graph-Based Lineage Tracking: NetworkX-powered lineage graphs with parent-child relationships
  • Multi-Mesh Merging: Stitch several overlapping meshes of the same timepoint into a single cell graph — a cell imaged in two meshes becomes one node (keeping both labels, so results still export back to each mesh), neighbours are linked across the seam, and cross-parents connect a cell to a parent living in the other mesh. Big samples rarely come out of the microscope as one clean mesh, and this is what lets you treat them as if they had
  • Attribute Propagation: Shift attributes a fixed number of layers along lineages, or inherit them from founders (parent→child) and descendants (child→parent)
  • Spatial Context: Maintain spatial neighbor relationships alongside lineage information
  • Error Lookup: Built-in checks for the ways lineage tracing quietly goes wrong — unparented cells, broken lineages, and cells invented by a bad parent file

Installation

pip install teal-bio

The distribution is called teal-bio (the name teal was already taken on PyPI), but you still import it as teal:

import teal

To work on the library itself:

git clone https://github.com/IronSheep16/Teal.git
cd Teal
pip install -e .

Requirements

Python 3.9+. numpy, pandas, networkx and matplotlib are installed automatically.


Quick Start

1. Export the cell graph from MorphoGraphX

Teal reads the cell connectivity of a segmented mesh as a .ply. In MGX, run this once per timepoint on the segmented mesh you want to analyse:

Process.Mesh__Export__Cell_Graph_Ply_File_Save(filename, 'No', 'Active Mesh')

Name the files so the timepoint is readable from the filename (02_T0_cellGraph.ply, 02_T1_cellGraph.ply, …) — that is what files_by_t keys them by. Attributes (growth, size, zones, …) and parent labels are the CSVs MGX already exports.

2. Build and analyse in Teal

import teal as tl
from teal import files_by_t

# gather the files, keyed by the timepoint in each filename
graph_files = files_by_t('data/graphs', 0)
parent_files = files_by_t('data/parents', 0)

# build the timelapse -- one graph, every cell is a node
tmlps = tl.make_timelapse(graph_files, parent_files=parent_files)

# map attributes straight onto the nodes
tmlps.load_attr('GROWTH', growth_files)

# shift along lineages, then reduce whatever the shift collected
tmlps << (1, 'GROWTH')                   # pull values up from descendants
tmlps.filter_attr('GROWTH', 'avg_all')   # a cell has many children -> average them

# one row per final-timepoint cell
tmlps.lineages('GROWTH')

Results go back to MGX as heatmap CSVs:

tmlps.heat('GROWTH_heat', 'GROWTH')      # one CSV per timepoint, per mesh

Core Concepts

Data model

The graph is the single source of truth. Every cell is one node, carrying its attribute values, its layer (timepoint), and the (label, file_n) mesh cells that collapse into it. Edges are either time (parent → child) or space (neighbors).

Timelapse
  └── data      NetworkX graph -- ALL cells, time + space edges   <- the only store
        └── Timelapse[t] -> Timepoint    a view onto one layer; owns no data

Reads and writes go straight to the nodes; there is nothing else to keep in sync.

Working with attributes

tmlps[t] the Timepoint view at timepoint t
tmlps[t]['GROWTH'] values at t as an array; assigning writes the nodes
tmlps['GROWTH'] the lineage matrix (lineages × timepoints)
tmlps >> (n, 'A') / tmlps << (n, 'A') shift: take the value n layers away
tmlps.inherit('A') inherit (down): every cell takes its founder's value
tmlps.inherit('A', backward=True) inherit (up): every cell gathers its descendants' values
tmlps.filter_attr('A', 'avg_all') reduce the lists left by overlaps, shifts and upward inherits
tmlps.lineages('A') one row per final cell, one column per timepoint

Which way inherit runs

inherit goes both ways, but the two directions are not symmetric — a cell has one parent but many children.

Down (inherit('FOUNDER')) — one parent each, so values stay scalar. This spreads founder/clonal identity:

t0: [1, 2, 3, 4]
t2: [1, 1, 1, 2, 3, 3, 3, 4, 4]        # founder 1 ended up with 3 descendants

Up (inherit('SIZE', backward=True)) — many children each, so values accumulate into a flattened list. This is a gather over the whole progeny, not a copy:

t2: [50, 45, 80, 180, 75, 40, 38, 70, 75]           # leaves keep their own value
t0: [[50, 45, 80], [180], [75, 40, 38], [70, 75]]   # each founder holds ALL its descendants

So an upward inherit almost always wants a filter_attr after it, to decide what the gathered list means — a total, a mean, or a resolution of conflicting values:

tmlps.inherit('SIZE', backward=True)
tmlps.filter_attr('SIZE', 'avg_all')         # mean size of my progeny

tmlps.inherit('ZONES', backward=True)        # a lineage may straddle two zones -> [1, 2, 2]
tmlps.filter_attr('ZONES', 'fetch_single', value=1)

A downward inherit never needs this.


Example Workflows

Both run standalone on toy datasets shipped with the repo:

python examples/minimal_pipeline.py      # start here
python examples/multi_file_pipeline.py   # the hard case

minimal_pipeline.py — one mesh per timepoint. Build → map attributes → inherit → shift → filter → lineages, on examples/data.

multi_file_pipeline.pytwo meshes per timepoint, on examples/data_multi. This is the part that trips people up, and it is worth reading before you point Teal at a real multi-mesh sample.

Multiple meshes per timepoint

Big samples are often exported as several overlapping meshes (an abaxial and an adaxial view of the same tissue, say). Three things follow, and each has its own file:

file what it solves
overlaps one physical cell appears in both meshes under two labels. Teal merges them into one node that remembers both (label, file_n) pairs. One file per pair of meshes.
neighbors two cells touch across the seam between meshes, so neither mesh's own graph knows they are adjacent. Format: label_a, "label_b label_c".
crossparents a cell's parent lives in the other mesh, so that mesh's parent file cannot express it.

file_n is decided by filename order within a timepoint: T0_A.plyfile_n=0, T0_B.plyfile_n=1. Every other per-mesh file (parents, attributes) must follow that same order.

A cross-parent filename encodes its own routing — T<t>_<parent_mesh><child_mesh>:

crossparents/T1_01.csv   # at T1: parent is in mesh 0, child is in mesh 1
crossparents/T2_10.csv   # at T2: parent is in mesh 1, child is in mesh 0

Get that number wrong and the parent lookup falls back to a raw mesh label, inventing a cell that never existed. That is what phantom_cells() catches — see Error lookup below.

Because a merged cell is measured once per mesh, its values arrive as a list, and a filter reconciles them:

tmlps.load_attr('SIZE', size_files, overwrite='add')
# merged cell -> SIZE = [100, 104]   (once from each mesh)
tmlps.filter_attr('SIZE', 'avg_all')
# merged cell -> SIZE = 102.0

Visualization & checks

Heatmaps

Exports one CSV per mesh file, ready to load back into MGX.

tmlps[4].heat('output_path', 'GROWTH')   # a single timepoint
tmlps.heat('output_path', 'GROWTH')      # every timepoint

Lineage graphs

tmlps.graph_viz(root='134_1', attr_name='GROWTH', edge_type='time')

Error lookup

tmlps.report(attrs=['GROWTH', 'SIZE'])   # summary of the usual failure modes
tmlps.unparented()                       # cells missing a time parent
tmlps.childless()                        # lineages dying before the last timepoint
tmlps.phantom_cells()                    # cells invented by a failed parent lookup (should be 0)
tmlps.missing_attr('GROWTH')             # cells with no value
tmlps.incomplete_lineages(prune=True)    # drop cells that don't span t0 -> tN

report() prints all of the above at once:

Nodes: 4217 | layers: 0 → 5
Lineage cycle present: False
Unparented (no time parent): 132
Childless (dies before last t): 78
Incomplete lineages: 1917
Phantom cells (invented parents): 0
Missing GROWTH: 1904  e.g. ['18_0', '21_0', '35_0']

Phantom cells should always be 0. Anything else means a parent lookup fell back to a raw mesh label instead of a real cell — almost always a wrong file index in a cross-parent filename. Every real cell carries the (label, file_n) pairs it came from; an invented one carries none, which is how they are spotted.

Warnings

Loading is deliberately noisy: a label in a file that matches no cell in the mesh usually means the file and the mesh came from different exports, and you want to know. Warnings name the file and the offending labels:

t5 [02_T4T5AD_PARENTS.csv]: 30 cells have no value in file_n=1: [730, 806, 909, ...]
t3 [01_T3_GROWTH.csv]: 6 labels have no cell in file_n=0: [11, 12, 13, ...]

Once a pipeline is settled and you know which gaps are expected, silence them globally:

import teal as tl

tl.verbose(False)   # quiet
tl.verbose(True)    # back on (the default)

This only silences the loading warnings — it never hides report(), which is the thing you actually want to read.


Documentation

Full documentation available at: [link to docs]

Key Methods

Building

  • make_timelapse(graph_files, overlaps, neighbors, parent_files, cross_parent_files) - build a Timelapse from files_by_t dicts
  • make_timepoint(graph_files, overlaps, neighbors) - build a standalone Timepoint
  • files_by_t(folder, nth) - key a folder's files by the timepoint in their filename

Timelapse

  • load_attr(attr_name, files, t=None) - write an attribute onto the nodes
  • >> / << (n, attr_name) - shift an attribute n layers along the lineages
  • inherit(attr_name, backward=False) - down: every cell takes its founder's value. backward=True - up: every cell gathers its descendants' values into a list (follow with filter_attr)
  • filter_attr(attr_name, func) - reduce list-valued attributes. func is a Filter name ('avg_all', 'avg_unique', 'fetch_single') or any callable
  • combine_attrs(a1, a2, out, func) - build one attribute from two. func is a Combine name ('divide', 'pick_zone') or any callable
  • lineages(attr_name) - one row per final cell, one column per timepoint
  • heat(path, attr_name) - export heatmap CSVs for every timepoint
  • save(path) / load(path) - pickle the timelapse

Timepoint (a view: timelapse[t])

  • tp[attr_name] - values at this timepoint as an array; assigning writes the nodes
  • tp.load_attr(...), tp.filter_attr(...), tp.combine_attrs(...) - scoped to this timepoint
  • tp.heat(path, attr_name) - export this timepoint's heatmap CSVs
  • tp.frame() - a throwaway DataFrame of the nodes, for ad-hoc pandas

Error lookup

  • report(attrs) - prints all of the checks below at once
  • unparented(), childless(), incomplete_lineages(prune), missing_attr(name)
  • phantom_cells() - cells invented by a failed parent lookup; should always be empty
  • graph_viz(root, attr_name), graph_check()
  • teal.verbose(False) - silence the loading warnings (default True)

Lineages

  • lineages(attr_name) - one row per final cell, one column per timepoint
  • map_lineages() - stamp each cell with its lineage number, so lineages can be exported as a heatmap. Early cells belong to several lineages, so reduce with filter_attr('LINEAGE', 'first') before heat(..., as_int=True)

Contributing

Contributions are welcome — bug reports, fixes, documentation and new analysis functions. See CONTRIBUTING.md for how to report a problem, get help, set up a development environment, and submit a change.

If you hit a bug, the output of tmlps.report() is the single most useful thing to include.


License

This project is licensed under the GNU License - see LICENSE file for details.


Contact

Benjamin Lapointe - benjamin.lapointe@umontreal.ca

Project Link: https://github.com/IronSheep16/Teal


Acknowledgments

  • Developed for analyzing Arabidopsis leaf development
  • Built on NetworkX, pandas, and matplotlib
  • Designed for MGX microscopy timelapse integration
  • Written with the help of Claude Code

Citation

If you use Teal in your research, please cite the archived release:

@software{teal,
  author    = {Benjamin Lapointe},
  title     = {Teal: Timelapse Express Analysis Library},
  year      = {2026},
  publisher = {Zenodo},
  doi       = {10.5281/zenodo.21340656},
  url       = {https://doi.org/10.5281/zenodo.21340656}
}

GitHub also renders a "Cite this repository" button from CITATION.cff.

About

Cell lineage analysis for timelapse microscopy. Build one spatiotemporal graph tracking cell lineage and spatial neighbours together, easily handling large and complex datasets.

Topics

Resources

License

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages