theme_generator / vendor /muses /src /algos /dynaprog.py
pachet's picture
Vendor app dependencies for Docker build
8b54dab
Raw
History Blame Contribute Delete
6.72 kB
"""
Copyright (c) 2025 Ynosound.
All rights reserved.
See LICENSE file in the project root for full license information.
"""
class VariableDomainSequenceOptimizer:
"""
A class for solving sequence assignment problems with variable domains:
We have positions i = 0..n-1, each with its own domain[i].
We want to minimize:
sum_{i=0}^{n-1} unary_cost(i, x_i)
+ sum_{i=0}^{n-2} binary_cost(i, x_i, i+1, x_{i+1})
using dynamic programming, supporting different domain sizes per position.
"""
def __init__(self, domains, unary_cost, binary_cost, precompute_binary=False):
"""
Parameters
----------
domains : list of lists
domains[i] is the list of allowable labels for position i.
E.g., domains[0] = [0,1,2], domains[1] = ['A','B'], etc.
unary_cost : function (i, x) -> float
A function that gives the cost of assigning value x at position i.
binary_cost : function (i, x, i+1, y) -> float
A function that gives the cost of assigning x at position i and y
at position i+1.
precompute_binary : bool
If true, materialize all binary-cost tables during initialization.
The default computes binary costs lazily during fit, which is faster
for the small variable domains typical of MusES chord analysis.
"""
self.n = len(domains)
self.domains = [tuple(domain) for domain in domains]
for i, domain in enumerate(self.domains):
if not domain:
raise ValueError(f"Domain at position {i} is empty")
self.unary_cost_func = unary_cost
self.binary_cost_func = binary_cost
self.U = self._compute_unary_tables()
if precompute_binary and self.n > 1:
self.B = self._compute_binary_tables()
elif precompute_binary:
self.B = []
else:
self.B = None
self.dp = [None] * self.n
self.backpointer = [None] * self.n
def _compute_unary_tables(self):
"""
For each position i, create a list where
U[i][d] = unary_cost_func(i, domains[i][d]).
"""
U = []
unary_cost = self.unary_cost_func
for i, domain in enumerate(self.domains):
U.append([float(unary_cost(i, label)) for label in domain])
return U
def _compute_binary_tables(self):
"""
For each i in [0..n-2], create a table where
B[i][d1][d2] = binary_cost_func(i, domains[i][d1], i+1,
domains[i+1][d2]).
"""
B = []
binary_cost = self.binary_cost_func
for i in range(self.n - 1):
dom_i = self.domains[i]
dom_next = self.domains[i + 1]
B.append([
[float(binary_cost(i, label1, i + 1, label2)) for label2 in dom_next]
for label1 in dom_i
])
return B
def fit(self):
"""
Run the dynamic programming to find the minimum total cost and the best assignment.
Returns
-------
(min_cost, best_sequence)
min_cost : float
The minimal total cost.
best_sequence : list
A list of length n with the optimal label for each position.
"""
if self.n == 0:
return 0.0, []
self.dp[self.n - 1] = list(self.U[self.n - 1])
self.backpointer[self.n - 1] = [-1] * len(self.domains[self.n - 1])
for i in range(self.n - 2, -1, -1):
next_dp = self.dp[i + 1]
unary_table = self.U[i]
dp_i = []
bp_i = []
if self.B is None:
next_domain = self.domains[i + 1]
binary_cost = self.binary_cost_func
for label, unary_cost in zip(self.domains[i], unary_table):
best_index = 0
best_cost = (
float(binary_cost(i, label, i + 1, next_domain[0]))
+ next_dp[0]
)
for next_index in range(1, len(next_domain)):
cost = (
float(binary_cost(i, label, i + 1, next_domain[next_index]))
+ next_dp[next_index]
)
if cost < best_cost:
best_cost = cost
best_index = next_index
dp_i.append(unary_cost + best_cost)
bp_i.append(best_index)
else:
binary_table = self.B[i]
for row, unary_cost in zip(binary_table, unary_table):
best_index = 0
best_cost = row[0] + next_dp[0]
for next_index in range(1, len(row)):
cost = row[next_index] + next_dp[next_index]
if cost < best_cost:
best_cost = cost
best_index = next_index
dp_i.append(unary_cost + best_cost)
bp_i.append(best_index)
self.dp[i] = dp_i
self.backpointer[i] = bp_i
best_start = min(range(len(self.dp[0])), key=self.dp[0].__getitem__)
min_cost = self.dp[0][best_start]
best_sequence = [None] * self.n
best_sequence[0] = self.domains[0][best_start]
prev_index = best_start
for i in range(0, self.n - 1):
next_index = self.backpointer[i][prev_index]
best_sequence[i + 1] = self.domains[i + 1][next_index]
prev_index = next_index
return min_cost, best_sequence
# ---------------------------------------------------------------------------
# Example usage:
if __name__ == "__main__":
# Suppose we have 4 positions, each with a different domain of labels:
domains = [
[0, 1], # position 0
[0, 1, 2], # position 1
['A', 'B'], # position 2
[10, 20, 30] # position 3
]
# A simple unary cost function that depends on i and x
def unary_cost(i, x):
# e.g., cost is i * int(x != 0) just as a silly example
# for non-integer x, we'll treat 'A'/'B' or whatever carefully
return 1.0 if x != 0 else 0.0
# A simple binary cost function
def binary_cost(i, x, j, y):
# For demonstration, let's say cost = 1 if x == y, else 0
return float(x == y)
optimizer = VariableDomainSequenceOptimizer(domains, unary_cost, binary_cost)
cost, best_seq = optimizer.fit()
print("Minimal cost:", cost)
print("Best sequence:", best_seq)