plateforce
Force-plate analysis where a result carries the method that produced it. You pick a published method from the registry.
pip install plateforce
The machine that installs it needs no compiler and no Rust toolchain. One abi3 wheel per platform covers Python 3.11 and every later version, and the method registry travels inside the wheel, so the digest a result reports names the same bytes on every machine that installed the same version.
plateforce is not forceplate. The similarly named CRAN package, by Hartmann, Koger
and Johannsen, analyses posturography: centre-of-pressure measures from quiet standing.
This one computes jump kinetics from a vertical ground reaction force trace. Neither is a
port of the other.
Analysing one jump
import numpy as np
import plateforce as pf
registry = pf.Registry.load()
force_newtons = np.loadtxt("trial.csv") # vertical ground reaction force, newtons
trial = pf.Trial(force_newtons, sample_rate_hz=1200.0)
jump = pf.analyse_countermovement_jump(
trial,
weighing_epoch=registry.method("bwepoch.fixed_window").bind(duration=1.0),
onset=registry.method("onset.threshold.noise_relative").bind(k=5.0),
takeoff=registry.method("takeoff.threshold.absolute_force").bind(threshold_n=20.0),
)
print(jump.jump_height_takeoff_frame_meters.describe())
0.4105176602724294 meters
jumpheight.takeoff.impulse_momentum {'gravity_meters_per_second_squared': 9.80665}
integration_anchor = integration.anchor.single_point
integration_direction = integration.direction.forward
integration_rule = integration.rule.trapezoid
integration_start = integration.start.detected_onset
registry declaring 2026-07-25 (content-e613e95011150591)
filter.none {}
passband_edge = none
bwepoch.fixed_window {'duration': 1, 'start_seconds': 0}
centre = mean
dispersion = sample
window_anchor = trial_start
onset.threshold.noise_relative {'k': 5}
degenerate_band = refuse
reference_distribution = quiet_stance_force
sd_convention = sample
onset.op.backward_offset_fixed {'offset_ms': 30}
onset.op.crossing_selection {}
selection = first
onset.op.direction {}
direction = below_only
onset.op.persistence {'span_ms': 30}
onset.op.search_floor_at_weighing_epoch_end {'weighing_epoch_end_seconds': 1}
takeoff.threshold.absolute_force {'persistence_ms': 0, 'threshold_n': 20}
takeoff.op.crossing_selection {}
selection = first
takeoff.op.residual_comparison {}
comparison = signed
takeoff.op.search_floor_at_weighing_epoch_end {'weighing_epoch_end_seconds': 1}
acquisition block incomplete, so this result cannot be declared to match another lab's
Everything under the number is what a second lab needs to reproduce it. The quiet-standing window appears three times because it moved the answer three times: as system weight, as the noise scale that placed onset, and through the impulse that produced the velocity.
Why the number carries all that
Two independent open-source implementations of the same published methods, run over the same 244 countermovement jumps, agree at r = 0.961 on jump height and r = 0.696 on time to takeoff. Both were tested. Both passed their own tests.
Across those trials, the spread between published methods within a single trial is a median 3.51 cm of jump height. The training intervention that dataset was collected to measure moved jump height by 1.98 cm.
A float cannot tell you which of nine published onset rules produced it, so two labs comparing floats have no way to find out they were never measuring the same thing.
A result is not a float
>>> jump.jump_height_takeoff_frame_meters
Measured(value=0.3419695652891413, unit='meters', method_id='jumpheight.takeoff.impulse_momentum')
>>> jump.jump_height_takeoff_frame_meters + 0.01
TypeError: unsupported operand type(s) for +: 'plateforce.Measured' and 'float'
The bare number is one attribute away, and asking for it is a visible act:
>>> jump.jump_height_takeoff_frame_meters.value
0.3419695652891413
Each result also carries .unit and .provenance. To find the parameter that moved a
downstream number, ask the chain rather than walking it:
>>> jump.time_to_takeoff_seconds.provenance.parameters_of("onset.threshold.noise_relative")
{'k': 5.0}
Look before you choose
Some published rules do not disagree, they find the wrong event. Two onset rules in this corpus place the start of the movement more than two seconds before takeoff on roughly one trial in seven, on a movement lasting under a second, and their median behaviour looks ordinary while they do it.
>>> for entry in registry.methods_that_can_fail():
... print(entry)
MethodEntry('onset.op.backward_offset_fixed', status='accepted', implemented=False, FAILS on 36 of 241 trials (14.9%, silent))
silent means nothing warns you, so the entry shows the failure rate before you can bind
it.
An entry also carries .rule, .citations, .biases, .parameters and .gui.surfacing,
the registry's own ruling on how hard an interface should push the choice at a user. A bias
always states the criterion it was measured against, because a bias figure without one
cannot be added to anything safely.
Refusals stay beside partial results
>>> partial = pf.analyse_countermovement_jump(
... quiet_standing_trial, weighing_epoch, onset, takeoff
... )
>>> partial.system_weight_newtons.value
600.0716666666667
>>> [(type(row).__name__, row.method_id) for row in partial.refusals]
[
('NoCrossingError', 'onset.threshold.noise_relative'),
('NoCrossingError', 'takeoff.threshold.absolute_force'),
]
>>> partial.onset_time_seconds is None
True
Each refusal carries the same class and fields it would carry if the whole request had to raise, so a notebook can branch on the record instead of parsing a sentence:
>>> error = partial.refusals[0]
>>> error.method_id, error.parameter, error.value
('onset.threshold.noise_relative', 'k', 5.0)
A request that cannot run still raises. The registry describes the literature, which is larger than the methods a selected analysis can bind. Selecting an entry with no rule behind it fails by name rather than resolving to something near it:
MethodNotImplementedError: 'onset.op.backward_offset_fixed' was passed as
the onset method, and 'onset.threshold.noise_relative' is the rule available for that step.
Available: ["bwepoch.fixed_window", "onset.threshold.noise_relative",
"takeoff.threshold.absolute_force"]
Check first with registry.method(id).implemented.
Missing data is reported, never inferred
Vendor exports write 0, -1 or 9999 to mean "no measurement". Reading one as a real
value moved a published correlation in this corpus by 0.16. Declare the convention your
export uses and the trial reports what matched it:
>>> trial = pf.Trial(force_newtons, sample_rate_hz=1200.0, sentinel=pf.Sentinel.zero())
>>> trial.exclusions
Exclusions(dropped_samples=600, sentinel_convention='zero', reason='600 sample(s) reported and kept in place: removing them would shift the time base')
Samples are counted and reported, never removed: closing a gap in a trace would shift every
timestamp after it. For a column of per-trial results, where dropping a row is the right
thing, use pf.partition_sentinel_values(values, pf.Sentinel.zero()).
Saying whether two results match
Matching analysis is not enough to make two numbers comparable. A 50 ms contact debounce living in one plate's firmware changes the answer and no reanalysis recovers from not knowing it, so acquisition is part of the record:
trial = pf.Trial(
force_newtons,
sample_rate_hz=1200.0,
acquisition=pf.Acquisition(
filter_at_capture="none",
tare_state="tared_before_trial",
plate_natural_frequency_hz=800.0,
floor_surface="concrete",
firmware_version="2.4.1",
),
)
Until every member is present, provenance.acquisition_complete stays False and every
result says so. pf.Acquisition(...).missing lists what is still needed.
Analysing a folder
A folder states the same rate and acquisition block as one trial. The rate has no default: these files do not carry it, and guessing it changes the answer.
run = pf.batch(
"trials",
registry="registry",
weighing="bwepoch.fixed_window",
onset="onset.threshold.noise_relative",
takeoff="takeoff.threshold.absolute_force",
sentinel=None,
sample_rate_hz=1200.0,
acquisition=pf.Acquisition(
filter_at_capture="none",
tare_state="tared_before_trial",
plate_natural_frequency_hz=800.0,
floor_surface="concrete",
firmware_version="2.4.1",
),
trial_file_suffixes=[".force.txt"],
weighing_parameters={"duration": 1.0},
onset_parameters={"k": 5.0},
takeoff_parameters={"threshold_n": 20.0},
)
print(run.run.run_fingerprint)
frame = run.to_pandas()
table = run.to_arrow()
The fingerprint stays None until every computed trial carries all 5 of 5 acquisition
members. to_pandas() and to_arrow() preserve the rows and their order. They import pandas
or pyarrow only when called, so neither table package is required for list, JSON, CSV, or
Parquet output.
Arrays
Trial reads any float64 buffer without a Python-level loop: a numpy array, an
array.array('d'), a memoryview, and non-contiguous views such as column[::2]. Lists and
tuples work too and convert element by element.
A narrower array type is refused rather than widened, because a float32 trace does not carry the precision the impulse identity is checked at:
>>> pf.Trial(force.astype(np.float32), sample_rate_hz=1200.0)
TrialError: force_newtons has dtype float32 and plateforce reads float64. Convert it with
.astype('float64') so the widening is recorded as your choice
numpy is not a dependency of this package.
Two fields worth reading
jump.unregistered_methods lists the steps that ran with no registry entry describing them.
They are choices that moved the number, printed rather than left to be discovered.
jump.weighing_epoch_tied_window_count is 1 for a fixed window. Above 1 means a search rule
found exact ties and did not identify a single window, so anything downstream treating the
selection as determined is reading an artefact of the arithmetic.
Licence
Apache-2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file plateforce-0.1.1.tar.gz.
File metadata
- Download URL: plateforce-0.1.1.tar.gz
- Upload date:
- Size: 1.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
127c2579af245e00e86565aab00e7f061b5cc3a04d641468919f7317f6969121
|
|
| MD5 |
86a530c7a8353d7e46be991e191248a2
|
|
| BLAKE2b-256 |
d0e8f2c3a31894034809ce6e13acbc4447a3291322b707c2f846b2dd5506b7f3
|
File details
Details for the file plateforce-0.1.1-cp311-abi3-win_amd64.whl.
File metadata
- Download URL: plateforce-0.1.1-cp311-abi3-win_amd64.whl
- Upload date:
- Size: 3.8 MB
- Tags: CPython 3.11+, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b6e2a864fea283ebb515759226e06a9e1c71adcf4d12a36515faa1f0bde7f12
|
|
| MD5 |
f1dcbc9046ba7fff2eaa6c798b8c7db2
|
|
| BLAKE2b-256 |
b6f71721bd843dca8da4f393b91bdf5d77fc2ca359d7b9119f40a45132287dee
|
File details
Details for the file plateforce-0.1.1-cp311-abi3-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: plateforce-0.1.1-cp311-abi3-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 4.0 MB
- Tags: CPython 3.11+, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f583a79815ebef80fca63722918c42751f5cbdbcbf36178ac62e55308e152cf0
|
|
| MD5 |
4e94d9745decf9ac80c7ef39183215bb
|
|
| BLAKE2b-256 |
9d83dde7ca1be67037ede81956958941187d121ad296bf382830883ae0b06909
|
File details
Details for the file plateforce-0.1.1-cp311-abi3-manylinux_2_28_aarch64.whl.
File metadata
- Download URL: plateforce-0.1.1-cp311-abi3-manylinux_2_28_aarch64.whl
- Upload date:
- Size: 4.0 MB
- Tags: CPython 3.11+, manylinux: glibc 2.28+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a50e210ea064f830b37c868af1898ed708de66157f404d6e8a37054852da2b50
|
|
| MD5 |
60377dcf7f1749d89d1efc2a7dd57ff6
|
|
| BLAKE2b-256 |
564bb01ffd3a1cd254bfc18a9491e32cde63e8ca02d690e0d80e84f0cfd0fdfe
|
File details
Details for the file plateforce-0.1.1-cp311-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: plateforce-0.1.1-cp311-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 3.6 MB
- Tags: CPython 3.11+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a8df733d2cc58b0f5fe383155de696132a5222ae74bf292c61a93c50c998858
|
|
| MD5 |
c61de63cd33a51c28d4d3e6e26a303c5
|
|
| BLAKE2b-256 |
e1f8ff1184e052e29bacd6e202c9aaf4fb755474d97ef4adc0f3802dcd1521e4
|
File details
Details for the file plateforce-0.1.1-cp311-abi3-macosx_10_13_x86_64.whl.
File metadata
- Download URL: plateforce-0.1.1-cp311-abi3-macosx_10_13_x86_64.whl
- Upload date:
- Size: 3.8 MB
- Tags: CPython 3.11+, macOS 10.13+ x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c2b37278a1642498582babe65d444c813e3597791c03f87395df1032d824bb09
|
|
| MD5 |
ea78e19b33d44a12db2e7d30adca9f32
|
|
| BLAKE2b-256 |
c1b4751aab93f154cfd8964fd8d8827f825e89d180389057054a21e2726a9b0b
|