quicopt 0.1.0 → 0.3.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 +4 -4
- data/.yardopts +2 -1
- data/README.md +262 -33
- data/lib/quicopt/client.rb +156 -49
- data/lib/quicopt/indicators.rb +235 -0
- data/lib/quicopt/model.rb +442 -57
- data/lib/quicopt/modeler/v1/program_pb.rb +10 -1
- data/lib/quicopt/structured.rb +437 -0
- data/lib/quicopt/version.rb +1 -1
- data/lib/quicopt/wire.rb +171 -36
- data/lib/quicopt.rb +7 -6
- data/proto/quicopt/modeler/v1/program.proto +160 -6
- metadata +12 -7
data/lib/quicopt/wire.rb
CHANGED
|
@@ -5,15 +5,15 @@
|
|
|
5
5
|
require "google/protobuf"
|
|
6
6
|
require_relative "modeler/v1/program_pb"
|
|
7
7
|
|
|
8
|
-
# quicopt/wire — a +Model+ → the
|
|
8
|
+
# quicopt/wire — a +Model+ → the +Program+ message → protobuf bytes.
|
|
9
9
|
#
|
|
10
|
-
# The generated
|
|
11
|
-
# the vendored +program.proto+)
|
|
12
|
-
# node constructors on top of
|
|
13
|
-
# polynomials into the
|
|
10
|
+
# The generated protobuf classes (+lib/quicopt/modeler/+, regenerated by
|
|
11
|
+
# +gen/gen.sh+ from the vendored +program.proto+) do the encoding; this file
|
|
12
|
+
# supplies the node constructors on top of them and the translation from the DSL's
|
|
13
|
+
# collected polynomials into the message's expression trees.
|
|
14
14
|
#
|
|
15
|
-
# The
|
|
16
|
-
#
|
|
15
|
+
# The forms below are shared with the Python and Julia clients, so the same model
|
|
16
|
+
# written in any of the three arrives as the same message:
|
|
17
17
|
#
|
|
18
18
|
# f = c ⇒ f − c = 0 (Zero)
|
|
19
19
|
# f ≥ l ⇒ f − l ≥ 0 (Nonneg)
|
|
@@ -22,33 +22,52 @@ require_relative "modeler/v1/program_pb"
|
|
|
22
22
|
# A unit coefficient is dropped rather than emitted as +1 * e+, an additive zero
|
|
23
23
|
# constant is dropped from a sum, and a sum of one term is that term.
|
|
24
24
|
#
|
|
25
|
-
#
|
|
26
|
-
# and
|
|
27
|
-
#
|
|
28
|
-
#
|
|
25
|
+
# Permutations, partitions and lookup tables count from 0 in the DSL and from 1
|
|
26
|
+
# on the wire, and this file is the one place that shifts between the two:
|
|
27
|
+
#
|
|
28
|
+
# route.item_at(k) ⇒ item_at(k + 1, route) − 1
|
|
29
|
+
# teams.part_of(i) ⇒ part_of(i + 1, teams) − 1
|
|
30
|
+
# teams.size_of(b) ⇒ size_of(b + 1, teams) (a count: not shifted)
|
|
31
|
+
# dist[i, j] ⇒ dist[i + 1, j + 1]
|
|
32
|
+
#
|
|
33
|
+
# A read used as a table position meets both shifts, and they cancel:
|
|
34
|
+
# +dist[route.item_at(0), …]+ goes out as +dist[item_at(1, route), …]+.
|
|
35
|
+
#
|
|
36
|
+
# The terms that switch (+quicopt/indicators+) go out as the catalog's
|
|
37
|
+
# +indicator+, +step+, +min+ and +max+; a +min+ or +max+ of more than two
|
|
38
|
+
# arguments as nested binary ones, folded from the left.
|
|
39
|
+
#
|
|
40
|
+
# Note on byte-level comparison: protobuf omits fields that hold their default
|
|
41
|
+
# value (+start: 0.0+) and does not fix the order of map entries, so two encodings
|
|
42
|
+
# of one model can differ byte for byte while meaning exactly the same thing — the
|
|
43
|
+
# same position the Julia client is in. Compare decoded +Program+ values in tests,
|
|
44
|
+
# never hex.
|
|
29
45
|
module Quicopt
|
|
30
|
-
# Node constructors over the generated
|
|
31
|
-
#
|
|
46
|
+
# Node constructors over the generated protobuf classes, and the translation
|
|
47
|
+
# from the DSL's collected polynomials into the message's expression trees.
|
|
32
48
|
module Wire
|
|
33
|
-
# The generated
|
|
34
|
-
#
|
|
49
|
+
# The generated protobuf classes — +Program+, +Expression+, +Constraint+, …
|
|
50
|
+
# Aliased for brevity throughout this file.
|
|
35
51
|
PB = Quicopt::Modeler::V1
|
|
36
52
|
|
|
37
53
|
class << self
|
|
38
|
-
#
|
|
54
|
+
# Turn +model+ into the +Program+ message the service reads.
|
|
39
55
|
#
|
|
40
56
|
# @param model [Quicopt::Model]
|
|
41
57
|
# @return [PB::Program]
|
|
42
58
|
def program(model)
|
|
43
59
|
PB::Program.new(
|
|
60
|
+
params: model.tables.sort_by(&:name).map { |t| param_table(t) },
|
|
44
61
|
vars: model.variables.map { |v| var_decl(v) },
|
|
45
62
|
objective: expression(model.objective),
|
|
46
63
|
sense: model.sense,
|
|
47
|
-
constraints: model.constraints.map { |c| constraint(c) }
|
|
64
|
+
constraints: model.constraints.map { |c| constraint(c) },
|
|
65
|
+
structures: (model.permutations + model.partitions).sort_by(&:name).map { |p| structure_decl(p) },
|
|
66
|
+
blocks: model.blocks.sort_by(&:name).map { |b| block_decl(b) }
|
|
48
67
|
)
|
|
49
68
|
end
|
|
50
69
|
|
|
51
|
-
#
|
|
70
|
+
# Encode a model (or an already-built +Program+) to bytes.
|
|
52
71
|
#
|
|
53
72
|
# @param model [Quicopt::Model, PB::Program]
|
|
54
73
|
# @return [String] binary protobuf bytes
|
|
@@ -56,8 +75,8 @@ module Quicopt
|
|
|
56
75
|
PB::Program.encode(model.is_a?(PB::Program) ? model : program(model))
|
|
57
76
|
end
|
|
58
77
|
|
|
59
|
-
# Parse
|
|
60
|
-
#
|
|
78
|
+
# Parse bytes back into a +Program+ — the inverse of +encode+, and the only
|
|
79
|
+
# sound way to compare two encodings.
|
|
61
80
|
#
|
|
62
81
|
# @param bytes [String]
|
|
63
82
|
# @return [PB::Program]
|
|
@@ -67,26 +86,26 @@ module Quicopt
|
|
|
67
86
|
|
|
68
87
|
# ── node constructors ──────────────────────────────────────────────────
|
|
69
88
|
|
|
70
|
-
# A constant expression node.
|
|
71
|
-
# survives the round-trip rather than vanishing as a
|
|
89
|
+
# A constant expression node. The +oneof+ case is always set, so a constant
|
|
90
|
+
# +0.0+ survives the round-trip rather than vanishing as a protobuf default.
|
|
72
91
|
def const(value)
|
|
73
92
|
PB::Expression.new(constant: number(value))
|
|
74
93
|
end
|
|
75
94
|
|
|
76
|
-
#
|
|
95
|
+
# Flatten negative zero before encoding.
|
|
77
96
|
#
|
|
78
97
|
# IEEE keeps +-0.0+ distinct from +0.0+ — a distinct bit pattern, and a
|
|
79
98
|
# distinct protobuf value — and moving a bound across an inequality
|
|
80
99
|
# (+f ≤ 0+ ⇒ +0 − f ≥ 0+) produces one whenever that bound is zero. The
|
|
81
|
-
#
|
|
82
|
-
#
|
|
100
|
+
# Python and Julia clients send +0.0+ there, so collapse the sign here
|
|
101
|
+
# rather than sending bytes that differ from theirs by a sign bit.
|
|
83
102
|
def number(value)
|
|
84
103
|
float = value.to_f
|
|
85
104
|
float.zero? ? 0.0 : float
|
|
86
105
|
end
|
|
87
106
|
|
|
88
|
-
# A reference to a flat (unindexed) variable. The +Index+ message is
|
|
89
|
-
# present-but-empty,
|
|
107
|
+
# A reference to a flat (unindexed) variable. The +Index+ message is sent
|
|
108
|
+
# present-but-empty, which is what the service expects.
|
|
90
109
|
def var(name)
|
|
91
110
|
PB::Expression.new(var: PB::VarRef.new(name: name, index: PB::Index.new))
|
|
92
111
|
end
|
|
@@ -96,6 +115,17 @@ module Quicopt
|
|
|
96
115
|
PB::Expression.new(apply: PB::Apply.new(op: op, args: args))
|
|
97
116
|
end
|
|
98
117
|
|
|
118
|
+
# A reference to a declared permutation, by name.
|
|
119
|
+
def structure(name)
|
|
120
|
+
PB::Expression.new(structure: PB::StructRef.new(name: name))
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# A read of the parameter table +param+ at the given positions (counted
|
|
124
|
+
# from 1, as on the wire).
|
|
125
|
+
def table(param, index)
|
|
126
|
+
PB::Expression.new(table: PB::TableRef.new(param: param, index: index))
|
|
127
|
+
end
|
|
128
|
+
|
|
99
129
|
# +coef · expr+, dropping a unit coefficient.
|
|
100
130
|
def scaled(coef, expr)
|
|
101
131
|
coef == 1.0 ? expr : apply("*", [const(coef), expr])
|
|
@@ -113,21 +143,124 @@ module Quicopt
|
|
|
113
143
|
|
|
114
144
|
# ── lowering ───────────────────────────────────────────────────────────
|
|
115
145
|
|
|
116
|
-
# A
|
|
117
|
-
# nonzero), then the linear terms, then the quadratic ones — the term
|
|
118
|
-
# the
|
|
146
|
+
# A collected DSL expression → the message's expression tree: the constant
|
|
147
|
+
# (when nonzero), then the linear terms, then the quadratic ones — the term
|
|
148
|
+
# order the Python and Julia clients use.
|
|
119
149
|
#
|
|
120
150
|
# @param expr [Quicopt::Expression]
|
|
121
151
|
# @return [PB::Expression]
|
|
122
152
|
def expression(expr)
|
|
123
153
|
terms = []
|
|
124
154
|
terms << const(expr.constant) unless expr.constant.zero?
|
|
125
|
-
expr.linear.each { |v, c| terms << scaled(c,
|
|
126
|
-
expr.quadratic.each { |(a, b), c| terms << scaled(c, apply("*", [
|
|
155
|
+
expr.linear.each { |v, c| terms << scaled(c, factor(v)) }
|
|
156
|
+
expr.quadratic.each { |(a, b), c| terms << scaled(c, apply("*", [factor(a), factor(b)])) }
|
|
127
157
|
sum(terms)
|
|
128
158
|
end
|
|
129
159
|
|
|
130
|
-
# One
|
|
160
|
+
# One factor of a term: a variable, a read of a permutation, partition or
|
|
161
|
+
# lookup table (each standing for its value counted from 0), or a term
|
|
162
|
+
# that switches.
|
|
163
|
+
#
|
|
164
|
+
# @param factor [Quicopt::Variable, Quicopt::Placement, Quicopt::PartRead,
|
|
165
|
+
# Quicopt::TableEntry, Quicopt::Condition, Quicopt::Extremum]
|
|
166
|
+
# @return [PB::Expression]
|
|
167
|
+
def factor(factor)
|
|
168
|
+
case factor
|
|
169
|
+
when Variable then var(factor.name)
|
|
170
|
+
when Placement
|
|
171
|
+
apply("-", [apply(factor.op.to_s, [const(factor.position + 1), structure(factor.permutation.name)]),
|
|
172
|
+
const(1.0)])
|
|
173
|
+
when PartRead then part_read(factor)
|
|
174
|
+
when TableEntry then table(factor.table.name, factor.index.map { |e| one_based(e) })
|
|
175
|
+
when Condition then condition(factor)
|
|
176
|
+
when Extremum
|
|
177
|
+
factor.args.map { |a| expression(a) }.reduce { |acc, e| apply(factor.op.to_s, [acc, e]) }
|
|
178
|
+
else raise Error, "unknown factor #{factor.inspect}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# A read of a partition: the part, shifted to count from 0; the size, a
|
|
183
|
+
# count, as it is.
|
|
184
|
+
#
|
|
185
|
+
# @param read [Quicopt::PartRead]
|
|
186
|
+
# @return [PB::Expression]
|
|
187
|
+
def part_read(read)
|
|
188
|
+
node = apply(read.op.to_s, [const(read.position + 1), structure(read.partition.name)])
|
|
189
|
+
read.op == :part_of ? apply("-", [node, const(1.0)]) : node
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# A 0/1 term: +indicator(e)+, +step(e)+, or +step(tol − abs(e))+ for a
|
|
193
|
+
# comparison with a tolerance.
|
|
194
|
+
#
|
|
195
|
+
# @param cond [Quicopt::Condition]
|
|
196
|
+
# @return [PB::Expression]
|
|
197
|
+
def condition(cond)
|
|
198
|
+
arg = expression(cond.argument)
|
|
199
|
+
case cond.op
|
|
200
|
+
when :indicator then apply("indicator", [arg])
|
|
201
|
+
when :step then apply("step", [arg])
|
|
202
|
+
when :near then apply("step", [apply("-", [const(cond.tol), apply("abs", [arg])])])
|
|
203
|
+
else raise Error, "unknown condition #{cond.op.inspect}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# A table position, counted from 0 in the DSL, as the wire's position
|
|
208
|
+
# counted from 1: a whole number moves up by one, a read of a permutation
|
|
209
|
+
# loses the − 1 its own shift added, and anything else gains + 1.
|
|
210
|
+
#
|
|
211
|
+
# @param position [Quicopt::Expression]
|
|
212
|
+
# @return [PB::Expression]
|
|
213
|
+
def one_based(position)
|
|
214
|
+
return const(position.constant + 1) if position.constant?
|
|
215
|
+
|
|
216
|
+
e = expression(position)
|
|
217
|
+
down = e.node == :apply && e.apply.op == "-" && e.apply.args.size == 2 &&
|
|
218
|
+
e.apply.args[1].node == :constant && e.apply.args[1].constant == 1.0
|
|
219
|
+
down ? e.apply.args[0] : apply("+", [e, const(1.0)])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# A lookup table → +ParamTable+: one entry per cell, keyed by its positions
|
|
223
|
+
# counted from 1, in row-major order.
|
|
224
|
+
#
|
|
225
|
+
# @param table [Quicopt::LookupTable]
|
|
226
|
+
# @return [PB::ParamTable]
|
|
227
|
+
def param_table(table)
|
|
228
|
+
cells = table.shape.map { |extent| (0...extent).to_a }
|
|
229
|
+
keys = cells.first.product(*cells.drop(1))
|
|
230
|
+
PB::ParamTable.new(name: table.name, entries: keys.map do |key|
|
|
231
|
+
PB::ParamEntry.new(key: PB::Index.new(elems: key.map { |k| PB::IndexElem.new(int: k + 1) }),
|
|
232
|
+
value: number(key.reduce(table.values) { |row, k| row[k] }))
|
|
233
|
+
end)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# A permutation or a partition → +StructureDecl+, its start (and a
|
|
237
|
+
# permutation's precedences) counted from 1.
|
|
238
|
+
#
|
|
239
|
+
# @param structure [Quicopt::Permutation, Quicopt::Partition]
|
|
240
|
+
# @return [PB::StructureDecl]
|
|
241
|
+
def structure_decl(structure)
|
|
242
|
+
start = (structure.start || []).map { |k| k + 1 }
|
|
243
|
+
if structure.is_a?(Partition)
|
|
244
|
+
return PB::StructureDecl.new(name: structure.name, kind: "partition", size: structure.size,
|
|
245
|
+
start: start, fixed: false, parts: structure.parts)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
PB::StructureDecl.new(
|
|
249
|
+
name: structure.name, kind: "permutation", size: structure.size, start: start, fixed: false,
|
|
250
|
+
precede: structure.precedences.map { |a, b| PB::Precedence.new(before: a + 1, after: b + 1) }
|
|
251
|
+
)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A block → +BlockDecl+: its name and its variables, in the order given.
|
|
255
|
+
#
|
|
256
|
+
# @param block [Quicopt::Block]
|
|
257
|
+
# @return [PB::BlockDecl]
|
|
258
|
+
def block_decl(block)
|
|
259
|
+
PB::BlockDecl.new(name: block.name,
|
|
260
|
+
members: block.variables.map { |v| PB::VarRef.new(name: v.name, index: PB::Index.new) })
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# One DSL constraint row → one constraint in the message.
|
|
131
264
|
#
|
|
132
265
|
# @param con [Quicopt::Constraint]
|
|
133
266
|
# @return [PB::Constraint]
|
|
@@ -142,9 +275,11 @@ module Quicopt
|
|
|
142
275
|
end
|
|
143
276
|
end
|
|
144
277
|
|
|
145
|
-
# A variable declaration → +VarDecl+. Bounds are
|
|
146
|
-
#
|
|
147
|
-
#
|
|
278
|
+
# A variable declaration → +VarDecl+. Bounds are plain numbers (the message
|
|
279
|
+
# can also take a bound from a parameter table, but that needs indexed
|
|
280
|
+
# variables, which this DSL does not have); ±Inf passes straight through and
|
|
281
|
+
# means "unbounded in that direction". A variable that may also be 0 goes
|
|
282
|
+
# out with its domain, +SEMICONTINUOUS+ or +SEMIINTEGER+.
|
|
148
283
|
#
|
|
149
284
|
# @param variable [Quicopt::Variable]
|
|
150
285
|
# @return [PB::VarDecl]
|
data/lib/quicopt.rb
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
require_relative "quicopt/version"
|
|
6
6
|
require_relative "quicopt/model"
|
|
7
|
+
require_relative "quicopt/structured"
|
|
8
|
+
require_relative "quicopt/indicators"
|
|
7
9
|
require_relative "quicopt/wire"
|
|
8
10
|
require_relative "quicopt/client"
|
|
9
11
|
|
|
10
12
|
# Quicopt — a Ruby client for the Quicopt optimization service.
|
|
11
13
|
#
|
|
12
|
-
#
|
|
13
|
-
# the service solve it:
|
|
14
|
+
# Write a model in the native DSL, encode it, and let the service solve it:
|
|
14
15
|
#
|
|
15
16
|
# require "quicopt"
|
|
16
17
|
#
|
|
@@ -26,12 +27,12 @@ require_relative "quicopt/client"
|
|
|
26
27
|
#
|
|
27
28
|
# Three pieces, nothing more: +Model+ authors, +Wire+ encodes, +Client+ ships.
|
|
28
29
|
module Quicopt
|
|
29
|
-
# The
|
|
30
|
-
# themselves rather than going through +Client+.
|
|
30
|
+
# The structured message a model becomes, re-exported so callers can inspect
|
|
31
|
+
# the bytes or route them themselves rather than going through +Client+.
|
|
31
32
|
Program = Wire::PB::Program
|
|
32
33
|
|
|
33
34
|
class << self
|
|
34
|
-
#
|
|
35
|
+
# Encode a model to bytes.
|
|
35
36
|
#
|
|
36
37
|
# @param model [Model, Program]
|
|
37
38
|
# @return [String] binary protobuf bytes
|
|
@@ -39,7 +40,7 @@ module Quicopt
|
|
|
39
40
|
Wire.encode(model)
|
|
40
41
|
end
|
|
41
42
|
|
|
42
|
-
# Parse
|
|
43
|
+
# Parse bytes back into a +Program+.
|
|
43
44
|
#
|
|
44
45
|
# @param bytes [String]
|
|
45
46
|
# @return [Program]
|
|
@@ -54,15 +54,52 @@ message Index {
|
|
|
54
54
|
|
|
55
55
|
message Expression {
|
|
56
56
|
oneof node {
|
|
57
|
-
double
|
|
58
|
-
|
|
59
|
-
ParamRef
|
|
60
|
-
VarRef
|
|
61
|
-
Apply
|
|
62
|
-
Reduce
|
|
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
|
+
SourceRef source = 6; // SourceRef(name) — a stochastic source, by name
|
|
64
|
+
StructRef structure = 7; // StructRef(name) — a declared structure, by name
|
|
65
|
+
TableRef table = 8; // TableRef(param, index) — a data table read at
|
|
66
|
+
// positions given by expressions
|
|
63
67
|
}
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
// A reference to a declared stochastic source (Program.sources). Identity lives
|
|
71
|
+
// in the NAME: every SourceRef("w") denotes the same random variable, however
|
|
72
|
+
// often the subtree containing it is copied — so tree encodings and rewrites
|
|
73
|
+
// preserve the model's correlation structure with no encoder discipline
|
|
74
|
+
// required. Independent sources are separate declarations. A server that
|
|
75
|
+
// predates this field rejects any Expression carrying it (an empty oneof), so
|
|
76
|
+
// a stochastic model can never be silently mis-solved as deterministic.
|
|
77
|
+
message SourceRef {
|
|
78
|
+
string name = 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// A reference to a declared structure (Program.structures), by name, exactly
|
|
82
|
+
// as SourceRef names a source. It is legal only as the second argument of a
|
|
83
|
+
// part operator (catalog tag struct_part): `item_at(3, tour)` is
|
|
84
|
+
// Apply("item_at", [constant 3, StructRef "tour"]). A server that predates this
|
|
85
|
+
// field rejects any Expression carrying it (an empty oneof).
|
|
86
|
+
message StructRef {
|
|
87
|
+
string name = 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// A declared Param table read at positions given by expressions: one index for
|
|
91
|
+
// a vector table, two for a matrix. Unlike ParamRef, whose index is a fixed
|
|
92
|
+
// coordinate or a bound loop index, the index expressions here may involve
|
|
93
|
+
// decisions, structure parts or random terms, so the entry is read when the
|
|
94
|
+
// model is solved. The table must be dense on the integer keys 1..a (one
|
|
95
|
+
// index) or 1..a by 1..b (two). An index value that is not an integer in range
|
|
96
|
+
// is rounded to the nearest integer and clamped into range, and a NaN index
|
|
97
|
+
// reads position 1.
|
|
98
|
+
message TableRef {
|
|
99
|
+
string param = 1;
|
|
100
|
+
repeated Expression index = 2; // one or two
|
|
101
|
+
}
|
|
102
|
+
|
|
66
103
|
message ParamRef {
|
|
67
104
|
string name = 1;
|
|
68
105
|
Index index = 2;
|
|
@@ -142,6 +179,11 @@ enum Domain {
|
|
|
142
179
|
CONTINUOUS = 1;
|
|
143
180
|
INTEGER = 2;
|
|
144
181
|
BINARY = 3;
|
|
182
|
+
// The two domains with a hole: the value is 0 or lies in [lower, upper],
|
|
183
|
+
// never in between. Both bounds must be finite; when 0 lies inside them the
|
|
184
|
+
// hole is empty and the variable is an ordinary CONTINUOUS or INTEGER one.
|
|
185
|
+
SEMICONTINUOUS = 4; // 0, or any value in [lower, upper]
|
|
186
|
+
SEMIINTEGER = 5; // 0, or a whole number in [lower, upper]
|
|
145
187
|
}
|
|
146
188
|
|
|
147
189
|
// A per-variable bound: a scalar, or the name of a per-index Param table to look
|
|
@@ -196,6 +238,97 @@ message FixEntry {
|
|
|
196
238
|
double value = 3;
|
|
197
239
|
}
|
|
198
240
|
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// Stochastic sources — named declarations, referenced by SourceRef
|
|
243
|
+
//
|
|
244
|
+
// The stochastic layer rides the SAME expression language: distribution heads
|
|
245
|
+
// and scenario aggregators (smean, scvar, sfreq_*) are catalog operators, so
|
|
246
|
+
// the distribution vocabulary can grow without changing this schema. What the
|
|
247
|
+
// schema adds is declarations: a source is declared once under a name and
|
|
248
|
+
// referenced by SourceRef leaves. The scenario count and seed are model data —
|
|
249
|
+
// they pin the sampled instance, so two solves of the same bytes see the same
|
|
250
|
+
// draws.
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
// A parametric source: a distribution head from the catalog (tag stoch_source)
|
|
254
|
+
// with deterministic parameter expressions, e.g. normal(mu, sigma) where mu
|
|
255
|
+
// may be a Param lookup. Parameters must not contain SourceRef or stochastic
|
|
256
|
+
// heads.
|
|
257
|
+
message Parametric {
|
|
258
|
+
string head = 1;
|
|
259
|
+
repeated Expression params = 2;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// An empirical source: a fixed scenario column of exactly Program.scenarios
|
|
263
|
+
// values. Joint distributions are several columns drawn jointly client-side —
|
|
264
|
+
// the scenario index is shared across all sources, so alignment preserves
|
|
265
|
+
// correlation.
|
|
266
|
+
message Empirical {
|
|
267
|
+
repeated double data = 1; // packed; length == Program.scenarios
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// One named source declaration.
|
|
271
|
+
message SourceDecl {
|
|
272
|
+
string name = 1;
|
|
273
|
+
oneof kind {
|
|
274
|
+
Parametric parametric = 2;
|
|
275
|
+
Empirical empirical = 3;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Structured decision variables — named declarations, referenced by StructRef
|
|
281
|
+
//
|
|
282
|
+
// A structure is a decision variable with a shape every candidate solution
|
|
283
|
+
// keeps. It is declared once under a name, like a source, and its parts are
|
|
284
|
+
// read in expressions through catalog operators tagged struct_part applied to
|
|
285
|
+
// a StructRef. Two kinds:
|
|
286
|
+
// permutation n items over n slots: `item_at(slot, P)` is the item in a
|
|
287
|
+
// slot and `slot_of(item, P)` the slot of an item, both
|
|
288
|
+
// decision-valued integers in 1..n;
|
|
289
|
+
// partition n items in k parts: `part_of(item, P)` is the part of an item
|
|
290
|
+
// (in 1..k) and `size_of(part, P)` the number of items in a
|
|
291
|
+
// part (in 0..n); a part may stay empty.
|
|
292
|
+
// The kind is a catalog key (tag structure). Kinds that do not use a field
|
|
293
|
+
// leave it empty; a field set on a kind that does not use it is refused.
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
// One precedence of a permutation: item `before` sits in an earlier slot than
|
|
297
|
+
// item `after` in every arrangement considered.
|
|
298
|
+
message Precedence {
|
|
299
|
+
uint64 before = 1; // items, 1-based
|
|
300
|
+
uint64 after = 2;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// One named structure declaration.
|
|
304
|
+
message StructureDecl {
|
|
305
|
+
string name = 1; // "tour"
|
|
306
|
+
string kind = 2; // "permutation" | "partition"
|
|
307
|
+
uint64 size = 3; // n: items 1..n (a permutation: n >= 2,
|
|
308
|
+
// slots 1..n; a partition: n >= 1)
|
|
309
|
+
repeated int64 start = 4; // empty, or start[i] = where item i starts:
|
|
310
|
+
// its slot (a permutation of 1..n) or its
|
|
311
|
+
// part (in 1..k)
|
|
312
|
+
bool fixed = 5; // pinned at `start`, which must then be full:
|
|
313
|
+
// how a solution is re-evaluated
|
|
314
|
+
repeated Precedence precede = 6; // permutation only
|
|
315
|
+
uint64 parts = 7; // partition only: k >= 2, parts 1..k
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Blocks — named groups of variables, one subproblem of the model each
|
|
320
|
+
//
|
|
321
|
+
// A block lists the variables of one subproblem (one customer's supply, one
|
|
322
|
+
// project's staffing). It changes nothing about what the model means: the
|
|
323
|
+
// same points are feasible and the objective is the same. A variable is in at
|
|
324
|
+
// most one block, and a member needs finite bounds.
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
message BlockDecl {
|
|
328
|
+
string name = 1;
|
|
329
|
+
repeated VarRef members = 2; // each a declared variable at a concrete index
|
|
330
|
+
}
|
|
331
|
+
|
|
199
332
|
// ---------------------------------------------------------------------------
|
|
200
333
|
// Program — the structured authoring container (what crosses the wire)
|
|
201
334
|
// ---------------------------------------------------------------------------
|
|
@@ -209,6 +342,27 @@ message Program {
|
|
|
209
342
|
string sense = 6; // "min" | "max"
|
|
210
343
|
repeated Constraint constraints = 7;
|
|
211
344
|
repeated FixEntry fix = 8;
|
|
345
|
+
|
|
346
|
+
// Stochastic layer (absent ⇒ deterministic; a deterministic Program encodes
|
|
347
|
+
// identically with or without this schema). For the two scalars, proto3
|
|
348
|
+
// conflates 0 with absent, so 0 is not a legal value on the wire: both mean
|
|
349
|
+
// "server default" (scenarios 1, seed 1). A model whose MEANING depends on
|
|
350
|
+
// these fields necessarily contains a SourceRef or a stochastic operator, so
|
|
351
|
+
// an old server rejects it cleanly rather than mis-solving it; `scenarios`
|
|
352
|
+
// without stochastic terms is semantically inert and safe to skip.
|
|
353
|
+
uint64 scenarios = 9; // number of scenarios in the sampled instance
|
|
354
|
+
uint64 scenario_seed = 10; // base-draw seed
|
|
355
|
+
repeated SourceDecl sources = 11; // canonical order: sorted by name
|
|
356
|
+
|
|
357
|
+
// Structured variables (absent ⇒ none; a Program without them encodes
|
|
358
|
+
// identically with or without this field). Canonical order: sorted by name.
|
|
359
|
+
// A model that uses one carries a StructRef, which an old server rejects, so
|
|
360
|
+
// it is never mis-solved as a plain model.
|
|
361
|
+
repeated StructureDecl structures = 12;
|
|
362
|
+
|
|
363
|
+
// Blocks (absent ⇒ none; a Program without them encodes identically with or
|
|
364
|
+
// without this field). Canonical order: sorted by name.
|
|
365
|
+
repeated BlockDecl blocks = 13;
|
|
212
366
|
}
|
|
213
367
|
|
|
214
368
|
// ---------------------------------------------------------------------------
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: quicopt
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tim Bode
|
|
@@ -23,8 +23,10 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '4.0'
|
|
26
|
-
description:
|
|
27
|
-
the
|
|
26
|
+
description: 'Describe an optimization problem in a small native Ruby DSL — the variables
|
|
27
|
+
you choose, the constraints that must hold, and the quantity to minimize or maximize
|
|
28
|
+
— and solve it on the Quicopt service. There is no local solver: the service does
|
|
29
|
+
the solving.'
|
|
28
30
|
email:
|
|
29
31
|
- 9047234+timbode@users.noreply.github.com
|
|
30
32
|
executables: []
|
|
@@ -36,8 +38,10 @@ files:
|
|
|
36
38
|
- README.md
|
|
37
39
|
- lib/quicopt.rb
|
|
38
40
|
- lib/quicopt/client.rb
|
|
41
|
+
- lib/quicopt/indicators.rb
|
|
39
42
|
- lib/quicopt/model.rb
|
|
40
43
|
- lib/quicopt/modeler/v1/program_pb.rb
|
|
44
|
+
- lib/quicopt/structured.rb
|
|
41
45
|
- lib/quicopt/version.rb
|
|
42
46
|
- lib/quicopt/wire.rb
|
|
43
47
|
- proto/quicopt/modeler/v1/program.proto
|
|
@@ -45,11 +49,13 @@ files:
|
|
|
45
49
|
- templates/default/fulldoc/html/css/quicopt-icon-color.svg
|
|
46
50
|
- templates/default/fulldoc/html/css/quicopt-icon-white.svg
|
|
47
51
|
- templates/default/fulldoc/html/setup.rb
|
|
48
|
-
homepage: https://github.com/
|
|
52
|
+
homepage: https://github.com/Quicopt/quicopt-ruby
|
|
49
53
|
licenses:
|
|
50
54
|
- Apache-2.0
|
|
51
55
|
metadata:
|
|
52
|
-
source_code_uri: https://github.com/
|
|
56
|
+
source_code_uri: https://github.com/Quicopt/quicopt-ruby
|
|
57
|
+
documentation_uri: https://quicopt.github.io/quicopt-ruby/
|
|
58
|
+
bug_tracker_uri: https://github.com/Quicopt/quicopt-ruby/issues
|
|
53
59
|
rubygems_mfa_required: 'true'
|
|
54
60
|
rdoc_options: []
|
|
55
61
|
require_paths:
|
|
@@ -67,6 +73,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
67
73
|
requirements: []
|
|
68
74
|
rubygems_version: 3.6.9
|
|
69
75
|
specification_version: 4
|
|
70
|
-
summary: Ruby client for the Quicopt service
|
|
71
|
-
IR.
|
|
76
|
+
summary: Ruby client for the Quicopt optimization service.
|
|
72
77
|
test_files: []
|