From cae4b251d745261b0fc1b5be5e29e7f5745e203a Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Fri, 7 Nov 2025 16:55:22 -0700 Subject: [PATCH 1/6] Added `optika.sensors.vmr_diffusion()` to compute the variance-to-mean ratio due to charge diffusion. --- optika/sensors/__init__.py | 2 + optika/sensors/materials/_diffusion.py | 70 ++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/optika/sensors/__init__.py b/optika/sensors/__init__.py index 1db9a1a..28e3d77 100644 --- a/optika/sensors/__init__.py +++ b/optika/sensors/__init__.py @@ -6,6 +6,7 @@ from .materials._diffusion import ( charge_diffusion, mean_charge_capture, + vmr_diffusion, kernel_diffusion, ) from .materials._materials import ( @@ -48,6 +49,7 @@ "electrons_measured_approx", "signal", "vmr_signal", + "vmr_diffusion", "materials", "AbstractImagingSensor", "ImagingSensor", diff --git a/optika/sensors/materials/_diffusion.py b/optika/sensors/materials/_diffusion.py index 600ee19..11f370d 100644 --- a/optika/sensors/materials/_diffusion.py +++ b/optika/sensors/materials/_diffusion.py @@ -7,6 +7,7 @@ "charge_diffusion", "mean_charge_capture", "kernel_diffusion", + "vmr_diffusion", ] @@ -416,3 +417,72 @@ def kernel_diffusion( inputs=na.Cartesian2dVectorArray(index_x, index_y), outputs=result, ) + + +def vmr_diffusion( + vmr_flat: u.Quantity | na.AbstractScalar, + mcc: u.Quantity | na.AbstractScalar, +) -> na.AbstractScalar: + """ + Compute the variance-to-mean (VMR) ratio of a flat-field image with a given + VMR and mean charge capture (MCC). + + Parameters + ---------- + vmr_flat + The variance-to-mean ratio of a flat-field image in the absence of + charge diffusion. + Intended to be computed with :func:`~optika.sensors.vmr_signal`. + mcc + The mean charge capture of the charge diffusion kernel calculated + using :func:`~optika.sensors.mean_charge_capture`. + + Notes + ----- + + Given a flat-field image :math:`a(x, y)`, + we can represent the blurring due to charge diffusion as + + .. math:: + + b(x, y) = \sum_i \sum_j k_ij a(x + i, y + j), + + where :math:`i` and :math:`j` are the indices of the pixels + and :math:`k_ij` is the charge diffusion kernel.\ + Since the `variance of a linear combination `_ is + + .. math:: + + \text{Var} \left( \sum_i a_i X_i \right) = \text{Var}(X_i) \sum_i a_i^2, + + we can write the variance of the blurred image as + + .. math:: + + \sigma_b^2 = \sigma_a \sum_i \sum_j k_ij^2. + + Because our kernel is separable, :math:`k_ij = k_i k_j`, + we can simplify this to + + .. math:: + + \sigma_b^2 = \sigma_a^2 \left( \sum k_i^2 \right)^2. + + In our case, + the charge diffusion has approximately the same scale as a pixel, + so we approximate it using only a :math:`3 \times 3` kernel. + Given that our kernel is also symmetric and unitary, + we can write it in terms of only the MCC, :math:`m`, + so that the variance of the blurred image becomes + + .. math:: + + \sigma_b^2 = \sigma_a \left[ \left( \frac{\sqrt{m} - 1}{2}\right)^2 + m + \left( \frac{\sqrt{m} - 1}{2}\right)^2 \right]^2, + + which can be simplified to + + .. math:: + + \sigma_b^2 = \frac{\sigma_a^2}{4} \left( 3 m - 2 \sqrt{m} + 1 \right). + """ + return vmr_flat * np.square(3 * mcc - 2 * np.sqrt(mcc) + 1) / 4 From a268197ac0f7b2d6de243f2203dbd81c30268b52 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Fri, 7 Nov 2025 19:11:25 -0700 Subject: [PATCH 2/6] doc fixes --- optika/sensors/materials/_diffusion.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/optika/sensors/materials/_diffusion.py b/optika/sensors/materials/_diffusion.py index 11f370d..3aa3144 100644 --- a/optika/sensors/materials/_diffusion.py +++ b/optika/sensors/materials/_diffusion.py @@ -423,7 +423,7 @@ def vmr_diffusion( vmr_flat: u.Quantity | na.AbstractScalar, mcc: u.Quantity | na.AbstractScalar, ) -> na.AbstractScalar: - """ + r""" Compute the variance-to-mean (VMR) ratio of a flat-field image with a given VMR and mean charge capture (MCC). @@ -445,7 +445,7 @@ def vmr_diffusion( .. math:: - b(x, y) = \sum_i \sum_j k_ij a(x + i, y + j), + b(x, y) = \sum_i \sum_j k_{ij} a(x + i, y + j), where :math:`i` and :math:`j` are the indices of the pixels and :math:`k_ij` is the charge diffusion kernel.\ @@ -461,7 +461,7 @@ def vmr_diffusion( \sigma_b^2 = \sigma_a \sum_i \sum_j k_ij^2. - Because our kernel is separable, :math:`k_ij = k_i k_j`, + Because our kernel is separable, :math:`k_{ij} = k_i k_j`, we can simplify this to .. math:: @@ -484,5 +484,12 @@ def vmr_diffusion( .. math:: \sigma_b^2 = \frac{\sigma_a^2}{4} \left( 3 m - 2 \sqrt{m} + 1 \right). + + The mean of the image is unchanged by the blurring operation, + since the kernel is unitary, + so the equation for the VMR is the same as it is for the variance, + + .. math:: + F_b = \frac{F_a^2}{4} \left( 3 m - 2 \sqrt{m} + 1 \right). """ return vmr_flat * np.square(3 * mcc - 2 * np.sqrt(mcc) + 1) / 4 From 273bfa35e4bfe675ac62d43b1d240598c10a0ca9 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 8 Nov 2025 10:00:05 -0700 Subject: [PATCH 3/6] doc fixes --- optika/sensors/materials/_diffusion.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/optika/sensors/materials/_diffusion.py b/optika/sensors/materials/_diffusion.py index 3aa3144..32a9d81 100644 --- a/optika/sensors/materials/_diffusion.py +++ b/optika/sensors/materials/_diffusion.py @@ -445,10 +445,10 @@ def vmr_diffusion( .. math:: - b(x, y) = \sum_i \sum_j k_{ij} a(x + i, y + j), + b(x, y) = \sum_i \sum_j k_{ij} \, a(x + i, y + j), where :math:`i` and :math:`j` are the indices of the pixels - and :math:`k_ij` is the charge diffusion kernel.\ + and :math:`k_{ij}` is the charge diffusion kernel. Since the `variance of a linear combination `_ is .. math:: @@ -459,7 +459,7 @@ def vmr_diffusion( .. math:: - \sigma_b^2 = \sigma_a \sum_i \sum_j k_ij^2. + \sigma_b^2 = \sigma_a^2 \sum_i \sum_j k_{ij}^2. Because our kernel is separable, :math:`k_{ij} = k_i k_j`, we can simplify this to @@ -472,24 +472,24 @@ def vmr_diffusion( the charge diffusion has approximately the same scale as a pixel, so we approximate it using only a :math:`3 \times 3` kernel. Given that our kernel is also symmetric and unitary, - we can write it in terms of only the MCC, :math:`m`, + we can write it in terms of only the MCC, :math:`m^2`, so that the variance of the blurred image becomes .. math:: - \sigma_b^2 = \sigma_a \left[ \left( \frac{\sqrt{m} - 1}{2}\right)^2 + m + \left( \frac{\sqrt{m} - 1}{2}\right)^2 \right]^2, + \sigma_b^2 = \sigma_a^2 \left[ \left( \frac{m - 1}{2}\right)^2 + m + \left( \frac{m - 1}{2}\right)^2 \right]^2, which can be simplified to .. math:: - \sigma_b^2 = \frac{\sigma_a^2}{4} \left( 3 m - 2 \sqrt{m} + 1 \right). + \sigma_b^2 = \frac{\sigma_a^2}{4} \left( 3 m^2 - 2 m + 1 \right)^2. - The mean of the image is unchanged by the blurring operation, - since the kernel is unitary, - so the equation for the VMR is the same as it is for the variance, + Since the kernel is unitary, + the mean of the image is unchanged by the blurring operation + and the equation for the VMR is the same as it is for the variance, .. math:: - F_b = \frac{F_a^2}{4} \left( 3 m - 2 \sqrt{m} + 1 \right). + F_b = \frac{F_a^2}{4} \left( 3 m^2 - 2 m + 1 \right)^2. """ return vmr_flat * np.square(3 * mcc - 2 * np.sqrt(mcc) + 1) / 4 From 3ef04ad93ca2c6aaaffcf7a3e689a6d190348baa Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sat, 8 Nov 2025 20:06:54 -0700 Subject: [PATCH 4/6] slight fix --- optika/sensors/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optika/sensors/__init__.py b/optika/sensors/__init__.py index 28e3d77..33006f8 100644 --- a/optika/sensors/__init__.py +++ b/optika/sensors/__init__.py @@ -6,8 +6,8 @@ from .materials._diffusion import ( charge_diffusion, mean_charge_capture, - vmr_diffusion, kernel_diffusion, + vmr_diffusion, ) from .materials._materials import ( energy_bandgap, From 52d9a20fbccffd562b4b2d073819f68c08e6cd93 Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Sun, 9 Nov 2025 10:40:07 -0700 Subject: [PATCH 5/6] added tests --- optika/sensors/materials/_diffusion_test.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/optika/sensors/materials/_diffusion_test.py b/optika/sensors/materials/_diffusion_test.py index d3e5115..be4ef15 100644 --- a/optika/sensors/materials/_diffusion_test.py +++ b/optika/sensors/materials/_diffusion_test.py @@ -90,3 +90,24 @@ def test_kernel_diffusion( assert isinstance(result.outputs, na.AbstractScalar) assert isinstance(result.inputs, na.Cartesian2dVectorArray) assert np.all(result.outputs.sum(("x", "y")) == 1) + + +@pytest.mark.parametrize( + argnames="vmr_flat", + argvalues=[1], +) +@pytest.mark.parametrize( + argnames="mcc", + argvalues=[0.5], +) +def test_vmr_diffusion( + vmr_flat: u.Quantity | na.AbstractScalar, + mcc: u.Quantity | na.AbstractScalar, +): + result = optika.sensors.vmr_diffusion( + vmr_flat=vmr_flat, + mcc=mcc, + ) + + assert isinstance(na.as_named_array(result), na.AbstractScalar) + assert np.all(result < vmr_flat) From f4b0ee0d96f598ae6f635196e77563d9b1f6f9dd Mon Sep 17 00:00:00 2001 From: Roy Smart Date: Tue, 11 Nov 2025 13:54:50 -0700 Subject: [PATCH 6/6] minor docs tweaks --- optika/sensors/materials/_diffusion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/optika/sensors/materials/_diffusion.py b/optika/sensors/materials/_diffusion.py index 32a9d81..c434b6f 100644 --- a/optika/sensors/materials/_diffusion.py +++ b/optika/sensors/materials/_diffusion.py @@ -490,6 +490,6 @@ def vmr_diffusion( and the equation for the VMR is the same as it is for the variance, .. math:: - F_b = \frac{F_a^2}{4} \left( 3 m^2 - 2 m + 1 \right)^2. + F_b = \frac{F_a}{4} \left( 3 m^2 - 2 m + 1 \right)^2. """ return vmr_flat * np.square(3 * mcc - 2 * np.sqrt(mcc) + 1) / 4