Linear solves and diagnostics¶
Solver choice and solve diagnostics for the inference's linear systems.
Every tension and pressure of this package is the solution of a linear least-squares system. Small
systems are solved densely (scipy.linalg.lstsq), exactly; large ones sparsely (lsqr, or a
sparse direct factorisation), because a dense system grows as the product of its dimensions in
memory and as their cube in time: the Young-Dupre system of a 1 866-cell tissue is 49 984 x 11 746,
4.7 GB dense, and at 10 000 cells it would be some 120 GB.
This module holds what the public entry points (infer_tensions*, infer_pressures*,
infer_forces) pass down to every solve:
solver:"auto"(the default) solves a system densely belowsparse_thresholdunknowns and sparsely at or above it;"dense"and"sparse"force one path whatever the size. Below the threshold,"auto"is bitwise the dense solve. Forcing"dense"on a system whose dense form would not fit in the memory available emits aDenseSolveMemoryWarningwith the estimate, before anything is allocated. The tension methodsEquilibrium,VariationalandForceBalance*have no sparse path: they are solved densely under"auto", and"sparse"raisesValueErrorfor them. A sparselsqrsolve that does not converge falls back to the dense one with aSparseSolveFallbackWarning(or, when the dense one cannot fit in memory, returnslsqr's last iterate with that warning).sparse_threshold: the number of unknowns at which"auto"switches (defaultfoambryo.junction_graph.SPARSE_SOLVE_MIN_UNKNOWNS, 2 000). What counts as "unknowns" is the number of columns of the system the dense path would assemble: the interfaces of one junction-graph component for a tension solve, the materials for a Laplace pressure, the interfaces plus the cells for the variational pressure. One exception: the variational pressure of a cropped mesh is solved densely up tofoambryo.pressure_inference.DENSE_CROP_PRESSURE_MAX_ENTRIESmatrix entries under"auto", whateversparse_thresholdis.diagnostics:False(the default: nothing is computed and each entry point returns its usual values),True(oneSolveRecordper solve: solver, shape, rank, residual norm, condition number, time) or"spectrum"(also the extreme singular values of the large, sparse systems, byscipy.sparse.linalg.svds, at a measured extra cost). With diagnostics on, each entry point returns one more value, aSolveDiagnostics, last;save_solve_diagnosticswrites it to a JSON file (the scalars) and an NPZ file (the singular values).
They are keyword-only on every entry point that takes them, and an invalid value raises
ValueError (validate_solve_options). A lower-level function called directly (for example
infer_tensions_young_dupre_local) uses the options in force, the defaults outside any entry point;
solve_options puts others in force around such a call.
What the condition numbers are. On a dense least-squares solve, the 2-norm condition number
s_max / s_min from the singular values scipy.linalg.lstsq computes anyway (free; inf when the
smallest is zero). On an lsqr solve, lsqr's own running estimate of the Frobenius-norm
condition number ||A||_F ||A^+||_F (free; an estimate, and an over-estimate of the 2-norm one by
up to a factor n). On a sparse direct solve, the 1-norm condition number estimated from the
factorisation (Hager-Higham, scipy.sparse.linalg.onenormest; cheap). The source is recorded
beside every number: the three are not the same quantity.
How the options travel. A public entry point sets them in a context variable for the duration
of its call, and the solve helpers read it, so the options reach every solve without threading an
argument through each builder. The context is per thread and per asynchronous task
(contextvars); outside any entry point the defaults apply.
DenseSolveMemoryWarning ¶
Bases: UserWarning
A dense solve was forced on a system whose dense form exceeds the memory available.
Emitted before anything is allocated, with the estimate, so that the caller can stop the run
(turn it into an error with warnings.simplefilter("error", DenseSolveMemoryWarning)). Only
solver="dense" emits it: "auto" never chooses the dense path for a large system.
SolveOptions
dataclass
¶
SolveOptions(solver: str = 'auto', sparse_threshold: int | None = None, diagnostics: bool | str = False)
The solver options in force for the solves of one entry-point call.
Attributes:
| Name | Type | Description |
|---|---|---|
solver |
str
|
|
sparse_threshold |
int | None
|
unknowns at which |
diagnostics |
bool | str
|
|
SolveRecord
dataclass
¶
SolveRecord(system: str, solver: str, n_rows: int, n_unknowns: int, rank: int | None = None, residual_norm: float | None = None, condition_number: float | None = None, condition_number_source: str | None = None, seconds: float | None = None, lsqr_istop: int | None = None, lsqr_iterations: int | None = None, spectrum_seconds: float | None = None, spectrum_note: str | None = None, singular_values: NDArray[float64] | None = None)
What one linear solve did. Scalars are JSON-ready; the singular values go to NPZ.
Attributes:
| Name | Type | Description |
|---|---|---|
system |
str
|
which system, e.g. |
solver |
str
|
the solver that produced the solution ( |
n_rows |
int
|
rows of the system solved (after any reduction, e.g. undetermined cells left out). |
n_unknowns |
int
|
columns of the system solved. |
rank |
int | None
|
numerical rank, where the solver computes it (dense |
residual_norm |
float | None
|
|
condition_number |
float | None
|
see the module docstring for what each source means; |
condition_number_source |
str | None
|
how |
seconds |
float | None
|
wall time of the solve, including the diagnostics' own cost. |
lsqr_istop, |
lsqr_iterations
|
|
spectrum_seconds |
float | None
|
time spent on |
spectrum_note |
str | None
|
why a requested spectrum is missing or partial, if it is. |
singular_values |
NDArray[float64] | None
|
the singular values in decreasing order: all of them on a dense
|
scalars ¶
scalars() -> dict[str, Any]
The record without its singular values, with non-finite floats as strings (JSON-safe).
SolveDiagnostics
dataclass
¶
SolveDiagnostics(options: SolveOptions, records: list[SolveRecord] = list())
The records of every solve one entry-point call ran, in the order they ran.
Attributes:
| Name | Type | Description |
|---|---|---|
options |
SolveOptions
|
the options in force. |
records |
list[SolveRecord]
|
one |
table ¶
table() -> list[dict[str, Any]]
The records' scalars, one dict per solve (what the JSON file holds).
save_solve_diagnostics ¶
save_solve_diagnostics(diagnostics: SolveDiagnostics, path_stem: str | PathLike[str]) -> tuple[Path, Path]
Write solve diagnostics to <path_stem>.json (scalars) and <path_stem>.npz (spectra).
The JSON file holds the options and one object per solve (SolveRecord.scalars, non-finite
numbers as the strings "inf"/"nan"). The NPZ file holds, for every solve that has them, its
singular values under the key singular_values_<index>, <index> being the solve's position in
the JSON list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
diagnostics
|
SolveDiagnostics
|
what an entry point returned with |
required |
path_stem
|
str | PathLike[str]
|
path without extension; parent directories must exist. |
required |
Returns:
| Type | Description |
|---|---|
tuple[Path, Path]
|
tuple[Path, Path]: the JSON path and the NPZ path. |
validate_solve_options ¶
validate_solve_options(solver: str, sparse_threshold: int | None, diagnostics: bool | str) -> SolveOptions
Check the three options and return them as a SolveOptions.
Raises:
| Type | Description |
|---|---|
ValueError
|
on a |
solve_options ¶
solve_options(solver: str = 'auto', sparse_threshold: int | None = None, diagnostics: bool | str = False) -> Iterator[SolveDiagnostics | None]
Put solver options in force for the solves run inside the block.
Used by the public entry points; usable directly around any lower-level call. Yields the
SolveDiagnostics the solves inside fill, or None when diagnostics is False.
Raises:
| Type | Description |
|---|---|
ValueError
|
see |
current_solve_options ¶
current_solve_options() -> SolveOptions
The options in force (the defaults outside any entry point).
collecting_diagnostics ¶
collecting_diagnostics() -> bool
Whether the solves now running should build SolveRecords.
record_solve ¶
record_solve(record: SolveRecord) -> None
Append record to the diagnostics being collected; nothing when none are.
sparse_threshold_in_force ¶
sparse_threshold_in_force() -> int
The sparse_threshold in force, the package default when the caller gave none.
use_sparse_solver ¶
use_sparse_solver(n_unknowns: int) -> bool
Whether the system of n_unknowns unknowns takes the sparse path under the options in force.
require_dense_only_method_is_not_forced_sparse ¶
require_dense_only_method_is_not_forced_sparse(method: str) -> None
Refuse solver="sparse" for a method that has no sparse path, rather than run it densely.
Raises:
| Type | Description |
|---|---|
ValueError
|
when |
available_memory_bytes ¶
available_memory_bytes() -> int | None
The memory a new allocation can use without swapping, as well as this platform tells.
psutil.virtual_memory().available when psutil is installed (it is not a dependency); else
MemAvailable from /proc/meminfo on Linux; else the total physical memory (os.sysconf),
an upper bound, on platforms that expose no available-memory figure without psutil (macOS).
None when nothing is readable.
dense_solve_bytes ¶
dense_solve_bytes(n_rows: int, n_cols: int, copies: int = _DENSE_COPIES) -> int
Estimated bytes of a dense float64 least-squares solve of an n_rows x n_cols system.
warn_if_forced_dense_exceeds_memory ¶
warn_if_forced_dense_exceeds_memory(n_rows: int, n_cols: int, system: str, *, copies: int = _DENSE_COPIES, stacklevel: int = 3) -> None
Emit DenseSolveMemoryWarning when solver="dense" is forced on a system too large for memory.
Call it before allocating the dense matrix. Does nothing under "auto" or "sparse".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n_rows
|
int
|
rows of the dense system. |
required |
n_cols
|
int
|
columns of the dense system. |
required |
system
|
str
|
what the system is, for the message. |
required |
copies
|
int
|
how many dense copies of that shape the solve holds at once. |
_DENSE_COPIES
|
stacklevel
|
int
|
passed to |
3
|
condition_from_singular_values ¶
condition_from_singular_values(singular_values: NDArray[float64]) -> float | None
2-norm condition number s_max / s_min (inf if s_min is 0, None if there are none).
extreme_singular_values ¶
extreme_singular_values(operator: spmatrix | sparray, *, symmetric_positive: bool = False) -> tuple[NDArray[np.float64] | None, float, str | None]
The largest and smallest SPECTRUM_EXTREME_VALUES singular values of a sparse operator.
The largest by scipy.sparse.linalg.svds (ARPACK, which="LM"). The smallest first by svds
with which="SM"; if ARPACK does not converge within SPECTRUM_MAX_ITERATIONS, by shift-invert
at zero (eigsh(..., sigma=0), a sparse LU factorisation) of the operator itself when it is
symmetric positive (symmetric_positive: its singular values are its eigenvalues) or of the
normal matrix A^T A otherwise. The normal matrix squares the condition number, so those values
are accurate only down to about 1e-8 of the largest; the note says which route gave them.
Never raises on a failure: what is missing is said in the note.
Returns:
| Name | Type | Description |
|---|---|---|
tuple |
NDArray[float64] | None
|
the values in decreasing order ( |
float
|
a note ( |
record_dense_lstsq ¶
record_dense_lstsq(system: str, solver: str, dense: NDArray[float64], vector: NDArray[float64], x: NDArray[float64], residuals: NDArray[float64], rank: int, singular_values: NDArray[float64], start: float, *, lsqr_istop: int | None = None, lsqr_iterations: int | None = None) -> dict[str, Any]
Append the SolveRecord of a dense scipy.linalg.lstsq solve; return its scalar summary.
Reads the solve's own outputs and never alters x. The residual norm is the square root of the
residual lstsq returned (a 0-d value for a 1-D right-hand side, shape (1,) from the sparse
path) when it is a finite number, else (rank-deficient system) computed as
||vector - dense @ x||.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
system
|
str
|
which system, see |
required |
solver
|
str
|
which solver, see |
required |
dense
|
NDArray[float64]
|
the matrix solved. |
required |
vector
|
NDArray[float64]
|
the right-hand side. |
required |
x
|
NDArray[float64]
|
|
required |
residuals
|
NDArray[float64]
|
|
required |
rank
|
int
|
|
required |
singular_values
|
NDArray[float64]
|
|
required |
start
|
float
|
|
required |
lsqr_istop
|
int | None
|
|
None
|
lsqr_iterations
|
int | None
|
|
None
|
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict[str, Any]
|
|
dict[str, Any]
|
|