Silhouette
Fit the curve. See the athlete. The intensity-duration modelling toolkit for endurance sports. Scikit-learn compatible.
Try the interactive playground 🚀
Models
Power (cycling)
| Model | Parameters |
|---|---|
TwoParamCriticalPowerRegressor |
CP, W' |
ThreeParamCriticalPowerRegressor |
CP, W', P_max |
OmniDomainPowerRegressor |
CP, W', P_max, a, tcp_max |
ExpPowerRegressor |
CP, P_max, tau |
MinimalPowerPowerRegressor ⚠️ experimental |
MAP, MAP duration, gamma_l, gamma_s |
FPCAPowerRegressor |
FPC1, FPC2, FPC3 |
VDOTPowerRegressor ⚠️ experimental |
VDOT |
Speed (running)
| Model | Parameters |
|---|---|
TwoParamCriticalSpeedRegressor |
CS, D' |
ThreeParamCriticalSpeedRegressor |
CS, D', S_max |
ExpSpeedRegressor ⚠️ experimental |
CS, S_max, tau |
OmniDomainSpeedRegressor ⚠️ experimental |
CS, D', S_max, a, tcp_max |
MinimalPowerSpeedRegressor |
MAS, MAS duration, gamma_l, gamma_s |
VDOTSpeedRegressor |
VDOT |
Installation
uv add silhouette
Or with pip:
pip install silhouette
Quick start
Power models (cycling)
import numpy as np
from silhouette import OmniDomainPowerRegressor
durations = np.array([5, 10, 30, 60, 120, 300, 600, 1200, 1800, 3600])
power = np.array([1050, 850, 600, 480, 400, 340, 310, 290, 275, 255])
reg = OmniDomainPowerRegressor()
reg.fit(durations.reshape(-1, 1), power)
reg.cp_ # critical power (W)
reg.p_max_ # peak power (W)
reg.w_prime_ # anaerobic work capacity (J)
reg.predict(np.array([[300]])) # predicted power at 5 minutes
All parametric models share the same interface. Swap OmniDomainPowerRegressor for TwoParamCriticalPowerRegressor or ThreeParamCriticalPowerRegressor and the code works the same way.
Speed models (running)
from silhouette import TwoParamCriticalSpeedRegressor
durations = np.array([120, 180, 300, 600, 900])
speed = np.array([5.8, 5.4, 5.0, 4.6, 4.4])
reg = TwoParamCriticalSpeedRegressor()
reg.fit(durations.reshape(-1, 1), speed)
reg.cs_ # critical speed (m/s)
reg.d_prime_ # distance capacity above CS (m)
Speed models use the same formulas as their power counterparts, with domain-appropriate parameter names, bounds, and defaults.
VDOT model (running)
from silhouette import VDOTSpeedRegressor
durations = np.array([180, 300, 600, 900, 1800, 3600])
speed = np.array([5.5, 5.2, 4.8, 4.6, 4.2, 3.9])
reg = VDOTSpeedRegressor()
reg.fit(durations.reshape(-1, 1), speed)
reg.vdot_ # VDOT fitness value (ml/kg/min)
The VDOT model (Daniels & Gilbert, 1979) is a single-parameter model that predicts performance across durations. Designed for 3 minutes to 2 hours. The speed variant is the original running model. The power variant is an experimental adaptation using 11.7 mL O2/W and requires body_mass:
from silhouette import VDOTPowerRegressor
reg = VDOTPowerRegressor(body_mass=75)
reg.fit(durations.reshape(-1, 1), power)
reg.vdot_ # VDOT fitness value (ml/kg/min)
FPCA model
from silhouette import FPCAPowerRegressor
reg = FPCAPowerRegressor.from_model()
reg.fit(durations.reshape(-1, 1), power)
reg.fpc1_ # overall power level
reg.fpc2_ # sprint vs endurance bias
reg.fpc3_ # mid-duration specialization
reg.predict(np.array([[300]]))
reg.percentiles() # {"fpc1": 72.3, "fpc2": 34.1, "fpc3": 55.8}
reg.z_scores() # {"fpc1": 0.87, "fpc2": -0.41, "fpc3": 0.14}
Known parameters
When parameters are already known, use curve directly without fitting:
from silhouette import TwoParamCriticalPowerRegressor, TwoParamCriticalSpeedRegressor
t = np.arange(1, 3601)
power = TwoParamCriticalPowerRegressor.curve(t, cp=250, w_prime=20_000)
speed = TwoParamCriticalSpeedRegressor.curve(t, cs=4, d_prime=200)
Duration range
Each model is designed for a specific duration range:
| Model | Recommended range |
|---|---|
| Two-parameter | 2-15 min |
| Three-parameter | up to 15 min |
| Exponential | up to 15 min |
| Omni-domain | any |
| Minimal power | 1 min+ |
| FPCA | any |
| VDOT | 3 min – 2 hours |
A warning is issued when data falls outside the recommended range. Use duration_range to restrict which data points are used for fitting:
reg = TwoParamCriticalPowerRegressor(duration_range=(120, 900))
reg.fit(X, power) # only uses data between 2 and 15 minutes
reg.predict(X) # predict still works at any duration
reg.duration_mask_ # boolean mask of which points were used
Custom bounds
reg = OmniDomainPowerRegressor(
bounds={"cp": (200, 400), "p_max": (800, 1500)},
initial_params={"cp": 280},
)
Fitting methods
The two-parameter models support an alternative fitting method that minimizes error in work/distance space instead of power/speed space:
reg = TwoParamCriticalPowerRegressor(fitting="work_duration")
reg.fit(X, power)
This linearizes the model to W = W' + CP·t and fits via OLS, giving more weight to longer durations. The default (fitting="nonlinear") minimizes error in power space.
Time to exhaustion
The inverse of the power-duration curve: given a power, how long can it be sustained?
# On a fitted model
tte = reg.predict_inverse(np.array([250, 300, 350]))
# With known parameters
tte = TwoParamCriticalPowerRegressor.curve_inverse(350, cp=250, w_prime=20_000)
Plotting
Install with plotting support:
uv add silhouette[plotting]
Plot data with fitted models (sklearn Display pattern):
from silhouette.plotting import PowerDurationDisplay
# Single model
display = PowerDurationDisplay.from_estimator(reg, durations.reshape(-1, 1), power)
# Compare models
display = PowerDurationDisplay.from_estimators(
[reg_2p, reg_omni], durations.reshape(-1, 1), power,
)
FPCA mode of variance:
from silhouette.plotting import ModeOfVarianceDisplay
display = ModeOfVarianceDisplay.from_estimator(fpca_reg)
Minimal power model (normalized coordinates with reference band):
from silhouette.plotting import MinimalPowerDisplay
display = MinimalPowerDisplay.from_estimator(reg_minimal, durations.reshape(-1, 1), power)
References
- Monod, H., & Scherrer, J. (1965). The work capacity of a synergic muscular group. Ergonomics, 8(3), 329-338.
- Hopkins, W. G., Edmond, I. M., Hamilton, B. H., Macfarlane, D. J., & Ross, B. H. (1989). Relation between power and endurance for treadmill running of short duration. Ergonomics, 32(12), 1565-1571.
- Morton, R. H. (1996). A 3-parameter critical power model. Ergonomics, 39(4), 611-619.
- Mulligan, M., Adam, G., & Emig, T. (2018). A minimal power model for human running performance. PloS one, 13(11), e0206645.
- Puchowicz, M. J., Baker, J., & Clarke, D. C. (2020). Development and field validation of an omni-domain power-duration model. Journal of Sports Sciences, 38(7), 801-813.
- Daniels, J., & Gilbert, J. (1979). Oxygen Power: Performance Tables for Distance Runners. Tempe, AZ.
- Puchowicz, M. J., & Skiba, P. F. (2025). Functional Data Analysis of the Power-Duration Relationship in Cyclists. International Journal of Sports Physiology and Performance, 1(aop), 1-10.
Release files for silhouette 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| silhouette-0.6.0.tar.gz | 72.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| silhouette-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 148.2 kB
Release files / silhouette-0.6.0.tar.gz
| Download URL | silhouette-0.6.0.tar.gz |
|---|---|
| Size | 72.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6856e617ad35bebd6a0d1538a8b522bfd61479110ff05f71c283473c66abd77c
|
|
BLAKE2b-256 checksum How to use checksums |
852f5f2e9af0c2124ded3a4fd68c6919ef1e5cb7e3aa2b7961fdecb347028693
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.10
|
Release files / silhouette-0.6.0-py3-none-any.whl
| Download URL | silhouette-0.6.0-py3-none-any.whl |
|---|---|
| Size | 76.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
700762e6d0f0cf4238af95e42ba8056e5a98c3ab1de5f0f87dd8f46413fc2e10
|
|
BLAKE2b-256 checksum How to use checksums |
5b048bdf67f944dc003e637010742acd0749d78c20770014fc0c5bbc78702b35
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.10.10
|