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. -
.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.
-
.number(value) ⇒ Object
Flatten negative zero before encoding.
-
.program(model) ⇒ PB::Program
Turn
modelinto theProgrammessage the service reads. -
.scaled(coef, expr) ⇒ Object
coef · expr, dropping a unit coefficient. -
.sum(terms) ⇒ Object
Fold terms into an n-ary sum, dropping additive-zero constants.
-
.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.
96 97 98 |
# File 'lib/quicopt/wire.rb', line 96 def apply(op, args) PB::Expression.new(apply: PB::Apply.new(op: op, args: args)) 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.
73 74 75 |
# File 'lib/quicopt/wire.rb', line 73 def const(value) PB::Expression.new(constant: number(value)) end |
.constraint(con) ⇒ PB::Constraint
One DSL constraint row → one constraint in the message.
135 136 137 138 139 140 141 142 143 144 |
# File 'lib/quicopt/wire.rb', line 135 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.
65 66 67 |
# File 'lib/quicopt/wire.rb', line 65 def decode(bytes) PB::Program.decode(bytes) end |
.encode(model) ⇒ String
Encode a model (or an already-built Program) to bytes.
56 57 58 |
# File 'lib/quicopt/wire.rb', line 56 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.
123 124 125 126 127 128 129 |
# File 'lib/quicopt/wire.rb', line 123 def expression(expr) terms = [] terms << const(expr.constant) unless expr.constant.zero? expr.linear.each { |v, c| terms << scaled(c, var(v.name)) } expr.quadratic.each { |(a, b), c| terms << scaled(c, apply("*", [var(a.name), var(b.name)])) } sum(terms) 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.
84 85 86 87 |
# File 'lib/quicopt/wire.rb', line 84 def number(value) float = value.to_f float.zero? ? 0.0 : float end |
.program(model) ⇒ PB::Program
Turn model into the Program message the service reads.
43 44 45 46 47 48 49 50 |
# File 'lib/quicopt/wire.rb', line 43 def program(model) PB::Program.new( vars: model.variables.map { |v| var_decl(v) }, objective: expression(model.objective), sense: model.sense, constraints: model.constraints.map { |c| constraint(c) } ) end |
.scaled(coef, expr) ⇒ Object
coef · expr, dropping a unit coefficient.
101 102 103 |
# File 'lib/quicopt/wire.rb', line 101 def scaled(coef, expr) coef == 1.0 ? expr : apply("*", [const(coef), expr]) 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.
107 108 109 110 111 112 113 |
# File 'lib/quicopt/wire.rb', line 107 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 |
.var(name) ⇒ Object
A reference to a flat (unindexed) variable. The Index message is sent
present-but-empty, which is what the service expects.
91 92 93 |
# File 'lib/quicopt/wire.rb', line 91 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".
153 154 155 156 157 158 159 |
# File 'lib/quicopt/wire.rb', line 153 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 |