Skip to content

Commit 542829f

Browse files
committed
Refactor & add tests
1 parent 18828c0 commit 542829f

File tree

2 files changed

+148
-39
lines changed

2 files changed

+148
-39
lines changed

src/ibex_bluesky_core/fitting.py

Lines changed: 34 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,19 @@ def _center_of_mass_and_mass(
514514
return com, total_area
515515

516516

517+
def _guess_cen_and_width(
518+
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
519+
) -> tuple[float, float]:
520+
"""Guess the center and width of a positive peak."""
521+
com, total_area = _center_of_mass_and_mass(x, y)
522+
y_range = np.max(y) - np.min(y)
523+
if y_range == 0.0:
524+
width = (np.max(x) - np.min(x)) / 2
525+
else:
526+
width = total_area / y_range
527+
return com, width
528+
529+
517530
class TopHat(Fit):
518531
"""Top Hat Fitting."""
519532

@@ -541,27 +554,31 @@ def guess(
541554
def guess(
542555
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
543556
) -> dict[str, lmfit.Parameter]:
544-
com, total_area = _center_of_mass_and_mass(x, y)
545-
width = (np.max(y) - np.min(y)) / total_area
557+
cen, width = _guess_cen_and_width(x, y)
546558

547559
init_guess = {
548-
"cen": lmfit.Parameter("cen", com, min=np.min(x), max=np.max(x)),
549-
"width": lmfit.Parameter("width", width, min=0, max=np.max(x) - np.min(x)),
560+
"cen": lmfit.Parameter("cen", cen),
561+
"width": lmfit.Parameter("width", width, min=0),
550562
"height": lmfit.Parameter(
551563
"height",
552-
(max(y) - min(y)),
553-
min=-(np.max(y) - np.min(y)),
554-
max=np.max(y) - np.min(y),
564+
np.max(y) - np.min(y),
555565
),
556-
"background": lmfit.Parameter("background", min(y), min=np.min(y), max=np.max(y)),
557-
"method": "basinhopping",
566+
"background": lmfit.Parameter("background", np.min(y)),
558567
}
559568

560569
return init_guess
561570

562571
return guess
563572

564573

574+
def _guess_trapezoid_gradient(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]) -> float:
575+
gradients = np.zeros_like(x[1:], dtype=np.float64)
576+
x_diffs = x[:-1] - x[1:]
577+
y_diffs = y[:-1] - y[1:]
578+
np.divide(y_diffs, x_diffs, out=gradients, where=x_diffs != 0)
579+
return np.max(np.abs(gradients))
580+
581+
565582
class Trapezoid(Fit):
566583
"""Trapezoid Fitting."""
567584

@@ -597,26 +614,15 @@ def guess(
597614
def guess(
598615
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
599616
) -> dict[str, lmfit.Parameter]:
600-
com, total_mass = _center_of_mass_and_mass(x, y)
601-
y_range = np.max(y) - np.min(y)
602-
if y_range != 0:
603-
width_guess = total_mass / y_range
604-
else:
605-
width_guess = (np.max(x) - np.min(x)) / 2
606-
607-
# Guess that the gradient is the maximum gradient between any two points
608-
gradients = np.zeros_like(x[1:], dtype=np.float64)
609-
x_diffs = x[:-1] - x[1:]
610-
y_diffs = y[:-1] - y[1:]
611-
np.divide(y_diffs, x_diffs, out=gradients, where=x_diffs != 0)
612-
gradient_guess = np.max(np.abs(gradients))
617+
cen, width = _guess_cen_and_width(x, y)
618+
gradient_guess = _guess_trapezoid_gradient(x, y)
613619

614620
height = np.max(y) - np.min(y)
615621
background = np.min(y)
616-
y_offset = gradient_guess * width_guess / 2.0
622+
y_offset = gradient_guess * width / 2.0
617623

618624
init_guess = {
619-
"cen": lmfit.Parameter("cen", com, min=np.min(x), max=np.max(x)),
625+
"cen": lmfit.Parameter("cen", cen, min=np.min(x), max=np.max(x)),
620626
"gradient": lmfit.Parameter("gradient", gradient_guess, min=0),
621627
"height": lmfit.Parameter("height", height, min=0),
622628
"background": lmfit.Parameter("background", background),
@@ -663,22 +669,15 @@ def guess(
663669
def guess(
664670
x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]
665671
) -> dict[str, lmfit.Parameter]:
666-
com, total_mass = _center_of_mass_and_mass(x, -y)
667-
width_guess = total_mass / (np.max(y) - np.min(y))
668-
669-
# Guess that the gradient is the maximum gradient between any two points
670-
gradients = np.zeros_like(x[1:], dtype=np.float64)
671-
x_diffs = x[:-1] - x[1:]
672-
y_diffs = y[:-1] - y[1:]
673-
np.divide(y_diffs, x_diffs, out=gradients, where=x_diffs != 0)
674-
gradient_guess = np.max(np.abs(gradients))
672+
cen, width = _guess_cen_and_width(x, -y)
673+
gradient_guess = _guess_trapezoid_gradient(x, y)
675674

676675
height = np.max(y) - np.min(y)
677676
background = np.max(y)
678-
y_offset = -gradient_guess * width_guess / 2.0
677+
y_offset = -gradient_guess * width / 2.0
679678

680679
init_guess = {
681-
"cen": lmfit.Parameter("cen", com, min=np.min(x), max=np.max(x)),
680+
"cen": lmfit.Parameter("cen", cen, min=np.min(x), max=np.max(x)),
682681
"gradient": lmfit.Parameter("gradient", gradient_guess, min=0),
683682
"height": lmfit.Parameter("height", height, min=0),
684683
"background": lmfit.Parameter("background", background),

tests/callbacks/fitting/test_fitting_methods.py

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
Gaussian,
1818
Linear,
1919
Lorentzian,
20+
NegativeTrapezoid,
2021
Polynomial,
2122
SlitScan,
2223
TopHat,
@@ -599,7 +600,7 @@ def test_width_guess(self):
599600

600601
outp = TopHat.guess()(x, y)
601602

602-
assert outp["width"] == pytest.approx(1.0, rel=1e-2)
603+
assert outp["width"] == pytest.approx(2.0, rel=1e-2)
603604

604605
def test_guess_given_flat_data(self):
605606
x = np.arange(-5.0, 5.0, 1.0, dtype=np.float64)
@@ -688,15 +689,15 @@ def test_gradient_guess(self):
688689

689690
outp = Trapezoid.guess()(x, y)
690691

691-
assert outp["gradient"] == pytest.approx(10.0, rel=1e-2)
692+
assert outp["gradient"] == pytest.approx(8.0, rel=1e-2)
692693

693694
def test_y_offset_guess(self):
694-
x = np.arange(-5.0, 5.0, 1.0, dtype=np.float64)
695+
x = np.linspace(-5.0, 5.0, num=11, dtype=np.float64)
695696
y = np.array([1.0, 1.0, 1.0, 2.0, 3.0, 3.0, 3.0, 2.0, 1.0, 1.0, 1.0], dtype=np.float64)
696697

697698
outp = Trapezoid.guess()(x, y)
698699

699-
x1 = np.arange(-6.0, 6.0, 1.0, dtype=np.float64)
700+
x1 = np.linspace(-6.0, 6.0, num=13, dtype=np.float64)
700701
y1 = np.array(
701702
[1.0, 1.0, 1.0, 2.0, 3.0, 3.0, 3.0, 3.0, 3.0, 2.0, 1.0, 1.0, 1.0], dtype=np.float64
702703
)
@@ -713,3 +714,112 @@ def test_guess_given_flat_data(self):
713714
outp = Trapezoid.guess()(x, y)
714715
# check that with flat data gradient guess is 0
715716
assert outp["gradient"] == pytest.approx(0.0, rel=1e-2)
717+
718+
719+
class TestNegativeTrapezoid:
720+
class TestNegativeTrapezoidModel:
721+
def test_negative_trapezoid_model(self):
722+
x = np.arange(-5.0, 6.0, 1.0, dtype=np.float64)
723+
cen = 0
724+
y_offset = -1
725+
height = 1
726+
background = 1
727+
gradient = 1
728+
729+
outp = NegativeTrapezoid.model().func(
730+
x,
731+
cen=cen,
732+
y_offset=y_offset,
733+
height=height,
734+
background=background,
735+
gradient=gradient,
736+
)
737+
738+
assert background == pytest.approx(np.max(outp), rel=1e-2)
739+
assert background - height == pytest.approx(np.min(outp), rel=1e-2)
740+
741+
outp1 = NegativeTrapezoid.model().func(
742+
x,
743+
cen=cen + 3,
744+
y_offset=y_offset,
745+
height=height,
746+
background=background,
747+
gradient=gradient - 0.5,
748+
)
749+
750+
# check centre moves when data is shifted
751+
assert np.mean(x[np.where(outp < background)]) < np.mean(
752+
x[np.where(outp1 < background)]
753+
)
754+
755+
# check gradient: a greater gradient means smaller average y values as wider
756+
assert np.mean(outp) > np.mean(outp1)
757+
758+
outp2 = NegativeTrapezoid.model().func(
759+
x,
760+
cen=cen,
761+
y_offset=y_offset - 5,
762+
height=height,
763+
background=background,
764+
gradient=gradient,
765+
)
766+
767+
# check y_offset: a smaller y_offset means smaller average y values as wider
768+
assert np.mean(outp) > np.mean(outp2)
769+
770+
class TestNegativeTrapezoidGuess:
771+
def test_background_guess(self):
772+
x = np.array([-1.0, 0.0, 1.0, 2.0, 3.0], dtype=np.float64)
773+
y = np.array([-1.0, -2.0, -2.0, -2.0, -1.0], dtype=np.float64)
774+
775+
outp = NegativeTrapezoid.guess()(x, y)
776+
777+
assert outp["background"] == pytest.approx(-1.0, rel=1e-2)
778+
779+
def test_cen_height_guess(self):
780+
x = np.array([-1.0, 0.0, 1.0, 2.0, 3.0], dtype=np.float64)
781+
y = np.array([-1.0, -1.0, -2.0, -1.0, -1.0], dtype=np.float64)
782+
783+
outp = NegativeTrapezoid.guess()(x, y)
784+
785+
assert outp["cen"] == pytest.approx(1.0, rel=1e-2)
786+
assert outp["height"] == pytest.approx(1.0, rel=1e-2)
787+
788+
def test_gradient_guess(self):
789+
x = np.array([-4.0, -3.0, -2.0, -1.0, 0.0, 1.0, 2.0, 3.0, 4.0], dtype=np.float64)
790+
y = np.array([1.0, 2.0, 4.0, 8.0, 16.0, 8.0, 4.0, 2.0, 1.0], dtype=np.float64)
791+
792+
# Should choose x = -1.0 as x1 and x = -2.5 as x0
793+
# height = 16 - 1 = 15
794+
# gradient = 15 / (-1 - -2.5) = 10
795+
796+
outp = NegativeTrapezoid.guess()(x, y)
797+
798+
assert outp["gradient"] == pytest.approx(8.0, rel=1e-2)
799+
800+
def test_y_offset_guess(self):
801+
x = np.linspace(-5.0, 5.0, num=11, dtype=np.float64)
802+
y = np.array(
803+
[-1.0, -1.0, -1.0, -2.0, -3.0, -3.0, -3.0, -2.0, -1.0, -1.0, -1.0], dtype=np.float64
804+
)
805+
806+
outp = NegativeTrapezoid.guess()(x, y)
807+
808+
x1 = np.linspace(-6.0, 6.0, num=13, dtype=np.float64)
809+
y1 = np.array(
810+
[-1.0, -1.0, -1.0, -2.0, -3.0, -3.0, -3.0, -3.0, -3.0, -2.0, -1.0, -1.0, -1.0],
811+
dtype=np.float64,
812+
)
813+
814+
outp1 = NegativeTrapezoid.guess()(x1, y1)
815+
816+
# Assert that with a greater top width, y_offset decreases
817+
assert outp["y_offset"] > outp1["y_offset"]
818+
819+
def test_guess_given_flat_data(self):
820+
x = np.arange(-5.0, 5.0, 1.0, dtype=np.float64)
821+
y = np.zeros_like(x, dtype=np.float64) + 1
822+
823+
outp = NegativeTrapezoid.guess()(x, y)
824+
# check that with flat data gradient guess is 0
825+
assert outp["gradient"] == pytest.approx(0.0, rel=1e-2)

0 commit comments

Comments
 (0)