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.
- nimopt-0.1.0/.claude/skills/nimopt/SKILL.md +180 -0
- nimopt-0.1.0/.github/workflows/pages.yml +60 -0
- nimopt-0.1.0/.gitignore +13 -0
- nimopt-0.1.0/.pre-commit-config.yaml +30 -0
- nimopt-0.1.0/CITATION.cff +11 -0
- nimopt-0.1.0/LICENSE +21 -0
- nimopt-0.1.0/PKG-INFO +280 -0
- nimopt-0.1.0/README.md +230 -0
- nimopt-0.1.0/benchmarks/bench_pypsa.py +143 -0
- nimopt-0.1.0/benchmarks/bench_pypsa_backend.py +264 -0
- nimopt-0.1.0/benchmarks/bench_storage.py +122 -0
- nimopt-0.1.0/benchmarks/bench_transport.py +122 -0
- nimopt-0.1.0/benchmarks/bench_vs_linopy.py +244 -0
- nimopt-0.1.0/benchmarks/compare.py +227 -0
- nimopt-0.1.0/benchmarks/data/README.md +24 -0
- nimopt-0.1.0/benchmarks/data/elec_s_10.npz +0 -0
- nimopt-0.1.0/benchmarks/data/elec_s_10_reference.json +114 -0
- nimopt-0.1.0/benchmarks/linopy_models.py +157 -0
- nimopt-0.1.0/benchmarks/ownership.py +79 -0
- nimopt-0.1.0/benchmarks/pdlp_gpu.py +317 -0
- nimopt-0.1.0/benchmarks/pypsa_fidelity.py +114 -0
- nimopt-0.1.0/benchmarks/pypsa_findings.md +72 -0
- nimopt-0.1.0/benchmarks/pypsa_network.py +841 -0
- nimopt-0.1.0/benchmarks/pypsa_reference.py +276 -0
- nimopt-0.1.0/pyproject.toml +129 -0
- nimopt-0.1.0/src/nimopt/__init__.py +55 -0
- nimopt-0.1.0/src/nimopt/absence.py +191 -0
- nimopt-0.1.0/src/nimopt/coefficient.py +345 -0
- nimopt-0.1.0/src/nimopt/constraint.py +202 -0
- nimopt-0.1.0/src/nimopt/definition.py +325 -0
- nimopt-0.1.0/src/nimopt/explanation.py +174 -0
- nimopt-0.1.0/src/nimopt/files.py +470 -0
- nimopt-0.1.0/src/nimopt/model.py +529 -0
- nimopt-0.1.0/src/nimopt/models/__init__.py +26 -0
- nimopt-0.1.0/src/nimopt/models/_arithmetic.py +35 -0
- nimopt-0.1.0/src/nimopt/models/commitment.py +95 -0
- nimopt-0.1.0/src/nimopt/models/dispatch.py +60 -0
- nimopt-0.1.0/src/nimopt/models/expansion.py +153 -0
- nimopt-0.1.0/src/nimopt/models/fleet.py +65 -0
- nimopt-0.1.0/src/nimopt/models/nodal.py +86 -0
- nimopt-0.1.0/src/nimopt/models/profiled.py +78 -0
- nimopt-0.1.0/src/nimopt/models/recourse.py +156 -0
- nimopt-0.1.0/src/nimopt/models/sector.py +103 -0
- nimopt-0.1.0/src/nimopt/models/storage.py +114 -0
- nimopt-0.1.0/src/nimopt/models/transport.py +114 -0
- nimopt-0.1.0/src/nimopt/names.py +8 -0
- nimopt-0.1.0/src/nimopt/param.py +145 -0
- nimopt-0.1.0/src/nimopt/progress.py +85 -0
- nimopt-0.1.0/src/nimopt/py.typed +0 -0
- nimopt-0.1.0/src/nimopt/row.py +191 -0
- nimopt-0.1.0/src/nimopt/session.py +216 -0
- nimopt-0.1.0/src/nimopt/sets.py +301 -0
- nimopt-0.1.0/src/nimopt/solution.py +111 -0
- nimopt-0.1.0/src/nimopt/solvers/__init__.py +75 -0
- nimopt-0.1.0/src/nimopt/solvers/base.py +111 -0
- nimopt-0.1.0/src/nimopt/solvers/gurobi.py +230 -0
- nimopt-0.1.0/src/nimopt/solvers/highs.py +261 -0
- nimopt-0.1.0/src/nimopt/solvers/mosek.py +292 -0
- nimopt-0.1.0/src/nimopt/solvers/options.py +135 -0
- nimopt-0.1.0/src/nimopt/spelling.py +264 -0
- nimopt-0.1.0/src/nimopt/symbol.py +109 -0
- nimopt-0.1.0/src/nimopt/term.py +553 -0
- nimopt-0.1.0/src/nimopt/variable.py +247 -0
- nimopt-0.1.0/tests/reference.py +21 -0
- nimopt-0.1.0/tests/test_absence.py +225 -0
- nimopt-0.1.0/tests/test_absence_rules.py +83 -0
- nimopt-0.1.0/tests/test_algebra_surface.py +165 -0
- nimopt-0.1.0/tests/test_alias.py +160 -0
- nimopt-0.1.0/tests/test_benchmarks_run.py +136 -0
- nimopt-0.1.0/tests/test_boundary_allocation.py +160 -0
- nimopt-0.1.0/tests/test_boundary_imports.py +64 -0
- nimopt-0.1.0/tests/test_boundary_public_api.py +336 -0
- nimopt-0.1.0/tests/test_bounds.py +205 -0
- nimopt-0.1.0/tests/test_capacity_expansion.py +80 -0
- nimopt-0.1.0/tests/test_coefficient.py +250 -0
- nimopt-0.1.0/tests/test_constant.py +74 -0
- nimopt-0.1.0/tests/test_constraint.py +214 -0
- nimopt-0.1.0/tests/test_definition.py +366 -0
- nimopt-0.1.0/tests/test_docs_contract.py +238 -0
- nimopt-0.1.0/tests/test_docs_examples.py +92 -0
- nimopt-0.1.0/tests/test_docs_surface.py +132 -0
- nimopt-0.1.0/tests/test_explanation.py +68 -0
- nimopt-0.1.0/tests/test_files.py +412 -0
- nimopt-0.1.0/tests/test_fixed_member.py +101 -0
- nimopt-0.1.0/tests/test_gurobi.py +182 -0
- nimopt-0.1.0/tests/test_lags.py +123 -0
- nimopt-0.1.0/tests/test_materialise.py +108 -0
- nimopt-0.1.0/tests/test_model_assemble.py +148 -0
- nimopt-0.1.0/tests/test_model_declare.py +174 -0
- nimopt-0.1.0/tests/test_model_explain.py +159 -0
- nimopt-0.1.0/tests/test_models.py +300 -0
- nimopt-0.1.0/tests/test_mosek.py +261 -0
- nimopt-0.1.0/tests/test_options.py +270 -0
- nimopt-0.1.0/tests/test_param.py +142 -0
- nimopt-0.1.0/tests/test_playground_wheels.py +79 -0
- nimopt-0.1.0/tests/test_progress.py +133 -0
- nimopt-0.1.0/tests/test_public_api.py +49 -0
- nimopt-0.1.0/tests/test_pypsa_fidelity.py +322 -0
- nimopt-0.1.0/tests/test_reading.py +199 -0
- nimopt-0.1.0/tests/test_row.py +133 -0
- nimopt-0.1.0/tests/test_session.py +220 -0
- nimopt-0.1.0/tests/test_sets.py +89 -0
- nimopt-0.1.0/tests/test_solution_dense.py +92 -0
- nimopt-0.1.0/tests/test_solve.py +150 -0
- nimopt-0.1.0/tests/test_solvers.py +132 -0
- nimopt-0.1.0/tests/test_spelling.py +311 -0
- nimopt-0.1.0/tests/test_term.py +206 -0
- nimopt-0.1.0/tests/test_variable.py +151 -0
- nimopt-0.1.0/tests/test_vs_linopy.py +70 -0
- nimopt-0.1.0/tests/test_where.py +187 -0
- nimopt-0.1.0/website/docs/explanation/a-variable-is-a-dimension.md +113 -0
- nimopt-0.1.0/website/docs/explanation/expressions-are-symbolic.md +102 -0
- nimopt-0.1.0/website/docs/explanation/the-array-is-the-matrix.md +78 -0
- nimopt-0.1.0/website/docs/explanation/the-package-boundary.md +81 -0
- nimopt-0.1.0/website/docs/explanation/what-the-numbers-measure.md +170 -0
- nimopt-0.1.0/website/docs/for-agents.md +181 -0
- nimopt-0.1.0/website/docs/get-started/index.md +129 -0
- nimopt-0.1.0/website/docs/guides/at-scale.md +118 -0
- nimopt-0.1.0/website/docs/guides/bounds-from-parameters.md +132 -0
- nimopt-0.1.0/website/docs/guides/coefficient-arithmetic.md +237 -0
- nimopt-0.1.0/website/docs/guides/conditions.md +194 -0
- nimopt-0.1.0/website/docs/guides/fixed-members.md +137 -0
- nimopt-0.1.0/website/docs/guides/highs-methods.md +177 -0
- nimopt-0.1.0/website/docs/guides/lags.md +163 -0
- nimopt-0.1.0/website/docs/guides/saving-and-loading.md +266 -0
- nimopt-0.1.0/website/docs/guides/subsets.md +172 -0
- nimopt-0.1.0/website/docs/index.md +236 -0
- nimopt-0.1.0/website/docs/models/commitment.md +89 -0
- nimopt-0.1.0/website/docs/models/dispatch.md +87 -0
- nimopt-0.1.0/website/docs/models/expansion.md +162 -0
- nimopt-0.1.0/website/docs/models/fleet.md +84 -0
- nimopt-0.1.0/website/docs/models/index.md +52 -0
- nimopt-0.1.0/website/docs/models/nodal.md +95 -0
- nimopt-0.1.0/website/docs/models/profiled.md +86 -0
- nimopt-0.1.0/website/docs/models/recourse.md +120 -0
- nimopt-0.1.0/website/docs/models/sector.md +90 -0
- nimopt-0.1.0/website/docs/models/storage.md +111 -0
- nimopt-0.1.0/website/docs/models/transport.md +91 -0
- nimopt-0.1.0/website/docs/nimblend/arrays.md +395 -0
- nimopt-0.1.0/website/docs/nimblend/domains.md +430 -0
- nimopt-0.1.0/website/docs/nimblend/index.md +42 -0
- nimopt-0.1.0/website/docs/reference/constraint.md +100 -0
- nimopt-0.1.0/website/docs/reference/definition.md +241 -0
- nimopt-0.1.0/website/docs/reference/explanation.md +78 -0
- nimopt-0.1.0/website/docs/reference/expression.md +369 -0
- nimopt-0.1.0/website/docs/reference/files.md +172 -0
- nimopt-0.1.0/website/docs/reference/inspection.md +157 -0
- nimopt-0.1.0/website/docs/reference/model.md +159 -0
- nimopt-0.1.0/website/docs/reference/param.md +206 -0
- nimopt-0.1.0/website/docs/reference/sets.md +148 -0
- nimopt-0.1.0/website/docs/reference/solution.md +104 -0
- nimopt-0.1.0/website/docs/reference/solvers.md +295 -0
- nimopt-0.1.0/website/docs/reference/variable.md +158 -0
- nimopt-0.1.0/website/docs/tutorial/constraints.md +189 -0
- nimopt-0.1.0/website/docs/tutorial/expressions.md +163 -0
- nimopt-0.1.0/website/docs/tutorial/reading-the-answer.md +218 -0
- nimopt-0.1.0/website/docs/tutorial/sets-and-parameters.md +172 -0
- nimopt-0.1.0/website/docs/tutorial/solving.md +146 -0
- nimopt-0.1.0/website/docs/tutorial/variables.md +96 -0
- nimopt-0.1.0/website/docs/vocabulary/index.md +138 -0
- nimopt-0.1.0/website/docusaurus.config.ts +42 -0
- nimopt-0.1.0/website/package.json +30 -0
- nimopt-0.1.0/website/pnpm-lock.yaml +11759 -0
- nimopt-0.1.0/website/scripts/README.md +12 -0
- nimopt-0.1.0/website/scripts/docs_blocks.py +83 -0
- nimopt-0.1.0/website/scripts/llms.mjs +124 -0
- nimopt-0.1.0/website/scripts/outputs.py +95 -0
- nimopt-0.1.0/website/scripts/readme.mjs +56 -0
- nimopt-0.1.0/website/scripts/skill.mjs +28 -0
- nimopt-0.1.0/website/scripts/solver_options.py +67 -0
- nimopt-0.1.0/website/scripts/surface.py +108 -0
- nimopt-0.1.0/website/scripts/watch.mjs +22 -0
- nimopt-0.1.0/website/sidebars.ts +91 -0
- nimopt-0.1.0/website/src/components/editor.js +39 -0
- nimopt-0.1.0/website/src/components/editor.module.css +28 -0
- nimopt-0.1.0/website/src/components/pyodide.js +71 -0
- nimopt-0.1.0/website/src/css/custom.css +9 -0
- nimopt-0.1.0/website/src/pages/playground.js +170 -0
- nimopt-0.1.0/website/src/pages/playground.module.css +38 -0
- nimopt-0.1.0/website/src/theme/CodeBlock/index.js +37 -0
- nimopt-0.1.0/website/src/theme/CodeBlock/styles.module.css +34 -0
- nimopt-0.1.0/website/static/img/favicon.svg +10 -0
- nimopt-0.1.0/website/static/llms-full.txt +7032 -0
- 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
|
nimopt-0.1.0/.gitignore
ADDED
|
@@ -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.
|