Class: HeadMusic::Style::CompositeAssessment

Inherits:
Object
  • Object
show all
Defined in:
lib/head_music/style/composite_assessment.rb

Overview

Several guides applied to one voice: the combined grade, and the per-guide assessments it was computed from.

Answers the same protocol as GuideAssessment, which is the point – a consumer walks assessments and never asks which of the two it is holding. The shared example group in spec/support is what keeps the two from drifting apart.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(guide, voice) ⇒ CompositeAssessment

Returns a new instance of CompositeAssessment.



13
14
15
16
17
18
19
20
# File 'lib/head_music/style/composite_assessment.rb', line 13

def initialize(guide, voice)
  unless guide.respond_to?(:composite?) && guide.composite?
    raise ArgumentError, "guide must be a composite (got #{guide.inspect})"
  end

  @guide = guide
  @voice = voice
end

Instance Attribute Details

#guideObject (readonly)

Returns the value of attribute guide.



11
12
13
# File 'lib/head_music/style/composite_assessment.rb', line 11

def guide
  @guide
end

#voiceObject (readonly)

Returns the value of attribute voice.



11
12
13
# File 'lib/head_music/style/composite_assessment.rb', line 11

def voice
  @voice
end

Instance Method Details

#adherent?Boolean

Returns:

  • (Boolean)


37
38
39
# File 'lib/head_music/style/composite_assessment.rb', line 37

def adherent?
  assessments.all?(&:adherent?)
end

#assessable?Boolean

Returns:

  • (Boolean)


41
42
43
# File 'lib/head_music/style/composite_assessment.rb', line 41

def assessable?
  assessments.all?(&:assessable?)
end

#assessmentsObject

Memoized because everything else here is derived from it: without this, each of fitness, messages, and guide_item_assessments re-runs the whole member analysis, and assessments.first is a different object every time.



25
26
27
# File 'lib/head_music/style/composite_assessment.rb', line 25

def assessments
  @assessments ||= guide.guides.map { |member| member.assess(voice) }
end

#fitnessObject

Geometric rather than arithmetic, because a species grade is two grades that must both hold. A perfect melody against a half-graded harmony reads 0.707 rather than 0.75, and either half at zero takes the whole grade to zero. Acing the melody buys no relief from the harmony.

When a member is unassessable the rubric is left out of it entirely. This is GuideAssessment’s own rule lifted one level – “a voice failing a precondition has not earned a bad grade on the rest” – with the nouns raised: the composite has not earned a grade on the OTHER MEMBERS either. Without the branch, an assessable member’s rubric leaks into a grade that was never earned, moving it up or down according to material the guide declined to look at.

With today’s registry the branch changes no number, and that is not an argument against it. The members of a species composite share MinimumNotes.with(3) and fail it identically, and the only gate that distinguishes them, SetAgainstAnotherVoice, scores exactly 0 or 1 – so a solo voice grades 0.0 either way. The rules part company where the members’ gates differ and one scores a fraction, which MinimumNotes does. The rule is chosen for what a gate means, not for a number it currently moves.



65
66
67
68
69
# File 'lib/head_music/style/composite_assessment.rb', line 65

def fitness
  @fitness ||= geometric_mean(
    assessable? ? assessments.map(&:fitness) : assessments.map(&:gate_factor)
  )
end

#fitness_by_categoryObject

Grouped rather than keyed, so a composite of two guides sharing a category reports both instead of silently dropping one. Follows fitness into the gate-factor branch: a consumer showing melody, harmony, and total must not be handed three numbers that do not combine.



79
80
81
82
# File 'lib/head_music/style/composite_assessment.rb', line 79

def fitness_by_category
  assessments.group_by { |assessment| assessment.guide.category }
    .transform_values { |group| geometric_mean(group.map(&method(:member_fitness))) }
end

#gate_factorObject



71
72
73
# File 'lib/head_music/style/composite_assessment.rb', line 71

def gate_factor
  @gate_factor ||= geometric_mean(assessments.map(&:gate_factor))
end

#geometric_mean(values) ⇒ Object (private)

Exact where exactness is required, which the log-sum form would not be: [1.0, 1.0] is exactly 1.0, which adherent? depends on, and any zero member is exactly 0.0. The empty case cannot arise – CompositeGuide refuses fewer than two members – so it is left to raise rather than answering a silent 1.0.



94
95
96
# File 'lib/head_music/style/composite_assessment.rb', line 94

def geometric_mean(values)
  values.reduce(:*)**(1.0 / values.length)
end

#guide_item_assessmentsObject



29
30
31
# File 'lib/head_music/style/composite_assessment.rb', line 29

def guide_item_assessments
  @guide_item_assessments ||= assessments.flat_map(&:guide_item_assessments)
end

#member_fitness(assessment) ⇒ Object (private)



86
87
88
# File 'lib/head_music/style/composite_assessment.rb', line 86

def member_fitness(assessment)
  assessable? ? assessment.fitness : assessment.gate_factor
end

#messagesObject



33
34
35
# File 'lib/head_music/style/composite_assessment.rb', line 33

def messages
  assessments.flat_map(&:messages)
end