Dimensions, domains and tensors
Domains and tensors¶
For constructors, reductions, indexing and their current limitations, see the Tensor API reference.
So far, every value in our examples has been a scalar:
Real engineering models quickly need something more.
A temperature may vary with time. Demand may vary with location. A quantity may vary over both:
Bilt represents these values using tensors over named dimensions.
Dimensions give axes names¶
In NumPy, an array has a shape:
which might give:
That tells us how many elements lie along each axis, but not what those axes mean.
Perhaps (2, 3) means:
or:
Those are very different things.
In Bilt, we can give dimensions explicit names:
A dimension identifies one direction in which a value may vary.
We can then describe a domain containing those dimensions:
rather than relying only on positional axes:
The distinction becomes important when different values vary over different sets of dimensions.
Tensors carry values over a domain¶
A Bilt tensor combines concrete values with the dimensions they vary over.
For example:
from bilt import tensor
demand = tensor(
[
[10.0, 12.0, 14.0],
[8.0, 9.0, 11.0],
],
over=(location, time),
)
The underlying values have shape:
and the domain is:
So we can think of the tensor as:
The numbers still live in an ordinary array. The domain tells Bilt what each axis represents.
Scalars have a domain too¶
A scalar is simply a value that does not vary over any dimensions.
Its domain is empty:
So these values can be viewed consistently:
This gives Bilt one vocabulary for scalar and non-scalar values rather than treating arrays as a completely separate modelling system.
Named dimensions make broadcasting explicit¶
Suppose demand varies by location and time:
but electricity price varies only with time:
If we calculate:
the price for each time can be applied across every location.
Conceptually:
Bilt can align the values using the names of their dimensions, rather than requiring the user to arrange axes purely by position.
Broadcasting rules¶
Bilt broadcasting follows dimension names rather than axis positions.
A few simple rules cover most cases:
A scalar can be combined with any domain:
If one value is missing a dimension, it is broadcast across that dimension:
Partially overlapping domains are combined:
Shared dimensions are aligned by name. Their original axis positions do not need to match.
There is one important restriction: if two values share a dimension, they must agree about that dimension during the evaluation. Bilt will not silently treat two incompatible definitions of time, for example, as though they were the same axis.
For most model code, this means thinking about which dimensions a value varies over, rather than manually arranging array axes to make broadcasting work.
The relationship can stay simple¶
One useful consequence is that the mathematical relationship does not necessarily need to know about every surrounding dimension.
Imagine a calculation:
The relationship itself is simple:
If demand and price are evaluated over compatible domains, Bilt can handle the domain alignment around that calculation.
This lets the model express the engineering relationship without filling every transform with array-axis bookkeeping.
The dimensions belong to the values.
The transform describes what to do with them.
Dimensions also have extents¶
Knowing that a tensor varies over time is not enough to store concrete data.
Bilt also needs to know how large that dimension is.
For example:
A tensor over:
therefore has shape:
During an evaluation, Bilt keeps track of the concrete dimensions being used and checks that values which share a dimension agree about its extent.
If one value says:
while another says:
they cannot silently represent the same time dimension in that evaluation.
Catching that mismatch early is preferable to letting incompatible arrays make their way further into the model.
Coordinates give dimensions concrete meaning¶
Sometimes an extent is all we need.
Often we also care which coordinate each position represents.
For a time dimension, for example, we might have:
Bilt represents this with a coordinate axis associated with the dimension.
For example:
from datetime import datetime
from bilt import coordinate_axis
time_axis = coordinate_axis(
time,
(
datetime(2026, 1, 1),
datetime(2026, 2, 1),
datetime(2026, 3, 1),
),
)
The dimension still identifies the axis:
while the coordinate axis tells us what the individual positions along it mean.
This separation is useful because many values can share the same dimension and coordinate system.
Shared dimensions need to agree¶
Suppose two values both vary over time:
If they participate in the same evaluation, time should mean the same thing for both.
It would be dangerous if one secretly represented:
while the other represented:
simply because both arrays happened to contain three elements.
Bilt therefore tracks dimension information during evaluation so that values sharing a named dimension also share a compatible concrete definition of that dimension.
This is one of the main reasons Bilt uses named domains rather than treating tensors as bare arrays.
Domains describe variation, not meaning¶
There is a useful boundary to keep in mind.
A domain tells Bilt where a value varies:
It does not tell Bilt what the value itself means.
These two tensors might share exactly the same domain:
but they remain different model values.
Likewise, the same dimension may be shared by many values:
The domain describes their structure.
The system and its relationships describe their role in the model.
Domains become part of planning¶
In the previous chapter, structural analysis asked whether a requested value could be reached from the values already known.
With tensors, there is another question:
Are the values on that route compatible over their domains?
A structurally plausible calculation might require:
and produce something over:
That may be perfectly valid.
Another route might contain values that disagree about the concrete extent or coordinates of a shared dimension.
That route is not valid for the current evaluation.
So the path from model to execution now contains a little more information:
The important point for model authors is simpler:
Missing dimensions can broadcast. Shared dimensions align by name and must be compatible.
From tensors to larger systems¶
We now have most of the basic pieces needed to describe individual Bilt models:
systems define model boundaries
values live on those boundaries
transforms describe directed calculations
equations describe mathematical relationships
implementations collect the relationships for a system
domains describe where values vary
tensors carry concrete values over those domains
The next step is to make models larger without turning them into one enormous system.
Bilt does that by allowing systems to be composed from other systems, while preserving the same boundaries and relationships we have been using throughout the guide.
That is the subject of the next chapter.