Lattice Points¶
While single-generator tunings like the “equal division tuning” family use integers for pitch indices, pitch class indices, and pitch differences, multi-generator tunings use vectors of integers that are called Lattice Points.
The semantics of a lattice point, i.e., what frequency or frequency ratio a specific vector represents, depends on the tuning, more specifically on the generator intervals of that tuning. That’s why lattice points are typically created from the tuning context:
from xenharmlib import PrimeLimitTuning
limit5 = PrimeLimitTuning(5)
lp = limit5.lattice.point((-2, 0, 1))
print(lp)
LatticePoint(-2, 0, 1)
The generator interval ratios can be inspected like this:
print(lp.base)
(FrequencyRatio(2), FrequencyRatio(3), FrequencyRatio(5))
In this example, the lattice point represents the following frequency ratio:
\(2^{-2} \cdot 3^{0} \cdot 5^{1} = \frac{5}{4}\)
When given as a pitch index, this lattice point represents the pitch that results from transposing the zero pitch by a ratio of \(\frac{5}{4}\), i.e. a “just E0”. When given as a pitch difference, it represents the “just major third interval”.
LatticePoint objects in many aspects
behave like regular integers. They support the following arithmetic
operations:
addition and subtraction fulfill the same purposes as in the integer case, for example transposing a pitch upwards or downwards by combining its pitch index with a pitch diff:
G0 = limit5.rs_pitch('3/2') P4 = limit5.rs_interval('4/3') pitch_index = G0.pitch_index + P4.pitch_diff C1 = limit5.pitch(pitch_index) pitch_index = G0.pitch_index - P4.pitch_diff D0 = limit5.pitch(pitch_index)scalar multiplication can be used to stack an interval
pitch_index = G0.pitch_index + 2 * P4.pitch_diff F1 = limit5.pitch(pitch_index)floor division and modulo can be used to calculate the pitch class index and base interval index of a pitch index:
octave_diff = limit5.lattice.point((1, 0, 0)) pc_index = F1.pitch_index % octave_diff bi_index = F1.pitch_index // octave_diff print(pc_index) print(bi_index)LatticePoint(2, -1, 0) 1negation and abs() can be used on pitch difference values to change direction of an interval or calculate its absolute value:
P4_downwards = limit5.diff_interval(-P4.pitch_diff) pitch_index = G0.pitch_index + P4_downwards.pitch_diff D0 = limit5.pitch(pitch_index) P4 = limit5.diff_interval(abs(P4_downwards.pitch_diff))
Floor division and modulo operations can be defined on lattice points, because lattice points implement a total ordering based on the frequency ratios they represent:
F0 = limit5.vec_pitch((2, -1, 0))
F1 = F0.transpose_bi_index(1)
G0 = limit5.vec_pitch((-1, 1, 0))
assert G0.pitch_index > F0.pitch_index
assert G0.pitch_index < F1.pitch_index
LatticePoint objects are bound to the
generator vector to which they relate. This acts as a safeguard so
lattice points of different origins can not be mixed:
from xenharmlib import MultiGenTuning
from xenharmlib import FrequencyRatio
tuning_a = MultiGenTuning(
(FrequencyRatio(2), FrequencyRatio(3)),
eq_diff_vec=(1, 0)
)
tuning_b = MultiGenTuning(
(FrequencyRatio(2), FrequencyRatio(7)),
eq_diff_vec=(1, 0)
)
try:
tuning_a.lattice.point((1, 2)) + tuning_b.lattice.point((4, 0))
except TypeError as exc:
print(exc)
unsupported operator +: lattice points originate from different lattices