Skip to content

elmarqz/sage-python

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

22 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SAGE-Python

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.


Table of Contents

  1. Installation
  2. Input file formats
  3. Launching the desktop GUI
  4. GUI walkthrough
  5. Launching the web interface
  6. Web interface walkthrough
  7. Headless Python API
  8. Worked example
  9. Output interpretation

Installation

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

From PyPI

# 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 default

From source (development)

git 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]"

Using conda / mamba

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.yml installs the desktop GUI only. To also include the web interface, run pip install -e ".[web]" after activating the environment.

environment.yml installs the package in editable mode (-e .), so after git pull you can just restart sage or sage-web — no reinstall needed. The cd sage-python step is required before running conda 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-dev

The -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.

Verify

python -c "import sage; print('SAGE', sage.__version__)"

Input file formats

Landmark data — TPS format

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

Landmark data — delimited XY format

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.

Protocol files

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.

Launching the desktop GUI

After installation, start the desktop application from a terminal:

sage

Or from Python:

from sage.gui.main_window import main
main()

GUI walkthrough

┌─────────────────────────────────────────────────────────┐
│  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]        │                                          │
└──────────────┴──────────────────────────────────────────┘

Step 1 — Load data

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.

Step 2 — Load protocols

Choose Object symmetry or Matching symmetry with the radio buttons, then click the two load buttons in order:

  1. Load IDs… — individual identification protocol.
  2. 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.

Step 3 — Configure analyses

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).

Step 4 — Run and explore results

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.

Step 5 — Export

Use the File menu to:

  • Save results — write a formatted plain-text summary.
  • Export coordinates — save Procrustes-aligned data as TPS or XY.

Launching the web interface

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 PyPI

Then start the server:

sage-web

The 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()

Web interface walkthrough

┌─────────────────────────────────────────────────────────────┐
│  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.

Step 1 — Upload data

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.

Step 2 — Upload protocols

Select Object symmetry or Matching symmetry, then upload:

  1. Individual IDs protocol — drag onto the IDs upload zone.
  2. Landmark pairs (object sym) or Side indicator (matching sym) — drag onto the second upload zone.

Step 3 — Configure and run

Tick the analyses to run (ANOVA, MANOVA, PCA, Matrix correlations), optionally set a permutation count, then click Run Analysis.

Step 4 — Explore results

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.

Step 5 — Download

  • 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.

Headless Python API

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")

Worked example

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

Landmark scheme

              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)

Run via the desktop GUI

  1. sage
  2. Browse… → select examples/toy_dataset/insect_head.tps
  3. Choose Object symmetry
  4. Load IDs…individual_ids.txt
  5. Load pairs…landmark_pairs.txt
  6. Tick ANOVA, MANOVA, PCARun Analysis

Run via the web interface

  1. sage-web (browser opens at http://localhost:8050)
  2. Drop insect_head.tps onto the Data upload zone
  3. Select Object symmetry
  4. Drop individual_ids.txt onto the IDs upload zone
  5. Drop landmark_pairs.txt onto the pairs upload zone
  6. Tick ANOVA, MANOVA, PCARun Analysis

Run via the Python API

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.py

Expected output

Effect                    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  ******

Output interpretation

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.


Running tests

# All tests
pytest tests/

# Single module
pytest tests/test_flapanova.py -v

# With coverage
pytest tests/ --cov=sage --cov-report=term-missing

Building a standalone executable

pip install pyinstaller
pyinstaller build/sage.spec
# Output: dist/SAGE.app  (macOS)
#         dist/SAGE/     (Windows/Linux)

References

  • 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.

About

Python implementation of SAGE (Symmetry and Asymmetry in Geometric Data)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages