nimopt 0.1.0__tar.gz

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.
Files changed (184) hide show
  1. nimopt-0.1.0/.claude/skills/nimopt/SKILL.md +180 -0
  2. nimopt-0.1.0/.github/workflows/pages.yml +60 -0
  3. nimopt-0.1.0/.gitignore +13 -0
  4. nimopt-0.1.0/.pre-commit-config.yaml +30 -0
  5. nimopt-0.1.0/CITATION.cff +11 -0
  6. nimopt-0.1.0/LICENSE +21 -0
  7. nimopt-0.1.0/PKG-INFO +280 -0
  8. nimopt-0.1.0/README.md +230 -0
  9. nimopt-0.1.0/benchmarks/bench_pypsa.py +143 -0
  10. nimopt-0.1.0/benchmarks/bench_pypsa_backend.py +264 -0
  11. nimopt-0.1.0/benchmarks/bench_storage.py +122 -0
  12. nimopt-0.1.0/benchmarks/bench_transport.py +122 -0
  13. nimopt-0.1.0/benchmarks/bench_vs_linopy.py +244 -0
  14. nimopt-0.1.0/benchmarks/compare.py +227 -0
  15. nimopt-0.1.0/benchmarks/data/README.md +24 -0
  16. nimopt-0.1.0/benchmarks/data/elec_s_10.npz +0 -0
  17. nimopt-0.1.0/benchmarks/data/elec_s_10_reference.json +114 -0
  18. nimopt-0.1.0/benchmarks/linopy_models.py +157 -0
  19. nimopt-0.1.0/benchmarks/ownership.py +79 -0
  20. nimopt-0.1.0/benchmarks/pdlp_gpu.py +317 -0
  21. nimopt-0.1.0/benchmarks/pypsa_fidelity.py +114 -0
  22. nimopt-0.1.0/benchmarks/pypsa_findings.md +72 -0
  23. nimopt-0.1.0/benchmarks/pypsa_network.py +841 -0
  24. nimopt-0.1.0/benchmarks/pypsa_reference.py +276 -0
  25. nimopt-0.1.0/pyproject.toml +129 -0
  26. nimopt-0.1.0/src/nimopt/__init__.py +55 -0
  27. nimopt-0.1.0/src/nimopt/absence.py +191 -0
  28. nimopt-0.1.0/src/nimopt/coefficient.py +345 -0
  29. nimopt-0.1.0/src/nimopt/constraint.py +202 -0
  30. nimopt-0.1.0/src/nimopt/definition.py +325 -0
  31. nimopt-0.1.0/src/nimopt/explanation.py +174 -0
  32. nimopt-0.1.0/src/nimopt/files.py +470 -0
  33. nimopt-0.1.0/src/nimopt/model.py +529 -0
  34. nimopt-0.1.0/src/nimopt/models/__init__.py +26 -0
  35. nimopt-0.1.0/src/nimopt/models/_arithmetic.py +35 -0
  36. nimopt-0.1.0/src/nimopt/models/commitment.py +95 -0
  37. nimopt-0.1.0/src/nimopt/models/dispatch.py +60 -0
  38. nimopt-0.1.0/src/nimopt/models/expansion.py +153 -0
  39. nimopt-0.1.0/src/nimopt/models/fleet.py +65 -0
  40. nimopt-0.1.0/src/nimopt/models/nodal.py +86 -0
  41. nimopt-0.1.0/src/nimopt/models/profiled.py +78 -0
  42. nimopt-0.1.0/src/nimopt/models/recourse.py +156 -0
  43. nimopt-0.1.0/src/nimopt/models/sector.py +103 -0
  44. nimopt-0.1.0/src/nimopt/models/storage.py +114 -0
  45. nimopt-0.1.0/src/nimopt/models/transport.py +114 -0
  46. nimopt-0.1.0/src/nimopt/names.py +8 -0
  47. nimopt-0.1.0/src/nimopt/param.py +145 -0
  48. nimopt-0.1.0/src/nimopt/progress.py +85 -0
  49. nimopt-0.1.0/src/nimopt/py.typed +0 -0
  50. nimopt-0.1.0/src/nimopt/row.py +191 -0
  51. nimopt-0.1.0/src/nimopt/session.py +216 -0
  52. nimopt-0.1.0/src/nimopt/sets.py +301 -0
  53. nimopt-0.1.0/src/nimopt/solution.py +111 -0
  54. nimopt-0.1.0/src/nimopt/solvers/__init__.py +75 -0
  55. nimopt-0.1.0/src/nimopt/solvers/base.py +111 -0
  56. nimopt-0.1.0/src/nimopt/solvers/gurobi.py +230 -0
  57. nimopt-0.1.0/src/nimopt/solvers/highs.py +261 -0
  58. nimopt-0.1.0/src/nimopt/solvers/mosek.py +292 -0
  59. nimopt-0.1.0/src/nimopt/solvers/options.py +135 -0
  60. nimopt-0.1.0/src/nimopt/spelling.py +264 -0
  61. nimopt-0.1.0/src/nimopt/symbol.py +109 -0
  62. nimopt-0.1.0/src/nimopt/term.py +553 -0
  63. nimopt-0.1.0/src/nimopt/variable.py +247 -0
  64. nimopt-0.1.0/tests/reference.py +21 -0
  65. nimopt-0.1.0/tests/test_absence.py +225 -0
  66. nimopt-0.1.0/tests/test_absence_rules.py +83 -0
  67. nimopt-0.1.0/tests/test_algebra_surface.py +165 -0
  68. nimopt-0.1.0/tests/test_alias.py +160 -0
  69. nimopt-0.1.0/tests/test_benchmarks_run.py +136 -0
  70. nimopt-0.1.0/tests/test_boundary_allocation.py +160 -0
  71. nimopt-0.1.0/tests/test_boundary_imports.py +64 -0
  72. nimopt-0.1.0/tests/test_boundary_public_api.py +336 -0
  73. nimopt-0.1.0/tests/test_bounds.py +205 -0
  74. nimopt-0.1.0/tests/test_capacity_expansion.py +80 -0
  75. nimopt-0.1.0/tests/test_coefficient.py +250 -0
  76. nimopt-0.1.0/tests/test_constant.py +74 -0
  77. nimopt-0.1.0/tests/test_constraint.py +214 -0
  78. nimopt-0.1.0/tests/test_definition.py +366 -0
  79. nimopt-0.1.0/tests/test_docs_contract.py +238 -0
  80. nimopt-0.1.0/tests/test_docs_examples.py +92 -0
  81. nimopt-0.1.0/tests/test_docs_surface.py +132 -0
  82. nimopt-0.1.0/tests/test_explanation.py +68 -0
  83. nimopt-0.1.0/tests/test_files.py +412 -0
  84. nimopt-0.1.0/tests/test_fixed_member.py +101 -0
  85. nimopt-0.1.0/tests/test_gurobi.py +182 -0
  86. nimopt-0.1.0/tests/test_lags.py +123 -0
  87. nimopt-0.1.0/tests/test_materialise.py +108 -0
  88. nimopt-0.1.0/tests/test_model_assemble.py +148 -0
  89. nimopt-0.1.0/tests/test_model_declare.py +174 -0
  90. nimopt-0.1.0/tests/test_model_explain.py +159 -0
  91. nimopt-0.1.0/tests/test_models.py +300 -0
  92. nimopt-0.1.0/tests/test_mosek.py +261 -0
  93. nimopt-0.1.0/tests/test_options.py +270 -0
  94. nimopt-0.1.0/tests/test_param.py +142 -0
  95. nimopt-0.1.0/tests/test_playground_wheels.py +79 -0
  96. nimopt-0.1.0/tests/test_progress.py +133 -0
  97. nimopt-0.1.0/tests/test_public_api.py +49 -0
  98. nimopt-0.1.0/tests/test_pypsa_fidelity.py +322 -0
  99. nimopt-0.1.0/tests/test_reading.py +199 -0
  100. nimopt-0.1.0/tests/test_row.py +133 -0
  101. nimopt-0.1.0/tests/test_session.py +220 -0
  102. nimopt-0.1.0/tests/test_sets.py +89 -0
  103. nimopt-0.1.0/tests/test_solution_dense.py +92 -0
  104. nimopt-0.1.0/tests/test_solve.py +150 -0
  105. nimopt-0.1.0/tests/test_solvers.py +132 -0
  106. nimopt-0.1.0/tests/test_spelling.py +311 -0
  107. nimopt-0.1.0/tests/test_term.py +206 -0
  108. nimopt-0.1.0/tests/test_variable.py +151 -0
  109. nimopt-0.1.0/tests/test_vs_linopy.py +70 -0
  110. nimopt-0.1.0/tests/test_where.py +187 -0
  111. nimopt-0.1.0/website/docs/explanation/a-variable-is-a-dimension.md +113 -0
  112. nimopt-0.1.0/website/docs/explanation/expressions-are-symbolic.md +102 -0
  113. nimopt-0.1.0/website/docs/explanation/the-array-is-the-matrix.md +78 -0
  114. nimopt-0.1.0/website/docs/explanation/the-package-boundary.md +81 -0
  115. nimopt-0.1.0/website/docs/explanation/what-the-numbers-measure.md +170 -0
  116. nimopt-0.1.0/website/docs/for-agents.md +181 -0
  117. nimopt-0.1.0/website/docs/get-started/index.md +129 -0
  118. nimopt-0.1.0/website/docs/guides/at-scale.md +118 -0
  119. nimopt-0.1.0/website/docs/guides/bounds-from-parameters.md +132 -0
  120. nimopt-0.1.0/website/docs/guides/coefficient-arithmetic.md +237 -0
  121. nimopt-0.1.0/website/docs/guides/conditions.md +194 -0
  122. nimopt-0.1.0/website/docs/guides/fixed-members.md +137 -0
  123. nimopt-0.1.0/website/docs/guides/highs-methods.md +177 -0
  124. nimopt-0.1.0/website/docs/guides/lags.md +163 -0
  125. nimopt-0.1.0/website/docs/guides/saving-and-loading.md +266 -0
  126. nimopt-0.1.0/website/docs/guides/subsets.md +172 -0
  127. nimopt-0.1.0/website/docs/index.md +236 -0
  128. nimopt-0.1.0/website/docs/models/commitment.md +89 -0
  129. nimopt-0.1.0/website/docs/models/dispatch.md +87 -0
  130. nimopt-0.1.0/website/docs/models/expansion.md +162 -0
  131. nimopt-0.1.0/website/docs/models/fleet.md +84 -0
  132. nimopt-0.1.0/website/docs/models/index.md +52 -0
  133. nimopt-0.1.0/website/docs/models/nodal.md +95 -0
  134. nimopt-0.1.0/website/docs/models/profiled.md +86 -0
  135. nimopt-0.1.0/website/docs/models/recourse.md +120 -0
  136. nimopt-0.1.0/website/docs/models/sector.md +90 -0
  137. nimopt-0.1.0/website/docs/models/storage.md +111 -0
  138. nimopt-0.1.0/website/docs/models/transport.md +91 -0
  139. nimopt-0.1.0/website/docs/nimblend/arrays.md +395 -0
  140. nimopt-0.1.0/website/docs/nimblend/domains.md +430 -0
  141. nimopt-0.1.0/website/docs/nimblend/index.md +42 -0
  142. nimopt-0.1.0/website/docs/reference/constraint.md +100 -0
  143. nimopt-0.1.0/website/docs/reference/definition.md +241 -0
  144. nimopt-0.1.0/website/docs/reference/explanation.md +78 -0
  145. nimopt-0.1.0/website/docs/reference/expression.md +369 -0
  146. nimopt-0.1.0/website/docs/reference/files.md +172 -0
  147. nimopt-0.1.0/website/docs/reference/inspection.md +157 -0
  148. nimopt-0.1.0/website/docs/reference/model.md +159 -0
  149. nimopt-0.1.0/website/docs/reference/param.md +206 -0
  150. nimopt-0.1.0/website/docs/reference/sets.md +148 -0
  151. nimopt-0.1.0/website/docs/reference/solution.md +104 -0
  152. nimopt-0.1.0/website/docs/reference/solvers.md +295 -0
  153. nimopt-0.1.0/website/docs/reference/variable.md +158 -0
  154. nimopt-0.1.0/website/docs/tutorial/constraints.md +189 -0
  155. nimopt-0.1.0/website/docs/tutorial/expressions.md +163 -0
  156. nimopt-0.1.0/website/docs/tutorial/reading-the-answer.md +218 -0
  157. nimopt-0.1.0/website/docs/tutorial/sets-and-parameters.md +172 -0
  158. nimopt-0.1.0/website/docs/tutorial/solving.md +146 -0
  159. nimopt-0.1.0/website/docs/tutorial/variables.md +96 -0
  160. nimopt-0.1.0/website/docs/vocabulary/index.md +138 -0
  161. nimopt-0.1.0/website/docusaurus.config.ts +42 -0
  162. nimopt-0.1.0/website/package.json +30 -0
  163. nimopt-0.1.0/website/pnpm-lock.yaml +11759 -0
  164. nimopt-0.1.0/website/scripts/README.md +12 -0
  165. nimopt-0.1.0/website/scripts/docs_blocks.py +83 -0
  166. nimopt-0.1.0/website/scripts/llms.mjs +124 -0
  167. nimopt-0.1.0/website/scripts/outputs.py +95 -0
  168. nimopt-0.1.0/website/scripts/readme.mjs +56 -0
  169. nimopt-0.1.0/website/scripts/skill.mjs +28 -0
  170. nimopt-0.1.0/website/scripts/solver_options.py +67 -0
  171. nimopt-0.1.0/website/scripts/surface.py +108 -0
  172. nimopt-0.1.0/website/scripts/watch.mjs +22 -0
  173. nimopt-0.1.0/website/sidebars.ts +91 -0
  174. nimopt-0.1.0/website/src/components/editor.js +39 -0
  175. nimopt-0.1.0/website/src/components/editor.module.css +28 -0
  176. nimopt-0.1.0/website/src/components/pyodide.js +71 -0
  177. nimopt-0.1.0/website/src/css/custom.css +9 -0
  178. nimopt-0.1.0/website/src/pages/playground.js +170 -0
  179. nimopt-0.1.0/website/src/pages/playground.module.css +38 -0
  180. nimopt-0.1.0/website/src/theme/CodeBlock/index.js +37 -0
  181. nimopt-0.1.0/website/src/theme/CodeBlock/styles.module.css +34 -0
  182. nimopt-0.1.0/website/static/img/favicon.svg +10 -0
  183. nimopt-0.1.0/website/static/llms-full.txt +7032 -0
  184. nimopt-0.1.0/website/static/llms.txt +83 -0
@@ -0,0 +1,180 @@
1
+ ---
2
+ name: nimopt
3
+ description: Use when writing, reading or debugging a model built with nimopt or nimblend -- declaring sets, parameters, variables, expressions and constraints, or reading a solution.
4
+ ---
5
+ # For agents
6
+
7
+ The whole manual is at [`/llms-full.txt`](/llms-full.txt); the index is at
8
+ [`/llms.txt`](/llms.txt).
9
+
10
+ ## The mental model
11
+
12
+ **A variable is a dimension.** `m.var("x", (P, W))` occupies a block of the
13
+ model's single column space. A member's column is computed from its
14
+ multi-index rather than stored, so a variable over millions of columns
15
+ costs its members and not its columns.
16
+
17
+ **An expression is symbolic.** `cost[P, W] * x[P, W]` holds references,
18
+ not arrays. Writing it costs nothing. It becomes matrix entries only when a
19
+ constraint is materialised.
20
+
21
+ **A constraint is an array.** It is a `nimblend` array over its free sets
22
+ crossed with the column space, so there is no assembly step: the array is
23
+ the matrix.
24
+
25
+ **A definition is a model without its data.** `Definition` mirrors the
26
+ vocabulary a model is written in, `set`, `param`, `var`, `eq` and
27
+ `set_objective`, over symbols declared with no members and no values.
28
+ `explain()` reports what it declares; `build(data)` binds a copy and returns
29
+ a `Model`, so one definition builds as many models as it is given datasets.
30
+
31
+ **A built model answers questions about itself.** `explain()` reports what
32
+ it built, `row(name, **coords)` reads one row back out of the assembled
33
+ matrix, and `absent(name)` reports which coordinates were dropped from a
34
+ constraint and by which rule. All three read what was built rather than
35
+ walking the expression a second time.
36
+
37
+ **A session keeps the solver open.** `model.session()` assembles once and
38
+ keeps the solver's model, so `diagnose()` asks the solved instance which
39
+ rows conflict, or which direction an unbounded model runs off in.
40
+ `available()` lists the adapters installed and `capabilities(name)` reports
41
+ what each does, including what it refuses: a model with integer columns has
42
+ no duals, because a mixed-integer model's duals are not its relaxation's.
43
+
44
+ **`nimblend` is the layer below.** It knows dimensions, labels, entries and
45
+ alignment, and nothing about optimization. Import from `nimblend` itself,
46
+ never from `nimblend.sparse` or another submodule, and never read an array's
47
+ `.index` or `.data` or a domain's `.codes`. Each has a reader above it:
48
+ `coordinates()`, `values()`, `positions_of_coordinates()` and `as_coord()`.
49
+ Nor build one: a domain returns the array over its own members through
50
+ `array(values)` and `identity(into, coord, start)`.
51
+
52
+ ## The public surface
53
+
54
+ <!-- surface -->
55
+ | From | Names |
56
+ | --- | --- |
57
+ | `nimopt` | `COLUMN`, `ROW`, `Absence`, `Alias`, `Assembled`, `Coefficient`, `Constraint`, `Definition`, `Diagnosis`, `Explanation`, `Expression`, `Model`, `Option`, `Param`, `Relation`, `Row`, `Session`, `Set`, `Solution`, `Sum`, `Term`, `Variable`, `available`, `capabilities`, `load`, `loads`, `options`, `product`, `save`, `subset`, `subset_of` |
58
+ | `nimblend` | `Array`, `DenseArray`, `Domain`, `EntryBuffer`, `SparseArray`, `combined_dims`, `from_long`, `from_dense`, `is_canonical`, `StoredCoord`, `ProductCoord`, `SubsetCoord` |
59
+ <!-- /surface -->
60
+
61
+ **A coefficient composes.** A coefficient is a parameter read at its sets
62
+ or an arithmetic combination of such readings: `price[G, T] / eta[G, T]` is
63
+ a coefficient written before any data exists, read at its sets like a
64
+ parameter, and evaluated once when the matrix is built. `+`, `-`, `*`, `/`
65
+ and a power by a number combine coefficients. An expression also carries a
66
+ constant, so `x + 1 <= 5` produces the row `x <= 4`.
67
+
68
+ ## What goes wrong
69
+
70
+ **A chained comparison.** `0 <= expr <= 10` raises `TypeError`. Python
71
+ evaluates it as two comparisons joined by `and`, which keeps only the
72
+ second, so a relation has no truth value rather than letting the first
73
+ bound be dropped. Write each bound as its own constraint.
74
+
75
+ **A sum over a lag.** `Sum(T - 1, ...)` raises: a sum runs over a set's
76
+ members. Put the lag on the variable reference, `x[T - 1]`.
77
+
78
+ **The built-in `sum` over a set's members.** `sum(x[S, t] for t in members)`
79
+ gives the correct answer at a cost: it produces one term per member, where
80
+ `Sum(T, x[S, T])` produces one term and reduces a dimension. The terms
81
+ concatenate pairwise and each materialises its own block, so building a
82
+ model that way runs 24 times slower at 25 members and 275 times at 400, and
83
+ the gap widens. Use the built-in `sum` for a short list of distinct
84
+ expressions and `Sum` for a set's members.
85
+
86
+ **A right-hand side over the wrong dimensions.** A constraint's right-hand
87
+ side is a parameter over exactly its free dimensions. The error message
88
+ gives both.
89
+
90
+ **Reading values from a model that did not solve.** `objective`, `primal`
91
+ and `dual` raise unless `status` is `"optimal"`. Read `status` first.
92
+
93
+ **A domain over a definition's sets.** `product((B, T))` needs each set's
94
+ coordinate, and a declared set has none. In a definition, give `where=`,
95
+ `over=` and `subset=` as a tuple of its sets or as one of its parameters,
96
+ whose coefficients are the coordinates.
97
+
98
+ **A row that is not there.** `row()` raises for a coordinate at which the
99
+ constraint has no row. `absent()` reports which rule dropped it: a
100
+ coefficient absent inside a sum removes a **term** and leaves the row
101
+ standing; a term absent along a **free** dimension removes the **row**.
102
+
103
+ **Reading a MILP's duals.** A model with integer columns has none, and
104
+ `dual()` raises rather than returning the relaxation's. Read `primal`.
105
+
106
+ **A conflict HiGHS cannot prove.** HiGHS computes its conflict over the
107
+ linear relaxation, so a model infeasible only through its integrality
108
+ produces none and `diagnose()` raises. Gurobi's covers the integrality.
109
+
110
+ **Two operands that share no dimension.** Every binary operator combines two
111
+ dimensioned operands only where they share a dimension, and the rule
112
+ applies to a coefficient meeting a variable exactly as it applies to two
113
+ coefficients. Frames sharing nothing raise: their combination would be an
114
+ outer product no model asks for. A number has no dimension and scales.
115
+
116
+ **A division by zero.** A divisor that is zero raises `ZeroDivisionError`
117
+ with the coordinate, for a Python number, a NumPy scalar and a coefficient
118
+ with a zero at one coordinate alike. Handle the divisor before it reaches an
119
+ expression.
120
+
121
+ **A derived coefficient read at the wrong sets.** A combination is read at
122
+ its sets as a parameter is, and the reading is checked against the
123
+ dimensions it has: `unit_cost[T, G]` raises where it is written, naming
124
+ `('G', 'T')`.
125
+
126
+ **Reaching into `nimblend`.** A test fails on an import from a `nimblend`
127
+ submodule, on any read of an array's `.index` or `.data` or a domain's
128
+ `.codes`, and on a module of the package assembling an index matrix of its
129
+ own.
130
+
131
+ ## Every refusal, and where it is shown
132
+
133
+ The prose above covers the mistakes worth explaining. This is every
134
+ refusal the documentation demonstrates, each executed to produce the
135
+ message beside it.
136
+
137
+ <!-- refusals -->
138
+ | Raises | Message | Shown at |
139
+ | --- | --- | --- |
140
+ | `ValueError` | the upper bound 'cap' carries no value for member ('b',) of variable 'x'; a bound covers every column of the variable it bounds | [/guides/bounds-from-parameters](/guides/bounds-from-parameters) |
141
+ | `ValueError` | variable 'x' is declared over ('G',) and does not carry ['W']; its upper bound 'cap' is declared over ('W',) | [/guides/bounds-from-parameters](/guides/bounds-from-parameters) |
142
+ | `TypeError` | a coefficient is a parameter; build one with `Param.from_dense` or `Param.from_long` and read it at its sets. A product of two expressions is not linear. | [/guides/coefficient-arithmetic](/guides/coefficient-arithmetic) |
143
+ | `ValueError` | coefficient (fuel_price / efficiency) is over ('G', 'T'); got ('T', 'G') | [/guides/coefficient-arithmetic](/guides/coefficient-arithmetic) |
144
+ | `ZeroDivisionError` | divisor holed carries a zero at 1 coordinate(s), the first at {'G': 'base', 'T': 1}; a quotient there states a coefficient no solver can read | [/guides/coefficient-arithmetic](/guides/coefficient-arithmetic) |
145
+ | `ValueError` | frames ('G',) and ('T',) share no dimension; there is nothing to align them on | [/guides/coefficient-arithmetic](/guides/coefficient-arithmetic) |
146
+ | `ValueError` | constraint 'capacity' has free dimensions ('P',); its condition is over ('W',) | [/guides/conditions](/guides/conditions) |
147
+ | `ValueError` | constraint 'capacity' states its rows with over= and narrows them with where=; state one | [/guides/conditions](/guides/conditions) |
148
+ | `ValueError` | variable 'x' is read at member 't9' of dimension 'T', which that set does not carry | [/guides/fixed-members](/guides/fixed-members) |
149
+ | `ValueError` | a lag is a whole number of members; got 1.7 | [/guides/lags](/guides/lags) |
150
+ | `ValueError` | a sum is over the members of ['T'], so it takes the set and not a lag of it; state the lag at the variable's reference | [/guides/lags](/guides/lags) [/reference/expression](/reference/expression) |
151
+ | `ValueError` | parameter 'rate' is read at a lag ['T']; state the lag at the variable's reference, where a coefficient multiplies the row it lands on | [/guides/lags](/guides/lags) |
152
+ | `ValueError` | 'max(gen[G, T]) <= 10': Sum is the one call the spelling carries | [/guides/saving-and-loading](/guides/saving-and-loading) |
153
+ | `ValueError` | capital does not fall from base to what follows it | [/models/expansion](/models/expansion) |
154
+ | `ValueError` | frames ('P',) and ('Q',) share no dimension; there is nothing to align them on | [/nimblend/arrays](/nimblend/arrays) |
155
+ | `ValueError` | label column 't' has length 2 and the value column has length 1; they name the same entries | [/nimblend/arrays](/nimblend/arrays) |
156
+ | `ValueError` | this array declares absence 'unknown' and does not carry every coordinate of its frame, so densifying must state fill=<value> to place at the rest | [/nimblend/arrays](/nimblend/arrays) [/tutorial/reading-the-answer](/tutorial/reading-the-answer) |
157
+ | `ValueError` | 3 member(s) numbered from 4 reach position 6, and dimension 'k' spans 6 | [/nimblend/domains](/nimblend/domains) |
158
+ | `ValueError` | a domain of 3 member(s) takes one value each, as a column of that length; got shape (2,) | [/nimblend/domains](/nimblend/domains) |
159
+ | `ValueError` | constraint 'supply' has free dimensions ('P',); its right-hand side 'demand' is over ('W',) | [/reference/constraint](/reference/constraint) [/tutorial/constraints](/tutorial/constraints) |
160
+ | `ValueError` | data does not cover ['S'] | [/reference/definition](/reference/definition) |
161
+ | `ValueError` | parameter 'S' is already declared as a set; a name means one symbol, in an expression and in the data | [/reference/definition](/reference/definition) |
162
+ | `TypeError` | a relation has no truth value; a chained comparison such as 0 <= expr <= 10 reads as two comparisons joined by `and` and keeps only the second, so state each bound separately | [/reference/expression](/reference/expression) [/tutorial/constraints](/tutorial/constraints) |
163
+ | `TypeError` | a relation is already an equation and states one bound; compare the expression a second time in its own equation rather than comparing the relation | [/reference/expression](/reference/expression) |
164
+ | `TypeError` | an LP has no row for a strict inequality; state `<=` or `>=`. `min` and `max` compare two expressions this way and are not linear either, so reduce with `Sum` over the sets instead | [/reference/expression](/reference/expression) |
165
+ | `TypeError` | an expression is reduced over the sets it is summed across; state them with `Sum(I, J, expression)` | [/reference/expression](/reference/expression) |
166
+ | `TypeError` | nimopt expresses a linear term, so a variable in a denominator is not one; state the reciprocal as a coefficient the variable multiplies | [/reference/expression](/reference/expression) |
167
+ | `TypeError` | nimopt expresses a linear term, so a variable raised to a power is not one; a coefficient takes the power instead, and a variable multiplies it | [/reference/expression](/reference/expression) |
168
+ | `TypeError` | nimopt expresses a linear term, so the absolute value of one is not linear; reduce with `Sum` over its sets, or state the magnitude with two rows bounding the expression | [/reference/expression](/reference/expression) |
169
+ | `ValueError` | term 'x' already sums over ['T']; a dimension is reduced once, and a second reduction has nothing left to reduce | [/reference/expression](/reference/expression) |
170
+ | `ValueError` | constraint 'cap' states where= with a domain that has no name; declare its members as a parameter and name that | [/reference/files](/reference/files) |
171
+ | `ValueError` | parameter 'c' is given columns ['value', 'S']; a table states the dimensions then value: ['S', 'value'] | [/reference/files](/reference/files) |
172
+ | `ValueError` | variable 'x' carries ['bound'], which the format does not; it takes ('sets', 'subset', 'lower', 'upper', 'integer') | [/reference/files](/reference/files) |
173
+ | `ValueError` | constraint 'cap' states no row at {'P': 'p3'}; `absent('cap')` names the rule that dropped it | [/reference/inspection](/reference/inspection) |
174
+ | `ValueError` | parameter 'cost': label column 'P' has length 1 and the value column has length 2; they name the same entries | [/reference/param](/reference/param) |
175
+ | `TypeError` | parameter 'price' carries ('G',) and states no coefficient until it is read; read it at its sets as price[G] | [/reference/param](/reference/param) |
176
+ | `ValueError` | the model's status is 'infeasible', so it carries no objective; read `status` before reading values | [/reference/solution](/reference/solution) [/tutorial/solving](/tutorial/solving) |
177
+ | `ValueError` | this model carries integer columns and 'highs' refuses duals for a model with integrality, so there is no dual for constraint 'cap' to read: a mixed-integer model's duals are not its relaxation's | [/reference/solvers](/reference/solvers) |
178
+ | `TypeError` | parameter 'supply' carries ('P',) and states no coefficient until it is read; read it at its sets as supply[P] | [/tutorial/constraints](/tutorial/constraints) |
179
+ | `ValueError` | parameter 'cost' is over sets of shape (2, 3); got values of shape (2, 2) | [/tutorial/sets-and-parameters](/tutorial/sets-and-parameters) |
180
+ <!-- /refusals -->
@@ -0,0 +1,60 @@
1
+ name: pages
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ pages: write
11
+ id-token: write
12
+
13
+ concurrency:
14
+ group: pages
15
+ cancel-in-progress: false
16
+
17
+ jobs:
18
+ build:
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+
23
+ - uses: astral-sh/setup-uv@v5
24
+
25
+ - name: the wheels the playground imports
26
+ run: |
27
+ uvx --from pip pip download nimblend==0.1.0 --no-deps --only-binary=:all: --python-version 3.13 --dest website/static/wheels
28
+ uv build --wheel -o website/static/wheels
29
+
30
+ - uses: pnpm/action-setup@v4
31
+ with:
32
+ version: 10
33
+
34
+ - uses: actions/setup-node@v4
35
+ with:
36
+ node-version: 22
37
+ cache: pnpm
38
+ cache-dependency-path: website/pnpm-lock.yaml
39
+
40
+ - name: the site
41
+ working-directory: website
42
+ run: |
43
+ pnpm install --frozen-lockfile
44
+ pnpm run build
45
+
46
+ - uses: actions/configure-pages@v5
47
+
48
+ - uses: actions/upload-pages-artifact@v3
49
+ with:
50
+ path: website/build
51
+
52
+ deploy:
53
+ needs: build
54
+ runs-on: ubuntu-latest
55
+ environment:
56
+ name: github-pages
57
+ url: ${{ steps.deployment.outputs.page_url }}
58
+ steps:
59
+ - id: deployment
60
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,13 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ .pytest_cache/
5
+ benchmarks/data/large/
6
+ website/node_modules/
7
+ website/build/
8
+ website/.docusaurus/
9
+ .coverage
10
+ coverage.json
11
+ htmlcov/
12
+ .ruff_cache/
13
+ website/scripts/cache/
@@ -0,0 +1,30 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ rev: v0.16.3
4
+ hooks:
5
+ - id: ruff-check
6
+ args: [--fix]
7
+ - id: ruff-format
8
+
9
+ - repo: local
10
+ hooks:
11
+ - id: ty
12
+ name: ty
13
+ entry: .venv/bin/ty check
14
+ language: system
15
+ pass_filenames: false
16
+ always_run: true
17
+
18
+ - id: vulture
19
+ name: vulture
20
+ entry: .venv/bin/vulture
21
+ language: system
22
+ pass_filenames: false
23
+ always_run: true
24
+
25
+ - id: pytest
26
+ name: pytest
27
+ entry: .venv/bin/pytest tests/ -q --tb=short
28
+ language: system
29
+ pass_filenames: false
30
+ always_run: true
@@ -0,0 +1,11 @@
1
+ cff-version: 1.2.0
2
+ title: nimopt
3
+ message: If you use this software, please cite it as below.
4
+ type: software
5
+ authors:
6
+ - family-names: Gaete-Morales
7
+ given-names: Carlos
8
+ email: cdgaete@gmail.com
9
+ abstract: An LP/MILP builder whose constraint blocks are labeled sparse arrays
10
+ version: 0.1.0
11
+ license: MIT
nimopt-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Carlos Gaete-Morales
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
nimopt-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,280 @@
1
+ Metadata-Version: 2.5
2
+ Name: nimopt
3
+ Version: 0.1.0
4
+ Summary: An LP/MILP builder whose constraint blocks are labeled sparse arrays
5
+ Project-URL: Homepage, https://github.com/cdgaete/nimopt
6
+ Project-URL: Documentation, https://cdgaete.github.io/nimopt/
7
+ Project-URL: Repository, https://github.com/cdgaete/nimopt
8
+ Project-URL: Issues, https://github.com/cdgaete/nimopt/issues
9
+ Author-email: Carlos Gaete-Morales <cdgaete@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: linear programming,mathematical programming,milp,optimization,solver,sparse
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: <3.15,>=3.13
23
+ Requires-Dist: nimblend>=0.1.0
24
+ Requires-Dist: numpy>=2.5.2
25
+ Requires-Dist: pyyaml>=6
26
+ Provides-Extra: bench
27
+ Requires-Dist: highspy>=1.15; extra == 'bench'
28
+ Requires-Dist: linopy>=0.5; extra == 'bench'
29
+ Requires-Dist: pypsa>=1.2; extra == 'bench'
30
+ Provides-Extra: dev
31
+ Requires-Dist: coverage>=7.16; extra == 'dev'
32
+ Requires-Dist: gurobipy>=12; extra == 'dev'
33
+ Requires-Dist: highspy>=1.15; extra == 'dev'
34
+ Requires-Dist: hypothesis>=6; extra == 'dev'
35
+ Requires-Dist: mosek>=11; extra == 'dev'
36
+ Requires-Dist: pre-commit>=4.6.2; extra == 'dev'
37
+ Requires-Dist: pytest>=8; extra == 'dev'
38
+ Requires-Dist: ruff>=0.16; extra == 'dev'
39
+ Requires-Dist: scipy>=1.11; extra == 'dev'
40
+ Requires-Dist: ty>=0.0.78; extra == 'dev'
41
+ Requires-Dist: vulture>=2.14; extra == 'dev'
42
+ Provides-Extra: gurobi
43
+ Requires-Dist: gurobipy>=12; extra == 'gurobi'
44
+ Requires-Dist: scipy>=1.11; extra == 'gurobi'
45
+ Provides-Extra: highs
46
+ Requires-Dist: highspy>=1.15; extra == 'highs'
47
+ Provides-Extra: mosek
48
+ Requires-Dist: mosek>=11; extra == 'mosek'
49
+ Description-Content-Type: text/markdown
50
+
51
+ # nimopt
52
+
53
+ `nimopt` is a Python library for building linear and mixed-integer programs. A model is declared symbolically over named index sets — as parameters, variables and constraints — and is expanded into a coefficient matrix only when it is assembled or solved. Solutions are returned as arrays over those same index sets, so a primal value is read by label rather than by column position.
54
+
55
+ `nimopt` is built on `nimblend`, a labelled sparse N-dimensional array library with no knowledge of optimisation. The dependency runs in one direction, and `nimblend` is documented in [its own section](website/docs/nimblend/index.md).
56
+
57
+ ## Design
58
+
59
+ **A variable is a dimension.** A constraint is an array indexed over its free sets crossed with the model's column space, with the coefficients as values. There is no assembly step converting the model into a matrix, because the array is the matrix.
60
+
61
+ **Absence is distinct from zero.** An entry is either stored or absent, and every array declares what absence means: `"empty"` for a coordinate that contributes nothing, `"unknown"` for one that was never modelled. A missing result is never counted as zero, and division by an absent value raises rather than producing an infinity.
62
+
63
+ **A subset stays a subset.** A variable declared over a subset of a set product has one column per member of the subset and none for the rest. The full product is never materialised, at declaration or at any point after it.
64
+
65
+ **Expressions are symbolic.** An expression holds references to variables and parameters rather than their values. `cost[P, W] * x[P, W]` costs the same to write over a million routes as over six; the values are read when the matrix is built.
66
+
67
+ **Dropped rows are reported.** A row whose terms have no value at some coordinate is dropped rather than written incompletely. `absent()` lists every dropped row with the rule that dropped it, and `row()` returns one row of the assembled matrix as the solver receives it.
68
+
69
+ ## Install
70
+
71
+ Neither package is published yet, so both are installed from a checkout. `nimblend` is a dependency and is installed first, from wherever it is cloned; `nimopt` then installs from its own root:
72
+
73
+ ```bash
74
+ pip install /path/to/nimblend
75
+ pip install ".[highs]"
76
+ ```
77
+
78
+ HiGHS is the default solver, and `[highs]` installs it. `[gurobi]` and `[mosek]` add those adapters instead, `[bench]` adds the comparison suite and `[dev]` the test and lint tooling. `available()` reports the solvers whose backend can be imported in the current environment, and `capabilities(name)` answers for an adapter whether or not its backend is installed.
79
+
80
+ ## A first model
81
+
82
+ A transport problem: two plants with limited supply ship to three warehouses with fixed demand, and the objective is total shipping cost.
83
+
84
+ ```python
85
+ import numpy as np
86
+ from nimopt import Model, Param, Set, Sum
87
+
88
+ P = Set("P", np.array(["lisbon", "porto"]))
89
+ W = Set("W", np.array(["berlin", "paris", "rome"]))
90
+
91
+ cost = Param.from_dense("cost", (P, W), np.array([[2.0, 4.0, 5.0], [3.0, 1.0, 6.0]]))
92
+ supply = Param.from_dense("supply", (P,), np.array([30.0, 25.0]))
93
+ demand = Param.from_dense("demand", (W,), np.array([20.0, 15.0, 15.0]))
94
+
95
+ m = Model("transport")
96
+ x = m.var("x", (P, W))
97
+
98
+ m.eq("supply", Sum(W, x[P, W]) <= supply[P])
99
+ m.eq("demand", Sum(P, x[P, W]) >= demand[W])
100
+ m.set_objective(Sum(P, W, cost[P, W] * x[P, W]))
101
+
102
+ solution = m.solve()
103
+ print(solution.status, solution.objective)
104
+ print(solution.primal("x").to_dense())
105
+ ```
106
+
107
+ <!-- output -->
108
+ <details open>
109
+ <summary>Output</summary>
110
+
111
+ ```text
112
+ optimal 135.0
113
+ [[20. 0. 10.]
114
+ [ 0. 15. 5.]]
115
+ ```
116
+
117
+ </details>
118
+ <!-- /output -->
119
+
120
+ The primal values are returned as a 2 by 3 array over plants and warehouses, in the order the sets declare their members.
121
+
122
+ ## Declaring before the data exists
123
+
124
+ A `Definition` states the same model without binding any data. Its sets and parameters are declared by name, its constraints are written in the same expression syntax, and `explain()` reports the whole declaration before a single value has been read.
125
+
126
+ ```python
127
+ import numpy as np
128
+ from nimopt import Definition, Sum
129
+
130
+ d = Definition("transport")
131
+ P, W = d.set("P"), d.set("W")
132
+ cost = d.param("cost", (P, W))
133
+ supply = d.param("supply", (P,))
134
+ demand = d.param("demand", (W,))
135
+ x = d.var("x", (P, W))
136
+
137
+ d.eq("supply", Sum(W, x[P, W]) <= supply[P])
138
+ d.eq("demand", Sum(P, x[P, W]) >= demand[W])
139
+ d.set_objective(Sum(P, W, cost[P, W] * x[P, W]))
140
+
141
+ print(d.explain())
142
+ ```
143
+
144
+ <!-- output -->
145
+ <details open>
146
+ <summary>Output</summary>
147
+
148
+ ```text
149
+ transport min not built
150
+ sets P · W
151
+ parameters cost (P,W) · supply (P) · demand (W)
152
+ variables x (P×W) [0.0, inf]
153
+ constraint supply (P) Sum(W, x[P, W]) <= supply[P]
154
+ constraint demand (W) Sum(P, x[P, W]) >= demand[W]
155
+ objective min Sum(P, W, cost[P, W] * x[P, W])
156
+ ```
157
+
158
+ </details>
159
+ <!-- /output -->
160
+
161
+ A definition is copied before it is bound, so one definition builds as many models as it is given datasets for and is unchanged by any of them. The built model is inspected the same way, and `row()` reads one row back out of the assembled matrix as the solver receives it.
162
+
163
+ ```python
164
+ import numpy as np
165
+ from nimopt import Definition, Sum
166
+
167
+ d = Definition("transport")
168
+ P, W = d.set("P"), d.set("W")
169
+ cost = d.param("cost", (P, W))
170
+ supply = d.param("supply", (P,))
171
+ demand = d.param("demand", (W,))
172
+ x = d.var("x", (P, W))
173
+ d.eq("supply", Sum(W, x[P, W]) <= supply[P])
174
+ d.eq("demand", Sum(P, x[P, W]) >= demand[W])
175
+ d.set_objective(Sum(P, W, cost[P, W] * x[P, W]))
176
+
177
+ data = {
178
+ "P": np.array(["lisbon", "porto"]),
179
+ "W": np.array(["berlin", "paris", "rome"]),
180
+ "cost": np.array([[2.0, 4.0, 5.0], [3.0, 1.0, 6.0]]),
181
+ "supply": np.array([30.0, 25.0]),
182
+ "demand": np.array([20.0, 15.0, 15.0]),
183
+ }
184
+ m = d.build(data)
185
+ print(m)
186
+ print(m.row("demand", W="paris"))
187
+ print(m.absent("demand"))
188
+ ```
189
+
190
+ <!-- output -->
191
+ <details open>
192
+ <summary>Output</summary>
193
+
194
+ ```text
195
+ Model('transport', 1 variables, 6 columns, 5 rows)
196
+ demand[W='paris'] row 3
197
+ 1·x[lisbon,paris] + 1·x[porto,paris] >= 15
198
+ demand 3 of 3 rows stated by terms
199
+ ```
200
+
201
+ </details>
202
+ <!-- /output -->
203
+
204
+ ## A variable over a subset
205
+
206
+ Where a variable spans an arc list rather than a full product, it has a column per arc and the product is never built. A thousand plants each serving three warehouses is three thousand columns, not a million.
207
+
208
+ ```python
209
+ import numpy as np
210
+ from nimopt import Model, Set, subset
211
+
212
+ P = Set("P", np.array([f"p{i}" for i in range(1000)]))
213
+ W = Set("W", np.array([f"w{i}" for i in range(1000)]))
214
+
215
+ served = np.array([f"w{(i * 7 + k) % 1000}" for i in range(1000) for k in range(3)])
216
+ arcs = subset((P, W), {"P": np.repeat(P.labels, 3), "W": served})
217
+
218
+ m = Model("transport")
219
+ x = m.var("x", (P, W), subset=arcs)
220
+ print(f"{m.n_columns} columns over a product of {len(P) * len(W)}")
221
+ ```
222
+
223
+ <!-- output -->
224
+ <details open>
225
+ <summary>Output</summary>
226
+
227
+ ```text
228
+ 3000 columns over a product of 1000000
229
+ ```
230
+
231
+ </details>
232
+ <!-- /output -->
233
+
234
+ ## Features
235
+
236
+ - Sets, aliases, subsets and set products as the index structure of every declaration
237
+ - Parameters from dense arrays or long-form columns, broadcast where a parameter is narrower than the variable it multiplies
238
+ - Composable coefficients: a parameter read at its sets, or an arithmetic of parameters written before data exists
239
+ - Conditions on a sum and on a constraint, lags that drop or wrap at the ends of a set, and members fixed at a label
240
+ - Per-column bounds from a parameter, and variables declared over a subset of a set product
241
+ - A `Definition` written before data exists and built against any number of datasets
242
+ - `explain()` on a definition or a built model, `row()` into the assembled matrix, and `absent()` reporting dropped rows and the rule that dropped each
243
+ - A `Session` that keeps the solved instance open, and `diagnose()` reporting the conflicting rows of an infeasible model or the ray of an unbounded one
244
+ - Primals and duals returned over their index sets, with absence distinct from zero
245
+ - Continuous and integer columns, solved through HiGHS, Gurobi or Mosek behind one adapter contract, with `capabilities()` stating what each adapter does and which capabilities it refuses together
246
+ - One option vocabulary translated into each solver's own spelling, so a time limit is stated the same way whatever solves the model
247
+ - A model written to and read back from YAML, with its data inline or in a sidecar
248
+ - A corpus of worked models under `nimopt.models`, each stating its formulation, inputs at any size, and an objective computed by arithmetic rather than by a solver
249
+
250
+ ## Performance
251
+
252
+ Where a variable's columns are a subset of a set product, not materialising the product is worth a great deal. On a transport model of 400 000 arcs over a 20 000 000-cell product, `nimopt` builds the same matrix in 72.9 MB of resident memory against a dense rival's 1 682.9 MB, and in 292.9 ms against 844.5 ms.
253
+
254
+ Where nothing is sparse, the alignment work is a cost with no corresponding saving. On a fully dense temporally-coupled model at 2 111 080 rows, the same comparison reverses: the rival builds the matrix three times faster, for seven per cent more resident memory.
255
+
256
+ Both rows are in the suite for the same reason. The [benchmark page](website/docs/explanation/what-the-numbers-measure.md) gives each figure, the baseline it is measured against, and what it does not claim.
257
+
258
+ ## Documentation
259
+
260
+ - [Get started](website/docs/get-started/index.md): installation, and the transport model above solved and read back.
261
+ - [Vocabulary](website/docs/vocabulary/index.md): the terms used throughout the documentation.
262
+ - [Tutorial](website/docs/tutorial/sets-and-parameters.md): the transport model built in six steps, one concept per page.
263
+ - Playground: every example runs in the browser and can be edited.
264
+ - [For agents](website/docs/for-agents.md): the mental model, the public surface and the failure modes on one page.
265
+
266
+ Every Python example on this site is executed by the test suite and shows the output it produced.
267
+
268
+ ## Licence
269
+
270
+ MIT. See `LICENSE`.
271
+
272
+ ## Citing
273
+
274
+ The package carries a `CITATION.cff`. Cite it by author, name and version:
275
+
276
+ > Gaete-Morales, Carlos. *nimopt* (version 0.1.0). MIT.
277
+
278
+ ## Contributing
279
+
280
+ Issues and patches are welcome once the repositories are published. Until then, the most useful contribution is a model that does not fit: the formulations that are awkward to write determine the next features.