Improviser.kt
src/jvmMain/kotlin/afield/math/oracle/Improviser.kt · 590 lines
package afield.math.oracle
import kotlin.math.exp
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sqrt
Playing a sequence memory back as a new performance: when to leave the passage being played, how to join the next one, and how to tell whether what came out still resembles the recording.
Improviser advances one sample at a time. It follows the recording, and once it has stayed on a passage for at least a phrase it may jump to another that shares context, joining the two with a correction that starts at the discontinuity and fades out. Every sample it emits is taken from the memory, and Performance records where from: the memory position behind each output sample, the size of the correction applied there, which channels the performer held, and a Segment wherever the source changed, carrying the Reason it changed.
Performance also carries the measurements, since whether a recombination still sounds like
the recording is not something the player can answer about itself. reuse and coverage count
memory positions; outputRepetition looks at the output alone, and so sees a loop assembled
from copies of one gesture taken from different places in memory, which reuse cannot;
joinRoughness and accelerationDistance compare the result against the original recording.
ContinuationJudge is declared here and implemented elsewhere: a second opinion that rates
proposed continuations, which Improviser either folds into its own scores or, in veto mode,
uses only to drop candidates. afield.math.temporal.ForecastJudge is the one that ships. A
judge can change which passage is played next; it proposes no samples of its own.
Policy selects between the oracle and two baselines (jumping to the nearest match in recent
history, with no oracle structure, and looping each episode unchanged), so a recombination can
be compared against doing less. Tests: VariableMarkovOracleTest, SeveralTakesTest and
ContinuationJudgeTest.
enum class Policy
How an Improviser chooses where to go next.
enum class Policy {
Play each episode straight through and start it again: the baseline that never varies.
Loop,
Jump to whatever earlier passage best matches the recent output, with no oracle structure.
NearestHistory,
Follow the recording and jump along the oracle's suffix links, which share context.
Oracle,
}
interface ContinuationJudge
Something that knows how a signal tends to move, asked to rate proposed futures — for an
Improviser that recombines memory, a second opinion on which continuation is plausible.
It sees every frame actually played (observe) and returns a loss for a proposed path
(pathchannel`, starting at output sampleindex); lower is better, null for no opinion.afield.math.temporal.ForecastJudge` rates paths against a Timestral forecast.
fun observe(index: Long, frame: DoubleArray)
fun loss(index: Long, path: Array<DoubleArray>): Double?
enum class JudgeMode
How an Improviser uses its ContinuationJudge.
enum class JudgeMode {
The judge's loss joins the score: it can favour one candidate, choose another, or keep the improviser where it is.
Prefer,
The judge only removes candidates: a jump whose loss is more than vetoRatio times that of
staying on the current passage is dropped, and the oracle chooses among the rest exactly as
it would alone. If every jump is vetoed, the improviser stays.
Veto,
}
enum class Reason { Start, Continue, SuffixLink, EpisodeEnd, Cue, Takeover, Hold, Restart }
Why a Segment began. Hold repeats the last frame because no acceptable continuation exists;
after holding for a phrase, Restart takes the closest match there is, whatever its distance or
correction, rather than hold for ever.
data class Segment(val outputStart: Int, val source: Int, val episode: Int, val reason: Reason, val distance: Double, val context: Int, val correction: Double)
One stretch of output played from one place in memory. outputStart is the output sample it
began on; source the memory sample it began from; episode which demonstration that is.
distance is the join distance (feature units) that admitted the jump, context the shared
context length in samples, and correction the size of the join correction it needed.
data class Segment(val outputStart: Int, val source: Int, val episode: Int, val reason: Reason, val distance: Double, val context: Int, val correction: Double)
class Performance
What an improviser played: the output itself, and for every output sample the memory sample it came from, the size of the join correction applied, and which channels a performer owned.
class Performance(
val output: Trajectory,
/** Memory sample per output sample (`-1` while holding). */
val source: IntArray,
/** Join-correction size per output sample: RMS over channels, in channel spreads (0 = played as recorded). */
val correction: DoubleArray,
/** `owned[channel][t]`: true where the performer drove that channel. */
val owned: Array<BooleanArray>,
val segments: List<Segment>,
) {
fun sourceSeconds(): List<Double>
The source trace in seconds of memory, ready to plot: a staircase of straight runs and jumps.
fun sourceSeconds(): List<Double> = source.map { if (it < 0) Double.NaN else it / output.sampleRate }
fun sourceReuse(window: Int = (output.sampleRate * 10).toInt()): Double
Fraction of output samples whose source passage was played at least once before within window samples.
fun sourceReuse(window: Int = (output.sampleRate * 10).toInt()): Double {
var r = 0
for (t in source.indices) {
val s = source[t]; if (s < 0) continue
for (u in max(0, t - window) until t - 1) if (source[u] == s) { r++; break }
}
return r.toDouble() / max(1, source.size)
}
fun coverage(memoryLength: Int): Double
Fraction of memory samples that appear somewhere in the output.
fun coverage(memoryLength: Int): Double = source.filter { it >= 0 }.toSet().size.toDouble() / memoryLength
fun outputRepetition(window: Double = 2.0, threshold: Double = 0.25, channels: List<Int> = output.channels.indices.toList(), rate: Double = 10.0): Double
How much the output repeats itself, whatever memory it came from: the fraction of
window-second stretches of output that closely match some earlier stretch, at least one
window back. "Closely" is an RMS difference below threshold channel spreads, measured on
channels (all by default; pick a few for a wide trajectory), with the output resampled to
rate Hz to keep the comparison cheap.
sourceReuse counts memory positions, so a loop made of copies of the same gesture from different places in memory does not show up there; it shows up here.
fun outputRepetition(window: Double = 2.0, threshold: Double = 0.25, channels: List<Int> = output.channels.indices.toList(), rate: Double = 10.0): Double {
val step = max(1, (output.sampleRate / rate).toInt())
val n = output.length / step
val w = max(2, (window * output.sampleRate / step).toInt())
if (n < 3 * w) return Double.NaN
val sd = channels.map { c ->
val v = output.values[c]; val m = v.average()
kotlin.math.sqrt(v.sumOf { (it - m) * (it - m) } / v.size).coerceAtLeast(1e-9)
}
val x = Array(n) { t -> DoubleArray(channels.size) { k -> output.values[channels[k]][t * step] / sd[k] } }
var repeated = 0; var total = 0
for (a in 2 * w until n - w + 1) {
total++
var found = false
var b = 0
while (!found && b + w <= a - w) {
var s = 0.0
for (k in 0 until w) for (c in channels.indices) { val d = x[a + k][c] - x[b + k][c]; s += d * d }
if (kotlin.math.sqrt(s / (w * channels.size)) < threshold) found = true
b++
}
if (found) repeated++
}
return repeated.toDouble() / max(1, total)
}
private fun accel(t: Trajectory, c: Int): DoubleArray =
DoubleArray(max(0, t.length - 2)) { i -> kotlin.math.abs(t.values[c][i + 2] - 2 * t.values[c][i + 1] + t.values[c][i]) }
fun joinRoughness(memory: Trajectory): Double
How rough the joins are, against the recording: the mean |second difference| within three
samples of every jump, divided by the memory's mean |second difference|, averaged over
channels. 1 means joins are as smooth as the recording is on average; above 1, the seams show.
NaN with no jumps.
fun joinRoughness(memory: Trajectory): Double {
val jumps = segments.filter { it.reason != Reason.Start && it.reason != Reason.Continue && it.reason != Reason.Hold }.map { it.outputStart }
if (jumps.isEmpty()) return Double.NaN
var s = 0.0; var n = 0
for (c in 0 until memory.width) {
val ref = accel(memory, c).average().coerceAtLeast(1e-12)
val a = accel(output, c)
for (j in jumps) for (k in j - 3..j + 3) if (k in a.indices) { s += a[k] / ref; n++ }
}
return s / max(1, n)
}
fun accelerationDistance(memory: Trajectory): Double
Kolmogorov–Smirnov distance between the output's and the memory's distributions of
|second difference| (how sharply things change), worst channel: 0 = indistinguishable, 1 = disjoint.
fun accelerationDistance(memory: Trajectory): Double = (0 until memory.width).maxOf { c ->
val a = accel(memory, c).sorted(); val b = accel(output, c).sorted()
var i = 0; var j = 0; var d = 0.0
while (i < a.size && j < b.size) {
val v = minOf(a[i], b[j])
while (i < a.size && a[i] == v) i++
while (j < b.size && b[j] == v) j++
d = maxOf(d, kotlin.math.abs(i.toDouble() / a.size - j.toDouble() / b.size))
}
d
}
override fun toString()
override fun toString() = "Performance(${output.length} samples, ${segments.size} segments, sourceReuse=${"%.2f".format(sourceReuse())})"
}
class Improviser
Plays an oracle's memory forward one sample at a time, recombining it.
By default it continues along the current source passage. Once it has stayed for at least
minPhrase seconds it may, with probability jumpProbability per sample, take an oracle
jump: to a state that shares a suffix with the current one. Candidates are scored by shared
context length, by the join distance (how far the candidate's preceding sample is from where
the output is now, value and velocity), and by how much of their next phrase was played within
the last reuseMemory seconds (scaled by reuseWeight; raise either to push towards material
not heard for a while); one is
sampled from a seeded softmax with temperature. A candidate whose join distance exceeds
maxJoin (a multiple of the oracle threshold), or whose join would need a correction larger
than maxCorrection (RMS over channels, in channel standard deviations), or that leaves less
than minPhrase before the end of its episode, is rejected rather than smoothed over. At the
end of an episode a jump is compulsory; if no candidate is acceptable it falls back to
matching recent history, and if that is worse than declineDistance it holds.
With several episodes and bridges on, the candidates also include VariableMarkovOracle.bridges:
the nearest moments in other takes, which carry no shared context, so the oracle prefers its own
links unless reuseWeight pushes it towards material it has not played recently.
With escapeReplays (on by default), playing material already played in the last reuseMemory
seconds is itself a reason to leave: after a phrase, a jump is considered on every step of a
replay rather than with jumpProbability, so the improviser does not coast through a repeat.
Every jump is joined by a correction that starts at the discontinuity and fades over
joinTime seconds; its magnitude is recorded in Performance.correction, not hidden.
perform hands a channel to the performer: the improviser plays the performer's values on it
and, every cueInterval seconds, realigns the free channels by cueing the memory with the
performed channels. It only moves after minPhrase seconds on a passage, and only to a match
whose distance is below takeoverMargin times the current passage's, so an input unlike
anything in memory leaves the free channels playing on rather than jumping around.
release gives the channel back with the same fading correction.
With a judge, every jump decision also rates each candidate's next judgeAhead seconds —
and staying on the current passage — by the judge's loss. The loss enters the score relative
to the candidates' mean (judgeWeight × loss / mean loss), so the judge can prefer a jump,
choose another, or veto jumping. With judgeMode = JudgeMode.Veto it only removes jumps
whose loss exceeds vetoRatio times that of staying, leaving the choice among the rest to the
oracle. Every sample still comes from memory. judgedDecisions,
judgeChanged and judgeStayed count how often it had a say and how often it changed the outcome.
Stepping is cheap (no allocation beyond the recent-history buffer, a cue every few samples while performing), so step can run directly in a frame loop.
declaration — 51 lines, from line 241
class Improviser(
val oracle: VariableMarkovOracle,
seed: Long = 1,
val policy: Policy = Policy.Oracle,
val minPhrase: Double = 1.0,
val jumpProbability: Double = 0.2,
val maxJoin: Double = 2.0,
val temperature: Double = 1.0,
val joinTime: Double = 0.15,
val cueWindow: Double = 1.0,
val cueInterval: Double = 0.25,
val declineDistance: Double = 1.5,
val maxCorrection: Double = 1.0,
val takeoverMargin: Double = 0.7,
val judge: ContinuationJudge? = null,
val judgeWeight: Double = 2.0,
val judgeAhead: Double = 2.0,
val judgeMode: JudgeMode = JudgeMode.Prefer,
val reuseWeight: Double = 1.0,
val reuseMemory: Double = 10.0,
val bridges: Boolean = true,
val restartTolerance: Double = 2.0,
val escapeReplays: Boolean = true,
val vetoRatio: Double = 1.5,
start: Int = 0,
) {
private val t = oracle.trajectory
private val rate = t.sampleRate
private val width = t.width
private var rng = java.util.Random(seed)
private val minPhraseSteps = max(1, (minPhrase * rate).roundToInt())
private val joinSteps = max(1, (joinTime * rate).roundToInt())
private val cueWindowSteps = max(2, (cueWindow * rate).roundToInt())
private val cueIntervalSteps = max(1, (cueInterval * rate).roundToInt())
private val recentCap = max(cueWindowSteps * 2, (rate * 10).toInt())
private var pos = -1
private var startAt = start
private var phrase = 0
private var outputIndex = 0
private val offset = DoubleArray(width)
private val offsetLeft = IntArray(width)
private val owned = BooleanArray(width)
private val performed = DoubleArray(width)
private var sinceCue = 0
private var holding = 0
private val reuseMemorySteps = max(1, (reuseMemory * rate).roundToInt())
private val lastPlayed = IntArray(t.length) { -1 }
private val recent = ArrayDeque<DoubleArray>()
private val recentSource = ArrayDeque<Int>()
private var pendingReason: Reason? = null
val segments = ArrayList<Segment>()
Segments played so far (the most recent 10 000).
var judgedDecisions = 0; private set
Jump decisions the judge scored.
var judgeChanged = 0; private set
Judged decisions the judge altered: in JudgeMode.Prefer, its most likely choice differed
from the unjudged one; in JudgeMode.Veto, it removed at least one candidate.
var judgeVetoed = 0; private set
Candidate jumps the judge vetoed (JudgeMode.Veto).
var judgeStayed = 0; private set
Judged decisions that ended with staying on the current passage.
val position: Int get() = pos
Memory sample of the last output (or -1 when holding).
var lastCorrection = 0.0; private set
Correction magnitude applied to the last output.
fun perform(channel: Int, value: Double) { owned[channel] = true; performed[channel] = value }
Hands channel to the performer at value; call every sample while performing.
fun perform(name: String, value: Double) = perform(t.channels.indexOf(name), value)
Hands the channel named name to the performer.
fun release(channel: Int)
Gives channel back to the memory, fading from the performed value over joinTime.
fun release(channel: Int) {
if (!owned[channel]) return
owned[channel] = false
// the fade starts from the performer's last value; the next step's raw value fills it in
offset[channel] = Double.NaN
offsetLeft[channel] = joinSteps
pendingReason = Reason.Cue
}
fun release(name: String) = release(t.channels.indexOf(name))
fun isPerformed(channel: Int) = owned[channel]
True while channel is performer-owned.
fun reset(seed: Long, at: Int = 0)
Restart from memory sample at with a fresh random sequence.
fun reset(seed: Long, at: Int = 0) {
rng = java.util.Random(seed); pos = -1; startAt = at; phrase = 0; outputIndex = 0
offset.fill(0.0); offsetLeft.fill(0); owned.fill(false); recent.clear(); recentSource.clear(); segments.clear(); lastPlayed.fill(-1)
}
fun step(): DoubleArray
Advances one sample and returns the output frame.
body — 86 lines, from line 345
fun step(): DoubleArray {
val last = recent.lastOrNull()
var reason = Reason.Continue
var dist = 0.0
var context = 0
var next: Int
if (pos < 0 && last == null) { next = startAt; reason = Reason.Start }
else {
val natural = if (pos >= 0 && !t.endsEpisode(pos)) pos + 1 else -1
next = natural
val performing = owned.any { it }
if (performing && sinceCue >= cueIntervalSteps && recent.size >= 2 && (phrase >= minPhraseSteps || natural < 0)) {
sinceCue = 0
val hist = recent.toList()
val c = oracle.cue(hist, cueWindowSteps, owned.copyOf(), count = 1).firstOrNull()
// hysteresis: stay on the current passage unless the new match is clearly better
val here = if (natural >= 0) oracle.matchDistance(hist, pos, cueWindowSteps, owned.copyOf()) else Double.POSITIVE_INFINITY
if (c != null && !t.endsEpisode(c.position) && c.position != pos && c.distance < takeoverMargin * here && correctionFor(c.position + 1) <= maxCorrection) { next = c.position + 1; reason = Reason.Takeover; dist = c.distance; context = c.context }
} else if (pendingReason == Reason.Cue && recent.size >= 2) {
val c = oracle.cue(recent.toList(), cueWindowSteps, count = 1).firstOrNull()
if (c != null && !t.endsEpisode(c.position) && c.distance < declineDistance) { next = c.position + 1; reason = Reason.Cue; dist = c.distance; context = c.context }
} else if (natural < 0 || (phrase >= minPhraseSteps && policy != Policy.Loop && (replaying(natural) || rng.nextDouble() < jumpProbability))) {
val j = jump()
if (j != null && j.second == Reason.Continue) { /* the judge chose to stay */ }
else if (j != null) { next = j.first; reason = j.second; dist = j.third.first; context = j.third.second }
else if (natural < 0) {
when (policy) {
Policy.Loop -> { next = t.episodeStarts[t.episodeOf(pos)]; reason = Reason.EpisodeEnd }
else -> {
val st = if (policy == Policy.Oracle) freshStart() else -1
val c = if (st >= 0) Cue(st - 1, 0.0, 0) else if (recent.size >= 2) cueAway() else null
if (c != null) { next = c.position + 1; reason = Reason.EpisodeEnd; dist = c.distance; context = c.context }
else if (holding >= minPhraseSteps && recent.size >= 2) {
// held a whole phrase: take the closest place there is rather than freeze
val best = oracle.cue(recent.toList(), cueWindowSteps, count = 6, separation = minPhraseSteps).firstOrNull { !t.endsEpisode(it.position) && runway(it.position + 1) >= minPhraseSteps }
if (best != null) { next = best.position + 1; reason = Reason.Restart; dist = best.distance; context = best.context }
else { next = -1; reason = Reason.Hold }
}
else { next = -1; reason = Reason.Hold }
}
}
}
}
pendingReason = null
}
val out: DoubleArray
if (next < 0) {
out = last!!.copyOf()
for (c in 0 until width) if (owned[c]) out[c] = performed[c]
lastCorrection = 0.0
} else {
val raw = t.frame(next)
val jumped = reason != Reason.Continue && reason != Reason.Start
if (jumped && last != null) {
val prev = if (recent.size >= 2) recent[recent.size - 2] else last
for (c in 0 until width) { offset[c] = (2 * last[c] - prev[c]) - raw[c]; offsetLeft[c] = joinSteps }
}
out = DoubleArray(width)
var mag = 0.0
for (c in 0 until width) {
if (owned[c]) { out[c] = performed[c]; continue }
if (offset[c].isNaN()) offset[c] = performed[c] - raw[c]
val o = if (offsetLeft[c] > 0) offset[c] * offsetLeft[c] / joinSteps else 0.0
if (offsetLeft[c] > 0) offsetLeft[c]--
out[c] = raw[c] + o
val s = o / oracle.features.scale[c]; mag += s * s
}
lastCorrection = sqrt(mag / width)
}
if (reason != Reason.Continue || segments.isEmpty()) {
segments.add(Segment(outputIndex, next, if (next >= 0) t.episodeOf(next) else -1, reason, dist, context, if (next >= 0) offsetMagnitude() else 0.0))
if (segments.size > 10_000) segments.removeAt(0)
phrase = 0
}
holding = if (next < 0) holding + 1 else 0
if (next >= 0) lastPlayed[next] = outputIndex
phrase++; sinceCue++; outputIndex++
pos = next
judge?.observe(outputIndex - 1L, out)
recent.addLast(out); recentSource.addLast(next)
if (recent.size > recentCap) { recent.removeFirst(); recentSource.removeFirst() }
return out
}
True when next was already played within the last reuseMemory seconds and escapeReplays is on.
private fun replaying(next: Int): Boolean =
escapeReplays && next >= 0 && lastPlayed[next] >= 0 && outputIndex - lastPlayed[next] <= reuseMemorySteps
Samples left in the episode from sample next, inclusive.
private fun runway(next: Int): Int {
val e = t.episodeOf(next)
val end = if (e + 1 < t.episodeStarts.size) t.episodeStarts[e + 1] else t.length
return end - next
}
Size (in channel spreads) of the correction a jump to next would start with.
private fun correctionFor(next: Int): Double {
val last = recent.lastOrNull() ?: return 0.0
val prev = if (recent.size >= 2) recent[recent.size - 2] else last
var m = 0.0; var n = 0
for (c in 0 until width) if (!owned[c]) { val s = ((2 * last[c] - prev[c]) - t.values[c][next]) / oracle.features.scale[c]; m += s * s; n++ }
return sqrt(m / max(1, n))
}
private fun offsetMagnitude(): Double {
var m = 0.0; var n = 0
for (c in 0 until width) if (!owned[c] && !offset[c].isNaN()) { val s = offset[c] / oracle.features.scale[c]; m += s * s; n++ }
return sqrt(m / max(1, n))
}
Oracle (or nearest-history) jump: next sample, reason, (join distance, context).
source — 88 lines, from line 459
private fun jump(): Triple<Int, Reason, Pair<Double, Int>>? {
if (pos < 0) return null
if (policy == Policy.NearestHistory) {
val c = cueAway() ?: return null
return Triple(c.position + 1, Reason.Cue, c.distance to c.context)
}
if (policy != Policy.Oracle) return null
val s = pos + 1
val cands = LinkedHashMap<Int, Int>()
val link = oracle.sfx[s]
if (link > 0) cands[link] = oracle.lrs[s]
for (r in oracle.reverseSuffixes(s)) cands[r] = oracle.lrs[r]
if (link > 0) for (r in oracle.reverseSuffixes(link)) if (r != s) cands.putIfAbsent(r, oracle.lrs[r])
// cross-take bridges: continue from the sample after a near match in another take (no shared context)
if (bridges) for (j in oracle.bridges(pos)) cands.putIfAbsent(j + 1, 0)
val th = oracle.threshold
val opts = ArrayList<Triple<Int, Double, Int>>()
for ((c, ctx) in cands) {
// state c holds sample c-1; continuing from it plays sample c
if (c - 1 == pos || c >= t.length || t.endsEpisode(c - 1) || runway(c) < minPhraseSteps) continue
val d = distance(oracle.frames[c - 1], oracle.frames[pos])
if (d > maxJoin * th || correctionFor(c) > maxCorrection) continue
opts.add(Triple(c, d, ctx))
}
if (opts.isEmpty()) return null
val base = opts.map { (c, d, ctx) ->
// the share of the next phrase from c that was played within the last reuseMemory seconds
var played = 0
for (k in c until min(t.length, c + minPhraseSteps)) if (lastPlayed[k] >= 0 && outputIndex - lastPlayed[k] <= reuseMemorySteps) played++
val reuse = played.toDouble() / minPhraseSteps
kotlin.math.ln(1.0 + ctx) - d / th - reuseWeight * reuse
}.toMutableList()
val natural = if (!t.endsEpisode(pos)) pos + 1 else -1
val scores = ArrayList(base)
val j = judge
if (j != null) {
val ahead = max(1, (judgeAhead * rate).roundToInt())
fun path(from: Int): Array<DoubleArray> {
var len = 0
while (len < ahead && from + len < t.length && (len == 0 || !t.startsEpisode(from + len))) len++
return Array(width) { c -> DoubleArray(len) { t.values[c][from + it] } }
}
// staying is an option the judge can prefer: scored as a zero-join, full-context candidate
if (natural >= 0) {
opts.add(Triple(natural, 0.0, oracle.lrs[natural]))
base.add(kotlin.math.ln(1.0 + oracle.lrs[natural]))
scores.add(base.last())
}
val losses = opts.map { (c, _, _) -> j.loss(outputIndex.toLong(), path(c)) }
if (judgeMode == JudgeMode.Veto) {
// staying is the reference, not a candidate: drop it, and any jump much worse than it
val stay = if (natural >= 0) losses.last() else null
if (natural >= 0) { opts.removeAt(opts.size - 1); base.removeAt(base.size - 1); scores.removeAt(scores.size - 1) }
if (stay != null && losses.all { it != null }) {
judgedDecisions++
val keep = opts.indices.filter { losses[it]!! <= vetoRatio * stay }
val vetoed = opts.size - keep.size
judgeVetoed += vetoed
if (keep.isEmpty()) { judgeStayed++; judgeChanged++; return Triple(natural, Reason.Continue, 0.0 to oracle.lrs[natural]) }
if (vetoed > 0) {
judgeChanged++
val ko = keep.map { opts[it] }; val kb = keep.map { base[it] }
opts.clear(); opts.addAll(ko); base.clear(); base.addAll(kb); scores.clear(); scores.addAll(kb)
}
}
} else if (losses.all { it != null }) {
val mean = losses.sumOf { it!! } / losses.size
if (mean > 0) {
judgedDecisions++
val before = base.indices.filter { natural < 0 || opts[it].first != natural }.maxBy { base[it] }
for (i in scores.indices) scores[i] = base[i] - judgeWeight * losses[i]!! / mean
val after = scores.indices.maxBy { scores[it] }
if (after != before) judgeChanged++
}
} else if (natural >= 0) {
// no opinion yet: decide as without a judge
opts.removeAt(opts.size - 1); base.removeAt(base.size - 1); scores.removeAt(scores.size - 1)
}
}
val m = scores.max()
val w = scores.map { exp((it - m) / temperature) }
var u = rng.nextDouble() * w.sum()
var pick = opts.size - 1
for (i in opts.indices) { u -= w[i]; if (u <= 0) { pick = i; break } }
val o = opts[pick]
if (o.first == natural && j != null) { judgeStayed++; return Triple(natural, Reason.Continue, 0.0 to o.third) }
return Triple(o.first, Reason.SuffixLink, o.second to o.third)
}
At the end of an episode: the start of another episode whose first sample is within
restartTolerance of where the output is, the one played least recently; -1 if none.
private fun freshStart(): Int {
if (t.episodeStarts.size < 2 || pos < 0) return -1
val here = t.episodeOf(pos)
return t.episodeStarts.withIndex()
.filter { (e, st) -> e != here && runway(st) >= minPhraseSteps && correctionFor(st) <= restartTolerance }
.minByOrNull { (_, st) -> lastPlayed[st] }?.value ?: -1
}
Best history match that is not where we already are.
private fun cueAway(): Cue? {
// at an episode end, the best matches are often other endings: insist on room to play
val need = if (pos >= 0 && t.endsEpisode(pos)) 3 * minPhraseSteps else minPhraseSteps
return oracle.cue(recent.toList(), cueWindowSteps, count = 12, separation = minPhraseSteps)
.firstOrNull { kotlin.math.abs(it.position - pos) >= minPhraseSteps && !t.endsEpisode(it.position) && runway(it.position + 1) >= need && it.distance < declineDistance && correctionFor(it.position + 1) <= maxCorrection }
}
fun take(samples: Int, script: (Improviser.(Int) -> Unit)?
Runs samples steps and returns the Performance. script runs before each step with the
step index, for scripted takeovers: { i -> if (i in 300..450) perform("x", 0.0) else if (i == 451) release("x") }.
fun take(samples: Int, script: (Improviser.(Int) -> Unit)? = null): Performance {
val out = Array(width) { DoubleArray(samples) }
val src = IntArray(samples)
val cor = DoubleArray(samples)
val own = Array(width) { BooleanArray(samples) }
val segStart = segments.size
for (i in 0 until samples) {
script?.invoke(this, i)
val f = step()
for (c in 0 until width) { out[c][i] = f[c]; own[c][i] = owned[c] }
src[i] = pos; cor[i] = lastCorrection
}
return Performance(Trajectory(rate, t.channels, out, name = "${policy.name.lowercase()} from ${t.name}"), src, cor, own, segments.subList(segStart, segments.size).toList())
}
fun take(seconds: Double, script: (Improviser.(Int) -> Unit)?
Runs for seconds of output.
fun take(seconds: Double, script: (Improviser.(Int) -> Unit)? = null): Performance = take((seconds * rate).roundToInt(), script)
}