Class: Quicopt::Model
- Inherits:
-
Object
- Object
- Quicopt::Model
- Defined in:
- lib/quicopt/model.rb
Overview
An optimization model: variables, one objective, and constraint rows.
m = Quicopt::Model.new
x = m.int_var(0, 4, "x")
y = m.bin_var("y")
m.maximize(3 * x + 2 * y)
m.add(x + y <= 5)
You build the model; to_program / encode turn it into the message the
service reads, and Quicopt::Client#solve sends it.
Instance Attribute Summary collapse
-
#blocks ⇒ Array<Block>
readonly
The declared blocks, in declaration order.
-
#constraints ⇒ Array<Constraint>
readonly
The constraint rows, in insertion order.
-
#objective ⇒ Expression
readonly
The objective (the zero expression until set).
-
#partitions ⇒ Array<Partition>
readonly
The declared partitions, in declaration order.
-
#permutations ⇒ Array<Permutation>
readonly
The declared permutations, in declaration order.
-
#sense ⇒ String
readonly
"min"or"max". -
#tables ⇒ Array<LookupTable>
readonly
The declared lookup tables, in declaration order.
-
#variables ⇒ Array<Variable>
readonly
The declared variables, in declaration order.
Instance Method Summary collapse
-
#add(constraint) ⇒ Constraint
Add a constraint row built by
<=,>=or==. -
#bin_var(name = nil, start: nil) ⇒ Variable
Declare a binary variable.
-
#block(name, vars) ⇒ Block
Declare the variables of one subproblem as a block: one customer's supply, one project's staffing.
-
#encode ⇒ String
Turn this model into the message the service reads and encode it.
-
#initialize ⇒ Model
constructor
A new instance of Model.
-
#inspect ⇒ String
A one-line summary: variable and row counts.
-
#int_var(lower = nil, upper = nil, name = nil, start: nil, semi: false) ⇒ Variable
Declare an integer variable.
-
#intern(key) ⇒ Object
private
The term stored under
key, built by the block on first use, so that equal terms are one object. -
#lookup_table(name, values) ⇒ LookupTable
Declare a table of data that can be indexed by decisions.
-
#maximize(expr) ⇒ Expression
Maximize
expr. -
#minimize(expr) ⇒ Expression
Minimize
expr. -
#next_order ⇒ Object
private
The
orderof the next read of a permutation, partition or lookup table, or of a term that switches: after every variable, then in the order they were first made. -
#num_var(lower = nil, upper = nil, name = nil, start: nil, semi: false) ⇒ Variable
Declare a continuous variable.
-
#partition(name, n_items, n_parts, start: nil) ⇒ Partition
Declare an assignment of
n_itemsitems ton_partsparts as a decision: which team each person joins, which truck each parcel rides in. -
#permutation(name, n_items, start: nil) ⇒ Permutation
Declare an ordering as a decision:
n_itemsitems placed in as many slots, one item per slot, both numbered from 0. -
#to_program ⇒ Quicopt::Modeler::V1::Program
Turn this model into the structured message the service reads.
-
#to_s ⇒ Object
Renders the whole model: objective, rows, then declarations.
-
#variable(name) ⇒ Variable?
Look a variable up by name.
Constructor Details
#initialize ⇒ Model
Returns a new instance of Model.
545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
# File 'lib/quicopt/model.rb', line 545 def initialize @variables = [] @by_name = {} @permutations = [] @partitions = [] @tables = [] @blocks = [] @structured = {} @interned = {} @reads = 0 @constraints = [] @objective = Expression.new @sense = "min" end |
Instance Attribute Details
#blocks ⇒ Array<Block> (readonly)
Returns the declared blocks, in declaration order.
543 544 545 |
# File 'lib/quicopt/model.rb', line 543 def blocks @blocks end |
#constraints ⇒ Array<Constraint> (readonly)
Returns the constraint rows, in insertion order.
531 532 533 |
# File 'lib/quicopt/model.rb', line 531 def constraints @constraints end |
#objective ⇒ Expression (readonly)
Returns the objective (the zero expression until set).
533 534 535 |
# File 'lib/quicopt/model.rb', line 533 def objective @objective end |
#partitions ⇒ Array<Partition> (readonly)
Returns the declared partitions, in declaration order.
541 542 543 |
# File 'lib/quicopt/model.rb', line 541 def partitions @partitions end |
#permutations ⇒ Array<Permutation> (readonly)
Returns the declared permutations, in declaration order.
537 538 539 |
# File 'lib/quicopt/model.rb', line 537 def permutations @permutations end |
#sense ⇒ String (readonly)
Returns "min" or "max".
535 536 537 |
# File 'lib/quicopt/model.rb', line 535 def sense @sense end |
#tables ⇒ Array<LookupTable> (readonly)
Returns the declared lookup tables, in declaration order.
539 540 541 |
# File 'lib/quicopt/model.rb', line 539 def tables @tables end |
#variables ⇒ Array<Variable> (readonly)
Returns the declared variables, in declaration order.
529 530 531 |
# File 'lib/quicopt/model.rb', line 529 def variables @variables end |
Instance Method Details
#add(constraint) ⇒ Constraint
Add a constraint row built by <=, >= or ==.
837 838 839 840 841 842 843 844 845 846 847 848 849 850 |
# File 'lib/quicopt/model.rb', line 837 def add(constraint) unless constraint.is_a?(Constraint) raise TypeError, "add expects a constraint such as `x + y <= 5`, got #{constraint.class}" end unless Constraint::ROWS.include?(constraint.sense) raise ArgumentError, "#{constraint} is not a constraint Quicopt can hold: use <=, >= or ==, " \ "or turn the comparison into a 0/1 expression with Quicopt.holds" end check_ownership(constraint.expr) @constraints << constraint constraint end |
#bin_var(name = nil, start: nil) ⇒ Variable
Declare a binary variable.
608 609 610 |
# File 'lib/quicopt/model.rb', line 608 def bin_var(name = nil, start: nil) declare(:BINARY, 0.0, 1.0, name, start) end |
#block(name, vars) ⇒ Block
Declare the variables of one subproblem as a block: one customer's supply, one project's staffing. The constraints over a block's own variables alone, such as that customer's demand, hold in every solution the service returns. A block changes nothing else about the model: the same solutions are feasible and the objective is the same.
supply = (0...3).map { |w| m.int_var(10, 25, "ship_#{w}", semi: true) }
m.block("customer", supply)
737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 |
# File 'lib/quicopt/model.rb', line 737 def block(name, vars) raise TypeError, "a block's name is a String or a Symbol, got #{name.inspect}" unless name.is_a?(String) || name.is_a?(Symbol) nm = name.to_s raise ArgumentError, "a block's name cannot be empty" if nm.empty? raise ArgumentError, "there is already a block named #{nm.inspect}" if @blocks.any? { |b| b.name == nm } members = Array(vars) raise ArgumentError, "block #{nm.inspect} has no variables" if members.empty? members.each_with_index do |v, k| raise TypeError, "block #{nm.inspect}: a member is a variable, got #{v.class}" unless v.is_a?(Variable) raise ArgumentError, "variable #{v.name.inspect} belongs to a different Quicopt::Model" unless v.model.equal?(self) raise ArgumentError, "variable #{v.name.inspect} is listed twice in block #{nm.inspect}" if members[0...k].any? { |u| u.equal?(v) } if (owner = @blocks.find { |b| b.variables.any? { |u| u.equal?(v) } }) raise ArgumentError, "variable #{v.name.inspect} is already in block #{owner.name.inspect}" end unless v.lower.finite? && v.upper.finite? raise ArgumentError, "variable #{v.name.inspect} has bounds [#{v.lower}, #{v.upper}]; " \ "a block's variables need finite bounds" end end block = Block.new(nm, members.dup.freeze) @blocks << block block end |
#encode ⇒ String
Turn this model into the message the service reads and encode it.
862 863 864 |
# File 'lib/quicopt/model.rb', line 862 def encode Wire.encode(self) end |
#inspect ⇒ String
Returns a one-line summary: variable and row counts.
888 889 890 |
# File 'lib/quicopt/model.rb', line 888 def inspect "#<Quicopt::Model #{@variables.size} vars, #{@constraints.size} constraints>" end |
#int_var(lower = nil, upper = nil, name = nil, start: nil, semi: false) ⇒ Variable
Declare an integer variable.
Bounds of exactly [0, 1] declare a :BINARY variable rather than an
integer one — the same rule the Python and Julia clients apply, so the same
model means the same thing whichever language wrote it.
With semi: true the variable is 0 or a whole number within its bounds,
never in between: a delivery of either nothing or 10 to 25 units. Such a
variable is called semi-integer. Its bounds must then be finite.
ship = m.int_var(10, 25, "ship", semi: true) # none, or 10 to 25 units
m.minimize(2 * ship + 30 * ship.active) # 2 per unit, 30 per delivery
595 596 597 598 599 600 601 602 603 |
# File 'lib/quicopt/model.rb', line 595 def int_var(lower = nil, upper = nil, name = nil, start: nil, semi: false) lo = coerce_bound(lower, -Float::INFINITY) hi = coerce_bound(upper, Float::INFINITY) domain = if semi then :SEMIINTEGER elsif lo.zero? && hi == 1.0 then :BINARY else :INTEGER end declare(domain, lo, hi, name, start) end |
#intern(key) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The term stored under key, built by the block on first use, so that
equal terms are one object.
811 812 813 |
# File 'lib/quicopt/model.rb', line 811 def intern(key) @interned[key] ||= yield end |
#lookup_table(name, values) ⇒ LookupTable
Declare a table of data that can be indexed by decisions. A model sometimes needs a number from a table at a position that is not known yet: the distance between the first and the second stop of a route whose ordering the service is still choosing, or the price of an option an integer variable picks. A Ruby array cannot be indexed this way; the table returned here can.
price = m.lookup_table("price", [3.0, 1.0, 4.0, 1.5])
choice = m.int_var(0, 3, "choice")
m.minimize(price[choice]) # picks option 1, at 1.0
Indexing with whole numbers only gives the entry itself; indexing with an
expression, such as Permutation#item_at or an integer variable, gives an
expression whose value is the entry at the positions the service chooses.
The whole table is sent with the model, so a very large table makes the request large.
790 791 792 793 794 795 796 |
# File 'lib/quicopt/model.rb', line 790 def lookup_table(name, values) nm = claim(name) data, shape = table_data(nm, values) table = LookupTable.new(self, nm, data, shape) @tables << table @structured[nm] = table end |
#maximize(expr) ⇒ Expression
Maximize expr.
825 826 827 |
# File 'lib/quicopt/model.rb', line 825 def maximize(expr) set_objective(expr, "max") end |
#minimize(expr) ⇒ Expression
Minimize expr.
818 819 820 |
# File 'lib/quicopt/model.rb', line 818 def minimize(expr) set_objective(expr, "min") end |
#next_order ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
The order of the next read of a permutation, partition or lookup table,
or of a term that switches: after every variable, then in the order they
were first made.
803 804 805 |
# File 'lib/quicopt/model.rb', line 803 def next_order [1, @reads += 1] end |
#num_var(lower = nil, upper = nil, name = nil, start: nil, semi: false) ⇒ Variable
Declare a continuous variable.
With semi: true the variable is 0 or within its bounds, never in between:
a shipment of either nothing or 2.5 to 8 tonnes. Such a variable is called
semicontinuous. Its bounds must then be finite, and when 0 lies within them
it is an ordinary variable.
575 576 577 |
# File 'lib/quicopt/model.rb', line 575 def num_var(lower = nil, upper = nil, name = nil, start: nil, semi: false) declare(semi ? :SEMICONTINUOUS : :CONTINUOUS, lower, upper, name, start) end |
#partition(name, n_items, n_parts, start: nil) ⇒ Partition
Declare an assignment of n_items items to n_parts parts as a decision:
which team each person joins, which truck each parcel rides in. Items and
parts are both numbered from 0, and a part may stay empty.
Six players of strengths 7, 5, 4, 3, 2 and 1 form a team of four and a team of two, as evenly matched as possible:
strength = [7, 5, 4, 3, 2, 1] # of players 0 to 5
m = Quicopt::Model.new
teams = m.partition("teams", 6, 2) # 6 players into teams 0 and 1
total = (0...2).map { |t| (0...6).sum { |p| strength[p] * Quicopt.holds(teams.part_of(p) == t) } }
m.add(teams.size_of(0) == 4) # team 0 has four players
m.minimize(Quicopt.max(total[0], total[1]))
result = Quicopt::Client.new.solve(m)
result.objective # 11.0
result.structures["teams"].part_of # [1, 0, 1, 0, 0, 0]: players 0 and 2 form team 1
699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 |
# File 'lib/quicopt/model.rb', line 699 def partition(name, n_items, n_parts, start: nil) unless n_items.is_a?(Integer) raise TypeError, "a partition's number of items is a whole number, got #{n_items.inspect}" end unless n_parts.is_a?(Integer) raise TypeError, "a partition's number of parts is a whole number, got #{n_parts.inspect}" end raise ArgumentError, "a partition has at least 1 item, got #{n_items}" if n_items < 1 raise ArgumentError, "a partition has at least 2 parts, got #{n_parts}" if n_parts < 2 nm = claim(name) unless start.nil? || (start.is_a?(Array) && start.size == n_items && start.all? { |b| b.is_a?(Integer) && b >= 0 && b < n_parts }) raise ArgumentError, "partition #{nm.inspect}: start gives each of the #{n_items} items a part " \ "from 0 to #{n_parts - 1}" end partition = Partition.new(self, nm, n_items, n_parts, start&.dup) @partitions << partition @structured[nm] = partition end |
#permutation(name, n_items, start: nil) ⇒ Permutation
Declare an ordering as a decision: n_items items placed in as many
slots, one item per slot, both numbered from 0. For a route the items are
the stops and the slots are the visits, so slot 0 holds the first stop
visited; for a machine the items are the jobs and the slots the positions
in its queue.
Five stops along a road; find the shortest route through all of them that visits stop 3 before stop 0:
position = [0, 3, 1, 4, 2] # km along the road, stops 0 to 4
m = Quicopt::Model.new
dist = m.lookup_table("dist", position.map { |a| position.map { |b| (a - b).abs } })
route = m.permutation("route", 5)
route.precede(3, 0) # stop 3 is visited before stop 0
m.minimize((0...4).sum { |k| dist[route.item_at(k), route.item_at(k + 1)] })
result = Quicopt::Client.new.solve(m)
result.objective # 4.0
result.structures["route"].item_at # [3, 1, 4, 2, 0]: the stops in the order visited
A model with a permutation is solved by a search, so its result has the
status "heuristic".
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 |
# File 'lib/quicopt/model.rb', line 653 def permutation(name, n_items, start: nil) unless n_items.is_a?(Integer) raise TypeError, "a permutation's number of items is a whole number, got #{n_items.inspect}" end raise ArgumentError, "a permutation has at least 2 items, got #{n_items}" if n_items < 2 nm = claim(name) unless start.nil? || (start.is_a?(Array) && start.all?(Integer) && start.sort == (0...n_items).to_a) raise ArgumentError, "permutation #{nm.inspect}: start must hold each of 0 to #{n_items - 1} once " \ "(start[i] is the slot item i starts in)" end permutation = Permutation.new(self, nm, n_items, start&.dup) @permutations << permutation @structured[nm] = permutation end |
#to_program ⇒ Quicopt::Modeler::V1::Program
Turn this model into the structured message the service reads.
855 856 857 |
# File 'lib/quicopt/model.rb', line 855 def to_program Wire.program(self) end |
#to_s ⇒ Object
Renders the whole model: objective, rows, then declarations.
867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 |
# File 'lib/quicopt/model.rb', line 867 def to_s lines = ["#{@sense} #{@objective}"] lines << "subject to" unless @constraints.empty? @constraints.each { |c| lines << " #{c}" } @variables.each do |v| range = "[#{Format.number(v.lower)}, #{Format.number(v.upper)}]" range = "{0} ∪ #{range}" if %i[SEMICONTINUOUS SEMIINTEGER].include?(v.domain) lines << " #{v.name} in #{v.domain} #{range}" end @permutations.each do |p| rules = p.precedences.map { |a, b| "#{a} before #{b}" } rules = rules.empty? ? "" : ", with #{rules.join(', ')}" lines << " #{p.name} is an ordering of items 0 to #{p.n_items - 1}#{rules}" end @partitions.each { |p| lines << " #{p.name} puts items 0 to #{p.n_items - 1} in parts 0 to #{p.n_parts - 1}" } @tables.each { |t| lines << " #{t.name} is a #{t.shape.join(' × ')} lookup table" } @blocks.each { |b| lines << " #{b.name} is a block of #{b.variables.join(', ')}" } lines.join("\n") end |
#variable(name) ⇒ Variable?
Look a variable up by name.
615 616 617 |
# File 'lib/quicopt/model.rb', line 615 def variable(name) @by_name[name.to_s] end |