Module: Quicopt::Wire
- Defined in:
- lib/quicopt/wire.rb
Overview
Node constructors over the generated protobuf classes, and the translation from the DSL's collected polynomials into the message's expression trees.
Constant Summary collapse
- PB =
The generated protobuf classes —
Program,Expression,Constraint, … Aliased for brevity throughout this file. Quicopt::Modeler::V1
Class Method Summary collapse
-
.apply(op, args) ⇒ Object
An operator application:
opapplied to child expressions. -
.block_decl(block) ⇒ PB::BlockDecl
A block →
BlockDecl: its name and its variables, in the order given. -
.condition(cond) ⇒ PB::Expression
A 0/1 term:
indicator(e),step(e), orstep(tol − abs(e))for a comparison with a tolerance. -
.const(value) ⇒ Object
A constant expression node.
-
.constraint(con) ⇒ PB::Constraint
One DSL constraint row → one constraint in the message.
-
.decode(bytes) ⇒ PB::Program
Parse bytes back into a
Program— the inverse ofencode, and the only sound way to compare two encodings. -
.encode(model) ⇒ String
Encode a model (or an already-built
Program) to bytes. -
.expression(expr) ⇒ PB::Expression
A collected DSL expression → the message's expression tree: the constant (when nonzero), then the linear terms, then the quadratic ones — the term order the Python and Julia clients use.
-
.factor(factor) ⇒ PB::Expression
One factor of a term: a variable, a read of a permutation, partition or lookup table (each standing for its value counted from 0), or a term that switches.
-
.number(value) ⇒ Object
Flatten negative zero before encoding.
-
.one_based(position) ⇒ PB::Expression
A table position, counted from 0 in the DSL, as the wire's position counted from 1: a whole number moves up by one, a read of a permutation loses the − 1 its own shift added, and anything else gains + 1.
-
.param_table(table) ⇒ PB::ParamTable
A lookup table →
ParamTable: one entry per cell, keyed by its positions counted from 1, in row-major order. -
.part_read(read) ⇒ PB::Expression
A read of a partition: the part, shifted to count from 0; the size, a count, as it is.
-
.program(model) ⇒ PB::Program
Turn
modelinto theProgrammessage the service reads. -
.scaled(coef, expr) ⇒ Object
coef · expr, dropping a unit coefficient. -
.structure(name) ⇒ Object
A reference to a declared permutation, by name.
-
.structure_decl(structure) ⇒ PB::StructureDecl
A permutation or a partition →
StructureDecl, its start (and a permutation's precedences) counted from 1. -
.sum(terms) ⇒ Object
Fold terms into an n-ary sum, dropping additive-zero constants.
-
.table(param, index) ⇒ Object
A read of the parameter table
paramat the given positions (counted from 1, as on the wire). -
.var(name) ⇒ Object
A reference to a flat (unindexed) variable.
-
.var_decl(variable) ⇒ PB::VarDecl
A variable declaration →
VarDecl.
Class Method Details
.apply(op, args) ⇒ Object
An operator application: op applied to child expressions.
115 116 117 |
# File 'lib/quicopt/wire.rb', line 115 def apply(op, args) PB::Expression.new(apply: PB::Apply.new(op: op, args: args)) end |
.block_decl(block) ⇒ PB::BlockDecl
A block → BlockDecl: its name and its variables, in the order given.
259 260 261 262 |
# File 'lib/quicopt/wire.rb', line 259 def block_decl(block) PB::BlockDecl.new(name: block.name, members: block.variables.map { |v| PB::VarRef.new(name: v.name, index: PB::Index.new) }) end |
.condition(cond) ⇒ PB::Expression
A 0/1 term: indicator(e), step(e), or step(tol − abs(e)) for a
comparison with a tolerance.
198 199 200 201 202 203 204 205 206 |
# File 'lib/quicopt/wire.rb', line 198 def condition(cond) arg = expression(cond.argument) case cond.op when :indicator then apply("indicator", [arg]) when :step then apply("step", [arg]) when :near then apply("step", [apply("-", [const(cond.tol), apply("abs", [arg])])]) else raise Error, "unknown condition #{cond.op.inspect}" end end |
.const(value) ⇒ Object
A constant expression node. The oneof case is always set, so a constant
0.0 survives the round-trip rather than vanishing as a protobuf default.
91 92 93 |
# File 'lib/quicopt/wire.rb', line 91 def const(value) PB::Expression.new(constant: number(value)) end |
.constraint(con) ⇒ PB::Constraint
One DSL constraint row → one constraint in the message.
268 269 270 271 272 273 274 275 276 277 |
# File 'lib/quicopt/wire.rb', line 268 def constraint(con) f = expression(con.body) c = con.bound case con.sense when :eq then PB::Constraint.new(f: minus(f, c), set: con_set(:zero)) when :ge then PB::Constraint.new(f: minus(f, c), set: con_set(:nonneg)) when :le then PB::Constraint.new(f: geq(c, f), set: con_set(:nonneg)) else raise Error, "unknown constraint sense #{con.sense.inspect}" end end |
.decode(bytes) ⇒ PB::Program
Parse bytes back into a Program — the inverse of encode, and the only
sound way to compare two encodings.
83 84 85 |
# File 'lib/quicopt/wire.rb', line 83 def decode(bytes) PB::Program.decode(bytes) end |
.encode(model) ⇒ String
Encode a model (or an already-built Program) to bytes.
74 75 76 |
# File 'lib/quicopt/wire.rb', line 74 def encode(model) PB::Program.encode(model.is_a?(PB::Program) ? model : program(model)) end |
.expression(expr) ⇒ PB::Expression
A collected DSL expression → the message's expression tree: the constant (when nonzero), then the linear terms, then the quadratic ones — the term order the Python and Julia clients use.
153 154 155 156 157 158 159 |
# File 'lib/quicopt/wire.rb', line 153 def expression(expr) terms = [] terms << const(expr.constant) unless expr.constant.zero? expr.linear.each { |v, c| terms << scaled(c, factor(v)) } expr.quadratic.each { |(a, b), c| terms << scaled(c, apply("*", [factor(a), factor(b)])) } sum(terms) end |
.factor(factor) ⇒ PB::Expression
One factor of a term: a variable, a read of a permutation, partition or lookup table (each standing for its value counted from 0), or a term that switches.
168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
# File 'lib/quicopt/wire.rb', line 168 def factor(factor) case factor when Variable then var(factor.name) when Placement apply("-", [apply(factor.op.to_s, [const(factor.position + 1), structure(factor.permutation.name)]), const(1.0)]) when PartRead then part_read(factor) when TableEntry then table(factor.table.name, factor.index.map { |e| one_based(e) }) when Condition then condition(factor) when Extremum factor.args.map { |a| expression(a) }.reduce { |acc, e| apply(factor.op.to_s, [acc, e]) } else raise Error, "unknown factor #{factor.inspect}" end end |
.number(value) ⇒ Object
Flatten negative zero before encoding.
IEEE keeps -0.0 distinct from 0.0 — a distinct bit pattern, and a
distinct protobuf value — and moving a bound across an inequality
(f ≤ 0 ⇒ 0 − f ≥ 0) produces one whenever that bound
is zero. The Python and Julia clients send 0.0 there, so collapse the
sign here rather than sending bytes that differ from theirs by a sign
bit.
103 104 105 106 |
# File 'lib/quicopt/wire.rb', line 103 def number(value) float = value.to_f float.zero? ? 0.0 : float end |
.one_based(position) ⇒ PB::Expression
A table position, counted from 0 in the DSL, as the wire's position counted from 1: a whole number moves up by one, a read of a permutation loses the − 1 its own shift added, and anything else gains + 1.
214 215 216 217 218 219 220 221 |
# File 'lib/quicopt/wire.rb', line 214 def one_based(position) return const(position.constant + 1) if position.constant? e = expression(position) down = e.node == :apply && e.apply.op == "-" && e.apply.args.size == 2 && e.apply.args[1].node == :constant && e.apply.args[1].constant == 1.0 down ? e.apply.args[0] : apply("+", [e, const(1.0)]) end |
.param_table(table) ⇒ PB::ParamTable
A lookup table → ParamTable: one entry per cell, keyed by its positions
counted from 1, in row-major order.
228 229 230 231 232 233 234 235 |
# File 'lib/quicopt/wire.rb', line 228 def param_table(table) cells = table.shape.map { |extent| (0...extent).to_a } keys = cells.first.product(*cells.drop(1)) PB::ParamTable.new(name: table.name, entries: keys.map do |key| PB::ParamEntry.new(key: PB::Index.new(elems: key.map { |k| PB::IndexElem.new(int: k + 1) }), value: number(key.reduce(table.values) { |row, k| row[k] })) end) end |
.part_read(read) ⇒ PB::Expression
A read of a partition: the part, shifted to count from 0; the size, a count, as it is.
188 189 190 191 |
# File 'lib/quicopt/wire.rb', line 188 def part_read(read) node = apply(read.op.to_s, [const(read.position + 1), structure(read.partition.name)]) read.op == :part_of ? apply("-", [node, const(1.0)]) : node end |
.program(model) ⇒ PB::Program
Turn model into the Program message the service reads.
58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/quicopt/wire.rb', line 58 def program(model) PB::Program.new( params: model.tables.sort_by(&:name).map { |t| param_table(t) }, vars: model.variables.map { |v| var_decl(v) }, objective: expression(model.objective), sense: model.sense, constraints: model.constraints.map { |c| constraint(c) }, structures: (model.permutations + model.partitions).sort_by(&:name).map { |p| structure_decl(p) }, blocks: model.blocks.sort_by(&:name).map { |b| block_decl(b) } ) end |
.scaled(coef, expr) ⇒ Object
coef · expr, dropping a unit coefficient.
131 132 133 |
# File 'lib/quicopt/wire.rb', line 131 def scaled(coef, expr) coef == 1.0 ? expr : apply("*", [const(coef), expr]) end |
.structure(name) ⇒ Object
A reference to a declared permutation, by name.
120 121 122 |
# File 'lib/quicopt/wire.rb', line 120 def structure(name) PB::Expression.new(structure: PB::StructRef.new(name: name)) end |
.structure_decl(structure) ⇒ PB::StructureDecl
A permutation or a partition → StructureDecl, its start (and a
permutation's precedences) counted from 1.
242 243 244 245 246 247 248 249 250 251 252 253 |
# File 'lib/quicopt/wire.rb', line 242 def structure_decl(structure) start = (structure.start || []).map { |k| k + 1 } if structure.is_a?(Partition) return PB::StructureDecl.new(name: structure.name, kind: "partition", size: structure.n_items, start: start, fixed: false, parts: structure.n_parts) end PB::StructureDecl.new( name: structure.name, kind: "permutation", size: structure.n_items, start: start, fixed: false, precede: structure.precedences.map { |a, b| PB::Precedence.new(before: a + 1, after: b + 1) } ) end |
.sum(terms) ⇒ Object
Fold terms into an n-ary sum, dropping additive-zero constants. No terms is the zero constant; one term is itself.
137 138 139 140 141 142 143 |
# File 'lib/quicopt/wire.rb', line 137 def sum(terms) surviving = terms.reject { |t| t.node == :constant && t.constant.zero? } return const(0.0) if surviving.empty? return surviving.first if surviving.size == 1 apply("+", surviving) end |
.table(param, index) ⇒ Object
A read of the parameter table param at the given positions (counted
from 1, as on the wire).
126 127 128 |
# File 'lib/quicopt/wire.rb', line 126 def table(param, index) PB::Expression.new(table: PB::TableRef.new(param: param, index: index)) end |
.var(name) ⇒ Object
A reference to a flat (unindexed) variable. The Index message is sent
present-but-empty, which is what the service expects.
110 111 112 |
# File 'lib/quicopt/wire.rb', line 110 def var(name) PB::Expression.new(var: PB::VarRef.new(name: name, index: PB::Index.new)) end |
.var_decl(variable) ⇒ PB::VarDecl
A variable declaration → VarDecl. Bounds are plain numbers (the message
can also take a bound from a parameter table, but that needs indexed
variables, which this DSL does not have); ±Inf passes straight through and
means "unbounded in that direction". A variable that may also be 0 goes
out with its domain, SEMICONTINUOUS or SEMIINTEGER.
287 288 289 290 291 292 293 |
# File 'lib/quicopt/wire.rb', line 287 def var_decl(variable) PB::VarDecl.new(name: variable.name, domain: variable.domain, lower: PB::Bound.new(scalar: number(variable.lower)), upper: PB::Bound.new(scalar: number(variable.upper)), start: number(variable.start)) end |