sim_lib_pitch_ratio/
relation.rs1use sim_lib_discrete_search::{
4 NeverInterrupt, SearchControl, SearchProblem, SearchRun, SearchStep, solve,
5};
6
7use crate::{PitchRatio, RatioPolicy};
8
9#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub struct RatioRelation {
12 pub label: String,
14 pub interval: PitchRatio,
16}
17
18impl RatioRelation {
19 pub fn new(label: impl Into<String>, interval: PitchRatio) -> Self {
21 Self {
22 label: label.into(),
23 interval,
24 }
25 }
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct RatioRelationPath {
31 pub nodes: Vec<PitchRatio>,
33 pub labels: Vec<String>,
35}
36
37pub fn expand_ratio_relation_tree(
39 root: PitchRatio,
40 relations: &[RatioRelation],
41 policy: RatioPolicy,
42 control: SearchControl,
43) -> SearchRun<RatioRelationPath> {
44 solve(
45 &RatioRelationProblem {
46 root: root.canonical(policy).unwrap_or(root),
47 relations,
48 policy,
49 },
50 control,
51 &NeverInterrupt,
52 )
53}
54
55#[derive(Clone, Debug, PartialEq, Eq)]
56struct RatioRelationState {
57 nodes: Vec<PitchRatio>,
58 labels: Vec<String>,
59 terminal: bool,
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
63enum RatioRelationChoice {
64 Emit,
65 Step { label: String, interval: PitchRatio },
66}
67
68struct RatioRelationProblem<'a> {
69 root: PitchRatio,
70 relations: &'a [RatioRelation],
71 policy: RatioPolicy,
72}
73
74impl SearchProblem for RatioRelationProblem<'_> {
75 type State = RatioRelationState;
76 type Choice = RatioRelationChoice;
77 type Output = RatioRelationPath;
78
79 fn initial_state(&self) -> Self::State {
80 RatioRelationState {
81 nodes: vec![self.root],
82 labels: Vec::new(),
83 terminal: false,
84 }
85 }
86
87 fn expand(&self, state: &Self::State, out: &mut Vec<Self::Choice>) {
88 if state.terminal {
89 return;
90 }
91 if !state.labels.is_empty() {
92 out.push(RatioRelationChoice::Emit);
93 }
94 out.extend(
95 self.relations
96 .iter()
97 .map(|relation| RatioRelationChoice::Step {
98 label: relation.label.clone(),
99 interval: relation.interval,
100 }),
101 );
102 }
103
104 fn apply(&self, state: &Self::State, choice: &Self::Choice) -> SearchStep<Self::State> {
105 let RatioRelationChoice::Step { label, interval } = choice else {
106 let mut terminal = state.clone();
107 terminal.terminal = true;
108 return SearchStep::Continue(terminal);
109 };
110 let Some(current) = state.nodes.last().copied() else {
111 return SearchStep::infeasible("relation path has no current node");
112 };
113 let Ok(next) = current
114 .multiply(*interval)
115 .and_then(|ratio| ratio.canonical(self.policy))
116 else {
117 return SearchStep::pruned("relation exceeds ratio policy");
118 };
119 if state.nodes.contains(&next) {
120 return SearchStep::pruned("ratio relation cycle");
121 }
122 let mut nodes = state.nodes.clone();
123 nodes.push(next);
124 let mut labels = state.labels.clone();
125 labels.push(label.clone());
126 SearchStep::Continue(RatioRelationState {
127 nodes,
128 labels,
129 terminal: false,
130 })
131 }
132
133 fn finish(&self, state: &Self::State) -> Option<Self::Output> {
134 if !state.terminal {
135 return None;
136 }
137 Some(RatioRelationPath {
138 nodes: state.nodes.clone(),
139 labels: state.labels.clone(),
140 })
141 }
142
143 fn score_state(&self, state: &Self::State) -> i64 {
144 i64::try_from(state.labels.len()).unwrap_or(i64::MAX)
145 }
146}