Skip to main content

sim_lib_pitch_serial/
matrix.rs

1//! Convention-explicit twelve-tone row matrices.
2
3use sim_lib_pitch_core::PitchClass;
4
5use crate::{RowFamily, RowForm, RowLabel, RowLabelConvention, RowOperation, ToneRow};
6
7/// The width and height of every twelve-tone row matrix.
8pub const ROW_MATRIX_SIZE: usize = 12;
9
10/// A validated zero-based coordinate in a twelve-tone matrix.
11#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
12pub struct MatrixCoordinate {
13    row: u8,
14    column: u8,
15}
16
17impl MatrixCoordinate {
18    /// Constructs a coordinate, returning `None` outside the 12-by-12 matrix.
19    pub const fn new(row: usize, column: usize) -> Option<Self> {
20        if row < ROW_MATRIX_SIZE && column < ROW_MATRIX_SIZE {
21            Some(Self {
22                row: row as u8,
23                column: column as u8,
24            })
25        } else {
26            None
27        }
28    }
29
30    /// Returns the zero-based row index.
31    pub const fn row(self) -> usize {
32        self.row as usize
33    }
34
35    /// Returns the zero-based column index.
36    pub const fn column(self) -> usize {
37        self.column as usize
38    }
39}
40
41/// One pitch-class cell paired with its matrix coordinate.
42#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
43pub struct RowMatrixCell {
44    coordinate: MatrixCoordinate,
45    class: PitchClass,
46}
47
48impl RowMatrixCell {
49    pub(crate) const fn new(coordinate: MatrixCoordinate, class: PitchClass) -> Self {
50        Self { coordinate, class }
51    }
52
53    /// Returns this cell's zero-based coordinate.
54    pub const fn coordinate(self) -> MatrixCoordinate {
55        self.coordinate
56    }
57
58    /// Returns the pitch class stored in this cell.
59    pub const fn class(self) -> PitchClass {
60        self.class
61    }
62}
63
64/// Labels printed on the four edges of a row matrix.
65///
66/// Left-to-right rows are P forms and right-to-left rows are R forms. Top-to-
67/// bottom columns are I forms and bottom-to-top columns are RI forms.
68#[derive(Copy, Clone, Debug, PartialEq, Eq)]
69pub struct RowMatrixEdgeLabels {
70    top: [RowLabel; ROW_MATRIX_SIZE],
71    right: [RowLabel; ROW_MATRIX_SIZE],
72    bottom: [RowLabel; ROW_MATRIX_SIZE],
73    left: [RowLabel; ROW_MATRIX_SIZE],
74}
75
76impl RowMatrixEdgeLabels {
77    /// Returns the I labels above the columns, ordered left to right.
78    pub const fn top(&self) -> &[RowLabel; ROW_MATRIX_SIZE] {
79        &self.top
80    }
81
82    /// Returns the R labels beside the rows, ordered top to bottom.
83    pub const fn right(&self) -> &[RowLabel; ROW_MATRIX_SIZE] {
84        &self.right
85    }
86
87    /// Returns the RI labels below the columns, ordered left to right.
88    pub const fn bottom(&self) -> &[RowLabel; ROW_MATRIX_SIZE] {
89        &self.bottom
90    }
91
92    /// Returns the P labels beside the rows, ordered top to bottom.
93    pub const fn left(&self) -> &[RowLabel; ROW_MATRIX_SIZE] {
94        &self.left
95    }
96}
97
98/// A conventional twelve-tone matrix retaining its source and label policy.
99///
100/// The first row is the source row (`P0` by operation identity). Each matrix
101/// row is a P form, each column is an I form, and the reverse readings are the
102/// corresponding R and RI forms. Operations stay algebraic; edge labels are
103/// projections through the matrix's explicit [`RowLabelConvention`].
104#[derive(Clone, Debug, PartialEq, Eq)]
105pub struct RowMatrix {
106    source: ToneRow,
107    convention: RowLabelConvention,
108    cells: [[PitchClass; ROW_MATRIX_SIZE]; ROW_MATRIX_SIZE],
109    row_operations: [RowOperation; ROW_MATRIX_SIZE],
110    column_operations: [RowOperation; ROW_MATRIX_SIZE],
111    edge_labels: RowMatrixEdgeLabels,
112}
113
114impl RowMatrix {
115    /// Constructs the matrix for `source` under an explicit edge-label convention.
116    pub fn new(source: &ToneRow, convention: RowLabelConvention) -> Self {
117        let source_first = source.classes()[0].value();
118        let row_operations = source.classes().map(|class| {
119            RowOperation::new(
120                RowFamily::P,
121                subtract_mod_twelve(source_first, class.value()),
122            )
123        });
124        let column_operations = source
125            .classes()
126            .map(|class| RowOperation::new(RowFamily::I, (source_first + class.value()) % 12));
127        let cells = std::array::from_fn(|row| *source.apply(row_operations[row]).classes());
128
129        debug_assert!((0..ROW_MATRIX_SIZE).all(|column| {
130            let expected = source.apply(column_operations[column]);
131            (0..ROW_MATRIX_SIZE).all(|row| cells[row][column] == expected.classes()[row])
132        }));
133
134        let edge_labels = RowMatrixEdgeLabels {
135            top: column_operations.map(|operation| source.apply(operation).label(convention)),
136            right: row_operations.map(|operation| {
137                source
138                    .apply(RowOperation::new(RowFamily::R, operation.addend))
139                    .label(convention)
140            }),
141            bottom: column_operations.map(|operation| {
142                source
143                    .apply(RowOperation::new(RowFamily::RI, operation.addend))
144                    .label(convention)
145            }),
146            left: row_operations.map(|operation| source.apply(operation).label(convention)),
147        };
148
149        Self {
150            source: source.clone(),
151            convention,
152            cells,
153            row_operations,
154            column_operations,
155            edge_labels,
156        }
157    }
158
159    /// Returns the source row retained by this matrix.
160    pub const fn source(&self) -> &ToneRow {
161        &self.source
162    }
163
164    /// Returns the label convention used by every edge label.
165    pub const fn convention(&self) -> RowLabelConvention {
166        self.convention
167    }
168
169    /// Returns all matrix pitch classes in row-major layout.
170    pub const fn cells(&self) -> &[[PitchClass; ROW_MATRIX_SIZE]; ROW_MATRIX_SIZE] {
171        &self.cells
172    }
173
174    /// Returns one coordinate-bearing cell.
175    pub const fn cell(&self, coordinate: MatrixCoordinate) -> RowMatrixCell {
176        RowMatrixCell::new(
177            coordinate,
178            self.cells[coordinate.row()][coordinate.column()],
179        )
180    }
181
182    /// Returns one left-to-right matrix row, or `None` for an invalid index.
183    pub fn row(&self, row: usize) -> Option<&[PitchClass; ROW_MATRIX_SIZE]> {
184        self.cells.get(row)
185    }
186
187    /// Returns one top-to-bottom matrix column, or `None` for an invalid index.
188    pub fn column(&self, column: usize) -> Option<[PitchClass; ROW_MATRIX_SIZE]> {
189        (column < ROW_MATRIX_SIZE).then(|| std::array::from_fn(|row| self.cells[row][column]))
190    }
191
192    /// Returns the P operation read left to right on one row.
193    pub fn row_operation(&self, row: usize) -> Option<RowOperation> {
194        self.row_operations.get(row).copied()
195    }
196
197    /// Returns the I operation read top to bottom on one column.
198    pub fn column_operation(&self, column: usize) -> Option<RowOperation> {
199        self.column_operations.get(column).copied()
200    }
201
202    /// Reconstructs the operation-bearing P form for one matrix row.
203    pub fn row_form(&self, row: usize) -> Option<RowForm> {
204        self.row_operation(row)
205            .map(|operation| self.source.apply(operation))
206    }
207
208    /// Reconstructs the operation-bearing I form for one matrix column.
209    pub fn column_form(&self, column: usize) -> Option<RowForm> {
210        self.column_operation(column)
211            .map(|operation| self.source.apply(operation))
212    }
213
214    /// Returns all four convention-dependent edge-label collections.
215    pub const fn edge_labels(&self) -> &RowMatrixEdgeLabels {
216        &self.edge_labels
217    }
218}
219
220const fn subtract_mod_twelve(left: u8, right: u8) -> u8 {
221    (left + 12 - right) % 12
222}