quicopt 0.1.0 → 0.4.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.
@@ -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\"\xdc\x01\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\x42\x06\n\x04node\"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\"\xe8\x02\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\";\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*I\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\x62\x06proto3"
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,438 @@
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 — orderings 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+: +n_items+ items placed in as
48
+ # many slots, one item per slot, both numbered from 0. Read it with +item_at+
49
+ # and +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 :n_items
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, n_items, start)
70
+ @model = model
71
+ @name = name
72
+ @n_items = n_items
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 +n_items - 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, @n_items, "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 +n_items - 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, @n_items, "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, @n_items, "item", owner)
113
+ b = Positions.check(after, @n_items, "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]], @n_items)
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 number of items
133
+ def inspect
134
+ "#<Quicopt::Permutation #{@name} (#{@n_items} items in #{@n_items} slots)>"
135
+ end
136
+
137
+ # Whether the +[before, after]+ requirements over items +0...n_items+
138
+ # contradict each other: Kahn's algorithm leaves an item unplaced exactly
139
+ # when they contain a cycle.
140
+ #
141
+ # @api private
142
+ def self.cycle?(edges, n_items)
143
+ indegree = Array.new(n_items, 0)
144
+ edges.each { |_, b| indegree[b] += 1 }
145
+ ready = (0...n_items).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_items
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+: +n_items+ items in +n_parts+
173
+ # parts, 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 :n_items
185
+ # @return [Integer] the number of parts
186
+ attr_reader :n_parts
187
+ # @return [Array<Integer>, nil] the part each item starts in, or +nil+ for the
188
+ # default (item +i+ in part +i % n_parts+)
189
+ attr_reader :start
190
+
191
+ # Declared with +Model#partition+, not built directly.
192
+ #
193
+ # @api private
194
+ def initialize(model, name, n_items, n_parts, start)
195
+ @model = model
196
+ @name = name
197
+ @n_items = n_items
198
+ @n_parts = n_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 +n_items - 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, @n_items, "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 +n_items+.
215
+ #
216
+ # @param part [Integer] from 0 to +n_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, @n_parts, "part", owner))
221
+ end
222
+
223
+ # @return [String] name, number of items and number of parts
224
+ def inspect
225
+ "#<Quicopt::Partition #{@name} (#{@n_items} items in #{@n_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: +[length]+ or
256
+ # +[rows, columns]+
257
+ attr_reader :shape
258
+
259
+ # Declared with +Model#lookup_table+, not built directly.
260
+ #
261
+ # @api private
262
+ def initialize(model, name, values, shape)
263
+ @model = model
264
+ @name = name
265
+ @values = values
266
+ @shape = shape.freeze
267
+ @reads = {}
268
+ end
269
+
270
+ # The entry at the given positions. With whole numbers only, that is the number
271
+ # itself. With an expression among them — +route.item_at(k)+, an integer
272
+ # variable — it is an expression whose value is the entry at the positions the
273
+ # service chooses. A position that is not a whole number is rounded, and one
274
+ # outside the table is moved to its nearest end.
275
+ #
276
+ # @param index [Array<Integer, Variable, Expression>] one position per dimension
277
+ # @return [Float, Expression]
278
+ # @raise [ArgumentError] on the wrong number of positions, or a whole number
279
+ # outside the table
280
+ # @raise [TypeError] on a position that is neither a whole number nor an
281
+ # expression
282
+ def [](*index)
283
+ unless index.size == @shape.size
284
+ raise ArgumentError, "#{owner} has #{@shape.size} dimension(s), so it takes #{@shape.size} " \
285
+ "position(s), got #{index.size}"
286
+ end
287
+ index.zip(@shape).each do |k, extent|
288
+ if k.is_a?(Integer)
289
+ Positions.check(k, extent, "position", owner)
290
+ elsif k.is_a?(Numeric)
291
+ raise TypeError, "#{owner}: a position is a whole number or an expression, got #{k}"
292
+ end
293
+ end
294
+ return index.reduce(@values) { |row, k| row[k] } if index.all?(Integer)
295
+
296
+ read(index.map { |k| Expression.cast(k) })
297
+ end
298
+
299
+ # @return [String] name and shape
300
+ def inspect
301
+ "#<Quicopt::LookupTable #{@name} (#{@shape.join(' × ')})>"
302
+ end
303
+
304
+ private
305
+
306
+ def owner
307
+ "lookup table #{@name.inspect}"
308
+ end
309
+
310
+ # One +TableEntry+ per distinct index, keyed by each position's terms in
311
+ # canonical order (the terms are interned, so they key by identity).
312
+ def read(index)
313
+ index.each do |e|
314
+ e.factors.each do |f|
315
+ next if f.model.equal?(@model)
316
+
317
+ raise ArgumentError, "#{f} belongs to a different Quicopt::Model than #{owner}"
318
+ end
319
+ end
320
+ key = index.map do |e|
321
+ [e.constant, e.linear.sort_by { |f, _| f.order }, e.quadratic.sort_by { |(a, b), _| [a.order, b.order] }]
322
+ end
323
+ entry = (@reads[key] ||= TableEntry.new(self, index.freeze, @model.next_order))
324
+ Expression.new(linear: { entry => 1.0 })
325
+ end
326
+ end
327
+
328
+ # A read of a permutation: the item in a slot, or the slot of an item. One
329
+ # factor of an +Expression+, standing for its value counted from 0.
330
+ #
331
+ # @api private
332
+ class Placement
333
+ # @return [Permutation]
334
+ attr_reader :permutation
335
+ # @return [Symbol] +:item_at+ or +:slot_of+
336
+ attr_reader :op
337
+ # @return [Integer] the slot (for +:item_at+) or the item (for +:slot_of+)
338
+ attr_reader :position
339
+ # @return [Array] where this term sorts among a model's terms
340
+ attr_reader :order
341
+
342
+ def initialize(permutation, op, position, order)
343
+ @permutation = permutation
344
+ @op = op
345
+ @position = position
346
+ @order = order
347
+ freeze
348
+ end
349
+
350
+ # @return [Model]
351
+ def model
352
+ @permutation.model
353
+ end
354
+
355
+ # @return [Array<Variable>] none: a read of a permutation involves no variable
356
+ def variables
357
+ []
358
+ end
359
+
360
+ # @return [String] the read as written, e.g. +route.item_at(0)+
361
+ def to_s
362
+ "#{@permutation.name}.#{@op}(#{@position})"
363
+ end
364
+ end
365
+
366
+ # A read of a partition: the part of an item (counted from 0), or the number
367
+ # of items in a part. One factor of an +Expression+.
368
+ #
369
+ # @api private
370
+ class PartRead
371
+ # @return [Partition]
372
+ attr_reader :partition
373
+ # @return [Symbol] +:part_of+ or +:size_of+
374
+ attr_reader :op
375
+ # @return [Integer] the item (for +:part_of+) or the part (for +:size_of+)
376
+ attr_reader :position
377
+ # @return [Array] where this term sorts among a model's terms
378
+ attr_reader :order
379
+
380
+ def initialize(partition, op, position, order)
381
+ @partition = partition
382
+ @op = op
383
+ @position = position
384
+ @order = order
385
+ freeze
386
+ end
387
+
388
+ # @return [Model]
389
+ def model
390
+ @partition.model
391
+ end
392
+
393
+ # @return [Array<Variable>] none: a read of a partition involves no variable
394
+ def variables
395
+ []
396
+ end
397
+
398
+ # @return [String] the read as written, e.g. +teams.part_of(2)+
399
+ def to_s
400
+ "#{@partition.name}.#{@op}(#{@position})"
401
+ end
402
+ end
403
+
404
+ # A read of a lookup table at positions given by expressions. One factor of an
405
+ # +Expression+.
406
+ #
407
+ # @api private
408
+ class TableEntry
409
+ # @return [LookupTable]
410
+ attr_reader :table
411
+ # @return [Array<Expression>] one position per dimension, counted from 0
412
+ attr_reader :index
413
+ # @return [Array] where this term sorts among a model's terms
414
+ attr_reader :order
415
+
416
+ def initialize(table, index, order)
417
+ @table = table
418
+ @index = index
419
+ @order = order
420
+ freeze
421
+ end
422
+
423
+ # @return [Model]
424
+ def model
425
+ @table.model
426
+ end
427
+
428
+ # @return [Array<Variable>] the variables the positions mention
429
+ def variables
430
+ @index.flat_map(&:variables).uniq
431
+ end
432
+
433
+ # @return [String] the read as written, e.g. +dist[route.item_at(0), route.item_at(1)]+
434
+ def to_s
435
+ "#{@table.name}[#{@index.join(', ')}]"
436
+ end
437
+ end
438
+ end
@@ -5,5 +5,5 @@
5
5
  module Quicopt
6
6
  # The gem version, tracking the client — not the wire schema, which is
7
7
  # versioned separately by its protobuf package (`quicopt.modeler.v1`).
8
- VERSION = "0.1.0"
8
+ VERSION = "0.4.0"
9
9
  end