from typing import Optional, Union
import abjad
from .. import mutate
from ..core.GeneticAlgorithm import GeneticAlgorithm
[docs]class GeneticAlgorithmMusicMaker():
r"""Uses two :class:`auxjad.GeneticAlgorithm`'s, one for pitch and another
for attack points, in order to create musical cells. At each call of
:meth:`__call__`, it iterates the genetic algorithms by one generation, and
returns an |abjad.Selection| created with the fittest pitch and attack
point individuals.
.. note::
Many of the properties of this class reflect the behaviour of
properties of :class:`GeneticAlgorithm`. Some, such as
:attr:`population_size`, :attr:`select_n_parents`,
:attr:`keep_n_parents`, :attr:`mutation_chance`,
:attr:`mutation_index`, and :attr:`evaluation_index`, have the same
name as those in :class:`GeneticAlgorithm`. :attr:`pitch_target` and
:attr:`attack_point_target`, :attr:`pitch_genes` and
:attr:`attack_point_genes`, and :attr:`pitch_initial_individual` and
:attr:`attack_point_initial_individual` work as
:attr:`GeneticAlgorithm.target`, :attr:`GeneticAlgorithm.genes`, and
:attr:`GeneticAlgorithm.initial_individual`, respectively.
For the details of how these properties work, please refer to
:class:`GeneticAlgorithm`'s documentation page.
Basic usage:
At its basic, this class needs a target and a list of genes for both
pitches and attack points. The evaluation function will compare all
individuals in the population against this target when scoring them.
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... )
>>> repr(maker)
pitches: ["c'", "d'", "e'", "f'"]
attack_points: [0, 4, 8, 12]
>>> len(maker)
4
>>> notes = maker.target_music
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
c'4
d'4
e'4
f'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-ahfUfls3cq.png
Calling the instance will apply the genetic algorithm process and
output an |abjad.Selection| with the fittest individual in the
population.
>>> notes = maker()
>>> maker.generation_number
0
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
c'4
f'4
~
f'8.
e'16
~
e'8
a'8
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-mIrHl4wwHA.png
Subsequent calls will create new generations of individuals, always
outputting the fittest measure.
>>> notes = maker()
>>> maker.generation_number
1
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
c'4
d'16
e'8.
~
e'4
~
e'16
g'8.
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-hDxTq3Y2Ek.png
:meth:`output_n`:
The method :meth:`output_n` can be used to output `n` iterations of the
process. They are output as a single |abjad.Selection|:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
r4
r16
c'8.
d'16
d'8.
f'4
c'2
d'16
c''8.
f'4
r16
c'8.
d'4
f'4.
f'8
c'4
d'8.
e'16
~
e'4
e'4
c'4
d'4
~
d'16
e'8.
e'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-PrfaIjhEbL.png
:attr:`pitch_genes`:
While :attr:`attack_point_genes` must always take a :obj:`list` of
:obj:`int`'s, :attr:`pitch_genes` can take a variety of object types.
The implementation of this class uses |abjad.LeafMaker|, so pitches can
take any objects accepted by that class. These include :obj:`int` and
:obj:`str` for pitches, ``None`` for rests, :obj:`tuple` for chords,
etc.
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", None, "e'", ("g'", "bf'")],
... pitch_genes=[None,
... "c'",
... "d'",
... "e'",
... "f'",
... "g'",
... ("g'", "a'"),
... ("g'", "bf'"),
... ("g'", "c''"),
... ],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... population_size=50,
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
<g' c''>4
g'4
~
g'16
<g' c''>8.
<g' c''>4
c'2
e'8.
r16
r4
c'4
r4
r16
e'8.
<g' a'>4
c'4
r4
e'8.
<g' a'>16
~
<g' a'>4
c'4
r4
e'8.
<g' a'>16
~
<g' a'>4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-etoHzdnAIu.png
Which is equivalent to:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=[0, None, 4, (7, 10)],
... pitch_genes=[None, 0, 2, 4, 5, 7, (7, 9), (7, 10), (7, 12)],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... population_size=50,
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
<g' c''>4
g'4
~
g'16
<g' c''>8.
<g' c''>4
c'2
e'8.
r16
r4
c'4
r4
r16
e'8.
<g' a'>4
c'4
r4
e'8.
<g' a'>16
~
<g' a'>4
c'4
r4
e'8.
<g' a'>16
~
<g' a'>4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-AfDY1QYNhW.png
:attr:`units_per_window` and :attr:`duration_unit`:
By default, there are ``16`` attack points in a window, each lasting
for ``abjad.Duration((1, 16))``. These can be changed using
:attr:`units_per_window` and :attr:`duration_unit`:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 8, 16, 24],
... attack_point_genes=list(range(32)),
... duration_unit=abjad.Duration((1, 32)),
... units_per_window=32,
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
c'8.
e'16
~
e'16.
e'32
~
e'8
~
e'32
b'4...
c'8.
d'16
~
d'2
f'4
c'4
d'8..
e'32
~
e'4
c'4
c'4
d'8..
e'32
~
e'4
e'4
c'4
d'8..
e'32
~
e'4
f'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-AqHoQPgphf.png
:attr:`omit_time_signature`:
By default, a time signature is added to the output automatically:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'", "g'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12, 16],
... attack_point_genes=list(range(20)),
... units_per_window=20,
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 5/4
r8
c'8
d'8
f'4.
e'4..
g'16
c'8.
d'16
~
d'4
~
d'16
g'16
f'8
~
f'4
g'4
c'4
d'8.
e'16
~
e'4
a'4
f'4
c'4
d'8.
g'16
~
g'4
f'4
g'4
c'4
d'4
g'4
f'4
g'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-bbsRWshiDI.png
Setting :attr:`omit_time_signature` to ``True`` will result in no time
signature. Note that the output might need to be cleaned up using
|abjad.Meter.rewrite_meter()|.
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'", "g'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12, 16],
... attack_point_genes=list(range(20)),
... units_per_window=20,
... omit_time_signature=True,
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
r8
c'8
d'8
f'4.
e'4..
g'16
c'8.
d'4.
g'16
f'4.
g'4
c'4
d'8.
e'4
~
e'16
a'4
f'4
c'4
d'8.
g'4
~
g'16
f'4
g'4
c'4
d'4
g'4
f'4
g'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-IVSrzPV4OT.png
:attr:`time_signatures`:
Time signatures can also be enforced in the output. Set
:attr:`time_signatures` to a single |abjad.TimeSignature| or a
:obj:`list` of |abjad.TimeSignature|'s as needed.
A single |abjad.TimeSignature| is applied to all measures:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... duration_unit=abjad.Duration((1, 32)),
... time_signatures=abjad.TimeSignature((2, 2))
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 2/2
r32
c''16.
~
c''32
f'16.
~
f'32
e'16.
f'8
r2
r32
c'16.
d'8
~
d'16
e'16
e'8
r2
c'8.
d'16
e'8
f'8
r2
r32
c'16.
d'8
e'8
f'8
r2
r32
c'16.
d'8
e'8
f'8
r2
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-Xc9iKXJoEx.png
A :obj:`list` of |abjad.TimeSignature|'s is applied cyclically.
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'", "g'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12, 16],
... attack_point_genes=list(range(20)),
... duration_unit=abjad.Duration((1, 16)),
... units_per_window=20,
... time_signatures=[abjad.TimeSignature((2, 4)),
... abjad.TimeSignature((3, 4)),
... ],
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 2/4
r8
c'8
d'8
f'8
~
\time 3/4
f'4
e'4..
g'16
\time 2/4
c'8.
d'16
~
d'4
~
\time 3/4
d'16
g'16
f'4.
g'4
\time 2/4
c'4
d'8.
e'16
~
\time 3/4
e'4
a'4
f'4
\time 2/4
c'4
d'8.
g'16
~
\time 3/4
g'4
f'4
g'4
\time 2/4
c'4
d'4
\time 3/4
g'4
f'4
g'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-lJCYtjGY52.png
:attr:`pitch_score_bias`:
Pitches and attack points are scored separately and, by default,
contribute equally to the total score of each individual. To change the
bias of the pitch score, set :attr:`pitch_score_bias` to a value
between ``0.0`` and ``1.0``.
This is the default output:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
r16
c'8
d'16
~
d'4
f'4
d'4
r16
c'8.
d'8
e'8
~
e'8.
f'16
~
f'4
c'4
d'4
e'4
f'4
c'4
d'4
e'4
f'4
c'4
d'4
e'4
f'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-VEinjiTe3F.png
With a high :attr:`pitch_score_bias`, pitch convergence will tend to be
faster at the expense of attack points:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... pitch_score_weight=0.95,
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
r8
c'16
d'16
~
d'4
~
d'8
e'4
d'8
r8
c'16
d'16
~
d'2
~
d'16
e'8.
c'16
d'8.
~
d'8.
e'16
~
e'4
f'4
c'16
d'8.
~
d'8.
e'16
~
e'4
f'4
c'4
~
c'16
d'8
e'16
~
e'4
f'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-iVD6l66hNl.png
In contrast, a low :attr:`pitch_score_bias`, attack point convergence
will tend to be faster at the expense of pitches:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... pitch_score_weight=0.05,
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
e'16
c''4..
f'4
c''4
e'4
d'4
d'4
f'4
e'4
d'4
d'4
f'4
e'4
d'4
d'4
f'4
e'4
d'4
d'4
f'4
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-1Ex8sgcS2s.png
:attr:`attack_points_mode`:
When using this class in attack points mode, each note will last a
single unit instead of being extended until the next attack point:
>>> maker = auxjad.GeneticAlgorithmMusicMaker(
... pitch_target=["c'", "d'", "e'", "f'"],
... pitch_genes=["c'", "d'", "e'", "f'", "g'", "a'", "b'", "c''"],
... attack_point_target=[0, 4, 8, 12],
... attack_point_genes=list(range(16)),
... attack_points_mode=True,
... )
>>> notes = maker.output_n(5)
>>> staff = abjad.Staff(notes)
>>> abjad.show(staff)
.. docs::
\new Staff
{
\time 4/4
e'16
r8.
c'16
r16
r16
g'16
r4
f'16
r8.
c'16
r8.
d'16
r8.
e'16
c'16
r4.
c'16
r8.
d'16
r16
r16
e'16
r4
f'16
r8.
c'16
r8.
d'16
r8.
e'16
r8.
f'16
r8.
c'16
r8.
d'16
r8.
e'16
r8.
f'16
r8.
}
.. figure:: ../_images/GeneticAlgorithmMusicMaker-5aInHHwMol.png
"""
### CLASS VARIABLES ###
__slots__ = ('_pitch_target',
'_pitch_genes',
'_pitch_initial_individual',
'_attack_point_target',
'_attack_point_genes',
'_attack_point_initial_individual',
'_population_size',
'_select_n_parents',
'_mutation_chance',
'_mutation_index',
'_evaluation_index',
'_duration_unit',
'_units_per_window',
'_omit_time_signature',
'_time_signatures',
'_attack_points_mode',
'_pitch_score_bias',
'_pitch_ga',
'_attack_point_ga',
'_fittest_measure',
'_fittest_pitch_individual',
'_fittest_attack_point_individual',
'_pitch_population',
'_attack_point_population',
'_scores',
'_target_music',
'_total_duration',
)
### INITIALISER ###
[docs] def __init__(self,
*,
pitch_target: list,
pitch_genes: list,
attack_point_target: list,
attack_point_genes: list,
duration_unit: abjad.Duration = abjad.Duration((1, 16)),
units_per_window: int = 16,
pitch_initial_individual: Optional[list] = None,
attack_point_initial_individual: Optional[list] = None,
population_size: int = 100,
select_n_parents: int = 10,
keep_n_parents: int = 0,
mutation_chance: float = 0.2,
mutation_index: float = 0.1,
evaluation_index: float = 0.2,
omit_time_signature: bool = False,
time_signatures: Optional[list] = None,
attack_points_mode: bool = False,
pitch_score_bias: float = 0.5,
) -> None:
r'Initialises self.'
if len(pitch_target) != len(attack_point_target):
raise ValueError("'pitch_target' and 'attack_point_target' must "
"have the same length")
self._pitch_ga = GeneticAlgorithm(
target=pitch_target,
genes=pitch_genes,
initial_individual=pitch_initial_individual,
population_size=population_size,
select_n_parents=select_n_parents,
keep_n_parents=keep_n_parents,
mutation_chance=mutation_chance,
mutation_index=mutation_index,
evaluation_index=evaluation_index,
)
self._attack_point_ga = GeneticAlgorithm(
target=attack_point_target,
genes=attack_point_genes,
initial_individual=attack_point_initial_individual,
population_size=population_size,
select_n_parents=select_n_parents,
keep_n_parents=keep_n_parents,
mutation_chance=mutation_chance,
mutation_index=mutation_index,
evaluation_index=evaluation_index,
)
if not isinstance(duration_unit, abjad.Duration):
raise TypeError("'duration_unit' must be 'abjad.Duration'")
if not isinstance(units_per_window, int):
raise TypeError("'units_per_window' must be 'int'")
if units_per_window < max(self._attack_point_ga.genes):
raise TypeError("'units_per_window' must be larger than the max "
"value in 'attack_point_genes'")
self._duration_unit = duration_unit
self._units_per_window = units_per_window
self._total_duration = self._units_per_window * self._duration_unit
self.omit_time_signature = omit_time_signature
self.time_signatures = time_signatures
self.attack_points_mode = attack_points_mode
self.pitch_score_bias = pitch_score_bias
self._target_individual_to_measure()
### SPECIAL METHODS ###
[docs] def __repr__(self) -> str:
r"""Returns interpreter representation of :attr:`target`'s of both
instances of the genetic algorithm (pitches and attack points).
"""
string = 'pitches: ' + repr(self._pitch_ga._target) + '\n'
string += 'attack_points: ' + repr(self._attack_point_ga._target)
return string
[docs] def __len__(self) -> int:
r'Returns the number of genes in each individual.'
return len(self._pitch_ga._target)
[docs] def __call__(self) -> abjad.Selection:
r"""Calls the genetic algorithm process for one iteration, returning an
|abjad.Selection|. Generates a new generation of length
:attr:`population_size` via reproduction and mutation processes and
scores each individual using the evaluation function.
"""
self._pitch_ga._generate_population()
self._pitch_ga._score_population()
self._attack_point_ga._generate_population()
for attack_point_individual in self._attack_point_ga._population:
attack_point_individual.sort()
self._attack_point_ga._score_population()
self._sort_population_by_evaluation()
self._fittest_individual_to_measure()
return self.fittest_measure
[docs] def __next__(self) -> None:
r"""Calls the genetic algorithm process for one iteration. Generates a
new generation of length :attr:`population_size` via reproduction and
mutation processes and scores each individual using the evaluation
function.
"""
try:
return self.__call__()
except RuntimeError:
raise StopIteration
[docs] def __iter__(self) -> None:
r'Returns an iterator, allowing instances to be used as iterators.'
return self
### PUBLIC METHODS ###
[docs] def reset(self) -> None:
r'Resets that genetic algorithm.'
self._pitch_ga.reset()
self._attack_point_ga.reset()
[docs] def output_n(self,
n: int
) -> abjad.Selection:
r"""Goes through ``n`` iterations of the genetic algorithm process and
outputs a single |abjad.Selection|.
"""
if not isinstance(n, int):
raise TypeError("first positional argument must be 'int'")
if n < 1:
raise ValueError("first positional argument must be a positive "
"'int'")
dummy_container = abjad.Container()
for _ in range(n):
dummy_container.append(self.__call__())
mutate.remove_repeated_time_signatures(dummy_container[:])
output = dummy_container[:]
dummy_container[:] = []
return output
### PRIVATE METHODS ###
def _sort_population_by_evaluation(self) -> None:
r"""Sorts the population (and their scores) according to the evaluation
of its individuals.
"""
self._scores = []
for pitch_score, attack_score in zip(self._pitch_ga._scores,
self._attack_point_ga._scores):
combined_score = pitch_score * self._pitch_score_bias
combined_score += attack_score * (1.0 - self._pitch_score_bias)
combined_score /= 2
self._scores.append(combined_score)
zipped_lists = list(zip(self._scores,
self._pitch_ga._population,
self._attack_point_ga._population,
))
zipped_lists.sort(
key=lambda tuple_triple: tuple_triple[0],
reverse=True,
)
self._pitch_population = [pitch_gene for _, pitch_gene, _
in zipped_lists]
self._attack_point_population = [attack_point_gene
for _, _, attack_point_gene
in zipped_lists]
self._scores = [score for score, _, _ in zipped_lists]
self._pitch_ga._population = self._pitch_population[:]
self._attack_point_ga._population = self._attack_point_population[:]
def _fittest_individual_to_measure(self) -> None:
r"""Converts the fittest pitch and attack point individuals to a
measure of music.
"""
self._fittest_measure = self._make_measure(
self.fittest_attack_point_individual[:],
self.fittest_pitch_individual[:],
)
def _target_individual_to_measure(self) -> None:
r"""Converts the target pitch and attack point individuals to a
measure of music.
"""
self._target_music = self._make_measure(
self._attack_point_ga.target[:],
self._pitch_ga.target[:],
)
@staticmethod
def _sort_by_attack_point(attack_points: list,
pitches: list,
) -> tuple:
r'Sorts pitches and attack points.'
zipped_lists = []
# zipping while removing simultaneous attacks
for attack_point, pitch in zip(attack_points, pitches):
if attack_point not in (p for p, _ in zipped_lists):
zipped_lists.append((attack_point, pitch))
zipped_lists.sort(
key=lambda tuple_pair: tuple_pair[0],
)
attack_points = [attack_point for attack_point, _ in zipped_lists]
pitches = [pitch for _, pitch in zipped_lists]
return attack_points, pitches
def _convert_attack_points_to_durations(self,
attack_points: list,
pitches: list,
) -> tuple:
r"""Converts attack points to effective durations. Adds an initial rest
if first attack point is not at the 0-th position.
"""
if attack_points[0] != 0:
attack_points.insert(0, 0)
pitches.insert(0, None)
durations = []
attack_points.append(self._units_per_window)
for i in range(len(attack_points) - 1):
difference = attack_points[i + 1] - attack_points[i]
duration = difference * self.duration_unit
durations.append(duration)
return durations, pitches
def _make_measure(self,
attack_points,
pitches,
) -> None:
r"""Converts a list of pitch and attack point individuals into a
measure of music.
"""
dummy_container = abjad.Container()
sorted_attack_points, sorted_pitches = self._sort_by_attack_point(
attack_points[:],
pitches[:],
)
if not self._attack_points_mode:
durations, pitches = self._convert_attack_points_to_durations(
sorted_attack_points,
sorted_pitches,
)
notes = abjad.LeafMaker()(
pitches,
durations,
)
dummy_container = abjad.Container(notes)
else:
notes = abjad.Selection()
for i in range(self._units_per_window):
if i in sorted_attack_points:
pitch_index = sorted_attack_points.index(i)
notes += abjad.LeafMaker()(
[sorted_pitches[pitch_index]],
[self._duration_unit],
)
else:
rest = abjad.Rest(self._duration_unit)
notes += abjad.select(rest).leaves()
dummy_container = abjad.Container(notes)
# adding time signature
if not self._omit_time_signature:
if self._time_signatures is None:
time_signature = abjad.TimeSignature((self._total_duration))
time_signature.simplify_ratio()
abjad.attach(time_signature,
dummy_container[0],
)
mutate.auto_rewrite_meter(dummy_container)
else:
mutate.enforce_time_signature(
dummy_container,
self._time_signatures,
)
return dummy_container[:]
### PUBLIC PROPERTIES ###
@property
def duration_unit(self) -> abjad.Duration:
r'Unit for the duration grid. Default is abjad.Duration((1, 16)).'
return self._duration_unit
@duration_unit.setter
def duration_unit(self,
duration_unit: abjad.Duration,
) -> None:
if not isinstance(duration_unit, abjad.Duration):
raise TypeError("'duration_unit' must be 'abjad.Duration'")
self._duration_unit = duration_unit
self._total_duration = self._units_per_window * self._duration_unit
@property
def units_per_window(self) -> int:
r'Number of units per window. Default is 16.'
return self._units_per_window
@units_per_window.setter
def units_per_window(self,
units_per_window: int,
) -> None:
if not isinstance(units_per_window, int):
raise TypeError("'units_per_window' must be 'int'")
if units_per_window < max(self._attack_point_ga.genes):
raise TypeError("'units_per_window' must be larger than the max "
"value in 'attack_point_genes'")
self._units_per_window = units_per_window
self._total_duration = self._units_per_window * self._duration_unit
@property
def omit_time_signature(self) -> bool:
r"""When ``True``, a time signature won't be added to the first leaf of
the output."""
return self._omit_time_signature
@omit_time_signature.setter
def omit_time_signature(self,
omit_time_signature: bool,
) -> None:
if not isinstance(omit_time_signature, bool):
raise TypeError("'omit_time_signature' must be 'bool'")
self._omit_time_signature = omit_time_signature
@property
def time_signatures(self) -> list:
r"""List of time signatures to be enforced on output. It is important
to note that :attr:`omit_time_signature` must be ``True`` for it to
take effect.
"""
return self._time_signatures
@time_signatures.setter
def time_signatures(self,
time_signatures: Optional[list],
) -> None:
if time_signatures is not None:
if isinstance(time_signatures, abjad.TimeSignature):
time_signatures = [time_signatures]
elif not isinstance(time_signatures, list):
raise TypeError("'time_signatures' must be a 'list' of "
"'abjad.TimeSignature' or 'None'")
if not all(isinstance(ts, abjad.TimeSignature) for ts
in time_signatures):
raise TypeError("'time_signatures' must be a 'list' of "
"'abjad.TimeSignature' or 'None'")
self._time_signatures = time_signatures
@property
def attack_points_mode(self) -> bool:
r"""When ``True``, each note will last only for the duration of the
unit, instead of extending it to the next attack point."""
return self._attack_points_mode
@attack_points_mode.setter
def attack_points_mode(self,
attack_points_mode: bool,
) -> None:
if not isinstance(attack_points_mode, bool):
raise TypeError("'attack_points_mode' must be 'bool'")
self._attack_points_mode = attack_points_mode
@property
def pitch_target(self) -> list:
r'Target pitch individual used for evaluation.'
return self._pitch_ga.target
@pitch_target.setter
def pitch_target(self,
pitch_target: list,
) -> None:
if len(pitch_target) != len(self._attack_point_ga.target):
raise ValueError("'pitch_target' must be the same length as "
"'attack_point_target'")
self._pitch_ga.target = pitch_target
self._target_individual_to_measure()
@property
def pitch_genes(self) -> list:
r'List of possible genes that make up all pitch individuals.'
return self._pitch_ga.genes
@pitch_genes.setter
def pitch_genes(self,
pitch_genes: list,
) -> None:
self._pitch_ga.genes = pitch_genes
@property
def pitch_initial_individual(self) -> Union[list, None]:
r'Optional initial pitch individual.'
return self._pitch_ga.initial_individual
@pitch_initial_individual.setter
def pitch_initial_individual(self,
pitch_initial_individual: Optional[list],
) -> None:
self._pitch_ga.initial_individual = pitch_initial_individual
@property
def attack_point_target(self) -> list:
r'Target attack point individual used for evaluation.'
return self._attack_point_ga.target
@attack_point_target.setter
def attack_point_target(self,
attack_point_target: list,
) -> None:
if len(attack_point_target) != len(self._pitch_ga.target):
raise ValueError("'attack_point_target' must be the same length "
"as 'pitch_target'")
self._attack_point_ga.target = attack_point_target
self._target_individual_to_measure()
@property
def attack_point_genes(self) -> list:
r'List of possible genes that make up all attack point individuals.'
return self._attack_point_ga.genes
@attack_point_genes.setter
def attack_point_genes(self,
attack_point_genes: list,
) -> None:
self._attack_point_ga.genes = attack_point_genes
@property
def attack_point_initial_individual(self) -> Union[list, None]:
r'Optional initial attack point individual.'
return self._attack_point_ga.initial_individual
@attack_point_initial_individual.setter
def attack_point_initial_individual(
self,
attack_point_initial_individual: Optional[list],
) -> None:
self._attack_point_ga.initial_individual = (
attack_point_initial_individual
)
@property
def population_size(self) -> int:
r'Number of individuals in any given generation.'
return self._pitch_ga.population_size
@population_size.setter
def population_size(self,
population_size: int,
) -> None:
self._pitch_ga.population_size = population_size
self._attack_point_ga.population_size = population_size
@property
def select_n_parents(self) -> int:
r"""Number of the best-fit individuals that are selected to be parents
of the next generation. They also survive into the next generation.
"""
return self._pitch_ga.select_n_parents
@select_n_parents.setter
def select_n_parents(self,
select_n_parents: int,
) -> None:
self._pitch_ga.select_n_parents = select_n_parents
self._attack_point_ga.select_n_parents = select_n_parents
@property
def keep_n_parents(self) -> int:
r"""Number of the best-fit individuals that survive into the next
generation. Default is ``0``.
"""
return self._pitch_ga.keep_n_parents
@keep_n_parents.setter
def keep_n_parents(self,
keep_n_parents: int,
) -> None:
self._pitch_ga.keep_n_parents = keep_n_parents
self._attack_point_ga.keep_n_parents = keep_n_parents
@property
def mutation_chance(self) -> float:
r'Percentage of the total population who will experience mutation.'
return self._pitch_ga.mutation_chance
@mutation_chance.setter
def mutation_chance(self,
mutation_chance: float,
) -> None:
self._pitch_ga.mutation_chance = mutation_chance
self._attack_point_ga.mutation_chance = mutation_chance
@property
def mutation_index(self) -> float:
r"""Given an individual selected to undergo mutation, this index gives
the percentage of genes of that individual which will be mutated.
"""
return self._pitch_ga.mutation_index
@mutation_index.setter
def mutation_index(self,
mutation_index: float,
) -> None:
self._pitch_ga.mutation_index = mutation_index
self._attack_point_ga.mutation_index = mutation_index
@property
def evaluation_index(self) -> float:
r"""The index used in the evaluation function. This index will be
raised by the difference between indices of the target value and the
current value. Consider the following example, where the available
genes are ``['A', 'B', 'C', 'D', 'E', 'F']`` and the target is
``['B', 'A', 'A', 'C']``. Suppose an individual has the genes
``['D', 'D', 'A', 'B']``.
To evaluate this individual, first the algorithm finds the indices of
both the target's genes (in this case, ``[1, 0, 0, 2]``) and also of
the individual to be evaluated (in this case, ``[3, 3, 0, 1]``). It
then scores each element of this individual against the target using:
.. code-block::
difference = abs(target_gene_index - individual_gene_index)
element_score = evaluation_index ** difference
Thus, when the difference is ``0``, the score of this element is
``1.0``. The higher the difference, the smaller the value; this decay
can be controlled using this very property, whose default value is
``0.2``. Thus when the difference is ``1`` or ``-1``, the score is
``0.2 ** 1 = 0.2``, when the difference is ``2`` or ``-2``, the score
is ``0.2 ** 2 = 0.04``, and so on. The total score of an individual
will be given by the normalised sum of the evaluation of each of its
genes.
"""
return self._pitch_ga.evaluation_index
@evaluation_index.setter
def evaluation_index(self,
evaluation_index: float,
) -> None:
self._pitch_ga.evaluation_index = evaluation_index
self._attack_point_ga.evaluation_index = evaluation_index
@property
def pitch_score_bias(self) -> float:
r"""By default, the score of each measure gives equal weight to pitches
as it gives to attack points. Changing this to a different value will
make the pitch score contribute more or less to the total score of a
measure.
"""
return self._pitch_score_bias
@pitch_score_bias.setter
def pitch_score_bias(self,
pitch_score_bias: float,
) -> None:
if not isinstance(pitch_score_bias, float):
raise TypeError("'pitch_score_bias' must be 'float'")
if pitch_score_bias < 0.0 or pitch_score_bias > 1.0:
raise ValueError("'pitch_score_bias' must be between 0.0 and "
"1.0")
self._pitch_score_bias = pitch_score_bias
@property
def fittest_measure(self) -> Union[abjad.Selection, None]:
r"""Read-only property, returns the fittest individual of the current
population as an |abjad.Selection|.
"""
return abjad.mutate.copy(self._fittest_measure)
@property
def target_music(self) -> abjad.Selection:
r'Read-only property, returns the target as an |abjad.Selection|.'
return abjad.mutate.copy(self._target_music)
@property
def total_duration(self) -> list:
r'Read-only property, returns the total duration of the window.'
return self._total_duration
@property
def generation_number(self) -> list:
r"""Read-only property, returns the number of the current generation
(initial generation is `0`).
"""
return self._pitch_ga._generation_number
@property
def pitch_population(self) -> Union[list, None]:
r"""Read-only property, returns a list with all the population of the
current generation.
"""
return self._pitch_population
@property
def attack_point_population(self) -> Union[list, None]:
r"""Read-only property, returns a list with all the population of the
current generation.
"""
return self._attack_point_population
@property
def scores(self) -> list:
r"""Read-only property, returns the list of individual scores of the
current population. Scores are normalised.
"""
return self._scores
@property
def fittest_pitch_individual(self) -> Union[list, None]:
r"""Read-only property, returns the fittest individual of the current
population.
"""
try:
return self._pitch_population[0]
except TypeError:
return None
@property
def fittest_attack_point_individual(self) -> Union[list, None]:
r"""Read-only property, returns the fittest individual of the current
population.
"""
try:
return self._attack_point_population[0]
except TypeError:
return None
@property
def fittest_individual_score(self) -> Union[list, float]:
r"""Read-only property, returns the score of the fittest individual of
the current population.
"""
try:
return self._scores[0]
except TypeError:
return None