Skip to main content

The package boundary

Two packages, with the dependency in one direction. nimopt imports nimblend; nimblend never imports nimopt.

nimblend is a labeled sparse N-dimensional array. Its vocabulary is dimensions, labels, entries and alignment. It contains no optimization term. A function in it referring to a row, a column or a constraint is a boundary violation.

nimopt is an LP/MILP builder in which a variable is a dimension. Its types are nimblend arrays with names attached, and its matrix is stored in a nimblend.EntryBuffer.

The boundary is not a convention. It is enforced by tests that fail when it moves.

What the tests enforce​

nimblend never refers to nimopt. A scan of the nimblend source fails on any mention. A second test imports nimblend alone and fails where nimopt is imported with it. nimblend enforces the other half of the rule in its own suite: no class, function or parameter it declares is named for a constraint, an objective, a solver or a variable, and no source file mentions one in prose. A package developed on its own fails in its own suite, and not in the suite of a consumer.

Every nimblend import is a public name of the top-level module. A model imports SparseArray from nimblend, never from nimblend.sparse. Importing a public name by its submodule path is how a dependency on an internal starts. The modules nimopt ships are constrained further: the set of nimblend names they import is pinned, and widening it requires an explicit change to the test.

No array's .index or .data is read. Those are the raw index matrix and value buffer of the layer below the array layer. The rule is checked by walking the syntax tree, not by a text search. It detects a read that is not a subscript, and it permits dims.index(name), a position lookup on a tuple.

And none is assembled. Reading a buffer is one half of the bypass, and building one is the other. An index matrix assembled in nimopt is array work performed one layer too high. That work is part of the interface that moves when the kernel below nimblend is replaced. A domain returns the array over its own members instead: array(values) assigns a value to each member, and identity(into, coord, start) pairs each with its position along a new dimension. No module of nimopt calls SparseArray(index, ...) or from_canonical.

Where the bytes are allocated​

The strongest boundary test measures allocation. It does not compare the totals of the two packages.

The cost per nonzero of a build is allocated in nimblend: the matrix is an int32 column and a float64 value per entry, stored in a nimblend buffer. A ninefold increase in the nonzeros grows the nimblend share by at least twelve bytes per added entry.

nimopt allocates a near-constant amount across the same change. What it allocates is what a solver takes beside the matrix: a lower bound, an upper bound and an integrality flag per column, and a pair of bounds per row. The sets fix those counts, and the density does not.

The test therefore measures the gradient and not the totals. An assertion that nimblend allocates more bytes would measure the workload instead of the boundary. nimopt allocates a vector per column at every density, and a sparse enough model puts nimopt above nimblend with no boundary violation.

Why the site lives in nimopt​

nimopt imports nimblend. A site inside nimopt documenting both packages follows that dependency. A site inside nimblend documenting nimopt would invert it, and the nimblend tests would then require a model to describe.