API guide¶
Almost every use of ScoreQuant is built from a dozen names. The rest of the surface is machinery you reach for when you have a specific question about a result.
Core. The two tasks, optimize_partition and fit_quantizer; the inputs ScoreSample,
ObservationSample, IntegrationSource and ScoreSchema; the objectives DOptimality,
ProfiledDOptimality and NormalizedTrace; a solver configuration; a provider such as
ScoreFunction, LinearComponentScore or DensityRatioScore; and Quantizer, the rule you
deploy.
Advanced. Certificates and bounds (certify_partition, exchange_stability_report,
efficient_score_bound), the information algebra (fisher_information,
binned_fisher_information, information_report, profiled_information_report), the ratio
algebra (ratios_from_posteriors, mixture_scores_from_ratios, ratio_closure_report),
FisherTransform, the report types, and the plotting helpers. Each answers a question you will
know you have; none is needed to fit anything.
The sections below follow that order. The generated reference covers every public object.
Core¶
Top-level tasks¶
optimize_partition¶
optimize_partition(
scores, # ScoreSample, or a raw score array
*,
weights=None,
n_bins,
criterion=None,
config=None,
provenance=None,
initial_labels=None,
) -> PartitionResult
This is fixed-sample assignment. scores is either a ScoreSample — the same weighted score law
fit_quantizer takes, carrying its own weights, schema and provenance — or a raw score array with
weights and provenance supplied separately. Passing a sample together with either keyword is
rejected rather than silently resolved. An observation source is deliberately not accepted:
converting observations to scores stays an explicit provider.score(X), so the fixed-sample
boundary remains visible. It accepts DOptimality or ProfiledDOptimality with either
DExchangeConfig (exact positive-gain relocation) or MahalanobisLloydConfig (guarded
nearest-centroid batches). A batch is adopted only when the exactly rebuilt objective strictly
improves, because the frozen-metric batch step is not monotone on its own; with the default
guard="exchange" the labels are then finished by the exchange engine, so the terminal state is
exchange-stable.
initial_labels starts the solver from a supplied [N] labeling instead of its own seeding, which
is how efficient_score_bound(...).labels is used as an initializer. Zero-weight rows carry no
measure and their labels are ignored, identical score rows are merged before the solver runs and
must therefore already agree on their bin, and every requested cell must remain nonempty
afterwards. Supplied labels replace the seeding of the first exchange restart only, so init and
initializer_restarts still govern any further restart and one call can compare both starts; the guarded
Mahalanobis-Lloyd solver starts from them directly.
fit_quantizer¶
fit_quantizer(
source,
*,
provider=None,
validation=None,
n_bins,
criterion=None,
config=None,
diagnostics="endpoints",
) -> QuantizerResult
Supported pairs are D exchange, guarded Mahalanobis-Lloyd, soft D, normalized-trace k-means, and
exact scalar interval dynamic programming. The two finite D solvers take the same route: optimize
the labels, then compile the verified rule. ScoreSample forbids a provider; observation and
integration sources require one. Validation must use the same score dimension — and, when both
sides declare a ScoreSchema, the same parameter names, since a reordering is invisible to a
column count — and remains diagnostic.
diagnostics controls how many recorded center snapshots are re-scored into
trace.train_hard_retention and trace.validation_hard_retention: "final" scores only the
terminal snapshot, "endpoints" (the default) scores the first and terminal snapshots, and
"full" scores every snapshot, matching the historical behavior. Unscored snapshots hold nan so
the returned history stays aligned with trace.steps; centers, labels, and both reports are
unaffected.
ScalarDPConfig pairs with DOptimality only and requires the effective score space to be rank
one after rank_rtol projection; a higher rank is rejected by name. On that rank the D-optimal
partition has ordered interval cells, so the weighted interval dynamic program returns the global
optimum rather than a local one, and max_rows bounds its exact quadratic work.
Naming the score coordinates¶
A score matrix is a table of partial derivatives, one column per model parameter. The order is
meaningful but invisible, which is bearable in a two-parameter toy and dangerous in a real problem
with dozens of components. ScoreSchema declares the names:
schema = sq.ScoreSchema(("T cells", "B cells", "monocytes", "mast cells", "HSPCs"))
sample = sq.ScoreSample(scores, weights, schema=schema)
criterion = sq.ProfiledDOptimality(interest=("HSPCs",))
Names are resolved to score columns exactly once, at the public task boundary, so every solver,
report and certificate downstream still works in indices. ProfiledDOptimality accepts either
form and refuses a mixture of the two; declaring names against a sample that carries no schema
fails by name rather than guessing. Reports then print interest: HSPCs instead of (4,).
A schema answers only what each coordinate means. Where the numbers came from and at which
reference point stays with ScoreProvenance, and the two are validated against each other rather
than each carrying a reference point that can drift.
Providers may supply the schema for the observation-space routes. LinearComponentScore derives
it from the component names the model already declares; ScoreFunction, DensityRatioScore and
CentralLogRatioScore accept schema= explicitly. It then reaches PartitionResult.schema and
QuantizerResult.schema.
The score-provider contract¶
ScoreProvider is a runtime-checkable protocol, not a closed union. Any object with a
provenance and a score method is a provider:
class MyExternalScore:
provenance = sq.ScoreProvenance(kind="estimated_ratio")
def score(self, observations):
return my_package.evaluate(observations)
score takes observations alone; the execution backend is ambient context established by the
public task, so an external implementation needs no knowledge of ExecutionConfig. A provider may
also expose schema, which is used when present and is not part of the required contract. The
four built-in providers are convenience implementations of this protocol.
Sources, providers, and density ratios¶
Sources carry the measure (ScoreSample, ObservationSample, IntegrationSource); providers
carry the observation-to-score map (ScoreFunction, LinearComponentScore, DensityRatioScore,
CentralLogRatioScore). Importance ratios for a sample drawn from a proposal distribution belong
in the source weights, never in a provider.
DensityRatioScore(ratio, parameterization, *, provenance=None) pairs a model density-ratio
callback [N, D] -> [N, K] with a declared ratio-to-score map: MixtureParameterization for a
normalized mixture with one simplex-dependent component (K - 1 score columns) or
IntensityParameterization for an extended linear-intensity model (all K columns).
DensityRatioScore.from_classifier(predict, class_priors, parameterization, ...) builds the ratio
callback from calibrated posteriors as ratios_from_posteriors(predict(X), class_priors) and
always records estimated provenance. CentralLogRatioScore(predict, deltas, class_priors) turns
paired minus/plus probabilities from classifiers trained at \(\theta_0\pm\delta\) into central
finite-difference scores.
The underlying array algebra is public: ratios_from_posteriors(posteriors, class_priors),
mixture_scores_from_ratios(ratios, reference_fractions, reference_component=-1), and
scores_from_components(components, coefficients) — the latter doubles as the intensity
ratio-to-score map because it is invariant under a common event-wise rescaling of a row.
ratio_closure_report(ratios, weights) checks that each ratio column integrates to one under the
declared measure and returns the per-component normalizers with their largest absolute residual.
ScoreProvenance(kind, ...) accepts "unknown", "exact", "autodiff", "estimated_ratio",
and "custom_estimated"; exact_fisher is derived from kind and cannot be set independently.
Ratio-derived scores carry a structured ScoreProvenance.ratio record (RatioProvenance) naming
the estimator, parameterization, reference fractions or coefficients, reference component,
training priors, calibration method, and finite-difference offsets.
The deployable rule¶
A fit produces two different things and they now have two different types. QuantizerResult is
the record of the fit — labels, reports, trace, solver diagnostics. result.quantizer is the rule
itself:
fit = sq.fit_quantizer(sample, n_bins=8)
fit.train_report # what the fit measured
fit.trace # how it got there
rule = fit.quantizer # what you deploy
rule.predict_scores(future_scores)
rule.save("flowcyt-8bins.sqz")
Quantizer carries the transform, centers, optional metric, score schema, provenance and
criterion — and nothing about the training sample. PartitionResult.compile_quantizer() returns
one too, under exactly the same theorem-backed conditions as before: compilation is bookkeeping on
an exchange-stable D partition, not a second fit, so it hands back a rule rather than something
that impersonates a new training result.
QuantizerResult.predict_scores remains the one prediction verb and delegates to the rule;
centers, metric, transform and schema read through to it.
The artifact format¶
Quantizer.save writes a zip archive holding one manifest.json and one .npy member per array.
- Versioned. The manifest declares a
format_version, and a reader refuses a version it does not know by name rather than interpreting a field it has never seen. - Not a pickle. Arrays are written with
allow_pickle=False, so loading an artifact executes no code from the file. - Backend-free. A rule fitted on the JAX backend loads and predicts in a process where JAX is not importable, which is the other half of the NumPy backend: fit offline, deploy where the fitting stack is not installed.
rule = sq.Quantizer.load("flowcyt-8bins.sqz")
bins = rule.predict_scores(scores, execution=sq.ExecutionConfig(backend="numpy"))
This is a durable format, unlike to_dict() on the result types, which stays JSON-ready
diagnostic state.
Advanced¶
The efficient-score upper bound¶
efficient_score_bound(
scores,
*,
interest,
weights=None,
n_bins,
config=None,
) -> EfficientScoreBound
Efficient-score domination bounds the profiled information of every n_bins-cell rule of the full
score space by the between-cell information of the full-data efficient score under that rule. For
one parameter of interest the maximizing rule has ordered interval cells and is found exactly, so
upper_bound is a certificate rather than an estimate, reported in the same log-determinant
convention as PartitionResult.objective under ProfiledDOptimality. gap_to(partition_result)
returns the remaining slack and is nonnegative up to floating-point error; labels doubles as the
initial_labels initializer for profiled exchange. More than one interest column raises
NotImplementedError, because a multivariate efficient score would need a multivariate solver and
the result would no longer be certified.
Certificates¶
exchange_stability_report(
scores,
labels,
*,
weights=None,
criterion=None,
rank_rtol=None,
gain_tolerance=1e-10,
) -> StabilityReport
certify_partition(
scores,
*,
weights=None,
n_bins,
incumbent=None,
criterion=None,
rank_rtol=None,
config=None,
) -> PartitionCertificate
exchange_stability_report runs exactly one complete exact scan of a supplied labeling and
nothing else, so labels from any source — a guard="reject" batch result, an external tool, a
hand edit — can be checked before they are trusted. It reports the exact objective, the best
remaining gain, and the improving (row, destination) move when one exists. The cell count comes
from the labels, and every declared cell must hold positive weight. Stability is always a verdict
at a tolerance: match gain_tolerance to the configuration that produced the labels, and read it
back from StabilityReport.gain_tolerance. Pass gain_tolerance=0.0 to ask the strict question
instead.
certify_partition decides global optimality by branch and bound with the singleton-completion
upper bound: unassigned atoms are left as singleton cells, so refinement monotonicity of the log
determinant bounds every completion of a partial assignment. It starts from an incumbent — normally
PartitionResult.labels, otherwise one default exchange — and returns status="optimal" only when
the tree was exhausted; a spent node budget returns status="budget_exhausted" with the best
outstanding bound and the remaining gap. The search is exponential, so CertificationConfig
guards both the node count and the number of distinct score atoms, refusing an oversized instance
by name. Certification is DOptimality only: the refinement bound uses Loewner monotonicity of
logdet, which the profiled Schur objective does not inherit, and a profiled criterion is rejected
rather than approximated. Neither entry point ever runs implicitly during fitting.
Result semantics¶
PartitionResult has labels, cell statistics, information matrices, rank, accepted_moves,
scans, lloyd_iterations, accepted_lloyd_steps, exchange_stable, and best_remaining_gain,
but no prediction method. One scan is one complete evaluation of every admissible relocation; with
the default batch_moves a single scan may relocate many rows, so accepted_moves normally
exceeds scans. The two Lloyd counters stay zero unless the guarded batch solver ran, and
objective_history records every accepted step of every phase in order. Its compile_quantizer()
rejects an unstable or geometrically degenerate result.
Geometry diagnostics are criterion-specific and never shared. A DOptimality result carries
geometry, a GeometryReport measuring the largest Mahalanobis-Voronoi violation of the terminal
metric, the guaranteed log-determinant gain such a violation would leave on the table, the largest
exact gain it actually leaves, and the cell-separation residual against the leverage bound; a
ProfiledDOptimality result carries profiled_geometry instead and leaves geometry as None.
The two describe different objects — a strict Voronoi rule that exchange stability forces, and an
efficient semimetric whose Voronoi rule a stable profiled partition may violate — so one shared
name would claim an implication that does not hold.
GeometryReport is a certificate at a tolerance, recorded as gain_tolerance and taken from the
configuration that produced the labels. voronoi_consistent means maximum_violation_gain <=
gain_tolerance: no Voronoi violation is worth more than the threshold the solver stopped at. It is
not a claim at tolerance zero, and on a large sample it must not be — the Theorem-3 guaranteed gain
falls like \(1/N^2\), so past roughly a million rows a few rows legitimately sit a hair past a cell
boundary inside the default \(10^{-10}\). compile_quantizer() reads that certificate rather than
demanding bit-exact label reproduction, so on such a partition predict_scores on the training
scores differs from PartitionResult.labels on those boundary rows and nowhere else; the
partition's labels stay authoritative for the fixed sample. Assignment itself is unchanged —
argmin decides, resolving a tie toward the lowest cell index.
QuantizerResult.predict_scores(scores) is the only prediction method. evaluate_scores assigns
new scores with the frozen rule and computes supplied-score information. The stored transform,
centers, and optional common metric define its score-space geometry; rank, train/validation
reports, hardening gap, solver contract, source kind, and score provenance remain inspectable.
OptimizationTrace.objective_label names the units of the recorded objective, because solvers do
not share one convention: "whitened_sse" is a minimized weighted within-cell squared error in
Fisher-whitened coordinates, while "logdet_retained" and "profiled_logdet" are maximized log
determinants. Two traces are comparable only under the same label.
Both result types expose information_kind: it is exact_fisher only for exact/autodiff
provenance and supplied_score_surrogate otherwise.
Shape and measure contracts¶
- Scores: finite
[N, P],N > 0,P > 0. - Observations: finite
[N, D]. - Weights: finite nonnegative
[N]with at least one positive value. - Classifier central probabilities: positive
[N, P, 2], normalized on the final axis. - Multiclass posteriors: nonnegative
[N, K], row-normalized, with positive normalized priors. - Model density ratios: finite nonnegative
[N, K], defined up to a common event-wise factor. - Integration bounds: finite
[D, 2]with strictly ordered endpoints and an explicit density.
Numerical null directions are projected out. Scores are never centered. to_dict() returns
JSON-ready diagnostic state but is not a durable artifact format.