Metadata-Version: 2.4
Name: stresspy
Version: 0.0.5
Summary: Core numerical routines for the Geometric Stress Criterion
Author: Dan James
License-Expression: PolyForm-Noncommercial-1.0.0
Project-URL: Homepage, https://pypi.org/project/stresspy/
Project-URL: Documentation, https://pypi.org/project/stresspy/
Keywords: geometric stress criterion,model diagnostics,tangent space,structural inadequacy,scientific computing
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Provides-Extra: jax
Requires-Dist: jax; extra == "jax"
Dynamic: license-file

## What is stressPy?

When a model disagrees with experimental data, conventional goodness-of-fit measures quantify the size of the discrepancy but do not reveal its geometric character. In particular, they do not distinguish between discrepancies aligned with the model’s available parameter-response directions and discrepancies lying outside those directions.

StressPy applies the Geometric Stress Criterion (GSC) to separate a model–data discrepancy into two components:

When a model disagrees with experimental data, conventional goodness-of-fit measures quantify the size of the discrepancy but do not reveal its geometric character. In particular, they do not distinguish between discrepancies aligned with the model’s available parameter-response directions and discrepancies lying outside those directions.

StressPy applies the Geometric Stress Criterion (GSC) to separate a model–data discrepancy into two components:

## Tangent stress 
— locally parameter-accessible discrepancy: The part aligned with changes the model can produce, to first order, by varying its existing parameters near the current parameter point. This component may be reducible through parameter adjustment, although local accessibility does not guarantee a practical finite repair.
## Normal stress 
— locally inaccessible discrepancy: The part orthogonal to the model’s local parameter-response directions in the selected observation metric. It cannot be removed by an infinitesimal parameter adjustment at the current point and can therefore provide evidence that the model structure, observation mapping, or represented mechanisms require reconsideration.

StressPy complements conventional goodness-of-fit measures such as RMSE and chi-squared error, as well as model-selection criteria such as AIC and BIC. These established methods quantify overall fit or balance fit against model complexity. StressPy addresses a different question: how much of the observed discrepancy is aligned with the model’s locally accessible parameter-response directions?

By distinguishing these components, stresspy helps scientists, engineers and model developers assess whether further parameter optimisation is promising or whether the model itself should be investigated. The result is a local geometric diagnostic whose interpretation depends on the chosen parameter point, observational weighting and numerical-rank threshold.

# StressPy

**Core numerical routines for the Geometric Stress Criterion (GSC).**

`stresspy` decomposes a model--data discrepancy into a component aligned with
the model's local parameter-accessible tangent space and a component normal to
that space. It also reports numerical rank, singular values, the normal
fraction, conditioning and a minimum-norm local repair vector.

StressPy is an early reference implementation. It performs deterministic local
geometry and can construct a Jacobian from a Python model function. It does not
fit models, solve ODEs on the user's behalf or perform bootstrap calibration.

## Installation

```bash
pip install stresspy
```

## Quick start

```python
import numpy as np
from stresspy import evaluate_gsc

def model(parameters, x):
    intercept, slope = parameters
    return intercept + slope * x

x = np.array([-1.0, 0.0, 1.0, 2.0])
parameters = np.array([1.0, 0.5])
observed = np.array([0.45, 1.10, 1.70, 2.35])
sigma = np.full(observed.size, 0.10)

result = evaluate_gsc(
    model_func=model,
    parameters=parameters,
    observed=observed,
    model_args=(x,),
    sigma=sigma,
)

print("Predictions:", result.predicted)
print("Jacobian:\n", result.jacobian)
print("Tangent stress:", result.tangent_stress)
print("Normal stress:", result.normal_stress)
print("Normal fraction (%):", result.normal_fraction_pct)
print("Numerical rank:", result.rank)
print("Repair vector:", result.repair_vector)
```

`evaluate_gsc` evaluates the model, constructs its Jacobian and performs the
decomposition in one call. Its default Jacobian method is dependency-free
forward finite differencing.

## Jacobian construction

The model function must accept the parameter vector as its first argument and
return one finite prediction per observation. Additional inputs can be supplied
through `model_args` and `model_kwargs`.

Forward finite differences are the default:

```python
result = evaluate_gsc(
    model,
    parameters,
    observed,
    model_args=(x,),
    jacobian_method="forward",
)
```

Central finite differences require twice as many perturbed model evaluations
but commonly improve derivative accuracy:

```python
result = evaluate_gsc(
    model,
    parameters,
    observed,
    model_args=(x,),
    jacobian_method="central",
)
```

StressPy chooses parameter-scaled finite-difference steps from machine
precision. A positive scalar or one step per parameter can instead be supplied
with `step`.

Finite differences assume that model outputs are deterministic and locally
smooth at the supplied parameter point. Discontinuities, solver failures,
stochastic simulations and poorly scaled parameters can make a numerical
Jacobian unreliable. Important analyses should be repeated with alternative
step sizes or central differences as a sensitivity check.

The Jacobian and repair vector use exactly the coordinates supplied in
`parameters`. To work in log-parameter coordinates, pass log parameters to a
model wrapper that exponentiates them before evaluating the underlying model.

The adapters can also be used independently:

```python
from stresspy import finite_difference_jacobian

jacobian = finite_difference_jacobian(
    model,
    parameters,
    method="central",
    model_args=(x,),
)
```

### Optional JAX automatic differentiation

Install the optional dependency with:

```bash
pip install "stresspy[jax]"
```

Then use a JAX-traceable model written with `jax.numpy` operations:

```python
result = evaluate_gsc(
    jax_model,
    parameters,
    observed,
    jacobian_method="jax",
)
```

StressPy never silently substitutes finite differences when JAX is explicitly
requested. An informative error is raised if JAX is unavailable or the model
cannot be differentiated by JAX.

## Analysis from a precomputed residual and Jacobian

When predictions and the Jacobian have already been calculated, use `analyze`:

```python
from stresspy import analyze

residual = observed - predicted
result = analyze(residual, jacobian, sigma=sigma)
```

## Weighting

An unweighted Euclidean analysis requires no additional argument:

```python
result = analyze(residual, jacobian)
```

Independent observational standard deviations can be supplied with `sigma`:

```python
result = analyze(residual, jacobian, sigma=sigma)
```

An optional absolute or quantile-based lower floor can prevent extremely small
standard deviations from dominating the observation metric:

```python
absolute_floor = analyze(
    residual,
    jacobian,
    sigma=sigma,
    sigma_floor=0.05,
)

quantile_floor = analyze(
    residual,
    jacobian,
    sigma=sigma,
    sigma_floor_quantile=0.10,
)
```

Positive diagonal precision weights may be supplied directly. They define the
metric `sum(weights * residual**2)` and are equivalent to
`sigma = 1 / sqrt(weights)`:

```python
weights = 1.0 / sigma**2
result = analyze(residual, jacobian, weights=weights)
```

For correlated observations, supply a positive-definite covariance matrix:

```python
result = analyze(residual, jacobian, covariance=covariance)
```

Supply only one of `sigma`, `weights`, `covariance` or `whitener`. Weighting is
part of the geometry: different defensible metrics can produce different
tangent--normal decompositions and should be reported explicitly.

The discrepancy convention is

\[
r = y - f(\hat{\theta}).
\]

With observation-space whitening matrix \(L\), StressPy forms
\(r_W=Lr\) and \(J_W=LJ\). If \(U_r\) contains the retained left singular
vectors of \(J_W\), then

\[
r_{\parallel,W}=U_rU_r^\top r_W,
\qquad
r_{\perp,W}=r_W-r_{\parallel,W}.
\]

The squared norms give total, tangent and normal stress. The minimum-norm local
repair is calculated in the parameter coordinates represented by the supplied
Jacobian. Consequently, repair magnitude is coordinate-dependent, and local
tangent accessibility does not guarantee a practical finite nonlinear repair.

## Principal functions

- `evaluate_gsc`: evaluate a Python model, construct its Jacobian and perform
  the complete GSC decomposition.
- `finite_difference_jacobian`: dependency-free forward or central numerical
  differentiation.
- `jax_jacobian`: optional forward-mode automatic differentiation using JAX.
- `analyze`: recommended high-level analysis from a residual and Jacobian,
  including common weighting and uncertainty-floor options.
- `decompose`: single tangent--normal decomposition with optional uncertainty
  or covariance weighting.
- `decompose_blocks`: joint interrogation of multiple independent observation
  blocks sharing the same parameter coordinates.
- `floor_sigma`: explicit uncertainty-floor preprocessing.
- `jacobian_to_log_coordinates`: conversion of selected Jacobian columns to
  log-parameter coordinates.

`analyze` and `decompose` return an immutable `GSCResult`. `evaluate_gsc`
returns its subclass `GSCEvaluationResult`, which adds the parameter vector,
observations, predictions, constructed Jacobian, Jacobian method and numerical
steps while preserving direct access to every geometric result field.

## Interpretation

Normal stress measures discrepancy outside the retained local Jacobian column
space in the selected observation metric. It is a local geometric diagnostic,
not by itself a calibrated hypothesis test. Conclusions can depend on the
chosen weighting, parameter point and singular-value threshold.

## Licence and commercial use

StressPy is available under the
[PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0).
It may be used, studied, modified and redistributed for permitted
non-commercial purposes under those terms. Commercial use requires separate
written permission from the copyright holder.

## Citation

If StressPy contributes to academic work, please cite the software and the
associated GSC publication when available. Citation metadata is provided in
`CITATION.cff`.

Suggested software citation:

> James, D. (2026). *StressPy: Core numerical routines for the Geometric Stress
> Criterion* (Version 0.0.5) [Computer software].
> https://pypi.org/project/stresspy/
