Skip to content

Commit 18e644a

Browse files
authored
Merge branch 'develop' into henryiii/ci/addft
2 parents 8652838 + 483077c commit 18e644a

File tree

4 files changed

+79
-60
lines changed

4 files changed

+79
-60
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
python-version: "pypy-3.9"
3434
- os: ubuntu-latest
3535
python-version: "3.13"
36-
installs: "'numpy>=2' scipy matplotlib"
36+
installs: "'numpy>=2' scipy~=1.15.0 matplotlib"
3737
fail-fast: false
3838
steps:
3939
- uses: actions/checkout@v4

.pre-commit-config.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
repos:
1616
# Standard hooks
1717
- repo: https://github.com/pre-commit/pre-commit-hooks
18-
rev: v5.0.0
18+
rev: v6.0.0
1919
hooks:
2020
- id: check-case-conflict
2121
- id: check-docstring-first
@@ -34,15 +34,15 @@ repos:
3434

3535
# Ruff linter and formatter
3636
- repo: https://github.com/astral-sh/ruff-pre-commit
37-
rev: 'v0.12.0'
37+
rev: 'v0.12.8'
3838
hooks:
3939
- id: ruff
4040
args: [--fix, --show-fixes]
4141
- id: ruff-format
4242

4343
# C++ formatting
4444
- repo: https://github.com/pre-commit/mirrors-clang-format
45-
rev: v20.1.6
45+
rev: v20.1.8
4646
hooks:
4747
- id: clang-format
4848
files: "src"
@@ -58,7 +58,7 @@ repos:
5858

5959
# Python type checking
6060
- repo: https://github.com/pre-commit/mirrors-mypy
61-
rev: 'v1.16.1'
61+
rev: 'v1.17.1'
6262
hooks:
6363
- id: mypy
6464
additional_dependencies: [numpy]
@@ -73,11 +73,11 @@ repos:
7373
args: [--drop-empty-cells]
7474

7575
- repo: https://github.com/python-jsonschema/check-jsonschema
76-
rev: 0.33.1
76+
rev: 0.33.2
7777
hooks:
7878
- id: check-github-workflows
7979

8080
- repo: https://github.com/henryiii/validate-pyproject-schema-store
81-
rev: 2025.06.13
81+
rev: 2025.08.07
8282
hooks:
8383
- id: validate-pyproject

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ test = [
4848
"joblib",
4949
"jacobi",
5050
"matplotlib",
51-
"numpy",
5251
"numba; platform_python_implementation=='CPython'",
5352
"numba-stats; platform_python_implementation=='CPython'",
5453
"pytest",
@@ -62,6 +61,7 @@ test = [
6261
"unicodeitplus",
6362
"pydantic",
6463
"annotated_types",
64+
"nox"
6565
]
6666
doc = [
6767
"sphinx-rtd-theme", # installs correct sphinx as well

tests/test_cost.py

Lines changed: 71 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,39 @@ def norm_cdf(x, mu, sigma):
3939
return (1 + np.vectorize(erf)(z)) * 0.5
4040

4141

42-
def mvnorm(mux, muy, sx, sy, rho):
42+
def mvnorm(mux, muy, sx, sy):
4343
stats = pytest.importorskip("scipy.stats")
44-
C = np.empty((2, 2))
45-
C[0, 0] = sx**2
46-
C[0, 1] = C[1, 0] = sx * sy * rho
47-
C[1, 1] = sy**2
48-
m = [mux, muy]
49-
return stats.multivariate_normal(m, C)
44+
45+
# This used to be stats.multivariate_normal, but it's cdf
46+
# is computed with a different algorithm since scipy 1.16.0
47+
# and that broke our tests. There is no closed-form solution for
48+
# the cdf of a bivariate normal distribution, so it's not a good
49+
# test function anyway.
50+
# Issue: https://github.com/scipy/scipy/issues/23469
51+
# Potentially related: https://github.com/scipy/scipy/issues/23413
52+
class mvnorm:
53+
@staticmethod
54+
def cdf(x_y):
55+
x, y = x_y.T
56+
return stats.norm(mux, sx).cdf(x) * stats.norm(muy, sy).cdf(y)
57+
58+
@staticmethod
59+
def pdf(x_y):
60+
x, y = x_y.T
61+
return stats.norm(mux, sx).pdf(x) * stats.norm(muy, sy).pdf(y)
62+
63+
@staticmethod
64+
def rvs(size=None, random_state=None):
65+
if random_state is None:
66+
random_state = np.random.default_rng()
67+
return np.transpose(
68+
[
69+
stats.norm(mux, sx).rvs(size=size, random_state=random_state),
70+
stats.norm(muy, sy).rvs(size=size, random_state=random_state),
71+
]
72+
)
73+
74+
return mvnorm
5075

5176

5277
def expon_cdf(x, a):
@@ -289,16 +314,15 @@ def test_UnbinnedNLL_name_bad():
289314

290315
@pytest.mark.parametrize("use_grad", (False, True))
291316
def test_UnbinnedNLL_2D(use_grad):
292-
def model(x_y, mux, muy, sx, sy, rho):
293-
return mvnorm(mux, muy, sx, sy, rho).pdf(x_y.T)
317+
def model(x_y, mux, muy, sx, sy):
318+
return mvnorm(mux, muy, sx, sy).pdf(x_y.T)
294319

295-
truth = 0.1, 0.2, 0.3, 0.4, 0.5
320+
truth = 0.1, 0.2, 0.3, 0.4
296321
x, y = mvnorm(*truth).rvs(size=100, random_state=1).T
297322

298323
cost = UnbinnedNLL((x, y), model, grad=numerical_model_gradient(model))
299324
m = Minuit(cost, *truth, grad=use_grad)
300325
m.limits["sx", "sy"] = (0, None)
301-
m.limits["rho"] = (-1, 1)
302326
m.migrad()
303327
assert m.valid
304328

@@ -384,10 +408,10 @@ def test_UnbinnedNLL_visualize(log):
384408
def test_UnbinnedNLL_visualize_2D():
385409
pytest.importorskip("matplotlib")
386410

387-
def model(x_y, mux, muy, sx, sy, rho):
388-
return mvnorm(mux, muy, sx, sy, rho).pdf(x_y.T)
411+
def model(x_y, mux, muy, sx, sy):
412+
return mvnorm(mux, muy, sx, sy).pdf(x_y.T)
389413

390-
truth = 0.1, 0.2, 0.3, 0.4, 0.5
414+
truth = 0.1, 0.2, 0.3, 0.4
391415
x, y = mvnorm(*truth).rvs(size=10, random_state=1).T
392416

393417
c = UnbinnedNLL((x, y), model)
@@ -466,10 +490,10 @@ def test_ExtendedUnbinnedNLL_name(unbinned):
466490

467491
@pytest.mark.parametrize("use_grad", (False, True))
468492
def test_ExtendedUnbinnedNLL_2D(use_grad):
469-
def model(x_y, n, mux, muy, sx, sy, rho):
470-
return n, n * mvnorm(mux, muy, sx, sy, rho).pdf(x_y.T)
493+
def model(x_y, n, mux, muy, sx, sy):
494+
return n, n * mvnorm(mux, muy, sx, sy).pdf(x_y.T)
471495

472-
truth = 100.0, 0.1, 0.2, 0.3, 0.4, 0.5
496+
truth = 100.0, 0.1, 0.2, 0.3, 0.4
473497
x, y = mvnorm(*truth[1:]).rvs(size=int(truth[0]), random_state=1).T
474498

475499
cost = ExtendedUnbinnedNLL(
@@ -478,7 +502,6 @@ def model(x_y, n, mux, muy, sx, sy, rho):
478502

479503
m = Minuit(cost, *truth, grad=use_grad)
480504
m.limits["n", "sx", "sy"] = (0, None)
481-
m.limits["rho"] = (-1, 1)
482505
m.migrad()
483506
assert m.valid
484507

@@ -560,10 +583,10 @@ def model(x, s, mu, sigma):
560583
def test_ExtendedUnbinnedNLL_visualize_2D():
561584
pytest.importorskip("matplotlib")
562585

563-
def model(x_y, n, mux, muy, sx, sy, rho):
564-
return n * 100, n * 100 * mvnorm(mux, muy, sx, sy, rho).pdf(x_y.T)
586+
def model(x_y, n, mux, muy, sx, sy):
587+
return n * 100, n * 100 * mvnorm(mux, muy, sx, sy).pdf(x_y.T)
565588

566-
truth = 1.0, 0.1, 0.2, 0.3, 0.4, 0.5
589+
truth = 1.0, 0.1, 0.2, 0.3, 0.4
567590
x, y = mvnorm(*truth[1:]).rvs(size=int(truth[0] * 100)).T
568591

569592
c = ExtendedUnbinnedNLL((x, y), model)
@@ -748,13 +771,13 @@ def test_BinnedNLL_ndof_zero():
748771

749772
@pytest.mark.parametrize("use_grad", (False, True))
750773
def test_BinnedNLL_2D(use_grad):
751-
truth = (0.1, 0.2, 0.3, 0.4, 0.5)
774+
truth = (0.1, 0.2, 0.3, 0.4)
752775
x, y = mvnorm(*truth).rvs(size=1000, random_state=1).T
753776

754777
w, xe, ye = np.histogram2d(x, y, bins=(20, 50))
755778

756-
def model(xy, mux, muy, sx, sy, rho):
757-
return mvnorm(mux, muy, sx, sy, rho).cdf(xy.T)
779+
def model(xy, mux, muy, sx, sy):
780+
return mvnorm(mux, muy, sx, sy).cdf(xy.T)
758781

759782
cost = BinnedNLL(w, (xe, ye), model, grad=numerical_model_gradient(model))
760783
assert cost.ndata == np.prod(w.shape)
@@ -765,7 +788,6 @@ def model(xy, mux, muy, sx, sy, rho):
765788

766789
m = Minuit(cost, *truth, grad=use_grad)
767790
m.limits["sx", "sy"] = (0, None)
768-
m.limits["rho"] = (-1, 1)
769791
m.migrad()
770792
assert m.valid
771793
assert_allclose(m.values, truth, atol=0.05)
@@ -783,19 +805,18 @@ def model(xy, mux, muy, sx, sy, rho):
783805

784806

785807
def test_BinnedNLL_2D_with_zero_bins():
786-
truth = (0.1, 0.2, 0.3, 0.4, 0.5)
808+
truth = (0.1, 0.2, 0.3, 0.4)
787809
x, y = mvnorm(*truth).rvs(size=1000, random_state=1).T
788810

789811
w, xe, ye = np.histogram2d(x, y, bins=(50, 100), range=((-5, 5), (-5, 5)))
790812
assert np.mean(w == 0) > 0.25
791813

792-
def model(xy, mux, muy, sx, sy, rho):
793-
return mvnorm(mux, muy, sx, sy, rho).cdf(xy.T)
814+
def model(xy, mux, muy, sx, sy):
815+
return mvnorm(mux, muy, sx, sy).cdf(xy.T)
794816

795817
cost = BinnedNLL(w, (xe, ye), model)
796818
m = Minuit(cost, *truth)
797819
m.limits["sx", "sy"] = (0, None)
798-
m.limits["rho"] = (-1, 1)
799820
m.migrad()
800821
assert m.valid
801822
assert_allclose(m.values, truth, atol=0.05)
@@ -849,12 +870,12 @@ def test_BinnedNLL_visualize():
849870
def test_BinnedNLL_visualize_2D():
850871
pytest.importorskip("matplotlib")
851872

852-
truth = (0.1, 0.2, 0.3, 0.4, 0.5)
873+
truth = (0.1, 0.2, 0.3, 0.4)
853874
x, y = mvnorm(*truth).rvs(size=10, random_state=1).T
854875
w, xe, ye = np.histogram2d(x, y, bins=(50, 100), range=((-5, 5), (-5, 5)))
855876

856-
def model(xy, mux, muy, sx, sy, rho):
857-
return mvnorm(mux, muy, sx, sy, rho).cdf(xy.T)
877+
def model(xy, mux, muy, sx, sy):
878+
return mvnorm(mux, muy, sx, sy).cdf(xy.T)
858879

859880
c = BinnedNLL(w, (xe, ye), model)
860881

@@ -1025,13 +1046,13 @@ def test_ExtendedBinnedNLL_bad_input():
10251046

10261047
@pytest.mark.parametrize("use_grad", (False, True))
10271048
def test_ExtendedBinnedNLL_2D(use_grad):
1028-
truth = (1.0, 0.1, 0.2, 0.3, 0.4, 0.5)
1049+
truth = (1.0, 0.1, 0.2, 0.3, 0.4)
10291050
x, y = mvnorm(*truth[1:]).rvs(size=int(truth[0] * 100), random_state=1).T
10301051

10311052
w, xe, ye = np.histogram2d(x, y, bins=(10, 20))
10321053

1033-
def model(xy, n, mux, muy, sx, sy, rho):
1034-
return n * 100 * mvnorm(mux, muy, sx, sy, rho).cdf(np.transpose(xy))
1054+
def model(xy, n, mux, muy, sx, sy):
1055+
return n * 100 * mvnorm(mux, muy, sx, sy).cdf(np.transpose(xy))
10351056

10361057
cost = ExtendedBinnedNLL(w, (xe, ye), model, grad=numerical_model_gradient(model))
10371058
assert cost.ndata == np.prod(w.shape)
@@ -1042,7 +1063,6 @@ def model(xy, n, mux, muy, sx, sy, rho):
10421063

10431064
m = Minuit(cost, *truth, grad=use_grad)
10441065
m.limits["n", "sx", "sy"] = (0, None)
1045-
m.limits["rho"] = (-1, 1)
10461066
m.migrad()
10471067
assert m.valid
10481068
assert_allclose(m.values, truth, atol=0.1)
@@ -1056,27 +1076,26 @@ def model(xy, n, mux, muy, sx, sy, rho):
10561076
def test_ExtendedBinnedNLL_3D():
10571077
norm = pytest.importorskip("scipy.stats").norm
10581078

1059-
truth = (1.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7)
1079+
truth = (1.0, 0.1, 0.2, 0.3, 0.4, 0.6, 0.7)
10601080
n = int(truth[0] * 10000)
10611081
x, y = mvnorm(*truth[1:-2]).rvs(size=n).T
10621082
z = norm(truth[-2], truth[-1]).rvs(size=n)
10631083

10641084
w, edges = np.histogramdd((x, y, z), bins=(5, 10, 20))
10651085

1066-
def model(xyz, n, mux, muy, sx, sy, rho, muz, sz):
1086+
def model(xyz, n, mux, muy, sx, sy, muz, sz):
10671087
*xy, z = xyz
10681088
return (
10691089
n
10701090
* 10000
1071-
* mvnorm(mux, muy, sx, sy, rho).cdf(np.transpose(xy))
1091+
* mvnorm(mux, muy, sx, sy).cdf(np.transpose(xy))
10721092
* norm(muz, sz).cdf(z)
10731093
)
10741094

10751095
cost = ExtendedBinnedNLL(w, edges, model)
10761096
assert cost.ndata == np.prod(w.shape)
10771097
m = Minuit(cost, *truth)
10781098
m.limits["n", "sx", "sy", "sz"] = (0, None)
1079-
m.limits["rho"] = (-1, 1)
10801099
m.migrad()
10811100
assert m.valid
10821101
assert_allclose(m.values, truth, atol=0.05)
@@ -1118,13 +1137,13 @@ def model(x, s, slope):
11181137
def test_ExtendedBinnedNLL_visualize_2D():
11191138
pytest.importorskip("matplotlib")
11201139

1121-
truth = (1.0, 0.1, 0.2, 0.3, 0.4, 0.5)
1140+
truth = (1.0, 0.1, 0.2, 0.3, 0.4)
11221141
x, y = mvnorm(*truth[1:]).rvs(size=int(truth[0] * 1000), random_state=1).T
11231142

11241143
w, xe, ye = np.histogram2d(x, y, bins=(10, 20))
11251144

1126-
def model(xy, n, mux, muy, sx, sy, rho):
1127-
return n * 1000 * mvnorm(mux, muy, sx, sy, rho).cdf(np.transpose(xy))
1145+
def model(xy, n, mux, muy, sx, sy):
1146+
return n * 1000 * mvnorm(mux, muy, sx, sy).cdf(np.transpose(xy))
11281147

11291148
c = ExtendedBinnedNLL(w, (xe, ye), model)
11301149

@@ -2062,26 +2081,26 @@ def test_Template_with_model():
20622081

20632082

20642083
def test_Template_with_model_2D():
2065-
truth1 = (1.0, 0.1, 0.2, 0.3, 0.4, 0.5)
2084+
truth1 = (1.0, 0.1, 0.2, 0.1, 0.4)
20662085
x1, y1 = mvnorm(*truth1[1:]).rvs(size=int(truth1[0] * 1000), random_state=1).T
2067-
truth2 = (1.0, 0.2, 0.1, 0.4, 0.3, 0.0)
2068-
x2, y2 = mvnorm(*truth2[1:]).rvs(size=int(truth2[0] * 1000), random_state=1).T
2086+
truth2 = (1.0, 0.2, 0.1, 0.4, 0.1)
2087+
x2, y2 = mvnorm(*truth2[1:]).rvs(size=int(truth2[0] * 1000), random_state=2).T
20692088

20702089
x = np.append(x1, x2)
20712090
y = np.append(y1, y2)
2072-
w, xe, ye = np.histogram2d(x, y, bins=(3, 5))
2091+
w, xe, ye = np.histogram2d(x, y, bins=(10, 10))
20732092

2074-
def model(xy, n, mux, muy, sx, sy, rho):
2075-
return n * 1000 * mvnorm(mux, muy, sx, sy, rho).cdf(np.transpose(xy))
2093+
def model(xy, n, mux, muy, sx, sy):
2094+
return n * 1000 * mvnorm(mux, muy, sx, sy).cdf(np.transpose(xy))
20762095

2077-
x3, y3 = mvnorm(*truth2[1:]).rvs(size=int(truth2[0] * 10000), random_state=2).T
2096+
x3, y3 = mvnorm(*truth2[1:]).rvs(size=int(truth2[0] * 10000), random_state=3).T
20782097
template = np.histogram2d(x3, y3, bins=(xe, ye))[0]
20792098

20802099
cost = Template(w, (xe, ye), (model, template))
20812100
assert cost.ndata == np.prod(w.shape)
20822101
m = Minuit(cost, *truth1, 1)
2083-
m.limits["x0_n", "x0_sx", "x0_sy"] = (0, None)
2084-
m.limits["x0_rho"] = (-1, 1)
2102+
m.limits["x0_n", "x0_sx", "x0_sy", "x1"] = (0, None)
2103+
m.limits["x0_mux", "x0_muy"] = (xe[0], xe[-1]), (ye[0], ye[-1])
20852104
m.migrad()
20862105
assert m.valid
20872106
assert_allclose(m.values, truth1 + (1e3,), rtol=0.1)

0 commit comments

Comments
 (0)