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.
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 wire IR +Program+ → protobuf bytes.
8
+ # quicopt/wire — a +Model+ → the +Program+ message → protobuf bytes.
9
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.
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 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:
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
- # 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.
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 codec, and the lowering from the DSL's
31
- # normalised polynomials into the IR's expression trees.
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 wire schema — +Program+, +Expression+, +Constraint+, … — as
34
- # the client's IR. Aliased for brevity throughout the codec.
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
- # Lower +model+ to the wire IR.
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
- # Serialize a model (or an already-lowered +Program+) to wire bytes.
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 wire bytes back into a +Program+ — the inverse of +encode+, and the
60
- # only sound way to compare two encodings.
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. Always a set +oneof+ case, so +Const(0.0)+
71
- # survives the round-trip rather than vanishing as a proto3 default.
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
- # Normalise negative zero on its way to the wire.
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
- # sibling clients emit +0.0+ there, so collapse the sign here rather than
82
- # shipping bytes that differ from theirs by a sign bit.
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 emitted
89
- # present-but-empty, matching the reference codec.
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 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.
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, var(v.name)) }
126
- expr.quadratic.each { |(a, b), c| terms << scaled(c, apply("*", [var(a.name), var(b.name)])) }
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 DSL constraint row → an IR function-in-set row.
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 scalars (a Param-table
146
- # bound needs indexed variables, which the DSL does not have); ±Inf passes
147
- # straight through as a free direction.
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
- # Author a model in the native DSL, encode it to the versioned wire IR, and let
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 wire IR container, re-exported so callers can inspect or route the bytes
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
- # Lower a model to wire bytes.
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 wire bytes back into a +Program+.
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 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)
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.1.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: Author an optimization model in a small native Ruby DSL, encode it to
27
- the versioned Quicopt wire IR, and solve it on the Quicopt service.
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/timbode/quicopt-ruby
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/timbode/quicopt-ruby
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 — author models, emit the Quicopt wire
71
- IR.
76
+ summary: Ruby client for the Quicopt optimization service.
72
77
  test_files: []