scipas 0.3.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.
- scipas/__init__.py +23 -0
- scipas/analysis/__init__.py +3 -0
- scipas/analysis/vedb/__init__.py +5 -0
- scipas/analysis/vedb/annihilation_fractions.py +105 -0
- scipas/analysis/vedb/diffusion_length.py +268 -0
- scipas/analysis/vedb/lineshape.py +167 -0
- scipas/analysis/vedb/ve_implanation.py +86 -0
- scipas/core/__init__.py +4 -0
- scipas/core/cdb.py +219 -0
- scipas/core/const.py +3 -0
- scipas/core/db.py +349 -0
- scipas/filter/__init__.py +8 -0
- scipas/filter/pals_coincidence.py +1 -0
- scipas/filter/pas_coincidence.py +215 -0
- scipas/libs/__init__.py +0 -0
- scipas/libs/positron_profile/__init__.py +0 -0
- scipas/libs/positron_profile/gosh_profile_parameters.txt +35 -0
- scipas/libs/positron_profile/makhov_profile_parameters.txt +35 -0
- scipas/model/__init__.py +5 -0
- scipas/model/layer.py +43 -0
- scipas/model/lifetime.py +53 -0
- scipas/model/material.py +86 -0
- scipas/model/sample.py +90 -0
- scipas/transport/__init__.py +4 -0
- scipas/transport/diffusion/__init__.py +3 -0
- scipas/transport/diffusion/positron_profile_solver.py +229 -0
- scipas/transport/implantation/__init__.py +4 -0
- scipas/transport/implantation/material_parameters.py +59 -0
- scipas/transport/implantation/multilayer.py +143 -0
- scipas/transport/implantation/profiles.py +102 -0
- scipas/transport/implantation/utils.py +39 -0
- scipas-0.3.0.dist-info/METADATA +231 -0
- scipas-0.3.0.dist-info/RECORD +36 -0
- scipas-0.3.0.dist-info/WHEEL +5 -0
- scipas-0.3.0.dist-info/licenses/LICENSE +21 -0
- scipas-0.3.0.dist-info/top_level.txt +1 -0
scipas/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Core analysis classes
|
|
2
|
+
from scipas.core import DB, CDB
|
|
3
|
+
from scipas.core.const import ELECTRON_REST_MASS_KEV
|
|
4
|
+
|
|
5
|
+
# Model / geometry
|
|
6
|
+
from scipas.model import Material, Defect, Layer, Sample
|
|
7
|
+
|
|
8
|
+
# Coincidence filter
|
|
9
|
+
from scipas.filter import PasCoincidenceFilter
|
|
10
|
+
|
|
11
|
+
# Transport
|
|
12
|
+
from scipas.transport import (
|
|
13
|
+
ghosh_profile,
|
|
14
|
+
makhov_profile,
|
|
15
|
+
ghosh_material_parameters,
|
|
16
|
+
makhov_material_parameters,
|
|
17
|
+
multilayer_implantation_profile,
|
|
18
|
+
profile_solver,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# VEDB analysis
|
|
22
|
+
from scipas.analysis import (DiffusionLengthOptimization, compute_s_lineshape, compute_w_lineshape,
|
|
23
|
+
variable_energy_implantation_profiles)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""Package for Variable Energy Doppler Broadening (VEDB) analysis"""
|
|
2
|
+
from .annihilation_fractions import compute_annihilation_fractions
|
|
3
|
+
from .diffusion_length import DiffusionLengthOptimization
|
|
4
|
+
from .lineshape import compute_s_lineshape, compute_w_lineshape
|
|
5
|
+
from .ve_implanation import variable_energy_implantation_profiles
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import xarray as xr
|
|
3
|
+
from warnings import warn
|
|
4
|
+
from scipas.model.sample import Sample
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def compute_annihilation_fractions(positron_profile: xr.DataArray, sample: Sample) -> xr.DataArray:
|
|
8
|
+
"""
|
|
9
|
+
Compute the annihilation fraction of positrons in each layer and at the surface.
|
|
10
|
+
|
|
11
|
+
The total annihilation rate is decomposed into contributions from:
|
|
12
|
+
- Surface (layer = -1): positrons annihilating at the sample surface,
|
|
13
|
+
governed by the surface absorption length.
|
|
14
|
+
- Bulk layers (layer = 0, 1, ...): positrons annihilating in each
|
|
15
|
+
material layer, weighted by the effective annihilation rate.
|
|
16
|
+
|
|
17
|
+
The fractions are normalized so that they sum to 1.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
positron_profile : xr.DataArray
|
|
22
|
+
1D positron density profile after diffusion [positrons/nm],
|
|
23
|
+
with a single depth coordinate (any name is accepted).
|
|
24
|
+
Must be normalized such that its integral equals 1.
|
|
25
|
+
sample : Sample
|
|
26
|
+
Multilayer sample defining geometry, material annihilation rates,
|
|
27
|
+
and surface absorption length.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
xr.DataArray
|
|
32
|
+
Normalized annihilation fractions with coordinate 'layer':
|
|
33
|
+
- layer = -1 : surface annihilation fraction
|
|
34
|
+
- layer = 0, 1, 2, ... : bulk annihilation fraction per layer
|
|
35
|
+
|
|
36
|
+
Raises
|
|
37
|
+
------
|
|
38
|
+
ValueError
|
|
39
|
+
If positron_profile is not 1D.
|
|
40
|
+
|
|
41
|
+
Warns
|
|
42
|
+
-----
|
|
43
|
+
UserWarning
|
|
44
|
+
If the positron profile extends beyond the sample length.
|
|
45
|
+
Annihilation beyond the sample boundary is ignored and
|
|
46
|
+
fractions will not sum to 1 correctly.
|
|
47
|
+
|
|
48
|
+
Examples
|
|
49
|
+
--------
|
|
50
|
+
>>> from scipas.model import Sample, Layer, Material
|
|
51
|
+
>>> from scipas.analysis import compute_annihilation_fractions
|
|
52
|
+
>>> import xarray as xr
|
|
53
|
+
>>> import numpy as np
|
|
54
|
+
>>> silicon = Material(name="Silicon",
|
|
55
|
+
... diffusion=1,
|
|
56
|
+
... mobility=1,
|
|
57
|
+
... bulk_annihilation_rate=1)
|
|
58
|
+
>>> layer = Layer(start=0.0, width=10000.0, material=silicon)
|
|
59
|
+
>>> sample = Sample(layers=[layer], absorption_length=1)
|
|
60
|
+
>>> depth = np.arange(0, layer.width+1, 1)
|
|
61
|
+
>>> positron_annihilation_profile = xr.DataArray(np.ones_like(depth), coords={'x':depth})
|
|
62
|
+
>>> positron_annihilation_profile /= positron_annihilation_profile.integrate('x')
|
|
63
|
+
>>> res = compute_annihilation_fractions(positron_annihilation_profile, sample)
|
|
64
|
+
>>> round(float(res.sum())) == 1.0
|
|
65
|
+
True
|
|
66
|
+
>>> round(float(res.sel(layer=-1)), 5) == 1e-4
|
|
67
|
+
True
|
|
68
|
+
"""
|
|
69
|
+
# check input
|
|
70
|
+
|
|
71
|
+
if positron_profile.ndim != 1:
|
|
72
|
+
raise ValueError(f"positron_profile must be 1D, got {positron_profile.ndim}D")
|
|
73
|
+
depth_dim = positron_profile.dims[0] # infer axis name
|
|
74
|
+
|
|
75
|
+
profile_max_depth = positron_profile.coords[depth_dim].max().item()
|
|
76
|
+
sample_length = sample.sample_length()
|
|
77
|
+
if profile_max_depth > sample_length:
|
|
78
|
+
warn(
|
|
79
|
+
f"Positron profile extends to {profile_max_depth:.1f} nm but sample ends at "
|
|
80
|
+
f"{sample_length:.1f} nm. Annihilation beyond the sample boundary is ignored, "
|
|
81
|
+
f"fractions will not sum to 1 correctly. Consider extending the last layer."
|
|
82
|
+
)
|
|
83
|
+
# defs
|
|
84
|
+
layers = sample.layers
|
|
85
|
+
num_of_layers = len(layers)
|
|
86
|
+
layers_names = range(-1, num_of_layers)
|
|
87
|
+
annihilation_rate = np.zeros(num_of_layers+1) # layers and surface
|
|
88
|
+
|
|
89
|
+
# surface positrons annihilation rate
|
|
90
|
+
annihilation_rate[0] = (positron_profile.sel({depth_dim: 0.0}, method='nearest').item() *
|
|
91
|
+
sample.layers[0].material.diffusion / sample.absorption_length)
|
|
92
|
+
|
|
93
|
+
# layers positrons annihilation rates
|
|
94
|
+
for i, layer in enumerate(layers):
|
|
95
|
+
layer_positron_profile = positron_profile.sel({depth_dim: slice(layer.start, layer.start + layer.width)})
|
|
96
|
+
positron_fraction_in_layer = layer_positron_profile.integrate(depth_dim)
|
|
97
|
+
annihilation_rate[i+1] = layer.material.effective_annihilation_rate() * positron_fraction_in_layer.item()
|
|
98
|
+
|
|
99
|
+
# norm
|
|
100
|
+
annihilation_fractions = xr.DataArray(annihilation_rate, coords={'layer':layers_names})/annihilation_rate.sum()
|
|
101
|
+
|
|
102
|
+
return annihilation_fractions
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from uncertainties import ufloat
|
|
4
|
+
from uncertainties.unumpy import nominal_values, std_devs
|
|
5
|
+
from scipy.optimize import least_squares
|
|
6
|
+
from scipas.model import Sample, Material, Layer
|
|
7
|
+
from scipas.transport.diffusion import profile_solver
|
|
8
|
+
from scipas.analysis.vedb.annihilation_fractions import compute_annihilation_fractions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DiffusionLengthOptimization:
|
|
12
|
+
"""
|
|
13
|
+
Optimize positron diffusion lengths in a multilayer sample to best fit
|
|
14
|
+
experimentally measured lineshpae-parameter from variable-energy Doppler broadening (VEDB).
|
|
15
|
+
The parametrer can be S or W.
|
|
16
|
+
|
|
17
|
+
The optimization solves the simplified positron transport equation in layered samples:
|
|
18
|
+
|
|
19
|
+
d²c(z)/dz² − (1 / L_eff²) * c(z) = -I(z)
|
|
20
|
+
|
|
21
|
+
where:
|
|
22
|
+
- c(z) : positron concentration as a function of depth
|
|
23
|
+
- L_eff : effective diffusion length, L_eff = sqrt(D / λ)
|
|
24
|
+
- I(z) : positron implantation source term
|
|
25
|
+
|
|
26
|
+
For each trial set of diffusion lengths, the class:
|
|
27
|
+
1. Constructs a normalized trial sample (D=1, λ = 1/L²).
|
|
28
|
+
2. Solves the positron transport equation for each implantation energy.
|
|
29
|
+
3. Computes annihilation fractions per layer and surface.
|
|
30
|
+
4. Estimates S-parameters via linear least squares.
|
|
31
|
+
5. Minimizes weighted residuals via nonlinear least squares.
|
|
32
|
+
|
|
33
|
+
The number of layers is inferred automatically from the initial guess sample,
|
|
34
|
+
making this class applicable to single-layer, two-layer, and three-layer models
|
|
35
|
+
without modification.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
positron_implantation_profiles : list of xr.DataArray
|
|
40
|
+
Depth profiles of implanted positrons for each beam energy [positrons/nm].
|
|
41
|
+
Each profile must be normalized and have a single depth coordinate.
|
|
42
|
+
s_measurement : pd.Series of uncertainties.ufloat or uarray
|
|
43
|
+
Measured S-parameters with uncertainties for each beam energy.
|
|
44
|
+
Index must correspond to the implantation profiles.
|
|
45
|
+
initial_guess : Sample
|
|
46
|
+
Initial sample defining layer geometry and material properties.
|
|
47
|
+
Used to extract layer widths and initial diffusion length estimates.
|
|
48
|
+
num_of_mesh_cells : int, optional
|
|
49
|
+
Number of mesh points for the transport equation solver. Default is 10000.
|
|
50
|
+
Higher values improve accuracy at the cost of computation time.
|
|
51
|
+
|
|
52
|
+
Attributes
|
|
53
|
+
----------
|
|
54
|
+
n_layers : int
|
|
55
|
+
Number of layers, inferred from initial_guess.
|
|
56
|
+
s_measurement : np.ndarray
|
|
57
|
+
Nominal S-parameter values extracted from the ufloat series.
|
|
58
|
+
s_measurement_dev : np.ndarray
|
|
59
|
+
Standard deviations of the S-parameter measurements.
|
|
60
|
+
|
|
61
|
+
Notes
|
|
62
|
+
-----
|
|
63
|
+
The trial sample uses normalized units (D=1, absorption_length=1) since
|
|
64
|
+
only the diffusion length L = sqrt(D/λ) affects the annihilation profile shape.
|
|
65
|
+
The absolute values of D and absorption_length do not affect the optimization result.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self,
|
|
69
|
+
positron_implantation_profiles: list,
|
|
70
|
+
s_measurement: pd.Series,
|
|
71
|
+
initial_guess: Sample,
|
|
72
|
+
num_of_mesh_cells: int = 10000):
|
|
73
|
+
|
|
74
|
+
self.positron_implantation_profiles = positron_implantation_profiles
|
|
75
|
+
self.initial_sample = initial_guess
|
|
76
|
+
self.s_measurement = nominal_values(s_measurement)
|
|
77
|
+
self.s_measurement_dev = std_devs(s_measurement)
|
|
78
|
+
self.num_of_mesh_cells = num_of_mesh_cells
|
|
79
|
+
self.n_layers = len(initial_guess.layers)
|
|
80
|
+
|
|
81
|
+
def make_sample(self, diffusion_lengths: list) -> Sample:
|
|
82
|
+
"""
|
|
83
|
+
Construct a normalized trial sample for a given set of diffusion lengths.
|
|
84
|
+
|
|
85
|
+
Each layer is assigned D=1 and λ = 1/L² so that the effective diffusion
|
|
86
|
+
length matches the trial value. Layer geometry (start, width) is preserved
|
|
87
|
+
from the initial guess. The absorption length is set to 1 (normalized)
|
|
88
|
+
since it does not affect the diffusion length optimization.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
diffusion_lengths : list of float
|
|
93
|
+
Trial diffusion lengths [nm], one per layer.
|
|
94
|
+
Must have the same length as the number of layers in the initial sample.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
Sample
|
|
99
|
+
Normalized single- or multilayer sample with the given diffusion lengths.
|
|
100
|
+
"""
|
|
101
|
+
layers = [
|
|
102
|
+
Layer(
|
|
103
|
+
start=self.initial_sample.layers[i].start,
|
|
104
|
+
width=self.initial_sample.layers[i].width,
|
|
105
|
+
material=Material(diffusion=1, mobility=0, bulk_annihilation_rate=1 / dl ** 2)
|
|
106
|
+
)
|
|
107
|
+
for i, dl in enumerate(diffusion_lengths)
|
|
108
|
+
]
|
|
109
|
+
return Sample(layers=layers, absorption_length=1)
|
|
110
|
+
|
|
111
|
+
def layers_transport_solver(self, sample: Sample, implantation_profiles: list) -> np.ndarray:
|
|
112
|
+
"""
|
|
113
|
+
Solve the positron transport equation for each implantation profile
|
|
114
|
+
and return the annihilation fraction matrix.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
sample : Sample
|
|
119
|
+
Trial sample for which the transport equation is solved.
|
|
120
|
+
implantation_profiles : list of xr.DataArray
|
|
121
|
+
Positron implantation profiles, one per beam energy.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
np.ndarray
|
|
126
|
+
Annihilation fraction matrix of shape (n_profiles, n_layers + 1).
|
|
127
|
+
Each row corresponds to one beam energy; columns are
|
|
128
|
+
[surface, layer_0, layer_1, ...].
|
|
129
|
+
"""
|
|
130
|
+
frac_matrix = np.zeros((len(implantation_profiles), self.n_layers + 1))
|
|
131
|
+
for i, p in enumerate(implantation_profiles):
|
|
132
|
+
frac_matrix[i] = compute_annihilation_fractions(
|
|
133
|
+
positron_profile=profile_solver(p, sample, mesh_size=self.num_of_mesh_cells),
|
|
134
|
+
sample=sample).values
|
|
135
|
+
return frac_matrix
|
|
136
|
+
|
|
137
|
+
def layer_s_value(self, frac_matrix: np.ndarray) -> np.ndarray:
|
|
138
|
+
"""
|
|
139
|
+
Estimate the S-parameter characteristic of each annihilation channel
|
|
140
|
+
via linear least squares.
|
|
141
|
+
|
|
142
|
+
Solves: frac_matrix @ s_layers ≈ s_measurement
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
frac_matrix : np.ndarray
|
|
147
|
+
Annihilation fraction matrix of shape (n_profiles, n_channels).
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
np.ndarray
|
|
152
|
+
Estimated S-parameter per annihilation channel [surface, layer_0, ...].
|
|
153
|
+
Length is n_layers + 1.
|
|
154
|
+
"""
|
|
155
|
+
return np.linalg.lstsq(frac_matrix, self.s_measurement, rcond=None)[0]
|
|
156
|
+
|
|
157
|
+
def residuals(self, diffusion_lengths: np.ndarray) -> np.ndarray:
|
|
158
|
+
"""
|
|
159
|
+
Compute weighted residuals between measured and modeled S-parameters.
|
|
160
|
+
|
|
161
|
+
For a given trial set of diffusion lengths, solves the transport equation,
|
|
162
|
+
estimates S-parameters per layer, and returns the normalized difference:
|
|
163
|
+
|
|
164
|
+
residuals = (S_calc - S_measured) / sigma_measured
|
|
165
|
+
|
|
166
|
+
If any estimated S-parameter per layer is unphysical (negative or >= 1),
|
|
167
|
+
returns a large residual vector to penalize the optimizer.
|
|
168
|
+
|
|
169
|
+
Parameters
|
|
170
|
+
----------
|
|
171
|
+
diffusion_lengths : np.ndarray
|
|
172
|
+
Trial diffusion lengths [nm], one per layer.
|
|
173
|
+
|
|
174
|
+
Returns
|
|
175
|
+
-------
|
|
176
|
+
np.ndarray
|
|
177
|
+
Weighted residual vector of length n_profiles.
|
|
178
|
+
"""
|
|
179
|
+
sample = self.make_sample(diffusion_lengths)
|
|
180
|
+
frac_matrix = self.layers_transport_solver(sample, self.positron_implantation_profiles)
|
|
181
|
+
s_vec = self.layer_s_value(frac_matrix)
|
|
182
|
+
s_calc = frac_matrix @ s_vec
|
|
183
|
+
if np.any(s_vec[1:] >= 1) or np.any(s_vec <= 0):
|
|
184
|
+
return np.full_like(s_calc, 1e6)
|
|
185
|
+
return (s_calc - self.s_measurement) / self.s_measurement_dev
|
|
186
|
+
|
|
187
|
+
def extract_fit_results(self, ls_results) -> list:
|
|
188
|
+
"""
|
|
189
|
+
Extract best-fit parameters and full covariance matrix from least-squares result.
|
|
190
|
+
|
|
191
|
+
The covariance matrix is estimated from the Jacobian via the Gauss-Newton
|
|
192
|
+
approximation using SVD:
|
|
193
|
+
|
|
194
|
+
J ≈ U S Vᵀ → cov ≈ Vᵀ diag(1/s²) V
|
|
195
|
+
|
|
196
|
+
Singular values below numerical precision are discarded to regularize
|
|
197
|
+
the inversion.
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
ls_results : OptimizeResult
|
|
201
|
+
Result from `scipy.optimize.least_squares`. Must contain
|
|
202
|
+
`jac` (Jacobian at solution) and `x` (best-fit parameters).
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
best_fit : np.ndarray
|
|
207
|
+
Best-fit diffusion lengths [nm], shape (n_layers,).
|
|
208
|
+
cov : np.ndarray
|
|
209
|
+
Full covariance matrix, shape (n_layers, n_layers).
|
|
210
|
+
Diagonal entries are variances; off-diagonal entries capture
|
|
211
|
+
parameter correlations and should not be neglected for
|
|
212
|
+
correlated layers.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
J = ls_results.jac
|
|
216
|
+
_, s, VT = np.linalg.svd(J, full_matrices=False)
|
|
217
|
+
threshold = np.finfo(float).eps * max(J.shape) * s[0]
|
|
218
|
+
s = s[s > threshold]
|
|
219
|
+
VT = VT[:s.size]
|
|
220
|
+
cov = np.dot(VT.T / s ** 2, VT)
|
|
221
|
+
return ls_results.x, cov
|
|
222
|
+
|
|
223
|
+
def optimize_diffusion_length(self, bounds=None) -> tuple:
|
|
224
|
+
"""
|
|
225
|
+
Optimize diffusion lengths for all layers simultaneously.
|
|
226
|
+
|
|
227
|
+
Performs nonlinear least-squares minimization of the weighted residuals
|
|
228
|
+
between measured and modeled S-parameters. Initial guesses are derived
|
|
229
|
+
from the material properties of the initial sample.
|
|
230
|
+
|
|
231
|
+
Parameters
|
|
232
|
+
----------
|
|
233
|
+
bounds : tuple of (float, float), optional
|
|
234
|
+
(lower, upper) bounds applied equally to all diffusion lengths [nm].
|
|
235
|
+
Must satisfy 0 <= lower <= upper. Default is (0, 1000).
|
|
236
|
+
|
|
237
|
+
Returns
|
|
238
|
+
-------
|
|
239
|
+
best_fit : np.ndarray
|
|
240
|
+
Best-fit diffusion lengths [nm], shape (n_layers,).
|
|
241
|
+
Ordered as [layer_0, layer_1, ...].
|
|
242
|
+
cov : np.ndarray
|
|
243
|
+
Full covariance matrix of shape (n_layers, n_layers).
|
|
244
|
+
Diagonal entries are variances; off-diagonal entries capture
|
|
245
|
+
parameter correlations. Marginal uncertainties can be obtained
|
|
246
|
+
via np.sqrt(np.diag(cov)).
|
|
247
|
+
|
|
248
|
+
Raises
|
|
249
|
+
------
|
|
250
|
+
ValueError
|
|
251
|
+
If lower bound exceeds upper bound.
|
|
252
|
+
"""
|
|
253
|
+
initial_guess = [
|
|
254
|
+
(m.diffusion / m.effective_annihilation_rate()) ** 0.5
|
|
255
|
+
for m in (layer.material for layer in self.initial_sample.layers)
|
|
256
|
+
]
|
|
257
|
+
|
|
258
|
+
if bounds is None:
|
|
259
|
+
bounds = (0, 1000)
|
|
260
|
+
elif not 0 <= bounds[0] <= bounds[1]:
|
|
261
|
+
raise ValueError("lower bound must be less than upper bound")
|
|
262
|
+
|
|
263
|
+
lb = [bounds[0]] * self.n_layers
|
|
264
|
+
ub = [bounds[1]] * self.n_layers
|
|
265
|
+
|
|
266
|
+
ls_result = least_squares(fun=self.residuals, x0=initial_guess, bounds=(lb, ub))
|
|
267
|
+
|
|
268
|
+
return self.extract_fit_results(ls_result)
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from scipas.core.const import ELECTRON_REST_MASS_KEV
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def compute_s_lineshape(
|
|
6
|
+
db_spectra: list,
|
|
7
|
+
energies,
|
|
8
|
+
energy_domain_total,
|
|
9
|
+
energy_domain_s,
|
|
10
|
+
centralize: bool = True,
|
|
11
|
+
center_value: float = ELECTRON_REST_MASS_KEV,
|
|
12
|
+
) -> pd.Series:
|
|
13
|
+
"""
|
|
14
|
+
Compute S-parameter vs. positron beam energy from a series of DB spectra.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
db_spectra : list of DB
|
|
19
|
+
DB spectra, one per beam energy, in the same order as ``energies``.
|
|
20
|
+
energies : array-like
|
|
21
|
+
Positron beam energies [keV], one per spectrum.
|
|
22
|
+
energy_domain_total : iterable of float
|
|
23
|
+
(e_low, e_high) — full integration window for normalization [keV].
|
|
24
|
+
energy_domain_s : iterable of float
|
|
25
|
+
(e_low, e_high) — central window for the S parameter [keV].
|
|
26
|
+
centralize : bool, optional
|
|
27
|
+
If True (default), recenters each spectrum's axis calibration so the
|
|
28
|
+
annihilation peak aligns with ``center_value`` before computing.
|
|
29
|
+
Modifies each DB's axis calibration in place.
|
|
30
|
+
center_value : float, optional
|
|
31
|
+
Target energy for peak centralization [keV].
|
|
32
|
+
Defaults to the electron rest mass energy (``ELECTRON_REST_MASS_KEV``).
|
|
33
|
+
Use 0.0 for CDB or momentum spectra expressed as energy shifts.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
pd.Series of uncertainties.ufloat
|
|
38
|
+
S parameter vs. beam energy, indexed by ``energies``, named ``'S'``.
|
|
39
|
+
|
|
40
|
+
Raises
|
|
41
|
+
------
|
|
42
|
+
ValueError
|
|
43
|
+
If ``db_spectra`` and ``energies`` have different lengths.
|
|
44
|
+
|
|
45
|
+
Examples
|
|
46
|
+
--------
|
|
47
|
+
>>> import numpy as np
|
|
48
|
+
>>> from scispectrum import Spectrum, AxisCalibration, ResolutionCalibration
|
|
49
|
+
>>> from scipas.core.db import DB
|
|
50
|
+
>>> from scipas.analysis.vedb.lineshape import compute_s_lineshape
|
|
51
|
+
>>> def make_db(center):
|
|
52
|
+
... bins = np.linspace(center - 10, center + 10, 200)
|
|
53
|
+
... ax = (bins[:-1] + bins[1:]) / 2
|
|
54
|
+
... counts = np.round(1e4 * np.exp(-0.5 * ((ax - center) / 1.5) ** 2) + 50).astype(int)
|
|
55
|
+
... spec = Spectrum(counts=counts, counts_err=np.sqrt(counts),
|
|
56
|
+
... axis_calib=AxisCalibration.from_array(ax),
|
|
57
|
+
... resolution_calib=ResolutionCalibration(lambda e: 3.0))
|
|
58
|
+
... return DB.from_spectrum(spec)
|
|
59
|
+
>>> db_list = [make_db(511.0), make_db(510.8)]
|
|
60
|
+
>>> s = compute_s_lineshape(db_list, [5.0, 10.0],
|
|
61
|
+
... energy_domain_total=[507.0, 515.0],
|
|
62
|
+
... energy_domain_s=[510.2, 511.8])
|
|
63
|
+
>>> list(s.index) == [5.0, 10.0]
|
|
64
|
+
True
|
|
65
|
+
>>> all(0 < float(v.nominal_value) < 1 for v in s)
|
|
66
|
+
True
|
|
67
|
+
"""
|
|
68
|
+
if len(db_spectra) != len(energies):
|
|
69
|
+
raise ValueError(
|
|
70
|
+
f"db_spectra and energies must have the same length "
|
|
71
|
+
f"(got {len(db_spectra)} spectra and {len(energies)} energies)."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
s_values = []
|
|
75
|
+
for db in db_spectra:
|
|
76
|
+
if centralize:
|
|
77
|
+
db.recenter(center_value)
|
|
78
|
+
s_values.append(db.s_parameter_calculation(energy_domain_total, energy_domain_s))
|
|
79
|
+
|
|
80
|
+
s = pd.Series(s_values, index=list(energies), name='S')
|
|
81
|
+
s.index.name = 'energy'
|
|
82
|
+
return s
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def compute_w_lineshape(
|
|
86
|
+
db_spectra: list,
|
|
87
|
+
energies,
|
|
88
|
+
energy_domain_total,
|
|
89
|
+
energy_domain_w_left,
|
|
90
|
+
energy_domain_w_right,
|
|
91
|
+
centralize: bool = True,
|
|
92
|
+
center_value: float = ELECTRON_REST_MASS_KEV,
|
|
93
|
+
) -> pd.Series:
|
|
94
|
+
"""
|
|
95
|
+
Compute W-parameter vs. positron beam energy from a series of DB spectra.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
db_spectra : list of DB
|
|
100
|
+
DB spectra, one per beam energy, in the same order as ``energies``.
|
|
101
|
+
energies : array-like
|
|
102
|
+
Positron beam energies [keV], one per spectrum.
|
|
103
|
+
energy_domain_total : iterable of float
|
|
104
|
+
(e_low, e_high) — full integration window for normalization [keV].
|
|
105
|
+
energy_domain_w_left : iterable of float
|
|
106
|
+
(e_low, e_high) — left wing window for the W parameter [keV].
|
|
107
|
+
energy_domain_w_right : iterable of float
|
|
108
|
+
(e_low, e_high) — right wing window for the W parameter [keV].
|
|
109
|
+
centralize : bool, optional
|
|
110
|
+
If True (default), recenters each spectrum's axis calibration so the
|
|
111
|
+
annihilation peak aligns with ``center_value`` before computing.
|
|
112
|
+
Modifies each DB's axis calibration in place.
|
|
113
|
+
center_value : float, optional
|
|
114
|
+
Target energy for peak centralization [keV].
|
|
115
|
+
Defaults to the electron rest mass energy (``ELECTRON_REST_MASS_KEV``).
|
|
116
|
+
Use 0.0 for CDB or momentum spectra expressed as energy shifts.
|
|
117
|
+
|
|
118
|
+
Returns
|
|
119
|
+
-------
|
|
120
|
+
pd.Series of uncertainties.ufloat
|
|
121
|
+
W parameter vs. beam energy, indexed by ``energies``, named ``'W'``.
|
|
122
|
+
|
|
123
|
+
Raises
|
|
124
|
+
------
|
|
125
|
+
ValueError
|
|
126
|
+
If ``db_spectra`` and ``energies`` have different lengths.
|
|
127
|
+
|
|
128
|
+
Examples
|
|
129
|
+
--------
|
|
130
|
+
>>> import numpy as np
|
|
131
|
+
>>> from scispectrum import Spectrum, AxisCalibration, ResolutionCalibration
|
|
132
|
+
>>> from scipas.core.db import DB
|
|
133
|
+
>>> from scipas.analysis.vedb.lineshape import compute_w_lineshape
|
|
134
|
+
>>> def make_db(center):
|
|
135
|
+
... bins = np.linspace(center - 10, center + 10, 200)
|
|
136
|
+
... ax = (bins[:-1] + bins[1:]) / 2
|
|
137
|
+
... counts = np.round(1e4 * np.exp(-0.5 * ((ax - center) / 1.5) ** 2) + 50).astype(int)
|
|
138
|
+
... spec = Spectrum(counts=counts, counts_err=np.sqrt(counts),
|
|
139
|
+
... axis_calib=AxisCalibration.from_array(ax),
|
|
140
|
+
... resolution_calib=ResolutionCalibration(lambda e: 3.0))
|
|
141
|
+
... return DB.from_spectrum(spec)
|
|
142
|
+
>>> db_list = [make_db(511.0), make_db(510.8)]
|
|
143
|
+
>>> w = compute_w_lineshape(db_list, [5.0, 10.0],
|
|
144
|
+
... energy_domain_total=[507.0, 515.0],
|
|
145
|
+
... energy_domain_w_left=[507.5, 509.3],
|
|
146
|
+
... energy_domain_w_right=[512.7, 514.5])
|
|
147
|
+
>>> list(w.index) == [5.0, 10.0]
|
|
148
|
+
True
|
|
149
|
+
>>> all(0 < float(v.nominal_value) < 1 for v in w)
|
|
150
|
+
True
|
|
151
|
+
"""
|
|
152
|
+
if len(db_spectra) != len(energies):
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"db_spectra and energies must have the same length "
|
|
155
|
+
f"(got {len(db_spectra)} spectra and {len(energies)} energies)."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
w_values = []
|
|
159
|
+
for db in db_spectra:
|
|
160
|
+
if centralize:
|
|
161
|
+
db.recenter(center_value)
|
|
162
|
+
w_values.append(db.w_parameter_calculation(
|
|
163
|
+
energy_domain_total, energy_domain_w_left, energy_domain_w_right))
|
|
164
|
+
|
|
165
|
+
w = pd.Series(w_values, index=list(energies), name='W')
|
|
166
|
+
w.index.name = 'energy'
|
|
167
|
+
return w
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import xarray as xr
|
|
3
|
+
from scipas.transport.implantation import multilayer_implantation_profile, makhov_profile
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def variable_energy_implantation_profiles(
|
|
7
|
+
energies,
|
|
8
|
+
depth_vector: np.ndarray,
|
|
9
|
+
materials_parameters: list,
|
|
10
|
+
densities: list,
|
|
11
|
+
widths: list,
|
|
12
|
+
implantation_profile_function=makhov_profile,
|
|
13
|
+
) -> list:
|
|
14
|
+
"""
|
|
15
|
+
Compute positron implantation profiles for a series of beam energies.
|
|
16
|
+
|
|
17
|
+
Wraps ``multilayer_implantation_profile`` in a loop over ``energies``,
|
|
18
|
+
returning one profile per energy. Handles both single-layer (homogeneous
|
|
19
|
+
substrate) and multilayer (thin film on substrate) geometries.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
energies : array-like
|
|
24
|
+
Positron beam energies [keV] for which to compute implantation profiles.
|
|
25
|
+
depth_vector : np.ndarray
|
|
26
|
+
1D depth grid [nm] spanning the full sample, e.g.
|
|
27
|
+
``np.linspace(0, sample.sample_length(), 10000)``.
|
|
28
|
+
materials_parameters : list of pd.Series
|
|
29
|
+
Material parameters for each layer, one entry per layer.
|
|
30
|
+
Obtain via ``makhov_material_parameters()`` or ``ghosh_material_parameters()``.
|
|
31
|
+
For a homogeneous substrate pass a single-element list.
|
|
32
|
+
densities : list of float
|
|
33
|
+
Actual material density [g/cm³] for each layer.
|
|
34
|
+
Must be the same length as ``materials_parameters``.
|
|
35
|
+
widths : list of float
|
|
36
|
+
Width of each layer [nm], from surface to substrate.
|
|
37
|
+
If ``depth_vector[-1]`` exceeds ``sum(widths)``, the last layer is
|
|
38
|
+
extended to fill the remaining depth (semi-infinite substrate approximation).
|
|
39
|
+
For a homogeneous substrate pass ``[sample_length]``.
|
|
40
|
+
implantation_profile_function : callable, optional
|
|
41
|
+
Profile function to use for each layer.
|
|
42
|
+
Must have the signature
|
|
43
|
+
``f(positron_energy, depth_vector, density, material_params) -> xr.DataArray``.
|
|
44
|
+
Defaults to ``makhov_profile``.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
list of xr.DataArray
|
|
49
|
+
Implantation profiles, one per entry in ``energies``, each with
|
|
50
|
+
coordinate ``'x'`` in nm and units of positrons/nm (normalised so
|
|
51
|
+
the integral over depth equals 1).
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
>>> import numpy as np
|
|
56
|
+
>>> from scipas.transport import makhov_material_parameters, makhov_profile
|
|
57
|
+
>>> from scipas.analysis.vedb.ve_implanation import variable_energy_implantation_profiles
|
|
58
|
+
>>> params = makhov_material_parameters()
|
|
59
|
+
>>> cu = params[params['Material'] == 'Cu'].iloc[0]
|
|
60
|
+
>>> depth = np.linspace(0, 10000, 5000)
|
|
61
|
+
>>> profiles = variable_energy_implantation_profiles(
|
|
62
|
+
... energies=[5.0, 10.0, 20.0],
|
|
63
|
+
... depth_vector=depth,
|
|
64
|
+
... materials_parameters=[cu],
|
|
65
|
+
... densities=[cu.density],
|
|
66
|
+
... widths=[10000],
|
|
67
|
+
... )
|
|
68
|
+
>>> len(profiles)
|
|
69
|
+
3
|
|
70
|
+
>>> all(isinstance(p, xr.DataArray) for p in profiles)
|
|
71
|
+
True
|
|
72
|
+
>>> all('x' in p.coords for p in profiles)
|
|
73
|
+
True
|
|
74
|
+
"""
|
|
75
|
+
profiles = []
|
|
76
|
+
for energy in energies:
|
|
77
|
+
profile = multilayer_implantation_profile(
|
|
78
|
+
positron_energy=float(energy),
|
|
79
|
+
depth_vector=depth_vector,
|
|
80
|
+
widths=list(widths),
|
|
81
|
+
materials_parameters=list(materials_parameters),
|
|
82
|
+
densities=list(densities),
|
|
83
|
+
implantation_profile_function=implantation_profile_function,
|
|
84
|
+
)
|
|
85
|
+
profiles.append(profile)
|
|
86
|
+
return profiles
|
scipas/core/__init__.py
ADDED