@cavelang/solver 0.34.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2155 -3
- package/dist/src/adapter.d.ts +6 -0
- package/dist/src/adapter.d.ts.map +1 -1
- package/dist/src/adapter.js +3 -0
- package/dist/src/adapter.js.map +1 -1
- package/dist/src/balanced-reduce.d.ts +3 -0
- package/dist/src/balanced-reduce.d.ts.map +1 -0
- package/dist/src/balanced-reduce.js +10 -0
- package/dist/src/balanced-reduce.js.map +1 -0
- package/dist/src/canonical-owned.d.ts +5 -0
- package/dist/src/canonical-owned.d.ts.map +1 -0
- package/dist/src/canonical-owned.js +206 -0
- package/dist/src/canonical-owned.js.map +1 -0
- package/dist/src/canonical.d.ts +3 -2
- package/dist/src/canonical.d.ts.map +1 -1
- package/dist/src/canonical.js +12 -85
- package/dist/src/canonical.js.map +1 -1
- package/dist/src/capability.d.ts.map +1 -1
- package/dist/src/capability.js +43 -33
- package/dist/src/capability.js.map +1 -1
- package/dist/src/clone.d.ts +3 -0
- package/dist/src/clone.d.ts.map +1 -0
- package/dist/src/clone.js +54 -0
- package/dist/src/clone.js.map +1 -0
- package/dist/src/constant-sign.d.ts +5 -0
- package/dist/src/constant-sign.d.ts.map +1 -0
- package/dist/src/constant-sign.js +54 -0
- package/dist/src/constant-sign.js.map +1 -0
- package/dist/src/context.d.ts +6 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +151 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/diagnostic-text.d.ts +3 -0
- package/dist/src/diagnostic-text.d.ts.map +1 -0
- package/dist/src/diagnostic-text.js +5 -0
- package/dist/src/diagnostic-text.js.map +1 -0
- package/dist/src/evaluate-expression.d.ts +4 -0
- package/dist/src/evaluate-expression.d.ts.map +1 -0
- package/dist/src/evaluate-expression.js +24 -0
- package/dist/src/evaluate-expression.js.map +1 -0
- package/dist/src/exact.d.ts +2 -0
- package/dist/src/exact.d.ts.map +1 -1
- package/dist/src/exact.js +125 -28
- package/dist/src/exact.js.map +1 -1
- package/dist/src/explain.d.ts +4 -0
- package/dist/src/explain.d.ts.map +1 -1
- package/dist/src/explain.js +330 -91
- package/dist/src/explain.js.map +1 -1
- package/dist/src/explanation-budget.d.ts +14 -0
- package/dist/src/explanation-budget.d.ts.map +1 -0
- package/dist/src/explanation-budget.js +46 -0
- package/dist/src/explanation-budget.js.map +1 -0
- package/dist/src/expression-key.d.ts +8 -0
- package/dist/src/expression-key.d.ts.map +1 -0
- package/dist/src/expression-key.js +54 -0
- package/dist/src/expression-key.js.map +1 -0
- package/dist/src/fraction-product.d.ts +3 -0
- package/dist/src/fraction-product.d.ts.map +1 -0
- package/dist/src/fraction-product.js +13 -0
- package/dist/src/fraction-product.js.map +1 -0
- package/dist/src/fraction-sum.d.ts +3 -0
- package/dist/src/fraction-sum.d.ts.map +1 -0
- package/dist/src/fraction-sum.js +23 -0
- package/dist/src/fraction-sum.js.map +1 -0
- package/dist/src/integer-gcd.d.ts +3 -0
- package/dist/src/integer-gcd.d.ts.map +1 -0
- package/dist/src/integer-gcd.js +50 -0
- package/dist/src/integer-gcd.js.map +1 -0
- package/dist/src/linear.d.ts +2 -1
- package/dist/src/linear.d.ts.map +1 -1
- package/dist/src/linear.js +85 -17
- package/dist/src/linear.js.map +1 -1
- package/dist/src/numeric-size.d.ts +2 -0
- package/dist/src/numeric-size.d.ts.map +1 -0
- package/dist/src/numeric-size.js +33 -0
- package/dist/src/numeric-size.js.map +1 -0
- package/dist/src/options.d.ts +4 -0
- package/dist/src/options.d.ts.map +1 -0
- package/dist/src/options.js +18 -0
- package/dist/src/options.js.map +1 -0
- package/dist/src/result.d.ts +4 -0
- package/dist/src/result.d.ts.map +1 -0
- package/dist/src/result.js +29 -0
- package/dist/src/result.js.map +1 -0
- package/dist/src/solve.d.ts.map +1 -1
- package/dist/src/solve.js +34 -3
- package/dist/src/solve.js.map +1 -1
- package/dist/src/validate.d.ts +5 -0
- package/dist/src/validate.d.ts.map +1 -1
- package/dist/src/validate.js +323 -65
- package/dist/src/validate.js.map +1 -1
- package/dist/src/workflow.d.ts.map +1 -1
- package/dist/src/workflow.js +98 -26
- package/dist/src/workflow.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +9 -0
- package/src/balanced-reduce.ts +9 -0
- package/src/canonical-owned.ts +186 -0
- package/src/canonical.ts +14 -90
- package/src/capability.ts +41 -30
- package/src/clone.ts +47 -0
- package/src/constant-sign.ts +59 -0
- package/src/context.ts +114 -0
- package/src/diagnostic-text.ts +4 -0
- package/src/evaluate-expression.ts +21 -0
- package/src/exact.ts +122 -27
- package/src/explain.ts +342 -94
- package/src/explanation-budget.ts +47 -0
- package/src/expression-key.ts +45 -0
- package/src/fraction-product.ts +10 -0
- package/src/fraction-sum.ts +20 -0
- package/src/integer-gcd.ts +47 -0
- package/src/linear.ts +82 -16
- package/src/numeric-size.ts +26 -0
- package/src/options.ts +18 -0
- package/src/result.ts +28 -0
- package/src/solve.ts +34 -3
- package/src/validate.ts +312 -66
- package/src/workflow.ts +92 -26
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Expression } from './model.ts'
|
|
2
|
+
|
|
3
|
+
/** Ordered syntax identity for an already captured, validated expression.
|
|
4
|
+
* Unlike model canonicalization, this preserves operand order and literal
|
|
5
|
+
* representation because local evaluation can fail before later operands run.
|
|
6
|
+
* The string-ID table must share the evaluation cache's report lifetime.
|
|
7
|
+
*/
|
|
8
|
+
export const expressionKey = (expression: Expression, strings: Map<string, number>): string => {
|
|
9
|
+
const intern = (value: string): number => {
|
|
10
|
+
let id = strings.get(value)
|
|
11
|
+
if (id === undefined) {
|
|
12
|
+
id = strings.size
|
|
13
|
+
strings.set(value, id)
|
|
14
|
+
}
|
|
15
|
+
return id
|
|
16
|
+
}
|
|
17
|
+
const parts: string[] = []
|
|
18
|
+
const pending = [expression]
|
|
19
|
+
while (pending.length > 0) {
|
|
20
|
+
const node = pending.pop()!
|
|
21
|
+
switch (node.kind) {
|
|
22
|
+
case 'literal': {
|
|
23
|
+
const value = node.sort === 'enum' ? intern(node.value) : node.sort === 'real' && typeof node.value === 'object'
|
|
24
|
+
? [node.value.numerator, node.value.denominator] : node.value
|
|
25
|
+
parts.push(JSON.stringify([node.kind, node.sort, node.sort === 'enum' ? intern(node.domain) : null, value]))
|
|
26
|
+
break
|
|
27
|
+
}
|
|
28
|
+
case 'variable': {
|
|
29
|
+
parts.push(JSON.stringify([node.kind, intern(node.id)]))
|
|
30
|
+
break
|
|
31
|
+
}
|
|
32
|
+
case 'not': case 'negate':
|
|
33
|
+
parts.push(node.kind); pending.push(node.value); break
|
|
34
|
+
case 'and': case 'or': case 'add': case 'multiply':
|
|
35
|
+
parts.push(JSON.stringify([node.kind, node.operands.length]))
|
|
36
|
+
for (let index = node.operands.length - 1; index >= 0; index--) pending.push(node.operands[index]!)
|
|
37
|
+
break
|
|
38
|
+
case 'if':
|
|
39
|
+
parts.push(node.kind); pending.push(node.else, node.then, node.condition); break
|
|
40
|
+
default:
|
|
41
|
+
parts.push(node.kind); pending.push(node.right, node.left)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return parts.join('\n')
|
|
45
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { gcd } from './integer-gcd.ts'
|
|
2
|
+
/** Cancel across factors before allocating a rational product's intermediates. */
|
|
3
|
+
export const product = (a: bigint, b: bigint, c: bigint, d: bigint): readonly [bigint, bigint] => {
|
|
4
|
+
// Leave zero-denominator diagnostics to the caller's existing normalizer.
|
|
5
|
+
if (b === 0n || d === 0n) return [0n, 0n]
|
|
6
|
+
if (a === 0n || c === 0n) return [0n, 1n]
|
|
7
|
+
const left = gcd(a, d)
|
|
8
|
+
const right = gcd(c, b)
|
|
9
|
+
return [(a / left) * (c / right), (b / right) * (d / left)]
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const probeThreshold = 1n << 1024n
|
|
2
|
+
|
|
3
|
+
/** Avoid repeated large denominator factors when a short Euclidean probe finds them. */
|
|
4
|
+
export const sum = (a: bigint, b: bigint, c: bigint, d: bigint): readonly [bigint, bigint] => {
|
|
5
|
+
if (b === d) return [a + c, b]
|
|
6
|
+
let x = b < 0n ? -b : b
|
|
7
|
+
let y = d < 0n ? -d : d
|
|
8
|
+
if (x >= probeThreshold && y >= probeThreshold) {
|
|
9
|
+
if (x < y) [x, y] = [y, x]
|
|
10
|
+
// A full GCD can cost more than the products it avoids for coprime inputs.
|
|
11
|
+
// An unfinished probe leaves the ordinary exact addition unchanged.
|
|
12
|
+
for (let step = 0; step < 6 && y !== 0n; step++) [x, y] = [y, x % y]
|
|
13
|
+
if (y === 0n && x > 1n) {
|
|
14
|
+
const leftScale = d / x
|
|
15
|
+
const rightScale = b / x
|
|
16
|
+
return [a * leftScale + c * rightScale, b * leftScale]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [a * d + c * b, b * d]
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const batchThreshold = 1n << 1024n
|
|
2
|
+
|
|
3
|
+
/** Exact nonnegative GCD, with Lehmer batches for large operands. */
|
|
4
|
+
export const gcd = (left: bigint, right: bigint): bigint => {
|
|
5
|
+
let a = left < 0n ? -left : left
|
|
6
|
+
let b = right < 0n ? -right : right
|
|
7
|
+
if (a < b) [a, b] = [b, a]
|
|
8
|
+
while (b !== 0n) {
|
|
9
|
+
if (a === b) return a
|
|
10
|
+
if (b < batchThreshold) {
|
|
11
|
+
while (b !== 0n) {
|
|
12
|
+
const remainder = a % b
|
|
13
|
+
a = b
|
|
14
|
+
b = remainder
|
|
15
|
+
}
|
|
16
|
+
return a
|
|
17
|
+
}
|
|
18
|
+
{
|
|
19
|
+
// Hex length gives a cheap common shift retaining at most 64 leading bits.
|
|
20
|
+
const shift = BigInt(Math.max(0, a.toString(16).length * 4 - 64))
|
|
21
|
+
let x = a >> shift
|
|
22
|
+
let y = b >> shift
|
|
23
|
+
let A = 1n, B = 0n, C = 0n, D = 1n
|
|
24
|
+
for (let step = 0; step < 64 && x >= y && y > 0n && y + C > 0n && y + D > 0n; step++) {
|
|
25
|
+
const q = (x + A) / (y + C)
|
|
26
|
+
if (q <= 0n || q !== (x + B) / (y + D)) break
|
|
27
|
+
;[A, B, C, D] = [C, D, A - q * C, B - q * D]
|
|
28
|
+
;[x, y] = [y, x - q * y]
|
|
29
|
+
}
|
|
30
|
+
if (B !== 0n) {
|
|
31
|
+
const nextA = A * a + B * b
|
|
32
|
+
const nextB = C * a + D * b
|
|
33
|
+
// Each matrix step has determinant -1, preserving the GCD exactly.
|
|
34
|
+
// Check full-width progress before accepting the approximate batch.
|
|
35
|
+
if (nextA >= nextB && nextB >= 0n && nextA < a && nextB < b) {
|
|
36
|
+
a = nextA
|
|
37
|
+
b = nextB
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const remainder = a % b
|
|
43
|
+
a = b
|
|
44
|
+
b = remainder
|
|
45
|
+
}
|
|
46
|
+
return a
|
|
47
|
+
}
|
package/src/linear.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { evaluate } from './evaluate-expression.ts'
|
|
2
|
+
import { constantSign, type Sign } from './constant-sign.ts'
|
|
3
|
+
import { balancedReduce } from './balanced-reduce.ts'
|
|
4
|
+
import { sum } from './fraction-sum.ts'
|
|
5
|
+
import { product } from './fraction-product.ts'
|
|
6
|
+
import { clone } from './clone.ts'
|
|
1
7
|
import type { Expression, Model } from './model.ts'
|
|
2
|
-
import {
|
|
8
|
+
import type { Limits } from './adapter.ts'
|
|
9
|
+
import { mergeLimits, model as validate } from './validate.ts'
|
|
10
|
+
import * as Exact from './exact.ts'
|
|
3
11
|
|
|
4
12
|
export type Analysis = {
|
|
5
13
|
readonly linear: boolean
|
|
@@ -8,48 +16,106 @@ export type Analysis = {
|
|
|
8
16
|
|
|
9
17
|
type Affine = { readonly linear: boolean, readonly constant: boolean }
|
|
10
18
|
|
|
11
|
-
const
|
|
19
|
+
const combine = (left: Exact.Normalized, right: Exact.Normalized, kind: 'add' | 'subtract' | 'multiply' | 'divide'): Exact.Normalized => {
|
|
20
|
+
const a = BigInt(left.numerator)
|
|
21
|
+
const b = BigInt(left.denominator)
|
|
22
|
+
const c = BigInt(right.numerator)
|
|
23
|
+
const d = BigInt(right.denominator)
|
|
24
|
+
const [numerator, denominator] = kind === 'add' ? sum(a, b, c, d)
|
|
25
|
+
: kind === 'subtract' ? sum(a, b, -c, d)
|
|
26
|
+
: kind === 'multiply' ? product(a, b, c, d) : product(a, b, d, c)
|
|
27
|
+
return Exact.fromBigInts(numerator, denominator)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Called only for numeric constant expressions already classified as affine. */
|
|
31
|
+
function* constantSteps(expression: Expression): Generator<Expression, Exact.Normalized, Exact.Normalized> {
|
|
32
|
+
switch (expression.kind) {
|
|
33
|
+
case 'literal':
|
|
34
|
+
if (expression.sort === 'int') return Exact.rational(String(Exact.integer(expression.value)))
|
|
35
|
+
if (expression.sort === 'real') return Exact.rational(expression.value)
|
|
36
|
+
break
|
|
37
|
+
case 'negate': return combine(Exact.rational('0'), (yield expression.value), 'subtract')
|
|
38
|
+
case 'add':
|
|
39
|
+
case 'multiply': {
|
|
40
|
+
const values: Exact.Normalized[] = []
|
|
41
|
+
for (const value of expression.operands) values.push(yield value)
|
|
42
|
+
if (expression.kind === 'add') return balancedReduce(values, (left, right) => combine(left, right, 'add'))
|
|
43
|
+
let numerator = 1n, denominator = 1n
|
|
44
|
+
for (const value of values) {
|
|
45
|
+
[numerator, denominator] = product(numerator, denominator, BigInt(value.numerator), BigInt(value.denominator))
|
|
46
|
+
}
|
|
47
|
+
return Exact.fromBigInts(numerator, denominator)
|
|
48
|
+
}
|
|
49
|
+
case 'subtract':
|
|
50
|
+
case 'divide': return combine((yield expression.left), (yield expression.right), expression.kind)
|
|
51
|
+
}
|
|
52
|
+
throw new TypeError('expected an affine numeric constant')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function* affineSteps(expression: Expression, constants: WeakMap<Expression, Exact.Normalized>, signs: WeakMap<Expression, Sign>): Generator<Expression, Affine, Affine> {
|
|
12
56
|
switch (expression.kind) {
|
|
13
57
|
case 'literal': return { linear: expression.sort === 'int' || expression.sort === 'real', constant: true }
|
|
14
58
|
case 'variable': return { linear: true, constant: false }
|
|
15
|
-
case 'negate': return
|
|
59
|
+
case 'negate': return (yield expression.value)
|
|
16
60
|
case 'add': {
|
|
17
|
-
|
|
18
|
-
|
|
61
|
+
let linear = true, constant = true
|
|
62
|
+
for (const value of expression.operands) {
|
|
63
|
+
const part = yield value
|
|
64
|
+
linear = linear && part.linear
|
|
65
|
+
constant = constant && part.constant
|
|
66
|
+
}
|
|
67
|
+
return { linear, constant }
|
|
19
68
|
}
|
|
20
69
|
case 'subtract': {
|
|
21
|
-
const left =
|
|
22
|
-
const right =
|
|
70
|
+
const left = (yield expression.left)
|
|
71
|
+
const right = (yield expression.right)
|
|
23
72
|
return { linear: left.linear && right.linear, constant: left.constant && right.constant }
|
|
24
73
|
}
|
|
25
74
|
case 'multiply': {
|
|
26
|
-
|
|
27
|
-
|
|
75
|
+
let linear = true, variableParts = 0
|
|
76
|
+
for (const value of expression.operands) {
|
|
77
|
+
const part = yield value
|
|
78
|
+
linear = linear && part.linear
|
|
79
|
+
if (!part.constant) variableParts++
|
|
80
|
+
}
|
|
81
|
+
return { linear: linear && variableParts <= 1, constant: variableParts === 0 }
|
|
28
82
|
}
|
|
29
83
|
case 'divide': {
|
|
30
|
-
const left =
|
|
31
|
-
const right =
|
|
32
|
-
|
|
84
|
+
const left = (yield expression.left)
|
|
85
|
+
const right = (yield expression.right)
|
|
86
|
+
let linear = false
|
|
87
|
+
if (left.linear && right.linear && right.constant) {
|
|
88
|
+
const sign = constantSign(expression.right, signs, value => evaluate(value, constantSteps, constants))
|
|
89
|
+
linear = sign === undefined ? !Exact.isZero(evaluate(expression.right, constantSteps, constants)) : sign !== 0
|
|
90
|
+
}
|
|
91
|
+
return { linear, constant: left.constant && right.constant }
|
|
33
92
|
}
|
|
34
93
|
default: return { linear: false, constant: false }
|
|
35
94
|
}
|
|
36
95
|
}
|
|
37
96
|
|
|
38
|
-
const constraintIsLinear = (expression: Expression): boolean => {
|
|
97
|
+
const constraintIsLinear = (expression: Expression, affine: (value: Expression) => Affine): boolean => {
|
|
39
98
|
if (expression.kind !== 'eq' && expression.kind !== 'lte' && expression.kind !== 'gte') return false
|
|
40
99
|
return affine(expression.left).linear && affine(expression.right).linear
|
|
41
100
|
}
|
|
42
101
|
|
|
43
102
|
/** Recognizes the portable LP/MIP subset without consulting a backend AST. */
|
|
44
|
-
export const model = (input: Model): Analysis => {
|
|
45
|
-
|
|
103
|
+
export const model = (input: Model, limits: Partial<Limits> = {}): Analysis => {
|
|
104
|
+
limits = mergeLimits(limits)
|
|
105
|
+
validate(input, limits)
|
|
106
|
+
input = clone(input)
|
|
107
|
+
validate(input, limits)
|
|
46
108
|
const problems: string[] = []
|
|
109
|
+
const constants = new WeakMap<Expression, Exact.Normalized>()
|
|
110
|
+
const classifications = new WeakMap<Expression, Affine>()
|
|
111
|
+
const signs = new WeakMap<Expression, Sign>()
|
|
112
|
+
const affine = (value: Expression): Affine => evaluate(value, expression => affineSteps(expression, constants, signs), classifications)
|
|
47
113
|
if (input.variables.some(variable => variable.sort === 'bool' || variable.sort === 'enum')) {
|
|
48
114
|
problems.push('Boolean and enum variables are outside the linear subset')
|
|
49
115
|
}
|
|
50
116
|
if ((input.softConstraints?.length ?? 0) > 0) problems.push('soft constraints are outside the linear subset')
|
|
51
117
|
input.constraints.forEach(constraint => {
|
|
52
|
-
if (!constraintIsLinear(constraint.expression)) problems.push(`constraint ${constraint.id} is not a non-strict linear comparison`)
|
|
118
|
+
if (!constraintIsLinear(constraint.expression, affine)) problems.push(`constraint ${constraint.id} is not a non-strict linear comparison`)
|
|
53
119
|
})
|
|
54
120
|
;(input.objectives ?? []).forEach(objective => {
|
|
55
121
|
if (!affine(objective.expression).linear) problems.push(`objective ${objective.id} is not linear`)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Estimate pre-reduction decimal digits without constructing a BigInt. */
|
|
2
|
+
const integerDigits = (value: unknown): number => {
|
|
3
|
+
if (typeof value === 'number') return Number.isSafeInteger(value) ? String(Math.abs(value)).length : 0
|
|
4
|
+
if (typeof value !== 'string' || !/^[+-]?\d+$/.test(value)) return 0
|
|
5
|
+
return value.length - (/^[+-]/.test(value) ? 1 : 0)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const numericDigits = (value: unknown, sort: 'int' | 'real'): number => {
|
|
9
|
+
if (sort === 'int') return integerDigits(value)
|
|
10
|
+
if (typeof value !== 'string') {
|
|
11
|
+
if (value === null || typeof value !== 'object') return 0
|
|
12
|
+
const pair = value as { numerator?: unknown, denominator?: unknown }
|
|
13
|
+
return integerDigits(pair.numerator) + integerDigits(pair.denominator)
|
|
14
|
+
}
|
|
15
|
+
const match = /^([+-]?)(\d+(?:\.\d*)?|\.\d+)(?:[eE]([+-]?\d+))?$/.exec(value)
|
|
16
|
+
if (match === null) return 0
|
|
17
|
+
const coefficient = match[2]!
|
|
18
|
+
const digits = coefficient.replace('.', '')
|
|
19
|
+
const exponent = match[3] === undefined ? 0 : Number(match[3])
|
|
20
|
+
if (!Number.isSafeInteger(exponent)) return 0 // Exact reports invalid exponent syntax.
|
|
21
|
+
// Zero never constructs a power of ten, but reading its coefficient still counts.
|
|
22
|
+
if (!/[1-9]/.test(digits)) return digits.length + 1
|
|
23
|
+
const dot = coefficient.indexOf('.')
|
|
24
|
+
const scale = (dot === -1 ? 0 : coefficient.length - dot - 1) - exponent
|
|
25
|
+
return digits.length + Math.max(0, -scale) + (scale > 0 ? scale + 1 : 1)
|
|
26
|
+
}
|
package/src/options.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { diagnosticText } from './diagnostic-text.ts'
|
|
2
|
+
import { mergeLimits } from './validate.ts'
|
|
3
|
+
import type { Options } from './adapter.ts'
|
|
4
|
+
|
|
5
|
+
/** Validate option names and capture supplied fields before applying defaults. */
|
|
6
|
+
export const validateOptions = (options: Options): Options => {
|
|
7
|
+
if (options === null || typeof options !== 'object' || Array.isArray(options)) {
|
|
8
|
+
throw new TypeError('solver options must be an object')
|
|
9
|
+
}
|
|
10
|
+
for (const key of Object.keys(options)) {
|
|
11
|
+
if (key !== 'limits' && key !== 'unsatCore') throw new TypeError(`unknown solver option ${diagnosticText(key)}`)
|
|
12
|
+
}
|
|
13
|
+
const unsatCore = options.unsatCore
|
|
14
|
+
if (unsatCore !== undefined && typeof unsatCore !== 'boolean') {
|
|
15
|
+
throw new TypeError('solver option unsatCore must be a Boolean')
|
|
16
|
+
}
|
|
17
|
+
return { unsatCore, limits: mergeLimits(options.limits) }
|
|
18
|
+
}
|
package/src/result.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Result } from './adapter.ts'
|
|
2
|
+
import { clone } from './clone.ts'
|
|
3
|
+
import { resultMetadata, unknownReason } from './validate.ts'
|
|
4
|
+
|
|
5
|
+
/** Capture the reported outcome before checking the adapter's proof contract. */
|
|
6
|
+
export const captureResult = (input: Result): Result => {
|
|
7
|
+
const result = clone(input)
|
|
8
|
+
if (result === null || typeof result !== 'object' || Array.isArray(result)) {
|
|
9
|
+
throw new TypeError('solver result must be an object')
|
|
10
|
+
}
|
|
11
|
+
switch (result.status) {
|
|
12
|
+
case 'optimal':
|
|
13
|
+
if (result.optimalityProved !== true) throw new TypeError('optimal solver result requires optimalityProved: true')
|
|
14
|
+
break
|
|
15
|
+
case 'unsatisfied':
|
|
16
|
+
if (result.infeasibilityProved !== true) throw new TypeError('unsatisfied solver result requires infeasibilityProved: true')
|
|
17
|
+
break
|
|
18
|
+
case 'satisfied':
|
|
19
|
+
break
|
|
20
|
+
case 'unknown':
|
|
21
|
+
unknownReason(result.reason)
|
|
22
|
+
break
|
|
23
|
+
default:
|
|
24
|
+
throw new TypeError('solver result status must be satisfied, optimal, unsatisfied or unknown')
|
|
25
|
+
}
|
|
26
|
+
resultMetadata(result)
|
|
27
|
+
return result
|
|
28
|
+
}
|
package/src/solve.ts
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
|
+
import { captureResult } from './result.ts'
|
|
2
|
+
import { validateContext } from './context.ts'
|
|
3
|
+
import { clone } from './clone.ts'
|
|
1
4
|
import type { Options, Result, SolverAdapter } from './adapter.ts'
|
|
2
5
|
import * as Capability from './capability.ts'
|
|
6
|
+
import * as Canonical from './canonical.ts'
|
|
3
7
|
import * as Explain from './explain.ts'
|
|
4
8
|
import type { Model } from './model.ts'
|
|
5
9
|
import * as Validate from './validate.ts'
|
|
10
|
+
import { validateOptions } from './options.ts'
|
|
11
|
+
|
|
12
|
+
const freeze = <T>(value: T): T => {
|
|
13
|
+
const pending: unknown[] = [value]
|
|
14
|
+
const seen = new Set<object>()
|
|
15
|
+
while (pending.length > 0) {
|
|
16
|
+
const next = pending.pop()
|
|
17
|
+
if (next === null || typeof next !== 'object' || seen.has(next)) continue
|
|
18
|
+
seen.add(next)
|
|
19
|
+
for (const child of Object.values(next)) pending.push(child)
|
|
20
|
+
Object.freeze(next)
|
|
21
|
+
}
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
6
24
|
|
|
7
25
|
const prepared = (adapter: SolverAdapter, model: Model, options: Options): {
|
|
8
26
|
readonly limits: ReturnType<typeof Validate.mergeLimits>
|
|
27
|
+
readonly model: Model
|
|
9
28
|
readonly run: () => Promise<Result>
|
|
10
29
|
} => {
|
|
11
|
-
|
|
30
|
+
options = validateOptions(options)
|
|
31
|
+
const limits = Object.freeze(Validate.mergeLimits(options.limits))
|
|
32
|
+
model = clone(model)
|
|
12
33
|
Validate.model(model, limits)
|
|
34
|
+
model = freeze(model)
|
|
13
35
|
const unsatCore = options.unsatCore ?? false
|
|
14
36
|
const missing = Capability.missing(adapter, model, unsatCore)
|
|
15
37
|
if (missing.length > 0) throw new Capability.UnsupportedModelError(adapter.backend.name, missing)
|
|
16
|
-
return { limits, run: () => adapter.solve(model, { limits, unsatCore }) }
|
|
38
|
+
return { model, limits, run: async () => captureResult(await adapter.solve(model, { limits, unsatCore })) }
|
|
17
39
|
}
|
|
18
40
|
|
|
19
41
|
/** Validates and negotiates a model before crossing the adapter boundary. */
|
|
@@ -29,5 +51,14 @@ export const runWithExplanation = async (
|
|
|
29
51
|
context: Explain.Context = {}
|
|
30
52
|
): Promise<Explain.Report> => {
|
|
31
53
|
const solve = prepared(adapter, model, options)
|
|
32
|
-
|
|
54
|
+
validateContext(context)
|
|
55
|
+
const snapshot = clone(context)
|
|
56
|
+
validateContext(snapshot)
|
|
57
|
+
if (snapshot.modelDigest !== undefined) {
|
|
58
|
+
const digest = Canonical.digest(solve.model, solve.limits)
|
|
59
|
+
if (snapshot.modelDigest !== digest) {
|
|
60
|
+
throw new TypeError(`scenario model digest ${snapshot.modelDigest} does not match ${digest}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return Explain.report(solve.model, await solve.run(), solve.limits, snapshot)
|
|
33
64
|
}
|