ffsi 0.1.0__py3-none-any.whl
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.
- ffsi/__init__.py +16 -0
- ffsi/api.py +191 -0
- ffsi/array_module.py +42 -0
- ffsi/crazy_distributions.py +34 -0
- ffsi/optimize_galahad.py +252 -0
- ffsi/optimize_galahad_bounded.py +292 -0
- ffsi/plotting.py +190 -0
- ffsi/sensitivity_analysis.py +180 -0
- ffsi/utils.py +47 -0
- ffsi-0.1.0.dist-info/METADATA +84 -0
- ffsi-0.1.0.dist-info/RECORD +14 -0
- ffsi-0.1.0.dist-info/WHEEL +5 -0
- ffsi-0.1.0.dist-info/licenses/LICENSE +28 -0
- ffsi-0.1.0.dist-info/top_level.txt +1 -0
ffsi/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Free-Form SAS Inversion
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 The Science and Technology Facilities Council (STFC)
|
|
5
|
+
Author: Jaroslav Fowkes (STFC)
|
|
6
|
+
"""
|
|
7
|
+
CUPY_INSTALLED = False
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import cupy as _cupy
|
|
11
|
+
CUPY_INSTALLED = True
|
|
12
|
+
print('INFO: CuPy is installed, GPU computation is available')
|
|
13
|
+
except Exception as e:
|
|
14
|
+
print('WARNING: CuPy is not installed, cannot use GPU computation')
|
|
15
|
+
print(e)
|
|
16
|
+
print('WARNING: continuing with CPU computation only')
|
ffsi/api.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Public API for free-form SAS inversion.
|
|
3
|
+
"""
|
|
4
|
+
import importlib.util
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from importlib import import_module
|
|
7
|
+
from inspect import getmembers, isabstract, isclass
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from ffsi.array_module import get_array_module, to_device, from_device
|
|
12
|
+
from ffsi.models.basemodel import SASModel
|
|
13
|
+
from ffsi.optimize_galahad import optimize
|
|
14
|
+
from ffsi.utils import contract_tensor, xi_to_scale
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Model names available through invert(), for error messages only
|
|
18
|
+
_MODEL_NAMES = ("sphere", "cylinder", "cylinder2d", "ellipsoid", "ellipsoid2d")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _resolve_model(model):
|
|
22
|
+
"""
|
|
23
|
+
Resolve `model` (a case-insensitive name or a `SASModel` subclass) to
|
|
24
|
+
`(class, name)`.
|
|
25
|
+
|
|
26
|
+
The model class is imported lazily from its own module `ffsi.models.<name>`
|
|
27
|
+
(module name == class name lowercased), so only the requested model is
|
|
28
|
+
loaded rather than every model up front.
|
|
29
|
+
"""
|
|
30
|
+
name = (model if isinstance(model, str) else model.__name__).lower()
|
|
31
|
+
|
|
32
|
+
# return a proper error
|
|
33
|
+
if importlib.util.find_spec(f"ffsi.models.{name}") is None:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
"Unknown model '{}', available models: {}".format(
|
|
36
|
+
name, ", ".join(_MODEL_NAMES)
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
module = import_module(f"ffsi.models.{name}")
|
|
41
|
+
|
|
42
|
+
classes = getmembers(
|
|
43
|
+
module,
|
|
44
|
+
lambda m: (
|
|
45
|
+
isclass(m)
|
|
46
|
+
and not isabstract(m)
|
|
47
|
+
and issubclass(m, SASModel)
|
|
48
|
+
and m is not SASModel
|
|
49
|
+
and m.__module__ == module.__name__
|
|
50
|
+
),
|
|
51
|
+
)
|
|
52
|
+
if not classes:
|
|
53
|
+
raise ValueError(f"Module 'ffsi.models.{name}' defines no SASModel subclass")
|
|
54
|
+
|
|
55
|
+
return classes[0][1], name
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ParamDistribution:
|
|
60
|
+
"""Fitted distribution of one model parameter."""
|
|
61
|
+
|
|
62
|
+
name: str # model parameter name, e.g. 'r', 'l', 'rp', 're'
|
|
63
|
+
grid: np.ndarray # bin centers
|
|
64
|
+
weights: np.ndarray # weights
|
|
65
|
+
|
|
66
|
+
volume_weights: np.ndarray = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class InversionResult:
|
|
71
|
+
"""Output of `invert()`: I_opt = xi * Gw + background."""
|
|
72
|
+
|
|
73
|
+
model: str # model name
|
|
74
|
+
xi: float # raw scale factor
|
|
75
|
+
background: float
|
|
76
|
+
distributions: list = field(default_factory=list)
|
|
77
|
+
theory: np.ndarray = None # I_opt on the input q
|
|
78
|
+
residuals: np.ndarray = None # (theory - intensity) / intensity_std
|
|
79
|
+
chi2: float = None # sum(residuals**2) / residuals.size
|
|
80
|
+
average_volume: float = None # <V> under the optimal weights
|
|
81
|
+
drho: float = None # contrast: sld - sld_solvent
|
|
82
|
+
scale: float = None # volume fraction: xi * <V> * 1e4
|
|
83
|
+
|
|
84
|
+
def distribution(self, name):
|
|
85
|
+
"""The fitted `ParamDistribution` for parameter `name`."""
|
|
86
|
+
for dist in self.distributions:
|
|
87
|
+
if dist.name == name:
|
|
88
|
+
return dist
|
|
89
|
+
raise KeyError("No distribution for parameter '{}', have: {}".format(name, ", ".join(d.name for d in self.distributions)))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _build_grid(spec, xp):
|
|
93
|
+
"""Bin centers (on backend `xp`) from a (min, max, nbins) triple"""
|
|
94
|
+
if isinstance(spec, np.ndarray) or (np.ndim(spec) == 1 and len(spec) > 3):
|
|
95
|
+
return xp.asarray(spec, dtype=float)
|
|
96
|
+
lo, hi, nbins = spec
|
|
97
|
+
return xp.linspace(float(lo), float(hi), int(nbins))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def invert(model, q, intensity, intensity_std, grids, *, sld, sld_solvent, sigma=None):
|
|
101
|
+
"""
|
|
102
|
+
Free-form inversion of 1D SAS data.
|
|
103
|
+
|
|
104
|
+
:param model: model name ('sphere', 'cylinder', 'ellipsoid';
|
|
105
|
+
:param q: scattering vectors
|
|
106
|
+
:param intensity: measured intensity `I(q)`
|
|
107
|
+
:param intensity_std: intensity standard deviations `dI(q)`
|
|
108
|
+
:param grids: `dict` keyed by the model's parameter names; each value
|
|
109
|
+
is a `(min, max, nbins)` triple or a prebuilt 1D array of bin centers
|
|
110
|
+
:param sld: scattering length density of the particle,
|
|
111
|
+
with `sld_solvent` it gives the contrast `drho = sld - sld_solvent`
|
|
112
|
+
:param sld_solvent: scattering length density of the solvent, in 1e-6 A^-2
|
|
113
|
+
:param sigma: smoothness regularization weight (`None` disables it)
|
|
114
|
+
:return: an `InversionResult`; `scale` is the volume fraction `xi * <V> * 1e4`
|
|
115
|
+
|
|
116
|
+
Computation runs on the GPU automatically whenever CuPy is installed; inputs
|
|
117
|
+
may be plain numpy arrays (they are moved onto the GPU here), so callers need
|
|
118
|
+
no CuPy dependency of their own. Without CuPy it runs on the CPU.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
# resolve a name or a SASModel subclass to (class, name)
|
|
122
|
+
model_class, model_name = _resolve_model(model)
|
|
123
|
+
param_names = list(model_class.param_names_scattering_intensity)
|
|
124
|
+
|
|
125
|
+
# contrast: drho = sld - sld_solvent
|
|
126
|
+
drho = float(sld) - float(sld_solvent)
|
|
127
|
+
|
|
128
|
+
# move host inputs onto the compute backend (GPU when CuPy is available)
|
|
129
|
+
# array-module dispatch below and everything downstream run on that backend
|
|
130
|
+
q, intensity, intensity_std = to_device(q, intensity, intensity_std)
|
|
131
|
+
|
|
132
|
+
# xp resolves to cupy when the inputs are on the GPU, else numpy
|
|
133
|
+
xp = get_array_module(q, intensity, intensity_std)
|
|
134
|
+
q = xp.ascontiguousarray(q, dtype=float)
|
|
135
|
+
intensity = xp.asarray(intensity, dtype=float)
|
|
136
|
+
intensity_std = xp.asarray(intensity_std, dtype=float)
|
|
137
|
+
# build grids on the same backend
|
|
138
|
+
param_list = [_build_grid(grids[name], xp) for name in param_names]
|
|
139
|
+
|
|
140
|
+
# scattering intensity (Green's tensor) and inversion
|
|
141
|
+
G = model_class.compute_scattering_intensity([q], param_list, drho)
|
|
142
|
+
xi, background, w_opt_list = optimize(G, intensity, intensity_std, sigma=sigma)
|
|
143
|
+
xi, background = float(xi), float(background)
|
|
144
|
+
# GALAHAD returns numpy weights; move them onto G's backend to reconstruct
|
|
145
|
+
w_list = [xp.asarray(w) for w in w_opt_list]
|
|
146
|
+
|
|
147
|
+
# fitted intensity, residuals and chi-squared
|
|
148
|
+
theory = xi * contract_tensor(G, w_list, skip_axes=[0]) + background
|
|
149
|
+
residuals = (theory - intensity) / intensity_std
|
|
150
|
+
chi2 = float(xp.sum(residuals**2) / residuals.size)
|
|
151
|
+
|
|
152
|
+
# average volume
|
|
153
|
+
volume_params = [param_list[param_names.index(name)]
|
|
154
|
+
for name in model_class.param_names_average_volume]
|
|
155
|
+
volume_weights_list = [w_list[param_names.index(name)]
|
|
156
|
+
for name in model_class.param_names_average_volume]
|
|
157
|
+
average_volume = float(model_class.compute_average_volume(volume_params, volume_weights_list))
|
|
158
|
+
|
|
159
|
+
# convert xi to SasView scale
|
|
160
|
+
scale = xi_to_scale(xi, average_volume)
|
|
161
|
+
|
|
162
|
+
# package results as host numpy as plotters and the GUI
|
|
163
|
+
# cannot take cupy arrays
|
|
164
|
+
distributions = []
|
|
165
|
+
for name, grid, weights in zip(param_names, param_list, w_list):
|
|
166
|
+
volume_weights = None
|
|
167
|
+
if len(param_names) == 1:
|
|
168
|
+
volume = model_class.compute_volume(param_list)
|
|
169
|
+
weighted = weights * volume
|
|
170
|
+
volume_weights = from_device(weighted / xp.sum(weighted))
|
|
171
|
+
distributions.append(
|
|
172
|
+
ParamDistribution(
|
|
173
|
+
name=name,
|
|
174
|
+
grid=from_device(grid),
|
|
175
|
+
weights=from_device(weights),
|
|
176
|
+
volume_weights=volume_weights,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return InversionResult(
|
|
181
|
+
model=model_name,
|
|
182
|
+
xi=xi,
|
|
183
|
+
background=background,
|
|
184
|
+
distributions=distributions,
|
|
185
|
+
theory=from_device(theory),
|
|
186
|
+
residuals=from_device(residuals),
|
|
187
|
+
chi2=chi2,
|
|
188
|
+
average_volume=average_volume,
|
|
189
|
+
drho=drho,
|
|
190
|
+
scale=scale,
|
|
191
|
+
)
|
ffsi/array_module.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Get array module that works when CuPy is not installed
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 The Science and Technology Facilities Council (STFC)
|
|
5
|
+
Author: Jaroslav Fowkes (STFC)
|
|
6
|
+
"""
|
|
7
|
+
import numpy as _numpy
|
|
8
|
+
import scipy as _scipy
|
|
9
|
+
|
|
10
|
+
from ffsi import CUPY_INSTALLED
|
|
11
|
+
|
|
12
|
+
def get_array_module(*args):
|
|
13
|
+
if CUPY_INSTALLED:
|
|
14
|
+
import cupy as cp
|
|
15
|
+
return cp.get_array_module(*args)
|
|
16
|
+
else:
|
|
17
|
+
return _numpy
|
|
18
|
+
|
|
19
|
+
def get_science_module(*args):
|
|
20
|
+
if CUPY_INSTALLED:
|
|
21
|
+
import cupyx.scipy as cps
|
|
22
|
+
return cps.get_array_module(*args)
|
|
23
|
+
else:
|
|
24
|
+
return _scipy
|
|
25
|
+
|
|
26
|
+
def to_device(*arrays):
|
|
27
|
+
"""
|
|
28
|
+
Bring arrays from the host to the device.
|
|
29
|
+
"""
|
|
30
|
+
if CUPY_INSTALLED:
|
|
31
|
+
import cupy as cp
|
|
32
|
+
return (cp.asarray(a) for a in arrays)
|
|
33
|
+
return arrays
|
|
34
|
+
|
|
35
|
+
def from_device(array):
|
|
36
|
+
"""
|
|
37
|
+
Bring an array from the device to the host.
|
|
38
|
+
"""
|
|
39
|
+
if CUPY_INSTALLED:
|
|
40
|
+
import cupy as cp
|
|
41
|
+
return cp.asnumpy(array)
|
|
42
|
+
return array
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Crazy distributions generator
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2026 The Science and Technology Facilities Council (STFC)
|
|
5
|
+
Author: Jaroslav Fowkes (STFC)
|
|
6
|
+
"""
|
|
7
|
+
from ffsi.array_module import get_array_module
|
|
8
|
+
|
|
9
|
+
def crazy_distribution(x, gaussians, noise_level, fade_start, fade_end, seed=0):
|
|
10
|
+
|
|
11
|
+
# use CPU or GPU as appropriate
|
|
12
|
+
xp = get_array_module(x)
|
|
13
|
+
|
|
14
|
+
# create
|
|
15
|
+
w_true = xp.zeros(x.shape)
|
|
16
|
+
|
|
17
|
+
# add Gaussians
|
|
18
|
+
for factor, mean, stddev in gaussians:
|
|
19
|
+
w_true += factor * xp.exp(-((x - mean) / stddev) ** 2)
|
|
20
|
+
|
|
21
|
+
# add noise
|
|
22
|
+
xp.random.seed(seed)
|
|
23
|
+
w_true += noise_level * xp.random.rand(*x.shape) * xp.random.rand(*x.shape)
|
|
24
|
+
|
|
25
|
+
# fade both ends to make it look nicer
|
|
26
|
+
if len(x) >= 3:
|
|
27
|
+
w_true[0:fade_start] = 0.
|
|
28
|
+
w_true[fade_start:fade_end] *= xp.linspace(0, 1, fade_end - fade_start)
|
|
29
|
+
w_true[-fade_start:] = 0.
|
|
30
|
+
w_true[-fade_end:-fade_start] *= xp.linspace(1, 0, fade_end - fade_start)
|
|
31
|
+
|
|
32
|
+
# normalize to 1
|
|
33
|
+
w_true /= xp.sum(w_true)
|
|
34
|
+
return w_true
|
ffsi/optimize_galahad.py
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Free-form SAS Optimization Interface to GALAHAD
|
|
3
|
+
|
|
4
|
+
Mandatory Parameters:
|
|
5
|
+
G - Green's function
|
|
6
|
+
I_data - intensity data
|
|
7
|
+
I_data_std - error on intensity data
|
|
8
|
+
|
|
9
|
+
Optional Parameters:
|
|
10
|
+
sigma - regularization parameter value
|
|
11
|
+
|
|
12
|
+
Returns:
|
|
13
|
+
xi_opt - optimal xi
|
|
14
|
+
b_opt - optimal b
|
|
15
|
+
w_opt_list - list of optimal parameters
|
|
16
|
+
|
|
17
|
+
Example usage:
|
|
18
|
+
|
|
19
|
+
xi_opt, b_opt, w_opt_list = optimize(G, I_data, I_data_std)
|
|
20
|
+
|
|
21
|
+
Copyright (C) 2026 The Science and Technology Facilities Council (STFC)
|
|
22
|
+
Author: Jaroslav Fowkes (STFC)
|
|
23
|
+
"""
|
|
24
|
+
import numpy as np
|
|
25
|
+
from galahad import snls
|
|
26
|
+
|
|
27
|
+
from ffsi.array_module import get_array_module
|
|
28
|
+
|
|
29
|
+
from ffsi.utils import contract_tensor
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def optimize(G, I_data, I_data_std, sigma=None):
|
|
33
|
+
|
|
34
|
+
# use CPU or GPU as appropriate
|
|
35
|
+
xp = get_array_module(G, I_data, I_data_std)
|
|
36
|
+
print("INFO: using " + xp.__name__ + " for residual and Jacobian computation")
|
|
37
|
+
|
|
38
|
+
# determine if data is 1D or 2D
|
|
39
|
+
if len(I_data.shape) == 1:
|
|
40
|
+
q_axes = (0,)
|
|
41
|
+
p_axes = tuple(range(1,G.ndim))
|
|
42
|
+
q_dims = G.shape[:1]
|
|
43
|
+
p_dims = G.shape[1:]
|
|
44
|
+
else:
|
|
45
|
+
q_axes = (0,1)
|
|
46
|
+
p_axes = tuple(range(2,G.ndim))
|
|
47
|
+
q_dims = G.shape[:2]
|
|
48
|
+
p_dims = G.shape[2:]
|
|
49
|
+
|
|
50
|
+
# w0 are uniform distributions
|
|
51
|
+
w0_list = [xp.ones(n) / n for n in p_dims]
|
|
52
|
+
|
|
53
|
+
# this averages out G over the parameters
|
|
54
|
+
G_ave = xp.sum(G, axis=p_axes) / np.prod(p_dims)
|
|
55
|
+
|
|
56
|
+
# and xi0 and b0 can be determined from
|
|
57
|
+
# min [1/sigma * (xi G_ave + b 1 - mu) ]^ 2
|
|
58
|
+
mu_over_nv = I_data / I_data_std
|
|
59
|
+
one_over_nv = 1 / I_data_std
|
|
60
|
+
G_ave_over_nv = G_ave / I_data_std
|
|
61
|
+
a11 = xp.sum(G_ave_over_nv ** 2)
|
|
62
|
+
a12 = xp.sum(G_ave_over_nv * one_over_nv)
|
|
63
|
+
a22 = xp.sum(one_over_nv ** 2)
|
|
64
|
+
b1 = xp.sum(mu_over_nv * G_ave_over_nv)
|
|
65
|
+
b2 = xp.sum(mu_over_nv * one_over_nv)
|
|
66
|
+
|
|
67
|
+
# solve xi0 and b0 using Cramer's rule
|
|
68
|
+
A = a11 * a22 - a12 * a12
|
|
69
|
+
xi0 = (b1 * a22 - b2 * a12) / A
|
|
70
|
+
b0 = (b2 * a11 - b1 * a12) / A
|
|
71
|
+
print('xi0: %.2e' % xi0)
|
|
72
|
+
print('b0: %.2e' % b0)
|
|
73
|
+
|
|
74
|
+
# determine xi0 and b0 scaling
|
|
75
|
+
xi_sc = 10 ** xp.floor(xp.log10(xp.abs(xi0)))
|
|
76
|
+
b_sc = 10 ** xp.floor(xp.log10(xp.abs(b0)))
|
|
77
|
+
|
|
78
|
+
# form residuals
|
|
79
|
+
def eval_r(x):
|
|
80
|
+
|
|
81
|
+
# move x to GPU if required
|
|
82
|
+
x = xp.asarray(x)
|
|
83
|
+
|
|
84
|
+
# extract variables and unscale
|
|
85
|
+
xi = x[0] * xi_sc
|
|
86
|
+
b = x[1] * b_sc
|
|
87
|
+
split_inds = np.cumsum(p_dims)[:-1]
|
|
88
|
+
w_list = xp.split(x[2:], split_inds)
|
|
89
|
+
|
|
90
|
+
# form Gw (the form factor)
|
|
91
|
+
Gw = contract_tensor(G, w_list, skip_axes=q_axes)
|
|
92
|
+
|
|
93
|
+
# intensity from forward model
|
|
94
|
+
I_model = xi * Gw + b
|
|
95
|
+
|
|
96
|
+
# intensity misfit
|
|
97
|
+
eps = (I_model - I_data) / I_data_std
|
|
98
|
+
|
|
99
|
+
# handle regularization
|
|
100
|
+
if sigma is None: # no regularization
|
|
101
|
+
res = eps.reshape(-1)
|
|
102
|
+
else: # regularisation terms sigma(w[i+1]-w[i])
|
|
103
|
+
reg = [sigma * xp.diff(w) for w in w_list]
|
|
104
|
+
res = xp.concatenate((eps.reshape(-1), *reg))
|
|
105
|
+
|
|
106
|
+
# move residual to CPU if required
|
|
107
|
+
if xp.__name__ == 'cupy':
|
|
108
|
+
return res.get()
|
|
109
|
+
else:
|
|
110
|
+
return res
|
|
111
|
+
|
|
112
|
+
# form Jacobian
|
|
113
|
+
def eval_Jr(x):
|
|
114
|
+
|
|
115
|
+
# move x to GPU if required
|
|
116
|
+
x = xp.asarray(x)
|
|
117
|
+
|
|
118
|
+
# extract variables and unscale
|
|
119
|
+
xi = x[0] * xi_sc
|
|
120
|
+
split_inds = np.cumsum(p_dims)[:-1]
|
|
121
|
+
w_list = xp.split(x[2:], split_inds)
|
|
122
|
+
|
|
123
|
+
# form Gw (the form factor)
|
|
124
|
+
Gw = contract_tensor(G, w_list, skip_axes=q_axes)
|
|
125
|
+
|
|
126
|
+
# preallocate storage for intensity misfit derivative
|
|
127
|
+
deps = xp.empty((*q_dims, 2+np.sum(p_dims)))
|
|
128
|
+
|
|
129
|
+
# xi derivative
|
|
130
|
+
deps[...,0] = (xi_sc * Gw ) / I_data_std # scaled
|
|
131
|
+
|
|
132
|
+
# b derivative
|
|
133
|
+
deps[...,1] = b_sc / I_data_std # scaled
|
|
134
|
+
|
|
135
|
+
# w derivatives
|
|
136
|
+
inds = np.cumsum((2,*p_dims))
|
|
137
|
+
for i in range(len(p_dims)):
|
|
138
|
+
slice_i = slice(inds[i],inds[i+1])
|
|
139
|
+
w_contract_list = [w for k,w in enumerate(w_list) if k != i]
|
|
140
|
+
Gw_dw = contract_tensor(G, w_contract_list, skip_axes=[*q_axes,p_axes[0]+i])
|
|
141
|
+
deps[...,slice_i] = ( xi * Gw_dw ) / I_data_std[...,None]
|
|
142
|
+
|
|
143
|
+
# flatten intensity misfit derivative
|
|
144
|
+
deps = deps.reshape(-1)
|
|
145
|
+
|
|
146
|
+
# handle regularization
|
|
147
|
+
if sigma is None: # no regularization
|
|
148
|
+
jac = deps
|
|
149
|
+
else: # regularization term derivatives (sparse)
|
|
150
|
+
dreg1 = [sigma * xp.ones(n-1) for n in p_dims] # w[i+1] terms
|
|
151
|
+
dreg2 = [-sigma * xp.ones(n - 1) for n in p_dims] # -w[i] terms
|
|
152
|
+
jac = xp.concatenate((deps, *dreg1, *dreg2))
|
|
153
|
+
|
|
154
|
+
# move Jacobian to CPU if required
|
|
155
|
+
if xp.__name__ == 'cupy':
|
|
156
|
+
return jac.get()
|
|
157
|
+
else:
|
|
158
|
+
return jac
|
|
159
|
+
|
|
160
|
+
# set GALAHAD SNLS options
|
|
161
|
+
options = snls.initialize()
|
|
162
|
+
options['maxit'] = 1000
|
|
163
|
+
options['print_level'] = 2
|
|
164
|
+
options['jacobian_available'] = 2
|
|
165
|
+
#options['slls_options']['print_level'] = 1
|
|
166
|
+
options['slls_options']['maxit'] = 250
|
|
167
|
+
options['slls_options']['sbls_options']['factorization'] = 1 # use Schur-complement
|
|
168
|
+
options['slls_options']['sbls_options']['symmetric_linear_solver'] = 'sytr '
|
|
169
|
+
options['slls_options']['sbls_options']['definite_linear_solver'] = 'potr '
|
|
170
|
+
options['sllsb_options']['symmetric_linear_solver'] = 'sytr '
|
|
171
|
+
options['sllsb_options']['fdc_options']['symmetric_linear_solver'] = 'sytr '
|
|
172
|
+
options['sllsb_options']['cro_options']['symmetric_linear_solver'] = 'sytr '
|
|
173
|
+
# stopping criteria
|
|
174
|
+
options['stop_pg_relative'] = 1e-15
|
|
175
|
+
options['stop_pg_absolute'] = 1e-7
|
|
176
|
+
|
|
177
|
+
# form and scale initial optimization variable
|
|
178
|
+
x0_scaled = xp.hstack((xi0/xi_sc,b0/b_sc,*w0_list))
|
|
179
|
+
|
|
180
|
+
# move initial guess to CPU if required
|
|
181
|
+
if xp.__name__ == 'cupy':
|
|
182
|
+
x0_scaled = x0_scaled.get()
|
|
183
|
+
|
|
184
|
+
# set GALAHAD SNLS dimensions
|
|
185
|
+
n = 2 + np.sum(p_dims)
|
|
186
|
+
if sigma is None: # no regularization
|
|
187
|
+
m_r = np.prod(q_dims)
|
|
188
|
+
else: # regularization requested
|
|
189
|
+
m_r = np.prod(q_dims) + np.sum(np.array(p_dims)-1)
|
|
190
|
+
m_c = len(p_dims)
|
|
191
|
+
|
|
192
|
+
# set GALAHAD SNLS cohorts
|
|
193
|
+
ch_list = [i * np.ones(n, dtype=int) for i,n in enumerate(p_dims)]
|
|
194
|
+
cohort = np.concat(( np.array([-1,-1]), *ch_list))
|
|
195
|
+
|
|
196
|
+
# set GALAHAD SNLS Jacobian info
|
|
197
|
+
if sigma is None: # no regularization
|
|
198
|
+
Jr_type = 'dense'
|
|
199
|
+
Jr_ne = m_r * n
|
|
200
|
+
Jr_row = None
|
|
201
|
+
Jr_col = None
|
|
202
|
+
else: # regularization requested
|
|
203
|
+
Jr_type = 'coordinate'
|
|
204
|
+
nq = np.prod(q_dims)
|
|
205
|
+
Jr_ne = nq*n + 2*np.sum(np.array(p_dims)-1)
|
|
206
|
+
# flattened intensity misfit derivative
|
|
207
|
+
Jr_eps_row = np.tile(np.arange(nq),(n,1)).flatten('F')
|
|
208
|
+
Jr_eps_col = np.tile(np.arange(n),nq)
|
|
209
|
+
# sparse regularization derivatives for w
|
|
210
|
+
split_inds = np.cumsum(np.array(p_dims)-1)[:-1]
|
|
211
|
+
Jr_reg1_row = np.split(np.arange(nq,nq+np.sum(np.array(p_dims)-1)), split_inds)
|
|
212
|
+
Jr_reg2_row = Jr_reg1_row.copy()
|
|
213
|
+
Jr_reg1_col = [] # w[i+1] terms
|
|
214
|
+
Jr_reg2_col = [] # -w[i] terms
|
|
215
|
+
starts = np.cumsum(p_dims) + 2 - p_dims
|
|
216
|
+
for st, dim in zip(starts, p_dims):
|
|
217
|
+
Jr_reg1_col.append(np.arange(st+1, st+dim))
|
|
218
|
+
Jr_reg2_col.append(np.arange(st, st+dim-1))
|
|
219
|
+
# combined derivative
|
|
220
|
+
Jr_row = np.concat((Jr_eps_row,*Jr_reg1_row,*Jr_reg2_row))
|
|
221
|
+
Jr_col = np.concat((Jr_eps_col,*Jr_reg1_col,*Jr_reg2_col))
|
|
222
|
+
Jr_ptr_ne = 0
|
|
223
|
+
Jr_ptr = None
|
|
224
|
+
|
|
225
|
+
# initialise GALAHAD SNLS
|
|
226
|
+
snls.load(n, m_r, m_c, Jr_type, Jr_ne, Jr_row, Jr_col, Jr_ptr_ne, Jr_ptr, cohort, options)
|
|
227
|
+
|
|
228
|
+
# call GALAHAD SNLS with variable scaling
|
|
229
|
+
print('\nCalling GALAHAD SNLS...')
|
|
230
|
+
x, y, z, r, g, x_stat = snls.solve(n, m_r, m_c, x0_scaled, eval_r, Jr_ne, eval_Jr)
|
|
231
|
+
|
|
232
|
+
# get information
|
|
233
|
+
info = snls.information()
|
|
234
|
+
#print("inform:", inform)
|
|
235
|
+
print(" f: %.4f" % info['obj'])
|
|
236
|
+
print('** snls exit status:', info['status'])
|
|
237
|
+
|
|
238
|
+
# extract results and unscale
|
|
239
|
+
xi_opt = x[0] * xi_sc
|
|
240
|
+
b_opt = x[1] * b_sc
|
|
241
|
+
split_inds = np.cumsum(p_dims)[:-1]
|
|
242
|
+
w_opt_list = np.split(x[2:], split_inds)
|
|
243
|
+
|
|
244
|
+
print()
|
|
245
|
+
print('xi*: %.2e' % xi_opt)
|
|
246
|
+
print('b*: %.2e' % b_opt)
|
|
247
|
+
print('r*: %.15e' % np.linalg.norm(eval_r(x)))
|
|
248
|
+
|
|
249
|
+
# finalise GALAHAD SNLS
|
|
250
|
+
snls.terminate()
|
|
251
|
+
|
|
252
|
+
return xi_opt, b_opt, w_opt_list
|