Skip to main content

Auto-Organotypic

Automated analysis of bioluminescence and fluorescence time-lapse recordings from organotypic suprachiasmatic nucleus (SCN) slices.

The SCN is the master circadian pacemaker of the anterior hypothalamus. Kept alive as an organotypic slice and imaged for days, it reports its own timekeeping as a movie. Auto-Organotypic turns those movies into per-cell and whole-tissue rhythm measurements.

Built-in usage guide

The installed package includes a searchable guide with complete examples, input and output conventions, stage choices and troubleshooting:

from auto_organotypic import context

print(context.read())
print(context.read("quickstart"))
print(context.search("missing sampling interval"))

The same guide is available with python -m auto_organotypic.context quickstart. Reading it works offline without loading recordings, imaging dependencies or the desktop. context.read(topic, format="json") and context.search(...) return structured responses. The action reference describes the selected headless control surface; auto_organotypic.actions.describe() and discover() provide its live contracts and coverage.

What is here

The Motion stage also offers Relative-Intensity Pattern Registration with the reviewed bright/dim, tissue-landmark and moving-cell recipes. Its Java engine ships inside the wheel; the existing registration default is retained.

The first step, and the one that usually costs a person an afternoon with a mouse: finding the tissue. One call reads a registered recording, draws the accepted two-lobe outline, rotates it so both lobes sit the same way up in every recording, and writes a square crop centred on the outline — with no hand-drawn region of interest anywhere in the chain.

from auto_organotypic import outline

result = outline.automatic(
    "meanred_MCG_04_1_595.tif",
    valid_mask="validfield_MCG_04_1_595.tif",
    output_dir="out",
)
result["output"]           # the two-lobe label image, oriented
result["cropped_output"]   # tight square crop around the SCN centre
result["report"]           # every setting, hash and measurement of that run

A registered ImageJ or OME hyperstack works as well as a two-dimensional time mean. scn_channel picks the outline channel and scn_z the depth plane, both using one-based ImageJ numbering; scn_time is "mean" (the default), "max", or a one-based frame number such as 320. Whichever plane is chosen determines one orientation and one crop, which are then applied to every plane in the stack. For a flattened RGB TIFF, scn_channel=1, 2, or 3 selects its red, green, or blue sample respectively. For a large online-only hyperstack, selected_source_only=True writes only the chosen two-dimensional plane and hash_source=False avoids downloading the rest merely to calculate a full-file hash; the selected plane and all outputs remain hashed. write_oriented_source=False keeps the orientation but skips writing the full oriented copy of the stack, which costs a second streaming pass over every plane and a second full-size file — wasted if the next step reads only the crop. The transform stays in the report either way.

lobes defaults to 2, preserving the accepted method. Set lobes=1 (or --lobes 1) to keep a single detected or drawn region whole, or lobes="any" to let the midline evidence return either one or two. A one-lobe outline cannot infer anatomical up from a gap between lobes, so it keeps source orientation unless an angle is supplied; its square crop and lobe_1 trace still run.

A folder of recordings

from auto_organotypic import batch

records = batch.run(
    "D:/recordings",                 # a folder, a file, or any list of paths
    output_root="D:/scn_out",        # each recording gets output_root/<stem>/
    scn_channel=1, scn_time="mean",  # shared by every recording
    per_source={"MCG_04_1_595": {"scn_channel": 2}},   # what differs
    workers=8,                       # recordings at once; unset chooses
    on_progress=lambda record: print(record["input"], record["ok"]),
)

workers is where a folder's wall-clock lives. The outline is single-threaded and around thirty seconds for a 512×512 field, against four milliseconds of file reading, so nothing about how the images are stored or passed in will move it. Left unset it is half the machine's cores, at most eight. A 27-recording library, measured on sixteen logical cores:

processes wall clock speed-up peak memory
1 434 s 1.00× 0.2 GB
2 196 s 2.21× 0.4 GB
4 141 s 3.07× 0.8 GB
8 90 s 4.83× 1.5 GB

The cap is memory, not speed: the gain is still real at eight, and each process holds its own recording, so a folder of stacks costs far more per process than the single frames measured there. Pass workers=1 for one process, or a bigger number on a machine with the memory for it.

On Windows a process pool needs the calling script to have an if __name__ == "__main__": guard — without one, each new process re-imports the script and runs it again. A run that chose its own worker count meets that by saying so and finishing in one process, so a script that worked before still works. A run that was told workers=8 fails loudly instead — every recording comes back carrying the error — because a caller who named a number is owed the news that it did not happen.

batch.run is a separate call rather than a list argument on outline.automatic, because the two want opposite things when something goes wrong: one recording should raise, a folder should finish and then tell you which three failed. Each record carries ok, skipped, the report path, and either the full single-call result or the error with its traceback; the same records are written to batch_scn_outline_manifest.json.

The same run from a shell:

python -m auto_organotypic D:/recordings -o D:/scn_out --workers 8 --scn-channel 1
python -m auto_organotypic D:/recordings --dry-run     # list what would be processed

Installing also puts a auto_organotypic command on the path. It is argparse over batch.run and nothing else — every flag is one of that call's keywords, one for one. It prints a line per recording as it finishes and exits 0 when every recording produced output, 1 when any failed.

Discovery keeps the recordings and drops the valid-field masks, the partial writes and anything an earlier run wrote — from names alone, because opening a folder's worth of online-only files to read their metadata downloads the folder. A re-run skips any recording whose report already exists, so an interrupted folder resumes where it stopped; pass overwrite=True to redo the work.

Crops are "tight", "standard" (the default), "wide", or an exact crop_size_px. Every preset is checked to keep every outline pixel, and a custom size that would cut the outline is refused rather than silently clipped.

An instrument's folder, straight in

A PyIncucyte or PyLV200 download writes a folder of TIFFs and a manifest beside them. read_manifest turns that manifest into recordings, and outline_plan turns those into the two arguments batch.run already takes:

from auto_organotypic import batch, outline_plan, read_manifest

recordings = read_manifest("D:/pull")        # either instrument, same records
sources, per_source = outline_plan(recordings, channel="red")
batch.run(sources, per_source=per_source, output_root="D:/scn_out")

channel takes a name — "red" also finds TRITC and mCherry, "phase" finds BF and brightfield — or a one-based number. The index it produces describes the stack as written, not the vessel, so a well that missed a channel does not shift every name after it by one.

No extra is needed to read a finished pull. A manifest is plain JSON, so the four dependencies above are enough. What a manifest says is worth looking at before a long run:

auto-organotypic plan D:/pull
VID1234_A1_PhRd_20260820.tif  well A1  TCYX  393 frames  [Phase, Red]  -> channel 2

The ClockCyteR cell grid, without Fiji

ClockCyteR.FIJI is four ImageJ macros that prepare a recording for ClockCyteR.spatial. The fourth lays a grid of 5 x 5 pixel squares over the frame and measures every one of them in every frame of every channel. cellgrid is that macro in NumPy:

from auto_organotypic import cellgrid
grid = cellgrid.extract("D:/scn/L_1.tif", "D:/scn/L_1_results")

It writes the same two files the macro does, grid_centroids.csv and one Ch<n>_<n>_grid_vals.csv per channel, and needs no Fiji.

The automatic pipeline enables tissue-only spatial grids by default. Its spatial stage measures all selected channels in the accepted outline, omits empty squares, and excludes outside pixels from partial squares. Saved grid previews and the desktop configuration preview are clipped to that same outline. Set the square side in Spatial grid, or pass --spatial-option cell_size=10; --no-spatial disables the stage. Channel tables, cell coordinates, acquisition times and previews share spatial/<recording>/.

Analyse grid rhythms is on by default: each square receives detrending, period estimation and a cosine fit, with period/amplitude/phase/quality maps, trace heatmaps, measured-data/fit overlays and channel comparisons. The shared results table includes channel identity and corrected spectral evidence; population summaries describe phase coherence across squares. This uses the rhythm extra. --spatial-option analyse=false keeps raw extraction only.

With the spatial extra installed, the same measured squares then go through ClockCytePy, the validated Python port of ClockCyteR.spatial: its own outlier removal, linear detrend, LOESS smoothing and FFT-NLLS fit per square, then the local coherence of period, phase and amplitude. One table per channel, spatial/<recording>/clockcyte_ch<n>.csv, holds every square with ClockCyte's columns unchanged (period, phase_h, phase_circ, amplitude, error, RAE, AUC, period_crn, amp_crn, phase_crn), and clockcyte_ch<n>_summary.json carries its channel summary and circular statistics. The maps ClockCyte publishes for each of those (period, amplitude, error, RAE, AUC, the three phase maps and the three coherence maps) are drawn by ClockCytePy's own renderer, in its own palettes, into visual/spatial_maps/ as <recording>_clockcyte_<map>_ch<n>. ClockCytePy is imported, never copied; without it the spatial stage still writes everything above and records the ClockCyte half as pending. --spatial-option clockcyte=false turns it off. ClockCyte's network and cluster analyses are not in the chain yet.

A path, a Series or a (frames, channels, y, x) array all work — but give it the path for anything long. Frames are measured and written a chunk at a time, so nothing scales with the length of the recording: 4000 frames of a three-channel 175 x 291 stack is a 1.2 GB file and 72 MB of memory. Handing it an array instead means having the whole recording resident first, which past a certain length is not a slow path but no path at all.

The edge squares are the whole difficulty. The macro's grid is sized with a float division and makeRectangle clips the last square to the image, so the final column and the final row come out narrower than five pixels: their means are over fewer pixels and their centroids sit off the lattice. Every slice of the published toy dataset is clipped, two of the three in both axes, so the obvious reshape(-1, 5, 5).mean() gets the square count, the centroids and the edge means all wrong. Laying it out the way the macro does reproduces all three published grid_centroids.csv files byte for byte.

To run the macros themselves instead, point AUTO_ORGANOTYPIC_FIJI at the launcher:

from auto_organotypic import fiji
fiji.extract_cellgrid("D:/scn")                 # macro 4
fiji.define_roi("D:/scn/hemis/Left")            # macro 3, re-using saved outlines

Only macro 4 is unattended in the first place. Macros 2 and 3 automate the second pass, off regions somebody drew once, and say so before starting Fiji if those are missing; macro 1 needs a line drawn on every stack and saves nothing to re-use, so auto_organotypic.orientation is the answer there rather than a wrapper.

Macro 4 measures in blocks of frames. ImageJ's Multi Measure gets slower the more rows are already in the Results table, so handing it a whole recording at once is the expensive way to ask. Measured across two grids, the cost per square sits at a median of about 74 microseconds while the table stays under roughly 50,000 rows and rises to about 272 by 230,400 — some 3.7 times worse. Blocks keep a run on the flat part: for a 4800-square grid over 48 frames and two channels the whole folder went from 131 s to 42 s. The blocks are stitched back into the one published Ch<n>_<n>_grid_vals.csv before extract_cellgrid returns, byte for byte what the unblocked macro wrote. The run-to-run spread on those timings is wide, so treat them as a floor and a direction rather than a law, and do not extrapolate past 230,400 rows. Even at that floor ImageJ is around 80 times slower than cellgrid — 42 seconds against 0.5 for that stack — so the macro route is for confirming the NumPy one, not for work.

The whole sequence

auto-organotypic pipeline D:/pull -o D:/scn_out --channel red
auto-organotypic pipeline D:/pull --dry-run     # resolve every stage, run none
auto-organotypic stages                         # what is installed on this machine

Seventeen stages, in order: acquire (PyIncucyte or PyLV200), index, trim_before_crop, broad_crop, trim, register, split, outline, image, grid, video, video_grid, change_map, change_video, trace (one call covering the cosmic-ray rule, a trace per region, the instrumental control and the rhythm verdict), spatial (tissue-only square measurements) and review (the quality check, below). Only the acquire stage needs another package, and the two movie stages need auto-organotypic[video] for its encoder. The two trims and the split do nothing at all unless asked, so a run that does not mention them is the run it always was.

Each is reached by dotted name, so a stage whose package is not installed is reported as pending at the top of a run rather than raising four steps in; --allow-pending runs the rest. A stage is also reported pending when a library it imports once it is already running is missing — registration reaches for scikit-image inside its estimator, and a run that discovered that forty minutes in would be the exact failure this design exists to prevent.

Every stage appends what it did to auto-organotypic-pipeline-run.json, written after each stage rather than at the end, because the stage that fails is the one whose record matters.

Five stages are opt-in. acquire runs only when an --experiment is given; without one the folder is taken as already pulled. broad_crop runs with --broad-crop, and rewrites every stack it crops, which is not something to do to a plate somebody already framed by hand. Three are display exports, below; the fourth, the grid sheet of every recording's best cycle, is drawn unasked and --no-grid leaves it out. The review runs by default at its minimal preset — the crop-and-orientation strip and its page — and --review off or --review full (everything, below) changes that; outline_geometry is another name for minimal.

A stage this package did not write

Your package can put a stage of its own into that chain, in a position it names, and have it treated exactly like one of these. Call this from a module of your own package:

from auto_organotypic import pipeline as chain

def count_cells(state, options):
    """Handed the recordings as they stand here, and the run's keywords."""
    settings = options.get("cell_count_options") or {}
    counted = {str(one.path): count(one, **settings)
               for one in state["recordings"]}
    return {"recordings": len(counted), "total": sum(counted.values())}

chain.register_stage(
    chain.Stage("cell_count", "YourPackage",         # name, and who owns it
                "yourpackage.counting:count",        # the function it runs
                "count objects in each recording",   # one line, for the record
                needs="yourpackage[count]",          # how to install it
                probes=("skimage",),                 # what it imports once running
                opt_in=True),                        # off unless asked for
    count_cells, after="split", verdict=would_it_rerun)

runner(state, options) is the stage body. state["recordings"] is what the run has carried this far and state["folder"] is the folder it was given; options is every keyword the run was called with. What you return goes into the run record under your stage's name, so keep it to what a person would want to read. To hand something to a later stage, put it in state under your own key — the way trace does with trace_results.

The dotted target does more than it looks. It is how --dry-run reports whether your stage can run before the run starts, how --cell-count-option is checked against your function's real signature, and how a package that is not installed becomes pending at the top of a run rather than an ImportError forty minutes into one.

From there it is a stage: --stages cell_count, --skip, --since, a place in on_progress, and "owner": "YourPackage" on its record entry, so a finished run says which package performed what. The optional verdict(recordings, options, output_root) -> {path: (status, why)} gives it a row in the --what-would-run staleness grid; without one that row reads unknown, the same answer this package gives for any stage it does not own.

Two rules make this safe to call at import time. opt_in=True means a plain run does not perform it — naming its options is how it is asked for, as for the exports above, except that --cell-count-option need not carry a setting: naming the mapping at all is the ask, since a stage added from outside has no flag of its own to fall back on. So installing your package cannot change what somebody's existing run does. And after= states the order, so two packages registering in either sequence produce one pipeline rather than two. Registering the same stage twice does nothing; a different stage under a name already taken is refused. registered_stages() lists what is registered and who owns each.

This package never reaches the other way. There is no entry-point scan and nothing is imported that is not in this wheel: the analysis ships inside the package, and registration is something an already-loaded package says about itself. A bare install has an empty registered_stages() and a stage table that names nobody.

Pictures and movies out of the same run

auto-organotypic pipeline D:/pull -o D:/scn_out                    # the sheet, unasked
auto-organotypic pipeline D:/pull -o D:/scn_out --image --video --video-grid
auto-organotypic pipeline D:/pull -o D:/scn_out --grid-option moments=12 --grid-option columns=4
auto-organotypic pipeline D:/pull -o D:/scn_out --no-grid --video-option hours_per_second=24
auto-organotypic pipeline D:/pull -o D:/scn_out --change-map
Flag Makes Lands in
--image one still per recording visual/images/A1_mean.png
none: drawn by default, --no-grid leaves it out every recording's best cycle at six circadian times, one sheet per channel, in the run's channel colours visual/images/grid_6moments_ch1.png
--change-map the same sheet as black-and-white departures from each pixel's own cycle mean visual/intensity_changes/maps/grid_intensity_change_7moments_ch1.png
--video one movie per recording visual/videos/A1_24hps.mp4
--video-grid every recording playing at once in one movie visual/videos/grid_24hps.mp4

The trace stage now mirrors that image structure after it measures the accepted outline: one visual/traces/A1_trace.png per recording and one visual/traces/all_slices_traces.png comparison grid. A second collective view, visual/traces/all_slices_trace_supergraph.png, overlays every recording and emphasizes their average. The numeric source stays under traces/A1/; figures are display-only and are never fed back into an analysis. The same folder holds A1_amplitude_by_cycle_ch1.png and all_slices_amplitude_by_cycle_ch1.png: each channel's amplitude per whole cycle with its fitted damping rate and half-life, whose numbers are traces/A1/cycles.csv (period, phase and amplitude per cycle, damping beside them) and traces/damping_recordings.csv.

Nobody opens six picture folders, so the run ends by writing index.html at the top of the output root (and index.md beside it, the same content as text): every figure and movie the run's ledgers record, grouped by well and then by channel, under a table of the run's conventions -- each channel's name, lookup table and trace colour and where the colour came from, the figure format, the review preset, the phase alignment. Every entry is a link to the file the run wrote, so the page is small, and nothing on it was found by filename: a figure without a ledger row is not there. Open it first. auto-organotypic index D:/scn_out rebuilds it for an older run.

auto-organotypic pipeline D:/pull -o D:/scn_out \
  --trace-option primary=48h \
  --trace-option circadian=periodogram \
  --trace-option xtick_hours=12 \
  --trace-option visual_normalise=zscore \
  --trace-option visual_signal=detrended \
  --trace-option visual_average=median

By default, trace figures show the pixel-weighted union of both outline lobes alone and a second view with both individual lobes overlaid with that union. They use a 24 h rolling detrend, periodogram plus cosinor annotation, x ticks every 24 h, and per-recording 0-1 normalization. visual_regions=all saves only the lobe overlay; visual_regions=both saves only the combined trace.

Every trace route—the analysis, one trace, comparison grid and supergraph—uses the same detrending controls. detrends and primary accept none, linear, running_mean, polynomial, kernel, amp_baseline, frequency, cubic, bicubic, poly6, lowess, first_difference, moving_median, savitzky_golay, robust_linear and asymmetric_least_squares. Here bicubic is the legacy name for a degree-3 time polynomial; poly6 fixes the degree at 6. Generic polynomial degree, local window coverage, Gaussian bandwidth, retained frequency-period limits and filter order are set with detrend_polynomial_degree, detrend_min_valid_fraction, detrend_bandwidth_hours, detrend_low_cut_hours, detrend_high_cut_hours and detrend_filter_order. LOWESS exposes its fitted-point fraction and robust iterations through detrend_lowess_fraction and detrend_lowess_iterations; asymmetric least squares exposes smoothness, above-baseline weight and iterations through detrend_asls_smoothness, detrend_asls_asymmetry and detrend_asls_iterations. Supply any of these as Python arguments or repeated --trace-option KEY=VALUE settings.

Period estimation is configurable on the main region analysis with period_method="fft_nlls", ls_pmin=2, ls_pmax=48 and, for example, period_config={"nlls_max_components": 5}. The single-trace, grid and supergraph functions accept the same estimator and configuration, with period_range=(2, 48) for their search limits. Changing the displayed baseline or analysed samples invalidates the saved fit.

The live Circadian Workbench catalogue is available through rhythm.available_period_methods(). It includes Lomb-Scargle, Enright/ Sokolove-Bushell chi-square and F periodograms; FFT-NLLS (fast Fourier transform plus nonlinear least squares); maximum entropy spectral analysis; mFourFit (multi-harmonic Fourier fitting); spectrum resampling; JTK_CYCLE; and empirical JTK_CYCLE. rhythm.estimate_period() and rhythm.compare_periods() accept every catalogue key. FFT-NLLS, maximum entropy, mFourFit and spectrum resampling estimate a period but do not test whether the trace is rhythmic, so pair them with a significance-bearing method such as Lomb-Scargle.

FFT-NLLS (fast Fourier transform followed by nonlinear least-squares fitting) uses the requested search band, with no additional 15–35-hour selection limit. significance_method separately selects the rhythmicity test; fit-only estimators default to Lomb–Scargle. The p-value tests the trace, not an individual fitted component. Period and amplitude errors, relative amplitude error (rae), all fitted components, method names and settings are saved. The displayed FFT-NLLS curve is the fitted multi-component model; the legacy cosinor result remains a separately identified cosine. signal="fit" (also accepted under the legacy cosinor spelling) selects the fitted curve.

The automatic chain defaults to visual_channel_mode="both": individual channel trace figures and an overlay of all analysed channels, including an overview grid. The desktop Channel trace figures choice also offers Individual and Overlay. Files share the existing trace figure folder: A1_trace_ch1.png, A1_trace_ch2.png, and A1_trace_overlay_ch1_ch2.png. The Lobe trace figures setting defaults to Combined + lobe overlay (visual_regions="paired"). It saves the combined measurement alone and a second figure overlaying left, right and combined lobes on the same axes. The latter adds _lobes to the filename, for example A1_trace_lobes_ch1.png and A1_trace_lobes_overlay_ch1_ch2.png. Channel overlays use colours for channels and line styles for lobes. Choose visual_regions="all" for the lobe overlay alone or "both" for the combined trace alone. A recording with one region produces one region view.

Default channel colours are red (1), grey/black (2), green (3), and purple (4), shared by traces, images and movies. Selecting channels individually or in a different order keeps their original colours. Image/movie lut overrides remain available. Trace lobes use line styles within the channel's colour. Experiment-specific trace names can be supplied with visual_channel_labels, for example {1: "syn-RCamp", 2: "brightfield", 3: "syn-GABASnFR"}. The direct trace plotting functions accept the same mapping as channel_labels.

visual_signal defaults to detrended: the detrended measurements with the cosine fit drawn on top. The fit shares the data's normalisation so its amplitude remains comparable. This applies to individual plots, channel and lobe overlays, grids, and averages. Average plots overlay the average of the individual saved fits, rather than fitting the averaged data again. cosinor selects the fit alone; raw shows the measured trace with its trend; both overlays raw and detrended data using the raw trace's normalisation. circadian=none or periodogram hides the fit overlay.

A cosinor-only figure needs a fit. Where there is none — circadian=none or circadian=periodogram was asked for, or the periodogram found no peak to fit — the figure falls back to detrended rather than drawing a curve for some recordings and a blank for others; in a grid or a supergraph one unfitted recording takes the whole figure with it, so every panel keeps showing the same thing. The report records requested_signal beside the signal that was actually drawn.

The supergraph defaults to the arithmetic mean over the time interval shared by every recording. visual_average=median is resistant to isolated extreme recordings; visual_average=trimmed_mean removes visual_trim_fraction from each tail at every time point. Set visual_time_range=union to retain the longest recording and average whichever recordings are present there. Detrending and region measurement live in auto_organotypic.region_trace; the display code lives in auto_organotypic.trace_plot. Neither reaches the external single-cell dLuc pipeline.

By default all four run after the outline and before the trace, so "index it, outline it and show me" is a complete run that never pays for the trace half. Files are named for the well and for what they are of, rather than for source stems that reach 149 characters by this point in the pipeline.

Movies can attach an animated trace below or beside the image. In the desktop, choose Pictures → Animated trace → Animated trace position (bottom or right; default off). The panel progressively draws saved detrended data and the saved fit, using each frame's acquisition time. Choose the movie channel or all analysed channels, and combined lobes or left/right/combined together. Each series uses its own minimum/maximum scale; its fit uses that same scale. Movie sheets have the same controls under experiment-wide presentations.

The automatic chain runs trace analysis before movies when a panel is enabled. Presentation-only reruns reuse saved traces. Direct stack_to_video calls take trace_panel="bottom", trace_source="path/to/traces/A1"; stack_to_video_grid takes a mapping from each source movie path to its trace folder. Use trace_channels="all", trace_regions="all" or trace_fit=False to adjust it. Playback speed, timestamp wording and acquisition gaps do not change the data alignment. The image retains its original size and labels; the panel is added outside it, with padding for very small crops.

Automatic trace runs also produce phase maps by default: one per analysed channel, a shared-scale comparison sheet and all pairwise phase-difference maps. These use the accepted tissue outline and cleaned frames, the selected trace window and detrending, and a common 24-hour fitted cycle. A 1 h peak minus a 23 h peak is shown as a +2 h delay. Weak or unavailable fits are grey. Maps appear in visual/spatial_maps/ (runs before 2026-09-15 keep theirs in visual/phase_maps/); channel-labelled numeric values and differences are saved beside the regional traces. Traces → Phase maps controls the square size, shared cycle and minimum R-squared (fit quality), or disables the maps. The headless equivalents are trace_options={"phase_maps": True, "phase_cell_size": 5, "phase_period_h": 24., "phase_min_r2": .1}. R-squared screening is separate from the regional rhythm and instrumental-control findings.

Every keyword of auto-organotypic image, grid and video is reachable, as --<stage>-option KEY=VALUE, repeatable. Values are read as JSON where they parse as it, so moments=12 is a number and shared_range=false is false, and as plain text otherwise, so lut=fire works without a quoting puzzle. In Python it is one mapping per stage:

run_pipeline(folder, image=True, video_grid=True,
             image_options={"when": "max", "lut": "fire"},
             video_grid_options={"columns": 4, "hours_per_second": 24})

Giving a setting is itself the asking, so --image-option when=max turns the still on (the sheet needs no asking; --grid-option moments=12 changes its columns). A key the export will not take is refused by name before the run starts, checked against the function's own signature — misspelling colums costs a second rather than the outline stage's minutes.

Four things are filled in from what the run already knows, each only as a default that anything you pass overrides: where the file goes, what it is called, the frame interval from the recording's own metadata, and the well names on a sheet's or a mosaic's tiles.

The mosaic is the one with opinions, because tiling recordings that disagree is how a movie comes out looking right and being wrong:

  • Unequal windows are refused. Nothing is dropped or held to make them look aligned. on_length=shortest is the explicit opt-out for a plate where one well stopped early, and it names what it cut. There is no "hold the last frame" at all: a finished recording still sitting there looking alive is a claim about the biology.
  • Two acquisition intervals cannot claim one biological speed. Asking for hours_per_second across them is refused; a plain fps claims nothing and is allowed, and each tile keeps its own recording's timestamp.
  • shared_range is one contrast per recording across its whole window, the same axis the still sheet uses — so a dim phase stays dim and a late bright phase is not clipped by an early frame. It is not one range across the plate: each well ranges itself, so comparing brightness between wells needs an explicit display_range=(black, white).
  • Accepted outlines can be drawn over the tiles, which the still sheet also does.
  • Every tile may start at its own hour zero. align="onset" finds each recording's first rise and begins its tile there, so nine slices that came up at nine different times play in phase. What that does not change is the frame-for-frame advance: two cadences still drift apart after the frame they were aligned on, which is the same reason hours_per_second is refused across them.

A plate is streamed, never assembled: one frame is painted from each recording, tiled, encoded and dropped before the next is read, so the memory is one row of frames rather than one row of stacks.

The mosaic is also its own subcommand, auto-organotypic video-grid, for a folder that never went through the pipeline.

Nothing any of the four writes may be measured from. Each marks its output as a display artefact, the run record says so per stage, and none of them touches the recordings it was handed — a lookup-table painted 8-bit picture reaching the trace stage would be the one failure that looks like a result.

Did it go well? The review

auto-organotypic pipeline D:/pull -o D:/scn_out                # the strip and its page, by default
auto-organotypic pipeline D:/pull -o D:/scn_out --review full  # every panel
auto-organotypic pipeline D:/pull -o D:/scn_out --review off   # nothing
auto-organotypic review D:/scn_out                             # afterwards, on its own

--review takes off, minimal (the default; outline_geometry is its older name) or full. The default draws review_geometry.png and the page; full draws all five panels below.

Every stage already writes down what it decided and how sure it was: the crop detector's reason for keeping a frame whole, registration's residual against its own threshold, the outline's verification line and open_questions, the accepted orientation rule's three top-end cues and which of them agreed, the square crop's clipped-pixel count, the instrumental control's verdict. That is six files per recording, in JSON and CSV, so on a plate of ninety-six nobody reads any of them.

The review reads them and draws them into visual/review/:

Panel Answers
review_scorecard.png every recording against every check, worst first
review_geometry.png one column per recording, one row per stage of the geometry — registered mean, oriented square crop, crop with the outline over it
review_registration.png each frame's residual, against the threshold the run recorded
review_orientation.png which end of the medial gap each of the three cues chose, and how square the rotation came out
review_rhythm.png period and power per region, and the instrumental control
review.html all five inlined, plus every number as searchable text

Five words and no sixth: ok (the stage's own gates passed and it left no question open), check (it finished, and marked its own answer as one to look at), given (a person answered instead of the method), failed, and not run.

check is the one worth being clear about. It does not mean something went wrong. An automatic outline on a recording nobody has hand-checked is check every time — nothing is wrong with it, and nothing has confirmed it either. The orientation is where it earns its keep: a slice can be outlined perfectly and turned upside down, nothing downstream notices because a lobe trace is a lobe trace either way, and the cue vote is the number that would have told you.

Automatic colour maps use embedded ImageJ lookup tables first, then explicit OME channel colours, recognised channel/reporter names, and channel-specific emission, excitation or linked laser wavelengths. A microscope model alone does not identify a channel colour. Missing or unusable metadata falls back to the numbered-channel palette. OME's default white colour is treated as unspecified; an embedded ImageJ grey table remains an explicit grey choice.

The image viewer, images, movies, reviews and traces use these channel defaults. Custom ImageJ tables are preserved, and trace lines use a readable hue from the same table. Colours and their source travel with processed stacks and saved trace tables. Explicit image/movie lut settings or trace channel_colours settings override the defaults. Display choices never assign a biological role.

The automatic chain writes a review for every selected output channel, using the same saved outline and orientation for each background. Channel identity is in the filenames (review_ch1_geometry.png, review_ch1.html); labels keep the same size across channels. For a finished run, review.run_to_review(output_root, channel=1) displays channel 1 beneath the original analysis outline.

The review re-judges nothing. A verdict here is the verdict the stage recorded, read back and given a colour — so it cannot disagree with a report, and a run from last month says the same thing to somebody who was not there. The picture strip resizes nothing either: a pixel means the same distance everywhere on the sheet, which is why a plate wraps into blocks of twelve (--columns N) rather than shrinking to fit.

auto-organotypic review D:/scn_out --crop tight       # fill each tile with tissue
auto-organotypic review D:/scn_out --panel geometry --strip-row raw \
    --strip-row cropped --strip-row outlined

--crop decides what the strip's tiles are framed on, and it is the one real choice:

  • none (the default) draws each row's whole frame, so the square crop visibly sits inside the frame it was cut from. This shows what the crop did.
  • tight, standard or wide frame every tile on the outline at the accepted crop's own scales (1.05, 1.25, 1.50 — imported from outline/crop.py, not copied), so the tissue fills the tile and the empty field goes. This shows what the boundary and the rotation are, which is what the strip is usually opened for.

Each row's box is the outline's own bounding box walked back into that row's frame — through the rotation, the registration overlap and the coarse box, each hop a number some stage recorded. A row whose walk is missing a step is drawn whole rather than guessed at, and the report names what every row was framed from.

--strip-row raw adds the pre-registration recording as a row. It is the only row that reopens a full source stack, so it is the slow one and is off unless asked. Display only, like everything else in this section; it needs auto-organotypic[image] and an output root, since a review is about a run and not about a recording.

Analysing part of a recording

auto-organotypic pipeline D:/pull -o D:/scn_out --trim 0..144h
auto-organotypic pipeline D:/pull -o D:/scn_out --split-at 72h
auto-organotypic pipeline D:/pull -o D:/scn_out --split baseline=0..70h --split drug=74h..144h

Both are said the same way: frames (1..288), hours (0..72h) or days (2d..5d), with .. between the two ends and either end left off to mean the beginning or the end. A time window is half-open, so 0..72h and 72h..144h are the two halves of a six-day recording and no frame is in both or in neither.

--trim 0..144h drops frames nothing should see. Use it when the microscope lost focus or the slice drifted out of the field: registration crops its export to the box every frame still covers, so a hundred bad frames at the end shrink the field of view for the whole recording, including the good days. It reruns registration, so it costs hours.

--split-at 72h produces two complete sets of outputs from one recording — traces/A1_start-72h/ and traces/A1_72h-end/ — measured through one registration, so the two are comparable. It reruns the outline and the trace, so it costs about a minute. Name the spans if you would rather read the folders later: --split baseline=0..70h --split drug=74h..144h, which also drops the four hours around a media change without leaving a gap inside either series.

A window said for one well beats the plate's, through the command that already exists for saying so:

auto-organotypic correct D:/pull --stage trim --only B2 --value '"0..120h"'
auto-organotypic correct D:/pull --stage split --only B2 --value '["0..48h", "48h.."]'

Moving a cut point writes new folders and leaves the old ones alone — nothing here deletes results. The run says which segment folders it did not write this time, so they are not read as current a month later.

--trim-before-crop puts the window on the stage before the broad crop instead of the one after it. It is for the recording whose bad frames fooled the crop detector, and it costs a full-frame copy of every recording it touches rather than a cropped one.

The broad crop

auto-organotypic crop D:/pull --mode wide --dry-run   # how much would wide take off?
auto-organotypic crop D:/pull -o D:/cropped --mode wide
auto-organotypic crop D:/pull --when max              # draw the box from other frames
auto-organotypic pipeline D:/pull --broad-crop --broad-crop-mode standard

--dry-run measures every recording, reports the box the chosen mode would draw, and writes nothing — worth running before committing a plate to a nine-day experiment. Without -o, crops go to AI_Exports/<stem>_broad_crop/ beside the source, which is where the folder form already knows not to look for inputs.

Registration costs what the frame costs. An Incucyte pull is 1152 × 1536 and the slice is about a fifth of it, so aligning the empty four fifths costs the same as aligning the tissue. This stage finds the tissue once and boxes it.

tight, standard and wide are 1.5, 2.0 and 2.8 times the box the tissue occupies — larger than the region crop's numbers because they scale the whole slice rather than the SCN outline, and because the surround they leave is what the accepted outline later reads its own background from. standard = 2.0 is not a taste: three Incucyte wells exist in both the full frame and a crop a person drew for this exact purpose, and those hand crops are 1.75 – 2.04 times the tissue box this finds.

--when picks which frames the box is drawn from, using the same three answers as --scn-time: mean, max, or a one-based frame number. Left alone it uses this stage's own rule — a median inside each of five chunks spread across the recording, then the maximum across them — which survives both a cosmic ray and the circadian trough while reading two dozen planes instead of all of them. Reach for --when when the default framed something you did not want; max holds the union of everywhere a drifting slice went, and a frame number is for the recording where you know which frame to look at.

It never cuts tissue, and it does not get tighter as the signal gets weaker. Otsu's threshold on the frame's own histogram says what is certainly tissue; a second, lower one says what is certainly background; and the box holds every connected piece that reaches the first while spreading out to the second. Both levels are read off the frame, so nothing has to be chosen in advance. Where the tissue fills the frame, or the well is empty, or the histogram cannot separate anything, it keeps the whole frame and the run record says why.

Four settings, three of them fractions of the frame and the fourth a compute budget — down from eleven, because Otsu now reads off the histogram what three of those settings used to have to be told.

single_object=False is the default and should stay off unless the stage did not move: the detection frame is a maximum across time chunks, so a slice that drifted shows as two blobs and the union of them is where the slice was. Turn it on for a large frame with bright debris elsewhere in the well. region=(x0, y0, x1, y1) crops to exactly what you name — obeyed, not judged, though the record still says whether it holds the tissue.

Measured across 75 real recordings spanning a 138-fold range of signal-to-noise ratio: 16 of the 75 cropped, none of the 16 hand-drawn outlines clipped, and the ten viral-reporter recordings that the first version of this stage was quietly cutting now keep every pixel of their tissue.

Drawing it yourself, or just saying it

auto-organotypic pipeline D:/pull --broad-crop-rois D:/rois/crops    # the broad crop
auto-organotypic pipeline D:/registered --stages outline     --outline-rois D:/rois/outlines                           # the SCN outline
auto-organotypic crop D:/pull --rois D:/rois/crops --rois-if-missing raise
auto-organotypic crop D:/pull --region 300,220,900,760              # one box, whole plate
auto-organotypic D:/registered --angle 74 --crop-region 40,40,360,360
auto-organotypic D:/registered --orient-rois D:/rois/up --outline-crop-rois D:/rois/squares

Four measured steps take the answer instead. Each has a folder of drawings and a value for the whole run, and they are separate options because they are cut from different frames:

step drawn said
broad crop --broad-crop-rois --broad-crop-region x0,y0,x1,y1
SCN outline --outline-rois —
which way is up --orient-rois --angle DEG, --flip
square crop --outline-crop-rois --outline-crop-region x0,y0,x1,y1

Every flag says which crop, because pipeline runs two of them. There is no --crop-rois there: it used to mean the broad one, and a spelling that means a different stage in a different command writes a finished run in which nothing looks amiss. Typed anyway, it stops the run and names the four flags that do say which. Under auto-organotypic <folder>, where the outline is the only stage, the square crop is also spelled --crop-rois and --crop-region, beside the --crop that already means it there.

Open the recording in Fiji, draw round the tissue with the freehand tool, save the region as a .roi or a ROI Manager RoiSet.zip named after the recording or its well, and point the run at the folder. A recording nobody drew is left to the automatic method, which is the point: a plate where three wells needed a person and ninety-three did not is the normal case. --rois-if-missing skip leaves the undrawn ones alone; raise stops the run, which is how a fully manual pass is asked for.

Which way is up is one line, not a region. Draw it from the ventral base to the dorsal tip with the straight-line tool: the direction of the stroke is the direction that ends up at the top. Analyze > Measure gives that stroke's angle, and typing it into --angle is the same instruction — 0 to the right, 90 straight up the screen. When the automatic rule found the axis and only called the dorsal end upside down, --flip is the whole correction. A per-recording angles.json — {"B2": 74.5}, or {"B2": {"flip": true}} — says either for a plate.

Draw on what the step sees. An ImageJ ROI stores pixel coordinates and not the size of the image they were drawn on, so a region drawn on the raw recording and applied to a registered stack lands in the wrong place and nothing in the file says so. That is why the flags are separate: --broad-crop-rois is drawn on the raw recording, --outline-rois on the plane the outline reads — the *_OUTLINE_INPUT_*.tif a previous run wrote — and the square crop on the *_ORIENTED_SOURCE.tif, because that crop is cut after the rotation. A drawing that cannot fit inside the frame it is handed is refused rather than clamped into a plausible crop. A drawn direction is the exception: an angle has no position, so the same stroke means the same thing on either frame.

A given box is obeyed, not judged — and counted. The presets keep every outline pixel and --crop-size-px is refused if it would not, but a box you named is taken as given: the report carries outline_pixels_clipped and raises an open question rather than quietly returning a smaller SCN.

The default outline is two lobes. Two drawn regions is one lobe each; one region plus a line down the middle is the region cut along that line; one region alone leaves the midline to the accepted split. --lobes 1 instead keeps that region whole, and --lobes any accepts either result. The report is written under its own tool name so nothing claims the frozen accepted geometry for an outline a person drew.

pip install "auto-organotypic[incucyte]" adds the Incucyte download and pip install "auto-organotypic[lv200]" the LV200 one; pip install "auto-organotypic[rhythm]" adds what the trace stage needs to put a period and a saved trace figure on a trace.

The rhythm adapter imports only Circadian Workbench's public package-root facade. Its period, phase, synchrony and channel answers therefore follow the same versioned contract as a direct circadian_workbench call; the adapter only reshapes those results for this pipeline.

Something to watch

auto-organotypic video D:/pull --filters broad_crop,register,bioluminescence
auto-organotypic video D:/pull/A1.tif --hours-per-second 6 --lut C1=green --lut C2=red
auto-organotypic video-grid D:/pull --columns 3              # every well together
auto-organotypic video D:/outlined --outline                 # plain + outlined copy
auto-organotypic video --list-filters                 # the steps and their parameters

One movie per recording, and the filtering happens on the way through. --filters is a chain of this package's own steps, run over each channel in memory: steps separated by commas, parameters by colons, so register:downsample=2,unmix:coefficient=0.04 is a chain of two. A raw instrument pull goes straight to something watchable, and the filtered stack that used to sit between them — a gigabyte nobody wanted — is never written. --save-filtered keeps it for the run where you do.

step what it does
broad_crop box the tissue, drop the empty four fifths of the sensor
register take the drift out, keep the field every frame still covers
unmix subtract a scaled autofluorescence channel from this one
cosmic_rays replace pixels sitting above what the same pixel does either side
static_background the per-pixel whole-record mean out, band-limited, and back
bioluminescence suppress only what matches each pixel's own noise
amplitude each pixel against its own quiet level, so the rhythm is the picture
no_drift the slow fade out, so what moves on screen is the rhythm
smooth, local_contrast display smoothing, and each frame against its own blur

Every step calls the owning module's own function rather than a copy: a chained bioluminescence comes out bit-identical to display.bioluminescence_display's TIFF, and a test asserts it. The first two are geometry — they change how big the picture is — so they are planned once for the recording and every channel gets the same box.

A week-long movie is mostly a picture of the battery running down. The substrate is used up, the tissue settles, the focus creeps, and that fade is larger than the daily rhythm the recording is of: on the ten-recording reference set only 0.40 of everything moving on screen is the rhythm. no_drift gives every pixel a moving average of its own circadian period, subtracts it, and puts the pixel's own long-run brightness back — a moving average over exactly one period cancels the rhythm, so what the average holds is the fade alone. The rhythm's share goes to 0.85, and the anatomy does not move.

auto-organotypic video D:/pull --filters no_drift:window_h=22.4 --time-gain 6

Give it the recording's own fitted period where you know it; a window 10 % off leaves about 9 % of the rhythm in the average and takes that much away. And expect to want --time-gain with it: once the fade is gone the rhythm is left using about 1.6 % of the display range where the fade had been using 11.6 %, so a drift-free video at 1x is honest and faint. It is display only.

Order is yours and it means something. broad_crop,register boxes the union of everywhere the slice went and registers the small frame — the pipeline's order, and the cheap one. register,broad_crop aligns the whole sensor first and boxes where the slice is. A filter either side is the same choice: filter the full frame, or filter the crop. The chain runs what is written down.

broad_crop takes the same --when the crop stage does: --filters broad_crop:when=max,register.

Nothing here may be measured from. Every movie records itself as a display artefact, and the one stack --save-filtered can write is marked _DISPLAY_ONLY whatever its chain was: it carries the movie's record rather than the unmixing coefficient's or the cosmic-ray rule's, so a number taken from it would have no artefact behind it. filtering.unmix and cosmic.remove_cosmic_rays are how a measurable stack is made.

A stack already marked _DISPLAY_ONLY can be cropped and registered — that is usually the one you most want held still to watch, and the movie is display-only either way. It still cannot be unmixed or de-spiked: that rule is about steps that change what a pixel means, and moving the picture is not one.

Playback is stated in experimental hours per second, not frames per second. At 6 — the default — one biological day takes four seconds of screen time whatever the acquisition interval was, and the frame rate is derived. No frame is ever dropped or duplicated to hit a rate. One number for every movie the package writes, single tiles and mosaics alike, so two exports of one recording never play at two speeds.

video-grid makes one moving comparison sheet from several recordings. Its visual controls are the image grid's: gutters are white and 2.5% of a tile, and each recording gets one automatic range measured across its complete selected window. --timestamp-format, --columns, --gap-px, --background, --tile-label and --own-range mean the same thing in both grid commands.

Two defaults differ from the sheet's, and both are about space. A sheet's rows are recordings, so one strip down the left names nine rows; a mosaic's tiles wrap, so every tile pays for that strip and it takes a quarter of the width. So --well-label-position defaults to top-left here — the name drawn over the frame, costing no layout — with top-right, bottom-left, bottom-right, and left for the outside strip. --well-label-orientation is a property of that strip and is ignored at a corner. And when the time cannot be hoisted into one heading (see below) it goes over a corner too, taking the one the name is not using; asking for both in the same corner by name is refused rather than quietly moved.

A mosaic on one time axis says the time once. Every tile is at the same experimental hour, so nine copies of it is eight too many. Two settings, because these are two questions:

--timestamp-scope how many times it is said auto (once when the tiles read one time, per tile when they do not), mosaic, tile, none
--timestamp-position where it goes auto, header, bottom, or a corner — of the mosaic or of each tile, whichever the scope is

none leaves the time off and keeps the names; mosaic insists on one caption and refuses a mosaic whose tiles disagree. A caption names the frame on the screen, but a frame chosen as the nearest to an instant is captioned by that instant, softened by exactly what the recording cannot resolve — the rule a contact sheet's columns already follow. Without it a tile sampled every 33 minutes reads 99.9 h beside eight reading 100, and the mosaic could never say its time once.

Every input contributes one frame to every output frame, so unequal selected frame counts are refused rather than silently truncated; --on-length shortest stops at the shortest tile and names what it cut.

Advancing by frame index is also what makes unlike cadences play at unlike speeds, and that is the refusal worth understanding. A plate holding a 30-minute recording and a 33-minute one covers different amounts of biological time in the same number of output frames, so the second plays 11% fast for its whole length and nothing on screen says so. Experimental-hours playback therefore refuses it. There are two ways past:

what it does what it costs
--on-cadence resample every tile on one grid of experimental hours, stepping at the finest cadence present; each tile shows its own nearest frame to each instant a coarser tile repeats a frame where it has none, counted per tile in resampled
--fps N plays at a stated frame rate and claims nothing about biological speed the tiles really are at different speeds

resample is the only one that makes the mosaic real time. It is the contact sheet's rule applied to a movie — a sheet's columns are already each recording's nearest frame to a requested hour. Each tile's timestamp always follows its own recording.

Something to put in a figure

auto-organotypic image D:/pull --filters broad_crop,register,bioluminescence
auto-organotypic image D:/pull/A1.tif --when 412 --lut red    # one named frame
auto-organotypic grid  D:/pull --moments 6                    # a time montage
auto-organotypic grid  D:/pull --when mean                    # every well, one sheet

image is video with the encoder taken off, argument for argument — the same --filters, the same --lut, the same --display-range, the same --display-gamma, because both draw through the same engine. A still made with a movie's arguments is that movie's frame, to the pixel.

--when is the whole difference:

--when the picture is
mean the window averaged. The default: a nine-day bioluminescence recording is mostly noise in any one frame
max the brightest each pixel ever got — and a hot pixel too, if the cosmic-ray rule has not run
412 recording frame 412, one-based, the same vocabulary --scn-time uses

A projection has its display range measured on itself, not on the frames behind it. The mean of a thousand frames has a lower maximum than any frame in it, so a range read off the frames would draw the mean too dark by exactly the amount the averaging removed. Its caption says a stretch rather than an instant — Time: 0 h 00 min - 240 h 00 min — because a mean happened at no single time.

grid tiles several of those into one file, and what varies decides what the sheet is:

tiles
--moments 12 one recording across time — a montage
a folder every recording at one moment — a contact sheet
a folder --moments 6 rows of recordings, columns of time

A montage covers one circadian cycle unless told otherwise: see below.

Time-course rows put each well name once on the left; use --well-label-orientation horizontal|vertical to turn those names.

A caption every row repeats is said once, above its column. Nine wells at six times used to carry the time in a black band under all fifty-four tiles; --timestamp-position auto, the default, puts six headings at the top instead and the other eight bands go. It hoists only when every row of every column says the same thing, so a contact sheet of nine differently named wells keeps its names on its tiles. header asks for the hoist and refuses a sheet whose columns disagree, naming the column that does; bottom is the old band under each tile, and top-left or top-right draw over that corner. --timestamp-format accepts hours, elapsed, clock, or a template such as {prefix} {total_hours:.0f} h, day {day}.

A montage is one circadian cycle, trough to peak to trough

What a sheet of a slice is for is showing how it looks at each stage of its rhythm. So --moments 7 gives seven columns evenly over one cycle — CT 0, 4, 8, 12, 16, 20, 24 — and not seven days at the same phase, which shows the cycle exactly once and the damping six times. That was the behaviour until 2026-08-28.

Three defaults move together to do it, and any of them may be set on its own:

default what else
--between cycle — one period from hour zero window, or 24:96 in hours
--time-scale ct — the caption counts from the rhythm elapsed, zt
--align best — hour zero is the best day's trough trough, onset, peak, start, clock, hours

The cycle closes, and it opens at a trough. Changed 2026-09-03, from six columns opening on a rise. The first tile and the last are now the same phase a day apart, which is a fixed point: if the two do not match, what is on the sheet is not the rhythm. Seven and not six because an odd count with both ends drawn puts one column exactly halfway, so a day cut at its trough shows its peak in the middle tile. --between 0:20 gets the open cycle back.

Which cycle is best is measured. Each complete cycle is scored on three things and the product decides:

catches
swing tenth percentile to ninetieth of the detrended cycle a dead day
goodness variance a cosinor at the period explains a cosmic ray
consistency how nearly it peaks where its neighbours peak a one-off bump

No two are enough. A cosmic ray gives a huge excursion and explains nothing; a flat cycle explains most of its own tiny variance; and the mounting transient — the slice recovering from being cut — is the largest, smoothest bump in most organotypic records and scores well on both. The first cycle is therefore passed over whenever another exists. Every cycle's score is in the report under cycles, so the choice can be checked and disagreed with; --align onset takes the first cycle deliberately, and --align 36 takes whichever you like.

A column is a time, not a frame number. Each recording supplies its nearest frame to the hour a column names. Until 2026-08-28 they were frame numbers shared across the plate, so on the reference set — eight wells imaged every 30 minutes and one every 33 — column three was 24 hours into eight of them and 26.6 into the ninth, and a reader comparing the row could not tell.

The gap divides the cycle: a half, third, quarter, sixth, eighth or twelfth of the period, or a whole number of them. Over more than one cycle that means every column falls at the same stage, so a row is one phase sampled repeatedly rather than a beat between the sampling and the rhythm. The count you asked for is honoured — the snap is capped at the widest gap that still fits it — and spacing in the report says what it settled on. --every-h 18 states the gap outright and is never snapped; --even-spacing divides the span evenly.

--period-h 23.4 says what the cycle is; --period-h fit reads it off the recordings and takes the median. Display only, and never a period measurement — six columns over a slice free-running at 23.4 hours walk 3.6 hours off its rhythm in a day if the sheet assumes 24.

--between window restores the old behaviour, and --between 24:96 restricts the columns to those hours. A recording too short to hold a cycle falls back to its window and says so in the report; asking for cycle outright is refused rather than shrunk.

What a tile is a picture of

A circadian day is a small change on a large structure. On the ten-recording reference set a whole day is 4–6 % of the tissue's brightness — about 1.2 units of CIELAB L*, where two units is the least a reader can tell apart between two tiles side by side — so eight of those ten dishes draw a day nobody can see. Four settings decide what to do about that, and the sheet ships doing none of them:

--plot change each tile against the average of its own day 68 grey levels where intensity shows 12
--time-gain 6 the same departures drawn six times larger 6.2 L* instead of 1.2
--soft-range the ends of the range bend instead of cutting auto: on for a closing cycle
--change-levels 28 a change sheet's full scale, shared by every recording own scales each by its own spread

A video grid takes the same four, with the same defaults and the same refusals — auto-organotypic video-grid ... --plot change, --time-gain 6 — and a frame is drawn against the mean image of the record exactly as a tile is drawn against the mean image of its day. Pair --time-gain with --filters no_drift on a long recording, or the gain amplifies the fade along with the rhythm. On a video the note is printed on every frame: one frame travels on its own.

One scale for the sheet, or the sheet is not fair. Scaling each recording by its own spread draws a well with no rhythm as vividly as the best one in the set: across the reference recordings, what a sheet showed correlated 0.598 with each dish's own fitted amplitude when the scale was per dish and 0.846 when it was shared. The same is true of the gain — one number, every pixel, every recording, so a well with half the rhythm shows half the change.

A change plot is drawn in the diverging change map by default — on a sheet and on a video alike: cyan below the day's average, black at no change, red above. A dim tile on it is not dim tissue; it is tissue below its own average that day, and a single-hue ramp cannot say which side of the average a pixel is on. Name a lut and that one is used instead.

Both are display only, and a sheet drawn either way prints CHANGE or DIFFERENCES x6 beside every recording's name and records the setting in its own report, so it cannot travel without saying what it is.

Three clocks

--time-scale decides what the number is and --timestamp-format decides how it is spelled, so ZT 16:14 and CT 3.4 h are both available to either.

--time-scale zero is wraps answers
elapsed the first drawn frame never how long in
zt midnight, real time 24 h what time of day
ct this slice's own rhythm the period where in its cycle

ZT here is the wall clock, not a light cycle. A slice has no retina, so nothing in the dish responds to light and there is no Zeitgeber to count from; what the name is borrowed for is the real time of day a frame was taken. It is read off the acquisition timestamp and refused without one — --clock-start 2026-07-10T16:13:53 supplies it for a derived stack that no longer carries it.

--align puts hour zero, and therefore column one, at best, onset, peak, start, clock (the last midnight), or a stated number of hours:

auto-organotypic grid D:/pull --moments 6                        # one cycle, CT, best
auto-organotypic grid D:/pull --moments 6 --align onset          # the first cycle
auto-organotypic grid D:/pull --moments 6 --between window       # five days, one phase

onset is the first rise of a recording's own rhythm — the upward crossing of the midpoint between its first trough and its first peak. Which cycle that is comes from a cosinor at the period, and where in it from the trace itself: the largest maximum is not the first one, and on the reference plate it is a mounting transient on day one in well 1423 and a cosmic ray on day nine in 1432. best is the same rise, of the best cycle rather than the first. Both are measured from a coarse tissue-mean trace, are display only, and are reported under clocks so they can be checked or replaced with a number.

The same arguments are on image, video and video-grid. On a movie --between cycle plays exactly one period and --between 0:72 cuts the playing window in hours rather than frames, so a movie can start at the best cycle's rise and run one day. On the mosaic --align best starts every tile at its own zero, so nine slices that came up at nine different times play in phase and the grid shows how their rhythms differ rather than when each was mounted.

Tiles have white gutters 2.5% of their short side by default, following the Plot That image-grid reference. --gap-px and --background override them.

One display range for each complete recording or split. It is measured across the selected time window and applied to every time tile from that input, so a dim phase stays dim and a late bright phase cannot be clipped by an early tile. --own-range turns sharing off for a layout check.

Automatic contrast: black at the 50th percentile of the empty field, white at the 99.8th of the stack. Measured, not chosen — see docs/display-contrast.md. The old white point at the 99.999th percentile was set by a handful of outlier pixels, so the tissue lived in the bottom sixth of the ramp and a circadian cycle moved the bright regions by 52 of 255 grey levels; at 99.8 it moves them by 66.

The black point took three rounds, and the first two were graded by the wrong ruler. Clipping was measured over largest_tissue_mask — the Otsu mask the floor is read outside of — so tissue the floor had crushed fell below that threshold, sat outside the mask, and was never counted. Measured over the accepted SCN outline instead, that mask covers 56% of the slice, a third of the "empty field" is accepted tissue, and at 90 the floor drew 33.5% of the slice pure black (46% on the worst recording). At 50 it draws 2.0%, the dim sixth of the slice goes from grey 2 to 44, and the empty field still renders at a median of zero — 50 is the last floor at which it does.

And the ramp is a curve, not a line: --display-gamma, 0.7 by default. Retuned on 2026-08-31, when the same complaint came back in the same words. The 2026-08-28 sweep measured only the bright tenth of the tissue and fixed the bright tenth; measured again with a column for the dim third — the pixels the complaint is about — a straight ramp draws it at 29 of 255, which a red lookup table draws as near-black. No pair of endpoints reaches it, because a slice glows on top of the medium it sits in and its dim third therefore sits just above the black point however that point is chosen. At 0.7 the dim third is drawn at 56 and swings 51 grey levels over a recording instead of 39, and because the curve is applied after the clip the two percentiles still mean exactly what they meant — clipping is unchanged at both ends. 1.0 is the old straight line; 0.5 doubles the effect again for a dim preparation, at the cost of visible grain in the empty field.

When even a curve is not enough, --filters amplitude. A slice whose tissue sits a thousand counts above the empty field and swings a tenth of that spends most of the ramp on the constant part however it is ranged. amplitude takes each pixel against its own quiet level — the tenth percentile of its own time course — so the ramp is spent on the rhythm, and on the reference plate that is five times the visible amplitude. The price is that a tile then shows change rather than brightness, which is why it is a filter you ask for and not a default. Display only, like everything it feeds.

A recording that will not open stops the sheet. --skip-bad finishes without it and prints what was left out, because a grid quietly missing three of its wells is worse than no grid.

A sheet is filed under the set of recordings that made it, and lands in that folder's own AI_Exports. Filing it under whichever recording was listed first is not just odd to read — add a well and the old sheet's key would still match, so a stale sheet would look current. A montage of a single recording is still filed under that recording, because there it really is a fact about it.

The still grid and video grid are display only on the same terms as each individual movie, and their records carry the same display_only mark.

Accepted outlines on display exports

Every image, movie and either kind of grid takes the same outline controls:

auto-organotypic image D:/outlined --outline
auto-organotypic grid D:/outlined --outline --outline-mode only
auto-organotypic video A1.tif --outline A1_SCN_LABELS_ROI_CROP_TIGHT.tif
auto-organotypic video-grid D:/pull --outline D:/analysis/batch_scn_outline_manifest.json

--outline without a path finds the accepted labels from the outline-stage source or batch_scn_outline_manifest.json. Python also accepts a label array, an ordered list for a grid, or a mapping keyed by source path, filename, stem or well name. The labels must already share the rendered image's orientation and crop; they are never resized into a plausible-looking but displaced boundary.

The default --outline-mode copy keeps the unchanged export and adds an *_outline copy. --outline-mode only writes just the outlined export at the requested name. The default boundary is opaque cyan and two pixels wide; --outline-colour, --outline-width-px and --outline-opacity change it.

The window

auto-organotypic ui                                       # a window on this machine
auto-organotypic ui --project D:/Results/experiment        # reopen saved work
auto-organotypic ui --browser                             # the same page, in a browser
auto-organotypic serve --host 0.0.0.0 --root D:/Imaging   # one machine, whole lab

A desktop window is the primary interface. Its whole-chain map opens full stage workspaces to configure processing and review real evidence. Choose existing TIFF stacks with native file dialogs, or select recordings through the PyLV200 or PyIncucyte native browser. A selection saves a request; only starting the saved experiment begins collection. Compatible completed pulls can also be imported. The connected workspaces include named periods, native-coordinate crop/outline/ midline/direction corrections, translation import, square-grid measurements, sample labels, group presentations, recipe restore and moved-source repair. The connection acceptance record separates implemented behavior, verification and external prerequisites.

The interface opens in dark mode, with a separate saved light-mode preference. Source/result stacks and saved movies have Play/Pause and direct frame seeking. Scroll over the image, drag the live frame slider, or use arrow keys to inspect a stack; Shift-scroll moves ten frames and Ctrl-scroll zooms. Nearby decoded frames are buffered within a fixed memory budget for quick back-and-forth inspection. A distant, uncached frame can still need a brief load. Draft colour, gamma and contrast changes preview real frames without saving a recipe or rerunning analysis; mean/max projections and overlays still require an export. The optional desktop-qt extra provides an explicit --gui qt native host that does not fall back to another toolkit if Qt cannot start.

In the Windows Qt desktop, Inspect in ImageJ on a source/result TIFF opens a resizable, dark inspection pane inside the workflow window. The bundled ImageJ engine reads real planes as a virtual stack, buffering raw frames up to 512 MiB and warming the active channel when it fits. Its wheel, live slider, channel selector, zoom/pan and contrast controls are independent of the export recipe. Two TIFF panels can open together with Compare both in ImageJ, sharing a slider and adjustable frame offset. Linking matches frame positions, not automatically acquisition times or spatial coordinates. Full/Half/Quarter preview detail is display-only; Quarter uses about one sixteenth of the cached pixel memory, though a cold TIFF read still decodes the full original plane. Use Full when reviewing fine features. Java 11 or newer must be installed (AUTO_ORGANOTYPIC_JAVA can select the runtime); a running Fiji installation is not needed. This first embedded inspector supports classic scalar 8/16-bit integer and 32-bit floating-point TIFFs, not BigTIFF, tiled images or multi-Z stacks. Existing previews and scientific editors remain available; this is not a replacement for every stage viewer. See the embedded inspector verification.

Traces & rhythm also offers live analysis comparison: change detrending, period estimator and rhythmicity test over saved measurements, then compare reference/trial traces, estimated periods and test statistics. The trace updates before longer-running tests finish. These are read-only trials; copy a chosen configuration into the recording's recipe draft and explicitly apply it to change future processing. Saved instrumental-control warnings remain visible, and trying multiple settings does not correct for multiple testing.

Underneath, the window is a loopback server drawing the same page serve serves. The page is drawn inside the application's own window, with no browser tab or address bar. Browser hosting is optional. Closing the window stops its interface server, but a saved experiment's separate processing worker continues while the computer stays powered on. An active Windows worker requests prevention of automatic idle sleep and releases it on exit; screen locking is unaffected. This does not prevent manual sleep, shutdown, power loss or a system restart. Reopen its project to reconnect. The older /classic interface still requires its server to remain open.

Processing settings use the packaged pipeline's accepted defaults. The native bridge selects files/folders, opens microscope selectors/setup applications, saves exports and opens validated results directories; processing and evidence use the same checked interface as the hosted page. Artifacts and compact result bundles use native Save dialogs in the desktop. Group figures use saved measurements and fits, not an undisclosed rerun of the analysis. Computation completion and human acceptance remain separate.

Two behaviours worth knowing. A run is submitted and watched rather than blocking, because registering ninety-six wells takes hours. And stopping takes effect at the next stage boundary — a stage is one call into another package, so "stop" honestly means "finish this one and do no more".

serve is the other half, and the only one with a host to worry about. There is no login, so binding to anything but loopback requires at least one --root, and every folder browsed, planned or run is checked against it.

$ auto-organotypic serve --host 0.0.0.0
--host 0.0.0.0 serves this to the network, so it needs at least one --root
saying which folders it may read and write. Without one the folder picker would
offer the whole machine to anybody who can reach the port, and there is no login
to stop them.

The method is frozen

The outline is accepted Round 6 attempt 7 and the orientation accepted Round 9 attempt 6, from the Cry1-DIO-dLuc red-channel tuning project. The public settings retain the accepted pixel values, because six declared attempts to simplify or normalise them failed the truth, shape or generalisation gates — a shorter interface would have meant a less portable method, not a tidier one.

tests/test_automatic_scn_outline_parity.py compares output bytes against ten accepted fields rather than comparing behaviour. That evidence lives in the governed tuning project rather than in this repository; point AUTO_ORGANOTYPIC_TUNING_ROOT at it to run those tests, and without it they skip while the synthetic crop, orientation and input-safety tests still run.

Install

pip install auto-organotypic

Only numpy, scipy, tifffile, scikit-image and imagecodecs. No plotting stack, no web framework, no audit layer: outlining a slice should not install any of them. The last two are there because the method genuinely reaches them. scikit-image for convex_hull_image, inside the consensus that places the midline; it was declared under the register extra alone until 2026-08-25, which meant a plain install imported, read a manifest, and then failed partway through the first outline. imagecodecs because it is what tifffile decodes LZW with — tifffile’s own fallback handles zlib, LZMA, Zstd and PackBits and not LZW — so without it a plain install raised on the first frame of any recording Bio-Formats or Fiji had written, which is most of them. Reading its own input is not an optional capability, so it is not an extra. tests/test_self_contained.py runs the outline with everything undeclared blocked, which is the check that found the first of the two.

Each extra is needed only for what it names — and none of them to read a finished pull, because a manifest is plain JSON:

Extra For
auto-organotypic[incucyte] starting an Incucyte acquisition
auto-organotypic[lv200] pulling from a running LV200 (PyLV200)
auto-organotypic[acquisition] installing both microscope clients
auto-organotypic[register] registration (OpenCV, scikit-image)
auto-organotypic[image] stills, grids and colour maps (Pillow, matplotlib)
auto-organotypic[video] movies too, which need an encoder (adds imageio-ffmpeg, imageio)
auto-organotypic[rhythm] periods, cosinor fits and the rhythm verdict (circadian-workbench)
auto-organotypic[spatial] the rhythm extra plus ClockCytePy's per-square period, phase, amplitude, error, AUC and coherence maps on the tissue grid
auto-organotypic[ui] the page and the server behind it (FastAPI, uvicorn)
auto-organotypic[desktop] the window (adds pywebview, which borrows the webview your machine already has)
auto-organotypic[desktop-qt] the optional Qt WebEngine window host; select it explicitly with auto-organotypic ui --gui qt, with no fallback to another host

Shared circadian calls

With the rhythm extra installed, extracted arrays use Circadian Workbench's actual public caller, argument definitions, completed results and figure engine:

from auto_organotypic import rhythm

recording = rhythm.trace(hours, values,
                        settings={"period_min_hours": 20, "period_max_hours": 28})
result = recording.detrend(window_hours=24).compare_periods()
result.plot(theme="classic").save("periods.svg")

hours and values are the measured arrays. The figure and its replay inputs go under .circadian-agent; use .save("periods.svg", root="my-figures") to choose another output root. No server, private frame construction or local circadian renderer is required. Completed-result plotting does not fit again.

rhythm.describe("compare_periods") exposes the live method contract, and rhythm.argument_group() returns its scientific settings with units, defaults, allowed values and aliases. These new calls use only installed Workbench defaults. Prior runs, themes and saved projects never supply hidden defaults. result.run_record carries the effective inputs and software/seed evidence; result.script checks that evidence when reproducing the completed call.

Existing dictionary wrappers retain their signatures and explicit 15–40-hour search range and 30-minute bins. Their values and source fields are unchanged; period estimation/comparison, periodograms, cosinor, detrending and normalization also return workbench_run_record. These compatibility choices are not a second default registry for rhythm.trace.

Array analysis does not certify an instrumental control. The existing source-based rhythm.test_rhythm still refuses a missing control and carries the control verdict alongside separately attributed estimation and significance records. Importing the base package requires no rhythm extra; requesting rhythm analysis without it gives the installation explanation.

Where this came from

These modules lived in PyMicroglia until 2026-08-23. Nothing about outlining a suprachiasmatic nucleus concerns microglia, and the code was already a leaf — nothing in that package imported it. PyMicroglia keeps the automatic_scn_outline action, which now delegates to auto_organotypic.outline:

pip install "PyMicroglia[scn]"

That extra reaches PyPI with PyMicroglia's next release; the version published there today predates the move and has neither the extra nor the delegating module.

Where this is going

Auto-Organotypic is the SCN layer of an automated instrument-to-rhythm pipeline: download, crop broadly, register, outline and crop, trace, test the rhythm, render videos. The headless command is stage 3 of that plan, the window stage 4 and this release stage 5. docs/automated-scn-pipeline.md, in the repository, records which package owns which step and why.

License

MIT.

Release files for Auto-Organotypic 0.7.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for Auto-Organotypic 0.7.2
File Size Uploaded
auto_organotypic-0.7.2.tar.gz 48.3 MB Details

Built distribution (wheel)

Table of built distributions (wheels) for Auto-Organotypic 0.7.2
File Interpreter ABI Platform
auto_organotypic-0.7.2-py3-none-any.whl Python 3 none any Details

Total release size: 86.0 MB

Release files / auto_organotypic-0.7.2.tar.gz

Download URL auto_organotypic-0.7.2.tar.gz
Size 48.3 MB
Tags Source
SHA-256 checksum
How to use checksums
a4ca8a9e3fc53d93a37a3ad6814696320ebd0ae4602c261afa10b1fcc2fd35cc
BLAKE2b-256 checksum
How to use checksums
3c72ca5b605d3674ab4f214ae541ad8e96be8574770a2e216a31a5fbf4106867
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release files / auto_organotypic-0.7.2-py3-none-any.whl

Download URL auto_organotypic-0.7.2-py3-none-any.whl
Size 37.7 MB
Tags Python 3
SHA-256 checksum
How to use checksums
e6b26cc11def69dde3979fde5a4dda54b159e2cf0dda1dfa5b76f228b6d89ef5
BLAKE2b-256 checksum
How to use checksums
7d1acd54c8495bfd83b082c7970e60e3dd6ec3fddd683a63b21aae41c0fd7755
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 23, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.7.2 This release

2 release files

0.7.1

2 release files

0.7.0

2 release files

0.6.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page