Wrap an Existing Model¶
Have a PyTorch STPP model already? You expose it to Seahorse by splitting it into
two nn.Module adapters — a StateModel and an EventModel — that
UnifiedSTPP drives. This page shows how a batch of events flows through them,
what each part owns, and where an optional PresetDescriptor fits.
How a batch flows through your model¶
A padded batch of sequences enters at the top; each component's method transforms it into the next shape, ending in the per-event likelihood Seahorse trains on.
Shapes: B sequences per batch, T events per sequence, T_q query
times, and H = hidden_dim.
What each part owns¶
encode(times, locs, mask)→ (B, T, H) hiddenevolve(hidden, query_times)→ (B, T_q, H) stateStateModel nn.Module
log_prob(times, locs, state, mask)→ (B, T) · requiredsample_next(state, t_last)→ next events · optionalintensity_grid(state, t, s)→ density · optionalEventModel nn.Module
data_init_overrides(dm)→ dict merged into the buildbuild_model(), given the fitted data module
UnifiedSTPP(state_model, event_model, *, hidden_dim) is the wiring that ties the
two modules together; the full API lives in seahorse/models/unified_model.py.
Minimal StateModel Adapter¶
import torch
import torch.nn as nn
class MyStateModel(nn.Module):
def __init__(self, hidden_dim: int):
super().__init__()
self.encoder = MyExistingEncoder(hidden_dim)
self.hidden_dim = hidden_dim
def encode(self, times, locations, mask):
"""Encode event history.
Args:
times: (B, T) event times
locations: (B, T, 2) event locations
mask: (B, T) bool mask — True where events exist
Returns:
hidden: (B, T, hidden_dim) per-event hidden states
"""
return self.encoder(times, locations, mask)
def evolve(self, hidden, query_times):
"""Evolve state to query times (for piecewise-constant families: no-op).
Args:
hidden: (B, T, hidden_dim)
query_times: (B, T_q) query times
Returns:
state: (B, T_q, hidden_dim)
"""
return hidden # piecewise-constant: return last hidden state before each query
Minimal EventModel Adapter¶
class MyEventModel(nn.Module):
def __init__(self, hidden_dim: int, spatial_dim: int = 2):
super().__init__()
self.decoder = MyExistingDecoder(hidden_dim, spatial_dim)
def log_prob(self, times, locations, state, mask):
"""Compute per-event log-probability.
Args:
times: (B, T) event times
locations: (B, T, 2) event locations
state: (B, T, hidden_dim) evolved state at event times
mask: (B, T) bool mask
Returns:
log_prob: (B, T) per-event log-likelihood (masked positions can be 0)
"""
return self.decoder.log_prob(times, locations, state, mask)
def sample_next(self, state, t_last, n_samples: int = 1):
"""Optional: sample next event given state.
Raise NotImplementedError if sampling is not supported.
"""
raise NotImplementedError("MyEventModel does not support next-event sampling")
Wire Into a Preset¶
Once you have MyStateModel and MyEventModel, create a ModelFamilyConfig:
from dataclasses import dataclass
from seahorse.models.configs.base import BaseModelConfig, ConfigRegistry
from seahorse.models.unified_model import UnifiedSTPP
@ConfigRegistry.register("my_preset")
@dataclass
class MyPresetConfig(BaseModelConfig):
hidden_dim: int = 64
@classmethod
def from_dict(cls, d, *, hidden_dim, **kwargs):
return cls(hidden_dim=hidden_dim)
def build_model(self) -> UnifiedSTPP:
state = MyStateModel(self.hidden_dim)
event = MyEventModel(self.hidden_dim)
return UnifiedSTPP(state, event, hidden_dim=self.hidden_dim)
Then follow the Register a Preset page to expose it through the CLI and Python API.
Common Pitfalls¶
- Shape mismatches: Seahorse passes
(B, T, *)tensors. Check your existing model's expected input shape. inference_modeconflict: if your model usestorch.autograd.gradinternally, ensure the runner is configured withinference_mode=False.- Missing mask handling: padding positions in a batch have
mask=False. Sum or mean log-prob overmask=Truepositions only.