@cavelang/solver 0.35.0 → 0.36.1
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
package/src/workflow.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { validateContext } from './context.ts'
|
|
2
|
+
import { clone } from './clone.ts'
|
|
3
|
+
import { numericDigits } from './numeric-size.ts'
|
|
4
|
+
import { defaultLimits } from './adapter.ts'
|
|
1
5
|
import type { Limits, Options, Result, SolverAdapter } from './adapter.ts'
|
|
2
6
|
import * as Canonical from './canonical.ts'
|
|
7
|
+
import * as Capability from './capability.ts'
|
|
3
8
|
import * as Exact from './exact.ts'
|
|
4
9
|
import * as Explain from './explain.ts'
|
|
5
10
|
import type {
|
|
@@ -13,6 +18,7 @@ import type {
|
|
|
13
18
|
} from './model.ts'
|
|
14
19
|
import * as Solve from './solve.ts'
|
|
15
20
|
import * as Validate from './validate.ts'
|
|
21
|
+
import { validateOptions } from './options.ts'
|
|
16
22
|
|
|
17
23
|
export const schema = 'cave.solver/workflow@1' as const
|
|
18
24
|
|
|
@@ -158,11 +164,11 @@ const boundedScope = (model: Model, limits: Partial<Limits> = {}): Scope => {
|
|
|
158
164
|
}
|
|
159
165
|
})
|
|
160
166
|
.sort((left, right) => compareText(left.variableId, right.variableId))
|
|
161
|
-
const
|
|
167
|
+
const required = Capability.required(model)
|
|
162
168
|
const theories: Scope['theories'][number][] = ['booleans']
|
|
163
|
-
if (
|
|
164
|
-
if (
|
|
165
|
-
if (
|
|
169
|
+
if (required.has('integers')) theories.push('bounded-integers')
|
|
170
|
+
if (required.has('rationals')) theories.push('exact-rationals')
|
|
171
|
+
if (required.has('finite-enums')) theories.push('finite-enums')
|
|
166
172
|
return {
|
|
167
173
|
domains,
|
|
168
174
|
assumptions: model.constraints.map(constraint => constraint.id),
|
|
@@ -174,13 +180,20 @@ const variableMap = (model: Model): ReadonlyMap<string, Variable> =>
|
|
|
174
180
|
new Map(model.variables.map(variable => [variable.id, variable]))
|
|
175
181
|
|
|
176
182
|
const normalizedValue = (variable: Variable, value: Value, domains: ReadonlyMap<string, readonly string[]>): Value => {
|
|
183
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
184
|
+
throw new WorkflowValidationError(`value for variable ${JSON.stringify(variable.id)} must be an object`)
|
|
185
|
+
}
|
|
177
186
|
if (variable.sort !== value.sort) {
|
|
178
187
|
throw new WorkflowValidationError(
|
|
179
188
|
`variable ${JSON.stringify(variable.id)} is ${variable.sort}, received ${value.sort}`
|
|
180
189
|
)
|
|
181
190
|
}
|
|
182
191
|
switch (value.sort) {
|
|
183
|
-
case 'bool':
|
|
192
|
+
case 'bool':
|
|
193
|
+
if (typeof value.value !== 'boolean') {
|
|
194
|
+
throw new WorkflowValidationError(`value for variable ${JSON.stringify(variable.id)} must be a boolean`)
|
|
195
|
+
}
|
|
196
|
+
return { sort: value.sort, value: value.value }
|
|
184
197
|
case 'int': {
|
|
185
198
|
const normalized = String(Exact.integer(value.value))
|
|
186
199
|
if (Exact.compare(normalized, String(Exact.integer((variable as Variable & { sort: 'int' }).min))) < 0 ||
|
|
@@ -208,7 +221,7 @@ const normalizedValue = (variable: Variable, value: Value, domains: ReadonlyMap<
|
|
|
208
221
|
if (!(domains.get(value.domain) ?? []).includes(value.value)) {
|
|
209
222
|
throw new WorkflowValidationError(`value ${JSON.stringify(value.value)} is outside enum ${JSON.stringify(value.domain)}`)
|
|
210
223
|
}
|
|
211
|
-
return value
|
|
224
|
+
return { sort: value.sort, domain: value.domain, value: value.value }
|
|
212
225
|
}
|
|
213
226
|
}
|
|
214
227
|
}
|
|
@@ -353,7 +366,14 @@ const run = async (
|
|
|
353
366
|
options: Options,
|
|
354
367
|
context: Explain.Context
|
|
355
368
|
): Promise<{ readonly explanation: Explain.Report, readonly tieBreak: readonly TieBreak[] }> => {
|
|
369
|
+
validateContext(context)
|
|
356
370
|
const limits = Validate.mergeLimits(options.limits)
|
|
371
|
+
if (context.modelDigest !== undefined) {
|
|
372
|
+
const digest = Canonical.digest(sourceModel, limits)
|
|
373
|
+
if (context.modelDigest !== digest) {
|
|
374
|
+
throw new TypeError(`scenario model digest ${context.modelDigest} does not match ${digest}`)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
357
377
|
const prepared = preparedModel(solveModel, kind)
|
|
358
378
|
const result = await Solve.run(adapter, prepared.model, {
|
|
359
379
|
limits,
|
|
@@ -369,7 +389,6 @@ const run = async (
|
|
|
369
389
|
}
|
|
370
390
|
|
|
371
391
|
const report = (
|
|
372
|
-
model: Model,
|
|
373
392
|
operation: Operation,
|
|
374
393
|
scope: Scope,
|
|
375
394
|
tie: readonly TieBreak[],
|
|
@@ -377,7 +396,7 @@ const report = (
|
|
|
377
396
|
): Report => ({
|
|
378
397
|
schema,
|
|
379
398
|
operation,
|
|
380
|
-
modelDigest:
|
|
399
|
+
modelDigest: explanation.run.modelDigest,
|
|
381
400
|
scope,
|
|
382
401
|
tieBreak: tie,
|
|
383
402
|
explanation
|
|
@@ -389,9 +408,12 @@ export const feasibility = async (
|
|
|
389
408
|
options: Options = {},
|
|
390
409
|
context: Explain.Context = {}
|
|
391
410
|
): Promise<Report> => {
|
|
411
|
+
options = validateOptions(options)
|
|
412
|
+
validateContext(context)
|
|
413
|
+
;({ model, options, context } = clone({ model, options, context }))
|
|
392
414
|
const scope = boundedScope(model, options.limits)
|
|
393
415
|
const result = await run(adapter, model, model, 'feasibility', options, context)
|
|
394
|
-
return report(
|
|
416
|
+
return report({ kind: 'feasibility' }, scope, result.tieBreak, result.explanation)
|
|
395
417
|
}
|
|
396
418
|
|
|
397
419
|
export const optimization = async (
|
|
@@ -400,9 +422,12 @@ export const optimization = async (
|
|
|
400
422
|
options: Options = {},
|
|
401
423
|
context: Explain.Context = {}
|
|
402
424
|
): Promise<Report> => {
|
|
425
|
+
options = validateOptions(options)
|
|
426
|
+
validateContext(context)
|
|
427
|
+
;({ model, options, context } = clone({ model, options, context }))
|
|
403
428
|
const scope = boundedScope(model, options.limits)
|
|
404
429
|
const result = await run(adapter, model, model, 'optimization', options, context)
|
|
405
|
-
return report(
|
|
430
|
+
return report({ kind: 'optimization' }, scope, result.tieBreak, result.explanation)
|
|
406
431
|
}
|
|
407
432
|
|
|
408
433
|
export const counterexample = async (
|
|
@@ -412,6 +437,9 @@ export const counterexample = async (
|
|
|
412
437
|
options: Options = {},
|
|
413
438
|
context: Explain.Context = {}
|
|
414
439
|
): Promise<Report> => {
|
|
440
|
+
options = validateOptions(options)
|
|
441
|
+
validateContext(context)
|
|
442
|
+
;({ model, options, context } = clone({ model, options, context }))
|
|
415
443
|
const baseScope = boundedScope(model, options.limits)
|
|
416
444
|
const invariant = model.constraints.find(constraint => constraint.id === invariantId)
|
|
417
445
|
if (invariant === undefined) {
|
|
@@ -432,7 +460,7 @@ export const counterexample = async (
|
|
|
432
460
|
...baseScope,
|
|
433
461
|
assumptions: baseScope.assumptions.filter(id => id !== invariantId)
|
|
434
462
|
}
|
|
435
|
-
return report(
|
|
463
|
+
return report({ kind: 'counterexample', invariantId, verdict }, scope, result.tieBreak, result.explanation)
|
|
436
464
|
}
|
|
437
465
|
|
|
438
466
|
const bindingConstraint = (binding: Binding, id: string): HardConstraint => ({
|
|
@@ -446,10 +474,14 @@ const bindingConstraint = (binding: Binding, id: string): HardConstraint => ({
|
|
|
446
474
|
})
|
|
447
475
|
|
|
448
476
|
const normalizeBindings = (model: Model, bindings: readonly Binding[]): readonly Binding[] => {
|
|
477
|
+
if (!Array.isArray(bindings)) throw new WorkflowValidationError('fixed bindings must be an array')
|
|
449
478
|
const variables = variableMap(model)
|
|
450
479
|
const domains = new Map((model.enums ?? []).map(domain => [domain.id, domain.values]))
|
|
451
480
|
const seen = new Set<string>()
|
|
452
|
-
return
|
|
481
|
+
return Array.from(bindings, (binding, index) => {
|
|
482
|
+
if (binding === null || typeof binding !== 'object' || Array.isArray(binding)) {
|
|
483
|
+
throw new WorkflowValidationError(`fixed binding at index ${index} must be an object`)
|
|
484
|
+
}
|
|
453
485
|
if (seen.has(binding.variableId)) {
|
|
454
486
|
throw new WorkflowValidationError(`variable ${JSON.stringify(binding.variableId)} is fixed more than once`)
|
|
455
487
|
}
|
|
@@ -517,20 +549,49 @@ export const sensitivity = async (
|
|
|
517
549
|
options: Options = {},
|
|
518
550
|
context: Explain.Context = {}
|
|
519
551
|
): Promise<SensitivityReport> => {
|
|
552
|
+
options = validateOptions(options)
|
|
553
|
+
validateContext(context)
|
|
554
|
+
;({ model, options, context, request } = clone({ model, options, context, request }))
|
|
520
555
|
const scope = boundedScope(model, options.limits)
|
|
556
|
+
if (request === null || typeof request !== 'object' || Array.isArray(request)) {
|
|
557
|
+
throw new WorkflowValidationError('sensitivity request must be an object')
|
|
558
|
+
}
|
|
559
|
+
if (!Array.isArray(request.samples)) {
|
|
560
|
+
throw new WorkflowValidationError('sensitivity samples must be an array')
|
|
561
|
+
}
|
|
562
|
+
for (const field of ['fixed', 'observe'] as const) {
|
|
563
|
+
if (request[field] !== undefined && !Array.isArray(request[field])) {
|
|
564
|
+
throw new WorkflowValidationError(`sensitivity ${field} must be an array`)
|
|
565
|
+
}
|
|
566
|
+
}
|
|
521
567
|
const maxRuns = request.maxRuns ?? defaultMaxSensitivityRuns
|
|
522
568
|
if (!Number.isSafeInteger(maxRuns) || maxRuns <= 0) {
|
|
523
|
-
|
|
569
|
+
const received = typeof maxRuns === 'object' || typeof maxRuns === 'function' ? typeof maxRuns : String(maxRuns)
|
|
570
|
+
throw new WorkflowValidationError(`maxRuns must be a positive safe integer, received ${received}`)
|
|
524
571
|
}
|
|
525
572
|
if (request.samples.length === 0) throw new WorkflowValidationError('sensitivity needs at least one sample')
|
|
526
573
|
if (request.samples.length > maxRuns) {
|
|
527
574
|
throw new WorkflowValidationError(`sensitivity samples exceed maxRuns: ${request.samples.length} > ${maxRuns}`)
|
|
528
575
|
}
|
|
576
|
+
const maximumDigits = options.limits?.maxNumericDigits ?? defaultLimits.maxNumericDigits
|
|
577
|
+
let requestDigits = 0
|
|
578
|
+
const charge = (value: Value): void => {
|
|
579
|
+
if (value === null || typeof value !== 'object') return // Shape validation below supplies context.
|
|
580
|
+
if (value.sort === 'int') requestDigits += numericDigits(value.value, 'int')
|
|
581
|
+
else if (value.sort === 'real') requestDigits += numericDigits(value, 'real')
|
|
582
|
+
if (requestDigits > maximumDigits) {
|
|
583
|
+
throw new WorkflowValidationError(`sensitivity request exceeds maxNumericDigits: ${requestDigits} > ${maximumDigits}`)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
for (const value of request.samples) charge(value)
|
|
587
|
+
for (const binding of request.fixed ?? []) {
|
|
588
|
+
if (binding !== null && typeof binding === 'object') charge(binding.value)
|
|
589
|
+
}
|
|
529
590
|
const variables = variableMap(model)
|
|
530
591
|
const selected = variables.get(request.variableId)
|
|
531
592
|
if (selected === undefined) throw new WorkflowValidationError(`unknown sensitivity variable ${JSON.stringify(request.variableId)}`)
|
|
532
593
|
const domains = new Map((model.enums ?? []).map(domain => [domain.id, domain.values]))
|
|
533
|
-
const samples = request.samples
|
|
594
|
+
const samples = Array.from(request.samples, value => normalizedValue(selected, value, domains))
|
|
534
595
|
if (new Set(samples.map(value => JSON.stringify(value))).size !== samples.length) {
|
|
535
596
|
throw new WorkflowValidationError('sensitivity samples contain duplicate values')
|
|
536
597
|
}
|
|
@@ -543,31 +604,36 @@ export const sensitivity = async (
|
|
|
543
604
|
if (!variables.has(id)) throw new WorkflowValidationError(`unknown observed variable ${JSON.stringify(id)}`)
|
|
544
605
|
}
|
|
545
606
|
const kind = request.operation ?? 'optimization'
|
|
607
|
+
if (request.operation !== undefined && request.operation !== 'feasibility' && request.operation !== 'optimization') {
|
|
608
|
+
throw new WorkflowValidationError('sensitivity operation must be feasibility or optimization')
|
|
609
|
+
}
|
|
546
610
|
const used = new Set(model.constraints.map(constraint => constraint.id))
|
|
547
611
|
const fixedConstraints = fixed.map((binding, index) =>
|
|
548
612
|
bindingConstraint(binding, freshId(used, `fixed/${index}`)))
|
|
613
|
+
const sampleConstraints = samples.map((value, index) =>
|
|
614
|
+
bindingConstraint({ variableId: request.variableId, value }, freshId(used, `sample/${index}`)))
|
|
615
|
+
const modelForSample = (constraint: HardConstraint): Model => ({
|
|
616
|
+
...model,
|
|
617
|
+
constraints: [...model.constraints, ...fixedConstraints, constraint]
|
|
618
|
+
})
|
|
619
|
+
// Validate the complete batch before any backend call without retaining a
|
|
620
|
+
// separate copy of all model constraints for every sample.
|
|
621
|
+
for (const constraint of sampleConstraints) {
|
|
622
|
+
Validate.model(preparedModel(modelForSample(constraint), kind).model, options.limits)
|
|
623
|
+
}
|
|
549
624
|
const points: SensitivityPoint[] = []
|
|
550
625
|
for (const [index, value] of samples.entries()) {
|
|
551
|
-
const
|
|
552
|
-
const solveModel: Model = {
|
|
553
|
-
...model,
|
|
554
|
-
constraints: [
|
|
555
|
-
...model.constraints,
|
|
556
|
-
...fixedConstraints,
|
|
557
|
-
bindingConstraint(sample, freshId(used, `sample/${index}`))
|
|
558
|
-
]
|
|
559
|
-
}
|
|
560
|
-
const solved = await run(adapter, model, solveModel, kind, options, context)
|
|
626
|
+
const solved = await run(adapter, model, modelForSample(sampleConstraints[index]!), kind, options, context)
|
|
561
627
|
points.push({
|
|
562
628
|
index,
|
|
563
629
|
value,
|
|
564
|
-
report: report(
|
|
630
|
+
report: report({ kind }, scope, solved.tieBreak, solved.explanation)
|
|
565
631
|
})
|
|
566
632
|
}
|
|
567
633
|
return {
|
|
568
634
|
schema,
|
|
569
635
|
operation: 'sensitivity',
|
|
570
|
-
modelDigest:
|
|
636
|
+
modelDigest: points[0]!.report.modelDigest,
|
|
571
637
|
variableId: request.variableId,
|
|
572
638
|
fixed,
|
|
573
639
|
observe,
|