Skip to main content

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

Power-Duration Models

FPCA mode of variance:

from silhouette.plotting import ModeOfVarianceDisplay

display = ModeOfVarianceDisplay.from_estimator(fpca_reg)

Mode of Variance

Minimal power model (normalized coordinates with reference band):

from silhouette.plotting import MinimalPowerDisplay

display = MinimalPowerDisplay.from_estimator(reg_minimal, durations.reshape(-1, 1), power)

Minimal Power Model

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

silhouette-0.6.0.tar.gz (72.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

silhouette-0.6.0-py3-none-any.whl (76.2 kB view details)

Uploaded Python 3

File details

Details for the file silhouette-0.6.0.tar.gz.

File metadata

  • Download URL: silhouette-0.6.0.tar.gz
  • Upload date:
  • Size: 72.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.10

File hashes

Hashes for silhouette-0.6.0.tar.gz
Algorithm Hash digest
SHA256 6856e617ad35bebd6a0d1538a8b522bfd61479110ff05f71c283473c66abd77c
MD5 e289715f202c5c7f10e0a26c9665e8de
BLAKE2b-256 852f5f2e9af0c2124ded3a4fd68c6919ef1e5cb7e3aa2b7961fdecb347028693

See more details on using hashes here.

File details

Details for the file silhouette-0.6.0-py3-none-any.whl.

File metadata

  • Download URL: silhouette-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 76.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.10

File hashes

Hashes for silhouette-0.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 700762e6d0f0cf4238af95e42ba8056e5a98c3ab1de5f0f87dd8f46413fc2e10
MD5 6349492a066040879efa59fb3f4dfabe
BLAKE2b-256 5b048bdf67f944dc003e637010742acd0749d78c20770014fc0c5bbc78702b35

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.6.0 This release

2 files

0.5.0

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page