Moment of Symmetry (MOS)
========================

Moment-of-symmetry — equivalently, *well-formed* — scales: generated by stacking
a single interval, with exactly two step sizes distributed as evenly as two
sizes can be. Almost every scale anyone plays is one, plus a large microtonal
hinterland ordinary keyboards cannot reach.

This subpackage implements Milne, Carlé, Sethares, Noll & Holland (2011),
*Scratching the Scale Labyrinth* (LNAI 6726, 180–195) — the combinatorics, the
labyrinth visualisation, the modal algebra, the Fourier Scratching performance
technique and the Dynamic Tonality timbre matching — and adds the piece the
paper does not have: **fitting these scales to a biosignal**.

Quick start
-----------

::

   from biotuner.mos import mos, fit_mos, forward_scales, plot_labyrinth

   d = mos(3 / 2, 7)              # stack fifths, take seven notes
   d.signature                    # '5L2s'
   print(d.summary())             # steps, landmarks, propriety, inverse, embedding

   plot_labyrinth(18)             # the whole scale universe

   fit = fit_mos(peak_ratios)[0]  # which well-formed scale is this signal in?
   fit.signature, fit.error_cents, fit.improvement

   # the other direction: an interval the signal states, used as the generator
   read = forward_scales(peak_ratios)[0]
   read.interval_pair, read.signature, read.error_cents

From a :class:`~biotuner.biotuner_object.compute_biotuner`::

   bt.fit_mos()                   # -> MOSFit, also sets bt.mos_scale / bt.mos_fits
   bt.fit_mos(source="diss_curve")  # any get_tuning() derivation can feed the fit
   bt.compare_mos_sources()       # all of them at once, ranked by evidence
   bt.get_tuning("mos")           # the fitted scale as ratios
   bt.plot_labyrinth()            # the signal's peaks on the labyrinth
   bt.mos_trajectory(...)         # a path through the labyrinth over time

   from biotuner.mos.derive import mos_from_biotuner
   mos_from_biotuner(bt, mode="forward")   # -> list of ForwardScale

Reading the labyrinth
---------------------

Angle is the generator as a fraction of the period, zero at the top. Ring is
cardinality. Spokes are equal temperaments, each touching without crossing the
ring giving its note count. Arcs are valid tuning ranges, thickened where the
scale is also coherent. The picture is left–right symmetric because a generator
and its complement build the same scale.

That symmetry is why the fit reports only the bright half ``(0.5, 1)`` of the
period: ``g`` and ``period - g`` are one solution, not two, so overlaid
generators all land on one side. The emptiness of the other side is bookkeeping,
not a property of the signal.

Relation to ``scale_construction``
----------------------------------

The older MOS helpers in :mod:`biotuner.scale_construction` (``find_MOS``,
``tuning_MOS_info``, ``Stern_Brocot``, ``tuning_range_to_MOS``) still work
unchanged. They brute-force what this package derives exactly, and several of
them silently assume a 2/1 period. :func:`biotuner.mos.scale.mos_family` is the
direct replacement for ``find_MOS``. ``vizs.plot_labyrinth`` and
``vizs.MOS_interactive`` now delegate here.

See also the architecture note, ``docs/mos_architecture.md``, and the worked
notebook under :doc:`../examples/index`.

theory
------

Stern–Brocot walk, landmarks, Christoffel words. Standard library only — exact
:class:`~fractions.Fraction` arithmetic, no floating-point comparison decides a
combinatorial fact.

.. automodule:: biotuner.mos.theory
   :members:
   :undoc-members:
   :show-inheritance:

scale
-----

.. automodule:: biotuner.mos.scale
   :members:
   :undoc-members:
   :show-inheritance:

modes
-----

.. automodule:: biotuner.mos.modes
   :members:
   :undoc-members:
   :show-inheritance:

metrics
-------

Myhill's property, Rothenberg propriety, Blackwood's ``R``, evenness and JI
error, all read off one interval matrix so they cannot disagree with each other.

:func:`biotuner.mos.metrics.mos_ness` points the same machinery at a *signal*
rather than a scale, and asks the question that survives the instability of
"which MOS is this?": how much of the structure needs a generator at all. It
fits an equal division, a well-formed scale and a three-step scale at one shared
cardinality — through the same scoring path and the same transposition search —
and reports how much error each extra free parameter removes.

.. automodule:: biotuner.mos.metrics
   :members:
   :undoc-members:
   :show-inheritance:

temperaments
------------

.. automodule:: biotuner.mos.temperaments
   :members:
   :undoc-members:
   :show-inheritance:

derive
------

Biosignal → MOS: candidate generators, the fit, time-resolved trajectories, and
the comparison across every way a ``compute_biotuner`` has of deriving ratios.

**Two directions.** :func:`biotuner.mos.derive.fit_mos` is the *inverse*
direction and the default: the generator is latent, searched for jointly with
the cardinality and the rotation, and it need not be an interval anything in the
signal states. :func:`biotuner.mos.derive.forward_scales` is the *forward*
direction: it takes an interval the signal does state — the quotient of two
peaks, or a peak ratio itself — declares it the generator, and reports the
:class:`~biotuner.mos.derive.ForwardScale` that stacking it produces. Nothing
there is optimised, so the result is a consequence rather than a fit; it is
scored against the same targets with the same objective, so its ``error_cents``
and ``coverage`` can be set beside a :class:`~biotuner.mos.derive.MOSFit`.
:func:`biotuner.mos.derive.mos_from_biotuner` switches between them with
``mode='inverse'`` / ``mode='forward'``. Both fold their generators into the
bright half of the period, since a generator and its complement build the same
scale — see ``docs/mos_architecture.md`` for why an apparently half-empty
labyrinth is that convention rather than a finding.

Any :meth:`~biotuner.biotuner_object.compute_biotuner.get_tuning` source can
feed a fit — peak ratios, consonant ratios, extended-peak ratios,
dissonance-curve minima, harmonic-entropy minima, an Euler-Fokker genus, a
common-harmonic or harmonic-fit tuning — via ``source=`` on
:func:`biotuner.mos.derive.mos_from_biotuner`,
:func:`biotuner.mos.derive.mos_trajectory` and ``bt.fit_mos``.
:func:`biotuner.mos.derive.compare_sources` fits all of them at once and ranks
the results by :attr:`MOSFit.evidence
<biotuner.mos.derive.MOSFit.evidence>`, the error expressed in standard errors
below chance, so that a two-point fit reporting 0.00 cents cannot outrank a
seven-point one. A source that raises still gets a row, with the exception in
``reason``. ``'mos'`` is refused outright: it would fit a moment-of-symmetry
scale to a moment-of-symmetry scale.

Ratios are folded into the period before fitting and merged within
:data:`biotuner.mos.derive.FOLD_TOLERANCE_CENTS`, so ``1/1`` and ``2/1`` count
as the one pitch class they are; pass ``fold=False`` to keep every ratio a
separate target. A fit with more degrees than targets is returned unchanged but
marked :attr:`MOSFit.is_underdetermined
<biotuner.mos.derive.MOSFit.is_underdetermined>`, because a scale with spare
notes can be rotated onto any data.

.. automodule:: biotuner.mos.derive
   :members:
   :undoc-members:
   :show-inheritance:

fourier
-------

.. automodule:: biotuner.mos.fourier
   :members:
   :undoc-members:
   :show-inheritance:

timbre
------

.. automodule:: biotuner.mos.timbre
   :members:
   :undoc-members:
   :show-inheritance:

plotting
--------

Matplotlib figures: the labyrinth, the Stern–Brocot tree, scale wheels, tuning
ranges, modes, and fits.
:func:`biotuner.mos.plotting.plot_forward_vs_inverse` puts both directions of
the derivation in one frame — every forward reading at its (generator,
cardinality), sized by how many observed intervals proposed that generator and
shaded by how well the resulting scale explains the signal, against the latent
generator :func:`~biotuner.mos.derive.fit_mos` settled on. It draws the bright
half only, because both directions fold there and a permanently empty semicircle
would read as absence of evidence.

.. automodule:: biotuner.mos.plotting
   :members:
   :undoc-members:
   :show-inheritance:

morph
-----

Moving between two well-formed scales, three ways: hold the structure and slide
the generator along one arc (``tuning``), hop between rings along the signature
graph (``tree``), or glide every tone to its counterpart and leave the space of
well-formed scales while doing it (``voice``). Also the signature graph itself,
the figures, and audio.

.. automodule:: biotuner.mos.morph
   :members:
   :undoc-members:
   :show-inheritance:

interactive
-----------

Plotly and ipywidgets explorers, ``morph_explorer`` among them — pick two
scales, switch strategy, and hear the journey change. Both dependencies are
optional — ``pip install biotuner[interactive]``.

.. automodule:: biotuner.mos.interactive
   :members:
   :undoc-members:
   :show-inheritance:
