///|
/// Repeatable model execution sessions.
///
/// A session records attempts, signatures, and solver counters around a model.
/// It is intended for local acceptance runs and for collecting evidence that a
/// change remains deterministic across repeated solves.
pub struct SolveAttempt {
index : Int
solved : Bool
solution_count : Int
work : Int
signature : Int
}
///|
/// Create an attempt record.
pub fn solve_attempt(
index : Int,
solved : Bool,
solution_count : Int,
stats : SearchStats,
signature : Int,
) -> SolveAttempt {
{
index,
solved,
solution_count,
work: stats.node_count() +
stats.propagation_count() +
stats.check_count() +
stats.pruned_count(),
signature,
}
}
///|
/// A repeated execution session.
pub struct SolveSession {
name : String
attempts : Array[SolveAttempt]
signatures : Array[Int]
}
///|
/// Create an empty session.
pub fn solve_session(name : String) -> SolveSession {
{ name, attempts: [], signatures: [] }
}
///|
/// Execute a solver once and record its result.
pub fn SolveSession::run(
self : SolveSession,
solver : Solver,
limit : Int,
) -> SolveAttempt {
solver.limit(limit)
let solutions = solver.solve_all()
let signature = if solutions.length() == 0 {
0
} else {
solution_signature(solutions[0])
}
let attempt = solve_attempt(
self.attempts.length(),
solutions.length() > 0,
solutions.length(),
solver.stats(),
signature,
)
self.attempts.push(attempt)
self.signatures.push(signature)
attempt
}
///|
/// Return attempt count.
pub fn SolveSession::length(self : SolveSession) -> Int {
self.attempts.length()
}
///|
/// Return attempts.
pub fn SolveSession::attempts(self : SolveSession) -> Array[SolveAttempt] {
self.attempts.copy()
}
///|
/// Return whether all attempts solved.
pub fn SolveSession::all_solved(self : SolveSession) -> Bool {
for attempt in self.attempts {
if !attempt.solved {
return false
}
}
true
}
///|
/// Return whether all signatures agree.
pub fn SolveSession::deterministic(self : SolveSession) -> Bool {
if self.signatures.length() < 2 {
return true
}
for signature in self.signatures {
if signature != self.signatures[0] {
return false
}
}
true
}
///|
/// Return total work.
pub fn SolveSession::total_work(self : SolveSession) -> Int {
let mut result = 0
for attempt in self.attempts {
result += attempt.work
}
result
}
///|
/// Return average work.
pub fn SolveSession::average_work(self : SolveSession) -> Int {
if self.attempts.length() == 0 {
0
} else {
self.total_work() / self.attempts.length()
}
}
///|
/// Return the largest work attempt.
pub fn SolveSession::maximum_work(self : SolveSession) -> Int {
let mut result = 0
for attempt in self.attempts {
if attempt.work > result {
result = attempt.work
}
}
result
}
///|
/// Return whether work is stable within a tolerance.
pub fn SolveSession::work_stable(
self : SolveSession,
tolerance_percent : Int,
) -> Bool {
if self.attempts.length() < 2 {
return true
}
let baseline = self.attempts[0].work
for attempt in self.attempts[1:] {
if regressed(baseline, attempt.work, tolerance_percent) ||
regressed(attempt.work, baseline, tolerance_percent) {
return false
}
}
true
}
///|
/// Return a quality review for a session.
pub fn SolveSession::review(self : SolveSession) -> QualityReview {
let review = quality_review()
review.require(
"all-solved",
self.all_solved(),
"every attempt produced a solution",
)
review.require(
"deterministic",
self.deterministic(),
"first solution signatures agree",
)
review.minimum("attempts", self.attempts.length(), 1, "at least one run")
review
}
///|
/// Return a compact session report.
pub fn SolveSession::describe(self : SolveSession) -> String {
"\{self.name}: attempts=\{self.attempts.length()}, solved=\{self.all_solved()}, deterministic=\{self.deterministic()}, average_work=\{self.average_work()}"
}
///|
/// Return a session fingerprint.
pub fn SolveSession::signature(self : SolveSession) -> Int {
let mut result = 31
for attempt in self.attempts {
result = result * 37 + attempt.signature + attempt.work
}
result
}
///|
/// Return a solution fingerprint.
pub fn solution_signature(solution : Solution) -> Int {
let mut result = 17
for index, value in solution.values {
result = result * 31 + value + index * 7
}
result
}
///|
/// Return whether two attempts agree on outcome and signature.
pub fn attempts_equal(left : SolveAttempt, right : SolveAttempt) -> Bool {
left.solved == right.solved &&
left.solution_count == right.solution_count &&
left.signature == right.signature
}
///|
/// Return attempts ordered by work.
pub fn sort_attempts_by_work(
attempts : Array[SolveAttempt],
) -> Array[SolveAttempt] {
let result = attempts.copy()
for left in 0.. String {
"attempt=\{self.index}, solved=\{self.solved}, solutions=\{self.solution_count}, work=\{self.work}, signature=\{self.signature}"
}
///|
/// Return the number of distinct outcomes.
pub fn distinct_attempt_signatures(attempts : Array[SolveAttempt]) -> Int {
let signatures : Array[Int] = []
for attempt in attempts {
if !signatures.contains(attempt.signature) {
signatures.push(attempt.signature)
}
}
signatures.length()
}