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.
Files changed (26) hide show
  1. scientificfitting-0.2.0/LICENSE +21 -0
  2. scientificfitting-0.2.0/PKG-INFO +86 -0
  3. scientificfitting-0.2.0/README.md +56 -0
  4. scientificfitting-0.2.0/pyproject.toml +41 -0
  5. scientificfitting-0.2.0/scientificfitting/__init__.py +34 -0
  6. scientificfitting-0.2.0/scientificfitting/_bridge.jl +234 -0
  7. scientificfitting-0.2.0/scientificfitting/_core.py +463 -0
  8. scientificfitting-0.2.0/scientificfitting/_diagnostic_plots.py +302 -0
  9. scientificfitting-0.2.0/scientificfitting/_inputs.py +120 -0
  10. scientificfitting-0.2.0/scientificfitting/_plotting.py +171 -0
  11. scientificfitting-0.2.0/scientificfitting/_results.py +263 -0
  12. scientificfitting-0.2.0/scientificfitting/_runtime.py +21 -0
  13. scientificfitting-0.2.0/scientificfitting/juliapkg.json +9 -0
  14. scientificfitting-0.2.0/scientificfitting.egg-info/PKG-INFO +86 -0
  15. scientificfitting-0.2.0/scientificfitting.egg-info/SOURCES.txt +24 -0
  16. scientificfitting-0.2.0/scientificfitting.egg-info/dependency_links.txt +1 -0
  17. scientificfitting-0.2.0/scientificfitting.egg-info/requires.txt +18 -0
  18. scientificfitting-0.2.0/scientificfitting.egg-info/top_level.txt +1 -0
  19. scientificfitting-0.2.0/setup.cfg +4 -0
  20. scientificfitting-0.2.0/tests/test_covariance.py +191 -0
  21. scientificfitting-0.2.0/tests/test_examples.py +170 -0
  22. scientificfitting-0.2.0/tests/test_interface.py +253 -0
  23. scientificfitting-0.2.0/tests/test_likelihoods.py +303 -0
  24. scientificfitting-0.2.0/tests/test_packaging.py +99 -0
  25. scientificfitting-0.2.0/tests/test_plotting.py +270 -0
  26. 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))