Effort¶
Continuous indices for the qualities of movement, after Rudolf Laban.
What Laban's Effort is¶
Laban Movement Analysis describes movement through four elements—body, space, shape, and effort—and effort is the one about quality: not what moves or where, but how. A movement can cover the same trajectory hurried or calm, grounded or weightless, and effort names that difference. It is an observational system, approached—as the choreographer Andre Austvold taught it in Oslo—by looking at the intention of the movement, "placing yourself within the body of your subject".
Effort has four motion factors, each a continuum between two poles (Laban & Lawrence, 1947; Hackney, 2002):
| factor | poles |
|---|---|
| Weight | light ↔ strong/firm |
| Time | sustained ↔ sudden |
| Space | indirect/flexible ↔ direct |
| Flow | free ↔ bound |
Two things about the system matter before any computation. Effort describes fluctuation, not level: the analysis is of a movement getting firmer or gentler over a phrase, so its natural output is a contour, and a single number for a whole recording flattens what the concept is about (Haga, 2008). And effort is the qualitative reading on top of an intensity contour: a quantity-of-motion curve says how much movement there is, and famously says it indiscriminately—a dancer moving their whole body reads high however the movement feels (Jensenius, 2007)—where effort says how the movement is performed.
What MGT computes¶
musicalgestures._effort is MGT's own operationalisation of the four factors—and
is named as such, because every computational version of an observational system is
somebody's operationalisation. Each function takes plain arrays and a sample rate,
so mocap speeds, pose trajectories, and quantity-of-motion tracks all qualify.
| factor | function | index | direction |
|---|---|---|---|
| Time | effort_time |
burst concentration of the speed profile | higher = more sudden |
| Weight | effort_weight |
peak (p95) acceleration | higher = stronger |
| Space | effort_space |
path directness per window | higher = more direct |
| Flow | effort_flow |
boundness, from spectral arc length | higher = more bound |
effort_profile computes all four in windows on one clock—the contour form the
concept asks for—and basic_effort_actions condenses each window into one of
Laban's eight basic effort actions (thrusting, slashing, pressing, wringing,
dabbing, flicking, gliding, floating): his own combinations of the Weight, Time
and Space poles, with Flow as a further colouring element outside the combination
(Laban, 1971; Haga, 2008). The poles are read against the mover's own medians, so
a label says which octant of this mover's range a window falls in. The labels
are proposals for looking with, never classifications.

import numpy as np
from musicalgestures._effort import effort_profile, basic_effort_actions
xy = np.load("trajectory.npy") # (frames, 2) positions, e.g. a wrist
profile = effort_profile(xy, fs=25.0, window_s=10.0)
actions = basic_effort_actions(profile)
skeleton_timeline draws the complementary picture—posture at sampled moments,
which the Effort indices deliberately discard:

What the indices can and cannot claim¶
Video sees kinematics—positions, velocities—and some of Laban's factors are about dynamics: the forces in the movement. Dynamics can only be inferred from kinematics (the kinematic-specification-of-dynamics hypothesis; Runeson & Frykholm, 1983), so a video-based Weight is a kinetic proxy for "strong", and the docstrings say so. The factors differ in evidential standing:
- Flow rests on SPARC (Balasubramanian et al., 2015), implemented from the paper and validated against the canonical minimum-jerk battery; its adaptive cutoff is what makes smoothness readable from tracked data at all, and the reason MGT reports no jerk-based smoothness from pose landmarks.
- Flow needs an adequate sample rate. Measured on a dance corpus, Flow computed from 5 fps pose disagreed entirely with the same section at 25 fps, while the other three factors agreed across two different pose detectors. Give SPARC 25 fps or better.
- Weight is the weakest claim, and comparable only within one recording and one mover: as a pixel-unit quantity it inherits every scale difference of its input.
- Time's direction is validated, its classification power measured and modest. On the 365-clip Sound Actions collection the index orders the label classes as the operationalisation predicts (impulsive median 3.52, sustained 3.01, iterative 2.69—lowest, since continuous repetition raises the mean and burst concentration is a ratio), but impulsive against sustained separates at only ROC AUC 0.645. Read the Time contour as description; it is not a classifier, and MGT makes no classification claim from it.
API reference¶
The MGT operationalisation of Laban's Effort, in continuous indices.
Laban's Effort is a qualitative observational system with four factors --- Weight,
Time, Space, Flow --- and every computational version of it is somebody's
operationalisation. This is MGT's, named as such, designed in
plans/2026-08-30-effort-layer-design.md and validated in tests/test_effort.py
before any docstring made a claim. Each function returns a continuous index with a
documented direction, never a category shipped as fact.
Input-agnostic on purpose: the functions take arrays and a sample rate, so mocap speeds, pose-trajectory speeds and quantity-of-motion tracks all qualify. What the index then describes is the mover the array describes --- a full-body QoM track yields ensemble Effort, one wrist yields that wrist's.
The factors differ in evidential standing, and their docstrings say so. Flow rests on SPARC, which carries the strongest evidence; Weight is the weakest claim --- "strong" without mass or force plates is a kinetic proxy --- and is labelled as one. That proxy status has a scholarly frame: dynamics are inferred from kinematics (the kinematic-specification-of-dynamics hypothesis, Runeson & Frykholm 1983), and Laban's factors mix the two --- Time is kinematical, Weight and Flow dynamical --- so a video-based Effort layer necessarily reads dynamics through what it can see (Haga 2008, ch. 4).
Two further points from Haga (2008) shape how these indices are meant to be read.
Effort elements denote fluctuation, not level --- "gentler and firmer", the way the
qualities change over a phrase --- so the windowed effort_profile contours are the
object of analysis, and a single number for a whole recording flattens what the
concept is about. And effort is the qualitative reading on top of a neutral
intensity contour (Stern's activation, which is what a quantity-of-motion track
measures): QoM says how much, Effort says how.
sparc ¶
sparc(speed, fs, padlevel=4, fc=10.0, amp_th=0.05)
Spectral arc length: movement smoothness from the speed profile's spectrum.
Implemented from Balasubramanian, Melendez-Calderon, Roby-Brami & Burdet (2015), On the analysis of movement smoothness: the negative arc length of the normalised Fourier magnitude spectrum below an adaptive cutoff. Values are negative, and closer to zero is smoother: a single minimum-jerk reach measures about -1.45 here, three chained submovements about -2.6, and the test suite holds both.
The adaptive cutoff is the reason to prefer SPARC over jerk from tracked data: under 10 per cent added tracker noise SPARC stays within a few per cent of its clean value while still separating submovement count, where jerk RMS cannot tell that noise from genuinely tripled submovements. Measured in the test suite, and the reason MGT reports no jerk-based smoothness from pose data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
speed
|
One-dimensional speed profile (non-negative magnitudes). |
required | |
fs
|
float
|
Sample rate of the profile in Hz. |
required |
padlevel
|
int
|
Zero-padding exponent for the FFT. Defaults to 4. |
4
|
fc
|
float
|
Maximum frequency of interest in Hz. Defaults to 10. |
10.0
|
amp_th
|
float
|
Amplitude threshold of the adaptive cutoff. Defaults to 0.05. |
0.05
|
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
The spectral arc length; negative, closer to zero is smoother. |
float
|
NaN for a profile with no movement. |
Source code in musicalgestures/_effort.py
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 | |
effort_time ¶
effort_time(speed, fs)
The Time factor: sudden against sustained, as burst concentration.
The MGT operationalisation of Laban's Time. A sudden mover concentrates their
movement into brief peaks; a sustained one spreads it evenly. The index is the
mean speed of the fastest tenth of samples over the mean speed of all of them:
1.0 for perfectly sustained movement, rising without bound as movement
concentrates into bursts. fs is accepted for interface symmetry; the index is
rate-invariant.
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Burst concentration; higher is more sudden. NaN with no movement. |
Source code in musicalgestures/_effort.py
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 | |
effort_weight ¶
effort_weight(speed, fs)
The Weight factor: strong against light, as peak acceleration.
The weakest claim of the four, and labelled as one: without mass or force plates, "strong" from video is a kinetic proxy. The index is the 95th percentile of the absolute acceleration of the speed profile, in the profile's units per second squared --- a strong mover changes speed hard, a light one gently. Comparable within one recording and one mover; across recordings it inherits every scale difference of the input.
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Peak (p95) absolute acceleration; higher is stronger. NaN with |
float
|
fewer than 3 samples. |
Source code in musicalgestures/_effort.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | |
effort_space ¶
effort_space(xy, fs, window_s=5.0)
The Space factor: direct against indirect, as windowed path directness.
The MGT operationalisation of Laban's Space, on a substrate with a long history: per window, the straight-line displacement over the path length actually travelled. A dead-straight path scores 1; a path that wanders scores toward 0. Windows with no movement are NaN, not a guess.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xy
|
Positions of shape (frames, 2). |
required | |
fs
|
float
|
Sample rate in Hz. |
required |
window_s
|
float
|
Window length in seconds. Defaults to 5. |
5.0
|
Returns:
| Type | Description |
|---|---|
ndarray
|
np.ndarray: Directness in [0, 1] per full window. |
Source code in musicalgestures/_effort.py
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | |
effort_flow ¶
effort_flow(speed, fs)
The Flow factor: bound against free, as negated SPARC.
The MGT operationalisation of Laban's Flow, on smoothness as its substrate: a
free mover's speed profile is smooth, a bound one's is held and corrected.
The index is -sparc(speed, fs), so higher is more bound; about 1.45 for a
single free reach, rising with every correction. SPARC's noise robustness
carries over, which is what makes this readable from tracked data at all.
Returns:
| Name | Type | Description |
|---|---|---|
float |
float
|
Boundness; higher is more bound. NaN with no movement. |
Source code in musicalgestures/_effort.py
161 162 163 164 165 166 167 168 169 170 171 172 173 | |
effort_profile ¶
effort_profile(xy_or_speed, fs, window_s=5.0)
All four factors, windowed onto one clock.
Given positions of shape (frames, 2), speed is their frame-to-frame displacement rate and Space is computable; given a one-dimensional speed or quantity-of-motion track, Space is NaN throughout, since directness needs a path. Each factor is computed per full window; a trailing partial window is dropped rather than guessed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
xy_or_speed
|
(frames, 2) positions, or a (frames,) speed track. |
required | |
fs
|
float
|
Sample rate in Hz. |
required |
window_s
|
float
|
Window length in seconds. Defaults to 5. |
5.0
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict
|
|
dict
|
|
|
dict
|
per window, NaN where a factor cannot be measured. |
Source code in musicalgestures/_effort.py
176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 | |
basic_effort_actions ¶
basic_effort_actions(profile)
Laban's eight basic effort actions, per window, as proposals.
Laban condensed the Weight, Time and Space poles into eight named actions --- thrusting, slashing, pressing, wringing, dabbing, flicking, gliding, floating --- with Flow as a further colouring element outside the combination (Laban 1971, via Haga 2008, Table 20, whose derivative verbs --- shove, pat, crush, beat, strew, pull, flip, smooth --- give the register these labels live in).
Each pole is read against the mover's own median over the profile, in the house style of median-anchored thresholds: "firm" means firmer than this mover's typical window, so the labels describe one mover's range and never compare two movers. They are proposals for looking with, not classifications --- Laban's categories are observational, and a window's label says which octant of this mover's own space it falls in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
profile
|
dict
|
As from :func: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
list |
list
|
One of the eight action names per window, or None where any of the |
list
|
three indices is NaN. |
Source code in musicalgestures/_effort.py
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 | |