quicopt 0.1.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.
- checksums.yaml +7 -0
- data/.yardopts +9 -0
- data/LICENSE +201 -0
- data/README.md +205 -0
- data/lib/quicopt/client.rb +504 -0
- data/lib/quicopt/model.rb +627 -0
- data/lib/quicopt/modeler/v1/program_pb.rb +48 -0
- data/lib/quicopt/version.rb +9 -0
- data/lib/quicopt/wire.rb +179 -0
- data/lib/quicopt.rb +50 -0
- data/proto/quicopt/modeler/v1/program.proto +240 -0
- data/templates/default/fulldoc/html/css/common.css +474 -0
- data/templates/default/fulldoc/html/css/quicopt-icon-color.svg +244 -0
- data/templates/default/fulldoc/html/css/quicopt-icon-white.svg +235 -0
- data/templates/default/fulldoc/html/setup.rb +17 -0
- metadata +72 -0
data/lib/quicopt/wire.rb
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2026 Tim Bode, PGI-12, Forschungszentrum Jülich
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require "google/protobuf"
|
|
6
|
+
require_relative "modeler/v1/program_pb"
|
|
7
|
+
|
|
8
|
+
# quicopt/wire — a +Model+ → the wire IR +Program+ → protobuf bytes.
|
|
9
|
+
#
|
|
10
|
+
# The generated codec (+lib/quicopt/modeler/+, regenerated by +gen/gen.sh+ from
|
|
11
|
+
# the vendored +program.proto+) does the serialization; this file supplies the
|
|
12
|
+
# node constructors on top of it and the lowering from the DSL's normalised
|
|
13
|
+
# polynomials into the IR's expression trees.
|
|
14
|
+
#
|
|
15
|
+
# The canonical forms below are shared with the other clients, so the same model
|
|
16
|
+
# authored in Ruby, Python or Julia reaches the service as the same IR:
|
|
17
|
+
#
|
|
18
|
+
# f = c ⇒ f − c = 0 (Zero)
|
|
19
|
+
# f ≥ l ⇒ f − l ≥ 0 (Nonneg)
|
|
20
|
+
# f ≤ u ⇒ u − f ≥ 0 (Nonneg)
|
|
21
|
+
#
|
|
22
|
+
# A unit coefficient is dropped rather than emitted as +1 * e+, an additive zero
|
|
23
|
+
# constant is dropped from a sum, and a sum of one term is that term.
|
|
24
|
+
#
|
|
25
|
+
# Note on byte-level comparison: proto3 omits zero-valued scalars (+start: 0.0+)
|
|
26
|
+
# and does not canonicalize field order, so these bytes are *decode-equivalent*
|
|
27
|
+
# to the service's reference codec, not byte-identical — the same position the
|
|
28
|
+
# Julia client is in. Compare decoded +Program+ values in tests, never hex.
|
|
29
|
+
module Quicopt
|
|
30
|
+
# Node constructors over the generated codec, and the lowering from the DSL's
|
|
31
|
+
# normalised polynomials into the IR's expression trees.
|
|
32
|
+
module Wire
|
|
33
|
+
# The generated wire schema — +Program+, +Expression+, +Constraint+, … — as
|
|
34
|
+
# the client's IR. Aliased for brevity throughout the codec.
|
|
35
|
+
PB = Quicopt::Modeler::V1
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# Lower +model+ to the wire IR.
|
|
39
|
+
#
|
|
40
|
+
# @param model [Quicopt::Model]
|
|
41
|
+
# @return [PB::Program]
|
|
42
|
+
def program(model)
|
|
43
|
+
PB::Program.new(
|
|
44
|
+
vars: model.variables.map { |v| var_decl(v) },
|
|
45
|
+
objective: expression(model.objective),
|
|
46
|
+
sense: model.sense,
|
|
47
|
+
constraints: model.constraints.map { |c| constraint(c) }
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Serialize a model (or an already-lowered +Program+) to wire bytes.
|
|
52
|
+
#
|
|
53
|
+
# @param model [Quicopt::Model, PB::Program]
|
|
54
|
+
# @return [String] binary protobuf bytes
|
|
55
|
+
def encode(model)
|
|
56
|
+
PB::Program.encode(model.is_a?(PB::Program) ? model : program(model))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Parse wire bytes back into a +Program+ — the inverse of +encode+, and the
|
|
60
|
+
# only sound way to compare two encodings.
|
|
61
|
+
#
|
|
62
|
+
# @param bytes [String]
|
|
63
|
+
# @return [PB::Program]
|
|
64
|
+
def decode(bytes)
|
|
65
|
+
PB::Program.decode(bytes)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# ── node constructors ──────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
# A constant expression node. Always a set +oneof+ case, so +Const(0.0)+
|
|
71
|
+
# survives the round-trip rather than vanishing as a proto3 default.
|
|
72
|
+
def const(value)
|
|
73
|
+
PB::Expression.new(constant: number(value))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Normalise negative zero on its way to the wire.
|
|
77
|
+
#
|
|
78
|
+
# IEEE keeps +-0.0+ distinct from +0.0+ — a distinct bit pattern, and a
|
|
79
|
+
# distinct protobuf value — and moving a bound across an inequality
|
|
80
|
+
# (+f ≤ 0+ ⇒ +0 − f ≥ 0+) produces one whenever that bound is zero. The
|
|
81
|
+
# sibling clients emit +0.0+ there, so collapse the sign here rather than
|
|
82
|
+
# shipping bytes that differ from theirs by a sign bit.
|
|
83
|
+
def number(value)
|
|
84
|
+
float = value.to_f
|
|
85
|
+
float.zero? ? 0.0 : float
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A reference to a flat (unindexed) variable. The +Index+ message is emitted
|
|
89
|
+
# present-but-empty, matching the reference codec.
|
|
90
|
+
def var(name)
|
|
91
|
+
PB::Expression.new(var: PB::VarRef.new(name: name, index: PB::Index.new))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# An operator application: +op+ applied to child expressions.
|
|
95
|
+
def apply(op, args)
|
|
96
|
+
PB::Expression.new(apply: PB::Apply.new(op: op, args: args))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# +coef · expr+, dropping a unit coefficient.
|
|
100
|
+
def scaled(coef, expr)
|
|
101
|
+
coef == 1.0 ? expr : apply("*", [const(coef), expr])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Fold terms into an n-ary sum, dropping additive-zero constants. No terms
|
|
105
|
+
# is the zero constant; one term is itself.
|
|
106
|
+
def sum(terms)
|
|
107
|
+
surviving = terms.reject { |t| t.node == :constant && t.constant.zero? }
|
|
108
|
+
return const(0.0) if surviving.empty?
|
|
109
|
+
return surviving.first if surviving.size == 1
|
|
110
|
+
|
|
111
|
+
apply("+", surviving)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# ── lowering ───────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
# A normalised DSL expression → an IR expression tree: the constant (when
|
|
117
|
+
# nonzero), then the linear terms, then the quadratic ones — the term order
|
|
118
|
+
# the sibling front-ends emit.
|
|
119
|
+
#
|
|
120
|
+
# @param expr [Quicopt::Expression]
|
|
121
|
+
# @return [PB::Expression]
|
|
122
|
+
def expression(expr)
|
|
123
|
+
terms = []
|
|
124
|
+
terms << const(expr.constant) unless expr.constant.zero?
|
|
125
|
+
expr.linear.each { |v, c| terms << scaled(c, var(v.name)) }
|
|
126
|
+
expr.quadratic.each { |(a, b), c| terms << scaled(c, apply("*", [var(a.name), var(b.name)])) }
|
|
127
|
+
sum(terms)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# One DSL constraint row → an IR function-in-set row.
|
|
131
|
+
#
|
|
132
|
+
# @param con [Quicopt::Constraint]
|
|
133
|
+
# @return [PB::Constraint]
|
|
134
|
+
def constraint(con)
|
|
135
|
+
f = expression(con.body)
|
|
136
|
+
c = con.bound
|
|
137
|
+
case con.sense
|
|
138
|
+
when :eq then PB::Constraint.new(f: minus(f, c), set: con_set(:zero))
|
|
139
|
+
when :ge then PB::Constraint.new(f: minus(f, c), set: con_set(:nonneg))
|
|
140
|
+
when :le then PB::Constraint.new(f: geq(c, f), set: con_set(:nonneg))
|
|
141
|
+
else raise Error, "unknown constraint sense #{con.sense.inspect}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# A variable declaration → +VarDecl+. Bounds are scalars (a Param-table
|
|
146
|
+
# bound needs indexed variables, which the DSL does not have); ±Inf passes
|
|
147
|
+
# straight through as a free direction.
|
|
148
|
+
#
|
|
149
|
+
# @param variable [Quicopt::Variable]
|
|
150
|
+
# @return [PB::VarDecl]
|
|
151
|
+
def var_decl(variable)
|
|
152
|
+
PB::VarDecl.new(name: variable.name,
|
|
153
|
+
domain: variable.domain,
|
|
154
|
+
lower: PB::Bound.new(scalar: number(variable.lower)),
|
|
155
|
+
upper: PB::Bound.new(scalar: number(variable.upper)),
|
|
156
|
+
start: number(variable.start))
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
# +f − c+, dropping the additive zero.
|
|
162
|
+
def minus(f, c)
|
|
163
|
+
c.zero? ? f : apply("-", [f, const(c)])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# +u − f+ — the body of +f ≤ u+ written as +u − f ≥ 0+.
|
|
167
|
+
def geq(u, f)
|
|
168
|
+
apply("-", [const(u), f])
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def con_set(kind)
|
|
172
|
+
case kind
|
|
173
|
+
when :zero then PB::ConSet.new(zero: PB::Zero.new)
|
|
174
|
+
when :nonneg then PB::ConSet.new(nonneg: PB::Nonneg.new)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
data/lib/quicopt.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2026 Tim Bode, PGI-12, Forschungszentrum Jülich
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
require_relative "quicopt/version"
|
|
6
|
+
require_relative "quicopt/model"
|
|
7
|
+
require_relative "quicopt/wire"
|
|
8
|
+
require_relative "quicopt/client"
|
|
9
|
+
|
|
10
|
+
# Quicopt — a Ruby client for the Quicopt optimization service.
|
|
11
|
+
#
|
|
12
|
+
# Author a model in the native DSL, encode it to the versioned wire IR, and let
|
|
13
|
+
# the service solve it:
|
|
14
|
+
#
|
|
15
|
+
# require "quicopt"
|
|
16
|
+
#
|
|
17
|
+
# m = Quicopt::Model.new
|
|
18
|
+
# x = m.int_var(0, 4, "x")
|
|
19
|
+
# y = m.bin_var("y")
|
|
20
|
+
# z = m.num_var(0, 10, "z")
|
|
21
|
+
# m.maximize(3 * x + 2 * y + z)
|
|
22
|
+
# m.add(x + y <= 5)
|
|
23
|
+
#
|
|
24
|
+
# result = Quicopt::Client.new.solve(m)
|
|
25
|
+
# puts result.display
|
|
26
|
+
#
|
|
27
|
+
# Three pieces, nothing more: +Model+ authors, +Wire+ encodes, +Client+ ships.
|
|
28
|
+
module Quicopt
|
|
29
|
+
# The wire IR container, re-exported so callers can inspect or route the bytes
|
|
30
|
+
# themselves rather than going through +Client+.
|
|
31
|
+
Program = Wire::PB::Program
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Lower a model to wire bytes.
|
|
35
|
+
#
|
|
36
|
+
# @param model [Model, Program]
|
|
37
|
+
# @return [String] binary protobuf bytes
|
|
38
|
+
def encode(model)
|
|
39
|
+
Wire.encode(model)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Parse wire bytes back into a +Program+.
|
|
43
|
+
#
|
|
44
|
+
# @param bytes [String]
|
|
45
|
+
# @return [Program]
|
|
46
|
+
def decode(bytes)
|
|
47
|
+
Wire.decode(bytes)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: (c) 2026 Tim Bode, PGI-12, Forschungszentrum Jülich
|
|
3
|
+
// ===========================================================================
|
|
4
|
+
// Quicopt wire schema — v1
|
|
5
|
+
//
|
|
6
|
+
// The language-neutral API contract of the Quicopt service. A client authors a
|
|
7
|
+
// model in a front-end (JuMP, Pyomo, …), lowers it to the structured `Program`
|
|
8
|
+
// below, and serializes it to protobuf bytes; the service decodes and solves it.
|
|
9
|
+
// This file is the source of truth clients compile against.
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Schema evolution (v1):
|
|
12
|
+
// - Field numbers are permanent. Never reuse a number; `reserved` removed
|
|
13
|
+
// ones. Additive-only: new optional fields/messages may be appended.
|
|
14
|
+
// - `oneof` cases are append-only.
|
|
15
|
+
// - Breaking changes bump the package to `quicopt.modeler.v2` in a new file;
|
|
16
|
+
// v1 stays frozen.
|
|
17
|
+
//
|
|
18
|
+
// Conventions:
|
|
19
|
+
// - IR names (`x`, `i`, `S`, `+`, `min`) are `string` here; a client maps them
|
|
20
|
+
// to its own symbol type at the boundary. Data is never carried as a bare
|
|
21
|
+
// string, so the mapping is lossless.
|
|
22
|
+
// - Tuple-keyed tables (params, indexed_sets, fix) cannot be protobuf `map`
|
|
23
|
+
// (keys aren't scalar) — each is a `repeated Entry { key, value }`.
|
|
24
|
+
// ===========================================================================
|
|
25
|
+
|
|
26
|
+
syntax = "proto3";
|
|
27
|
+
|
|
28
|
+
package quicopt.modeler.v1;
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Index machinery
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
// One entry of an index tuple: a concrete coordinate (`3`) or a bound index
|
|
35
|
+
// name (`i`).
|
|
36
|
+
message IndexElem {
|
|
37
|
+
oneof kind {
|
|
38
|
+
int64 int = 1; // a concrete coordinate
|
|
39
|
+
string name = 2; // a bound index name, e.g. i
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// An index tuple: () scalar, (i,), (3,), (i, j) …
|
|
44
|
+
message Index {
|
|
45
|
+
repeated IndexElem elems = 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Expression tree (e ::= c │ p[idx] │ x[idx] │ op(e…) │ ⊕_{k∈S} e(k))
|
|
50
|
+
//
|
|
51
|
+
// Serialized as a tree — each node stands alone, so the structure is acyclic by
|
|
52
|
+
// construction.
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
message Expression {
|
|
56
|
+
oneof node {
|
|
57
|
+
double constant = 1; // Const(value) — always emitted (a set oneof case),
|
|
58
|
+
// so Const(0.0) survives the round-trip
|
|
59
|
+
ParamRef param = 2; // Param(name, index)
|
|
60
|
+
VarRef var = 3; // Var(name, index)
|
|
61
|
+
Apply apply = 4; // Apply(op, args)
|
|
62
|
+
Reduce reduce = 5; // Reduce(op, idx, over, body, cond)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
message ParamRef {
|
|
67
|
+
string name = 1;
|
|
68
|
+
Index index = 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
message VarRef {
|
|
72
|
+
string name = 1;
|
|
73
|
+
Index index = 2;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Operator application. `op` is a key into the operator catalog (see Catalog),
|
|
77
|
+
// not code; an unknown op or an arity mismatch is rejected.
|
|
78
|
+
message Apply {
|
|
79
|
+
string op = 1;
|
|
80
|
+
repeated Expression args = 2;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A reference to an index set, possibly applied to enclosing bound indices.
|
|
84
|
+
// args = () → a flat set (resolved against IndexSet); args = (i,) → a dependent
|
|
85
|
+
// set "S(i)" (resolved against IndexedSet once i is bound).
|
|
86
|
+
message SetRef {
|
|
87
|
+
string name = 1;
|
|
88
|
+
Index args = 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Aggregation ⊕_{idx ∈ over} body, optionally filtered by `cond` (keep a term
|
|
92
|
+
// only where cond ≠ 0). `cond` absent (message not set) ⇒ no filter.
|
|
93
|
+
message Reduce {
|
|
94
|
+
string op = 1; // the fold operator key (+, *, …)
|
|
95
|
+
string idx = 2; // the bound dummy index name
|
|
96
|
+
SetRef over = 3;
|
|
97
|
+
Expression body = 4;
|
|
98
|
+
Expression cond = 5; // optional
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Constraints — function-in-set, both sides open
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
message Zero {} // f = 0
|
|
106
|
+
message Nonneg {} // f ≥ 0
|
|
107
|
+
|
|
108
|
+
// Indicator: the binary `bin` being active implies f lies in `inner`. Kept
|
|
109
|
+
// first-class on the wire (not desugared); the service decides how it is realized.
|
|
110
|
+
message Indicator {
|
|
111
|
+
VarRef bin = 1;
|
|
112
|
+
ConSet inner = 2;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
message ConSet {
|
|
116
|
+
oneof kind {
|
|
117
|
+
Zero zero = 1;
|
|
118
|
+
Nonneg nonneg = 2;
|
|
119
|
+
Indicator indicator = 3;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// One (index symbol => set) binding of a constraint's quantifier.
|
|
124
|
+
message QuantBinding {
|
|
125
|
+
string idx = 1;
|
|
126
|
+
SetRef set = 2;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// f ∈ set, quantified over `over` (∅ over ⇒ a single flat constraint).
|
|
130
|
+
message Constraint {
|
|
131
|
+
Expression f = 1;
|
|
132
|
+
ConSet set = 2;
|
|
133
|
+
repeated QuantBinding over = 3;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// Variables, sets, data
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
enum Domain {
|
|
141
|
+
DOMAIN_UNSPECIFIED = 0; // invalid on the wire; rejected on decode
|
|
142
|
+
CONTINUOUS = 1;
|
|
143
|
+
INTEGER = 2;
|
|
144
|
+
BINARY = 3;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// A per-variable bound: a scalar, or the name of a per-index Param table to look
|
|
148
|
+
// the bound up in (so a bound may vary by index).
|
|
149
|
+
message Bound {
|
|
150
|
+
oneof kind {
|
|
151
|
+
double scalar = 1;
|
|
152
|
+
string param = 2; // name of a Param table
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// A variable *family* indexed over `axes` (set names; empty ⇒ scalar).
|
|
157
|
+
message VarDecl {
|
|
158
|
+
string name = 1;
|
|
159
|
+
repeated string axes = 2;
|
|
160
|
+
Domain domain = 3;
|
|
161
|
+
Bound lower = 4;
|
|
162
|
+
Bound upper = 5;
|
|
163
|
+
double start = 6;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A flat index set: name + concrete elements.
|
|
167
|
+
message IndexSet {
|
|
168
|
+
string name = 1;
|
|
169
|
+
repeated IndexElem elements = 2;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// A dependent set carried as data: one fibre per key tuple, e.g. S[(i,)] → [j…].
|
|
173
|
+
message IndexedSetFibre {
|
|
174
|
+
Index key = 1;
|
|
175
|
+
repeated IndexElem value = 2;
|
|
176
|
+
}
|
|
177
|
+
message IndexedSet {
|
|
178
|
+
string name = 1;
|
|
179
|
+
repeated IndexedSetFibre fibres = 2;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// A named indexed data table: one entry per key tuple, e.g. p[(i,)], A[(i,j)].
|
|
183
|
+
message ParamEntry {
|
|
184
|
+
Index key = 1;
|
|
185
|
+
double value = 2;
|
|
186
|
+
}
|
|
187
|
+
message ParamTable {
|
|
188
|
+
string name = 1;
|
|
189
|
+
repeated ParamEntry entries = 2;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// A per-index pin: fix variable `var` at `index` to `value` (e.g. x[1] = 0).
|
|
193
|
+
message FixEntry {
|
|
194
|
+
string var = 1;
|
|
195
|
+
Index index = 2;
|
|
196
|
+
double value = 3;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Program — the structured authoring container (what crosses the wire)
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
message Program {
|
|
204
|
+
repeated IndexSet sets = 1;
|
|
205
|
+
repeated IndexedSet indexed_sets = 2;
|
|
206
|
+
repeated ParamTable params = 3;
|
|
207
|
+
repeated VarDecl vars = 4;
|
|
208
|
+
Expression objective = 5;
|
|
209
|
+
string sense = 6; // "min" | "max"
|
|
210
|
+
repeated Constraint constraints = 7;
|
|
211
|
+
repeated FixEntry fix = 8;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Rebinding — graph once, data per instance
|
|
216
|
+
//
|
|
217
|
+
// A client sends the Program (the graph) once, then a ParamData per instance to
|
|
218
|
+
// re-solve the same structure on new data without re-sending the expression trees.
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
message ParamData {
|
|
222
|
+
repeated ParamTable params = 1;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// Operator catalog — the versioned set of operators a client may emit
|
|
227
|
+
//
|
|
228
|
+
// Clients fetch the Catalog to discover the supported ops; `version` co-versions
|
|
229
|
+
// with the schema. arity = -1 ≡ variadic.
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
message OpDef {
|
|
233
|
+
string op = 1;
|
|
234
|
+
int32 arity = 2;
|
|
235
|
+
repeated string tags = 3;
|
|
236
|
+
}
|
|
237
|
+
message Catalog {
|
|
238
|
+
uint32 version = 1;
|
|
239
|
+
repeated OpDef ops = 2;
|
|
240
|
+
}
|