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.
- checksums.yaml +4 -4
- data/.yardopts +2 -1
- data/README.md +262 -33
- data/lib/quicopt/client.rb +156 -49
- data/lib/quicopt/indicators.rb +235 -0
- data/lib/quicopt/model.rb +442 -57
- data/lib/quicopt/modeler/v1/program_pb.rb +10 -1
- data/lib/quicopt/structured.rb +437 -0
- data/lib/quicopt/version.rb +1 -1
- data/lib/quicopt/wire.rb +171 -36
- data/lib/quicopt.rb +7 -6
- data/proto/quicopt/modeler/v1/program.proto +160 -6
- metadata +12 -7
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
require 'google/protobuf'
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
descriptor_data = "\n quicopt/modeler/v1/program.proto\x12\x12quicopt.modeler.v1\"2\n\tIndexElem\x12\r\n\x03int\x18\x01 \x01(\x03H\x00\x12\x0e\n\x04name\x18\x02 \x01(\tH\x00\x42\x06\n\x04kind\"5\n\x05Index\x12,\n\x05\x65lems\x18\x01 \x03(\x0b\x32\x1d.quicopt.modeler.v1.IndexElem\"\
|
|
11
|
+
descriptor_data = "\n quicopt/modeler/v1/program.proto\x12\x12quicopt.modeler.v1\"2\n\tIndexElem\x12\r\n\x03int\x18\x01 \x01(\x03H\x00\x12\x0e\n\x04name\x18\x02 \x01(\tH\x00\x42\x06\n\x04kind\"5\n\x05Index\x12,\n\x05\x65lems\x18\x01 \x03(\x0b\x32\x1d.quicopt.modeler.v1.IndexElem\"\xf0\x02\n\nExpression\x12\x12\n\x08\x63onstant\x18\x01 \x01(\x01H\x00\x12-\n\x05param\x18\x02 \x01(\x0b\x32\x1c.quicopt.modeler.v1.ParamRefH\x00\x12)\n\x03var\x18\x03 \x01(\x0b\x32\x1a.quicopt.modeler.v1.VarRefH\x00\x12*\n\x05\x61pply\x18\x04 \x01(\x0b\x32\x19.quicopt.modeler.v1.ApplyH\x00\x12,\n\x06reduce\x18\x05 \x01(\x0b\x32\x1a.quicopt.modeler.v1.ReduceH\x00\x12/\n\x06source\x18\x06 \x01(\x0b\x32\x1d.quicopt.modeler.v1.SourceRefH\x00\x12\x32\n\tstructure\x18\x07 \x01(\x0b\x32\x1d.quicopt.modeler.v1.StructRefH\x00\x12-\n\x05table\x18\x08 \x01(\x0b\x32\x1c.quicopt.modeler.v1.TableRefH\x00\x42\x06\n\x04node\"\x19\n\tSourceRef\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x19\n\tStructRef\x12\x0c\n\x04name\x18\x01 \x01(\t\"H\n\x08TableRef\x12\r\n\x05param\x18\x01 \x01(\t\x12-\n\x05index\x18\x02 \x03(\x0b\x32\x1e.quicopt.modeler.v1.Expression\"B\n\x08ParamRef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12(\n\x05index\x18\x02 \x01(\x0b\x32\x19.quicopt.modeler.v1.Index\"@\n\x06VarRef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12(\n\x05index\x18\x02 \x01(\x0b\x32\x19.quicopt.modeler.v1.Index\"A\n\x05\x41pply\x12\n\n\x02op\x18\x01 \x01(\t\x12,\n\x04\x61rgs\x18\x02 \x03(\x0b\x32\x1e.quicopt.modeler.v1.Expression\"?\n\x06SetRef\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\'\n\x04\x61rgs\x18\x02 \x01(\x0b\x32\x19.quicopt.modeler.v1.Index\"\xa7\x01\n\x06Reduce\x12\n\n\x02op\x18\x01 \x01(\t\x12\x0b\n\x03idx\x18\x02 \x01(\t\x12(\n\x04over\x18\x03 \x01(\x0b\x32\x1a.quicopt.modeler.v1.SetRef\x12,\n\x04\x62ody\x18\x04 \x01(\x0b\x32\x1e.quicopt.modeler.v1.Expression\x12,\n\x04\x63ond\x18\x05 \x01(\x0b\x32\x1e.quicopt.modeler.v1.Expression\"\x06\n\x04Zero\"\x08\n\x06Nonneg\"_\n\tIndicator\x12\'\n\x03\x62in\x18\x01 \x01(\x0b\x32\x1a.quicopt.modeler.v1.VarRef\x12)\n\x05inner\x18\x02 \x01(\x0b\x32\x1a.quicopt.modeler.v1.ConSet\"\x9c\x01\n\x06\x43onSet\x12(\n\x04zero\x18\x01 \x01(\x0b\x32\x18.quicopt.modeler.v1.ZeroH\x00\x12,\n\x06nonneg\x18\x02 \x01(\x0b\x32\x1a.quicopt.modeler.v1.NonnegH\x00\x12\x32\n\tindicator\x18\x03 \x01(\x0b\x32\x1d.quicopt.modeler.v1.IndicatorH\x00\x42\x06\n\x04kind\"D\n\x0cQuantBinding\x12\x0b\n\x03idx\x18\x01 \x01(\t\x12\'\n\x03set\x18\x02 \x01(\x0b\x32\x1a.quicopt.modeler.v1.SetRef\"\x90\x01\n\nConstraint\x12)\n\x01\x66\x18\x01 \x01(\x0b\x32\x1e.quicopt.modeler.v1.Expression\x12\'\n\x03set\x18\x02 \x01(\x0b\x32\x1a.quicopt.modeler.v1.ConSet\x12.\n\x04over\x18\x03 \x03(\x0b\x32 .quicopt.modeler.v1.QuantBinding\"2\n\x05\x42ound\x12\x10\n\x06scalar\x18\x01 \x01(\x01H\x00\x12\x0f\n\x05param\x18\x02 \x01(\tH\x00\x42\x06\n\x04kind\"\xb4\x01\n\x07VarDecl\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x61xes\x18\x02 \x03(\t\x12*\n\x06\x64omain\x18\x03 \x01(\x0e\x32\x1a.quicopt.modeler.v1.Domain\x12(\n\x05lower\x18\x04 \x01(\x0b\x32\x19.quicopt.modeler.v1.Bound\x12(\n\x05upper\x18\x05 \x01(\x0b\x32\x19.quicopt.modeler.v1.Bound\x12\r\n\x05start\x18\x06 \x01(\x01\"I\n\x08IndexSet\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\x08\x65lements\x18\x02 \x03(\x0b\x32\x1d.quicopt.modeler.v1.IndexElem\"g\n\x0fIndexedSetFibre\x12&\n\x03key\x18\x01 \x01(\x0b\x32\x19.quicopt.modeler.v1.Index\x12,\n\x05value\x18\x02 \x03(\x0b\x32\x1d.quicopt.modeler.v1.IndexElem\"O\n\nIndexedSet\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x33\n\x06\x66ibres\x18\x02 \x03(\x0b\x32#.quicopt.modeler.v1.IndexedSetFibre\"C\n\nParamEntry\x12&\n\x03key\x18\x01 \x01(\x0b\x32\x19.quicopt.modeler.v1.Index\x12\r\n\x05value\x18\x02 \x01(\x01\"K\n\nParamTable\x12\x0c\n\x04name\x18\x01 \x01(\t\x12/\n\x07\x65ntries\x18\x02 \x03(\x0b\x32\x1e.quicopt.modeler.v1.ParamEntry\"P\n\x08\x46ixEntry\x12\x0b\n\x03var\x18\x01 \x01(\t\x12(\n\x05index\x18\x02 \x01(\x0b\x32\x19.quicopt.modeler.v1.Index\x12\r\n\x05value\x18\x03 \x01(\x01\"J\n\nParametric\x12\x0c\n\x04head\x18\x01 \x01(\t\x12.\n\x06params\x18\x02 \x03(\x0b\x32\x1e.quicopt.modeler.v1.Expression\"\x19\n\tEmpirical\x12\x0c\n\x04\x64\x61ta\x18\x01 \x03(\x01\"\x8c\x01\n\nSourceDecl\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x34\n\nparametric\x18\x02 \x01(\x0b\x32\x1e.quicopt.modeler.v1.ParametricH\x00\x12\x32\n\tempirical\x18\x03 \x01(\x0b\x32\x1d.quicopt.modeler.v1.EmpiricalH\x00\x42\x06\n\x04kind\"+\n\nPrecedence\x12\x0e\n\x06\x62\x65\x66ore\x18\x01 \x01(\x04\x12\r\n\x05\x61\x66ter\x18\x02 \x01(\x04\"\x97\x01\n\rStructureDecl\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04kind\x18\x02 \x01(\t\x12\x0c\n\x04size\x18\x03 \x01(\x04\x12\r\n\x05start\x18\x04 \x03(\x03\x12\r\n\x05\x66ixed\x18\x05 \x01(\x08\x12/\n\x07precede\x18\x06 \x03(\x0b\x32\x1e.quicopt.modeler.v1.Precedence\x12\r\n\x05parts\x18\x07 \x01(\x04\"F\n\tBlockDecl\x12\x0c\n\x04name\x18\x01 \x01(\t\x12+\n\x07members\x18\x02 \x03(\x0b\x32\x1a.quicopt.modeler.v1.VarRef\"\xa9\x04\n\x07Program\x12*\n\x04sets\x18\x01 \x03(\x0b\x32\x1c.quicopt.modeler.v1.IndexSet\x12\x34\n\x0cindexed_sets\x18\x02 \x03(\x0b\x32\x1e.quicopt.modeler.v1.IndexedSet\x12.\n\x06params\x18\x03 \x03(\x0b\x32\x1e.quicopt.modeler.v1.ParamTable\x12)\n\x04vars\x18\x04 \x03(\x0b\x32\x1b.quicopt.modeler.v1.VarDecl\x12\x31\n\tobjective\x18\x05 \x01(\x0b\x32\x1e.quicopt.modeler.v1.Expression\x12\r\n\x05sense\x18\x06 \x01(\t\x12\x33\n\x0b\x63onstraints\x18\x07 \x03(\x0b\x32\x1e.quicopt.modeler.v1.Constraint\x12)\n\x03\x66ix\x18\x08 \x03(\x0b\x32\x1c.quicopt.modeler.v1.FixEntry\x12\x11\n\tscenarios\x18\t \x01(\x04\x12\x15\n\rscenario_seed\x18\n \x01(\x04\x12/\n\x07sources\x18\x0b \x03(\x0b\x32\x1e.quicopt.modeler.v1.SourceDecl\x12\x35\n\nstructures\x18\x0c \x03(\x0b\x32!.quicopt.modeler.v1.StructureDecl\x12-\n\x06\x62locks\x18\r \x03(\x0b\x32\x1d.quicopt.modeler.v1.BlockDecl\";\n\tParamData\x12.\n\x06params\x18\x01 \x03(\x0b\x32\x1e.quicopt.modeler.v1.ParamTable\"0\n\x05OpDef\x12\n\n\x02op\x18\x01 \x01(\t\x12\r\n\x05\x61rity\x18\x02 \x01(\x05\x12\x0c\n\x04tags\x18\x03 \x03(\t\"B\n\x07\x43\x61talog\x12\x0f\n\x07version\x18\x01 \x01(\r\x12&\n\x03ops\x18\x02 \x03(\x0b\x32\x19.quicopt.modeler.v1.OpDef*n\n\x06\x44omain\x12\x16\n\x12\x44OMAIN_UNSPECIFIED\x10\x00\x12\x0e\n\nCONTINUOUS\x10\x01\x12\x0b\n\x07INTEGER\x10\x02\x12\n\n\x06\x42INARY\x10\x03\x12\x12\n\x0eSEMICONTINUOUS\x10\x04\x12\x0f\n\x0bSEMIINTEGER\x10\x05\x62\x06proto3"
|
|
12
12
|
|
|
13
13
|
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
14
14
|
pool.add_serialized_file(descriptor_data)
|
|
@@ -19,6 +19,9 @@ module Quicopt
|
|
|
19
19
|
IndexElem = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.IndexElem").msgclass
|
|
20
20
|
Index = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.Index").msgclass
|
|
21
21
|
Expression = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.Expression").msgclass
|
|
22
|
+
SourceRef = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.SourceRef").msgclass
|
|
23
|
+
StructRef = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.StructRef").msgclass
|
|
24
|
+
TableRef = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.TableRef").msgclass
|
|
22
25
|
ParamRef = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.ParamRef").msgclass
|
|
23
26
|
VarRef = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.VarRef").msgclass
|
|
24
27
|
Apply = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.Apply").msgclass
|
|
@@ -38,6 +41,12 @@ module Quicopt
|
|
|
38
41
|
ParamEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.ParamEntry").msgclass
|
|
39
42
|
ParamTable = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.ParamTable").msgclass
|
|
40
43
|
FixEntry = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.FixEntry").msgclass
|
|
44
|
+
Parametric = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.Parametric").msgclass
|
|
45
|
+
Empirical = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.Empirical").msgclass
|
|
46
|
+
SourceDecl = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.SourceDecl").msgclass
|
|
47
|
+
Precedence = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.Precedence").msgclass
|
|
48
|
+
StructureDecl = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.StructureDecl").msgclass
|
|
49
|
+
BlockDecl = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.BlockDecl").msgclass
|
|
41
50
|
Program = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.Program").msgclass
|
|
42
51
|
ParamData = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.ParamData").msgclass
|
|
43
52
|
OpDef = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("quicopt.modeler.v1.OpDef").msgclass
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: (c) 2026 Tim Bode, PGI-12, Forschungszentrum Jülich
|
|
3
|
+
# frozen_string_literal: true
|
|
4
|
+
|
|
5
|
+
# quicopt/structured — orders as decisions (+Permutation+), assignments to
|
|
6
|
+
# groups as decisions (+Partition+) and data read at decided positions
|
|
7
|
+
# (+LookupTable+).
|
|
8
|
+
#
|
|
9
|
+
# All three are declared on a +Model+ (+Model#permutation+, +Model#partition+,
|
|
10
|
+
# +Model#lookup_table+) and enter its expressions as *reads*: +route.item_at(0)+
|
|
11
|
+
# is an +Expression+ whose one linear term is a +Placement+, +teams.part_of(2)+
|
|
12
|
+
# one whose term is a +PartRead+, +dist[i, j]+ one whose term is a +TableEntry+.
|
|
13
|
+
# A read takes a variable's place in the collected polynomial — it is a factor
|
|
14
|
+
# of degree 1 — so the degree check stays exact and the arithmetic needs no new
|
|
15
|
+
# cases.
|
|
16
|
+
#
|
|
17
|
+
# Reads are interned: asking twice for the same read returns the same object.
|
|
18
|
+
# Terms are hashed by identity (like +Variable+), so interning is what lets
|
|
19
|
+
# +route.item_at(0) - route.item_at(0)+ cancel and +x * dist[i, j]+ meet
|
|
20
|
+
# +dist[i, j] * x+ on one coefficient.
|
|
21
|
+
#
|
|
22
|
+
# The DSL counts items, slots, parts and table positions from 0, like Ruby; the
|
|
23
|
+
# service counts them from 1. A part's size is a count and is never shifted. The shift happens once, in +Quicopt::Wire+: a read stands for
|
|
24
|
+
# its 0-based value everywhere on this side, so +to_s+ prints what the user wrote.
|
|
25
|
+
|
|
26
|
+
module Quicopt
|
|
27
|
+
# Position checks shared by +Permutation+ and +LookupTable+.
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
module Positions
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
# +k+, checked to be an Integer in +0...extent+.
|
|
34
|
+
#
|
|
35
|
+
# @raise [TypeError] if +k+ is not an Integer
|
|
36
|
+
# @raise [ArgumentError] if it is out of range
|
|
37
|
+
def check(k, extent, what, owner)
|
|
38
|
+
raise TypeError, "#{owner}: a #{what} is a whole number, got #{k.inspect}" unless k.is_a?(Integer)
|
|
39
|
+
unless k >= 0 && k < extent
|
|
40
|
+
raise ArgumentError, "#{owner} has #{what}s 0 to #{extent - 1}, got #{k}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
k
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# A permutation declared by +Model#permutation+: +size+ items placed in +size+
|
|
48
|
+
# slots, one item per slot, both numbered from 0. Read it with +item_at+ and
|
|
49
|
+
# +slot_of+; constrain it with +precede+.
|
|
50
|
+
#
|
|
51
|
+
# route = m.permutation("route", 5)
|
|
52
|
+
# route.item_at(0) # the first stop visited
|
|
53
|
+
# route.slot_of(3) # when stop 3 is visited
|
|
54
|
+
# route.precede(3, 0) # stop 3 is visited before stop 0
|
|
55
|
+
class Permutation
|
|
56
|
+
# @return [Model] the model that declared this permutation
|
|
57
|
+
attr_reader :model
|
|
58
|
+
# @return [String] the permutation's name; its arrangement comes back keyed by it
|
|
59
|
+
attr_reader :name
|
|
60
|
+
# @return [Integer] the number of items, which is also the number of slots
|
|
61
|
+
attr_reader :size
|
|
62
|
+
# @return [Array<Integer>, nil] the slot each item starts in, or +nil+ for the
|
|
63
|
+
# default (item +i+ in slot +i+)
|
|
64
|
+
attr_reader :start
|
|
65
|
+
|
|
66
|
+
# Declared with +Model#permutation+, not built directly.
|
|
67
|
+
#
|
|
68
|
+
# @api private
|
|
69
|
+
def initialize(model, name, size, start)
|
|
70
|
+
@model = model
|
|
71
|
+
@name = name
|
|
72
|
+
@size = size
|
|
73
|
+
@start = start&.freeze
|
|
74
|
+
@precedences = []
|
|
75
|
+
@reads = {}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# The item in slot +slot+, an expression whose value the service chooses. For
|
|
79
|
+
# a route whose items are stops, +route.item_at(0)+ is the first stop visited.
|
|
80
|
+
# Index a +LookupTable+ with it to read data about that item.
|
|
81
|
+
#
|
|
82
|
+
# @param slot [Integer] from 0 to +size - 1+
|
|
83
|
+
# @return [Expression]
|
|
84
|
+
# @raise [TypeError, ArgumentError] if +slot+ is not one of the slots
|
|
85
|
+
def item_at(slot)
|
|
86
|
+
read(:item_at, Positions.check(slot, @size, "slot", owner))
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# The slot item +item+ is placed in, an expression whose value the service
|
|
90
|
+
# chooses. For a route whose items are stops, +route.slot_of(3)+ is when stop 3
|
|
91
|
+
# is visited.
|
|
92
|
+
#
|
|
93
|
+
# @param item [Integer] from 0 to +size - 1+
|
|
94
|
+
# @return [Expression]
|
|
95
|
+
# @raise [TypeError, ArgumentError] if +item+ is not one of the items
|
|
96
|
+
def slot_of(item)
|
|
97
|
+
read(:slot_of, Positions.check(item, @size, "item", owner))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Require item +before+ to be in an earlier slot than item +after+: a pickup
|
|
101
|
+
# before its delivery, or a job before the job that needs its output. The
|
|
102
|
+
# service only considers arrangements that meet every such requirement, so
|
|
103
|
+
# every solution meets it.
|
|
104
|
+
#
|
|
105
|
+
# @param before [Integer] an item
|
|
106
|
+
# @param after [Integer] another item
|
|
107
|
+
# @return [Permutation] +self+, so requirements can be chained
|
|
108
|
+
# @raise [ArgumentError] if the two items are the same, if the requirement
|
|
109
|
+
# contradicts those already given (1 before 2 and 2 before 1), or if the
|
|
110
|
+
# +start+ places +before+ after +after+
|
|
111
|
+
def precede(before, after)
|
|
112
|
+
a = Positions.check(before, @size, "item", owner)
|
|
113
|
+
b = Positions.check(after, @size, "item", owner)
|
|
114
|
+
raise ArgumentError, "an item cannot precede itself" if a == b
|
|
115
|
+
if @start && @start[a] > @start[b]
|
|
116
|
+
raise ArgumentError, "#{owner}: the start places item #{a} after item #{b}, which it must precede"
|
|
117
|
+
end
|
|
118
|
+
if Permutation.cycle?(@precedences + [[a, b]], @size)
|
|
119
|
+
raise ArgumentError, "requiring #{a} before #{b} contradicts the requirements already given for #{owner}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
@precedences << [a, b].freeze
|
|
123
|
+
self
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @return [Array<Array(Integer, Integer)>] the +[before, after]+ requirements,
|
|
127
|
+
# in the order given
|
|
128
|
+
def precedences
|
|
129
|
+
@precedences.dup.freeze
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @return [String] name and size
|
|
133
|
+
def inspect
|
|
134
|
+
"#<Quicopt::Permutation #{@name} (#{@size} items in #{@size} slots)>"
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Whether the +[before, after]+ requirements over items +0...n+ contradict
|
|
138
|
+
# each other: Kahn's algorithm leaves an item unplaced exactly when they
|
|
139
|
+
# contain a cycle.
|
|
140
|
+
#
|
|
141
|
+
# @api private
|
|
142
|
+
def self.cycle?(edges, n)
|
|
143
|
+
indegree = Array.new(n, 0)
|
|
144
|
+
edges.each { |_, b| indegree[b] += 1 }
|
|
145
|
+
ready = (0...n).select { |i| indegree[i].zero? }
|
|
146
|
+
placed = 0
|
|
147
|
+
until ready.empty?
|
|
148
|
+
x = ready.pop
|
|
149
|
+
placed += 1
|
|
150
|
+
edges.each do |a, b|
|
|
151
|
+
next unless a == x
|
|
152
|
+
|
|
153
|
+
indegree[b] -= 1
|
|
154
|
+
ready << b if indegree[b].zero?
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
placed < n
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def owner
|
|
163
|
+
"permutation #{@name.inspect}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def read(op, position)
|
|
167
|
+
placement = (@reads[[op, position]] ||= Placement.new(self, op, position, @model.next_order))
|
|
168
|
+
Expression.new(linear: { placement => 1.0 })
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# A partition declared by +Model#partition+: +size+ items in +parts+ parts,
|
|
173
|
+
# both numbered from 0. Read it with +part_of+ and +size_of+.
|
|
174
|
+
#
|
|
175
|
+
# teams = m.partition("teams", 6, 2)
|
|
176
|
+
# teams.part_of(4) # the team person 4 joins
|
|
177
|
+
# teams.size_of(0) # how many people team 0 has
|
|
178
|
+
class Partition
|
|
179
|
+
# @return [Model] the model that declared this partition
|
|
180
|
+
attr_reader :model
|
|
181
|
+
# @return [String] the partition's name; its assignment comes back keyed by it
|
|
182
|
+
attr_reader :name
|
|
183
|
+
# @return [Integer] the number of items
|
|
184
|
+
attr_reader :size
|
|
185
|
+
# @return [Integer] the number of parts
|
|
186
|
+
attr_reader :parts
|
|
187
|
+
# @return [Array<Integer>, nil] the part each item starts in, or +nil+ for the
|
|
188
|
+
# default (item +i+ in part +i % parts+)
|
|
189
|
+
attr_reader :start
|
|
190
|
+
|
|
191
|
+
# Declared with +Model#partition+, not built directly.
|
|
192
|
+
#
|
|
193
|
+
# @api private
|
|
194
|
+
def initialize(model, name, size, parts, start)
|
|
195
|
+
@model = model
|
|
196
|
+
@name = name
|
|
197
|
+
@size = size
|
|
198
|
+
@parts = parts
|
|
199
|
+
@start = start&.freeze
|
|
200
|
+
@reads = {}
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# The part item +item+ is in, an expression whose value the service
|
|
204
|
+
# chooses. Index a +LookupTable+ with it to read data about that part.
|
|
205
|
+
#
|
|
206
|
+
# @param item [Integer] from 0 to +size - 1+
|
|
207
|
+
# @return [Expression]
|
|
208
|
+
# @raise [TypeError, ArgumentError] if +item+ is not one of the items
|
|
209
|
+
def part_of(item)
|
|
210
|
+
read(:part_of, Positions.check(item, @size, "item", owner))
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# The number of items in part +part+, an expression whose value the service
|
|
214
|
+
# chooses: a count from 0 to +size+.
|
|
215
|
+
#
|
|
216
|
+
# @param part [Integer] from 0 to +parts - 1+
|
|
217
|
+
# @return [Expression]
|
|
218
|
+
# @raise [TypeError, ArgumentError] if +part+ is not one of the parts
|
|
219
|
+
def size_of(part)
|
|
220
|
+
read(:size_of, Positions.check(part, @parts, "part", owner))
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @return [String] name and sizes
|
|
224
|
+
def inspect
|
|
225
|
+
"#<Quicopt::Partition #{@name} (#{@size} items in #{@parts} parts)>"
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
private
|
|
229
|
+
|
|
230
|
+
def owner
|
|
231
|
+
"partition #{@name.inspect}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def read(op, position)
|
|
235
|
+
entry = (@reads[[op, position]] ||= PartRead.new(self, op, position, @model.next_order))
|
|
236
|
+
Expression.new(linear: { entry => 1.0 })
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# A table of data declared by +Model#lookup_table+, to be indexed with
|
|
241
|
+
# expressions: +table[i]+ for a vector, +table[i, j]+ for a matrix, positions
|
|
242
|
+
# counted from 0.
|
|
243
|
+
#
|
|
244
|
+
# price = m.lookup_table("price", [3.0, 1.0, 4.0, 1.5])
|
|
245
|
+
# choice = m.int_var(0, 3, "choice")
|
|
246
|
+
# price[choice] # the price of the option the service picks
|
|
247
|
+
# price[2] # 4.0, the number itself
|
|
248
|
+
class LookupTable
|
|
249
|
+
# @return [Model] the model that declared this table
|
|
250
|
+
attr_reader :model
|
|
251
|
+
# @return [String] the table's name
|
|
252
|
+
attr_reader :name
|
|
253
|
+
# @return [Array<Float>, Array<Array<Float>>] the entries, as given
|
|
254
|
+
attr_reader :values
|
|
255
|
+
# @return [Array<Integer>] the extent of each dimension: +[n]+ or +[rows, columns]+
|
|
256
|
+
attr_reader :shape
|
|
257
|
+
|
|
258
|
+
# Declared with +Model#lookup_table+, not built directly.
|
|
259
|
+
#
|
|
260
|
+
# @api private
|
|
261
|
+
def initialize(model, name, values, shape)
|
|
262
|
+
@model = model
|
|
263
|
+
@name = name
|
|
264
|
+
@values = values
|
|
265
|
+
@shape = shape.freeze
|
|
266
|
+
@reads = {}
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# The entry at the given positions. With whole numbers only, that is the number
|
|
270
|
+
# itself. With an expression among them — +route.item_at(k)+, an integer
|
|
271
|
+
# variable — it is an expression whose value is the entry at the positions the
|
|
272
|
+
# service chooses. A position that is not a whole number is rounded, and one
|
|
273
|
+
# outside the table is moved to its nearest end.
|
|
274
|
+
#
|
|
275
|
+
# @param index [Array<Integer, Variable, Expression>] one position per dimension
|
|
276
|
+
# @return [Float, Expression]
|
|
277
|
+
# @raise [ArgumentError] on the wrong number of positions, or a whole number
|
|
278
|
+
# outside the table
|
|
279
|
+
# @raise [TypeError] on a position that is neither a whole number nor an
|
|
280
|
+
# expression
|
|
281
|
+
def [](*index)
|
|
282
|
+
unless index.size == @shape.size
|
|
283
|
+
raise ArgumentError, "#{owner} has #{@shape.size} dimension(s), so it takes #{@shape.size} " \
|
|
284
|
+
"position(s), got #{index.size}"
|
|
285
|
+
end
|
|
286
|
+
index.zip(@shape).each do |k, extent|
|
|
287
|
+
if k.is_a?(Integer)
|
|
288
|
+
Positions.check(k, extent, "position", owner)
|
|
289
|
+
elsif k.is_a?(Numeric)
|
|
290
|
+
raise TypeError, "#{owner}: a position is a whole number or an expression, got #{k}"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
return index.reduce(@values) { |row, k| row[k] } if index.all?(Integer)
|
|
294
|
+
|
|
295
|
+
read(index.map { |k| Expression.cast(k) })
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# @return [String] name and shape
|
|
299
|
+
def inspect
|
|
300
|
+
"#<Quicopt::LookupTable #{@name} (#{@shape.join(' × ')})>"
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
private
|
|
304
|
+
|
|
305
|
+
def owner
|
|
306
|
+
"lookup table #{@name.inspect}"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# One +TableEntry+ per distinct index, keyed by each position's terms in
|
|
310
|
+
# canonical order (the terms are interned, so they key by identity).
|
|
311
|
+
def read(index)
|
|
312
|
+
index.each do |e|
|
|
313
|
+
e.factors.each do |f|
|
|
314
|
+
next if f.model.equal?(@model)
|
|
315
|
+
|
|
316
|
+
raise ArgumentError, "#{f} belongs to a different Quicopt::Model than #{owner}"
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
key = index.map do |e|
|
|
320
|
+
[e.constant, e.linear.sort_by { |f, _| f.order }, e.quadratic.sort_by { |(a, b), _| [a.order, b.order] }]
|
|
321
|
+
end
|
|
322
|
+
entry = (@reads[key] ||= TableEntry.new(self, index.freeze, @model.next_order))
|
|
323
|
+
Expression.new(linear: { entry => 1.0 })
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# A read of a permutation: the item in a slot, or the slot of an item. One
|
|
328
|
+
# factor of an +Expression+, standing for its value counted from 0.
|
|
329
|
+
#
|
|
330
|
+
# @api private
|
|
331
|
+
class Placement
|
|
332
|
+
# @return [Permutation]
|
|
333
|
+
attr_reader :permutation
|
|
334
|
+
# @return [Symbol] +:item_at+ or +:slot_of+
|
|
335
|
+
attr_reader :op
|
|
336
|
+
# @return [Integer] the slot (for +:item_at+) or the item (for +:slot_of+)
|
|
337
|
+
attr_reader :position
|
|
338
|
+
# @return [Array] where this term sorts among a model's terms
|
|
339
|
+
attr_reader :order
|
|
340
|
+
|
|
341
|
+
def initialize(permutation, op, position, order)
|
|
342
|
+
@permutation = permutation
|
|
343
|
+
@op = op
|
|
344
|
+
@position = position
|
|
345
|
+
@order = order
|
|
346
|
+
freeze
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# @return [Model]
|
|
350
|
+
def model
|
|
351
|
+
@permutation.model
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# @return [Array<Variable>] none: a read of a permutation involves no variable
|
|
355
|
+
def variables
|
|
356
|
+
[]
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# @return [String] the read as written, e.g. +route.item_at(0)+
|
|
360
|
+
def to_s
|
|
361
|
+
"#{@permutation.name}.#{@op}(#{@position})"
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# A read of a partition: the part of an item (counted from 0), or the number
|
|
366
|
+
# of items in a part. One factor of an +Expression+.
|
|
367
|
+
#
|
|
368
|
+
# @api private
|
|
369
|
+
class PartRead
|
|
370
|
+
# @return [Partition]
|
|
371
|
+
attr_reader :partition
|
|
372
|
+
# @return [Symbol] +:part_of+ or +:size_of+
|
|
373
|
+
attr_reader :op
|
|
374
|
+
# @return [Integer] the item (for +:part_of+) or the part (for +:size_of+)
|
|
375
|
+
attr_reader :position
|
|
376
|
+
# @return [Array] where this term sorts among a model's terms
|
|
377
|
+
attr_reader :order
|
|
378
|
+
|
|
379
|
+
def initialize(partition, op, position, order)
|
|
380
|
+
@partition = partition
|
|
381
|
+
@op = op
|
|
382
|
+
@position = position
|
|
383
|
+
@order = order
|
|
384
|
+
freeze
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# @return [Model]
|
|
388
|
+
def model
|
|
389
|
+
@partition.model
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# @return [Array<Variable>] none: a read of a partition involves no variable
|
|
393
|
+
def variables
|
|
394
|
+
[]
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# @return [String] the read as written, e.g. +teams.part_of(2)+
|
|
398
|
+
def to_s
|
|
399
|
+
"#{@partition.name}.#{@op}(#{@position})"
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# A read of a lookup table at positions given by expressions. One factor of an
|
|
404
|
+
# +Expression+.
|
|
405
|
+
#
|
|
406
|
+
# @api private
|
|
407
|
+
class TableEntry
|
|
408
|
+
# @return [LookupTable]
|
|
409
|
+
attr_reader :table
|
|
410
|
+
# @return [Array<Expression>] one position per dimension, counted from 0
|
|
411
|
+
attr_reader :index
|
|
412
|
+
# @return [Array] where this term sorts among a model's terms
|
|
413
|
+
attr_reader :order
|
|
414
|
+
|
|
415
|
+
def initialize(table, index, order)
|
|
416
|
+
@table = table
|
|
417
|
+
@index = index
|
|
418
|
+
@order = order
|
|
419
|
+
freeze
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# @return [Model]
|
|
423
|
+
def model
|
|
424
|
+
@table.model
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# @return [Array<Variable>] the variables the positions mention
|
|
428
|
+
def variables
|
|
429
|
+
@index.flat_map(&:variables).uniq
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# @return [String] the read as written, e.g. +dist[route.item_at(0), route.item_at(1)]+
|
|
433
|
+
def to_s
|
|
434
|
+
"#{@table.name}[#{@index.join(', ')}]"
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
data/lib/quicopt/version.rb
CHANGED