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.
@@ -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,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
@@ -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.3.0"
9
9
  end