hyrax.models.hyrax_ts2vec#
TS2Vec: universal self-supervised representation learning for time series.
An implementation of TS2Vec (Yue et al., AAAI 2022, zhihanyue/ts2vec)
adapted to the Hyrax model interface. The reference implementation splits its logic between
a fit() method that owns the training loop and an encode() method that owns
inference; here the per-batch core of fit() lives in HyraxTs2Vec.train_batch() and
the pooling core of encode() lives in HyraxTs2Vec.infer_batch(), so Hyrax’s own
verbs (train, train_stream, infer, infer_stream) drive the loop.
The method is fully self-supervised: two overlapping random crops of the same series are encoded, and a hierarchical contrastive loss pulls together the representations of the same timestamp across the two views (temporal contrast) while pushing apart different instances at the same timestamp (instance contrast). No labels are required.
Input convention#
TSEncoder consumes a single (batch, time, channels) float32 array in which
NaN marks a missing or padded timestep, and excludes those positions from the
convolution - so ragged light curves need no separate mask tensor once they reach the
encoder.
build_event_sequence() produces that array from ordinary padded light-curve columns,
and HyraxTs2Vec.prepare_inputs() calls it. No per-survey dataset subclass is needed:
LSDBStreamDataset already collates every nested
column into <column>_<subcolumn> plus a boolean <column>_<subcolumn>_mask, and the
column names, band list, sequence length and normalization are read from
[model.HyraxTs2Vec].
Encoding in prepare_inputs rather than in collation is deliberate. Hyrax’s
handle_nans runs over every float array produced by collation - warning when
data_set.nan_mode is false and overwriting NaN when it is "zero" or
"quantile" - and prepare_inputs is the first hook downstream of it, so it is the
earliest point at which the NaN sentinel is safe to emit.
Warning
Leave data_set.nan_mode at false for this pipeline. The raw flux column reaches
handle_nans with its own NaNs still in it, and "zero" or "quantile" would
overwrite a missing measurement at a valid timestep with a number indistinguishable
from a real one. With false those NaNs are dropped here instead, by the validity
mask. Better still, drop them upstream in LSDB and avoid the per-batch warning.
Attributes#
Classes#
A 1D convolution padded so that the output keeps the input length. |
|
Two same-padded convolutions at one dilation, with a residual connection. |
|
A stack of |
|
Project, mask, and dilate-convolve a |
|
Sum the instance and temporal contrastive losses over a max-pooling hierarchy. |
|
The |
|
TS2Vec, a self-supervised encoder for multivariate time series. |
Functions#
|
Return a |
|
Return a |
|
Contrast each instance against the other instances at the same timestamp. |
|
Contrast each timestamp against the other timestamps of the same instance. |
|
Gather |
|
Pull the light-curve columns and the validity mask out of a collated batch. |
|
Sort each row by time and fit it to exactly |
|
Center and rescale each object's flux, applying the same scale to its error. |
|
Map band values to one-hot column offsets, returning |
|
Turn padded light-curve columns into the event sequence |
Module Contents#
- class SamePadConv(in_channels, out_channels, kernel_size, dilation=1, groups=1)[source]#
Bases:
torch.nn.ModuleA 1D convolution padded so that the output keeps the input length.
Initialize internal Module state, shared by both nn.Module and ScriptModule.
- class ConvBlock(in_channels, out_channels, kernel_size, dilation, final=False)[source]#
Bases:
torch.nn.ModuleTwo same-padded convolutions at one dilation, with a residual connection.
Initialize internal Module state, shared by both nn.Module and ScriptModule.
- class DilatedConvEncoder(in_channels, channels, kernel_size)[source]#
Bases:
torch.nn.ModuleA stack of
ConvBlockwhose dilation doubles at every level.Initialize internal Module state, shared by both nn.Module and ScriptModule.
- generate_binomial_mask(batch_size: int, length: int, p: float = 0.5) torch.Tensor[source]#
Return a
(batch_size, length)bool mask keeping each position with probabilityp.
- generate_continuous_mask(batch_size: int, length: int, n: int | float = 5, mask_length: int | float = 0.1) torch.Tensor[source]#
Return a
(batch_size, length)bool mask withncontiguous spans masked out.nandmask_lengthmay be floats, in which case they are read as fractions oflength.
- class TSEncoder(input_dims, output_dims, hidden_dims=64, depth=10, mask_mode='binomial')[source]#
Bases:
torch.nn.ModuleProject, mask, and dilate-convolve a
(batch, time, channels)series.NaN timesteps are detected and folded into the mask, so missing observations and padding contribute nothing to the convolution.
Initialize internal Module state, shared by both nn.Module and ScriptModule.
- instance_contrastive_loss(z1: torch.Tensor, z2: torch.Tensor) torch.Tensor[source]#
Contrast each instance against the other instances at the same timestamp.
Returns zero for a batch of one, where there are no negatives to contrast against.
- temporal_contrastive_loss(z1: torch.Tensor, z2: torch.Tensor) torch.Tensor[source]#
Contrast each timestamp against the other timestamps of the same instance.
Returns zero for a single-timestamp series, where there are no negatives.
- class HierarchicalContrastiveLoss(alpha: float = 0.5, temporal_unit: int = 0)[source]#
Bases:
torch.nn.ModuleSum the instance and temporal contrastive losses over a max-pooling hierarchy.
Each level halves the time axis with a max-pool, so the loss is applied from per-timestamp granularity all the way up to the whole series. This is an
nn.Moduleso that assigning it toself.criterionsatisfies the@hyrax_modeldecorator and suppresses the config-driven criterion.- Parameters:
alpha (float) – Weight on the instance-contrastive term;
1 - alphaweights the temporal term.temporal_unit (int) – Hierarchy level below which the temporal term is skipped.
state (Initialize internal Module)
ScriptModule. (shared by both nn.Module and)
- take_per_row(tensor: torch.Tensor, indices, num_elem: int) torch.Tensor[source]#
Gather
num_elemconsecutive timesteps from each row, starting at that row’s index.- Parameters:
tensor (torch.Tensor) – Shape
(batch, time, channels).indices (array-like) – Per-row start offset, length
batch.num_elem (int) – Number of consecutive timesteps to take from each row.
- Returns:
Shape
(batch, num_elem, channels).- Return type:
torch.Tensor
- class EncodingSettings(config: dict)[source]#
The
[model.HyraxTs2Vec]keys that describe the input rather than the architecture.Resolved and validated in one place so that
build_event_sequence()andHyraxTs2Vecagree on the channel count, and so a bad setting is caught when the model is constructed rather than midway through a run.time_field,flux_fieldandflux_err_fieldare flattened column names, asLSDBStreamDatasetexposes nested columns.band_fieldisNonefor a single-band survey, where the one-hot block is dropped entirely andbandsis empty; otherwisebandsgives the one-hot channel order.max_sequence_lengthis how many observations are kept per object, which fixes the width of the time axis, andnormalizeis one ofNORMALIZE_MODESorFalse.n_channelsis3 + len(bands), the model’s input width.- Parameters:
config (dict) – The runtime configuration.
- _select_columns(data: dict, settings: EncodingSettings)[source]#
Pull the light-curve columns and the validity mask out of a collated batch.
Returns
(time, flux, flux_err, band, valid), each(batch, length).bandisNonefor a single-band catalog.validstarts from the collation mask and then drops non-finite observations, so it is the single source of truth for what is real.
- _sort_and_trim(columns, valid, length)[source]#
Sort each row by time and fit it to exactly
lengthtimesteps.Invalid entries sort to the tail, so the kept observations are the earliest real ones, matching what a per-object sort-then-truncate would give.
- _normalize_flux(flux, flux_err, valid, mode)[source]#
Center and rescale each object’s flux, applying the same scale to its error.
Per-object rather than per-batch: raw fluxes span orders of magnitude across bands and objects, and TS2Vec applies no normalization of its own. The shared scale is what keeps the error interpretable next to the flux.
- _band_indices(band, valid, settings: EncodingSettings)[source]#
Map band values to one-hot column offsets, returning
-1for unrecognized ones.Accepts integer band codes directly and matches everything else against the configured band names. Mapping goes through
np.uniqueso the dict lookups cost one per distinct band rather than one per observation. Unrecognized bands are reported only where the mask says there is a real observation, so zero padding is not mistaken for a band.
- build_event_sequence(data_dict: dict, config: dict) numpy.ndarray[source]#
Turn padded light-curve columns into the event sequence
TSEncoderconsumes.Observations are sorted by time and laid out as an ordered sequence of events, one timestep per observation, with channels:
0 flux, normalized per object 1 flux error, on the same scale as the flux 2 log1p of the gap since the previous observation (0 for the first) 3 .. 3 + len(bands) one-hot band indicator
Uneven sampling is therefore carried as a feature (channel 2) rather than being resampled away, and a light curve that spans several filters stays a single sequence. Cross-matching more surveys to add bands only widens the one-hot block; the encoder sizes its input layer from
bands, so no architecture change is needed.The input columns are whatever the dataset collated under the names configured in
[model.HyraxTs2Vec].LSDBStreamDatasetproduces them for every nested column without any per-survey subclass:<column>_<subcolumn>padded to the batch’s longest row, plus a boolean<column>_<subcolumn>_mask.- Parameters:
data_dict (dict) – The collated batch, with a
"data"key holding the light-curve columns.config (dict) – The runtime configuration.
- Returns:
(batch, max_sequence_length, 3 + len(bands))float32, NaN at every padded or missing timestep.- Return type:
numpy.ndarray
- class HyraxTs2Vec(config)[source]#
Bases:
torch.nn.ModuleTS2Vec, a self-supervised encoder for multivariate time series.
Consumes
(batch, time, channels)float32 input where NaN marks a missing or padded timestep, and produces one fixed-length representation per object frominfer_batch()- ready forreduce_dimensionsand thevisualizeverbs.channelsis the event-sequence encoding built bybuild_event_sequence(): flux, flux error, log-scaled time gap, then a one-hot band indicator. Adding bands (by cross-matching more surveys) only widenschannels; no architecture change is needed.Notes
This model sets
self.optimizeritself (AdamW at[model.HyraxTs2Vec].learning_rate) rather than taking the config-driven optimizer, because the Hyrax default is SGD atlr = 0.01while TS2Vec is specified on AdamW at1e-3. That is a supported path, but it does mean Hyrax logs one benign “Both model and config define an optimizer” warning per run.Differences from the reference implementation, both deliberate:
No SWA. The reference wraps the encoder in
torch.optim.swa_utils.AveragedModeland encodes from the averaged weights. Skipped here: it puts non-gradient parameters into the optimizer, complicatesstate_dictsave/load, and risks DDP unused-parameter errors.No
max_train_lengthsplitting. The reference chops over-long series into segments stacked along the batch axis before training. Sequence length is instead bounded by the dataset’smax_sequence_length.
Initialize internal Module state, shared by both nn.Module and ScriptModule.
- forward(x, mask=None)[source]#
Encode a batch of series into per-timestamp representations.
- Parameters:
x (torch.Tensor) – Shape
(batch, time, channels), NaN at missing or padded timesteps.mask (str or torch.Tensor, optional) – Overrides the configured masking strategy for this call.
- Returns:
Shape
(batch, time, output_dims).- Return type:
torch.Tensor
- _drop_empty_rows(x)[source]#
Drop rows that are NaN everywhere, as the reference’s
fitdoes.Safe during training because the loss does not need to align with
object_id.
- _contrastive_loss(x)[source]#
Encode two overlapping random crops of
xand return their contrastive loss.
- train_batch(batch)[source]#
Run one self-supervised training step, the inner loop of the reference
fit().- Parameters:
batch (torch.Tensor) – Shape
(batch, time, channels), NaN at missing or padded timesteps.- Returns:
{"loss": float}for the current batch.- Return type:
dict
- validate_batch(batch)[source]#
Compute the training loss without updating any weights.
- Parameters:
batch (torch.Tensor) – Shape
(batch, time, channels), NaN at missing or padded timesteps.- Returns:
{"loss": float}for the current batch.- Return type:
dict
- test_batch(batch)[source]#
Identical to
validate_batch().- Parameters:
batch (torch.Tensor) – Shape
(batch, time, channels), NaN at missing or padded timesteps.- Returns:
{"loss": float}for the current batch.- Return type:
dict
- infer_batch(batch)[source]#
Produce the latent representation of each series, the core of
encode().The model is in eval mode here, so
TSEncoderresolves its mask to"all_true"and the output is deterministic.Series that are entirely missing are not dropped, because the inference output must stay aligned one-to-one with
object_id; they encode a fully masked, all-zero input.- Parameters:
batch (torch.Tensor) – Shape
(batch, time, channels), NaN at missing or padded timesteps.- Returns:
(batch, output_dims)whenencoding_windowis"full_series", or(batch, time, output_dims * levels)when it is"multiscale".- Return type:
torch.Tensor
- static prepare_inputs(data_dict)[source]#
Build the event sequence this model consumes from a collated batch.
The real work is in
build_event_sequence(); this is a shim so that the function written out toprepare_inputs.pynext to the saved weights stays short and self-contained. That does mean a checkpoint pins the settings of the encoding - they are saved in the run’sruntime_config.toml- rather than its code, which tracks the installed Hyrax.- Parameters:
data_dict (dict) – The collated batch, with a
"data"key holding the padded light-curve columns named in[model.HyraxTs2Vec].- Returns:
Shape
(batch, max_sequence_length, channels), float32, NaN at padded timesteps. Hyrax converts this to a tensor and moves it to the right device.- Return type:
numpy.ndarray