Observables

Observables are quantities computed from simulation trajectories. During optimization, observables provide the measured values that are compared to experimental targets — the difference drives gradient-based parameter updates.

Observable API

All observables follow a simple callable interface:

observable(trajectory)  jnp.ndarray

Given a SimulatorTrajectory, an observable returns a JAX array of shape (n_states, ...) containing the per-frame measured values.

There are two observable patterns in mythos:

Single observables return a single array:

from mythos.observables.propeller import PropellerTwist

obs = PropellerTwist(
    rigid_body_transform_fn=transform_fn,
    h_bonded_base_pairs=jnp.array([[1, 14], [2, 13], [3, 12]]),
)
values = obs(trajectory)  # shape: (n_states,)

Mapped observables return a dictionary of arrays, keyed by name. These are useful when a single simulation produces multiple related measurements (e.g., distances for every bond type in a Martini topology):

from mythos.observables.bond_distances import BondDistancesMapped

obs = BondDistancesMapped(topology=martini_topology)
values = obs(trajectory)  # {"DMPC_NC3_PO4": array(...), "DMPC_PO4_GL1": array(...), ...}

BaseObservable

The BaseObservable class is the base for DNA/RNA structural observables. It requires a rigid_body_transform_fn callable that converts rigid body coordinates into nucleotide site positions, and provides helpers for computing helical axes and duplex quartets.

See mythos.observables.base for the full API.

Martini observables (BondDistances, TripletAngles, membrane observables) do not inherit from BaseObservable — they are standalone frozen dataclasses that operate directly on Cartesian coordinates.

Using Observables in Loss Functions

Observables are typically paired with loss functions to produce scalar losses for optimization.

ObservableLossFn

The ObservableLossFn wrapper combines an observable with a loss function in a single callable. Given a trajectory, it computes the observable value (as a weighted sum over frames), then evaluates the loss against a target:

from mythos.losses.observable_wrappers import ObservableLossFn, RootMeanSquaredError
from mythos.observables.propeller import PropellerTwist

loss_fn = ObservableLossFn(
    observable=PropellerTwist(
        rigid_body_transform_fn=transform_fn,
        h_bonded_base_pairs=h_bonded_pairs,
    ),
    loss_fn=RootMeanSquaredError(),
    return_observable=True,  # also return the measured value
)

loss, measured = loss_fn(trajectory, target, weights)

The weights array controls how frames are aggregated — typically zeros for equilibration frames and uniform weights for production frames:

eq_steps = 2000
prod_steps = 18000
weights = jnp.concat([
    jnp.zeros(eq_steps),
    jnp.ones(prod_steps) / prod_steps,
])

Available Loss Functions

SquaredError

\((y_{\text{target}} - y_{\text{actual}})^2\)

RootMeanSquaredError

\(\sqrt{\frac{1}{N}\sum_i (y_{\text{target},i} - y_{\text{actual},i})^2}\)

l2_loss

Standalone function: \(\sum_i (y_{\text{actual},i} - y_{\text{target},i})^2\)

See mythos.losses for the full API.

Using Observables with Objectives

In the Optimization framework, observables can also be used inside Objective and DiffTReObjective instances. In this context, the observable is called on the trajectory exposed by a simulator, and the objective’s grad_or_loss_fn computes gradients from the result.

For DiffTRe-based optimization, observables that support weighted evaluation (via the weights argument) allow Boltzmann-reweighted loss computation without re-running the simulation.

WassersteinDistance

The WassersteinDistance observable wraps another observable and computes the 1D Wasserstein (Earth Mover’s) distance between its output distribution and a fixed reference distribution. This is particularly useful for bottom-up fitting where you want to match a full distribution shape rather than a single scalar:

from mythos.observables.wasserstein import WassersteinDistance

obs = WassersteinDistance(
    observable=my_bond_observable,
    v_distribution=reference_distribution,  # target distribution array
)

The mapped variant WassersteinDistanceMapped operates on multiple named distributions simultaneously.

Available Observables

DNA / RNA Structural

Class

Description

Module

Rise

Helical rise (Å) from quartet midpoint projections

rise

PitchAngle

Average pitch angle (rad); pitch = π / ⟨angle⟩

pitch

Diameter

Helical diameter from backbone distances

diameter

PropellerTwist

Angle between hydrogen-bonded base normals

propeller

TwistXY

Total twist in the X-Y plane

stretch_torsion

ExtensionZ

Extension in the Z direction

stretch_torsion

PersistenceLength

Persistence length from vector autocorrelation + linear fit

persistence_length

RMSE

SVD-aligned RMSE vs. a reference structure

rmse

MeltingTemp

Melting temperature via umbrella sampling + histogram reweighting

melting_temp

Membrane

Class

Description

Module

MembraneThickness

Bilayer thickness via leaflet assignment

membrane_thickness

AreaPerLipid

Area per lipid via leaflet assignment

area_per_lipid

MembraneMeltingTemp

Membrane Tm from sigmoid fit to APL vs. temperature

membrane_melting_temp

Bonded / Angle

Class

Description

Module

BondDistances / BondDistancesMapped

Per-bond distances (single or multi-bond mapped variant)

bond_distances

TripletAngles / TripletAnglesMapped

Angles at central atom in triplets (single or mapped variant)

triplet_angles

Distance Metrics

Class

Description

Module

WassersteinDistance / WassersteinDistanceMapped

1D Wasserstein (Earth Mover’s) distance to a reference distribution

wasserstein

For the full API reference, see mythos.observables.