Skip to content

Commit 648d587

Browse files
authored
Merge pull request #224 from ISISComputingGroup/MomentumFit
Momentum fit
2 parents 4f35fb9 + b25c03d commit 648d587

File tree

4 files changed

+213
-0
lines changed

4 files changed

+213
-0
lines changed
22.9 KB
Loading

doc/fitting/standard_fits.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,22 @@ g(x) = \max(f(x), \text{background} - \text{height})
183183
```{math}
184184
y = \min(g(x), \text{background})
185185
```
186+
187+
## Muon Momentum
188+
189+
API Reference: [`MuonMomentum`](ibex_bluesky_core.fitting.MuonMomentum)
190+
191+
Fits data from a momentum scan, it is designed for the specific use case of scanning over magnet current on muon instruments.
192+
193+
- `x0` - The center (x) of the model
194+
- `w` - The horizontal stretch factor of the model
195+
- `R` - The amplitude of the model
196+
- `b` - The minimum value (y) of the model
197+
- `p` - Changes the gradient of the tail ends of the model
198+
199+
``` {math}
200+
y = \left (\text{erfc} \mathopen{} \left(\frac{x-x_0}{w} \right) \mathclose{} \cdot \frac{R}{2} + b \right) \cdot \left (\frac{x}{x_0} \right)^p
201+
```
202+
203+
![MuonMomentumModel](../_static/images_fits/muons_momentum.png)
204+

src/ibex_bluesky_core/fitting.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"Gaussian",
2121
"Linear",
2222
"Lorentzian",
23+
"MuonMomentum",
2324
"NegativeTrapezoid",
2425
"Polynomial",
2526
"SlitScan",
@@ -687,3 +688,65 @@ def guess(
687688
return init_guess
688689

689690
return guess
691+
692+
693+
class MuonMomentum(Fit):
694+
"""Muon momentum fitting."""
695+
696+
equation = """
697+
y=(erfc((x-x0/w))*(r/2)+b)*((x/x0)**p)"""
698+
699+
@classmethod
700+
def model(cls, *args: int) -> lmfit.Model:
701+
"""Momentum scan model."""
702+
703+
def model(
704+
x: npt.NDArray[np.float64], x0: float, r: float, w: float, p: float, b: float
705+
) -> npt.NDArray[np.float64]:
706+
return (scipy.special.erfc((x - x0) / w) * (r / 2) + b) * ((x / x0) ** p)
707+
708+
return lmfit.Model(model, name=f"{cls.__name__} [{cls.equation}]")
709+
710+
@classmethod
711+
def guess(
712+
cls, *args: int
713+
) -> Callable[[npt.NDArray[np.float64], npt.NDArray[np.float64]], dict[str, lmfit.Parameter]]:
714+
"""Momentum Scan Fit Guessing."""
715+
716+
def guess(
717+
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
718+
) -> dict[str, lmfit.Parameter]:
719+
index_array = np.argsort(x)
720+
x = x[index_array]
721+
y = y[index_array]
722+
723+
index_min_y = np.argmin(y)
724+
index_max_y = np.argmax(y)
725+
726+
b = np.min(y)
727+
r = np.max(y) - b
728+
729+
x_slope = x[
730+
index_max_y:index_min_y
731+
] # Gets all x values between the maximum and minimum y
732+
733+
if len(x_slope) != 0:
734+
x0 = np.mean(x_slope)
735+
else:
736+
x0 = x[-1] # Picked as it can't be 0
737+
738+
p = 1 # Expected value, not likely to change
739+
740+
const = 3 # largest difference in erfc function is between -1.5 and 1.5
741+
w = np.abs(x[index_max_y] - x[index_min_y]) / const
742+
743+
init_guess = {
744+
"b": lmfit.Parameter("b", b),
745+
"r": lmfit.Parameter("r", r, min=0),
746+
"x0": lmfit.Parameter("x0", x0, min=0),
747+
"p": lmfit.Parameter("p", p, min=0),
748+
"w": lmfit.Parameter("w", w, min=0),
749+
}
750+
return init_guess
751+
752+
return guess

tests/callbacks/fitting/test_fitting_methods.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import typing
12
import warnings
23
from collections.abc import Callable
34
from unittest import mock
@@ -17,6 +18,7 @@
1718
Gaussian,
1819
Linear,
1920
Lorentzian,
21+
MuonMomentum,
2022
NegativeTrapezoid,
2123
Polynomial,
2224
SlitScan,
@@ -823,3 +825,132 @@ def test_guess_given_flat_data(self):
823825
outp = NegativeTrapezoid.guess()(x, y)
824826
# check that with flat data gradient guess is 0
825827
assert outp["gradient"] == pytest.approx(0.0, rel=1e-2)
828+
829+
830+
class TestMuonMomentum:
831+
values: typing.ClassVar[dict[str, float]] = {
832+
"x0": 21.89,
833+
"w": 0.301,
834+
"r": 110,
835+
"b": 3.37,
836+
"p": 1.82,
837+
}
838+
x: typing.ClassVar[npt.NDArray[np.float64]] = np.linspace(20.5, 23, 15, dtype=np.float64)
839+
840+
out: typing.ClassVar[npt.NDArray[np.float64]] = MuonMomentum.model().func(
841+
x=x,
842+
x0=values["x0"],
843+
w=values["w"],
844+
r=values["r"],
845+
b=values["b"],
846+
p=values["p"],
847+
)
848+
849+
class TestMuonMomentumModel:
850+
def test_muon_momentum_model_w(self):
851+
out2 = MuonMomentum.model().func(
852+
x=TestMuonMomentum.x,
853+
x0=TestMuonMomentum.values["x0"],
854+
w=TestMuonMomentum.values["w"] + 1,
855+
r=TestMuonMomentum.values["r"],
856+
b=TestMuonMomentum.values["b"],
857+
p=TestMuonMomentum.values["p"],
858+
)
859+
860+
assert np.mean(TestMuonMomentum.out) < np.mean(out2)
861+
862+
def test_muon_momentum_model_b(self):
863+
out2 = MuonMomentum.model().func(
864+
x=TestMuonMomentum.x,
865+
x0=TestMuonMomentum.values["x0"],
866+
w=TestMuonMomentum.values["w"],
867+
r=TestMuonMomentum.values["r"],
868+
b=TestMuonMomentum.values["b"] + 1,
869+
p=TestMuonMomentum.values["p"],
870+
)
871+
872+
assert np.min(TestMuonMomentum.out) < np.min(out2)
873+
874+
def test_muon_momentum_model_r(self):
875+
out2 = MuonMomentum.model().func(
876+
x=TestMuonMomentum.x,
877+
x0=TestMuonMomentum.values["x0"],
878+
w=TestMuonMomentum.values["w"],
879+
r=TestMuonMomentum.values["r"] + 10,
880+
b=TestMuonMomentum.values["b"],
881+
p=TestMuonMomentum.values["p"],
882+
)
883+
884+
assert np.max(TestMuonMomentum.out) - np.min(TestMuonMomentum.out) < np.max(
885+
out2
886+
) - np.min(out2)
887+
888+
def test_muon_momentum_model_x0(self):
889+
out2 = MuonMomentum.model().func(
890+
x=TestMuonMomentum.x,
891+
x0=TestMuonMomentum.values["x0"] + 1,
892+
w=TestMuonMomentum.values["w"],
893+
r=TestMuonMomentum.values["r"],
894+
b=TestMuonMomentum.values["b"],
895+
p=TestMuonMomentum.values["p"],
896+
)
897+
898+
assert (
899+
TestMuonMomentum.x[np.argmax(TestMuonMomentum.out)]
900+
< TestMuonMomentum.x[np.argmax(out2)]
901+
)
902+
assert (
903+
TestMuonMomentum.x[np.argmin(TestMuonMomentum.out)]
904+
< TestMuonMomentum.x[np.argmin(out2)]
905+
)
906+
907+
class TestMuonMomentumGuess:
908+
def test_muon_momentum_guess_b(self):
909+
x = np.array([0, 1, 2], dtype=np.float64)
910+
y = np.array([20, 20, 10], dtype=np.float64)
911+
912+
out = MuonMomentum.guess()(x, y)
913+
914+
assert out["b"].value is not None
915+
assert np.min(y) == out["b"].value
916+
917+
def test_muon_momentum_guess_r(self):
918+
x = np.array([0, 1, 2], dtype=np.float64)
919+
y = np.array([20, 20, 10], dtype=np.float64)
920+
921+
out = MuonMomentum.guess()(x, y)
922+
923+
assert out["r"].value is not None
924+
assert np.max(y) - np.min(y) == out["r"].value
925+
926+
def test_muon_momentum_guess_w(self):
927+
x = np.array([0, 1, 2], dtype=np.float64)
928+
y = np.array([20, 20, 10], dtype=np.float64)
929+
x1 = np.array([0, 1, 1.5], dtype=np.float64)
930+
931+
out = MuonMomentum.guess()(x, y)
932+
out1 = MuonMomentum.guess()(x1, y)
933+
934+
assert out["w"].value is not None
935+
assert out1["w"].value is not None
936+
assert out["w"].value > out1["w"].value
937+
938+
def test_muon_momentum_guess_x0(self):
939+
x = np.array([0, 1, 2], dtype=np.float64)
940+
y = np.array([20, 20, 10], dtype=np.float64)
941+
x1 = np.array([3, 4, 5], dtype=np.float64)
942+
943+
out = MuonMomentum.guess()(x, y)
944+
out1 = MuonMomentum.guess()(x1, y)
945+
946+
assert out["x0"].value is not None
947+
assert out1["x0"].value is not None
948+
assert out1["x0"].value > out["x0"].value
949+
950+
def test_muon_momentum_guess_x0_noslope(self):
951+
x = np.array([0, 1, 2], dtype=np.float64)
952+
y = np.array([20, 20, 20], dtype=np.float64)
953+
954+
out = MuonMomentum.guess()(x, y)
955+
956+
assert out["x0"].value == x[-1]

0 commit comments

Comments
 (0)