Merge branch 'main' into feature/cuda_clusterfinder
Build on RHEL8 / build (push) Successful in 3m34s
Build on RHEL9 / build (push) Successful in 3m42s
Run tests using data on local RHEL8 / build (push) Failing after 4m46s

This commit is contained in:
Khalil Ferjaoui authored and GitHub committed 2026-09-02 14:18:37 +02:00
commit 1d963a778b
100 files changed
+4748 -2464

No files matched your search

+10 -7
View File
@@ -103,15 +103,18 @@ def test_max_sum():
def test_cluster_finder():
"""Test ClusterFinder"""
shape = [100,100]
cf = _aare.ClusterFinder_Cluster3x3i(shape)
clusterfinder = _aare.ClusterFinder_Cluster3x3i([100,100])
#Push 1000 frames to the pedestal
for i in range(1000):
frame = np.random.normal(loc = 100, scale = 5, size = shape).astype(np.uint16)
cf.push_pedestal_frame(frame)
cf.update_threshold()
frame = np.zeros(shape=shape, dtype=np.uint16)
cf.find_clusters(frame)
#frame = np.random.rand(100,100)
frame = np.zeros(shape=[100,100])
clusterfinder.find_clusters(frame)
clusters = clusterfinder.steal_clusters(False) #conversion does not work
clusters = cf.steal_clusters(False) #conversion does not work
assert clusters.size == 0
+3 -1
View File
@@ -14,6 +14,8 @@ from conftest import test_data_path
def test_cluster_file(test_data_path):
"""Test ClusterFile"""
f = ClusterFile(test_data_path / "clust/single_frame_97_clustrers.clust")
assert f.estimate_n_clusters() == 97
assert f.tell() == 0
cv = f.read_clusters(10) #conversion does not work
@@ -62,4 +64,4 @@ def test_read_clusters_and_fill_histogram(test_data_path):
hist_py = pickle.load(f)
#Compare the two histograms
assert hist_aare == hist_py
assert hist_aare == hist_py
+3 -1
View File
@@ -16,6 +16,7 @@ def test_create_cluster_vector():
assert cv.cluster_size_x == 3
assert cv.cluster_size_y == 3
assert cv.size == 0
assert cv.empty()
def test_push_back_on_cluster_vector():
@@ -27,6 +28,7 @@ def test_push_back_on_cluster_vector():
cluster = _aare.Cluster2x2i(19, 22, np.ones(4, dtype=np.int32))
cv.push_back(cluster)
assert cv.size == 1
assert not cv.empty()
arr = np.array(cv, copy=False)
assert arr[0]['x'] == 19
@@ -127,4 +129,4 @@ def test_masking():
assert cv_masked_array[0]["x"] == 1
assert cv_masked_array[0]["y"] == 2
assert (cv_masked_array[0]["data"] == np.ones((3,3),dtype=np.int32)).all()
assert (cv_masked_array[0]["data"] == np.ones((3,3),dtype=np.int32)).all()
+145
View File
@@ -0,0 +1,145 @@
import numpy as np
import pytest
from aare import (
FastPedestal,
FastPedestal_d,
FastPedestal_f,
FastPedestal_i16,
)
@pytest.mark.parametrize(
("dtype", "pedestal_type"),
[
(np.float64, FastPedestal_d),
(np.float32, FastPedestal_f),
(np.int16, FastPedestal_i16),
],
)
def test_fast_pedestal_factory(dtype, pedestal_type):
pedestal = FastPedestal(2, 3, n_samples=4, dtype=dtype)
assert isinstance(pedestal, pedestal_type)
assert pedestal.rows == 2
assert pedestal.cols == 3
assert pedestal.n_samples == 4
def test_fast_pedestal_factory_defaults_to_double():
assert isinstance(FastPedestal(2, 3), FastPedestal_d)
def test_fast_pedestal_factory_rejects_unbound_dtype():
with pytest.raises(ValueError, match="Unsupported dtype for FastPedestal"):
FastPedestal(2, 3, dtype=np.int32)
@pytest.mark.parametrize(
("kwargs", "expected_n_samples"),
[
({"rows": 2, "cols": 3}, 1000),
({"rows": 2, "cols": 3, "n_samples": 4}, 4),
],
)
def test_fast_pedestal_binding_accepts_constructor_keywords(
kwargs, expected_n_samples
):
pedestal = FastPedestal_d(**kwargs)
assert pedestal.rows == 2
assert pedestal.cols == 3
assert pedestal.n_samples == expected_n_samples
@pytest.mark.parametrize(
("dtype", "pedestal_type", "expected_dtype"),
[
(np.float64, FastPedestal_d, np.float64),
(np.float32, FastPedestal_f, np.float32),
(np.int16, FastPedestal_i16, np.int16),
],
)
def test_fast_pedestal_factory_from_file(
tmp_path, dtype, pedestal_type, expected_dtype
):
frames = np.array(
[[[100, 100]], [[2, 4]], [[4, 6]], [[5, 7]]], dtype=np.uint16
)
filename = tmp_path / "frames.npy"
np.save(filename, frames)
pedestal = FastPedestal.from_file(
filename, n_samples=2, skip_first=1, dtype=dtype
)
assert isinstance(pedestal, pedestal_type)
assert pedestal.ready
assert pedestal.cur_samples == 2
assert pedestal.mean().dtype == expected_dtype
np.testing.assert_array_equal(pedestal.mean(), [[4, 6]])
def test_fast_pedestal_factory_from_file_rejects_unbound_dtype():
with pytest.raises(ValueError, match="Unsupported dtype for FastPedestal"):
FastPedestal.from_file("unused.npy", dtype=np.int32)
def test_fast_pedestal_from_file_rejects_skip_beyond_end(tmp_path):
filename = tmp_path / "frames.npy"
np.save(filename, np.zeros((1, 1, 1), dtype=np.uint16))
with pytest.raises(RuntimeError, match="less frames"):
FastPedestal.from_file(filename, n_samples=1, skip_first=2)
@pytest.mark.parametrize(
("pedestal_type", "expected_dtype"),
[(FastPedestal_d, np.float64), (FastPedestal_f, np.float32)],
)
def test_fast_pedestal_initialization(pedestal_type, expected_dtype):
pedestal = pedestal_type(2, 3, 2)
first = np.array([[2, 4, 6], [8, 10, 12]], dtype=np.uint16)
second = np.array([[4, 6, 8], [10, 12, 14]], dtype=np.uint16)
pedestal.add_init_frame(first)
pedestal.add_init_frame(second)
expected_mean = np.array(
[[3, 5, 7], [9, 11, 13]], dtype=expected_dtype
)
np.testing.assert_array_equal(pedestal.mean(), expected_mean)
np.testing.assert_array_equal(pedestal.std(), np.ones((2, 3)))
def test_fast_pedestal_steady_state_push_ema():
pedestal = FastPedestal_d(1, 2, 2)
pedestal.add_init_frame(np.array([[2, 4]], dtype=np.uint16))
pedestal.add_init_frame(np.array([[4, 6]], dtype=np.uint16))
pedestal.push_ema(np.array([[6, 8]], dtype=np.uint16))
np.testing.assert_array_equal(pedestal.mean(), [[4.5, 6.5]])
def test_fast_pedestal_exposes_read_only_buffer_and_subtraction():
pedestal = FastPedestal_d(1, 2, 1)
pedestal.add_init_frame(np.array([[2, 4]], dtype=np.uint16))
view = np.asarray(pedestal)
result = np.array([[12, 14]], dtype=np.uint16) - pedestal
np.testing.assert_array_equal(view, [[2, 4]])
np.testing.assert_array_equal(result, [[10, 10]])
assert np.shares_memory(view, pedestal.view())
assert not view.flags.writeable
def test_fast_pedestal_rejects_wrong_shape():
pedestal = FastPedestal_d(2, 3)
with pytest.raises(RuntimeError, match="shape"):
pedestal.add_init_frame(np.zeros((2, 2), dtype=np.uint16))
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
import aare
def test_gaussian_model_evaluates_and_fits_data():
x = np.linspace(-5.0, 5.0, 51)
expected = np.array([20.0, 0.5, 1.2])
model = aare.Gaussian()
y = model(x, expected)
result = model.fit(x, y)
np.testing.assert_allclose(result["par"], expected, atol=2e-3)
+408 -483
View File
@@ -1,488 +1,413 @@
{
"cells":
[
{
"cell_type": "code",
"execution_count": 2,
"id": "efef8e20-6571-4561-8f0b-6048b57907de",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import random\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.gridspec import GridSpec\n",
"import sys\n",
"sys.path.insert(0, '/home/ferjao_k/aare/build')\n",
"from aare import fit_gaus # lmfit\n",
"from aare import Gaussian, fit # minuit2"
]
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "efef8e20-6571-4561-8f0b-6048b57907de",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import random\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.gridspec import GridSpec\n",
"from aare import Gaussian\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "35772e2c-37c6-4986-a9c0-7dca4a034827",
"metadata": {},
"outputs": [],
"source": [
"ROWS = 100\n",
"COLS = 100\n",
"N_SCAN = 100\n",
"NOISE_FRAC = 0.05\n",
"SEED = 42\n",
"N_THREADS = 4\n",
"\n",
"N_REPEATS = 7\n",
"N_WARMUP = 3 # untimed iterations (icache + branch predictor warmup)\n",
"COOLDOWN = 2.0 # seconds between (method, thread_count) pairs"
]
},
{
"cell_type": "markdown",
"id": "be455445-7df8-47cc-9f39-d2a38856e0bd",
"metadata": {},
"source": [
"## Data generator"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "06c8fddb-56d9-4f84-8b21-4ffd8b7bd26f",
"metadata": {},
"outputs": [],
"source": [
"def generate_3d_data(rows, cols, n_scan, noise_frac, seed):\n",
" \"\"\"\n",
" Generate a synthetic detector image stack where each pixel has a\n",
" Gaussian response curve with per-pixel variation in A, mu, sigma.\n",
"\n",
" Returns x (n_scan,), y (rows, cols, n_scan), y_err (rows, cols, n_scan),\n",
" and the ground-truth parameter arrays.\n",
" \"\"\"\n",
" rng = np.random.default_rng(seed)\n",
"\n",
" # Per-pixel true params each of shape: [rows, cols, 1]\n",
" A_true = rng.uniform(200, 1000, size=(rows, cols))\n",
" mu_true = rng.uniform(20, 80, size=(rows, cols))\n",
" sig_true = rng.uniform(3, 12, size=(rows, cols))\n",
" \n",
" # One common binned energy array\n",
" x = np.linspace(0, 100, n_scan) # shape [1, 1, nscan]\n",
"\n",
" # Build ground truth signals per-pixel\n",
" exponent = -0.5 * ((x[None, None, :] - mu_true[:, :, None]) / sig_true[:,:, None])**2 # shape [rows, cols, nscan]\n",
" y_clean = A_true[:, :, None] * np.exp(exponent)\n",
"\n",
" # Perturb with noise\n",
" noise_sigma = noise_frac * A_true[:, :, None] * np.ones_like(y_clean) # shape [rows, cols, nscan]\n",
" noise = rng.normal(0, noise_sigma)\n",
" y = y_clean + noise\n",
"\n",
" y_err = noise_sigma.copy()\n",
"\n",
" return x, y, y_err, A_true, mu_true, sig_true "
]
},
{
"cell_type": "markdown",
"id": "30ed6bb8-4798-497f-9990-4c518f885855",
"metadata": {},
"source": [
"## Profiling function"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dc0b52b6-9fc3-453d-b6eb-e325a2b8342e",
"metadata": {},
"outputs": [],
"source": [
"def bench(fn, n_warmup=N_WARMUP, n_repeats=N_REPEATS):\n",
" \"\"\"\n",
" Warmup then time `fn` over `n_repeats` calls.\n",
" Returns (last_result, list_of_walltimes_in_seconds).\n",
" \"\"\"\n",
" # warmup: primes icache, branch predictor, and lets CPU ramp to boost clock\n",
" for _ in range(n_warmup):\n",
" res = fn()\n",
"\n",
" times = []\n",
" for _ in range(n_repeats):\n",
" t0 = time.perf_counter()\n",
" res = fn()\n",
" t1 = time.perf_counter()\n",
" times.append(t1 - t0)\n",
" return res, times"
]
},
{
"cell_type": "markdown",
"id": "377fc820-95b2-48aa-b104-a272c50e4103",
"metadata": {},
"source": [
"# Quick check on small (2x2) frame"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9cc999b9-e0d8-4deb-ae29-e8bd0141534b",
"metadata": {},
"outputs": [],
"source": [
"# Generate 2 x 2 dataset of Gaussian-like profiles for each pixel\n",
"x2, y2, yerr2, true_A2, true_mu2, true_sig2 = generate_3d_data(\n",
" 2, 2, N_SCAN, NOISE_FRAC, SEED\n",
")\n",
"model_g = Gaussian()\n",
"model_g.compute_errors = True\n",
"result = model_g.fit(x2, y2, yerr2)\n",
"\n",
"from pprint import pprint\n",
"print(\"== True Gaussian params == \")\n",
"print(\"A_true = \\n\", true_A2)\n",
"print(\"mu_true = \\n\", true_mu2)\n",
"print(\"sig_true = \\n\",true_sig2)\n",
"print(\"\\n\")\n",
"\n",
"print(\"== Fit results ==\")\n",
"par = result['par']\n",
"# print(par)\n",
"A_fit = par[:, :, 0]\n",
"mu_fit = par[:, :, 1]\n",
"sig_fit = par[:, :, 2]\n",
"print(\"A_fit = \\n\", A_fit)\n",
"print(\"mu_fit = \\n\", mu_fit)\n",
"print(\"sig_fit = \\n\", sig_fit)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a98a41f3-fd2e-4bfc-9ec6-0d23dd38e896",
"metadata": {},
"outputs": [],
"source": [
"fig, ax = plt.subplots(2, 2, figsize=(12,8))\n",
"\n",
"# Gaussians in 2x2 frame: True vs Fit\n",
"for row in range(2):\n",
" for col in range(2):\n",
" ax[row, col].plot(x2, y2[row, col,:], label=\"data\")\n",
" ax[row, col].plot(x2, model_g(x2, result['par'][row, col,:]), linewidth=1, color=\"green\", label=\"minuit\")\n",
" ax[row, col].set_title(f\"Gaussian Fit to data in pixel [{row}, {col}]\")\n",
" ax[row, col].legend()"
]
},
{
"cell_type": "markdown",
"id": "fcf52481-0278-4f95-8676-829a6d61eff8",
"metadata": {},
"source": [
"## Fit data with different backends"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1f6bc651-80c1-41dd-8a05-7f15aba006aa",
"metadata": {},
"outputs": [],
"source": [
"# ===============\n",
"# DATA GENERATION\n",
"# ===============\n",
"print(f\"Generating synthetic data: {ROWS}x{COLS} pixels, \"\n",
" f\"{N_SCAN} scan points, noise_frac={NOISE_FRAC}\\n\")\n",
"\n",
"x, y, yerr, true_A, true_mu, true_sig = generate_3d_data(\n",
" ROWS, COLS, N_SCAN, NOISE_FRAC, SEED\n",
")\n",
"\n",
"model = Gaussian()\n",
"print(f\"model.max_calls = {model.max_calls}\")\n",
"print(f\"model.tolerance = {model.tolerance}\")\n",
"print(\"model.compute_errors =\", model.compute_errors)\n",
"METHOD_DEFS = [\n",
" (\"Minuit2 (obj API)\",\n",
" lambda nt: lambda: model.fit(x, y, n_threads=nt),\n",
" \"#FF9800\", {\"linewidth\": 2.5, \"linestyle\": \":\"}),\n",
"]\n",
"\n",
"colors = {label: c for label, _, c, _ in METHOD_DEFS}\n",
"styles = {label: s for label, _, _, s in METHOD_DEFS}"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a417145-ce42-4c3a-a7bf-05ff97ba0450",
"metadata": {},
"outputs": [],
"source": [
"# ====================================\n",
"# SINGLE-CALL BENCHMARK (at N_THREADS)\n",
"# ====================================\n",
"def extract_result(label, res):\n",
" \"\"\"Normalize return values across fitters into a common dict.\"\"\"\n",
" if isinstance(res, dict):\n",
" out = {\"par\": res[\"par\"]}\n",
" if \"par_err\" in res:\n",
" out[\"par_err\"] = res[\"par_err\"]\n",
" if \"chi2\" in res:\n",
" out[\"chi2\"] = res[\"chi2\"]\n",
" return out\n",
" \n",
"methods = {}\n",
"for label, factory, _, _ in METHOD_DEFS:\n",
" time.sleep(COOLDOWN)\n",
" res, times = bench(factory(N_THREADS))\n",
" entry = extract_result(label, res)\n",
" entry[\"times\"] = times\n",
" methods[label] = entry\n",
"\n",
"# ---- Print summary ----\n",
"ndf = N_SCAN - 3\n",
"print(f\"{'Method':24s} {'time (ms)':>10s} {'med|dA|':>10s} {'med|dMu|':>10s} {'med|dSig|':>10s}\")\n",
"print(\"-\" * 80)\n",
"for name, m in methods.items():\n",
" par = m[\"par\"]\n",
" med_t = np.median(m[\"times\"]) * 1e3\n",
" dA = np.median(np.abs(par[:,:,0] - true_A))\n",
" dMu = np.median(np.abs(par[:,:,1] - true_mu))\n",
" dSig = np.median(np.abs(par[:,:,2] - true_sig))\n",
"\n",
" chi2_str = \"\"\n",
" if \"chi2\" in m:\n",
" chi2_str = f\" chi2/ndf={np.median(m['chi2'] / ndf):.4f}\"\n",
"\n",
" print(f\"[{name:22s}] {med_t:8.2f} ms \"\n",
" f\"{dA:10.3f} {dMu:10.4f} {dSig:10.4f}{chi2_str}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "55ecc77c-0823-408d-a811-8e4f4900f332",
"metadata": {
"scrolled": true
},
{
"cell_type": "code",
"execution_count": 3,
"id": "35772e2c-37c6-4986-a9c0-7dca4a034827",
"metadata": {},
"outputs": [],
"source": [
"ROWS = 100\n",
"COLS = 100\n",
"N_SCAN = 100\n",
"NOISE_FRAC = 0.05\n",
"SEED = 42\n",
"N_THREADS = 4\n",
"\n",
"N_REPEATS = 7\n",
"N_WARMUP = 3 # untimed iterations (icache + branch predictor warmup)\n",
"COOLDOWN = 2.0 # seconds between (method, thread_count) pairs"
]
},
{
"cell_type": "markdown",
"id": "be455445-7df8-47cc-9f39-d2a38856e0bd",
"metadata": {},
"source": [
"## Data generator"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "06c8fddb-56d9-4f84-8b21-4ffd8b7bd26f",
"metadata": {},
"outputs": [],
"source": [
"def generate_3d_data(rows, cols, n_scan, noise_frac, seed):\n",
" \"\"\"\n",
" Generate a synthetic detector image stack where each pixel has a\n",
" Gaussian response curve with per-pixel variation in A, mu, sigma.\n",
"\n",
" Returns x (n_scan,), y (rows, cols, n_scan), y_err (rows, cols, n_scan),\n",
" and the ground-truth parameter arrays.\n",
" \"\"\"\n",
" rng = np.random.default_rng(seed)\n",
"\n",
" # Per-pixel true params each of shape: [rows, cols, 1]\n",
" A_true = rng.uniform(200, 1000, size=(rows, cols))\n",
" mu_true = rng.uniform(20, 80, size=(rows, cols))\n",
" sig_true = rng.uniform(3, 12, size=(rows, cols))\n",
" \n",
" # One common binned energy array\n",
" x = np.linspace(0, 100, n_scan) # shape [1, 1, nscan]\n",
"\n",
" # Build ground truth signals per-pixel\n",
" exponent = -0.5 * ((x[None, None, :] - mu_true[:, :, None]) / sig_true[:,:, None])**2 # shape [rows, cols, nscan]\n",
" y_clean = A_true[:, :, None] * np.exp(exponent)\n",
"\n",
" # Perturb with noise\n",
" noise_sigma = noise_frac * A_true[:, :, None] * np.ones_like(y_clean) # shape [rows, cols, nscan]\n",
" noise = rng.normal(0, noise_sigma)\n",
" y = y_clean + noise\n",
"\n",
" y_err = noise_sigma.copy()\n",
"\n",
" return x, y, y_err, A_true, mu_true, sig_true "
]
},
{
"cell_type": "markdown",
"id": "30ed6bb8-4798-497f-9990-4c518f885855",
"metadata": {},
"source": [
"## Profiling function"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "dc0b52b6-9fc3-453d-b6eb-e325a2b8342e",
"metadata": {},
"outputs": [],
"source": [
"def bench(fn, n_warmup=N_WARMUP, n_repeats=N_REPEATS):\n",
" \"\"\"\n",
" Warmup then time `fn` over `n_repeats` calls.\n",
" Returns (last_result, list_of_walltimes_in_seconds).\n",
" \"\"\"\n",
" # warmup: primes icache, branch predictor, and lets CPU ramp to boost clock\n",
" for _ in range(n_warmup):\n",
" res = fn()\n",
"\n",
" times = []\n",
" for _ in range(n_repeats):\n",
" t0 = time.perf_counter()\n",
" res = fn()\n",
" t1 = time.perf_counter()\n",
" times.append(t1 - t0)\n",
" return res, times"
]
},
{
"cell_type": "markdown",
"id": "377fc820-95b2-48aa-b104-a272c50e4103",
"metadata": {},
"source": [
"# Quick check on small (2x2) frame"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "9cc999b9-e0d8-4deb-ae29-e8bd0141534b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"== True Gaussian params == \n",
"A_true = \n",
" [[819.16483884 551.1027518 ]\n",
" [886.87833593 757.89442325]]\n",
"mu_true = \n",
" [[25.65064087 78.5373411 ]\n",
" [65.66838212 67.16385832]]\n",
"sig_true = \n",
" [[ 4.15302269 7.05347344]\n",
" [ 6.33718222 11.3408849 ]]\n",
"\n",
"\n",
"== Fit results ==\n",
"A_fit = \n",
" [[812.09277132 559.04069721]\n",
" [899.09335849 759.24481682]]\n",
"mu_fit = \n",
" [[25.6598209 78.40461782]\n",
" [65.52261318 66.84540995]]\n",
"sig_fit = \n",
" [[ 4.2778026 7.041045 ]\n",
" [ 6.29190225 11.34233504]]\n"
]
}
],
"source": [
"# Generate 2 x 2 dataset of Gaussian-like profiles for each pixel\n",
"x2, y2, yerr2, true_A2, true_mu2, true_sig2 = generate_3d_data(\n",
" 2, 2, N_SCAN, NOISE_FRAC, SEED\n",
")\n",
"model_g = Gaussian()\n",
"model_g.compute_errors = True\n",
"result = model_g.fit(x2, y2, yerr2)\n",
"\n",
"from pprint import pprint\n",
"print(\"== True Gaussian params == \")\n",
"print(\"A_true = \\n\", true_A2)\n",
"print(\"mu_true = \\n\", true_mu2)\n",
"print(\"sig_true = \\n\",true_sig2)\n",
"print(\"\\n\")\n",
"\n",
"print(\"== Fit results ==\")\n",
"par = result['par']\n",
"# print(par)\n",
"A_fit = par[:, :, 0]\n",
"mu_fit = par[:, :, 1]\n",
"sig_fit = par[:, :, 2]\n",
"print(\"A_fit = \\n\", A_fit)\n",
"print(\"mu_fit = \\n\", mu_fit)\n",
"print(\"sig_fit = \\n\", sig_fit)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "a98a41f3-fd2e-4bfc-9ec6-0d23dd38e896",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"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 truncated
"text/plain": [
"<Figure size 1200x800 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots(2, 2, figsize=(12,8))\n",
"\n",
"# Gaussians in 2x2 frame: True vs Fit\n",
"for row in range(2):\n",
" for col in range(2):\n",
" ax[row, col].plot(x2, y2[row, col,:], label=\"data\")\n",
" ax[row, col].plot(x2, model_g(x2, result['par'][row, col,:]), linewidth=1, color=\"green\", label=\"minuit\")\n",
" ax[row, col].set_title(f\"Gaussian Fit to data in pixel [{row}, {col}]\")\n",
" ax[row, col].legend()"
]
},
{
"cell_type": "markdown",
"id": "fcf52481-0278-4f95-8676-829a6d61eff8",
"metadata": {},
"source": [
"## Fit data with different backends"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "1f6bc651-80c1-41dd-8a05-7f15aba006aa",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Generating synthetic data: 100x100 pixels, 100 scan points, noise_frac=0.05\n",
"\n",
"model.max_calls = 100\n",
"model.tolerance = 0.5\n",
"model.compute_errors = False\n"
]
}
],
"source": [
"# ===============\n",
"# DATA GENERATION\n",
"# ===============\n",
"print(f\"Generating synthetic data: {ROWS}x{COLS} pixels, \"\n",
" f\"{N_SCAN} scan points, noise_frac={NOISE_FRAC}\\n\")\n",
"\n",
"x, y, yerr, true_A, true_mu, true_sig = generate_3d_data(\n",
" ROWS, COLS, N_SCAN, NOISE_FRAC, SEED\n",
")\n",
"\n",
"model = Gaussian()\n",
"print(f\"model.max_calls = {model.max_calls}\")\n",
"print(f\"model.tolerance = {model.tolerance}\")\n",
"print(\"model.compute_errors =\", model.compute_errors)\n",
"METHOD_DEFS = [\n",
" (\"lmfit (LM)\",\n",
" lambda nt: lambda: fit_gaus(x, y, n_threads=nt),\n",
" \"#2196F3\", {\"linewidth\": 3.0, \"linestyle\": \"-\"}),\n",
"\n",
" (\"Minuit2 (obj API)\",\n",
" lambda nt: lambda: model.fit(x, y, n_threads=nt),\n",
" \"#FF9800\", {\"linewidth\": 2.5, \"linestyle\": \":\"}),\n",
"]\n",
"\n",
"colors = {label: c for label, _, c, _ in METHOD_DEFS}\n",
"styles = {label: s for label, _, _, s in METHOD_DEFS}"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "5a417145-ce42-4c3a-a7bf-05ff97ba0450",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Method time (ms) med|dA| med|dMu| med|dSig|\n",
"--------------------------------------------------------------------------------\n",
"[lmfit (LM) ] 90.30 ms 6.272 0.0940 0.0949\n",
"[Minuit2 (obj API) ] 59.60 ms 6.272 0.0940 0.0949 chi2/ndf=880.9946\n"
]
}
],
"source":
[
"# ====================================\n",
"# SINGLE-CALL BENCHMARK (at N_THREADS)\n",
"# ====================================\n",
"def extract_result(label, res):\n",
" \"\"\"Normalize return values across fitters into a common dict.\"\"\"\n",
" if isinstance(res, dict):\n",
" out = {\"par\": res[\"par\"]}\n",
" if \"par_err\" in res:\n",
" out[\"par_err\"] = res[\"par_err\"]\n",
" if \"chi2\" in res:\n",
" out[\"chi2\"] = res[\"chi2\"]\n",
" return out\n",
" # fit_gaus without y_err returns a raw array\n",
" return {\"par\": res}\n",
" \n",
"methods = {}\n",
"for label, factory, _, _ in METHOD_DEFS:\n",
" time.sleep(COOLDOWN)\n",
" res, times = bench(factory(N_THREADS))\n",
" entry = extract_result(label, res)\n",
" entry[\"times\"] = times\n",
" methods[label] = entry\n",
"\n",
"# ---- Print summary ----\n",
"ndf = N_SCAN - 3\n",
"print(f\"{'Method':24s} {'time (ms)':>10s} {'med|dA|':>10s} {'med|dMu|':>10s} {'med|dSig|':>10s}\")\n",
"print(\"-\" * 80)\n",
"for name, m in methods.items():\n",
" par = m[\"par\"]\n",
" med_t = np.median(m[\"times\"]) * 1e3\n",
" dA = np.median(np.abs(par[:,:,0] - true_A))\n",
" dMu = np.median(np.abs(par[:,:,1] - true_mu))\n",
" dSig = np.median(np.abs(par[:,:,2] - true_sig))\n",
"\n",
" chi2_str = \"\"\n",
" if \"chi2\" in m:\n",
" chi2_str = f\" chi2/ndf={np.median(m['chi2'] / ndf):.4f}\"\n",
"\n",
" print(f\"[{name:22s}] {med_t:8.2f} ms \"\n",
" f\"{dA:10.3f} {dMu:10.4f} {dSig:10.4f}{chi2_str}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "55ecc77c-0823-408d-a811-8e4f4900f332",
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"# ===============\n",
"# THREAD SCALING\n",
"# ===============\n",
"thread_counts = [1, 2, 4, 8, 16]\n",
"\n",
"thread_times = {label: [] for label, _, _, _ in METHOD_DEFS}\n",
"ttimes_stddev = {label: [] for label, _, _, _ in METHOD_DEFS}\n",
"\n",
"for nt in thread_counts:\n",
" # shuffle method order per thread count to decorrelate thermal bias\n",
" run_order = list(METHOD_DEFS)\n",
" random.shuffle(run_order)\n",
"\n",
" for label, factory, _, _ in run_order:\n",
" time.sleep(COOLDOWN)\n",
" _, times = bench(factory(nt))\n",
"\n",
" med = np.median(times) * 1e3\n",
" std = np.std(times) * 1e3\n",
" thread_times[label].append(med)\n",
" ttimes_stddev[label].append(std)\n",
"\n",
" per_px = med / (ROWS * COLS) * 1e3\n",
" per_px_std = std / (ROWS * COLS) * 1e3\n",
" print(f\" {label:22s} n_threads={nt:2d} \"\n",
" f\"{med:8.2f} ± {std:6.2f} ms \"\n",
" f\"({per_px:.4f} ± {per_px_std:.4f} μs/pixel)\")\n",
" print(\"\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5f3152d-be84-420e-8045-9c42ac5c24cb",
"metadata": {},
"outputs": [],
"source": [
"# =============================\n",
"# FIGURE 1: Residual histograms\n",
"# =============================\n",
"param_names = [\"A\", \"μ\", \"σ\"]\n",
"param_truths = [true_A, true_mu, true_sig]\n",
"\n",
"fig1, axes1 = plt.subplots(1, 3, figsize=(15, 5))\n",
"fig1.suptitle(f\"Parameter Residuals — {ROWS}×{COLS} pixels, {N_SCAN} scan points\",\n",
" fontsize=14, fontweight=\"bold\")\n",
"\n",
"for col, (pname, truth) in enumerate(zip(param_names, param_truths)):\n",
" ax = axes1[col]\n",
"\n",
" # collect residuals across all methods for shared bin edges\n",
" res_by_method = {}\n",
" all_res = []\n",
" for mname, m in methods.items():\n",
" residual = (m[\"par\"][:, :, col] - truth).ravel()\n",
" res_by_method[mname] = residual\n",
" all_res.append(residual)\n",
" all_res = np.concatenate(all_res)\n",
"\n",
" lo, hi = np.percentile(all_res, [0.5, 99.5])\n",
" edges = np.linspace(lo, hi, 101)\n",
"\n",
" for mname, r in res_by_method.items():\n",
" ax.hist(r, bins=edges, histtype=\"step\", label=mname,\n",
" color=colors[mname],\n",
" linewidth=styles[mname][\"linewidth\"],\n",
" linestyle=styles[mname][\"linestyle\"])\n",
"\n",
" ax.axvline(0, color=\"k\", linestyle=\"--\", linewidth=1, alpha=0.7)\n",
" ax.set_xlabel(f\"Fitted {pname} − True {pname}\")\n",
" ax.set_ylabel(\"Pixel count\")\n",
" ax.set_title(f\"Δ{pname}\")\n",
" ax.legend(fontsize=8)\n",
" ax.grid(alpha=0.3)\n",
"\n",
"fig1.tight_layout()\n",
"# fig1.savefig(\"fig1_residual_histograms.png\", dpi=150, bbox_inches=\"tight\")\n",
"# print(\"\\nSaved fig1_residual_histograms.png\")\n",
"\n",
"# ====================================================\n",
"# FIGURE 2: Performance — bar chart + thread scaling\n",
"# ====================================================\n",
"fig2 = plt.figure(figsize=(14, 5))\n",
"gs = GridSpec(1, 2, figure=fig2, width_ratios=[1, 1.3])\n",
"\n",
"# -- Left: bar chart at N_THREADS --\n",
"ax2a = fig2.add_subplot(gs[0])\n",
"names = list(methods.keys())\n",
"medians = [np.median(methods[n][\"times\"]) * 1e3 for n in names]\n",
"bars = ax2a.barh(names, medians,\n",
" color=[colors[n] for n in names],\n",
" edgecolor=\"white\", height=0.5)\n",
"ax2a.set_xlabel(\"Median wall time (ms)\")\n",
"ax2a.set_title(f\"Single call — {ROWS}×{COLS} px, {N_THREADS} threads\")\n",
"for bar, val in zip(bars, medians):\n",
" ax2a.text(bar.get_width() + max(medians) * 0.02,\n",
" bar.get_y() + bar.get_height() / 2,\n",
" f\"{val:.1f} ms\", va=\"center\", fontsize=10)\n",
"ax2a.grid(axis=\"x\", alpha=0.3)\n",
"ax2a.set_xlim(0, max(medians) * 1.25)\n",
"\n",
"# -- Right: thread scaling with error bars --\n",
"ax2b = fig2.add_subplot(gs[1])\n",
"for label, _, _, _ in METHOD_DEFS:\n",
" tt = thread_times[label]\n",
" sd = ttimes_stddev[label]\n",
" speedup = [tt[0] / t for t in tt]\n",
" # propagate uncertainty: S = t0/t → δS/S = sqrt((δt0/t0)² + (δt/t)²)\n",
" speedup_err = [\n",
" s * np.sqrt((sd[0] / tt[0])**2 + (sd[i] / tt[i])**2)\n",
" for i, s in enumerate(speedup)\n",
" ]\n",
" ax2b.errorbar(thread_counts, speedup, yerr=speedup_err,\n",
" fmt=\"o-\", label=label, color=colors[label],\n",
" linewidth=2, markersize=7, capsize=4)\n",
"\n",
"ax2b.plot(thread_counts, thread_counts, \"k--\", alpha=0.4, label=\"Ideal linear\")\n",
"ax2b.set_xlabel(\"Number of threads\")\n",
"ax2b.set_ylabel(\"Speedup vs 1 thread\")\n",
"ax2b.set_title(\"Thread scaling\")\n",
"ax2b.set_xticks(thread_counts)\n",
"ax2b.legend(fontsize=9)\n",
"ax2b.grid(alpha=0.3)\n",
"\n",
"fig2.tight_layout()\n",
"# fig2.savefig(\"fig2_performance.png\", dpi=150, bbox_inches=\"tight\")\n",
"# print(\"Saved fig2_performance.png\")\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "92a8c724-e139-45d4-a354-1b8408637ead",
"metadata": {},
"outputs": [],
"source": []
}
],
"outputs": [],
"source": [
"# ===============\n",
"# THREAD SCALING\n",
"# ===============\n",
"thread_counts = [1, 2, 4, 8, 16]\n",
"\n",
"thread_times = {label: [] for label, _, _, _ in METHOD_DEFS}\n",
"ttimes_stddev = {label: [] for label, _, _, _ in METHOD_DEFS}\n",
"\n",
"for nt in thread_counts:\n",
" # shuffle method order per thread count to decorrelate thermal bias\n",
" run_order = list(METHOD_DEFS)\n",
" random.shuffle(run_order)\n",
"\n",
" for label, factory, _, _ in run_order:\n",
" time.sleep(COOLDOWN)\n",
" _, times = bench(factory(nt))\n",
"\n",
" med = np.median(times) * 1e3\n",
" std = np.std(times) * 1e3\n",
" thread_times[label].append(med)\n",
" ttimes_stddev[label].append(std)\n",
"\n",
" per_px = med / (ROWS * COLS) * 1e3\n",
" per_px_std = std / (ROWS * COLS) * 1e3\n",
" print(f\" {label:22s} n_threads={nt:2d} \"\n",
" f\"{med:8.2f} \u00b1 {std:6.2f} ms \"\n",
" f\"({per_px:.4f} \u00b1 {per_px_std:.4f} \u03bcs/pixel)\")\n",
" print(\"\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5f3152d-be84-420e-8045-9c42ac5c24cb",
"metadata": {},
"outputs": [],
"source": [
"# =============================\n",
"# FIGURE 1: Residual histograms\n",
"# =============================\n",
"param_names = [\"A\", \"\u03bc\", \"\u03c3\"]\n",
"param_truths = [true_A, true_mu, true_sig]\n",
"\n",
"fig1, axes1 = plt.subplots(1, 3, figsize=(15, 5))\n",
"fig1.suptitle(f\"Parameter Residuals \u2014 {ROWS}\u00d7{COLS} pixels, {N_SCAN} scan points\",\n",
" fontsize=14, fontweight=\"bold\")\n",
"\n",
"for col, (pname, truth) in enumerate(zip(param_names, param_truths)):\n",
" ax = axes1[col]\n",
"\n",
" # collect residuals across all methods for shared bin edges\n",
" res_by_method = {}\n",
" all_res = []\n",
" for mname, m in methods.items():\n",
" residual = (m[\"par\"][:, :, col] - truth).ravel()\n",
" res_by_method[mname] = residual\n",
" all_res.append(residual)\n",
" all_res = np.concatenate(all_res)\n",
"\n",
" lo, hi = np.percentile(all_res, [0.5, 99.5])\n",
" edges = np.linspace(lo, hi, 101)\n",
"\n",
" for mname, r in res_by_method.items():\n",
" ax.hist(r, bins=edges, histtype=\"step\", label=mname,\n",
" color=colors[mname],\n",
" linewidth=styles[mname][\"linewidth\"],\n",
" linestyle=styles[mname][\"linestyle\"])\n",
"\n",
" ax.axvline(0, color=\"k\", linestyle=\"--\", linewidth=1, alpha=0.7)\n",
" ax.set_xlabel(f\"Fitted {pname} \u2212 True {pname}\")\n",
" ax.set_ylabel(\"Pixel count\")\n",
" ax.set_title(f\"\u0394{pname}\")\n",
" ax.legend(fontsize=8)\n",
" ax.grid(alpha=0.3)\n",
"\n",
"fig1.tight_layout()\n",
"# fig1.savefig(\"fig1_residual_histograms.png\", dpi=150, bbox_inches=\"tight\")\n",
"# print(\"\\nSaved fig1_residual_histograms.png\")\n",
"\n",
"# ====================================================\n",
"# FIGURE 2: Performance \u2014 bar chart + thread scaling\n",
"# ====================================================\n",
"fig2 = plt.figure(figsize=(14, 5))\n",
"gs = GridSpec(1, 2, figure=fig2, width_ratios=[1, 1.3])\n",
"\n",
"# -- Left: bar chart at N_THREADS --\n",
"ax2a = fig2.add_subplot(gs[0])\n",
"names = list(methods.keys())\n",
"medians = [np.median(methods[n][\"times\"]) * 1e3 for n in names]\n",
"bars = ax2a.barh(names, medians,\n",
" color=[colors[n] for n in names],\n",
" edgecolor=\"white\", height=0.5)\n",
"ax2a.set_xlabel(\"Median wall time (ms)\")\n",
"ax2a.set_title(f\"Single call \u2014 {ROWS}\u00d7{COLS} px, {N_THREADS} threads\")\n",
"for bar, val in zip(bars, medians):\n",
" ax2a.text(bar.get_width() + max(medians) * 0.02,\n",
" bar.get_y() + bar.get_height() / 2,\n",
" f\"{val:.1f} ms\", va=\"center\", fontsize=10)\n",
"ax2a.grid(axis=\"x\", alpha=0.3)\n",
"ax2a.set_xlim(0, max(medians) * 1.25)\n",
"\n",
"# -- Right: thread scaling with error bars --\n",
"ax2b = fig2.add_subplot(gs[1])\n",
"for label, _, _, _ in METHOD_DEFS:\n",
" tt = thread_times[label]\n",
" sd = ttimes_stddev[label]\n",
" speedup = [tt[0] / t for t in tt]\n",
" # propagate uncertainty: S = t0/t \u2192 \u03b4S/S = sqrt((\u03b4t0/t0)\u00b2 + (\u03b4t/t)\u00b2)\n",
" speedup_err = [\n",
" s * np.sqrt((sd[0] / tt[0])**2 + (sd[i] / tt[i])**2)\n",
" for i, s in enumerate(speedup)\n",
" ]\n",
" ax2b.errorbar(thread_counts, speedup, yerr=speedup_err,\n",
" fmt=\"o-\", label=label, color=colors[label],\n",
" linewidth=2, markersize=7, capsize=4)\n",
"\n",
"ax2b.plot(thread_counts, thread_counts, \"k--\", alpha=0.4, label=\"Ideal linear\")\n",
"ax2b.set_xlabel(\"Number of threads\")\n",
"ax2b.set_ylabel(\"Speedup vs 1 thread\")\n",
"ax2b.set_title(\"Thread scaling\")\n",
"ax2b.set_xticks(thread_counts)\n",
"ax2b.legend(fontsize=9)\n",
"ax2b.grid(alpha=0.3)\n",
"\n",
"fig2.tight_layout()\n",
"# fig2.savefig(\"fig2_performance.png\", dpi=150, bbox_inches=\"tight\")\n",
"# print(\"Saved fig2_performance.png\")\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "92a8c724-e139-45d4-a354-1b8408637ead",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
+24 -136
View File
@@ -2,26 +2,19 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"id": "efef8e20-6571-4561-8f0b-6048b57907de",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import random\n",
"import numpy as np\n",
"np.set_printoptions(suppress=True, precision=6)\n",
"\n",
"import matplotlib.pyplot as plt\n",
"from scipy.special import erf\n",
"\n",
"import sys\n",
"sys.path.insert(0, '/home/ferjao_k/sw/aare/build')\n",
"\n",
"from aare import fit_scurve, fit_scurve2 # lmfit\n",
"from aare import RisingScurve, FallingScurve, fit # minuit2 (object based API)\n",
"\n",
"from pprint import pprint"
"from aare import RisingScurve, FallingScurve\n",
"from pprint import pprint\n"
]
},
{
@@ -34,7 +27,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "35772e2c-37c6-4986-a9c0-7dca4a034827",
"metadata": {},
"outputs": [],
@@ -58,7 +51,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "06c8fddb-56d9-4f84-8b21-4ffd8b7bd26f",
"metadata": {},
"outputs": [],
@@ -92,22 +85,10 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "9a0b3292-7c57-4483-aecc-7ef683ff0b62",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"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 truncated
"text/plain": [
"<Figure size 1200x400 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"outputs": [],
"source": [
"fig, ax = plt.subplots(1, 2, figsize=(12,4), sharex=True)\n",
"\n",
@@ -134,38 +115,11 @@
},
{
"cell_type": "code",
"execution_count": 18,
"execution_count": null,
"id": "37d2fda4-40b8-4ea2-8f2c-6c3f2384d158",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Paramter list of the Scurve:\n",
"['p0', 'p1', 'mu', 'sigma', 'A', 'C']\n",
"6 \n",
"\n",
"== Tuned fit settings ==\n",
"max_calls : 500\n",
"tolerance : 0.01\n",
"compute_erros: True\n",
"\n",
"\n",
"== Results ==\n",
"True rising params: [100. 0.25 60. 6. 120. 1. ]\n",
"lmfit rising result: [ 99.467831 0.287017 60.078405 5.715461 117.257939 0.967234]\n",
"minuit_grad (obj api ) rising result:\n",
"{'par': array([100. , 0.269137, 60.050193, 5.729932, 117.777635,\n",
" 0.984073]),\n",
" 'par_err': array([1. , 0.054406, 0.747753, 0.909864, 7.887499, 0.174058]),\n",
" 'chi2': array([7.351743])}\n"
]
}
],
"outputs": [],
"source": [
"res_r_lmfit = fit_scurve(x, y_rising)\n",
"\n",
"model_r = RisingScurve()\n",
"print(\"Paramter list of the Scurve:\")\n",
"# print(model_r.GetParNames())\n",
@@ -187,7 +141,6 @@
"res_r_munuit_obj = model_r.fit(x, y_rising, np.sqrt(y_rising))\n",
"\n",
"print(\"True rising params: \", p_true_rising)\n",
"print(\"lmfit rising result: \", res_r_lmfit)\n",
"print(\"minuit_grad (obj api ) rising result:\")\n",
"pprint(res_r_munuit_obj, sort_dicts=False)"
]
@@ -202,26 +155,11 @@
},
{
"cell_type": "code",
"execution_count": 19,
"execution_count": null,
"id": "5ce22d2d-1adb-47d9-bf36-6cb535fa8b77",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True falling params: [100. 0.25 60. 6. 120. -1. ]\n",
"lmfit falling result: [101.73448 0.225786 60.251971 6.092915 118.476735 -0.998677]\n",
"minuit_grad (obj api ) falling result\n",
"{'par': array([101.741754, 0.225723, 60.251604, 6.092887, 118.473268,\n",
" -0.998565]),\n",
" 'chi2': array([1865.728797])}\n"
]
}
],
"outputs": [],
"source": [
"res_f_lmfit = fit_scurve2(x, y_falling)\n",
"\n",
"model_f = FallingScurve()\n",
"model_f.SetParameter(2, 20) # set start p2 = 20\n",
"model_f.SetParameter(4, 90) # set start p4 = 90\n",
@@ -230,36 +168,22 @@
"res_f_munuit_obj = model_f.fit(x, y_falling)\n",
"\n",
"print(\"True falling params: \", p_true_falling)\n",
"print(\"lmfit falling result: \", res_f_lmfit)\n",
"print(\"minuit_grad (obj api ) falling result\")\n",
"pprint(res_f_munuit_obj, sort_dicts=False)"
]
},
{
"cell_type": "code",
"execution_count": 20,
"execution_count": null,
"id": "fe72503a-fc77-4b17-9b17-b98055e2fc87",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"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p06duuR9appGmzZt8i2rVasWhw4d8jxft26dp015Pfnkk5e0D7PZzMKFC9m5cyeTJk3iiSee4OTJk4wdO5aqVauyZ88ewD1P2uFw5HsA7N27l3///ZdevXrh5eV1ycd2MQ888EC+JHMANWvW5K677mLmzJmeZbt27eKPP/6gZ8+enmU//vgjNWrU4M4778zX3hYtWly37O533303sbGxvP3222zcuBG73X7N9ymEEOLKzZ49m02bNuV7GAwGHA4H77zzDtWqVcNkMmEwGDCZTOzbt49du3YV2f6bNm2aL1dKREQE4eHhnnN+ZmYmf/75J+3atcNkMnnW8/PzO+9aoTD16tVj//79LF26lDfeeIMGDRqwatUqunXrxqOPPurJy5Lbu5/7yJ2//fPPPwPQr1+/IjnmXB06dDhvWc+ePcnOzmb+/PmeZTNnzsRsNtOlSxfAPax/9+7dPPXUUwD52tyqVSvi4+M91zHXyp133onJZKJPnz7MmjWLAwcOXNP9iVuPBNrippd7Al69ejXPPfccu3btuqRg9umnn2bGjBkcOnSIDh06EB4ezj333MOKFSsu+l4fH5/zglez2YzVavU8P336NBEREee9t6BlF1K1alUGDhzInDlzOHz4MBMnTuT06dO8+eabAMyaNQuj0ZjvAe65yECBw6uvRmFzyHr27Mlvv/3G7t27gbMn3bw/ixMnTrBt27bz2muxWFBKXdbNjis1f/58unfvzvTp02nQoAHBwcF069aNhISEa75vIYQQl69q1arUrVs33wNg0KBBvPnmm7Rr147Fixfz+++/s2nTJu644w6ys7OLbP8hISHnLTObzZ59JCcno5S66nO+0WikRYsWjB07lmXLlnHkyBGaNGnCjz/+6AmkH3zwwXznz9yb2SdPnkSv1xMZGXklh1iogs751atXp169ep6b606nkzlz5tC2bVuCg4OBs3lyhgwZct45v2/fvgDX/JxfsWJFVq5cSXh4OP369aNixYpUrFiRyZMnX9P9iluHzNEWN73cEzC47zo7nU6mT5/OggUL6Nix4wXf+8wzz/DMM8+QmZnJL7/8wsiRI2ndujV79+4lJibmqtoVEhLCH3/8cd7yqwnoNE3j5ZdfZsyYMZ4M3G3atGHTpk3nrZs7J/no0aMX3KaXl9d5Sdyg8BNgYXPGnnzySQYNGkRsbCxjx47liy++oF27dvl6v0NDQ/H29mbGjBkFbiM0NPSCbS0KoaGhfPDBB3zwwQccPnyYH374gddff53ExESWLl16zfcvhBCiaMyZM4du3brxzjvv5Ft+6tSp61rmKSgoCE3TCpyPfTXn/JCQEAYOHMjatWvZvn07rVq14pNPPiE9Pd2zTuLine truncated
"text/plain": [
"<Figure size 1200x400 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"outputs": [],
"source": [
"fig, ax = plt.subplots(1, 2, figsize=(12,4))\n",
"\n",
"# rising\n",
"ax[0].plot(x, y_true_rising, label=\"true\")\n",
"ax[0].scatter(x, y_rising, s=12, label=\"data\")\n",
"ax[0].scatter(x, scurve(x, res_r_lmfit[:6]), s=20, marker=\"x\", label=\"lmfit\")\n",
"ax[0].plot(x, model_r(x, res_r_munuit_obj['par']), linewidth=1, color=\"green\", label=\"minuit\")\n",
"ax[0].set_title(\"Rising S-curve fits\")\n",
"ax[0].legend()\n",
@@ -267,7 +191,6 @@
"# falling\n",
"ax[1].plot(x, y_true_falling, label=\"true\")\n",
"ax[1].scatter(x, y_falling, s=12, label=\"data\")\n",
"ax[1].scatter(x, scurve2(x, res_f_lmfit[:6]), s=20, marker=\"x\", label=\"lmfit\")\n",
"ax[1].plot(x, model_f(x, res_f_munuit_obj['par']), linewidth=1, color=\"green\", label=\"minuit\")\n",
"ax[1].set_title(\"Falling S-curve fits\")\n",
"ax[1].legend()\n",
@@ -285,29 +208,13 @@
},
{
"cell_type": "code",
"execution_count": 21,
"execution_count": null,
"id": "3ba40e9a-0c64-4ef6-ba90-b1116f99b862",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Rising abs error lmfit : [0.532169 0.037017 0.078405 0.284539 2.742061 0.032766]\n",
"Rising abs error minuit_grad: [0. 0.019137 0.050193 0.270068 2.222365 0.015927]\n",
"\n",
"\n",
"Falling abs error lmfit : [1.73448 0.024214 0.251971 0.092915 1.523265 0.001323]\n",
"Falling abs error minuit_grad: [1.741754 0.024277 0.251604 0.092887 1.526732 0.001435]\n"
]
}
],
"outputs": [],
"source": [
"print(\"Rising abs error lmfit : \", np.abs(res_r_lmfit[:6] - p_true_rising))\n",
"print(\"Rising abs error minuit_grad: \", np.abs(res_r_munuit_obj['par'] - p_true_rising))\n",
"print(\"\\n\")\n",
"print(\"Falling abs error lmfit : \", np.abs(res_f_lmfit[:6] - p_true_falling))\n",
"print(\"Falling abs error minuit_grad: \", np.abs(res_f_munuit_obj['par'] - p_true_falling))"
"print(\"Rising absolute parameter error:\", np.abs(res_r_munuit_obj['par'] - p_true_rising))\n",
"print(\"Falling absolute parameter error:\", np.abs(res_f_munuit_obj['par'] - p_true_falling))\n"
]
},
{
@@ -320,25 +227,12 @@
},
{
"cell_type": "code",
"execution_count": 22,
"execution_count": null,
"id": "dc0b52b6-9fc3-453d-b6eb-e325a2b8342e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Rising lmfit : 0.334 ms\n",
"Rising minuit2 : 0.239 ms\n",
"\n",
"Falling lmfit : 0.294 ms\n",
"Falling minuit2 : 0.214 ms\n"
]
}
],
"outputs": [],
"source": [
"def bench(fn, n_repeats=200):\n",
" # warmup\n",
" for _ in range(3):\n",
" fn()\n",
"\n",
@@ -348,20 +242,14 @@
" t1 = time.perf_counter()\n",
" return res, (t1 - t0) / n_repeats\n",
"\n",
"model_rising = RisingScurve()\n",
"model_rising = RisingScurve()\n",
"model_falling = FallingScurve()\n",
"\n",
"res_r_lmfit, t_r_lmfit = bench(lambda: fit_scurve(x, y_rising))\n",
"res_r_minuit, t_r_minuit_grad = bench(lambda: model_rising.fit(x, y_rising))\n",
"_, t_rising = bench(lambda: model_rising.fit(x, y_rising))\n",
"_, t_falling = bench(lambda: model_falling.fit(x, y_falling))\n",
"\n",
"res_f_lmfit, t_f_lmfit = bench(lambda: fit_scurve2(x, y_falling))\n",
"res_f_minuit, t_f_minuit_grad = bench(lambda: model_falling.fit(x, y_falling))\n",
"\n",
"print(f\"Rising lmfit : {1e3*t_r_lmfit:.3f} ms\")\n",
"print(f\"Rising minuit2 : {1e3*t_r_minuit_grad:.3f} ms\")\n",
"print()\n",
"print(f\"Falling lmfit : {1e3*t_f_lmfit:.3f} ms\")\n",
"print(f\"Falling minuit2 : {1e3*t_f_minuit_grad:.3f} ms\")"
"print(f\"Rising Minuit2: {1e3*t_rising:.3f} ms\")\n",
"print(f\"Falling Minuit2: {1e3*t_falling:.3f} ms\")\n"
]
},
{
+22 -121
View File
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"id": "efef8e20-6571-4561-8f0b-6048b57907de",
"metadata": {},
"outputs": [],
@@ -13,30 +13,15 @@
"from scipy.special import erf\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.gridspec import GridSpec\n",
"import sys\n",
"sys.path.insert(0, '/home/ferjao_k/aare/build')\n",
"\n",
"from aare import fit_scurve2\n",
"from aare import FallingScurve, fit"
"from aare import FallingScurve\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "2536939d-8f34-438e-850a-6f3bdf48f658",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'/home/ferjao_k/aare/build/aare/__init__.py'"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"import aare\n",
"aare.__file__"
@@ -44,7 +29,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "35772e2c-37c6-4986-a9c0-7dca4a034827",
"metadata": {},
"outputs": [],
@@ -71,7 +56,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "06c8fddb-56d9-4f84-8b21-4ffd8b7bd26f",
"metadata": {},
"outputs": [],
@@ -127,7 +112,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": null,
"id": "dc0b52b6-9fc3-453d-b6eb-e325a2b8342e",
"metadata": {},
"outputs": [],
@@ -154,19 +139,10 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": null,
"id": "13bcd5a6-8f95-4300-a002-04023d0f9bab",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Generating synthetic data: 100x100 pixels, 100 scan points, noise_frac=0.05\n",
"\n"
]
}
],
"outputs": [],
"source": [
"print(f\"Generating synthetic data: {ROWS}x{COLS} pixels, \"\n",
" f\"{N_SCAN} scan points, noise_frac={NOISE_FRAC}\\n\")\n",
@@ -186,17 +162,13 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"id": "1f6bc651-80c1-41dd-8a05-7f15aba006aa",
"metadata": {},
"outputs": [],
"source": [
"model = FallingScurve()\n",
"METHOD_DEFS = [\n",
" (\"lmfit (LM)\",\n",
" lambda nt: lambda: fit_scurve2(x, y, n_threads=nt),\n",
" \"#FF9800\", {\"linewidth\": 3.0, \"linestyle\": \"-\"}),\n",
"\n",
" (\"Minuit2 (analytic)\",\n",
" lambda nt: lambda: model.fit(x, y, n_threads=nt),\n",
" \"#4CAF50\", {\"linewidth\": 4.0, \"linestyle\": \":\"}),\n",
@@ -216,27 +188,13 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": null,
"id": "5a417145-ce42-4c3a-a7bf-05ff97ba0450",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Method time (ms) med|dp0| med|dp1| med|dp2| med|dp3| med|dp4| med|dp5|\n",
"--------------------------------------------------------------------------------------------------------------\n",
"[lmfit (LM) ] 359.31 ms 18.6946 0.2249 0.2177 0.2663 11.9347 0.3635\n",
"[Minuit2 (analytic) ] 257.18 ms 17.1035 0.2147 0.2293 0.2668 11.9449 0.3518 chi2/ndf=596.8385\n"
]
}
],
"outputs": [],
"source": [
"PARAM_NAMES = [\"p0\", \"p1\", \"p2\", \"p3\", \"p4\", \"p5\"]\n",
"ndf = N_SCAN - len(PARAM_NAMES)\n",
"# NOTE: fit_scurve returns Ndf = n_scan - 2 in its dict, which looks wrong\n",
"# for a 6-parameter model. We use our own ndf = n_scan - 6 everywhere.\n",
"\n",
"def extract_result(label, res):\n",
" if isinstance(res, dict):\n",
" out = {\"par\": res[\"par\"]}\n",
@@ -245,8 +203,6 @@
" if \"chi2\" in res:\n",
" out[\"chi2\"] = res[\"chi2\"]\n",
" return out\n",
" # fallback: raw array, assume shape (rows, cols, 6)\n",
" return {\"par\": res}\n",
"\n",
"methods = {}\n",
"for label, factory, _, _ in METHOD_DEFS:\n",
@@ -288,35 +244,12 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": null,
"id": "55ecc77c-0823-408d-a811-8e4f4900f332",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" lmfit (LM) n_threads= 1 1404.39 ± 11.48 ms (140.4392 ± 1.1478 µs/pixel)\n",
" Minuit2 (analytic) n_threads= 1 1022.42 ± 1.69 ms (102.2417 ± 0.1690 µs/pixel)\n",
"\n",
"\n",
" lmfit (LM) n_threads= 2 708.78 ± 6.14 ms (70.8775 ± 0.6142 µs/pixel)\n",
" Minuit2 (analytic) n_threads= 2 521.15 ± 0.96 ms (52.1151 ± 0.0962 µs/pixel)\n",
"\n",
"\n",
" Minuit2 (analytic) n_threads= 4 256.73 ± 2.40 ms (25.6728 ± 0.2399 µs/pixel)\n",
" lmfit (LM) n_threads= 4 352.83 ± 0.77 ms (35.2833 ± 0.0767 µs/pixel)\n",
"\n",
"\n",
" lmfit (LM) n_threads= 8 226.62 ± 1.48 ms (22.6616 ± 0.1479 µs/pixel)\n",
" Minuit2 (analytic) n_threads= 8 164.39 ± 0.51 ms (16.4386 ± 0.0509 µs/pixel)\n",
"\n",
"\n"
]
}
],
"outputs": [],
"source": [
"thread_counts = [1, 2, 4, 8]\n",
" \n",
@@ -339,8 +272,8 @@
" per_px = med / (ROWS * COLS) * 1e3\n",
" per_px_std = std / (ROWS * COLS) * 1e3\n",
" print(f\" {label:22s} n_threads={nt:2d} \"\n",
" f\"{med:8.2f} ± {std:6.2f} ms \"\n",
" f\"({per_px:.4f} ± {per_px_std:.4f} µs/pixel)\")\n",
" f\"{med:8.2f} \u00b1 {std:6.2f} ms \"\n",
" f\"({per_px:.4f} \u00b1 {per_px_std:.4f} \u00b5s/pixel)\")\n",
" \n",
" print(\"\\n\")"
]
@@ -355,49 +288,17 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": null,
"id": "d5f3152d-be84-420e-8045-9c42ac5c24cb",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"iVBORw0KGgoAAAANSUhEUgAABjUAAAN2CAYAAACxdwtsAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Qd0VNXWwPGdQioBQkjo3RCQoogIogKKFBXQh4oVbE95gigKYsGCigqIgB9gQyyAgKgPlfcUxQYqVREVxYCCCNIC0tOT+dY+vDvMnUxCyiRT8v+tdTOZO3fu3DYzZ84+Z58Qh8PhEAAAAAAAAAAAAD8X6usNAAAAAAAAAAAAKA6CGgAAAAAAAAAAICAQ1AAAAAAAAAAAAAGBoAYAAAAAAAAAAAgIBDUAAAAAAAAAAEBAIKgBAAAAAAAAAAACAkENAAAAAAAAAAAQEAhqAAAAAAAAAACAgEBQAwAAAAAAAAAABASCGgAAACjUjTfeKCEhIWbq3r17sY+ULms9T9fhC19++aVzG3T6448/fLIdQGFef/112zXqC66vr9sDBNr3DQAAqHwIagAA4KfcK2Rdp6pVq8qpp54qw4cPly1btvh6UwOCv1bcFXaew8LCpEaNGnLGGWfIfffdJ7t37/b1psLHli5dKnfddZd06dJFYmJiShSwWbhwofTs2VMSEhIkMjJSmjRpIrfccots3ry50Ofs2rXLvF5ycrJER0dLfHy8nHvuufLyyy9LXl5eOeyhyMyZM+Xmm2+Wtm3bSnh4uHP/dHuLkpmZKRMnTpQOHTpItWrVJDY2Vtq0aSMPPfSQHDp0qNDnffvtt3L11VdLvXr1zHGpXbu29O/fXz799NNy2Dt4cvDgQZkwYYJceeWV5jy7XtcnC4iW9hotzfsBgcn1mho7dqyvNwcAAHhJuLdWBAAAKs6xY8dk48aNZnr11Vfl/ffflwsvvJBTEETy8/NNZez3339vptmzZ8uaNWukYcOGFbodWuGrlcOqol8bdjNmzDDv9ZJwOBxy0003yRtvvGGbv23bNvPZMW/ePHn33Xfl4osvLlDZ37t3b/n7779tgYNvvvnGTP/+97/lvffek6ioKK+epnvvvbfIIIQn+/fvNxXU+j5x9fPPP5vpzTfflM8//1yaNm1qe/yVV16RIUOGmPeaZe/evbJ48WIzPfLII/LYY49JeerYsaM888wzUplpQO7+++8v8fNKc42W9v2AisH3DQAAKC6CGgAABIirrrpKzjzzTMnOzpaVK1fKf/7zHzM/PT1dBg0aZCqGtMWpt+Xm5kpOTo5pBYuS0Qo27XFRpUqVEp/nw4cPmwq5n376yczXnhpTpkyRyZMnV+hp6NOnj5nge9rSuEGDBub60FboWvFenECIawWuVhpqL68FCxbIL7/8Yq7Ra6+91vyvvRWsoOkVV1zhrCzWYJZWBOs1OGvWLPPaH3/8sTz88MMFKuR1ff/85z9lzJgx0qpVK4/bpM9r3769DBgwoMBj+n7R5+k+6rW/fv36k+6jBiasgIZ+Tul9rcjWXh8a8NDPxmuuuUZWrFghoaHHO6rrem+//XZnQKNz587St29fUxn+0UcfmXmPP/64nHXWWXLJJZdIeWndurWZKjs9X+3atTPnff78+XLgwIEily/tNVqa9wMqDt83AACg2BwAAMAvffHFFw79qram1157zfb4ddddZ3v8s88+M/OXLl3quOmmmxynn366o3bt2o6IiAhHdHS045RTTjHzf/zxxwKvdcMNNzjX061bN8fvv//uGDhwoCMhIcEREhJitkW9+OKLjiuuuMKRkpJiHgsPD3fExcWZ17rvvvscaWlpBdbduHFj57offfRRx4cffujo3Lmz2ab69es7xowZ48jOzjbLvvDCC45WrVo5IiMjHU2bNnU8+eSTjvz8/ALrzM3Ndbz++uuOHj16OGrVqmW2IzEx0dG/f3/H559/bltW98f1OLlPun2udu7cafalbdu2jqpVq5ptSU5Odtx9992OXbt2FdgW1/XrcVy3bp3joosuctSoUcPM27p1a6nP88GDB835sx7r3bt3oeu48sorHQ0aNDDLV6tWzXHuuec6XnnlFUdeXl6B5ZcvX+647LLLHPXq1XNUqVLFERsba45Dnz59zDnS1y3s2nD373//29GxY0dHVFSUIykpyXHzzTc79uzZU+C4FLa/7sensGOh6xw1apTj/PPPdzRq1MicG912fc2ePXs65syZU+BaKeq1jh496njssccc7du3N+uyrqHTTjvN8c9//tPx0UcfOfxNenq68389NkUdR+t9oufYWubaa691PrZ//37z3rUe02ve8vzzzzvn6/v/119/dT724IMPOh/Tc/7333/bXvOqq64yj9WpU8eRmppaYJse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 truncated
"text/plain": [
"<Figure size 1600x900 with 6 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"<Figure size 1400x500 with 0 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png":
"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 truncated
"text/plain": [
"<Figure size 1400x500 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"outputs": [],
"source": [
"# FIGURE 1: Residual histograms (6 panels)\n",
"truth_arrays = [truths[pn] for pn in PARAM_NAMES]\n",
" \n",
"fig1, axes1 = plt.subplots(2, 3, figsize=(16, 9))\n",
"fig1.suptitle(\n",
" f\"Parameter Residuals — {ROWS}×{COLS} pixels, {N_SCAN} scan points\",\n",
" f\"Parameter Residuals \u2014 {ROWS}\u00d7{COLS} pixels, {N_SCAN} scan points\",\n",
" fontsize=14, fontweight=\"bold\")\n",
" \n",
"for idx, (pname, truth) in enumerate(zip(PARAM_NAMES, truth_arrays)):\n",
@@ -421,9 +322,9 @@
" linestyle=styles[mname][\"linestyle\"])\n",
" \n",
" ax.axvline(0, color=\"k\", linestyle=\"--\", linewidth=1, alpha=0.7)\n",
" ax.set_xlabel(f\"Fitted {pname} − True {pname}\")\n",
" ax.set_xlabel(f\"Fitted {pname} \u2212 True {pname}\")\n",
" ax.set_ylabel(\"Pixel count\")\n",
" ax.set_title(f\"Δ{pname}\")\n",
" ax.set_title(f\"\u0394{pname}\")\n",
" ax.legend(fontsize=8)\n",
" ax.grid(alpha=0.3)\n",
" \n",
@@ -444,7 +345,7 @@
" color=[colors[n] for n in names],\n",
" edgecolor=\"white\", height=0.5)\n",
"ax2a.set_xlabel(\"Median wall time (ms)\")\n",
"ax2a.set_title(f\"Single call — {ROWS}×{COLS} px, {N_THREADS} threads\")\n",
"ax2a.set_title(f\"Single call \u2014 {ROWS}\u00d7{COLS} px, {N_THREADS} threads\")\n",
"for bar, val in zip(bars, medians):\n",
" ax2a.text(bar.get_width() + max(medians) * 0.02,\n",
" bar.get_y() + bar.get_height() / 2,\n",
@@ -0,0 +1,147 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
import pytest
import aare
from aare.experimental import MultiThreadedFileReader
@pytest.fixture
def frame_file(tmp_path):
data = np.arange(10 * 2 * 3, dtype=np.uint16).reshape(10, 2, 3)
path = tmp_path / "frames.npy"
np.save(path, data)
return path, data
def test_experimental_import_path():
assert aare.experimental.MultiThreadedFileReader is MultiThreadedFileReader
assert MultiThreadedFileReader.__module__ == "aare.experimental"
assert not hasattr(aare, "MultiThreadedFileReader")
def test_reads_all_frames_in_order(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(path, n_threads=2, chunk_size=3)
first = reader.read()
second = reader.read()
exhausted = reader.read()
assert np.array_equal(first, expected[:6])
assert np.array_equal(second, expected[6:])
assert exhausted.shape == (0, 2, 3)
assert first.dtype == np.uint16
assert reader.n_threads == 2
assert reader.chunk_size == 3
assert reader.total_frames == 10
assert reader.source_total_frames == 10
assert reader.rows == 2
assert reader.cols == 3
assert reader.bitdepth == 16
assert reader.dtype == np.dtype(np.uint16)
assert reader.bytes_per_frame == 12
assert reader.total_bytes == expected.nbytes
assert len(reader) == 10
assert reader.tell() == 10
assert reader.remaining_frames == 0
assert reader.next_read_frames == 0
assert reader.next_read_bytes == 0
def test_total_frame_limit(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(
path, n_threads=2, chunk_size=2, total_frames=7
)
assert np.array_equal(reader.read(), expected[:4])
assert np.array_equal(reader.read_all(), expected[4:7])
def test_iteration_yields_one_chunk_per_thread(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(path, n_threads=2, chunk_size=2)
batches = list(reader)
assert [len(batch) for batch in batches] == [4, 4, 2]
assert np.array_equal(np.concatenate(batches), expected)
def test_seek_resets_iteration_position(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(path, n_threads=2, chunk_size=2)
reader.read()
assert reader.tell() == 4
reader.seek(1)
assert reader.tell() == 1
assert np.array_equal(reader.read(), expected[1:5])
with pytest.raises(IndexError):
reader.seek(11)
def test_context_manager_closes_worker_files(frame_file):
path, expected = frame_file
with MultiThreadedFileReader(path, n_threads=2, chunk_size=2) as reader:
assert not reader.closed
assert np.array_equal(reader.read(), expected[:4])
assert reader.closed
reader.close()
with pytest.raises(RuntimeError):
reader.read()
with pytest.raises(RuntimeError):
reader.seek(0)
with pytest.raises(RuntimeError):
with reader:
pass
def test_explicit_zero_frame_limit(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(
path, n_threads=8, chunk_size=3, total_frames=0
)
actual = reader.read()
assert actual.shape == (0, *expected.shape[1:])
assert actual.dtype == expected.dtype
@pytest.mark.parametrize(
"dtype", [np.int8, np.int32, np.uint64, np.float32, np.float64]
)
def test_preserves_numpy_dtype(tmp_path, dtype):
expected = np.arange(4 * 2 * 3, dtype=dtype).reshape(4, 2, 3)
path = tmp_path / "typed-frames.npy"
np.save(path, expected)
reader = MultiThreadedFileReader(path, n_threads=2, chunk_size=3)
actual = reader.read()
assert actual.dtype == expected.dtype
assert reader.dtype == expected.dtype
assert np.array_equal(actual, expected)
@pytest.mark.parametrize(
("n_threads", "chunk_size", "total_frames"),
[(0, 1, None), (1, 0, None), (1, 1, 11)],
)
def test_invalid_configuration(
frame_file, n_threads, chunk_size, total_frames
):
path, _ = frame_file
with pytest.raises(ValueError):
MultiThreadedFileReader(
path,
n_threads=n_threads,
chunk_size=chunk_size,
total_frames=total_frames,
)
+101
View File
@@ -0,0 +1,101 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
import pytest
from aare import PixelHistogram
def _random_frames(rows, cols, n, xmin, xmax, seed=0):
rng = np.random.default_rng(seed)
return [rng.uniform(xmin - 0.25, xmax + 0.25, size=(rows, cols)).astype(np.float64)
for _ in range(n)]
def _reference_hdata(frames, rows, cols, n_bins, xmin, xmax):
expected = np.zeros((rows, cols, n_bins), dtype=np.uint16)
inv_range = n_bins / (xmax - xmin)
for img in frames:
for r in range(rows):
for c in range(cols):
v = float(img[r, c])
if not (xmin <= v < xmax):
continue
b = int((v - xmin) * inv_range)
if b >= n_bins:
b = n_bins - 1
expected[r, c, b] += 1
return expected
def test_async_fill_matches_reference():
rows, cols, n_bins = 5, 7, 8
xmin, xmax = 0.0, 2.0
frames = _random_frames(rows, cols, n=3, xmin=xmin, xmax=xmax, seed=1)
hist = PixelHistogram(rows=rows, cols=cols, n_bins=n_bins, xmin=xmin, xmax=xmax)
for img in frames:
hist.fill_async(img)
np.testing.assert_array_equal(
hist.values(),
_reference_hdata(frames, rows, cols, n_bins, xmin, xmax),
)
def test_fill_async_copies_buffer():
# After fill_async returns, the caller should be free to mutate the
# numpy array without affecting the pending fill.
rows, cols, n_bins = 4, 4, 4
xmin, xmax = 0.0, 1.0
hist = PixelHistogram(rows=rows, cols=cols, n_bins=n_bins, xmin=xmin, xmax=xmax, n_threads=1, max_pending=8)
img = np.full((rows, cols), 0.1, dtype=np.float64) # falls in bin 0
hist.fill_async(img)
# Mutate the original array immediately; this must not affect the
# value that was already enqueued.
img[:] = 0.9 # would be bin 3
hist.flush()
h = hist.values()
assert h.shape == (rows, cols, n_bins)
# Every pixel saw one value in bin 0, none elsewhere.
assert (h[:, :, 0] == 1).all()
assert (h[:, :, 1:] == 0).all()
def test_fill_async_rejects_wrong_shape():
hist = PixelHistogram(8, 8, 4, 0.0, 1.0)
bad = np.zeros((4, 4), dtype=np.float32)
with pytest.raises(ValueError):
hist.fill_async(bad)
def test_hdata_flushes_pending():
# Submit several frames with a tiny queue and read hdata() without an
# explicit flush(); hdata() must drain everything first.
rows, cols, n_bins = 3, 3, 4
xmin, xmax = 0.0, 1.0
hist = PixelHistogram(rows=rows, cols=cols, n_bins=n_bins, xmin=xmin, xmax=xmax,
n_threads=1, max_pending=1)
frames = _random_frames(rows, cols, n=8, xmin=xmin, xmax=xmax, seed=3)
for img in frames:
hist.fill_async(img)
h = hist.values() # no explicit flush()
np.testing.assert_array_equal(
h, _reference_hdata(frames, rows, cols, n_bins, xmin, xmax)
)
def test_bin_centers_and_edges():
n_bins = 5
xmin, xmax = 0.0, 1.0
hist = PixelHistogram(rows=2, cols=2, n_bins=n_bins, xmin=xmin, xmax=xmax)
edges = hist.bin_edges()
centers = hist.bin_centers()
assert edges.shape == (n_bins + 1,)
assert centers.shape == (n_bins,)
np.testing.assert_allclose(edges, np.linspace(xmin, xmax, n_bins + 1), atol=1e-6)
np.testing.assert_allclose(centers, 0.5 * (edges[:-1] + edges[1:]), atol=1e-6)