Multi-Dim Datasets#
Traveler uses JaxDataset for named collections of JAX arrays that do not
naturally form a table. Each variable has a name, but the variables may have different numbers of
dimensions and different shapes. This makes a JaxDataset useful for multidimensional model inputs
such as skims, other related level-of-service (LOS) matrices, and accessibility arrays.
Like a JaxTable, a JaxDataset:
stores its numerical data as JAX arrays;
provides variables through attributes and string keys;
carries a separate metadata dictionary;
is a JAX pytree, so its arrays can participate in JAX transformations; and
supports functional updates with
assign().
The relationship is one of specialization: JaxTable is a subclass of JaxDataset that adds table
rules.
|
|
|
|---|---|---|
Variable shapes |
May differ |
Must be scalar or one-dimensional |
Common leading length |
Not required |
Required for non-scalar columns |
Typical meaning |
Named model arrays |
Rows and columns |
Typical use |
Skims and other multidimensional data |
Households, persons, tours, or land use |
A dataset is therefore not an xarray-style labeled array and does not perform alignment by coordinate name. Its variables are ordinary JAX arrays indexed by position. The variable names, class annotations, categorical coordinates, and metadata provide the model-specific meaning around those arrays.
Multidimensional LOS data#
A skim describes the impedance between an origin and a destination. Some LOS measures have two dimensions:
distance[origin, destination]
Others vary by time period and need a third dimension:
travel_time[origin, destination, time_period]
These variables cannot be columns of one JaxTable: they are not one-dimensional and do not
represent one record per row. They can coexist in one JaxDataset because the dataset does not
impose a shared rank or shape.
import jax.numpy as jnp
from traveler.trees import JaxDataset
distance = jnp.array(
[
[0, 4, 7],
[4, 0, 3],
[7, 3, 0],
],
dtype=jnp.float32,
)
am_time = jnp.array(
[
[0, 8, 13],
[8, 0, 7],
[13, 7, 0],
],
dtype=jnp.float32,
)
pm_time = jnp.array(
[
[0, 11, 18],
[11, 0, 9],
[18, 9, 0],
],
dtype=jnp.float32,
)
skims = JaxDataset(
{
"distance": distance,
"travel_time": jnp.stack([am_time, pm_time], axis=-1),
},
name="skims",
metadata={"time_periods": ["AM", "PM"]},
)
The two variables retain their independent shapes:
skims.distance.shape # (3, 3)
skims.travel_time.shape # (3, 3, 2)
skims["distance"] is skims.distance # True
skims.travel_time[:, :, 0] # the complete AM matrix
For model records with coded origin, destination, and period indices, JAX advanced indexing selects one LOS value per record:
origins = jnp.array([0, 2], dtype=jnp.int32)
destinations = jnp.array([2, 0], dtype=jnp.int32)
periods = jnp.array([1, 0], dtype=jnp.int32)
skims.travel_time[origins, destinations, periods]
# Array([18., 13.], dtype=float32)
The same dataset can be placed in a Store and accessed as store["skims"]. Model
code can then use the appropriate two- or three-dimensional variable without copying it into a
row-oriented table.
Describing the data model with TypeMaker#
A plain JaxDataset accepts any mapping of names to array-like values. For an application-specific
dataset, a subclass can declare which variables are expected and describe their shapes and dtypes
with TypeMaker.
TypeMaker binds a symbolic shape expression to JAX array types. Accessing a dtype attribute
produces a jaxtyping annotation with that shape and dtype; it does not allocate an array.
from traveler import TypeMaker
taz = TypeMaker("TAZ")
od = TypeMaker("TAZ TAZ")
odt = TypeMaker("TAZ TAZ TIMEPERIOD")
These three TypeMaker instances describe the shapes of the skims arrays, and can be used to
annotate the variables of a JaxDataset subclass. Each annotation also should add a dtype to
specify the expected data type of the array, using attributes on the TypeMaker instance. For
example:
taz.Int32means a one-dimensionalint32JAX array indexed byTAZ;od.Float32means a two-dimensionalfloat32JAX array whose twoTAZaxes have the same size; andodt.Float32means a three-dimensionalfloat32JAX array with origin TAZ, destination TAZ, and time-period axes.
Reusing a dimension name communicates that its size should be consistent wherever it appears. In
particular, "TAZ TAZ" describes a square origin-destination matrix. Different names such as
"ORIGIN DESTINATION" would describe two axes without asserting that they have the same size.
The annotations form the data model for a skim-specific subclass:
from traveler.trees import JaxDataset
class Skims(JaxDataset):
zone_id: taz.Int32
distance: od.Float32
travel_time: odt.Float32
We can now construct the typed dataset with the same arrays:
skims = Skims(
{
"zone_id": jnp.array([101, 102, 103], dtype=jnp.int32),
"distance": distance,
"travel_time": jnp.stack([am_time, pm_time], axis=-1),
},
name="skims",
metadata={"time_periods": ["AM", "PM"]},
)
skims.distance.shape # (3, 3)
skims.travel_time.dtype # dtype('float32')
This pattern puts domain vocabulary in one place. A loader is responsible for reading source data
and constructing the arrays, while the Skims class says which variables downstream model code
expects and how their axes and dtypes are intended to be organized. TypeMaker also provides
attributes for other common JAX dtypes, including Bool, Int8, Int16, Int64, and Float64.
skims.validate() after loading data to check required variables, dtypes, shapes, and the
consistency of named dimensions. Validation safely coerces a dtype by default only when no values
would be lost; pass coerce=False to require exact dtypes without conversion. Use skims.schema()
to inspect the shapes and dtypes of the arrays actually present in an instance. :::
When zone or time-period labels need to travel with the arrays, they can be stored as categorical variables or metadata. Numerical model calculations still index the LOS arrays with integer codes, keeping the data directly usable by JAX.