Module: Quicopt::Wire

Defined in:
lib/quicopt/wire.rb

Overview

Node constructors over the generated codec, and the lowering from the DSL's normalised polynomials into the IR's expression trees.

Constant Summary collapse

PB =

The generated wire schema — Program, Expression, Constraint, … — as the client's IR. Aliased for brevity throughout the codec.

Quicopt::Modeler::V1

Class Method Summary collapse

Class Method Details

.apply(op, args) ⇒ Object

An operator application: op applied to child expressions.



95
96
97
# File 'lib/quicopt/wire.rb', line 95

def apply(op, args)
  PB::Expression.new(apply: PB::Apply.new(op: op, args: args))
end

.const(value) ⇒ Object

A constant expression node. Always a set oneof case, so Const(0.0) survives the round-trip rather than vanishing as a proto3 default.



72
73
74
# File 'lib/quicopt/wire.rb', line 72

def const(value)
  PB::Expression.new(constant: number(value))
end

.constraint(con) ⇒ PB::Constraint

One DSL constraint row → an IR function-in-set row.

Parameters:

Returns:

  • (PB::Constraint)


134
135
136
137
138
139
140
141
142
143
# File 'lib/quicopt/wire.rb', line 134

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 wire bytes back into a Program — the inverse of encode, and the only sound way to compare two encodings.

Parameters:

  • bytes (String)

Returns:

  • (PB::Program)


64
65
66
# File 'lib/quicopt/wire.rb', line 64

def decode(bytes)
  PB::Program.decode(bytes)
end

.encode(model) ⇒ String

Serialize a model (or an already-lowered Program) to wire bytes.

Parameters:

Returns:

  • (String)

    binary protobuf bytes



55
56
57
# File 'lib/quicopt/wire.rb', line 55

def encode(model)
  PB::Program.encode(model.is_a?(PB::Program) ? model : program(model))
end

.expression(expr) ⇒ PB::Expression

A normalised DSL expression → an IR expression tree: the constant (when nonzero), then the linear terms, then the quadratic ones — the term order the sibling front-ends emit.

Parameters:

Returns:

  • (PB::Expression)


122
123
124
125
126
127
128
# File 'lib/quicopt/wire.rb', line 122

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

Normalise negative zero on its way to the wire.

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 sibling clients emit 0.0 there, so collapse the sign here rather than shipping bytes that differ from theirs by a sign bit.



83
84
85
86
# File 'lib/quicopt/wire.rb', line 83

def number(value)
  float = value.to_f
  float.zero? ? 0.0 : float
end

.program(model) ⇒ PB::Program

Lower model to the wire IR.

Parameters:

Returns:

  • (PB::Program)


42
43
44
45
46
47
48
49
# File 'lib/quicopt/wire.rb', line 42

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.



100
101
102
# File 'lib/quicopt/wire.rb', line 100

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.



106
107
108
109
110
111
112
# File 'lib/quicopt/wire.rb', line 106

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 emitted present-but-empty, matching the reference codec.



90
91
92
# File 'lib/quicopt/wire.rb', line 90

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 scalars (a Param-table bound needs indexed variables, which the DSL does not have); ±Inf passes straight through as a free direction.

Parameters:

Returns:

  • (PB::VarDecl)


151
152
153
154
155
156
157
# File 'lib/quicopt/wire.rb', line 151

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