1use crate::Addressed;
4use cubecl::zspace::{SmallVec, Tiling};
5
6use crate::{
7 Axis, Composition, DimsBuilder, Divisor, Geometry, Offset, PhysicalAxisMap, Refusal, Space,
8};
9
10#[derive(Clone, PartialEq, Eq, Hash, Debug)]
12pub struct Projection {
13 physical: SmallVec<[PhysicalAxisMap; Space::MAX_RANK]>,
15 axes: SmallVec<[Axis; Space::MAX_RANK]>,
17}
18
19impl Projection {
20 pub fn direct(axes: &[Axis]) -> Self {
22 Projection {
23 physical: axes.iter().map(|&a| PhysicalAxisMap::of(a)).collect(),
24 axes: SmallVec::from_slice(axes),
25 }
26 }
27
28 pub(crate) fn direct_over(space: &crate::Space) -> Self {
30 Projection::direct(&space.axes().collect::<Vec<_>>())
31 }
32
33 pub fn tiled(axes: &[Axis], labels: &[Axis]) -> Self {
36 let physical: Vec<PhysicalAxisMap> =
37 labels.iter().copied().map(PhysicalAxisMap::of).collect();
38 Projection::new(axes, &physical)
39 }
40
41 pub(crate) fn untiled(&self) -> Projection {
43 let carried = self.carried_groups();
44 let physical: Vec<PhysicalAxisMap> = carried
45 .iter()
46 .map(|&pa| self.physical[pa].clone())
47 .collect();
48 Projection::new(&self.axes, &physical)
49 }
50
51 pub fn coordinate_rank(&self) -> usize {
53 self.carried_groups().len()
54 }
55
56 pub(crate) fn positional(&self) -> Projection {
58 let carried = self.carried_groups();
59 let axes: Vec<Axis> = (0..carried.len()).map(|p| Axis(p as u8)).collect();
60 let physical: Vec<PhysicalAxisMap> = self
61 .physical
62 .iter()
63 .enumerate()
64 .map(|(pa, map)| {
65 let at = match map.addressed() {
66 Addressed::Broadcast => carried.iter().position(|&q| q == pa),
67 Addressed::By(axis) => carried
68 .iter()
69 .position(|&q| self.physical[q].addressed() == Addressed::By(axis)),
70 }
71 .expect("collected above");
72 PhysicalAxisMap::of(axes[at])
73 })
74 .collect();
75 Projection::new(&axes, &physical)
76 }
77
78 fn carried_groups(&self) -> Vec<usize> {
80 assert!(
81 self.is_invertible() || !self.is_tiled(),
82 "Projection: an affine map cannot also be storage-tiled; its physical axes do not \
83 group into coordinates"
84 );
85 let mut carried: Vec<usize> = Vec::new();
86 for (pa, map) in self.physical.iter().enumerate() {
87 match map.addressed() {
88 Addressed::Broadcast => carried.push(pa),
90 Addressed::By(axis) => {
91 if !carried
92 .iter()
93 .any(|&q| self.physical[q].addressed() == Addressed::By(axis))
94 {
95 carried.push(pa);
96 }
97 }
98 }
99 }
100 carried
101 }
102
103 pub fn tiling(&self) -> Tiling {
106 let fragments: Vec<usize> = self.axes.iter().map(|&axis| self.fragments(axis)).collect();
107 Tiling::new(&fragments)
108 .unwrap_or_else(|e| panic!("Projection::tiling: {fragments:?} fragments: {e:?}"))
109 }
110
111 pub(crate) fn is_tiled(&self) -> bool {
113 self.axes.iter().any(|&axis| self.fragments(axis) > 1)
114 }
115
116 fn fragments(&self, axis: Axis) -> usize {
119 match self.addresses(axis) {
120 false => 1,
121 true => self.carriers(axis).len(),
122 }
123 }
124
125 pub(crate) fn carried_by(
129 &self,
130 pa: usize,
131 axis: Axis,
132 ) -> (SmallVec<[usize; Space::MAX_RANK]>, Option<usize>) {
133 let carriers = self.carriers(axis);
134 assert!(
135 carriers.contains(&pa),
136 "Projection::carried_by: physical axis {pa} does not carry {axis:?} (carried by \
137 {carriers:?})"
138 );
139 let finer = carriers.iter().copied().filter(|&q| q > pa).collect();
140 (finer, (carriers[0] != pa).then_some(pa))
141 }
142
143 pub(crate) fn addresses(&self, axis: Axis) -> bool {
145 self.physical.iter().any(|m| m.addresses(axis))
146 }
147
148 pub(crate) fn carriers(&self, axis: Axis) -> SmallVec<[usize; Space::MAX_RANK]> {
150 let carriers: SmallVec<[usize; Space::MAX_RANK]> = (0..self.physical.len())
151 .filter(|&q| self.physical[q].terms().iter().any(|t| t.axis == axis))
152 .collect();
153 assert!(
154 !carriers.is_empty(),
155 "Projection::carriers: {axis:?} addresses no physical axis of this operand"
156 );
157 carriers
158 }
159
160 pub fn scales_per(&self, block: Axis) -> Projection {
163 let pa = self
164 .physical
165 .iter()
166 .position(|map| map.terms().iter().any(|term| term.axis == block))
167 .unwrap_or_else(|| {
168 panic!("Projection::scales_per: {block:?} addresses no dim of this operand")
169 });
170 let map = &self.physical[pa];
171 let terms = map.terms();
172 let at = terms.iter().position(|term| term.axis == block).unwrap();
173 assert!(
174 map.composition() == Composition::Disjoint && at + 1 < terms.len(),
175 "Projection::scales_per: {block:?} must partition its dim with an axis inside it \
176 (PhysicalAxisMap::disjoint(&[({block:?}, block), (inside, 1)]))"
177 );
178 let radix = terms[at].scale.get();
179 let kept: Vec<(Axis, usize)> = terms[..=at]
180 .iter()
181 .map(|term| (term.axis, term.scale.get() / radix))
182 .collect();
183 let omitted: Vec<Axis> = terms[at + 1..].iter().map(|term| term.axis).collect();
184 let mut physical = self.physical.clone();
185 physical[pa] = match kept.as_slice() {
186 [(axis, 1)] => PhysicalAxisMap::of(*axis),
187 _ => PhysicalAxisMap::disjoint(&kept),
188 };
189 Projection {
190 physical,
191 axes: self
192 .axes
193 .iter()
194 .copied()
195 .filter(|axis| !omitted.contains(axis))
196 .collect(),
197 }
198 }
199
200 pub fn new(axes: &[Axis], physical: &[PhysicalAxisMap]) -> Self {
202 Projection::of_maps(axes, physical.iter().cloned().collect())
203 }
204
205 pub(crate) fn of_maps(
207 axes: &[Axis],
208 physical: SmallVec<[PhysicalAxisMap; Space::MAX_RANK]>,
209 ) -> Self {
210 Projection {
211 physical,
212 axes: SmallVec::from_slice(axes),
213 }
214 }
215
216 pub(crate) fn is_direct(&self) -> bool {
218 self.physical.len() == self.axes.len()
219 && self
220 .physical
221 .iter()
222 .zip(self.axes.iter())
223 .all(|(map, &axis)| map.is_identity(axis))
224 }
225
226 pub(crate) fn dense_labels(&self) -> Vec<Axis> {
228 let mut labels: Vec<Axis> = self
229 .physical
230 .iter()
231 .rev()
232 .map_while(|map| match map.addressed() {
233 Addressed::By(axis) => Some(axis),
234 Addressed::Broadcast => None,
235 })
236 .collect();
237 labels.reverse();
238 labels
239 }
240
241 pub fn physical_rank(&self) -> usize {
242 self.physical.len()
243 }
244
245 pub(crate) fn logical_rank(&self) -> usize {
246 self.axes.len()
247 }
248
249 pub(crate) fn logical_axes(&self) -> &[Axis] {
250 &self.axes
251 }
252
253 pub fn position(&self, axis: Axis) -> usize {
255 self.axes
256 .iter()
257 .position(|&a| a == axis)
258 .expect("Projection::position: axis not spanned by this operand")
259 }
260
261 pub(crate) fn physical_axis(&self, pa: usize) -> &PhysicalAxisMap {
262 &self.physical[pa]
263 }
264
265 pub fn scale(&self, pa: usize, axis: Axis) -> usize {
267 self.physical[pa].scale(axis)
268 }
269
270 pub fn offset(&self, pa: usize) -> Offset {
272 self.physical[pa].offset()
273 }
274
275 pub(crate) fn divisor(&self, pa: usize) -> Divisor {
277 self.physical[pa].divisor()
278 }
279
280 pub(crate) fn is_rational(&self) -> bool {
282 self.physical.iter().any(|m| m.is_rational())
283 }
284
285 pub(crate) fn may_underflow(&self) -> bool {
287 self.physical.iter().any(|m| match m.offset() {
288 Offset::Static(o) => o < 0,
289 Offset::Dynamic => true,
290 })
291 }
292
293 pub(crate) fn span(&self, pa: usize, extent_of: impl Fn(Axis) -> usize) -> usize {
296 let map = &self.physical[pa];
297 let field: usize = map
298 .terms()
299 .iter()
300 .map(|t| (extent_of(t.axis) - 1) * t.scale.bound())
301 .sum();
302 1 + field.div_ceil(map.divisor().bound())
303 }
304
305 pub(crate) fn composition(&self) -> Composition {
307 match self
308 .physical
309 .iter()
310 .all(|m| m.composition() == Composition::Disjoint && m.divisor().is_unit())
311 {
312 true => Composition::Disjoint,
313 false => Composition::Overlapping,
314 }
315 }
316
317 pub(crate) fn validate_composition(&self, extent_of: impl Fn(Axis) -> usize) {
319 for (pa, map) in self.physical.iter().enumerate() {
320 let radices = map.claimed_radices();
321 if radices.is_empty() {
322 continue;
323 }
324 assert!(
325 map.offset() == Offset::Static(0),
326 "Projection: physical axis {pa} claims to be partitioned by its axes, but carries \
327 the offset {:?}; a partition starts at 0",
328 map.offset()
329 );
330 let mut expected = 1;
332 for (i, (axis, coefficient)) in radices.iter().enumerate().rev() {
333 assert!(
334 *coefficient == expected,
335 "Projection: physical axis {pa} claims its axes partition it, so {axis:?} \
336 must step by {expected} (the extents below it); it steps by {coefficient}"
337 );
338 if i == 0 {
339 break;
340 }
341 expected *= extent_of(*axis);
342 }
343 }
344 }
345
346 pub(crate) fn is_invertible(&self) -> bool {
347 self.physical.iter().all(|m| m.identity_axis().is_some())
348 }
349
350 pub(crate) fn validate(&self, vector_size: usize) -> Result<(), Refusal> {
352 if self.is_invertible() {
353 return Ok(());
354 }
355 if self.physical.is_empty() || self.axes.is_empty() {
356 return Err(Refusal::NoCoordinate);
357 }
358 if vector_size > 1 {
360 let innermost = self.axes[self.axes.len() - 1];
361 let last = &self.physical[self.physical.len() - 1];
362 let steps_by_lines = match last.composition() {
363 Composition::Disjoint => last.terms().last().map(|t| t.axis) == Some(innermost),
364 Composition::Overlapping => last.is_identity(innermost),
365 };
366 if !steps_by_lines {
367 return Err(Refusal::GatherInnermostNotInLines);
368 }
369 }
370 for &axis in self.axes.iter() {
371 let count = self.physical.iter().filter(|m| m.addresses(axis)).count();
373 if count > 1 {
374 return Err(Refusal::GatherAxisAddressedTwice(axis));
375 }
376 }
377 Ok(())
378 }
379
380 pub fn dims() -> DimsBuilder {
382 DimsBuilder::new()
383 }
384}
385
386impl Projection {
388 pub fn is_addressable(&self, geometry: &Geometry) -> bool {
390 if self.composition() == Composition::Overlapping {
391 return true;
392 }
393 geometry.addressing_dims_by_stride().windows(2).all(|pair| {
394 let (_, (_, stride)) = pair[0];
395 let (_, (finer_extent, finer_stride)) = pair[1];
396 stride >= finer_extent * finer_stride
397 })
398 }
399}
400
401impl Projection {
404 pub(crate) fn dynamic_scale_index(&self, pa: usize, t: usize) -> Option<usize> {
406 if !self.physical_axis(pa).terms()[t].scale.is_dynamic() {
407 return None;
408 }
409 let within = self.physical_axis(pa).terms()[..t]
410 .iter()
411 .filter(|term| term.scale.is_dynamic())
412 .count();
413 Some(self.coefficient_base(pa) + within)
414 }
415
416 pub(crate) fn dynamic_divisor_index(&self, pa: usize) -> Option<usize> {
418 if !self.physical_axis(pa).divisor().is_dynamic() {
419 return None;
420 }
421 Some(self.coefficient_base(pa) + self.physical_axis(pa).dynamic_scale_count())
422 }
423
424 fn coefficient_base(&self, pa: usize) -> usize {
426 (0..pa)
427 .map(|i| self.physical_axis(i))
428 .map(|m| m.dynamic_scale_count() + m.divisor().is_dynamic() as usize)
429 .sum()
430 }
431
432 pub(crate) fn dynamic_coefficient_count(&self) -> usize {
434 self.coefficient_base(self.physical_rank())
435 }
436
437 pub(crate) fn dynamic_offset_index(&self, pa: usize) -> Option<usize> {
439 if !self.physical_axis(pa).offset().is_dynamic() {
440 return None;
441 }
442 Some(
443 (0..pa)
444 .map(|i| self.physical_axis(i))
445 .filter(|m| m.offset().is_dynamic())
446 .count(),
447 )
448 }
449
450 pub(crate) fn dynamic_offset_count(&self) -> usize {
452 self.physical
453 .iter()
454 .filter(|m| m.offset().is_dynamic())
455 .count()
456 }
457
458 pub(crate) fn has_dynamic_scales(&self) -> bool {
460 self.physical.iter().any(|m| m.has_dynamic_scale())
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467 use crate::Scale;
468
469 #[test]
471 fn scales_per_block_omit_the_axis_inside_it() {
472 let values = Projection::new(
473 &[M, KB, KI],
474 &[
475 PhysicalAxisMap::of(M),
476 PhysicalAxisMap::disjoint(&[(KB, 32), (KI, 1)]),
477 ],
478 );
479 let scales = values.scales_per(KB);
480 assert_eq!(scales.logical_axes(), &[M, KB]);
481 assert_eq!(scales.physical_rank(), 2);
482 assert!(scales.physical[1].is_identity(KB));
483 }
484
485 #[test]
487 fn scales_per_a_middle_axis_keep_what_is_above_it() {
488 let values = Projection::new(
489 &[M, KO, KB, KI],
490 &[
491 PhysicalAxisMap::of(M),
492 PhysicalAxisMap::disjoint(&[(KO, 64), (KB, 32), (KI, 1)]),
493 ],
494 );
495 let per_block = values.scales_per(KB);
496 assert_eq!(per_block.logical_axes(), &[M, KO, KB]);
497 assert_eq!(per_block.scale(1, KO), 2);
498 assert_eq!(per_block.scale(1, KB), 1);
499 let per_outer = values.scales_per(KO);
500 assert_eq!(per_outer.logical_axes(), &[M, KO]);
501 assert!(per_outer.physical[1].is_identity(KO));
502 }
503
504 #[test]
505 #[should_panic(expected = "must partition its dim with an axis inside it")]
506 fn scales_per_an_unsplit_axis_are_refused() {
507 Projection::direct(&[M, KB]).scales_per(KB);
508 }
509
510 const A: Axis = Axis(0);
511 const B: Axis = Axis(1);
512 const R: Axis = Axis(2);
513
514 const M: Axis = Axis(3);
515 const KB: Axis = Axis(4);
516 const KI: Axis = Axis(5);
517 const KO: Axis = Axis(6);
518
519 #[test]
520 fn direct_is_direct() {
521 let p = Projection::direct(&[A, B]);
522 assert!(p.is_direct());
523 assert_eq!(p.physical_rank(), 2);
524 assert_eq!(p.logical_axes(), &[A, B]);
525 }
526
527 #[test]
528 fn direct_span_is_the_edge() {
529 let p = Projection::direct(&[A, B]);
530 assert_eq!(p.span(0, |_| 8), 8);
531 }
532
533 #[test]
534 fn affine_span_is_the_receptive_field() {
535 let p = Projection::new(
536 &[A, R, B],
537 &[
538 PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
539 PhysicalAxisMap::of(B),
540 ],
541 );
542 assert!(!p.is_direct());
543 assert_eq!(p.scale(0, A), 2);
544 assert_eq!(p.scale(0, R), 3);
545 assert_eq!(p.scale(0, B), 0);
546 assert_eq!(p.span(0, |a| if a == A { 4 } else { 3 }), 13);
548 let q = Projection::new(
549 &[A, R, B],
550 &[
551 PhysicalAxisMap::affine(&[(A, 1), (R, 1)]),
552 PhysicalAxisMap::of(B),
553 ],
554 );
555 assert_eq!(q.span(0, |a| if a == A { 4 } else { 1 }), 4);
556 }
557
558 #[test]
559 fn innermost_axis_rides_no_coarser_physical_axis() {
560 let refused = Projection::new(
561 &[A, B],
562 &[
563 PhysicalAxisMap::affine(&[(A, 1), (B, 2)]),
564 PhysicalAxisMap::of(B),
565 ],
566 )
567 .validate(4);
568 assert_eq!(refused, Err(Refusal::GatherAxisAddressedTwice(B)));
569 }
570
571 #[test]
572 fn innermost_must_be_identity() {
573 let refused = Projection::new(
574 &[A, R],
575 &[PhysicalAxisMap::of(A), PhysicalAxisMap::affine(&[(R, 2)])],
576 )
577 .validate(4);
578 assert_eq!(refused, Err(Refusal::GatherInnermostNotInLines));
579 }
580
581 #[test]
582 fn innermost_may_gather_when_scalar() {
583 Projection::new(
584 &[A, R],
585 &[PhysicalAxisMap::of(A), PhysicalAxisMap::affine(&[(R, 2)])],
586 )
587 .validate(1)
588 .unwrap();
589 }
590
591 #[test]
592 fn a_gather_still_rejects_tiled_storage_when_scalar() {
593 let refused = Projection::new(
594 &[A, R, B],
595 &[
596 PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
597 PhysicalAxisMap::of(B),
598 PhysicalAxisMap::of(B),
599 ],
600 )
601 .validate(1);
602 assert_eq!(refused, Err(Refusal::GatherAxisAddressedTwice(B)));
603 }
604
605 #[test]
606 fn a_gather_rejects_tiled_storage() {
607 let refused = Projection::new(
608 &[A, R, B],
609 &[
610 PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
611 PhysicalAxisMap::of(B),
612 PhysicalAxisMap::of(B),
613 ],
614 )
615 .validate(4);
616 assert_eq!(refused, Err(Refusal::GatherAxisAddressedTwice(B)));
617 }
618
619 #[test]
620 fn an_axis_addressing_nothing_is_a_broadcast() {
621 let p = Projection::new(
622 &[A, R, B],
623 &[PhysicalAxisMap::affine(&[(A, 2)]), PhysicalAxisMap::of(B)],
624 );
625 p.validate(4).unwrap();
626 assert!(!p.addresses(R));
627 assert!(p.addresses(A) && p.addresses(B));
628 }
629
630 #[test]
631 fn tiled_is_not_a_gather() {
632 let p = Projection::tiled(&[A, B], &[A, B, A, B]);
633 assert!(!p.is_direct());
634 assert!(p.is_tiled());
635 assert!(p.is_invertible());
636 p.validate(4).unwrap();
637 }
638
639 #[test]
640 fn a_gather_is_not_tiled() {
641 let p = Projection::new(
642 &[A, R, B],
643 &[
644 PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
645 PhysicalAxisMap::of(B),
646 ],
647 );
648 assert!(!p.is_tiled());
649 p.validate(4).unwrap();
650 }
651
652 #[test]
653 fn a_tiled_spec_matches_its_buffer() {
654 use crate::TileSpec;
655
656 const BATCH: Axis = Axis(3);
657 let spec = TileSpec::new(Projection::new(
658 &[BATCH, A, B],
659 &[BATCH, A, B, A, B].map(PhysicalAxisMap::of),
660 ));
661
662 assert_eq!(spec.projection.physical_rank(), 5);
663 let [p0, p1, p2] = [Axis(0), Axis(1), Axis(2)];
664 assert_eq!(
665 spec.projection.positional(),
666 Projection::tiled(&[p0, p1, p2], &[p0, p1, p2, p1, p2])
667 );
668 assert_eq!(
669 Projection::tiled(&[BATCH, A, B], &[BATCH, A, B, A, B]),
670 spec.projection
671 );
672 assert_eq!(spec.projection.tiling(), Tiling::new(&[1, 2, 2]).unwrap());
673 }
674
675 #[test]
676 fn positional_of_a_plain_operand_is_the_identity() {
677 assert_eq!(
678 Projection::direct(&[B, A]).positional(),
679 Projection::direct(&[Axis(0), Axis(1)])
680 );
681 }
682
683 #[test]
684 fn positional_of_a_gather_drops_the_affine_terms() {
685 let p = Projection::new(
686 &[A, R, B],
687 &[
688 PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
689 PhysicalAxisMap::of(B),
690 ],
691 );
692 assert_eq!(p.positional(), Projection::direct(&[Axis(0), Axis(1)]));
693 }
694
695 #[test]
696 fn a_ragged_tiling_orders_level_major() {
697 let p = Projection::tiled(&[A, B], &[A, B, A, B, A]);
698 assert_eq!(p.physical_rank(), 5);
699 assert_eq!(p.carriers(A).as_slice(), &[0, 2, 4]);
701 assert_eq!(p.carriers(B).as_slice(), &[1, 3]);
702 assert_eq!(p.carried_by(0, A), (SmallVec::from_slice(&[2, 4]), None));
703 assert_eq!(p.carried_by(2, A), (SmallVec::from_slice(&[4]), Some(2)));
704 assert_eq!(p.carried_by(4, A), (SmallVec::new(), Some(4)));
705 assert_eq!(p.carried_by(1, B), (SmallVec::from_slice(&[3]), None));
706 assert!(p.is_invertible());
707 p.validate(4).unwrap();
708 }
709
710 #[test]
712 fn carried_coordinates_weigh_back_to_the_coordinate() {
713 let shape = [3usize, 8];
714 let p = Projection::new(&[A], &[PhysicalAxisMap::of(A), PhysicalAxisMap::of(A)]);
715 let block = |pa| {
716 p.carried_by(pa, A)
717 .0
718 .iter()
719 .map(|&q| shape[q])
720 .product::<usize>()
721 };
722
723 for coord in [0usize, 1, 7, 8, 9, 19, 23] {
724 let physical = |pa| match p.carried_by(pa, A).1 {
725 Some(m) => (coord / block(pa)) % shape[m],
726 None => coord / block(pa),
727 };
728 assert_eq!(physical(0) * block(0) + physical(1) * block(1), coord);
729 }
730 }
731
732 #[test]
734 fn tiling_reads_back_the_counts_tiled_was_built_from() {
735 for fragments in [[1, 1, 1], [3, 3, 3], [1, 2, 2], [3, 1, 2]] {
736 let axes: Vec<Axis> = (0..3).map(|p| Axis(p as u8)).collect();
737 let labels = crate::StoragePartitioning::level_major(&axes, &fragments);
738 let p = Projection::tiled(&axes, &labels);
739 assert_eq!(p.tiling(), Tiling::new(&fragments).unwrap());
740 assert_eq!(p.physical_rank(), fragments.iter().sum::<usize>());
741 assert_eq!(p.is_tiled(), fragments.iter().any(|&n| n > 1));
742 }
743 }
744
745 #[test]
746 fn is_invertible_false_for_a_stencil_map() {
747 let p = Projection::new(
748 &[A, R, B],
749 &[
750 PhysicalAxisMap::affine(&[(A, 2), (R, 3)]),
751 PhysicalAxisMap::of(B),
752 ],
753 );
754 assert!(!p.is_invertible());
755 assert!(Projection::direct(&[A, B]).is_invertible());
756
757 let with_offset = Projection::new(
758 &[A, B],
759 &[
760 PhysicalAxisMap::affine(&[(A, 1)]).shifted(-1),
761 PhysicalAxisMap::of(B),
762 ],
763 );
764 assert!(!with_offset.is_invertible());
765 assert_eq!(with_offset.offset(0), Offset::Static(-1));
766 assert_eq!(with_offset.offset(1), Offset::Static(0));
767 }
768
769 #[test]
770 fn may_underflow_tracks_negative_offsets_only() {
771 let padded = Projection::new(
772 &[A, R, B],
773 &[
774 PhysicalAxisMap::affine(&[(A, 2), (R, 1)]).shifted(-1),
775 PhysicalAxisMap::of(B),
776 ],
777 );
778 assert!(padded.may_underflow());
779
780 let shifted = Projection::new(
781 &[A, R, B],
782 &[
783 PhysicalAxisMap::affine(&[(A, 2), (R, 1)]).shifted(1),
784 PhysicalAxisMap::of(B),
785 ],
786 );
787 assert!(!shifted.may_underflow());
788 assert!(!Projection::direct(&[A, B]).may_underflow());
789
790 let dynamic_offset = Projection::new(
791 &[A, R, B],
792 &[
793 PhysicalAxisMap::affine(&[(A, 2), (R, 1)]).shifted(Offset::Dynamic),
794 PhysicalAxisMap::of(B),
795 ],
796 );
797 assert!(dynamic_offset.may_underflow());
798 }
799
800 #[test]
802 fn dynamic_terms_index_in_projection_order() {
803 let p = Projection::new(
804 &[A, R, B],
805 &[
806 PhysicalAxisMap::scaled(&[
807 (A, Scale::Dynamic { max: 2 }),
808 (R, Scale::Dynamic { max: 2 }),
809 ]),
810 PhysicalAxisMap::of(B),
811 ],
812 );
813 assert_eq!(p.dynamic_coefficient_count(), 2);
814 assert_eq!(p.dynamic_offset_count(), 0);
815 assert_eq!(p.dynamic_scale_index(0, 0), Some(0));
816 assert_eq!(p.dynamic_scale_index(0, 1), Some(1));
817 assert_eq!(p.dynamic_offset_index(0), None);
818 assert_eq!(p.dynamic_scale_index(1, 0), None);
819
820 let mixed = Projection::new(
821 &[A, R, B],
822 &[
823 PhysicalAxisMap::scaled(&[(A, Scale::Static(2)), (R, Scale::Dynamic { max: 2 })]),
824 PhysicalAxisMap::of(B),
825 ],
826 );
827 assert_eq!(mixed.dynamic_coefficient_count(), 1);
828 assert_eq!(mixed.dynamic_scale_index(0, 0), None);
829 assert_eq!(mixed.dynamic_scale_index(0, 1), Some(0));
830 assert_eq!(mixed.dynamic_offset_index(0), None);
831
832 let with_dynamic_offset = Projection::new(
833 &[A, R, B],
834 &[
835 PhysicalAxisMap::scaled(&[(A, Scale::Dynamic { max: 2 }), (R, Scale::Static(1))])
836 .shifted(Offset::Dynamic),
837 PhysicalAxisMap::of(B),
838 ],
839 );
840 assert_eq!(with_dynamic_offset.dynamic_coefficient_count(), 1);
841 assert_eq!(with_dynamic_offset.dynamic_offset_count(), 1);
842 assert_eq!(with_dynamic_offset.dynamic_scale_index(0, 0), Some(0));
843 assert_eq!(with_dynamic_offset.dynamic_scale_index(0, 1), None);
844 assert_eq!(with_dynamic_offset.dynamic_offset_index(0), Some(0));
845 assert_eq!(with_dynamic_offset.dynamic_offset_index(1), None);
846
847 let two_offsets = Projection::new(
848 &[A, R, B],
849 &[
850 PhysicalAxisMap::affine(&[(A, 2), (R, 1)]).shifted(Offset::Dynamic),
851 PhysicalAxisMap::scaled(&[(B, Scale::Dynamic { max: 2 })]).shifted(Offset::Dynamic),
852 ],
853 );
854 assert_eq!(two_offsets.dynamic_offset_index(0), Some(0));
855 assert_eq!(two_offsets.dynamic_offset_index(1), Some(1));
856 assert_eq!(two_offsets.dynamic_scale_index(1, 0), Some(0));
857 }
858
859 #[test]
860 fn a_dynamic_coefficient_is_not_invertible() {
861 let p = Projection::new(
862 &[A, B],
863 &[
864 PhysicalAxisMap::scaled(&[(A, Scale::Dynamic { max: 2 })]),
865 PhysicalAxisMap::of(B),
866 ],
867 );
868 assert!(!p.is_invertible());
869 p.validate(4).unwrap();
870 }
871
872 #[test]
873 fn rational_projection_properties() {
874 let p = Projection::new(
875 &[A, R, B],
876 &[
877 PhysicalAxisMap::affine(&[(A, 100), (R, 133)])
878 .shifted(-50)
879 .over(133),
880 PhysicalAxisMap::of(B),
881 ],
882 );
883 assert!(p.is_rational());
884 assert_eq!(p.divisor(0), Divisor::Static(133));
885 assert_eq!(p.divisor(1), Divisor::Static(1));
886 assert_eq!(p.span(1, |_| 8), 8);
887 }
888
889 #[test]
890 fn rational_axis_conservative_span() {
891 let p = Projection::new(
892 &[A, R, B],
893 &[
894 PhysicalAxisMap::affine(&[(A, 100), (R, 133)])
895 .shifted(-50)
896 .over(133),
897 PhysicalAxisMap::of(B),
898 ],
899 );
900 assert_eq!(p.span(0, |a| if a == A { 4 } else { 1 }), 4);
902 assert_eq!(p.span(0, |a| if a == A { 4 } else { 2 }), 5);
904 }
905
906 #[test]
907 fn a_dynamic_divisor_spans_against_its_min() {
908 let bounded = |min| {
909 Projection::new(
910 &[A, R, B],
911 &[
912 PhysicalAxisMap::affine(&[(A, 100), (R, 133)])
913 .shifted(-50)
914 .over(Divisor::Dynamic { min }),
915 PhysicalAxisMap::of(B),
916 ],
917 )
918 };
919 let extents = |a| if a == A { 4 } else { 1 };
921 assert_eq!(bounded(133).span(0, extents), 4);
922 assert_eq!(bounded(100).span(0, extents), 4);
923 assert_eq!(bounded(50).span(0, extents), 7);
924 let exact = |d| {
925 Projection::new(
926 &[A, R, B],
927 &[
928 PhysicalAxisMap::affine(&[(A, 100), (R, 133)])
929 .shifted(-50)
930 .over(d),
931 PhysicalAxisMap::of(B),
932 ],
933 )
934 .span(0, extents)
935 };
936 for d in 50..160 {
937 assert!(exact(d) <= bounded(50).span(0, extents));
938 }
939 }
940
941 #[test]
942 fn a_dynamic_coefficient_spans_against_its_max() {
943 let p = Projection::new(
944 &[A, R, B],
945 &[
946 PhysicalAxisMap::scaled(&[(A, Scale::Dynamic { max: 3 }), (R, Scale::Static(1))]),
947 PhysicalAxisMap::of(B),
948 ],
949 );
950 assert_eq!(p.span(0, |a| if a == A { 4 } else { 2 }), 11);
952 let at_max = Projection::new(
953 &[A, R, B],
954 &[
955 PhysicalAxisMap::affine(&[(A, 3), (R, 1)]),
956 PhysicalAxisMap::of(B),
957 ],
958 );
959 assert_eq!(at_max.span(0, |a| if a == A { 4 } else { 2 }), 11);
960 }
961
962 #[test]
963 fn dynamic_divisor_indexing() {
964 let p = Projection::new(
965 &[A, R, B],
966 &[
967 PhysicalAxisMap::scaled(&[(A, Scale::Dynamic { max: 2 }), (R, Scale::Static(1))])
968 .over(Divisor::Dynamic { min: 4 }),
969 PhysicalAxisMap::scaled(&[(B, Scale::Dynamic { max: 2 })]),
970 ],
971 );
972 assert_eq!(p.dynamic_coefficient_count(), 3);
973 assert_eq!(p.dynamic_scale_index(0, 0), Some(0));
974 assert_eq!(p.dynamic_scale_index(0, 1), None);
975 assert_eq!(p.dynamic_divisor_index(0), Some(1));
976 assert_eq!(p.dynamic_scale_index(1, 0), Some(2));
977 }
978
979 #[test]
980 fn a_partition_of_higher_logical_rank_is_still_dense() {
981 let blocked = Projection::new(
982 &[A, B, R],
983 &[
984 PhysicalAxisMap::of(A),
985 PhysicalAxisMap::disjoint(&[(B, 32), (R, 1)]),
986 ],
987 );
988 assert_eq!(blocked.composition(), Composition::Disjoint);
989 assert!(!blocked.is_direct());
990
991 let gathered = Projection::new(
992 &[A, B, R],
993 &[
994 PhysicalAxisMap::of(A),
995 PhysicalAxisMap::affine(&[(B, 32), (R, 1)]),
996 ],
997 );
998 assert_eq!(gathered.composition(), Composition::Overlapping);
999 }
1000
1001 #[test]
1002 fn a_partition_whose_radices_match_the_extents_is_accepted() {
1003 let extents = |axis| match axis {
1004 x if x == B => 4,
1005 x if x == R => 32,
1006 _ => 8,
1007 };
1008 Projection::new(
1009 &[A, B, R],
1010 &[
1011 PhysicalAxisMap::of(A),
1012 PhysicalAxisMap::disjoint(&[(B, 32), (R, 1)]),
1013 ],
1014 )
1015 .validate_composition(extents);
1016 }
1017
1018 #[test]
1019 #[should_panic(expected = "must step by 32")]
1020 fn a_partition_whose_radices_contradict_the_extents_is_refused() {
1021 let extents = |axis| match axis {
1022 x if x == B => 4,
1023 x if x == R => 32,
1024 _ => 8,
1025 };
1026 Projection::new(
1027 &[A, B, R],
1028 &[
1029 PhysicalAxisMap::of(A),
1030 PhysicalAxisMap::disjoint(&[(B, 16), (R, 1)]),
1031 ],
1032 )
1033 .validate_composition(extents);
1034 }
1035}
1036
1037#[cfg(test)]
1039mod geometry_tests {
1040 use super::*;
1041
1042 const K: Axis = Axis(0);
1043 const N: Axis = Axis(1);
1044 const B: Axis = Axis(2);
1045 const H: Axis = Axis(3);
1046 const S: Axis = Axis(4);
1047 const D: Axis = Axis(5);
1048 const M: Axis = Axis(6);
1049 const C: Axis = Axis(7);
1050
1051 fn geometry(dims: &[(usize, usize)]) -> Geometry {
1052 Geometry::new(dims)
1053 }
1054
1055 #[test]
1056 fn padding_is_addressable() {
1057 let p = Projection::direct(&[K, N]);
1058 let padded = geometry(&[(4096, 11136), (11008, 1)]);
1059
1060 assert!(p.is_addressable(&padded));
1061 }
1062
1063 #[test]
1064 fn a_narrowed_row_aliases() {
1065 let p = Projection::direct(&[K, N]);
1066 let narrowed = geometry(&[(4096, 4096), (11008, 1)]);
1067
1068 assert!(!p.is_addressable(&narrowed));
1069 }
1070
1071 #[test]
1072 fn an_in_place_cache_is_addressable_padded_or_permuted() {
1073 let p = Projection::direct(&[B, H, S, D]);
1074
1075 let padded = geometry(&[(2, 8 * 4096 * 136), (8, 4096 * 136), (4096, 136), (128, 1)]);
1076 assert!(p.is_addressable(&padded));
1077
1078 let permuted = geometry(&[(2, 8 * 4096 * 128), (8, 128), (4096, 8 * 128), (128, 1)]);
1079 assert!(p.is_addressable(&permuted));
1080 }
1081
1082 #[test]
1083 fn a_window_is_exempt_from_the_aliasing_check() {
1084 let p = Projection::dims()
1085 .dim(B)
1086 .dim(crate::layout::projection::dims_builder::stencil(&[(M, 2), (K, 1)]).pad(1))
1087 .dim(C)
1088 .build();
1089 let g = geometry(&[(8, 64 * 32), (64, 32), (32, 1)]);
1090
1091 assert_eq!(p.composition(), Composition::Overlapping);
1092 assert!(p.is_addressable(&g));
1093 }
1094}