---
# System prepended metadata

title: Draft design document for dim support in pytensor

---

# Draft design document for dim support in pytensor

## What do we mean by dim support?

- PyMC has had support for named dimensions of random variables for some time
- This information is currently not represented in the pytensor backend, so we
  can not take advantage of it to check shapes or for indexing operations.
- The dimensions should be first class objects. It does not make sense to add a
  vector with dim `country` to a vector with dim `experiment`, even if both
  happen to have the same length.
- Broadcasting with named dimensions is different from shapes: With shape only
  the order of the axes is used for broadcasting, while the identity of the
  dimensions is used when we use dims.

I think we probably want to achieve an API that looks something like this:

```python
country = pt.Dim("country")
age = pt.Dim("age")
country_effect = pt.named.dvector(dims=country)
age_effect = pt.named.dvector(dims=age)
effect = country_effect + age_effect   # dims=[country, age]

# Go from new api to old
effect_old_api = effect.to_shaped([country, age])

effect.ravel()  # dims={ProductDim({country, age})}

effect.rename_dim(country) + effect  # dims=(RenamedDim(country), country, age)

# Go from old api to new
second_dim = pt.Dim()  # Should names be required for a dim?
effects_imported = pt.named.from_shaped(effect_old_api, dims=[county, second_dim])

effects_imported.sum(second_dim)

effects.sel(country=["USA", "Italy"])  # dims=(SelDim(country, ["USA", "Italy"]), age)
```

## Naming?

Do we want to call this "named tensor", or "dim tensor" or something else?
I'm kind of used to "dim" right now, but I actually think "named-" sounds
better?

## How do we represent a dimension?

```python
@dataclass(frozen=True)
class Dim:
    name: str
    size: ScalarVariable(pytensor.scalar.ScalarType("uint64"))
    size_hint: int
    index_type: Optional[Type[xr.IndexVariable]]
    unique_index: bool

    def __eq__(self, other):
        return self is other

    def __init__(self, name, *, size_hint=10, size: int = None, index_type=None, unique_index=False):
        self.name = name
        if isinstance(size, int):
            size_hint = size
        self.size_hint = size_hint
        if size is None:
            size = pytensor.scalar.ScalarVariable(pytensor.scalar.ScalarType("uint64"), name=name)
        self.size = pytensor.scalar.as_scalar(size, dtype="uint64")
        self.index_type = index_type
        self.unique_index = unique_index
```

For many Ops we will also need derived dimensions, such as:

```python
# (The dataclasses aren't quite accurate yet I think. Maybe we
# want an abstract dim class, and other dims inherit from that
# instead from the Dim above)?

# The resulting dimension of a `ravel` or some reshape operations.
@dataclass(frozen=True)
class ProductDim(Dim):
    items: List[Dim]

# The dimension that represents a slice of some other dimension,
# where the slice refers to indices (is xarray isel with a slice)
@dataclass(frozen=True)
class SliceDim(Dim):
    base: Dim
    slice: slice

# The dimension that represents a slice of some other dimension,
# based on labels (ie xarray sel with a slice)
@dataclass(frozen=True)
class SliceDim(Dim):
    base: Dim
    slice: slice

# The result of concatenating different dimensions
@dataclass(frozen=True)
class ConcatDim(Dim):
    items: Tuple[Dim, ...]

@dataclass(frozen=True)
class ISelDim(Dim):
    base: Dim
    isel: Tuple[int, ...]

@dataclass(frozen=True)
class SelDim(Dim):
    base: Dim
    sel: Tuple[Any, ...]

# Basically the same dimension, but with a different identity. We need
# this so that we can work with objects like covariance matrices
# that contain the same dimension more than onces, which I think
# we should not allow directly.
@dataclass(frozen=True)
class RenameDim(Dim):
    orig: Dim
    

# Maybe we need something like this? A dim that doesn't
# just depend on other dims but directly the shape of some
# non-named tensor? I think something along those lines
# is needed to represent things like
# `normal(size=poisson())`.
@dataclass(frozen=True)
class DynamicDim(Dim):
    size_of: Variable
```

## What other new types do we need?

- `Variables`: I think we need a new type `NamedTensorVariable`. I don't think
  this can reasonably be a subtype of our current `TensorVariable`, because
  values of those types behave differently (for instance they behave
  differently when multiplied). But maybe we could refactor a bit so that there
  is a common super type (called `TensorVariable`) and two inheriting classes
  `NamedTensorVariable` and `UnnamedTensorVariable`, where the second is the
  current `TensorVariable`. 
- `TensorType`: Does the same hold for TensorTypes? I'm actually not sure,
  maybe we could get away with just adding a `dims` attribute that can be None
  for `UnnamedTensorVariables`?

I guess most Ops can be generic and accept either (all?) NamedTensorVariables
or UnnamedTensorVariables. But this probably involves changing all `make_node`
methods?

There would probably also need to be a couple of new functions in a namespace
like `pytensor.named_tensor` that only make sense for named tensors, and
conversely some functions in `pytensor.tensor` might want to check that the
input is a `UnnamedTensorVariable`.


## What do we need to change about Ops?

We need some additional information from each Op, that is currently
unavailable.

If we compute a matrix vector product for instance using a `Dot` op, we need to
know what which dimensions the output will have. Or for a `reshape` op, we need
to get the information which dimensions of the output correspond to which
`ProductDim`.

I think the correct place for this information is in the `make_node` method, or
some other similar mechanism that replaces the `make_node` function:
- We have previously discussed changing this to a signature object, and using
  minikanren to figure out type parameters of the Op.
- Alternatively we could simply incorporate it into the existing `make_node`
  functions, they could simply check if the input variables are named or
  unnamed, probably raise an error if this is not consistent and choose
  corresponding output variables with the correct dimensions in their types.
- We could also come up with a dispatch mechanism similar to the backend
  implementations for the current `make_node` method, that also dispatches by
  the type of the input variables.

In general, I think there is a lot of overlap with these changes and shape
inference and Op signatures using minikanren.

## Rewrites

I haven't really though about consequences this might have for rewrites. Would
some of them have to be changed so that they can work with
`NamedTensorVariables`?

Maybe we could also just start with a rewrite that replaces
`NamedTensorVariables` with corresponding `UnnamedTensorVariables` and keep all
later rewrites exactly as they are? But Could dimension information maybe
sometimes be useful for rewrites?
