PyTorch¶
Optional — requires pip install "pybvh-ml[torch]". See the PyTorch Integration guide for the training-loop context, the set_epoch contract, and the length semantics.
Datasets¶
datasets
¶
PyTorch Dataset classes for motion capture data.
EpochState()
¶
Shared-memory epoch counter for DataLoader-worker visibility.
Pairs with :func:rng_for: it supplies the epoch term that makes
a seeded sample's draw change from one epoch to the next. Public
because a Dataset that isn't a :class:MotionDataset subclass needs
exactly this to honor the set_epoch contract — hold one, call
:meth:set from set_epoch, and read :attr:current in
__getitem__.
The epoch lives in a multiprocessing.Value so that
set_epoch() in the main process is observed by DataLoader
workers — including persistent ones (persistent_workers=True),
which are created once and never re-receive the dataset. Workers
inherit the shared handle when the DataLoader passes the dataset
as Process args, which works under both fork and spawn start
methods.
The Value is built from an explicit spawn context rather than
the process default, and that choice is load-bearing: a
fork-context lock is an anonymous semaphore, unlinked at creation,
whose handle is meaningless in a spawn-started child — it unpickles
without complaint and segfaults the worker on first use. A
spawn-context lock is named, so it survives both inheritance (fork)
and reopen-by-name (spawn). Linux defaults to fork, so the
mismatch is reachable with a plain
DataLoader(..., multiprocessing_context="spawn").
-1 is the never-set sentinel (replaces a separate boolean — one
would live in whichever process wrote it and read False in every
forked worker; :attr:is_set reads the shared sentinel instead).
Deliberately no __getstate__/__setstate__: swapping the
Value for a plain int during pickling would silently break sharing
under spawn (worker creation uses the same pickle machinery). The
cost is that holders cannot be copy.deepcopy-ed or
torch.save-ed directly — shared ctypes only travel via process
inheritance.
Note that a spawn DataLoader additionally pickles the whole dataset,
so everything it holds must be picklable — in particular a
:class:~pybvh_ml.AugmentationPipeline whose kwargs are lambda
callables cannot cross a spawn boundary; use module-level functions
there.
current: int
property
¶
Current epoch (0 when never set), with no warning.
For read-only callers: the warn-once budget in :meth:effective
belongs to the training path, and a diagnostic read must not
spend it and mask the real warning later.
is_set: bool
property
¶
Whether :meth:set has been called yet.
:attr:current answers "which epoch do I augment as", and for
that question "never set" and "epoch 0" are the same answer,
deliberately. They are not the same fact, though, and code
that needs the fact — a trainer hook claiming epoch 0 only if
nothing has claimed it, a test asserting the hook ran — was left
reaching for the private _raw().
Reads the shared value, so it is worker-visible like everything
else here, and costs no warn-once budget. It is not atomic
with :meth:set: if not state.is_set: state.set(0) is a
check-then-act, so run it in the main process before the
DataLoader starts its workers — which is where an epoch-0 claim
has to happen anyway, since workers that fork beforehand carry
the unset state into their first batches.
MotionDataset(clips: list[dict], labels: np.ndarray | None = None, target_length: int | None = None, augmentation: AugmentationPipeline | None = None, seed: int | None = None, *, center_root: bool = False, temporal: str = 'pad', layout: str = 'flat', streams: tuple[str, ...] = DEFAULT_STREAMS, position_centering: str | None = None, source_repr: str | None = None, target_repr: str | None = None, euler_orders: list[str] | None = None, names: Sequence[str] | None = None)
¶
Bases: Dataset
Dataset that loads preprocessed motion clips.
Designed to work with the output of
:func:pybvh_ml.preprocessing.load_preprocessed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
clips
|
list of dict
|
Each dict must have |
required |
labels
|
array - like or None
|
Per-clip integer labels. Must cover every clip when given. |
None
|
names
|
sequence of str or None
|
Per-clip identity, surfaced as The convention is the filename stem — what
:func: |
None
|
target_length
|
int or None
|
If given, standardize all clips to this length using
|
None
|
temporal
|
('pad', 'crop', 'resample', 'resample_deterministic')
|
How |
"pad"
|
layout
|
('flat', 'ctv', 'tvc')
|
Tensor layout of the returned |
"flat"
|
streams
|
tuple of str
|
Which streams the packed tensor carries, and in what order —
forwarded to the packer, so the vocabulary and shape rules are
:func: The derived streams ( |
DEFAULT_STREAMS
|
position_centering
|
('world', 'skeleton', 'first')
|
Frame the clips' position arrays are in, recorded on every
:class: |
"world"
|
source_repr
|
str or None
|
Convert each clip's |
None
|
target_repr
|
str or None
|
Convert each clip's |
None
|
euler_orders
|
list of str or None
|
Per-joint Euler orders, required when either end of the
conversion is |
None
|
augmentation
|
AugmentationPipeline or None
|
Applied on-the-fly during |
None
|
center_root
|
bool
|
If True, subtract each clip's first-frame root position in
|
False
|
seed
|
int or None
|
Base seed for reproducible augmentation. When set, combined
with the current epoch (see :meth: |
None
|
Notes
Per-epoch augmentation variety: call
dataset.set_epoch(epoch) at the start of each training epoch
so the seeded augmentation changes across epochs — same contract
as :class:torch.utils.data.distributed.DistributedSampler. The
epoch lives in shared memory, so this works with
num_workers > 0 including persistent_workers=True. When
seed is set and set_epoch is never called, every epoch
sees the same augmentation per sample index (useful for
debugging, harmful for training dynamics).
Pickling: because of the shared-memory epoch, instances cannot
be copy.deepcopy-ed or torch.save-ed directly — shared
state only travels via process inheritance (which is exactly how
the DataLoader hands the dataset to its workers).
epoch: int
property
¶
Epoch this dataset augments as — 0 when none was set.
epoch_is_set: bool
property
¶
Whether :meth:set_epoch has been called.
Distinct from epoch == 0, which is also what an unset dataset
reports. The question matters under a training framework that
builds its DataLoader — and forks its workers — before the hook
you put set_epoch in: if not ds.epoch_is_set:
ds.set_epoch(0) in an earlier hook claims the epoch before any
worker inherits the unset state. Reads the shared counter, so
the answer is the same in every worker.
from_preprocessed(loaded: dict, **kwargs) -> 'MotionDataset'
classmethod
¶
Build a dataset from a :func:~pybvh_ml.preprocessing.load_preprocessed result.
Wires the metadata that would otherwise be restated by hand at
the call site: the clips, labels and filenames (as
names), the stored representation (as source_repr,
which target_repr conversion needs),
skeleton_info["euler_orders"], and the stored
position_centering.
center_root defaults to False because the stored arrays
already reflect the choice made at preprocessing time — centering
again here would be a second, unrecorded transform.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
loaded
|
dict
|
The dict returned by
:func: |
required |
**kwargs
|
Forwarded to :class: |
{}
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
set_epoch(epoch: int) -> None
¶
Set the current epoch for per-epoch reproducible augmentation.
Mirrors :meth:torch.utils.data.distributed.DistributedSampler.set_epoch;
reaches DataLoader workers (persistent ones included) via
shared memory.
explain_augmentation(idx: int, *, epoch: int | None = None) -> list[dict]
¶
Report what the augmentation did to sample idx.
Re-runs this sample's augmentation on the same
(seed, epoch, idx) rng the loader used, so the records
describe the draw that actually ran rather than a fresh one.
Their layout is the pipeline's return_params format:
{"name", "applied", "params"} per step.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
idx
|
int
|
Sample index; negative indexing works as in |
required |
epoch
|
int
|
Epoch to replay. Defaults to the dataset's current epoch — pass it explicitly to ask about an earlier one. |
None
|
Returns:
| Type | Description |
|---|---|
list of dict
|
One record per configured augmentation step, or |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the dataset was built without a |
Notes
The replay is truthful only while its inputs are unchanged: the same pipeline (same steps, probabilities and ranges) over the same clip arrays. Rebuild the pipeline differently and the records describe a run that no longer exists.
OnTheFlyDataset(bvh_paths: list[str | Path], representation: str | None = '6d', target_length: int | None = None, augmentation: AugmentationPipeline | None = None, center_root: bool = True, label_fn: Callable[[str], int] | None = None, seed: int | None = None, *, world_up: str = 'auto', lr_mapping: dict[str, str] | None = None, temporal: str = 'pad', layout: str = 'flat', streams: tuple[str, ...] = DEFAULT_STREAMS, include_positions: bool = False, position_space: str = 'joint', position_centering: str = 'world')
¶
Bases: Dataset
Dataset that loads BVH files on-the-fly for maximum augmentation variety.
Slower than :class:MotionDataset but avoids pre-extracting arrays,
so every epoch sees freshly augmented data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bvh_paths
|
list of str or Path
|
Paths to BVH files (coerced to :class: |
required |
representation
|
str or None
|
Rotation representation for joint data. Extraction happens per
clip, so this is already the "target" representation —
:class: |
'6d'
|
include_positions
|
bool
|
Also extract positions per clip. This class calls
:func: |
False
|
position_space
|
('joint', 'node')
|
Which index space to extract — see
:func: |
"joint"
|
position_centering
|
('world', 'skeleton', 'first')
|
Which frame to extract them in, recorded on every
:class: |
"world"
|
streams
|
tuple of str
|
Which streams the packed tensor carries — see
:class: |
DEFAULT_STREAMS
|
target_length
|
int or None
|
If given, standardize to this length using |
None
|
temporal
|
('pad', 'crop', 'resample', 'resample_deterministic')
|
How |
"pad"
|
layout
|
('flat', 'ctv', 'tvc')
|
Tensor layout of the returned |
"flat"
|
augmentation
|
AugmentationPipeline or None
|
|
None
|
center_root
|
bool
|
If True (default), subtract each clip's first-frame root position after extraction. |
True
|
label_fn
|
callable or None
|
|
None
|
Notes
Every item carries name — the source file's stem — with no
parameter to enable it, because unlike :class:MotionDataset this
class has the paths and identity is never a guess. The stem
rather than the full path is what makes it the same identity a
preprocessed dataset reports (filenames), so a clip keeps its
name across both paths; it is also what label_fn receives.
world_up : str
Forwarded to :func:pybvh.read_bvh_file per clip. "auto"
(default) auto-detects; pass "+y" etc. to override — same
semantics as :func:~pybvh_ml.preprocessing.preprocess_directory.
lr_mapping : dict or None
Forwarded to :func:pybvh.read_bvh_file. Explicit left/right
joint pair mapping for uniform dataset conventions.
seed : int or None
See :class:MotionDataset for seeding semantics. Call
:meth:set_epoch at the start of each epoch for reproducible
per-epoch variety — reaches DataLoader workers (persistent
ones included) via shared memory; see :class:MotionDataset
for the pickling caveat.
epoch: int
property
¶
Epoch this dataset augments as — 0 when none was set.
epoch_is_set: bool
property
¶
Whether :meth:set_epoch has been called.
Distinct from epoch == 0, which is also what an unset dataset
reports. The question matters under a training framework that
builds its DataLoader — and forks its workers — before the hook
you put set_epoch in: if not ds.epoch_is_set:
ds.set_epoch(0) in an earlier hook claims the epoch before any
worker inherits the unset state. Reads the shared counter, so
the answer is the same in every worker.
set_epoch(epoch: int) -> None
¶
Set the current epoch for reproducible per-epoch augmentation.
explain_augmentation(idx: int, *, epoch: int | None = None) -> list[dict]
¶
Report what the augmentation did to sample idx.
Re-runs this sample's augmentation on the same
(seed, epoch, idx) rng the loader used, so the records
describe the draw that actually ran rather than a fresh one.
Their layout is the pipeline's return_params format:
{"name", "applied", "params"} per step. The source file is
re-read, so this costs a parse per call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
idx
|
int
|
Sample index; negative indexing works as in |
required |
epoch
|
int
|
Epoch to replay. Defaults to the dataset's current epoch — pass it explicitly to ask about an earlier one. |
None
|
Returns:
| Type | Description |
|---|---|
list of dict
|
One record per configured augmentation step, or |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the dataset was built without a |
Notes
The replay is truthful only while its inputs are unchanged: the same pipeline (same steps, probabilities and ranges) over the same source file. Edit the BVH on disk and the records describe a run that no longer exists.
rng_for(seed: int | None, epoch: int, idx: int) -> np.random.Generator
¶
Build the per-sample generator for one (seed, epoch, idx) triple.
The seeding scheme both Dataset classes use, exposed because any
Dataset needs it — not only subclasses of the two shipped here. A
SeedSequence([seed, epoch, idx]) makes each sample's stream
independent of the order samples are drawn in and of which worker
draws them, so a clip augments identically whether it lands in
worker 0 or worker 3, and shuffling doesn't change what any sample
receives.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seed
|
int or None
|
Base seed. |
required |
epoch
|
int
|
Current epoch; see :class: |
required |
idx
|
int
|
Sample index. Must be non-negative — resolve Python negative indexing before calling. |
required |
Returns:
| Type | Description |
|---|---|
Generator
|
|
Examples:
>>> from pybvh_ml.torch import EpochState, rng_for
>>> class MyFeeder(torch.utils.data.Dataset):
... def __init__(self, seed=0):
... self.seed = seed
... self.epoch_state = EpochState()
... def set_epoch(self, epoch):
... self.epoch_state.set(epoch)
... def __getitem__(self, idx):
... rng = rng_for(self.seed, self.epoch_state.current, idx)
... ...
Collate¶
collate
¶
Collate function for variable-length motion sequences.
collate_motion_batch(batch: list[dict]) -> dict[str, torch.Tensor]
¶
Collate variable-length motion clips into a padded batch.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
batch
|
list of dict
|
Each dict must have |
required |
Returns:
| Type | Description |
|---|---|
dict
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If any |