Symmetry and Asymmetry in Geometric data — a Python toolkit for analysing fluctuating asymmetry (FA) and directional asymmetry (DA) in 2D biological landmark data.
SAGE implements the statistical framework of Klingenberg & McIntyre (1998) and Palmer & Strobeck (1986), for object symmetry and matching symmetry designs. All core statistics (Procrustes ANOVA, Lawley-Hotelling MANOVA, thin-plate spline decomposition, matrix correlations) are ported line-by-line from the original MATLAB SAGE suite.
- Installation
- Input file formats
- Launching the desktop GUI
- GUI walkthrough
- Launching the web interface
- Web interface walkthrough
- Headless Python API
- Worked example
- Output interpretation
Requirements: Python 3.11 or later.
SAGE ships two interfaces. Install only what you need:
| Interface | Extra | Additional dependencies |
|---|---|---|
| Desktop GUI | (included by default) | PySide6, Matplotlib |
| Web interface | [web] |
Dash, Plotly, dash-bootstrap-components |
# Desktop GUI only (default)
pip install sage-morpho
# Web interface only
pip install "sage-morpho[web]"
# Both interfaces
pip install "sage-morpho[web]" # GUI deps are always installed by defaultgit clone https://github.com/elmarqz/sage-python.git
cd sage-python
# Desktop GUI only
pip install -e .
# Desktop GUI + web interface
pip install -e ".[web]"
# Everything (GUI + web + dev tools)
pip install -e ".[web,dev]"Note: numpy, scipy, and matplotlib are installed from conda-forge; PySide6, Dash, and Plotly are installed via pip inside the environment.
Option A — one-step from the provided environment file:
# 1. Clone the repository
git clone https://github.com/elmarqz/sage-python.git
cd sage-python # ← must be inside the repo when running env create
# 2. Create and activate (use mamba for faster solves)
conda env create -f environment.yml # or: mamba env create -f environment.yml
conda activate sage-morpho
environment.ymlinstalls the desktop GUI only. To also include the web interface, runpip install -e ".[web]"after activating the environment.
environment.ymlinstalls the package in editable mode (-e .), so aftergit pullyou can just restartsageorsage-web— no reinstall needed. Thecd sage-pythonstep is required before runningconda env create.
Option B — manual step-by-step:
git clone https://github.com/elmarqz/sage-python.git
cd sage-python
conda create -n sage-morpho python=3.11 numpy scipy matplotlib -c conda-forge
conda activate sage-morpho
# Desktop GUI only:
pip install pyside6 .
# Desktop GUI + web interface:
pip install pyside6 ".[web]"For development (editable install + test/docs dependencies + web):
git clone https://github.com/elmarqz/sage-python.git
cd sage-python
conda env create -f environment-dev.yml
conda activate sage-morpho-devThe -e . entry in environment-dev.yml installs the package in editable
mode so changes to source files take effect immediately without reinstalling.
environment-dev.yml includes the web interface dependencies.
python -c "import sage; print('SAGE', sage.__version__)"One block per specimen. The SCALE= line is optional (defaults to 1.0).
ID= is optional but recommended for bookkeeping.
LM=8
256.000 490.000
185.000 310.000
327.000 310.000
120.000 260.000
392.000 260.000
140.000 430.000
372.000 430.000
256.000 100.000
SCALE=1.0
ID=ind01_rep1
One specimen per row; columns are X1 Y1 X2 Y2 … Xk Yk (space- or
tab-delimited). An optional trailing column is interpreted as centroid size.
File extensions recognised: .dat, .tab, .txt, .prn.
Plain text, one integer per line.
| Protocol | Content |
|---|---|
| Individual IDs | One entry per specimen row. Repeated IDs = replicates. |
| Landmark pairing (object sym) | Length-k vector (1-based). Entry i = index of landmark paired with landmark i. Midline landmarks pair with themselves. |
| Side indicator (matching sym) | One entry per specimen row. 0 = right, 1 = left. |
After installation, start the desktop application from a terminal:
sageOr from Python:
from sage.gui.main_window import main
main()┌─────────────────────────────────────────────────────────┐
│ File Help │
├──────────────┬──────────────────────────────────────────┤
│ Data │ │
│ [Browse…] │ Plot area (upper) │
│ ☐ Ruler │ Procrustes scatter / PCA scores │
├──────────────┤ │
│ Protocol │ Plot area (lower) │
│ ● Object sym │ TPS deformation grid │
│ ○ Match sym ├──────────────────────────────────────────┤
│ [Load IDs…] │ Results │
│ [Load pairs] │ ┌──────────┬──────────┬──────────┐ │
├──────────────┤ │ ANOVA │ MANOVA │ Corr │ │
│ Analysis │ └──────────┴──────────┴──────────┘ │
│ ☑ ANOVA │ │
│ ☑ MANOVA │ PCA source ▾ PC x ▾ PC y ▾ │
│ ☑ PCA │ Variance: PC1=38.6% PC2=23.6% │
│ ☐ Mxcorr │ [Save scores] │
│ [Run] │ │
└──────────────┴──────────────────────────────────────────┘
Click Browse… in the Data panel and select a TPS, DAT, TAB, TXT, or PRN file. The status bar reports how many specimens and landmarks were loaded.
If your file includes a ruler landmark pair, tick Use Ruler, enter the two landmark numbers, and set the known distance. The data will be scaled before analysis.
Choose Object symmetry or Matching symmetry with the radio buttons, then click the two load buttons in order:
- Load IDs… — individual identification protocol.
- Load pairs… (object sym) — landmark pairing protocol. Load sides… (matching sym) — side indicator protocol.
A green status message confirms the protocols are consistent with the data.
Tick the analyses to run:
| Checkbox | What it computes |
|---|---|
| ANOVA | Procrustes ANOVA: SS, df, MS, F, p for Individuals, Sides (DA), Ind×Sides (FA), and Measurement error |
| MANOVA | Lawley-Hotelling trace T, F-approximation, and p for multivariate effects |
| PCA | Principal components of each variance-component covariance matrix |
| Matrix corr | Pearson r + Mantel permutation test between selected covariance matrices |
Enable Permutation tests and set the number of iterations for non-parametric p-values (slower but assumption-free).
Click Run Analysis. Results appear in the tabbed panel on the right:
- ANOVA tab — effect table with significance codes (
ns,*…******). - MANOVA tab — Lawley-Hotelling T, F, and p for each multivariate effect.
- Matrix Corr tab — Pearson r and Mantel p for each matrix pair.
Use the PCA source dropdown to choose a variance component (e.g., Fluctuating asymmetry (FA)), then set the PC axes to display. The upper plot shows a score scatter; the lower plot shows the corresponding TPS deformation grid.
Click Save scores to export PC scores for a selected component.
Use the File menu to:
- Save results — write a formatted plain-text summary.
- Export coordinates — save Procrustes-aligned data as TPS or XY.
The web interface requires the [web] optional dependencies (Dash, Plotly,
dash-bootstrap-components). Install them if you haven't already:
pip install -e ".[web]" # from source
# or
pip install "sage-morpho[web]" # from PyPIThen start the server:
sage-webThe browser opens automatically at http://localhost:8050. The server runs
locally — no internet connection is required and no data leaves your machine.
Or from Python:
from sage.web.app import main
main()┌─────────────────────────────────────────────────────────────┐
│ SAGE — Symmetry and Asymmetry in Geometric data [navbar] │
├──────────────┬──────────────────────────────────────────────┤
│ Data │ │
│ [Drop TPS…] │ Plot area (upper) │
│ │ Procrustes scatter / PCA scores │
│ Protocol │ (interactive: pan, zoom, hover, SVG export) │
│ ● Object sym │ │
│ ○ Match sym │ Plot area (lower) │
│ [Drop IDs…] │ TPS deformation grid │
│ [Drop pairs] │ │
├──────────────┼──────────────────────────────────────────────┤
│ Analysis │ ANOVA │ MANOVA │ Matrix Corr (tabs) │
│ ☑ ANOVA │ │
│ ☑ MANOVA │ PCA source ▾ PC X ▾ PC Y ▾ │
│ ☑ PCA │ 38.6% variance │
│ ☐ Mxcorr │ [Download scores] [Download results] │
│ Perms: 0 │ │
│ [Run] │ │
└──────────────┴──────────────────────────────────────────────┘
The web interface provides the same analyses as the desktop GUI with two key differences: all plots are interactive (Plotly), and files are loaded via drag-and-drop upload rather than a file-picker dialog.
Drag a TPS, DAT, TAB, TXT, or PRN file onto the Data upload zone (or click it to browse). A status message confirms how many specimens and landmarks were loaded.
For ruler-scaled data, click Ruler scaling ▾ to expand the ruler controls, enter the two landmark indices and known distance, then click Apply ruler.
Select Object symmetry or Matching symmetry, then upload:
- Individual IDs protocol — drag onto the IDs upload zone.
- Landmark pairs (object sym) or Side indicator (matching sym) — drag onto the second upload zone.
Tick the analyses to run (ANOVA, MANOVA, PCA, Matrix correlations), optionally set a permutation count, then click Run Analysis.
Results populate the three tabs (ANOVA, MANOVA, Matrix Corr) and the PCA controls below the plots. Use the PCA source and PC X / PC Y dropdowns to navigate components. The upper plot shows the score scatter; the lower plot shows the TPS deformation grid for the selected PC.
Every plot has a toolbar (top-right on hover) with pan, zoom, and Download as SVG buttons.
- Download scores — exports the PCA scores for the selected source as a tab-delimited text file.
- Download results — exports the full ANOVA/MANOVA/matrix correlation summary as a plain-text file.
All analyses run without the GUI. The two entry points are
object_symmetry() and matching_symmetry() in sage.analysis.symmetry.
import numpy as np
from sage.io.readers import read_tps, tps_to_array, read_protocol
from sage.analysis.symmetry import object_symmetry
# Load data
specimens = read_tps("my_data.tps")
data, ids = tps_to_array(specimens) # (n, 2k) float array
ind_ids = read_protocol("ind_ids.txt") # (n,) int array
lm_pairs = read_protocol("lm_pairs.txt") # (k,) int array (1-based)
# Run full pipeline
result = object_symmetry(
data=data,
ind_protocol=ind_ids,
pair_protocol=lm_pairs,
run_anova=True,
run_manova=True,
run_pca=True,
)
# ANOVA table
for eff in result.anova.effects:
print(f"{eff.name:<25} F={eff.F:8.4f} p={eff.p:.4f} {eff.sig}")
# PC scores for the FA component
fa_pca = result.pca["Fluctuating asymmetry (FA)"]
print(f"FA PC1 explains {fa_pca.variance_pct[0]:.1f}% of FA variance")The examples/toy_dataset/ directory contains a synthetic dataset modelling
a bilateral insect head measured under a stereomicroscope:
examples/toy_dataset/
insect_head.tps # 30 rows: 15 individuals × 2 replicates, 8 landmarks
individual_ids.txt # individual ID per specimen (1–15, each repeated twice)
landmark_pairs.txt # landmark pairing for object symmetry
LM2
(posterior vertex)
LM5 LM3 · LM4 LM6
(outer) (inner) (inner) (outer)
eye corners
LM7 LM8
(left mandible) (right mandible)
LM1
(anterior clypeus)
| LM | Structure | Pairs with |
|---|---|---|
| 1 | Anterior clypeus (midline) | self |
| 2 | Posterior vertex (midline) | self |
| 3 | Left inner-eye corner | 4 |
| 4 | Right inner-eye corner | 3 |
| 5 | Left outer-eye corner | 6 |
| 6 | Right outer-eye corner | 5 |
| 7 | Left mandible tip | 8 |
| 8 | Right mandible tip | 7 |
Pairing protocol (landmark_pairs.txt): 1 2 4 3 6 5 8 7
(entry i = paired counterpart of landmark i, 1-based)
sage- Browse… → select
examples/toy_dataset/insect_head.tps - Choose Object symmetry
- Load IDs… →
individual_ids.txt - Load pairs… →
landmark_pairs.txt - Tick ANOVA, MANOVA, PCA → Run Analysis
sage-web(browser opens athttp://localhost:8050)- Drop
insect_head.tpsonto the Data upload zone - Select Object symmetry
- Drop
individual_ids.txtonto the IDs upload zone - Drop
landmark_pairs.txtonto the pairs upload zone - Tick ANOVA, MANOVA, PCA → Run Analysis
from sage.io.readers import read_tps, tps_to_array, read_protocol
from sage.analysis.symmetry import object_symmetry
from sage.io.writers import write_results
# ── Load ──────────────────────────────────────────────────────────
specimens = read_tps("examples/toy_dataset/insect_head.tps")
data, _ = tps_to_array(specimens)
ind = read_protocol("examples/toy_dataset/individual_ids.txt")
pairs = read_protocol("examples/toy_dataset/landmark_pairs.txt")
# ── Analyse ───────────────────────────────────────────────────────
result = object_symmetry(data, ind, pairs,
run_anova=True, run_manova=True, run_pca=True)
# ── ANOVA table ───────────────────────────────────────────────────
print(f"{'Effect':<25} {'SS':>10} {'df':>5} {'MS':>10} {'F':>9} {'p':>9} sig")
print("-" * 76)
for eff in result.anova.effects:
f_s = f"{eff.F:9.4f}" if eff.F != -999 else " —"
p_s = f"{eff.p:9.4f}" if eff.p != -999 else " —"
print(f"{eff.name:<25} {eff.ss:10.5f} {int(eff.df):5d} "
f"{eff.ms:10.6f} {f_s} {p_s} {eff.sig}")
# ── MANOVA ────────────────────────────────────────────────────────
print()
for eff in result.manova.effects:
if eff.df1 < 0:
print(f" {eff.name}: ill-conditioned (too few individuals for landmark count)")
else:
print(f" {eff.name}: T={eff.trace:.4f}, "
f"F({int(eff.df1)}, {eff.df2:.1f})={eff.F:.4f}, "
f"p={eff.p:.4f} {eff.sig}")
# ── PCA ───────────────────────────────────────────────────────────
print()
for label, pca in result.pca.items():
top3 = ", ".join(
f"PC{i+1}={pca.variance_pct[i]:.1f}%"
for i in range(min(3, len(pca.variance_pct)))
)
print(f" {label}: {top3}")
# ── Save ──────────────────────────────────────────────────────────
write_results("insect_head_results.txt",
anova=result.anova, manova=result.manova)
print("\nResults written to insect_head_results.txt")Run it:
python examples/object_symmetry.py
# or, for matching symmetry:
python examples/matching_symmetry.pyEffect SS df MS F p sig
----------------------------------------------------------------------------
Individuals 0.19708 84 0.002346 0.8213 0.8155 ns
Sides 0.01292 6 0.002154 0.7540 0.6080 ns
Individuals x Sides 0.23995 84 0.002857 63.4784 0.0000 ******
Measurement error 0.00810 180 0.000045 — — --
Sides: T=41.71, F(36, 218.0)=1.0719, p=0.3687 ns
Individuals x Sides: T=50023.89, F(504, 431.2)=91.9605, p=0.0000 ******
| Effect | Biological meaning |
|---|---|
| Individuals | Among-individual shape variation (overall size-corrected shape differences) |
| Sides | Directional asymmetry (DA) — consistent left/right difference across all individuals |
| Individuals × Sides | Fluctuating asymmetry (FA) — random, individual-specific left/right differences; the primary measure of developmental instability |
| Measurement error | Repeatability of landmark placement; should be small relative to FA |
Significance codes: ns p > 0.05 · * ≤ 0.05 · ** ≤ 0.01 · *** ≤ 0.005 · **** ≤ 0.001 · ***** ≤ 0.0005 · ****** ≤ 0.0001
A significant Individuals × Sides (FA) effect with a non-significant Sides (DA) effect is the ideal outcome for a fluctuating-asymmetry study. A significant DA effect does not invalidate the analysis but should be noted and discussed in terms of functional or developmental causes.
The FA variance components (column 4 of the variance-components table) provide per-landmark estimates of FA magnitude; comparing them across landmarks can pinpoint which regions of the structure are most developmentally unstable.
# All tests
pytest tests/
# Single module
pytest tests/test_flapanova.py -v
# With coverage
pytest tests/ --cov=sage --cov-report=term-missingpip install pyinstaller
pyinstaller build/sage.spec
# Output: dist/SAGE.app (macOS)
# dist/SAGE/ (Windows/Linux)- Palmer A.R. & Strobeck C. (1986) Fluctuating asymmetry: measurement, analysis, patterns. Annual Review of Ecology and Systematics 17, 391–421.
- Klingenberg C.P. & McIntyre G.S. (1998) Geometric morphometrics of developmental instability: analysing patterns of fluctuating asymmetry with Procrustes methods. Evolution 52, 1363–1375.