scientificfitting 0.2.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.
- scientificfitting-0.2.0/LICENSE +21 -0
- scientificfitting-0.2.0/PKG-INFO +86 -0
- scientificfitting-0.2.0/README.md +56 -0
- scientificfitting-0.2.0/pyproject.toml +41 -0
- scientificfitting-0.2.0/scientificfitting/__init__.py +34 -0
- scientificfitting-0.2.0/scientificfitting/_bridge.jl +234 -0
- scientificfitting-0.2.0/scientificfitting/_core.py +463 -0
- scientificfitting-0.2.0/scientificfitting/_diagnostic_plots.py +302 -0
- scientificfitting-0.2.0/scientificfitting/_inputs.py +120 -0
- scientificfitting-0.2.0/scientificfitting/_plotting.py +171 -0
- scientificfitting-0.2.0/scientificfitting/_results.py +263 -0
- scientificfitting-0.2.0/scientificfitting/_runtime.py +21 -0
- scientificfitting-0.2.0/scientificfitting/juliapkg.json +9 -0
- scientificfitting-0.2.0/scientificfitting.egg-info/PKG-INFO +86 -0
- scientificfitting-0.2.0/scientificfitting.egg-info/SOURCES.txt +24 -0
- scientificfitting-0.2.0/scientificfitting.egg-info/dependency_links.txt +1 -0
- scientificfitting-0.2.0/scientificfitting.egg-info/requires.txt +18 -0
- scientificfitting-0.2.0/scientificfitting.egg-info/top_level.txt +1 -0
- scientificfitting-0.2.0/setup.cfg +4 -0
- scientificfitting-0.2.0/tests/test_covariance.py +191 -0
- scientificfitting-0.2.0/tests/test_examples.py +170 -0
- scientificfitting-0.2.0/tests/test_interface.py +253 -0
- scientificfitting-0.2.0/tests/test_likelihoods.py +303 -0
- scientificfitting-0.2.0/tests/test_packaging.py +99 -0
- scientificfitting-0.2.0/tests/test_plotting.py +270 -0
- scientificfitting-0.2.0/tests/test_results.py +266 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amin El Sayed
|
|
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.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scientificfitting
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Python interface to the ScientificFitting Julia numerical core
|
|
5
|
+
Author: Amin El Sayed
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://amin-el-sayed.github.io/ScientificFitting.jl/python.html
|
|
8
|
+
Project-URL: Source, https://github.com/Amin-El-Sayed/ScientificFitting.jl
|
|
9
|
+
Project-URL: Issues, https://github.com/Amin-El-Sayed/ScientificFitting.jl/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy>=1.24
|
|
17
|
+
Requires-Dist: juliacall<0.9.35,>=0.9.34
|
|
18
|
+
Provides-Extra: plot
|
|
19
|
+
Requires-Dist: matplotlib>=3.7; extra == "plot"
|
|
20
|
+
Provides-Extra: sparse
|
|
21
|
+
Requires-Dist: scipy>=1.10; extra == "sparse"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "test"
|
|
24
|
+
Requires-Dist: matplotlib>=3.7; extra == "test"
|
|
25
|
+
Requires-Dist: scipy>=1.10; extra == "test"
|
|
26
|
+
Requires-Dist: build>=1; extra == "test"
|
|
27
|
+
Requires-Dist: setuptools>=77; extra == "test"
|
|
28
|
+
Requires-Dist: tomli>=2; python_version < "3.11" and extra == "test"
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# ScientificFitting for Python
|
|
32
|
+
|
|
33
|
+
Fit NumPy models to measurement data with Gaussian errors, Poisson counts, or
|
|
34
|
+
your own likelihood. Bounds, shared parameters, profiles, and diagnostic
|
|
35
|
+
reports use the same numerical core as ScientificFitting.jl. Optional plots
|
|
36
|
+
are ordinary, editable Matplotlib figures, not Julia/Makie objects.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
In a Python 3.10+ environment:
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
python -m pip install 'scientificfitting[plot]'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
JuliaCall installs a compatible Julia runtime and dependencies automatically.
|
|
47
|
+
First use requires internet access and compilation; subsequent fits reuse
|
|
48
|
+
that installation. The wheel is small, but Julia and its dependencies are
|
|
49
|
+
separate downloads and take additional disk space. No manual Julia setup
|
|
50
|
+
is required. Matplotlib and SciPy are optional (`plot` and `sparse` extras).
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import numpy as np
|
|
54
|
+
from scientificfitting import fit_model, plot_fit
|
|
55
|
+
|
|
56
|
+
def line(x, slope, offset):
|
|
57
|
+
return slope * x + offset
|
|
58
|
+
|
|
59
|
+
x = np.array([0., 1., 2., 3.])
|
|
60
|
+
y = np.array([0.1, 1.2, 1.9, 3.2])
|
|
61
|
+
fit = fit_model(line, x, y, p0={"slope": 1., "offset": 0.}, sigma_y=0.2)
|
|
62
|
+
print(fit.report())
|
|
63
|
+
fig, ax = plot_fit(fit, xlabel="x / mm", ylabel="U / V")
|
|
64
|
+
ax.axvline(1.5, color="black", linestyle="--")
|
|
65
|
+
fig.savefig("calibration.pdf")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Measurement errors are inputs. Reported parameter errors are local covariance
|
|
69
|
+
approximations; profiles help examine asymmetry and non-quadratic behavior.
|
|
70
|
+
This is likelihood optimization, not posterior sampling. Python callbacks
|
|
71
|
+
use finite derivatives or supplied analytic Jacobians, not Julia dual numbers.
|
|
72
|
+
|
|
73
|
+
See the [Python guide](https://amin-el-sayed.github.io/ScientificFitting.jl/python.html)
|
|
74
|
+
for likelihoods, covariance, diagnostics, and native Matplotlib composition.
|
|
75
|
+
[Bug reports and scientific use cases](https://github.com/Amin-El-Sayed/ScientificFitting.jl/issues)
|
|
76
|
+
are welcome. MIT licensed, copyright Amin El Sayed.
|
|
77
|
+
|
|
78
|
+
## Development
|
|
79
|
+
|
|
80
|
+
To use a source checkout instead of the registered Julia core:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
python -m pip install -e './python[plot,test]'
|
|
84
|
+
python python/develop.py
|
|
85
|
+
python -m pytest python/tests
|
|
86
|
+
```
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# ScientificFitting for Python
|
|
2
|
+
|
|
3
|
+
Fit NumPy models to measurement data with Gaussian errors, Poisson counts, or
|
|
4
|
+
your own likelihood. Bounds, shared parameters, profiles, and diagnostic
|
|
5
|
+
reports use the same numerical core as ScientificFitting.jl. Optional plots
|
|
6
|
+
are ordinary, editable Matplotlib figures, not Julia/Makie objects.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
In a Python 3.10+ environment:
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
python -m pip install 'scientificfitting[plot]'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
JuliaCall installs a compatible Julia runtime and dependencies automatically.
|
|
17
|
+
First use requires internet access and compilation; subsequent fits reuse
|
|
18
|
+
that installation. The wheel is small, but Julia and its dependencies are
|
|
19
|
+
separate downloads and take additional disk space. No manual Julia setup
|
|
20
|
+
is required. Matplotlib and SciPy are optional (`plot` and `sparse` extras).
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scientificfitting import fit_model, plot_fit
|
|
25
|
+
|
|
26
|
+
def line(x, slope, offset):
|
|
27
|
+
return slope * x + offset
|
|
28
|
+
|
|
29
|
+
x = np.array([0., 1., 2., 3.])
|
|
30
|
+
y = np.array([0.1, 1.2, 1.9, 3.2])
|
|
31
|
+
fit = fit_model(line, x, y, p0={"slope": 1., "offset": 0.}, sigma_y=0.2)
|
|
32
|
+
print(fit.report())
|
|
33
|
+
fig, ax = plot_fit(fit, xlabel="x / mm", ylabel="U / V")
|
|
34
|
+
ax.axvline(1.5, color="black", linestyle="--")
|
|
35
|
+
fig.savefig("calibration.pdf")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Measurement errors are inputs. Reported parameter errors are local covariance
|
|
39
|
+
approximations; profiles help examine asymmetry and non-quadratic behavior.
|
|
40
|
+
This is likelihood optimization, not posterior sampling. Python callbacks
|
|
41
|
+
use finite derivatives or supplied analytic Jacobians, not Julia dual numbers.
|
|
42
|
+
|
|
43
|
+
See the [Python guide](https://amin-el-sayed.github.io/ScientificFitting.jl/python.html)
|
|
44
|
+
for likelihoods, covariance, diagnostics, and native Matplotlib composition.
|
|
45
|
+
[Bug reports and scientific use cases](https://github.com/Amin-El-Sayed/ScientificFitting.jl/issues)
|
|
46
|
+
are welcome. MIT licensed, copyright Amin El Sayed.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
To use a source checkout instead of the registered Julia core:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
python -m pip install -e './python[plot,test]'
|
|
54
|
+
python python/develop.py
|
|
55
|
+
python -m pytest python/tests
|
|
56
|
+
```
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scientificfitting"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Python interface to the ScientificFitting Julia numerical core"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "Amin El Sayed"}]
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
# 0.9.35 loses Julia's real bindir when its executable is a symlink.
|
|
15
|
+
# Revisit this cap after the loader regression test passes with an upstream fix.
|
|
16
|
+
dependencies = ["numpy>=1.24", "juliacall>=0.9.34,<0.9.35"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Scientific/Engineering",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Documentation = "https://amin-el-sayed.github.io/ScientificFitting.jl/python.html"
|
|
25
|
+
Source = "https://github.com/Amin-El-Sayed/ScientificFitting.jl"
|
|
26
|
+
Issues = "https://github.com/Amin-El-Sayed/ScientificFitting.jl/issues"
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
plot = ["matplotlib>=3.7"]
|
|
30
|
+
sparse = ["scipy>=1.10"]
|
|
31
|
+
test = ["pytest>=7", "matplotlib>=3.7", "scipy>=1.10", "build>=1", "setuptools>=77",
|
|
32
|
+
"tomli>=2; python_version<'3.11'"]
|
|
33
|
+
|
|
34
|
+
[tool.setuptools]
|
|
35
|
+
packages = ["scientificfitting"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
scientificfitting = ["*.jl", "juliapkg.json"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Native Python fitting with a shared Julia numerical core and optional Matplotlib.
|
|
2
|
+
|
|
3
|
+
Development API: models receive `(x, **parameters)` as NumPy arrays and floats.
|
|
4
|
+
Matplotlib is imported only when plotting; Julia/Makie figures are never exposed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ._core import (
|
|
8
|
+
Result, fit_custom, fit_extended_unbinned_model, fit_histogram_density,
|
|
9
|
+
fit_histogram_model, fit_indexed_model, fit_likelihood_model, fit_model,
|
|
10
|
+
fit_multi_model, fit_poisson_model, fit_unbinned_model,
|
|
11
|
+
)
|
|
12
|
+
from ._inputs import ErrorComponent, WhiteningOperator
|
|
13
|
+
from ._results import (
|
|
14
|
+
ContourResult, DiagnosticFinding, DiagnosticReport, FitReport, ParameterEstimate,
|
|
15
|
+
ProfileInterval, ProfileMatrixPanelTriage, ProfileMatrixResult, ProfileResult,
|
|
16
|
+
)
|
|
17
|
+
# Renderers import Matplotlib inside calls, preserving real signatures/docstrings
|
|
18
|
+
# on the public API without making plotting dependencies mandatory at import.
|
|
19
|
+
from ._plotting import add_report, plot_fit, plot_style
|
|
20
|
+
from ._diagnostic_plots import (
|
|
21
|
+
plot_contour, plot_diagnostics, plot_profile, plot_profile_matrix, plot_residuals,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Result", "ErrorComponent", "WhiteningOperator", "fit_model", "fit_custom", "fit_likelihood_model", "fit_poisson_model",
|
|
27
|
+
"fit_histogram_model", "fit_histogram_density", "fit_unbinned_model",
|
|
28
|
+
"fit_extended_unbinned_model", "fit_indexed_model", "fit_multi_model", "plot_fit",
|
|
29
|
+
"DiagnosticFinding", "DiagnosticReport", "FitReport", "ParameterEstimate",
|
|
30
|
+
"ProfileResult", "ProfileInterval", "ContourResult", "ProfileMatrixResult",
|
|
31
|
+
"ProfileMatrixPanelTriage",
|
|
32
|
+
"plot_style", "add_report", "plot_profile", "plot_contour", "plot_profile_matrix",
|
|
33
|
+
"plot_residuals", "plot_diagnostics",
|
|
34
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
using ScientificFitting, PythonCall, SparseArrays
|
|
2
|
+
using ScientificFitting: _TypedCallback
|
|
3
|
+
|
|
4
|
+
"""One vectorized foreign call; no dual numbers or per-observation Python loops."""
|
|
5
|
+
vector_model(f) = _TypedCallback{Vector{Float64}}((x, p) -> pyconvert(Vector{Float64}, f(x, p)))
|
|
6
|
+
matrix_model(f) = _TypedCallback{Matrix{Float64}}((x, p) -> pyconvert(Matrix{Float64}, f(x, p)))
|
|
7
|
+
scalar_cost(f) = _TypedCallback{Float64}(p -> pyconvert(Float64, f(p)))
|
|
8
|
+
vector_constraint(f) = _TypedCallback{Vector{Float64}}(p -> pyconvert(Vector{Float64}, f(p)))
|
|
9
|
+
scalar_model(f) = _TypedCallback{Float64}((x, p) -> pyconvert(Float64, f(x, p)))
|
|
10
|
+
mutating_model(f) = _TypedCallback{Nothing}((out, x, p) -> (f(out, x, p); nothing))
|
|
11
|
+
density_model(f, options) = get(options, :vectorized, false) ? vector_model(f) : scalar_model(f)
|
|
12
|
+
vector(x) = pyconvert(Vector{Float64}, Py(x))
|
|
13
|
+
matrix(x) = pyconvert(Matrix{Float64}, Py(x))
|
|
14
|
+
|
|
15
|
+
"""Reconstruct canonical CSC without allocating an n-by-n dense intermediary."""
|
|
16
|
+
function covariance(value::Py)
|
|
17
|
+
pyisinstance(value, pybuiltins.dict) || return matrix(value)
|
|
18
|
+
n, m = pyconvert(Tuple{Int, Int}, value["shape"])
|
|
19
|
+
return SparseMatrixCSC(n, m, pyconvert(Vector{Int}, value["indptr"]) .+ 1,
|
|
20
|
+
pyconvert(Vector{Int}, value["indices"]) .+ 1, vector(value["data"]))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
"""Copy scalar, vector, or covariance metadata once, not during fit evaluation."""
|
|
24
|
+
function uncertainty_values(value::Py)
|
|
25
|
+
pyisinstance(value, pybuiltins.dict) && return covariance(value)
|
|
26
|
+
ndim = pyhasattr(value, "ndim") ? pyconvert(Int, value.ndim) : 0
|
|
27
|
+
return ndim == 0 ? pyconvert(Float64, value) : ndim == 1 ? vector(value) : matrix(value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
"""Convert the supported Python keyword boundary once, outside numerical loops."""
|
|
31
|
+
function fit_keywords(options)
|
|
32
|
+
result = Dict{Symbol, Any}(:derivatives => :finite)
|
|
33
|
+
values = pyconvert(Dict{String, Py}, Py(options))
|
|
34
|
+
inplace = haskey(values, "inplace") && pyconvert(Bool, values["inplace"])
|
|
35
|
+
for (key, value) in values
|
|
36
|
+
name = Symbol(key)
|
|
37
|
+
pyis(value, pybuiltins.None) && continue
|
|
38
|
+
result[name] = if name in (:backend, :cost, :scale_covariance, :cost_name, :optimizer, :parameter_covariance)
|
|
39
|
+
Symbol(pyconvert(String, value))
|
|
40
|
+
elseif name in (:cov_x, :cov_y)
|
|
41
|
+
covariance(value)
|
|
42
|
+
elseif name == :whitening
|
|
43
|
+
callback = value[0]
|
|
44
|
+
marginal = pyis(value[2], pybuiltins.None) ? nothing : uncertainty_values(value[2])
|
|
45
|
+
WhiteningOperator(_TypedCallback{Nothing}((out, residual) -> (callback(out, residual); nothing));
|
|
46
|
+
logdet_covariance=pyconvert(Float64, value[1]), marginal_sigma=marginal)
|
|
47
|
+
elseif name == :error_components
|
|
48
|
+
[ErrorComponent(Symbol(pyconvert(String, row[0])), Symbol(pyconvert(String, row[1])),
|
|
49
|
+
Symbol(pyconvert(String, row[2])), uncertainty_values(row[3]);
|
|
50
|
+
active=pyconvert(Bool, row[4])) for row in value]
|
|
51
|
+
elseif name in (:sigma_x, :sigma_y)
|
|
52
|
+
vector(value)
|
|
53
|
+
elseif name == :bounds
|
|
54
|
+
lower, upper = pyconvert(Tuple{Py, Py}, value)
|
|
55
|
+
(vector(lower), vector(upper))
|
|
56
|
+
elseif name == :constraints
|
|
57
|
+
callbacks = pyconvert(Dict{String, Py}, value)
|
|
58
|
+
ConstraintSpec(;
|
|
59
|
+
eq=haskey(callbacks, "eq") ? vector_constraint(callbacks["eq"]) : nothing,
|
|
60
|
+
ineq=haskey(callbacks, "ineq") ? vector_constraint(callbacks["ineq"]) : nothing,
|
|
61
|
+
)
|
|
62
|
+
elseif name in (:parameter_priors, :fixed_parameters)
|
|
63
|
+
constructor = name == :parameter_priors ? ParameterPrior : FixedParameter
|
|
64
|
+
[constructor(Int(row[1]), row[2:end]...) for row in pyconvert(Vector{Vector{Float64}}, value)]
|
|
65
|
+
elseif name == :parameter_constraints
|
|
66
|
+
[ParameterConstraint(pyconvert(Vector{Int}, row[0]), vector(row[1]), matrix(row[2]))
|
|
67
|
+
for row in value]
|
|
68
|
+
elseif name == :parameter_names
|
|
69
|
+
pyconvert(Vector{String}, value)
|
|
70
|
+
elseif name == :initial_guesses
|
|
71
|
+
pyconvert(Vector{Vector{Float64}}, value)
|
|
72
|
+
elseif name == :jacobian
|
|
73
|
+
inplace ? mutating_model(value) : matrix_model(value)
|
|
74
|
+
elseif name == :x_derivative
|
|
75
|
+
vector_model(value)
|
|
76
|
+
elseif name == :gof
|
|
77
|
+
scalar_cost(value)
|
|
78
|
+
elseif name == :logprob
|
|
79
|
+
_TypedCallback{Vector{Float64}}((y, mu, p) -> pyconvert(Vector{Float64}, value(y, mu, p)))
|
|
80
|
+
elseif name in (:maxiters, :multistart, :nobs)
|
|
81
|
+
pyconvert(Int, value)
|
|
82
|
+
elseif name in (:inplace, :vectorized)
|
|
83
|
+
pyconvert(Bool, value)
|
|
84
|
+
else
|
|
85
|
+
pyconvert(Float64, value)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
return result
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
"""
|
|
92
|
+
Dispatch to existing Julia fits; the bridge owns conversion, never statistics.
|
|
93
|
+
|
|
94
|
+
`invokelatest` is a once-per-fit inference boundary: Python's dynamic argument
|
|
95
|
+
conversion must not infer every solver branch. The selected Julia fit then
|
|
96
|
+
specializes on concrete arrays and callbacks; its numerical loops stay native.
|
|
97
|
+
"""
|
|
98
|
+
function run_fit(kind::String, callback::Py, x, y, p0, options)
|
|
99
|
+
kwargs = fit_keywords(options)
|
|
100
|
+
start = vector(p0)
|
|
101
|
+
kind == "custom" && return Base.invokelatest(fit_custom, scalar_cost(callback); p0=start, kwargs...)
|
|
102
|
+
kind == "unbinned" && return Base.invokelatest(fit_unbinned_model, density_model(callback, kwargs), vector(y); p0=start, kwargs...)
|
|
103
|
+
if kind == "extended_unbinned"
|
|
104
|
+
domain = vector(x)
|
|
105
|
+
length(domain) == 2 || throw(ArgumentError("domain must contain exactly two endpoints"))
|
|
106
|
+
return Base.invokelatest(fit_extended_unbinned_model, density_model(callback, kwargs), vector(y), Tuple(domain); p0=start, kwargs...)
|
|
107
|
+
end
|
|
108
|
+
kind == "histogram_density" && return Base.invokelatest(fit_histogram_density, density_model(callback, kwargs), vector(x), vector(y); p0=start, kwargs...)
|
|
109
|
+
kind in ("gaussian", "poisson", "histogram", "indexed", "likelihood") ||
|
|
110
|
+
throw(ArgumentError("unknown fit family: $kind"))
|
|
111
|
+
model = kind == "gaussian" && get(kwargs, :inplace, false) ? mutating_model(callback) : vector_model(callback)
|
|
112
|
+
fit_function = kind == "gaussian" ? fit_model :
|
|
113
|
+
kind == "poisson" ? fit_poisson_model :
|
|
114
|
+
kind == "indexed" ? fit_indexed_model :
|
|
115
|
+
kind == "likelihood" ? fit_likelihood_model : fit_histogram_model
|
|
116
|
+
return Base.invokelatest(fit_function, model, vector(x), vector(y); p0=start, kwargs...)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
"""Preserve the single global parameter map while converting dataset arrays once."""
|
|
120
|
+
function run_multi(callbacks, xs, ys, sigma, maps, p0, options)
|
|
121
|
+
models = [vector_model(f) for f in Py(callbacks)]
|
|
122
|
+
scales = [pyis(s, pybuiltins.None) ? nothing : vector(s) for s in Py(sigma)]
|
|
123
|
+
return Base.invokelatest(fit_multi_model, models, [vector(x) for x in Py(xs)], [vector(y) for y in Py(ys)];
|
|
124
|
+
p0=vector(p0), sigma_y=scales, parameter_map=pyconvert(Vector{Vector{Int}}, Py(maps)),
|
|
125
|
+
fit_keywords(options)...)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
"""Convert scalar records only; symbols become strings and missing counts become None."""
|
|
129
|
+
scalar_value(value) = value isa Symbol ? String(value) : ismissing(value) ? nothing : value
|
|
130
|
+
scalar_fields(record) = pydict(String(name) => scalar_value(getproperty(record, name)) for name in propertynames(record))
|
|
131
|
+
|
|
132
|
+
"""Stored numerical checks, with parameter names instead of one-based indices."""
|
|
133
|
+
function numerical_values(diagnostics, names)
|
|
134
|
+
return pydict(warnings=pylist(diagnostics.warnings),
|
|
135
|
+
covariance_condition=diagnostics.covariance_condition, hessian_condition=diagnostics.hessian_condition,
|
|
136
|
+
active_bounds=pylist(names[diagnostics.active_bounds]),
|
|
137
|
+
findings=pylist(scalar_fields(f) for f in diagnostics.findings))
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
"""Transfer actual core reports; Python never infers findings by parsing text."""
|
|
141
|
+
function diagnostic_values(report::DiagnosticReport, max_actions::Int=5)
|
|
142
|
+
dashboard = diagnostic_dashboard(report; max_actions)
|
|
143
|
+
return pydict(findings=pylist(scalar_fields(f) for f in report.findings),
|
|
144
|
+
summary=report.summary, status=String(dashboard.status),
|
|
145
|
+
severity_counts=pydict(String(k) => v for (k, v) in dashboard.severity_counts),
|
|
146
|
+
next_actions=pylist(dashboard.next_actions), text=diagnose_text(report),
|
|
147
|
+
dashboard_text=diagnostic_dashboard_text(dashboard))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
"""Return snapshots, retaining the Julia fit privately for later refits."""
|
|
151
|
+
function result_values(result, names)
|
|
152
|
+
labels = pyconvert(Vector{String}, Py(names))
|
|
153
|
+
return pydict(params=Py(result.params), stderr=Py(result.param_stderr),
|
|
154
|
+
covariance=Py(result.param_covariance), correlation=Py(result.param_correlation),
|
|
155
|
+
converged=result.converged, statistics=scalar_fields(result.stats),
|
|
156
|
+
options=scalar_fields(result.options), backend=String(result.backend),
|
|
157
|
+
iterations=scalar_value(result.iterations), message=result.message,
|
|
158
|
+
numerical_diagnostics=numerical_values(result.diagnostics, labels),
|
|
159
|
+
data=result isa FitResult ? pydict(x=Py(result.problem.x), y=Py(result.problem.y),
|
|
160
|
+
model_y=Py(result.model_y), residuals=Py(result.residuals),
|
|
161
|
+
weighted_residuals=Py(result.weighted_residuals), jacobian=Py(result.jacobian)) : pybuiltins.None)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
function report_values(report::FitReport, names, sigdigits::Int)
|
|
165
|
+
return pydict(parameters=pylist(pydict(name=p.name, value=p.value, uncertainty=p.uncertainty,
|
|
166
|
+
uncertainty_minus=p.uncertainty_minus, uncertainty_plus=p.uncertainty_plus, fixed=p.fixed)
|
|
167
|
+
for p in report.parameters),
|
|
168
|
+
statistics=scalar_fields(report.statistics), covariance=Py(report.covariance),
|
|
169
|
+
correlation=Py(report.correlation), backend=String(report.backend), converged=report.converged,
|
|
170
|
+
iterations=scalar_value(report.iterations), message=report.message,
|
|
171
|
+
numerical_diagnostics=numerical_values(report.diagnostics, pyconvert(Vector{String}, Py(names))),
|
|
172
|
+
text=report_text(report; sigdigits))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
function run_report(result, names, errors::String, threshold::Float64, npoints::Int, nsigma::Float64)
|
|
176
|
+
return fit_report(result; parameter_names=pyconvert(Vector{String}, Py(names)),
|
|
177
|
+
errors=Symbol(errors), profile_threshold=threshold, profile_npoints=npoints, profile_nsigma=nsigma)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
result_diagnose(result, max_actions::Int) = diagnostic_values(diagnose(result), max_actions)
|
|
181
|
+
prediction(result, x, uncertainty::Bool) = predict(result, vector(x); uncertainty=uncertainty)
|
|
182
|
+
|
|
183
|
+
"""Pass scan controls without recomputing profile costs in Python."""
|
|
184
|
+
function scan_keywords(options)
|
|
185
|
+
result = Dict{Symbol, Any}()
|
|
186
|
+
for (key, value) in pyconvert(Dict{String, Py}, Py(options))
|
|
187
|
+
name = Symbol(key)
|
|
188
|
+
result[name] = if name in (:values, :xvalues, :yvalues, :levels, :contour_levels)
|
|
189
|
+
vector(value)
|
|
190
|
+
elseif name == :on_failure
|
|
191
|
+
Symbol(pyconvert(String, value))
|
|
192
|
+
else
|
|
193
|
+
pyconvert(Any, value)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
return result
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
run_profile(result, index::Int, options) = profile(result, index; scan_keywords(options)...)
|
|
200
|
+
run_contour(result, i::Int, j::Int, options) = contour(result, i, j; scan_keywords(options)...)
|
|
201
|
+
run_interval(result, index::Int, options) = profile_interval(result, index; scan_keywords(options)...)
|
|
202
|
+
function run_matrix(result, indices, names, options)
|
|
203
|
+
return profile_matrix(result; parameters=pyconvert(Vector{Int}, Py(indices)),
|
|
204
|
+
parameter_names=pyconvert(Vector{String}, Py(names)), scan_keywords(options)...)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
profile_diagnostics(scan::ProfileResult, sigma::Real, tolerance::Real=0.25, max_actions::Int=5) = diagnostic_values(
|
|
208
|
+
isfinite(sigma) && sigma > 0 ? diagnose(scan; local_sigma=sigma, tolerance) : diagnose(scan; tolerance), max_actions)
|
|
209
|
+
contour_diagnostics(scan::ContourResult, center, covariance, tolerance::Real=0.5, max_actions::Int=5) = diagnostic_values(
|
|
210
|
+
diagnose(scan; local_center=vector(center), local_covariance=matrix(covariance), tolerance), max_actions)
|
|
211
|
+
|
|
212
|
+
"""Keep core ordering and axis orientation while replacing indices with names."""
|
|
213
|
+
function matrix_values(result::ProfileMatrixResult)
|
|
214
|
+
labels = Dict(zip(result.parameters, result.parameter_names))
|
|
215
|
+
triage = profile_matrix_triage(result; include_ok=true)
|
|
216
|
+
return pydict(parameters=pylist(result.parameter_names),
|
|
217
|
+
best_values=Py(result.best_values), local_stderr=Py(result.local_stderr),
|
|
218
|
+
local_covariance=Py(result.local_covariance), local_correlation=Py(result.local_correlation),
|
|
219
|
+
profiles=pylist(pytuple((labels[i], Py(scan), diagnostic_values(result.profile_diagnostics[i])))
|
|
220
|
+
for (i, scan) in result.profiles),
|
|
221
|
+
contours=pylist(pytuple((labels[i], labels[j], Py(scan), diagnostic_values(result.contour_diagnostics[(i, j)])))
|
|
222
|
+
for ((i, j), scan) in result.contours),
|
|
223
|
+
panel_status=pydict(pytuple((labels[i], labels[j])) => String(status)
|
|
224
|
+
for ((i, j), status) in result.panel_status),
|
|
225
|
+
diagnostics=diagnostic_values(result.report),
|
|
226
|
+
triage=pylist(pydict(parameters=pytuple(row.parameter_names), status=String(row.status),
|
|
227
|
+
severity_counts=pydict(String(k) => v for (k, v) in row.severity_counts),
|
|
228
|
+
finding_codes=pylist(String.(row.finding_codes)), next_action=row.next_action) for row in triage))
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
plot_errors(result) = (ScientificFitting._xerror_for_plot(result.problem, result.params),
|
|
232
|
+
ScientificFitting._yerror_for_plot(result.problem, result.params))
|
|
233
|
+
|
|
234
|
+
diagnostic_data(result::FitResult, kind::String) = ScientificFitting._diagnostic_values(result, Symbol(kind))
|