Skip to main content

Fit the curve. See the athlete. The intensity-duration modelling toolkit for endurance sports. Scikit-learn compatible.

Project description

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.

Project details


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.

Supported by

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