Skip to content

Commit

Permalink
refac: Split evaluation.py file into smaller files
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikolay-Lysenko committed Jul 27, 2024
1 parent c27fa7b commit 2f9677d
Show file tree
Hide file tree
Showing 11 changed files with 3,919 additions and 3,796 deletions.
2 changes: 1 addition & 1 deletion .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ checks:
threshold: 8
file-lines:
config:
threshold: 1500
threshold: 1000
method-complexity:
config:
threshold: 10
1,416 changes: 33 additions & 1,383 deletions dodecaphony/evaluation.py

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions dodecaphony/scoring_functions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Evaluate melodic, harmonic, and rhythmic properties of a fragment.
Author: Nikolay Lysenko
"""
687 changes: 687 additions & 0 deletions dodecaphony/scoring_functions/harmony.py

Large diffs are not rendered by default.

547 changes: 547 additions & 0 deletions dodecaphony/scoring_functions/melody.py

Large diffs are not rendered by default.

186 changes: 186 additions & 0 deletions dodecaphony/scoring_functions/rhythm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""
Evaluate rhythmic properties of a fragment.
Author: Nikolay Lysenko
"""


import itertools
from functools import cache

from dodecaphony.fragment import Fragment


def evaluate_cadence_duration(
fragment: Fragment,
max_duration: float,
last_sonority_weight: float,
last_notes_weight: float
) -> float:
"""
Evaluate that cadence has enough duration.
:param fragment:
a fragment to be evaluated
:param max_duration:
maximum enough duration (in reference beats); higher durations do not increase score
:param last_sonority_weight:
weight that determines contribution of last sonority duration to final score
:param last_notes_weight:
weight that determines contribution of duration of last note of each melodic line
to final score; this term is rather a mean than a goal, because it rewards intermediate
steps when last sonority duration is unchanged, but one of the last notes extends its
duration
:return:
minus one multiplied by missing in last sonority and last notes fraction of `max_duration`
"""
clipped_durations = [min(event.duration, max_duration) for event in fragment.sonorities[-1]]
last_sonority_duration = min(clipped_durations)
avg_last_note_duration = sum(clipped_durations) / len(clipped_durations)
total_weight = last_sonority_weight + last_notes_weight
last_sonority_weight /= total_weight
last_notes_weight /= total_weight
last_sonority_term = last_sonority_weight * (last_sonority_duration / max_duration - 1)
last_notes_term = last_notes_weight * (avg_last_note_duration / max_duration - 1)
score = last_sonority_term + last_notes_term
return score


def evaluate_presence_of_required_pauses(
fragment: Fragment, pauses: list[tuple[float, float]]
) -> float:
"""
Evaluate presence of pauses required by a user.
:param fragment:
a fragment to be evaluated
:param pauses:
list of pairs of start time and end time for user-defined pauses
:return:
minus one multiplied by share of declared pauses occupied with sounds
"""
numerator = 0
denominator = 0
for melodic_line in fragment.melodic_lines:
event_index = 0
for pause_start_time, pause_end_time in pauses:
while True:
event = melodic_line[event_index]
event_end_time = event.start_time + event.duration
if event_end_time <= pause_start_time:
event_index += 1
continue
intersection_start_time = max(event.start_time, pause_start_time)
intersection_end_time = min(event_end_time, pause_end_time)
intersection_duration = intersection_end_time - intersection_start_time
if event.pitch_class != 'pause':
numerator += intersection_duration
denominator += intersection_duration
if event_end_time >= pause_end_time:
break
event_index += 1
score = -numerator / denominator
return score


def evaluate_rhythmic_homogeneity(fragment: Fragment) -> float:
"""
Evaluate rhythmic homogeneity among all measures except the last one.
:param fragment:
a fragment to be evaluated
:return:
a score between minis one and zero depending on rhythmic variation
"""
score = 0
for durations_of_measures_for_one_line in fragment.temporal_content:
pairs = itertools.combinations(durations_of_measures_for_one_line[:-1], 2)
for first_durations, second_durations in pairs:
first_end_times = list(itertools.accumulate(first_durations))
second_end_times = list(itertools.accumulate(second_durations))
avg_n_events = (len(first_end_times) + len(second_end_times)) / 2
n_unique_ends = len(set(first_end_times + second_end_times))
score -= (n_unique_ends / avg_n_events - 1)
n_non_last_measures = (fragment.n_beats // fragment.meter_numerator) - 1
n_pairs = n_non_last_measures * (n_non_last_measures - 1) / 2
score /= len(fragment.melodic_lines) * n_pairs
return score


@cache
def find_normalization_coefficient_for_rhythmic_intensity(
n_beats: float, half_life: float, max_intensity_factor: float
) -> float:
"""
Find maximum possible value of exponentially decaying counter of sound events.
:param n_beats:
duration of a melodic line (in reference beats)
:param half_life:
half life (in reference beats) of exponentially decaying counter of sound events
:param max_intensity_factor:
factor that rescales maximum possible intensity (given high enough half life,
its unscaled value might be too high, because it is calculated at the last event
of the line consisting entirely of the shortest possible events)
:return:
normalization coefficient for a single melodic line
"""
min_event_duration = 0.25 # TODO: Calculate this value as min value from `measure_durations`.
max_possible_n_events = n_beats / min_event_duration
decay_coef = 0.5 ** (min_event_duration / half_life)
max_intensity = (1 - decay_coef ** max_possible_n_events) / (1 - decay_coef)
normalization_coefficient = max_intensity_factor * max_intensity
return normalization_coefficient


def evaluate_rhythmic_intensity_by_positions(
fragment: Fragment, positions: list[float], ranges: list[list[tuple[float, float]]],
half_life: float, max_intensity_factor: float = 1
) -> float:
"""
Evaluate values of rhythmic intensity at particular time moments.
:param fragment:
a fragment to be evaluated
:param positions:
moments of time (in reference beats) at which rhythmic intensity is evaluated
:param ranges:
minimum and maximum desired levels of rhythmic intensity for each position and
for each melodic line
:param half_life:
half life (in reference beats) of exponentially decaying counter of sound events
:param max_intensity_factor:
factor that rescales maximum possible intensity (given high enough half life,
its unscaled value might be too high, because it is calculated at the last event
of the line consisting entirely of the shortest possible events)
:return:
average over all positions deviation of rhythmic intensity from its ranges
"""
score = 0
normalization_coef = find_normalization_coefficient_for_rhythmic_intensity(
fragment.n_beats, half_life, max_intensity_factor
)
for melodic_line, line_ranges in zip(fragment.melodic_lines, ranges):
events_counter = 0
previous_time_moment = 0
annotated_positions = [
(position, True, current_range)
for position, current_range in zip(positions, line_ranges)
]
event_starts = [
(event.start_time, False, (0.0, 0.0)) # The last value of this tuple affects nothing.
for event in melodic_line if event.pitch_class != 'pause'
]
all_time_moments = sorted(annotated_positions + event_starts)
for time_moment, evaluation_needed, current_range in all_time_moments:
time_diff = time_moment - previous_time_moment
events_counter *= 0.5 ** (time_diff / half_life)
previous_time_moment = time_moment
if evaluation_needed:
normalized_counter = events_counter / normalization_coef
score += min(normalized_counter - current_range[0], 0)
score += min(current_range[1] - normalized_counter, 0)
else:
events_counter += 1
score /= len(fragment.melodic_lines) * len(positions)
return score
5 changes: 5 additions & 0 deletions tests/scoring_functions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Test `dodecaphony.scoring_functions` package.
Author: Nikolay Lysenko
"""
Loading

0 comments on commit 2f9677d

Please sign in to comment.