Skip to content

Add Affine and InvAffine for sparse latticed tensors - #15

Merged
ppraneth merged 4 commits into
mainfrom
sparse
Oct 23, 2026
Merged

ppraneth merged 4 commits into
mainfrom
sparse

Conversation

@ppraneth

Copy link
Copy Markdown
Collaborator

Closes #14: adds Affine (physical to virtual, basis·x + offset) and its inverse InvAffine, which holds the Affine it inverts.
A sparse latticed tensor is a Sum over InvAffine, so it reads 0 off the lattice, and sparse_latticed_tensor(physical, basis, offset, shape) builds one.
The examples from the subclass_zoo issue linked in #14 are added as tests.

@ppraneth
ppraneth requested a review from ValerianRey October 11, 2026 11:50

@ValerianRey ValerianRey left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very cool thanks! We still need to solve the complexity problem of the InvAffine.map. It's not trivial. I can help with that if you need.

Comment thread tests/mapping.rs
Comment on lines +134 to +152
// Dropping the middle axis is affine too, and sends several indices onto each
// one, which the inverse finds all of.
#[test]
fn an_inv_affine_of_a_projection_matches_the_inv_projection() {
let inv_affine = InvAffine::new(Affine::new(
HRect::new(vec![2, 3, 4]),
vec![vec![1, 0, 0], vec![0, 0, 1]],
vec![0, 0],
HRect::new(vec![2, 4]),
));
let inv_proj = InvProjection::new(HRect::new(vec![2, 3, 4]), 1);

for m in 0..2 {
for p in 0..4 {
assert_eq!(inv_affine.map(&[m, p]), inv_proj.map(&[m, p]));
}
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, I think this is a compilation example btw

Comment thread src/mapping.rs
Comment on lines +680 to +687
fn map(&self, index: &[usize]) -> Powerset {
// A scan of the codomain, which holds for any basis, whether several
// indices land on `index` or none do.
indices(self.signature.codomain().shape())
.into_iter()
.filter(|x| HRectMapping::map(&self.affine, x) == index)
.collect()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is not efficient enough. The complexity is O(k) where k is the number of elements of the codomain (which is typically much smaller than the domain, but still too large to make it ok).

I think we can change that into something that grows like O(m * n), where m is the number of domain dimensions and n is the number of codomain dimensions.

First, let's think of the simple case where f is the InvAffine mapping with m=n=1, with offset 0 and basis [[3]].
For example, the physical tensor would be P = [1 2 3 4 5] and the virtual tensor would be V = [1 0 0 2 0 0 3 0 0 4 0 0 5]

We can see that f is such that:
f(0) = {0}
f(1) = {}
f(2) = {}
f(3) = {1}
...
f(12) = {4}

More generally, we have f(i) = {i / 3} if i % 3 == 0, else {}

Note that things are a bit more complex when the shape is so that we stop using elements from the physical after some point (e.g. [1 0 0 2 0 0 3 0 0 4 0 0 5 0 0 0 0 0 ... 0]). We'd have to add a condition depending on the shape to handle those cases.

So we can see that for the base case m=n=1, this mapping can be implemented very efficiently (O(m * n) = O(1)).

Now the question is how to generalize this to arbitrary m and n?

Let's take now the example of a sparse "diagonal" matrix, with physical P=[1 2 3] and virtual V =
[[1 0 0 0 0 0 0]
[0 0 0 0 0 0 0]
[0 0 0 2 0 0 0]
[0 0 0 0 0 0 0]
[0 0 0 0 0 0 3]]

This corresponds to an InvMapping with m=2 and n=1 (still one phyiscal dimension but 2 virtual dimensions). The basis would be
[[2]
[3]]

i.e. when moving by 1 in the physical we move by 2 rows in the virtual and 3 columns in the virtual.

Here, the mapping would be:
f(0, 0) = {0}
f(2, 3) = {1}
f(4, 6) = {2}
f(x, y) = {} in all other cases

this can be translated into
f(x, y) = {x / 2} if 2y == 3x, else {}

See that the values 2 and 3 come from the basis.

Let's try to go in this direction and generalize even more (handle n > 1, handle offset, handle shape), while keeping a very efficient algorithm for this.

@ppraneth ppraneth Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think when the basis columns are independent (like in both your examples, diagonal, padding etc) we can just solve it directly, so each read is only a few divisions and checks.

The harder case is when there are more physical dims than that, like repeat or block-diagonal. There the shape bounds decide which x is the right one. My idea was to solve what we can and only loop over the leftover dims, which is like 2 or 3 steps in those examples.

What do u think? Do u have any other ideas for that case? I Would be happy to get ur help on this.

@ppraneth
ppraneth requested a review from ValerianRey October 13, 2026 03:19
@ppraneth
ppraneth merged commit a307be8 into main Oct 23, 2026
4 checks passed
@ppraneth
ppraneth deleted the sparse branch October 23, 2026 05:44
@ppraneth
ppraneth restored the sparse branch October 23, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add sparse latticed tensor support

2 participants