From 9573f924674207481513033a0c15a6e7d6d3b104 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Wed, 23 Aug 2023 10:16:37 +0100 Subject: [PATCH 01/33] Added file with unit tests for slicer averagers based on constructed data distributions with analytical solutions. This file will eventually replace utest_averaging.py --- .../utest_averaging_analytical.py | 891 ++++++++++++++++++ 1 file changed, 891 insertions(+) create mode 100644 test/sasdataloader/utest_averaging_analytical.py diff --git a/test/sasdataloader/utest_averaging_analytical.py b/test/sasdataloader/utest_averaging_analytical.py new file mode 100644 index 0000000..fcf4a75 --- /dev/null +++ b/test/sasdataloader/utest_averaging_analytical.py @@ -0,0 +1,891 @@ +""" +This file contains unit tests for the various averagers found in +sasdata/data_util/manipulations.py - These tests are based on analytical +formulae rather than imported data files. +""" + +import unittest +from unittest.mock import patch + +import numpy as np +from scipy import integrate + +from sasdata.dataloader import data_info +from sasdata.data_util.manipulations import (_Slab, SlabX, SlabY, Boxsum, + Boxavg, CircularAverage, Ring, + _Sector, SectorQ, SectorPhi) + + +class MatrixToData2D: + """ + Create Data2D objects from supplied 2D arrays of data. + Error data can also be included. + + Adapted from sasdata.data_util.manipulations.reader_2D_converter + """ + + def __init__(self, data2d=None, err_data=None): + if data2d is None: + msg = "Data must be supplied to convert to Data2D" + raise ValueError(msg) + else: + matrix = np.asarray(data2d) + + if matrix.ndim != 2: + msg = "Supplied array must have 2 dimensions to convert to Data2D" + raise ValueError(msg) + + if err_data is not None: + err_data = np.asarray(err_data) + if err_data.shape != matrix.shape: + msg = "Data and errors must have the same shape" + raise ValueError(msg) + + # qmax can be any number, 1 just makes things simple. + self.qmax = 1 + qx_bins = np.linspace(start=-1 * self.qmax, + stop=self.qmax, + num=matrix.shape[1], + endpoint=True) + qy_bins = np.linspace(start=-1 * self.qmax, + stop=self.qmax, + num=matrix.shape[0], + endpoint=True) + + # Creating arrays in Data2D's preferred format. + data2d = matrix.flatten() + if err_data is None or np.any(err_data <= 0): + # Error data of some kind is needed, so we fabricate some + err_data = np.sqrt(np.abs(data2d)) # TODO - use different approach + else: + err_data = err_data.flatten() + qx_data = np.tile(qx_bins, (len(qy_bins), 1)).flatten() + qy_data = np.tile(qy_bins, (len(qx_bins), 1)).swapaxes(0, 1).flatten() + q_data = np.sqrt(qx_data * qx_data + qy_data * qy_data) + mask = np.ones(len(data2d), dtype=bool) + + # Creating a Data2D object to use for testing the averagers. + self.data = data_info.Data2D(data=data2d, err_data=err_data, + qx_data=qx_data, qy_data=qy_data, + q_data=q_data, mask=mask) + + +class CircularTestingMatrix: + """ + This class is used to generate a 2D array representing a function in polar + coordinates. The function, f(r, φ) = R(r) * Φ(φ), factorises into simple + radial and angular parts. This makes it easy to determine the form of the + function after one of the parts has been averaged over, and therefore good + for testing the directional averagers in manipulations.py. + This testing is done by comparing the area under the functions, as these + will only match if the limits defining the ROI were applied correctly. + + f(r, φ) = R(r) * Φ(φ) + R(r) = r ; where 0 <= r <= 1. + Φ(φ) = 1 + sin(ν * φ) ; where ν is the frequency and 0 <= φ <= 2π. + """ + + def __init__(self, frequency=1, matrix_size=201, major_axis=None): + """ + :param frequency: No. times Φ(φ) oscillates over the 0 <= φ <= 2π range + This parameter is largely arbitrary. + :param matrix_size: The len() of the output matrix. + Note that odd numbers give a centrepoint of 0,0. + :param major_axis: 'Q' or 'Phi' - the axis plotted against by the + averager being tested. + """ + if major_axis not in ('Q', 'Phi'): + msg = "Major axis must be either 'Q' or 'Phi'." + raise ValueError(msg) + + self.freq = frequency + self.matrix_size = matrix_size + self.major = major_axis + + # Grid with same dimensions as data matrix, ranging from -1 to 1 + x, y = np.meshgrid(np.linspace(-1, 1, self.matrix_size), + np.linspace(-1, 1, self.matrix_size)) + # radius is 0 at the centre, and 1 at (0, +/-1) and (+/-1, 0) + radius = np.sqrt(x**2 + y**2) + angle = np.arctan2(y, x) + # Create the 2D array of data + # The sinusoidal part is shifted up by 1 so its average is never 0 + self.matrix = radius * (1 + np.sin(self.freq * angle)) + + def area_under_region(self, r_min=0, r_max=1, phi_min=0, phi_max=2*np.pi): + """ + Integral of the testing matrix along the major axis, between the limits + specified. This can be compared to the integral under the 1D data + output by the averager being tested to confirm it's working properly. + + :param r_min: value defining the minimum Q in the ROI. + :param r_max: value defining the maximum Q in the ROI. + :param phi_min: value defining the minimum Phi in the ROI. + :param phi_max: value defining the maximum Phi in the ROI. + """ + + phi_range = phi_max - phi_min + # ∫(1 + sin(ν * φ)) dφ = φ + (-cos(ν * φ) / ν) + constant. + sine_part_integ = phi_range - (np.cos(self.freq * phi_max) - + np.cos(self.freq * phi_min)) / self.freq + sine_part_avg = sine_part_integ / phi_range + + # ∫(r) dr = r²/2 + constant. + linear_part_integ = (r_max ** 2 - r_min ** 2) / 2 + # The average radius is weighted towards higher radii. The probability + # of a point having a given radius value is proportional to the radius: + # P(r) = k * r ; where k is some proportionality constant. + # ∫[r₀, r₁] P(r) dr = 1, which can be solved for k. This can then be + # substituted into ⟨r⟩ = ∫[r₀, r₁] P(r) * r dr, giving: + linear_part_avg = 2/3 * (r_max**3 - r_min**3) / (r_max**2 - r_min**2) + + # The integral along the major axis is modulated by the average value + # along the minor axis (between the limits). + if self.major == 'Q': + calculated_area = sine_part_avg * linear_part_integ + else: + calculated_area = linear_part_avg * sine_part_integ + + return calculated_area + + +class SlabTests(unittest.TestCase): + """ + This class contains all the unit tests for the _Slab class from + manipulations.py + """ + + def test_slab_init(self): + """ + Test that _Slab's __init__ method does what it's supposed to. + """ + x_min = 1 + x_max = 2 + y_min = 3 + y_max = 4 + bin_width = 0.1 + fold = True + + slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, + bin_width=bin_width, fold=fold) + + self.assertEqual(slab_object.x_min, x_min) + self.assertEqual(slab_object.x_max, x_max) + self.assertEqual(slab_object.y_min, y_min) + self.assertEqual(slab_object.y_max, y_max) + self.assertEqual(slab_object.bin_width, bin_width) + self.assertEqual(slab_object.fold, fold) + + def test_slab_multiple_detectors(self): + """ + Test that _Slab raises an error when there are multiple detectors + """ + averager_data = MatrixToData2D(np.ones([100, 100])) + detector1 = data_info.Detector() + detector2 = data_info.Detector() + averager_data.data.detector.append(detector1) + averager_data.data.detector.append(detector2) + + slab_object = _Slab() + self.assertRaises(RuntimeError, slab_object._avg, averager_data.data, 'x') + + def test_slab_unknown_axis(self): + """ + Test that _Slab raises an error when given an invalid major axis + """ + averager_data = MatrixToData2D(np.ones([100, 100])) + major = 'neither_x_nor_y' + + slab_object = _Slab() + self.assertRaises(RuntimeError, slab_object._avg, averager_data.data, major) + + def test_slab_no_points_to_average(self): + """ + Test _Slab raises ValueError when the ROI contains no data + """ + test_data = np.ones([100, 100]) + averager_data = MatrixToData2D(data2d=test_data) + + # Default params for _Slab are all zeros. Effectively, there is no ROI. + slab_object = _Slab() + self.assertRaises(ValueError, slab_object._avg, averager_data.data, 'x') + + def test_slab_averaging_x_without_fold(self): + """ + Test that _Slab can average correctly when x is the major axis + """ + matrix_size = 201 + x, y = np.meshgrid(np.linspace(-1, 1, matrix_size), + np.linspace(-1, 1, matrix_size)) + # Create a distribution which is quadratic in x and linear in y + test_data = x**2 * y + averager_data = MatrixToData2D(data2d=test_data) + + # Set up region of interest to average over - the limits are arbitrary. + x_min = -0.5 * averager_data.qmax # = -0.5 + x_max = averager_data.qmax # = 1 + y_min = -0.5 * averager_data.qmax # = -0.5 + y_max = averager_data.qmax # = 1 + bin_width = (x_max - x_min) / matrix_size + # Explicitly not using fold in this test + fold = False + + slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, + bin_width=bin_width, fold=fold) + data1d = slab_object._avg(averager_data.data, maj='x') + + # ∫x² dx = x³ / 3 + constant. + x_part_integ = (x_max**3 - x_min**3) / 3 + # ∫y dy = y² / 2 + constant. + y_part_integ = (y_max**2 - y_min**2) / 2 + y_part_avg = y_part_integ / (y_max - y_min) + expected_area = y_part_avg * x_part_integ + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 2) + + # TODO - also check the errors are being calculated correctly + + def test_slab_averaging_y_without_fold(self): + """ + Test that _Slab can average correctly when y is the major axis + """ + matrix_size = 201 + x, y = np.meshgrid(np.linspace(-1, 1, matrix_size), + np.linspace(-1, 1, matrix_size)) + # Create a distribution which is linear in x and quadratic in y + test_data = x * y**2 + averager_data = MatrixToData2D(data2d=test_data) + + # Set up region of interest to average over - the limits are arbitrary. + x_min = -0.5 * averager_data.qmax # = -0.5 + x_max = averager_data.qmax # = 1 + y_min = -0.5 * averager_data.qmax # = -0.5 + y_max = averager_data.qmax # = 1 + bin_width = (y_max - y_min) / matrix_size + # Explicitly not using fold in this test + fold = False + + slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, + bin_width=bin_width, fold=fold) + data1d = slab_object._avg(averager_data.data, maj='y') + + # ∫x dx = x² / 2 + constant. + x_part_integ = (x_max**2 - x_min**2) / 2 + x_part_avg = x_part_integ / (x_max - x_min) # or (x_min + x_max) / 2 + # ∫y² dy = y³ / 3 + constant. + y_part_integ = (y_max**3 - y_min**3) / 3 + expected_area = x_part_avg * y_part_integ + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 2) + + # TODO - also check the errors are being calculated correctly + + def test_slab_averaging_x_with_fold(self): + """ + Test that _Slab can average correctly when x is the major axis + """ + matrix_size = 201 + x, y = np.meshgrid(np.linspace(-1, 1, matrix_size), + np.linspace(-1, 1, matrix_size)) + # Create a distribution which is quadratic in x and linear in y + test_data = x**2 * y + averager_data = MatrixToData2D(data2d=test_data) + + # Set up region of interest to average over - the limits are arbitrary. + x_min = -0.5 * averager_data.qmax # = -0.5 + x_max = averager_data.qmax # = 1 + y_min = -0.5 * averager_data.qmax # = -0.5 + y_max = averager_data.qmax # = 1 + bin_width = (x_max - x_min) / matrix_size + # Explicitly using fold in this test + fold = True + + slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, + bin_width=bin_width, fold=fold) + data1d = slab_object._avg(averager_data.data, maj='x') + + # Negative values of x are not graphed when fold = True + x_min = 0 + # ∫x² dx = x³ / 3 + constant. + x_part_integ = (x_max**3 - x_min**3) / 3 + # ∫y dy = y² / 2 + constant. + y_part_integ = (y_max**2 - y_min**2) / 2 + y_part_avg = y_part_integ / (y_max - y_min) + expected_area = y_part_avg * x_part_integ + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 2) + + # TODO - also check the errors are being calculated correctly + + def test_slab_averaging_y_with_fold(self): + """ + Test that _Slab can average correctly when y is the major axis + """ + matrix_size = 201 + x, y = np.meshgrid(np.linspace(-1, 1, matrix_size), + np.linspace(-1, 1, matrix_size)) + # Create a distribution which is linear in x and quadratic in y + test_data = x * y**2 + averager_data = MatrixToData2D(data2d=test_data) + + # Set up region of interest to average over - the limits are arbitrary. + x_min = -0.5 * averager_data.qmax # = -0.5 + x_max = averager_data.qmax # = 1 + y_min = -0.5 * averager_data.qmax # = -0.5 + y_max = averager_data.qmax # = 1 + bin_width = (y_max - y_min) / matrix_size + # Explicitly using fold in this test + fold = True + + slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, + bin_width=bin_width, fold=fold) + data1d = slab_object._avg(averager_data.data, maj='y') + + # Negative values of y are not graphed when fold = True, so don't + # include them in the area calculation. + y_min = 0 + # ∫x dx = x² / 2 + constant. + x_part_integ = (x_max**2 - x_min**2) / 2 + x_part_avg = x_part_integ / (x_max - x_min) # or (x_min + x_max) / 2 + # ∫y² dy = y³ / 3 + constant. + y_part_integ = (y_max**3 - y_min**3) / 3 + expected_area = x_part_avg * y_part_integ + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 2) + + # TODO - also check the errors are being calculated correctly + + +class BoxsumTests(unittest.TestCase): + """ + This class contains all the unit tests for the Boxsum class from + manipulations.py + """ + + def test_boxsum_init(self): + """ + Test that Boxsum's __init__ method does what it's supposed to. + """ + x_min = 1 + x_max = 2 + y_min = 3 + y_max = 4 + + box_object = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + + self.assertEqual(box_object.x_min, x_min) + self.assertEqual(box_object.x_max, x_max) + self.assertEqual(box_object.y_min, y_min) + self.assertEqual(box_object.y_max, y_max) + + def test_boxsum_multiple_detectors(self): + """ + Test Boxsum raises an error when there are multiple detectors. + """ + averager_data = MatrixToData2D(np.ones([100, 100])) + detector1 = data_info.Detector() + detector2 = data_info.Detector() + averager_data.data.detector.append(detector1) + averager_data.data.detector.append(detector2) + + box_object = Boxsum() + self.assertRaises(RuntimeError, box_object, averager_data.data) + + def test_boxsum_total(self): + """ + Test that Boxsum can find the sum of all of a data set + """ + # Creating a 100x100 matrix for a distribution which is flat in y + # and linear in x. + test_data = np.tile(np.arange(100), (100, 1)) + averager_data = MatrixToData2D(data2d=test_data) + + # Selected region is entire data set + x_min = -1 * averager_data.qmax + x_max = averager_data.qmax + y_min = -1 * averager_data.qmax + y_max = averager_data.qmax + box_object = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + result, error, npoints = box_object(averager_data.data) + correct_sum = np.sum(test_data) + # When averager_data was created, we didn't include any error data. + # Stand-in error data is created, equal to np.sqrt(data2D). + # With the current method of error calculation, this is the result we + # should expect. This may need to change at some point. + correct_error = np.sqrt(np.sum(test_data)) + + self.assertAlmostEqual(result, correct_sum, 6) + self.assertAlmostEqual(error, correct_error, 6) + + def test_boxsum_subset_total(self): + """ + Test that Boxsum can find the sum of a portion of a data set + """ + # Creating a 100x100 matrix for a distribution which is flat in y + # and linear in x. + test_data = np.tile(np.arange(100), (100, 1)) + averager_data = MatrixToData2D(data2d=test_data) + + # Selection region covers the inner half of the +&- x&y axes + x_min = -0.5 * averager_data.qmax + x_max = 0.5 * averager_data.qmax + y_min = -0.5 * averager_data.qmax + y_max = 0.5 * averager_data.qmax + # Extracting the inner half of the data set + inner_portion = test_data[25:75, 25:75] + + box_object = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + result, error, npoints = box_object(averager_data.data) + correct_sum = np.sum(inner_portion) + # When averager_data was created, we didn't include any error data. + # Stand-in error data is created, equal to np.sqrt(data2D). + # With the current method of error calculation, this is the result we + # should expect. This may need to change at some point. + correct_error = np.sqrt(np.sum(inner_portion)) + + self.assertAlmostEqual(result, correct_sum, 6) + self.assertAlmostEqual(error, correct_error, 6) + + def test_boxsum_zero_sum(self): + """ + Test that Boxsum returns 0 when there are no points within the ROI + """ + test_data = np.ones([100, 100]) + # Make a hole in the middle with zeros + test_data[25:75, 25:75] = np.zeros([50, 50]) + averager_data = MatrixToData2D(data2d=test_data) + + # Selection region covers the inner half of the +&- x&y axes + x_min = -0.5 * averager_data.qmax + x_max = 0.5 * averager_data.qmax + y_min = -0.5 * averager_data.qmax + y_max = 0.5 * averager_data.qmax + box_object = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + result, error, npoints = box_object(averager_data.data) + + self.assertAlmostEqual(result, 0, 6) + self.assertAlmostEqual(error, 0, 6) + + +class BoxavgTests(unittest.TestCase): + """ + This class contains all the unit tests for the Boxavg class from + manipulations.py + """ + + def test_boxavg_init(self): + """ + Test that Boxavg's __init__ method does what it's supposed to. + """ + x_min = 1 + x_max = 2 + y_min = 3 + y_max = 4 + + box_object = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + + self.assertEqual(box_object.x_min, x_min) + self.assertEqual(box_object.x_max, x_max) + self.assertEqual(box_object.y_min, y_min) + self.assertEqual(box_object.y_max, y_max) + + def test_boxavg_multiple_detectors(self): + """ + Test Boxavg raises an error when there are multiple detectors. + """ + averager_data = MatrixToData2D(np.ones([100, 100])) + detector1 = data_info.Detector() + detector2 = data_info.Detector() + averager_data.data.detector.append(detector1) + averager_data.data.detector.append(detector2) + + box_object = Boxavg() + self.assertRaises(RuntimeError, box_object, averager_data.data) + + def test_boxavg_total(self): + """ + Test that Boxavg can find the average of all of a data set + """ + # Creating a 100x100 matrix for a distribution which is flat in y + # and linear in x. + test_data = np.tile(np.arange(100), (100, 1)) + averager_data = MatrixToData2D(data2d=test_data) + + # Selected region is entire data set + x_min = -1 * averager_data.qmax + x_max = averager_data.qmax + y_min = -1 * averager_data.qmax + y_max = averager_data.qmax + box_object = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + result, error = box_object(averager_data.data) + correct_avg = np.mean(test_data) + # When averager_data was created, we didn't include any error data. + # Stand-in error data is created, equal to np.sqrt(data2D). + # With the current method of error calculation, this is the result we + # should expect. This may need to change at some point. + correct_error = np.sqrt(np.sum(test_data)) / test_data.size + + self.assertAlmostEqual(result, correct_avg, 6) + self.assertAlmostEqual(error, correct_error, 6) + + def test_boxavg_subset_total(self): + """ + Test that Boxavg can find the average of a portion of a data set + """ + # Creating a 100x100 matrix for a distribution which is flat in y + # and linear in x. + test_data = np.tile(np.arange(100), (100, 1)) + averager_data = MatrixToData2D(data2d=test_data) + + # Selection region covers the inner half of the +&- x&y axes + x_min = -0.5 * averager_data.qmax + x_max = 0.5 * averager_data.qmax + y_min = -0.5 * averager_data.qmax + y_max = 0.5 * averager_data.qmax + # Extracting the inner half of the data set + inner_portion = test_data[25:75, 25:75] + + box_object = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + result, error = box_object(averager_data.data) + correct_avg = np.mean(inner_portion) + # When averager_data was created, we didn't include any error data. + # Stand-in error data is created, equal to np.sqrt(data2D). + # With the current method of error calculation, this is the result we + # should expect. This may need to change at some point. + correct_error = np.sqrt(np.sum(inner_portion)) / inner_portion.size + + self.assertAlmostEqual(result, correct_avg, 6) + self.assertAlmostEqual(error, correct_error, 6) + + def test_boxavg_zero_average(self): + """ + Test that Boxavg returns 0 when there are no points within the ROI + """ + test_data = np.ones([100, 100]) + # Make a hole in the middle with zeros + test_data[25:75, 25:75] = np.zeros([50, 50]) + averager_data = MatrixToData2D(data2d=test_data) + + # Selection region covers the inner half of the +&- x&y axes + x_min = -0.5 * averager_data.qmax + x_max = 0.5 * averager_data.qmax + y_min = -0.5 * averager_data.qmax + y_max = 0.5 * averager_data.qmax + box_object = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + result, error = box_object(averager_data.data) + + self.assertAlmostEqual(result, 0, 6) + self.assertAlmostEqual(error, 0, 6) + + +class CircularAverageTests(unittest.TestCase): + """ + This class contains all the tests for the CircularAverage class + from manipulations.py + """ + + def test_circularaverage_init(self): + """ + Test that CircularAverage's __init__ method does what it's supposed to. + """ + r_min = 1 + r_max = 2 + bin_width = 0.01 + + circ_object = CircularAverage(r_min=r_min, r_max=r_max, + bin_width=bin_width) + + self.assertEqual(circ_object.r_min, r_min) + self.assertEqual(circ_object.r_max, r_max) + self.assertEqual(circ_object.bin_width, bin_width) + + def test_circularaverage_dq_retrieval(self): + """ + Test that CircularAverage is able to calclate dq_data correctly when + the data provided has dqx_data and dqy_data. + """ + + # I'm saving the implementation of this bit for later + pass + + def test_circularaverage_multiple_detectors(self): + """ + Test CircularAverage raises an error when there are multiple detectors + """ + + # This test can't be implemented yet, because CircularAverage does not + # check the number of detectors. + # TODO - establish whether CircularAverage should be making this check. + pass + + def test_circularaverage_check_q_data(self): + """ + Check CircularAverage ensures the data supplied has `q_data` populated + """ + # test_data = np.ones([100, 100]) + # averager_data = DataMatrixToData2D(test_data) + # # Overwrite q_data so it's empty + # averager_data.data.q_data = np.array([]) + # circ_object = CircularAverage() + # self.assertRaises(RuntimeError, circ_object, averager_data.data) + + # This doesn't work. I'll come back to this later too + pass + + def test_circularaverage_check_valid_radii(self): + """ + Test that CircularAverage raises ValueError when r_min > r_max + """ + test_data = np.ones([100, 100]) + averager_data = MatrixToData2D(test_data) + + circ_object = CircularAverage(r_min=0.1, r_max=0.05) + self.assertRaises(ValueError, circ_object, averager_data.data) + + def test_circularaverage_no_points_to_average(self): + """ + Test CircularAverage raises ValueError when the ROI contains no data + """ + test_data = np.ones([100, 100]) + averager_data = MatrixToData2D(test_data) + + # Region of interest well outside region with data + circ_object = CircularAverage(r_min=2 * averager_data.qmax, + r_max=3 * averager_data.qmax) + self.assertRaises(ValueError, circ_object, averager_data.data) + + def test_circularaverage_averages_circularly(self): + """ + Test that CircularAverage can calculate a circular average correctly. + """ + test_data = CircularTestingMatrix(frequency=2, major_axis='Q') + averager_data = MatrixToData2D(test_data.matrix) + + # Test the ability to average over a subsection of the data + r_min = averager_data.qmax * 0.25 + r_max = averager_data.qmax * 0.75 + + circ_object = CircularAverage(r_min=r_min, r_max=r_max) + data1d = circ_object(averager_data.data) + + expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max) + actual_area = integrate.trapezoid(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 3) + + # TODO - also check the errors are being calculated correctly + + +class RingTests(unittest.TestCase): + """ + This class contains the tests for the Ring class from manipulations.py + A.K.A AnnulusSlicer on the sasview side + """ + + def test_ring_init(self): + """ + Test that Ring's __init__ method does what it's supposed to. + """ + r_min = 1 + r_max = 2 + nbins = 100 + + # Note that Ring also has params center_x and center_y, but these are + # not used by the slicers and there is a 'todo' in manipulations.py to + # remove them. For this reason, I have not tested their initialisation. + ring_object = Ring(r_min=r_min, r_max=r_max, nbins=nbins) + + self.assertEqual(ring_object.r_min, r_min) + self.assertEqual(ring_object.r_max, r_max) + self.assertEqual(ring_object.nbins_phi, nbins) + + def test_ring_non_plottable_data(self): + """ + Test that RuntimeError is raised if the data supplied isn't plottable + """ + # with patch("sasdata.data_util.manipulations.Ring.data2D.__class__.__name__") as p: + # p.return_value = "bad_name" + # ring_object = Ring() + # self.assertRaises(RuntimeError, ring_object.__call__) + + # I can't seem to get patch working, in this test or in others. + pass + + def test_ring_no_points_to_average(self): + """ + Test Ring raises ValueError when the ROI contains no data + """ + test_data = np.ones([100, 100]) + averager_data = MatrixToData2D(test_data) + + # Region of interest well outside region with data + ring_object = Ring(r_min=2 * averager_data.qmax, + r_max=3 * averager_data.qmax) + self.assertRaises(ValueError, ring_object, averager_data.data) + + def test_ring_averages_azimuthally(self): + """ + Test that Ring can calculate an azimuthal average correctly. + """ + test_data = CircularTestingMatrix(frequency=1, matrix_size=201, + major_axis='Phi') + averager_data = MatrixToData2D(test_data.matrix) + + # Test the ability to average over a subsection of the data + r_min = 0.25 * averager_data.qmax + r_max = 0.75 * averager_data.qmax + nbins = int(test_data.matrix_size / 2) + + ring_object = Ring(r_min=r_min, r_max=r_max, nbins=nbins) + data1d = ring_object(averager_data.data) + + expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max) + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 1) + + # TODO - also check the errors are being calculated correctly + + +class SectorTests(unittest.TestCase): + """ + This class contains the tests for the _Sector class from manipulations.py + On the sasview side, this includes SectorSlicer and WedgeSlicer. + + The parameters frequency, r_min, r_max, phi_min and phi_max are largely + arbitrary, and the tests should pass if any sane value is used for them. + """ + + def test_sector_init(self): + """ + Test that _Sector's __init__ method does what it's supposed to. + """ + r_min = 1 + r_max = 2 + phi_min = 0 + phi_max = np.pi + nbins = 100 + base = 10 + + sector_object = _Sector(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins, base=base) + + self.assertEqual(sector_object.r_min, r_min) + self.assertEqual(sector_object.r_max, r_max) + self.assertEqual(sector_object.phi_min, phi_min) + self.assertEqual(sector_object.phi_max, phi_max) + self.assertEqual(sector_object.nbins, nbins) + self.assertEqual(sector_object.base, base) + + def test_sector_non_plottable_data(self): + """ + Test that RuntimeError is raised if the data supplied isn't plottable + """ + # Implementing this test can wait + pass + + def test_sector_phi_averaging(self): + """ + Test _Sector can average correctly with a major axis of phi, when all + of min/max r & phi params are specified and have their expected form. + """ + test_data = CircularTestingMatrix(frequency=1, matrix_size=201, + major_axis='Phi') + averager_data = MatrixToData2D(test_data.matrix) + + r_min = 0.1 * averager_data.qmax + r_max = 0.9 * averager_data.qmax + phi_min = np.pi/6 + phi_max = 5*np.pi/6 + nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable + + wedge_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min + np.pi, + phi_max=phi_max + np.pi, nbins=nbins) + data1d = wedge_object(averager_data.data) + + expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max, + phi_min=phi_min, + phi_max=phi_max) + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 1) + + # TODO - Something is very wrong with this test + + def test_sector_q_averaging_without_fold(self): + """ + Test _Sector can average correctly w/ major axis q and fold disabled. + All min/max r & phi params are specified and have their expected form. + """ + test_data = CircularTestingMatrix(frequency=1, matrix_size=201, + major_axis='Q') + averager_data = MatrixToData2D(test_data.matrix) + + r_min = 0.1 * averager_data.qmax + r_max = 0.9 * averager_data.qmax + phi_min = np.pi/6 + phi_max = 5*np.pi/6 + nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable + + wedge_object = SectorQ(r_min=r_min, r_max=r_max, phi_min=phi_min + np.pi, + phi_max=phi_max + np.pi, nbins=nbins) + # Explicitly set fold to False - results span full +/- range + wedge_object.fold = False + data1d = wedge_object(averager_data.data) + + expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max, + phi_min=phi_min, + phi_max=phi_max) + # With fold set to False, the sector on the opposite side of the origin + # to the one specified is also graphed as negative Q values. Therefore, + # the area of this other half needs to be accounted for. + expected_area += test_data.area_under_region(r_min=r_min, r_max=r_max, + phi_min=phi_min+np.pi, + phi_max=phi_max+np.pi) + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 1) + + def test_sector_q_averaging_with_fold(self): + """ + Test _Sector can average correctly w/ major axis q and fold enabled. + All min/max r & phi params are specified and have their expected form. + """ + test_data = CircularTestingMatrix(frequency=1, matrix_size=201, + major_axis='Q') + averager_data = MatrixToData2D(test_data.matrix) + + r_min = 0.1 * averager_data.qmax + r_max = 0.9 * averager_data.qmax + phi_min = np.pi/6 + phi_max = 5*np.pi/6 + nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable + + wedge_object = SectorQ(r_min=r_min, r_max=r_max, + phi_min=phi_min + np.pi, + phi_max=phi_max + np.pi, nbins=nbins) + # Explicitly set fold to True - points either side of 0,0 are averaged + wedge_object.fold = True + data1d = wedge_object(averager_data.data) + + expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max, + phi_min=phi_min, + phi_max=phi_max) + # With fold set to True, points from the sector on the opposite side of + # the origin to the one specified are averaged with points from the + # specified sector. + expected_area += test_data.area_under_region(r_min=r_min, r_max=r_max, + phi_min=phi_min+np.pi, + phi_max=phi_max+np.pi) + expected_area /= 2 + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 1) + + +if __name__ == '__main__': + unittest.main() From a5cb0a444ee84c8517aabfdbc6934cbc08677f00 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Thu, 31 Aug 2023 11:03:02 +0100 Subject: [PATCH 02/33] Initial version of averagers with cartesian ROI, and made corresponding changes to the unit tests --- sasdata/data_util/new_manipulations.py | 324 ++++++++++++++++++ .../utest_averaging_analytical.py | 240 +++++++------ 2 files changed, 450 insertions(+), 114 deletions(-) create mode 100644 sasdata/data_util/new_manipulations.py diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py new file mode 100644 index 0000000..591b668 --- /dev/null +++ b/sasdata/data_util/new_manipulations.py @@ -0,0 +1,324 @@ +import numpy as np +from abc import ABC, abstractmethod +from typing import Union + +from sasdata.dataloader.data_info import Data1D, Data2D + + +class Binning: + """ + """ + + def __init__(self, min_value, max_value, nbins, base=None): + """ + """ + self.minimum = min_value + self.maximum = max_value + self.nbins = nbins + self.base = base + + def get_index(self, value): + """ + """ + if self.base: + numerator = (np.log(value) - np.log(self.minimum)) \ + / np.log(self.base) + denominator = (np.log(self.maximum) - np.log(self.minimum)) \ + / np.log(self.base) + else: + numerator = value - self.minimum + denominator = self.maximum - self.minimum + + bin_index = int(np.floor(self.nbins * numerator / denominator)) + + # Bins are indexed from 0 to nbins-1, so this check protects against + # out-of-range indices when value == maximum. + if bin_index == self.nbins: + bin_index -= 1 + + return bin_index + + +class CartesianROI(ABC): + """ + Base class for manipulators with a rectangular region of interest. + """ + + def __init__(self, qx_min: float = 0, qx_max: float = 0, + qy_min: float = 0, qy_max: float = 0) -> None: + """ + Placeholder + """ + + # Units A^-1 + self.qx_min = qx_min + self.qx_max = qx_max + self.qy_min = qy_min + self.qy_max = qy_max + + # Define data related variables + self.data = None + self.err_data = None + self.qx_data = None + self.qy_data = None + self.mask_data = None + + @abstractmethod + def __call__(self, data2d: Data2D = None) -> Union[float, Data1D]: + """ + Placeholder + """ + return + + # This method might be better placed in a parent class + def validate_and_assign_data(self, data2d: Data2D = None) -> None: + """ + Check that the data supplied valid and assign data variables. + """ + if not isinstance(data2d, Data2D): + msg = "Data supplied must be of type Data2D." + raise TypeError(msg) + if len(data2d.detector) > 1: + msg = f"Invalid number of detectors: {len(data2d.detector)}" + raise ValueError(msg) + + finite_data = np.isfinite(data2d.data) + self.data = data2d.data[finite_data] + self.err_data = data2d.err_data[finite_data] + self.qx_data = data2d.qx_data[finite_data] + self.qy_data = data2d.qy_data[finite_data] + self.mask_data = data2d.mask[finite_data] + + @property + def roi_mask(self): + """ + Return a boolean array listing the elements of self.data which are + inside the ROI. This property should only be accessed after + CartesianROI has been called. + """ + if any(data is None for data in [self.qx_data, self.qy_data, + self.mask_data]): + raise RuntimeError + + within_x_lims = (self.qx_data >= self.qx_min) & \ + (self.qx_data <= self.qx_max) + within_y_lims = (self.qy_data >= self.qy_min) & \ + (self.qy_data <= self.qy_max) + + # Don't return masked-off data + return within_x_lims & within_y_lims & self.mask_data + + +class PolarROI(ABC): + """ + Base class for manipulators whose ROI is defined with polar coordinates. + """ + + def __init__(self, r_min: float = 0, r_max: float = 1000, + phi_min: float = 0, phi_max: float = 2*np.pi) -> None: + """ + Placeholder + """ + + # Units A^-1 for radii, radians for angles + self.r_min = r_min + self.r_max = r_max + self.phi_min = phi_min + self.phi_max = phi_max + + # Define data related variables + self.data = None + self.err_data = None + self.q_data = None + self.qx_data = None + self.qy_data = None + self.mask_data = None + + @abstractmethod + def __call__(self, data2d: Data2D = None) -> Union[float, Data1D]: + """ + Placeholder + """ + return + + # This method might be better placed in a parent class + def validate_and_assign_data(self, data2d: Data2D = None) -> None: + """ + Check that the data supplied valid and assign data variables. + """ + if not isinstance(data2d, Data2D): + msg = "Data supplied must be of type Data2D." + raise TypeError(msg) + if len(data2d.detector) > 1: + msg = f"Invalid number of detectors: {len(data2d.detector)}" + raise ValueError(msg) + + finite_data = np.isfinite(data2d.data) + self.data = data2d.data[finite_data] + self.err_data = data2d.err_data[finite_data] + self.q_data = data2d.q_data[finite_data] + self.qx_data = data2d.qx_data[finite_data] + self.qy_data = data2d.qy_data[finite_data] + self.mask_data = data2d.mask[finite_data] + + +class Boxsum(CartesianROI): + """ + Perform the sum of counts in a 2D region of interest. + """ + + def __init__(self, qx_min: float = 0, qx_max: float = 0, + qy_min: float = 0, qy_max: float = 0) -> None: + super().__init__(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) + + def __call__(self, data2d: Data2D = None) -> float: + """ + Placeholder + """ + self.validate_and_assign_data(data2d) + total_sum, error, count = self._sum() + + return total_sum, error, count + + def _sum(self) -> float: + """ + Placeholder + """ + + # Currently the weights are binary, but could be fractional in future + weights = self.roi_mask.astype(int) + + data = weights * self.data + err_squared = weights * weights * self.err_data * self.err_data + # No points should have zero error, if they do then assume the worst + err_squared[self.err_data == 0] = (weights * data)[self.err_data == 0] + + total_sum = np.sum(data) + total_count = np.sum(weights) + total_errors_squared = np.sum(err_squared) + + return total_sum, np.sqrt(total_errors_squared), total_count + + +class Boxavg(Boxsum): + """ + Perform the average of counts in a 2D region of interest. + """ + + def __init__(self, qx_min: float = 0, qx_max: float = 0, + qy_min: float = 0, qy_max: float = 0) -> None: + super().__init__(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) + + def __call__(self, data2d: Data2D) -> float: + """ + Placeholder + """ + self.validate_and_assign_data(data2d) + total_sum, error, count = super()._sum() + + return (total_sum / count), (error / count) + + +class _Slab(CartesianROI): + """ + Compute average I(Q) for a region of interest + """ + + def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, + qy_max: float = 0, nbins: int = 100, fold: bool = False): + super().__init__(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) + self.nbins = nbins + self.fold = fold + + def __call__(self, data2d: Data2D = None) -> Data1D: + pass + + def _avg(self, data2d: Data2D, major_axis: str) -> Data1D: + """ + Placeholder + """ + self.validate_and_assign_data(data2d) + + # TODO - change weights + weights = self.roi_mask.astype(int) + + if major_axis == 'x': + q_major = self.qx_data + binning = Binning(min_value=0 if self.fold else self.qx_min, + max_value=self.qx_max, nbins=self.nbins) + elif major_axis == 'y': + q_major = self.qy_data + binning = Binning(min_value=0 if self.fold else self.qy_min, + max_value=self.qy_max, nbins=self.nbins) + else: + msg = f"Unrecognised axis: {major_axis}" + raise ValueError(msg) + + q_values = np.zeros(self.nbins) + intensity = np.zeros(self.nbins) + errs_squared = np.zeros(self.nbins) + bin_counts = np.zeros(self.nbins) + + for index, q_value in enumerate(q_major): + # Skip over datapoints with no relevance + # This should include masked datapoints. + if weights[index] == 0: + continue + + if self.fold and q_value < 0: + q_value = -q_value + + q_bin = binning.get_index(q_value) + q_values[q_bin] += weights[index] * q_value + intensity[q_bin] += weights[index] * self.data[index] + errs_squared[q_bin] += (weights[index] * self.err_data[index]) ** 2 + # No points should have zero error, assume the worst if they do + if self.err_data[index] == 0.0: + errs_squared[q_bin] += weights[index] ** 2 * abs(self.data[index]) + else: + errs_squared[q_bin] += (weights[index] * self.err_data[index]) ** 2 + bin_counts[q_bin] += weights[index] + + errors = np.sqrt(errs_squared) + q_values /= bin_counts + intensity /= bin_counts + errors /= bin_counts + + finite = (np.isfinite(q_values) & np.isfinite(intensity)) + if not finite.any(): + msg = "Average Error: No points inside ROI to average..." + raise ValueError(msg) + + return Data1D(x=q_values[finite], y=intensity[finite], dy=errors[finite]) + + +class SlabX(_Slab): + """ + Compute average I(Qx) for a region of interest + """ + + def __call__(self, data2d: Data2D = None) -> Data1D: + """ + Compute average I(Qx) for a region of interest + :param data2d: Data2D object + :return: Data1D object + """ + return self._avg(data2d, 'x') + + +class SlabY(_Slab): + """ + Compute average I(Qy) for a region of interest + """ + + def __call__(self, data2d: Data2D = None) -> Data1D: + """ + Compute average I(Qy) for a region of interest + :param data2d: Data2D object + :return: Data1D object + """ + return self._avg(data2d, 'y') + diff --git a/test/sasdataloader/utest_averaging_analytical.py b/test/sasdataloader/utest_averaging_analytical.py index fcf4a75..2d850e5 100644 --- a/test/sasdataloader/utest_averaging_analytical.py +++ b/test/sasdataloader/utest_averaging_analytical.py @@ -11,9 +11,9 @@ from scipy import integrate from sasdata.dataloader import data_info -from sasdata.data_util.manipulations import (_Slab, SlabX, SlabY, Boxsum, - Boxavg, CircularAverage, Ring, - _Sector, SectorQ, SectorPhi) +from sasdata.data_util.manipulations import (CircularAverage, Ring, _Sector, + SectorQ, SectorPhi) +from sasdata.data_util.new_manipulations import _Slab, SlabX, SlabY, Boxsum, Boxavg class MatrixToData2D: @@ -159,21 +159,21 @@ def test_slab_init(self): """ Test that _Slab's __init__ method does what it's supposed to. """ - x_min = 1 - x_max = 2 - y_min = 3 - y_max = 4 - bin_width = 0.1 + qx_min = 1 + qx_max = 2 + qy_min = 3 + qy_max = 4 + nbins = 100 fold = True - slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, - bin_width=bin_width, fold=fold) + slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + qy_max=qy_max, nbins=nbins, fold=fold) - self.assertEqual(slab_object.x_min, x_min) - self.assertEqual(slab_object.x_max, x_max) - self.assertEqual(slab_object.y_min, y_min) - self.assertEqual(slab_object.y_max, y_max) - self.assertEqual(slab_object.bin_width, bin_width) + self.assertEqual(slab_object.qx_min, qx_min) + self.assertEqual(slab_object.qx_max, qx_max) + self.assertEqual(slab_object.qy_min, qy_min) + self.assertEqual(slab_object.qy_max, qy_max) + self.assertEqual(slab_object.nbins, nbins) self.assertEqual(slab_object.fold, fold) def test_slab_multiple_detectors(self): @@ -187,7 +187,7 @@ def test_slab_multiple_detectors(self): averager_data.data.detector.append(detector2) slab_object = _Slab() - self.assertRaises(RuntimeError, slab_object._avg, averager_data.data, 'x') + self.assertRaises(ValueError, slab_object._avg, averager_data.data, 'x') def test_slab_unknown_axis(self): """ @@ -197,7 +197,7 @@ def test_slab_unknown_axis(self): major = 'neither_x_nor_y' slab_object = _Slab() - self.assertRaises(RuntimeError, slab_object._avg, averager_data.data, major) + self.assertRaises(ValueError, slab_object._avg, averager_data.data, major) def test_slab_no_points_to_average(self): """ @@ -222,23 +222,23 @@ def test_slab_averaging_x_without_fold(self): averager_data = MatrixToData2D(data2d=test_data) # Set up region of interest to average over - the limits are arbitrary. - x_min = -0.5 * averager_data.qmax # = -0.5 - x_max = averager_data.qmax # = 1 - y_min = -0.5 * averager_data.qmax # = -0.5 - y_max = averager_data.qmax # = 1 - bin_width = (x_max - x_min) / matrix_size + qx_min = -0.5 * averager_data.qmax # = -0.5 + qx_max = averager_data.qmax # = 1 + qy_min = -0.5 * averager_data.qmax # = -0.5 + qy_max = averager_data.qmax # = 1 + nbins = int((qx_max - qx_min) / 2 * matrix_size) # Explicitly not using fold in this test fold = False - slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, - bin_width=bin_width, fold=fold) - data1d = slab_object._avg(averager_data.data, maj='x') + slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + qy_max=qy_max, nbins=nbins, fold=fold) + data1d = slab_object._avg(averager_data.data, major_axis='x') # ∫x² dx = x³ / 3 + constant. - x_part_integ = (x_max**3 - x_min**3) / 3 + x_part_integ = (qx_max**3 - qx_min**3) / 3 # ∫y dy = y² / 2 + constant. - y_part_integ = (y_max**2 - y_min**2) / 2 - y_part_avg = y_part_integ / (y_max - y_min) + y_part_integ = (qy_max**2 - qy_min**2) / 2 + y_part_avg = y_part_integ / (qy_max - qy_min) expected_area = y_part_avg * x_part_integ actual_area = integrate.simpson(data1d.y, data1d.x) @@ -258,23 +258,23 @@ def test_slab_averaging_y_without_fold(self): averager_data = MatrixToData2D(data2d=test_data) # Set up region of interest to average over - the limits are arbitrary. - x_min = -0.5 * averager_data.qmax # = -0.5 - x_max = averager_data.qmax # = 1 - y_min = -0.5 * averager_data.qmax # = -0.5 - y_max = averager_data.qmax # = 1 - bin_width = (y_max - y_min) / matrix_size + qx_min = -0.5 * averager_data.qmax # = -0.5 + qx_max = averager_data.qmax # = 1 + qy_min = -0.5 * averager_data.qmax # = -0.5 + qy_max = averager_data.qmax # = 1 + nbins = int((qx_max - qx_min) / 2 * matrix_size) # Explicitly not using fold in this test fold = False - slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, - bin_width=bin_width, fold=fold) - data1d = slab_object._avg(averager_data.data, maj='y') + slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + qy_max=qy_max, nbins=nbins, fold=fold) + data1d = slab_object._avg(averager_data.data, major_axis='y') # ∫x dx = x² / 2 + constant. - x_part_integ = (x_max**2 - x_min**2) / 2 - x_part_avg = x_part_integ / (x_max - x_min) # or (x_min + x_max) / 2 + x_part_integ = (qx_max**2 - qx_min**2) / 2 + x_part_avg = x_part_integ / (qx_max - qx_min) # or (x_min + x_max) / 2 # ∫y² dy = y³ / 3 + constant. - y_part_integ = (y_max**3 - y_min**3) / 3 + y_part_integ = (qy_max**3 - qy_min**3) / 3 expected_area = x_part_avg * y_part_integ actual_area = integrate.simpson(data1d.y, data1d.x) @@ -294,25 +294,25 @@ def test_slab_averaging_x_with_fold(self): averager_data = MatrixToData2D(data2d=test_data) # Set up region of interest to average over - the limits are arbitrary. - x_min = -0.5 * averager_data.qmax # = -0.5 - x_max = averager_data.qmax # = 1 - y_min = -0.5 * averager_data.qmax # = -0.5 - y_max = averager_data.qmax # = 1 - bin_width = (x_max - x_min) / matrix_size + qx_min = -0.5 * averager_data.qmax # = -0.5 + qx_max = averager_data.qmax # = 1 + qy_min = -0.5 * averager_data.qmax # = -0.5 + qy_max = averager_data.qmax # = 1 + nbins = int((qx_max - qx_min) / 2 * matrix_size) # Explicitly using fold in this test fold = True - slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, - bin_width=bin_width, fold=fold) - data1d = slab_object._avg(averager_data.data, maj='x') + slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + qy_max=qy_max, nbins=nbins, fold=fold) + data1d = slab_object._avg(averager_data.data, major_axis='x') # Negative values of x are not graphed when fold = True - x_min = 0 + qx_min = 0 # ∫x² dx = x³ / 3 + constant. - x_part_integ = (x_max**3 - x_min**3) / 3 + x_part_integ = (qx_max**3 - qx_min**3) / 3 # ∫y dy = y² / 2 + constant. - y_part_integ = (y_max**2 - y_min**2) / 2 - y_part_avg = y_part_integ / (y_max - y_min) + y_part_integ = (qy_max**2 - qy_min**2) / 2 + y_part_avg = y_part_integ / (qy_max - qy_min) expected_area = y_part_avg * x_part_integ actual_area = integrate.simpson(data1d.y, data1d.x) @@ -332,26 +332,26 @@ def test_slab_averaging_y_with_fold(self): averager_data = MatrixToData2D(data2d=test_data) # Set up region of interest to average over - the limits are arbitrary. - x_min = -0.5 * averager_data.qmax # = -0.5 - x_max = averager_data.qmax # = 1 - y_min = -0.5 * averager_data.qmax # = -0.5 - y_max = averager_data.qmax # = 1 - bin_width = (y_max - y_min) / matrix_size + qx_min = -0.5 * averager_data.qmax # = -0.5 + qx_max = averager_data.qmax # = 1 + qy_min = -0.5 * averager_data.qmax # = -0.5 + qy_max = averager_data.qmax # = 1 + nbins = int((qx_max - qx_min) / 2 * matrix_size) # Explicitly using fold in this test fold = True - slab_object = _Slab(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max, - bin_width=bin_width, fold=fold) - data1d = slab_object._avg(averager_data.data, maj='y') + slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + qy_max=qy_max, nbins=nbins, fold=fold) + data1d = slab_object._avg(averager_data.data, major_axis='y') # Negative values of y are not graphed when fold = True, so don't # include them in the area calculation. - y_min = 0 + qy_min = 0 # ∫x dx = x² / 2 + constant. - x_part_integ = (x_max**2 - x_min**2) / 2 - x_part_avg = x_part_integ / (x_max - x_min) # or (x_min + x_max) / 2 + x_part_integ = (qx_max**2 - qx_min**2) / 2 + x_part_avg = x_part_integ / (qx_max - qx_min) # or (x_min + x_max) / 2 # ∫y² dy = y³ / 3 + constant. - y_part_integ = (y_max**3 - y_min**3) / 3 + y_part_integ = (qy_max**3 - qy_min**3) / 3 expected_area = x_part_avg * y_part_integ actual_area = integrate.simpson(data1d.y, data1d.x) @@ -370,17 +370,18 @@ def test_boxsum_init(self): """ Test that Boxsum's __init__ method does what it's supposed to. """ - x_min = 1 - x_max = 2 - y_min = 3 - y_max = 4 + qx_min = 1 + qx_max = 2 + qy_min = 3 + qy_max = 4 - box_object = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + box_object = Boxsum(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) - self.assertEqual(box_object.x_min, x_min) - self.assertEqual(box_object.x_max, x_max) - self.assertEqual(box_object.y_min, y_min) - self.assertEqual(box_object.y_max, y_max) + self.assertEqual(box_object.qx_min, qx_min) + self.assertEqual(box_object.qx_max, qx_max) + self.assertEqual(box_object.qy_min, qy_min) + self.assertEqual(box_object.qy_max, qy_max) def test_boxsum_multiple_detectors(self): """ @@ -393,7 +394,7 @@ def test_boxsum_multiple_detectors(self): averager_data.data.detector.append(detector2) box_object = Boxsum() - self.assertRaises(RuntimeError, box_object, averager_data.data) + self.assertRaises(ValueError, box_object, averager_data.data) def test_boxsum_total(self): """ @@ -405,11 +406,12 @@ def test_boxsum_total(self): averager_data = MatrixToData2D(data2d=test_data) # Selected region is entire data set - x_min = -1 * averager_data.qmax - x_max = averager_data.qmax - y_min = -1 * averager_data.qmax - y_max = averager_data.qmax - box_object = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + qx_min = -1 * averager_data.qmax + qx_max = averager_data.qmax + qy_min = -1 * averager_data.qmax + qy_max = averager_data.qmax + box_object = Boxsum(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) result, error, npoints = box_object(averager_data.data) correct_sum = np.sum(test_data) # When averager_data was created, we didn't include any error data. @@ -431,14 +433,15 @@ def test_boxsum_subset_total(self): averager_data = MatrixToData2D(data2d=test_data) # Selection region covers the inner half of the +&- x&y axes - x_min = -0.5 * averager_data.qmax - x_max = 0.5 * averager_data.qmax - y_min = -0.5 * averager_data.qmax - y_max = 0.5 * averager_data.qmax + qx_min = -0.5 * averager_data.qmax + qx_max = 0.5 * averager_data.qmax + qy_min = -0.5 * averager_data.qmax + qy_max = 0.5 * averager_data.qmax # Extracting the inner half of the data set inner_portion = test_data[25:75, 25:75] - box_object = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + box_object = Boxsum(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) result, error, npoints = box_object(averager_data.data) correct_sum = np.sum(inner_portion) # When averager_data was created, we didn't include any error data. @@ -460,11 +463,12 @@ def test_boxsum_zero_sum(self): averager_data = MatrixToData2D(data2d=test_data) # Selection region covers the inner half of the +&- x&y axes - x_min = -0.5 * averager_data.qmax - x_max = 0.5 * averager_data.qmax - y_min = -0.5 * averager_data.qmax - y_max = 0.5 * averager_data.qmax - box_object = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + qx_min = -0.5 * averager_data.qmax + qx_max = 0.5 * averager_data.qmax + qy_min = -0.5 * averager_data.qmax + qy_max = 0.5 * averager_data.qmax + box_object = Boxsum(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) result, error, npoints = box_object(averager_data.data) self.assertAlmostEqual(result, 0, 6) @@ -481,17 +485,18 @@ def test_boxavg_init(self): """ Test that Boxavg's __init__ method does what it's supposed to. """ - x_min = 1 - x_max = 2 - y_min = 3 - y_max = 4 + qx_min = 1 + qx_max = 2 + qy_min = 3 + qy_max = 4 - box_object = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + box_object = Boxavg(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) - self.assertEqual(box_object.x_min, x_min) - self.assertEqual(box_object.x_max, x_max) - self.assertEqual(box_object.y_min, y_min) - self.assertEqual(box_object.y_max, y_max) + self.assertEqual(box_object.qx_min, qx_min) + self.assertEqual(box_object.qx_max, qx_max) + self.assertEqual(box_object.qy_min, qy_min) + self.assertEqual(box_object.qy_max, qy_max) def test_boxavg_multiple_detectors(self): """ @@ -504,7 +509,7 @@ def test_boxavg_multiple_detectors(self): averager_data.data.detector.append(detector2) box_object = Boxavg() - self.assertRaises(RuntimeError, box_object, averager_data.data) + self.assertRaises(ValueError, box_object, averager_data.data) def test_boxavg_total(self): """ @@ -516,11 +521,12 @@ def test_boxavg_total(self): averager_data = MatrixToData2D(data2d=test_data) # Selected region is entire data set - x_min = -1 * averager_data.qmax - x_max = averager_data.qmax - y_min = -1 * averager_data.qmax - y_max = averager_data.qmax - box_object = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + qx_min = -1 * averager_data.qmax + qx_max = averager_data.qmax + qy_min = -1 * averager_data.qmax + qy_max = averager_data.qmax + box_object = Boxavg(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) result, error = box_object(averager_data.data) correct_avg = np.mean(test_data) # When averager_data was created, we didn't include any error data. @@ -542,14 +548,15 @@ def test_boxavg_subset_total(self): averager_data = MatrixToData2D(data2d=test_data) # Selection region covers the inner half of the +&- x&y axes - x_min = -0.5 * averager_data.qmax - x_max = 0.5 * averager_data.qmax - y_min = -0.5 * averager_data.qmax - y_max = 0.5 * averager_data.qmax + qx_min = -0.5 * averager_data.qmax + qx_max = 0.5 * averager_data.qmax + qy_min = -0.5 * averager_data.qmax + qy_max = 0.5 * averager_data.qmax # Extracting the inner half of the data set inner_portion = test_data[25:75, 25:75] - box_object = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + box_object = Boxavg(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) result, error = box_object(averager_data.data) correct_avg = np.mean(inner_portion) # When averager_data was created, we didn't include any error data. @@ -571,11 +578,12 @@ def test_boxavg_zero_average(self): averager_data = MatrixToData2D(data2d=test_data) # Selection region covers the inner half of the +&- x&y axes - x_min = -0.5 * averager_data.qmax - x_max = 0.5 * averager_data.qmax - y_min = -0.5 * averager_data.qmax - y_max = 0.5 * averager_data.qmax - box_object = Boxavg(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) + qx_min = -0.5 * averager_data.qmax + qx_max = 0.5 * averager_data.qmax + qy_min = -0.5 * averager_data.qmax + qy_max = 0.5 * averager_data.qmax + box_object = Boxavg(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) result, error = box_object(averager_data.data) self.assertAlmostEqual(result, 0, 6) @@ -594,14 +602,18 @@ def test_circularaverage_init(self): """ r_min = 1 r_max = 2 - bin_width = 0.01 + bin_width = 0.001 + # nbins = 100 circ_object = CircularAverage(r_min=r_min, r_max=r_max, bin_width=bin_width) + # circ_object = CircularAverage(r_min=r_min, r_max=r_max, + # nbins=nbins) self.assertEqual(circ_object.r_min, r_min) self.assertEqual(circ_object.r_max, r_max) self.assertEqual(circ_object.bin_width, bin_width) + # self.assertEqual(circ_object.nbins, nbins) def test_circularaverage_dq_retrieval(self): """ From b8423bac86fc4b363bd5bed4bdfebf9904adb58e Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 1 Sep 2023 07:29:30 +0100 Subject: [PATCH 03/33] Changed the binning/weighting process for _Slab to make the fractional binning upgrade easier --- sasdata/data_util/new_manipulations.py | 116 +++++++++++++++++-------- 1 file changed, 78 insertions(+), 38 deletions(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index 591b668..82caf57 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -1,12 +1,32 @@ -import numpy as np from abc import ABC, abstractmethod from typing import Union +import numpy as np + from sasdata.dataloader.data_info import Data1D, Data2D +def weights_for_interval(array, l_bound, u_bound, interval_type='half-open'): + """ + If and when fractional binning is implemented (ask Lucas), this function + will be changed so that instead of outputting zeros and ones, it gives + fractional values instead. These will depend on how close the array value + is to being within the interval defined. + """ + if interval_type == 'half-open': + in_range = (l_bound <= array) & (array < u_bound) + elif interval_type == 'closed': + in_range = (l_bound <= array) & (array <= u_bound) + else: + msg = f"Unrecognised interval_type: {interval_type}" + raise ValueError(msg) + + return np.asarray(in_range, dtype=int) + + class Binning: """ + TODO - add docstring """ def __init__(self, min_value, max_value, nbins, base=None): @@ -16,8 +36,9 @@ def __init__(self, min_value, max_value, nbins, base=None): self.maximum = max_value self.nbins = nbins self.base = base + self.bin_width = (max_value - min_value) / nbins - def get_index(self, value): + def get_index(self, value: float) -> int: """ """ if self.base: @@ -38,6 +59,14 @@ def get_index(self, value): return bin_index + def get_interval(self, bin_number: int) -> float: + """ + """ + start = self.minimum + self.bin_width * bin_number + stop = self.minimum + self.bin_width * (bin_number + 1) + + return start, stop + class CartesianROI(ABC): """ @@ -47,7 +76,7 @@ class CartesianROI(ABC): def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0) -> None: """ - Placeholder + TODO - add docstring """ # Units A^-1 @@ -66,7 +95,7 @@ def __init__(self, qx_min: float = 0, qx_max: float = 0, @abstractmethod def __call__(self, data2d: Data2D = None) -> Union[float, Data1D]: """ - Placeholder + TODO - add docstring """ return @@ -89,6 +118,11 @@ def validate_and_assign_data(self, data2d: Data2D = None) -> None: self.qy_data = data2d.qy_data[finite_data] self.mask_data = data2d.mask[finite_data] + # No points should have zero error, if they do then assume the error is + # the square root of the data. + self.err_data[self.err_data == 0] = \ + np.sqrt(np.abs(self.data[self.err_data == 0])) + @property def roi_mask(self): """ @@ -117,7 +151,7 @@ class PolarROI(ABC): def __init__(self, r_min: float = 0, r_max: float = 1000, phi_min: float = 0, phi_max: float = 2*np.pi) -> None: """ - Placeholder + TODO - add docstring """ # Units A^-1 for radii, radians for angles @@ -137,7 +171,7 @@ def __init__(self, r_min: float = 0, r_max: float = 1000, @abstractmethod def __call__(self, data2d: Data2D = None) -> Union[float, Data1D]: """ - Placeholder + TODO - add docstring """ return @@ -161,6 +195,11 @@ def validate_and_assign_data(self, data2d: Data2D = None) -> None: self.qy_data = data2d.qy_data[finite_data] self.mask_data = data2d.mask[finite_data] + # No points should have zero error, if they do then assume the error is + # the square root of the data. + self.err_data[self.err_data == 0] = \ + np.sqrt(np.abs(self.data[self.err_data == 0])) + class Boxsum(CartesianROI): """ @@ -183,7 +222,7 @@ def __call__(self, data2d: Data2D = None) -> float: def _sum(self) -> float: """ - Placeholder + TODO - add docstring """ # Currently the weights are binary, but could be fractional in future @@ -191,12 +230,10 @@ def _sum(self) -> float: data = weights * self.data err_squared = weights * weights * self.err_data * self.err_data - # No points should have zero error, if they do then assume the worst - err_squared[self.err_data == 0] = (weights * data)[self.err_data == 0] total_sum = np.sum(data) - total_count = np.sum(weights) total_errors_squared = np.sum(err_squared) + total_count = np.sum(weights) return total_sum, np.sqrt(total_errors_squared), total_count @@ -213,7 +250,7 @@ def __init__(self, qx_min: float = 0, qx_max: float = 0, def __call__(self, data2d: Data2D) -> float: """ - Placeholder + TODO - add docstring """ self.validate_and_assign_data(data2d) total_sum, error, count = super()._sum() @@ -238,49 +275,52 @@ def __call__(self, data2d: Data2D = None) -> Data1D: def _avg(self, data2d: Data2D, major_axis: str) -> Data1D: """ - Placeholder + TODO - add docstring """ self.validate_and_assign_data(data2d) - # TODO - change weights - weights = self.roi_mask.astype(int) - if major_axis == 'x': q_major = self.qx_data + q_minor = self.qy_data + minor_lims = (self.qy_min, self.qy_max) binning = Binning(min_value=0 if self.fold else self.qx_min, max_value=self.qx_max, nbins=self.nbins) elif major_axis == 'y': q_major = self.qy_data + q_minor = self.qx_data + minor_lims = (self.qx_min, self.qx_max) binning = Binning(min_value=0 if self.fold else self.qy_min, max_value=self.qy_max, nbins=self.nbins) else: msg = f"Unrecognised axis: {major_axis}" raise ValueError(msg) - q_values = np.zeros(self.nbins) - intensity = np.zeros(self.nbins) - errs_squared = np.zeros(self.nbins) - bin_counts = np.zeros(self.nbins) - - for index, q_value in enumerate(q_major): - # Skip over datapoints with no relevance - # This should include masked datapoints. - if weights[index] == 0: - continue - - if self.fold and q_value < 0: - q_value = -q_value - - q_bin = binning.get_index(q_value) - q_values[q_bin] += weights[index] * q_value - intensity[q_bin] += weights[index] * self.data[index] - errs_squared[q_bin] += (weights[index] * self.err_data[index]) ** 2 - # No points should have zero error, assume the worst if they do - if self.err_data[index] == 0.0: - errs_squared[q_bin] += weights[index] ** 2 * abs(self.data[index]) + if self.fold: + q_major = np.abs(q_major) + + major_weights = np.zeros((self.nbins, q_major.size)) + for m in range(self.nbins): + # Include the value at the end of the binning range, but otherwise + # use half-open intervals so each value belongs in only one bin. + if m == self.nbins - 1: + interval = 'closed' else: - errs_squared[q_bin] += (weights[index] * self.err_data[index]) ** 2 - bin_counts[q_bin] += weights[index] + interval = 'half-open' + bin_start, bin_end = binning.get_interval(bin_number=m) + major_weights[m] \ + = weights_for_interval(array=q_major, l_bound=bin_start, + u_bound=bin_end, + interval_type=interval) + minor_weights = weights_for_interval(array=q_minor, + l_bound=minor_lims[0], + u_bound=minor_lims[1], + interval_type='closed') + weights = major_weights * minor_weights + + q_values = np.sum(weights * q_major, axis=1) + intensity = np.sum(weights * self.data, axis=1) + errs_squared = np.sum((weights * self.err_data)**2, axis=1) + bin_counts = np.sum(weights, axis=1) errors = np.sqrt(errs_squared) q_values /= bin_counts From b10edd3729c94305436c96f55a2a994482689820 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Mon, 4 Sep 2023 08:32:49 +0100 Subject: [PATCH 04/33] Refactoring to test SlabX, SlabY, SectorQ and SectorPhi rather than their respective parent classes: _Slab and _Sector (which will be removed soon) --- .../utest_averaging_analytical.py | 298 ++++++++++++------ 1 file changed, 194 insertions(+), 104 deletions(-) diff --git a/test/sasdataloader/utest_averaging_analytical.py b/test/sasdataloader/utest_averaging_analytical.py index 2d850e5..3b2f3ee 100644 --- a/test/sasdataloader/utest_averaging_analytical.py +++ b/test/sasdataloader/utest_averaging_analytical.py @@ -11,9 +11,8 @@ from scipy import integrate from sasdata.dataloader import data_info -from sasdata.data_util.manipulations import (CircularAverage, Ring, _Sector, - SectorQ, SectorPhi) -from sasdata.data_util.new_manipulations import _Slab, SlabX, SlabY, Boxsum, Boxavg +from sasdata.data_util.manipulations import CircularAverage, Ring, SectorQ, SectorPhi +from sasdata.data_util.new_manipulations import SlabX, SlabY, Boxsum, Boxavg class MatrixToData2D: @@ -25,11 +24,11 @@ class MatrixToData2D: """ def __init__(self, data2d=None, err_data=None): - if data2d is None: + if data2d is not None: + matrix = np.asarray(data2d) + else: msg = "Data must be supplied to convert to Data2D" raise ValueError(msg) - else: - matrix = np.asarray(data2d) if matrix.ndim != 2: msg = "Supplied array must have 2 dimensions to convert to Data2D" @@ -149,15 +148,15 @@ def area_under_region(self, r_min=0, r_max=1, phi_min=0, phi_max=2*np.pi): return calculated_area -class SlabTests(unittest.TestCase): +class SlabXTests(unittest.TestCase): """ - This class contains all the unit tests for the _Slab class from + This class contains all the unit tests for the SlabX class from manipulations.py """ - def test_slab_init(self): + def test_slabx_init(self): """ - Test that _Slab's __init__ method does what it's supposed to. + Test that SlabX's __init__ method does what it's supposed to. """ qx_min = 1 qx_max = 2 @@ -166,7 +165,7 @@ def test_slab_init(self): nbins = 100 fold = True - slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + slab_object = SlabX(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max, nbins=nbins, fold=fold) self.assertEqual(slab_object.qx_min, qx_min) @@ -176,9 +175,9 @@ def test_slab_init(self): self.assertEqual(slab_object.nbins, nbins) self.assertEqual(slab_object.fold, fold) - def test_slab_multiple_detectors(self): + def test_slabx_multiple_detectors(self): """ - Test that _Slab raises an error when there are multiple detectors + Test that SlabX raises an error when there are multiple detectors """ averager_data = MatrixToData2D(np.ones([100, 100])) detector1 = data_info.Detector() @@ -186,33 +185,29 @@ def test_slab_multiple_detectors(self): averager_data.data.detector.append(detector1) averager_data.data.detector.append(detector2) - slab_object = _Slab() - self.assertRaises(ValueError, slab_object._avg, averager_data.data, 'x') - - def test_slab_unknown_axis(self): - """ - Test that _Slab raises an error when given an invalid major axis - """ - averager_data = MatrixToData2D(np.ones([100, 100])) - major = 'neither_x_nor_y' - - slab_object = _Slab() - self.assertRaises(ValueError, slab_object._avg, averager_data.data, major) + slab_object = SlabX() + self.assertRaises(ValueError, slab_object, averager_data.data) - def test_slab_no_points_to_average(self): + def test_slabx_no_points_to_average(self): """ - Test _Slab raises ValueError when the ROI contains no data + Test SlabX raises ValueError when the ROI contains no data """ test_data = np.ones([100, 100]) averager_data = MatrixToData2D(data2d=test_data) - # Default params for _Slab are all zeros. Effectively, there is no ROI. - slab_object = _Slab() - self.assertRaises(ValueError, slab_object._avg, averager_data.data, 'x') + # Region of interest well outside region with data + qx_min = 2 * averager_data.qmax + qx_max = 3 * averager_data.qmax + qy_min = 2 * averager_data.qmax + qy_max = 3 * averager_data.qmax + + slab_object = SlabX(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) + self.assertRaises(ValueError, slab_object, averager_data.data) - def test_slab_averaging_x_without_fold(self): + def test_slabx_averaging_without_fold(self): """ - Test that _Slab can average correctly when x is the major axis + Test that SlabX can average correctly when x is the major axis """ matrix_size = 201 x, y = np.meshgrid(np.linspace(-1, 1, matrix_size), @@ -230,9 +225,9 @@ def test_slab_averaging_x_without_fold(self): # Explicitly not using fold in this test fold = False - slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + slab_object = SlabX(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max, nbins=nbins, fold=fold) - data1d = slab_object._avg(averager_data.data, major_axis='x') + data1d = slab_object(averager_data.data) # ∫x² dx = x³ / 3 + constant. x_part_integ = (qx_max**3 - qx_min**3) / 3 @@ -246,15 +241,15 @@ def test_slab_averaging_x_without_fold(self): # TODO - also check the errors are being calculated correctly - def test_slab_averaging_y_without_fold(self): + def test_slabx_averaging_with_fold(self): """ - Test that _Slab can average correctly when y is the major axis + Test that SlabX can average correctly when x is the major axis """ matrix_size = 201 x, y = np.meshgrid(np.linspace(-1, 1, matrix_size), np.linspace(-1, 1, matrix_size)) - # Create a distribution which is linear in x and quadratic in y - test_data = x * y**2 + # Create a distribution which is quadratic in x and linear in y + test_data = x**2 * y averager_data = MatrixToData2D(data2d=test_data) # Set up region of interest to average over - the limits are arbitrary. @@ -263,34 +258,94 @@ def test_slab_averaging_y_without_fold(self): qy_min = -0.5 * averager_data.qmax # = -0.5 qy_max = averager_data.qmax # = 1 nbins = int((qx_max - qx_min) / 2 * matrix_size) - # Explicitly not using fold in this test - fold = False + # Explicitly using fold in this test + fold = True - slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + slab_object = SlabX(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max, nbins=nbins, fold=fold) - data1d = slab_object._avg(averager_data.data, major_axis='y') + data1d = slab_object(averager_data.data) - # ∫x dx = x² / 2 + constant. - x_part_integ = (qx_max**2 - qx_min**2) / 2 - x_part_avg = x_part_integ / (qx_max - qx_min) # or (x_min + x_max) / 2 - # ∫y² dy = y³ / 3 + constant. - y_part_integ = (qy_max**3 - qy_min**3) / 3 - expected_area = x_part_avg * y_part_integ + # Negative values of x are not graphed when fold = True + qx_min = 0 + # ∫x² dx = x³ / 3 + constant. + x_part_integ = (qx_max**3 - qx_min**3) / 3 + # ∫y dy = y² / 2 + constant. + y_part_integ = (qy_max**2 - qy_min**2) / 2 + y_part_avg = y_part_integ / (qy_max - qy_min) + expected_area = y_part_avg * x_part_integ actual_area = integrate.simpson(data1d.y, data1d.x) self.assertAlmostEqual(actual_area, expected_area, 2) # TODO - also check the errors are being calculated correctly - def test_slab_averaging_x_with_fold(self): + +class SlabYTests(unittest.TestCase): + """ + This class contains all the unit tests for the SlabY class from + manipulations.py + """ + + def test_slaby_init(self): + """ + Test that SlabY's __init__ method does what it's supposed to. + """ + qx_min = 1 + qx_max = 2 + qy_min = 3 + qy_max = 4 + nbins = 100 + fold = True + + slab_object = SlabY(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + qy_max=qy_max, nbins=nbins, fold=fold) + + self.assertEqual(slab_object.qx_min, qx_min) + self.assertEqual(slab_object.qx_max, qx_max) + self.assertEqual(slab_object.qy_min, qy_min) + self.assertEqual(slab_object.qy_max, qy_max) + self.assertEqual(slab_object.nbins, nbins) + self.assertEqual(slab_object.fold, fold) + + def test_slaby_multiple_detectors(self): """ - Test that _Slab can average correctly when x is the major axis + Test that SlabY raises an error when there are multiple detectors + """ + averager_data = MatrixToData2D(np.ones([100, 100])) + detector1 = data_info.Detector() + detector2 = data_info.Detector() + averager_data.data.detector.append(detector1) + averager_data.data.detector.append(detector2) + + slab_object = SlabY() + self.assertRaises(ValueError, slab_object, averager_data.data) + + def test_slaby_no_points_to_average(self): + """ + Test SlabY raises ValueError when the ROI contains no data + """ + test_data = np.ones([100, 100]) + averager_data = MatrixToData2D(data2d=test_data) + + # Region of interest well outside region with data + qx_min = 2 * averager_data.qmax + qx_max = 3 * averager_data.qmax + qy_min = 2 * averager_data.qmax + qy_max = 3 * averager_data.qmax + + slab_object = SlabY(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) + self.assertRaises(ValueError, slab_object, averager_data.data) + + def test_slaby_averaging_without_fold(self): + """ + Test that SlabY can average correctly when y is the major axis """ matrix_size = 201 x, y = np.meshgrid(np.linspace(-1, 1, matrix_size), np.linspace(-1, 1, matrix_size)) - # Create a distribution which is quadratic in x and linear in y - test_data = x**2 * y + # Create a distribution which is linear in x and quadratic in y + test_data = x * y**2 averager_data = MatrixToData2D(data2d=test_data) # Set up region of interest to average over - the limits are arbitrary. @@ -299,21 +354,19 @@ def test_slab_averaging_x_with_fold(self): qy_min = -0.5 * averager_data.qmax # = -0.5 qy_max = averager_data.qmax # = 1 nbins = int((qx_max - qx_min) / 2 * matrix_size) - # Explicitly using fold in this test - fold = True + # Explicitly not using fold in this test + fold = False - slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + slab_object = SlabY(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max, nbins=nbins, fold=fold) - data1d = slab_object._avg(averager_data.data, major_axis='x') + data1d = slab_object(averager_data.data) - # Negative values of x are not graphed when fold = True - qx_min = 0 - # ∫x² dx = x³ / 3 + constant. - x_part_integ = (qx_max**3 - qx_min**3) / 3 - # ∫y dy = y² / 2 + constant. - y_part_integ = (qy_max**2 - qy_min**2) / 2 - y_part_avg = y_part_integ / (qy_max - qy_min) - expected_area = y_part_avg * x_part_integ + # ∫x dx = x² / 2 + constant. + x_part_integ = (qx_max**2 - qx_min**2) / 2 + x_part_avg = x_part_integ / (qx_max - qx_min) # or (x_min + x_max) / 2 + # ∫y² dy = y³ / 3 + constant. + y_part_integ = (qy_max**3 - qy_min**3) / 3 + expected_area = x_part_avg * y_part_integ actual_area = integrate.simpson(data1d.y, data1d.x) self.assertAlmostEqual(actual_area, expected_area, 2) @@ -322,7 +375,7 @@ def test_slab_averaging_x_with_fold(self): def test_slab_averaging_y_with_fold(self): """ - Test that _Slab can average correctly when y is the major axis + Test that SlabY can average correctly when y is the major axis """ matrix_size = 201 x, y = np.meshgrid(np.linspace(-1, 1, matrix_size), @@ -340,9 +393,9 @@ def test_slab_averaging_y_with_fold(self): # Explicitly using fold in this test fold = True - slab_object = _Slab(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, + slab_object = SlabY(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max, nbins=nbins, fold=fold) - data1d = slab_object._avg(averager_data.data, major_axis='y') + data1d = slab_object(averager_data.data) # Negative values of y are not graphed when fold = True, so don't # include them in the area calculation. @@ -713,6 +766,7 @@ def test_ring_init(self): self.assertEqual(ring_object.r_min, r_min) self.assertEqual(ring_object.r_max, r_max) + # TODO - replace nbins_phi with nbins for consitency self.assertEqual(ring_object.nbins_phi, nbins) def test_ring_non_plottable_data(self): @@ -763,7 +817,7 @@ def test_ring_averages_azimuthally(self): # TODO - also check the errors are being calculated correctly -class SectorTests(unittest.TestCase): +class SectorQTests(unittest.TestCase): """ This class contains the tests for the _Sector class from manipulations.py On the sasview side, this includes SectorSlicer and WedgeSlicer. @@ -772,9 +826,9 @@ class SectorTests(unittest.TestCase): arbitrary, and the tests should pass if any sane value is used for them. """ - def test_sector_init(self): + def test_sectorq_init(self): """ - Test that _Sector's __init__ method does what it's supposed to. + Test that SectorQ's __init__ method does what it's supposed to. """ r_min = 1 r_max = 2 @@ -783,7 +837,7 @@ def test_sector_init(self): nbins = 100 base = 10 - sector_object = _Sector(r_min=r_min, r_max=r_max, phi_min=phi_min, + sector_object = SectorQ(r_min=r_min, r_max=r_max, phi_min=phi_min, phi_max=phi_max, nbins=nbins, base=base) self.assertEqual(sector_object.r_min, r_min) @@ -793,44 +847,16 @@ def test_sector_init(self): self.assertEqual(sector_object.nbins, nbins) self.assertEqual(sector_object.base, base) - def test_sector_non_plottable_data(self): + def test_sectorq_non_plottable_data(self): """ Test that RuntimeError is raised if the data supplied isn't plottable """ # Implementing this test can wait pass - def test_sector_phi_averaging(self): - """ - Test _Sector can average correctly with a major axis of phi, when all - of min/max r & phi params are specified and have their expected form. + def test_sectorq_averaging_without_fold(self): """ - test_data = CircularTestingMatrix(frequency=1, matrix_size=201, - major_axis='Phi') - averager_data = MatrixToData2D(test_data.matrix) - - r_min = 0.1 * averager_data.qmax - r_max = 0.9 * averager_data.qmax - phi_min = np.pi/6 - phi_max = 5*np.pi/6 - nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable - - wedge_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min + np.pi, - phi_max=phi_max + np.pi, nbins=nbins) - data1d = wedge_object(averager_data.data) - - expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max, - phi_min=phi_min, - phi_max=phi_max) - actual_area = integrate.simpson(data1d.y, data1d.x) - - self.assertAlmostEqual(actual_area, expected_area, 1) - - # TODO - Something is very wrong with this test - - def test_sector_q_averaging_without_fold(self): - """ - Test _Sector can average correctly w/ major axis q and fold disabled. + Test SectorQ can average correctly w/ major axis q and fold disabled. All min/max r & phi params are specified and have their expected form. """ test_data = CircularTestingMatrix(frequency=1, matrix_size=201, @@ -862,9 +888,9 @@ def test_sector_q_averaging_without_fold(self): self.assertAlmostEqual(actual_area, expected_area, 1) - def test_sector_q_averaging_with_fold(self): + def test_sectorq_averaging_with_fold(self): """ - Test _Sector can average correctly w/ major axis q and fold enabled. + Test SectorQ can average correctly w/ major axis q and fold enabled. All min/max r & phi params are specified and have their expected form. """ test_data = CircularTestingMatrix(frequency=1, matrix_size=201, @@ -899,5 +925,69 @@ def test_sector_q_averaging_with_fold(self): self.assertAlmostEqual(actual_area, expected_area, 1) +class SectorPhiTests(unittest.TestCase): + """ + This class contains the tests for the SectorPhi class from manipulations.py + On the sasview side, this includes SectorSlicer and WedgeSlicer. + + The parameters frequency, r_min, r_max, phi_min and phi_max are largely + arbitrary, and the tests should pass if any sane value is used for them. + """ + + def test_sectorphi_init(self): + """ + Test that SectorPhi's __init__ method does what it's supposed to. + """ + r_min = 1 + r_max = 2 + phi_min = 0 + phi_max = np.pi + nbins = 100 + base = 10 + + sector_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins, base=base) + + self.assertEqual(sector_object.r_min, r_min) + self.assertEqual(sector_object.r_max, r_max) + self.assertEqual(sector_object.phi_min, phi_min) + self.assertEqual(sector_object.phi_max, phi_max) + self.assertEqual(sector_object.nbins, nbins) + self.assertEqual(sector_object.base, base) + + def test_sectorphi_non_plottable_data(self): + """ + Test that RuntimeError is raised if the data supplied isn't plottable + """ + # Implementing this test can wait + pass + + def test_sectorphi_averaging(self): + """ + Test _Sector can average correctly with a major axis of phi, when all + of min/max r & phi params are specified and have their expected form. + """ + test_data = CircularTestingMatrix(frequency=1, matrix_size=201, + major_axis='Phi') + averager_data = MatrixToData2D(test_data.matrix) + + r_min = 0.1 * averager_data.qmax + r_max = 0.9 * averager_data.qmax + phi_min = np.pi/6 + phi_max = 5*np.pi/6 + nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable + + wedge_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min + np.pi, + phi_max=phi_max + np.pi, nbins=nbins) + data1d = wedge_object(averager_data.data) + + expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max, + phi_min=phi_min, + phi_max=phi_max) + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 1) + + if __name__ == '__main__': unittest.main() From 7670d2ba330e890d43d0785f71752e89c7df0cbe Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Thu, 7 Sep 2023 11:21:11 +0100 Subject: [PATCH 05/33] Restructured ROI classes and added DirectionalAverage class, which offers a generalised method of calculating the directional average. All the averagers from the old manipulations.py have been rewritten to use the DirecitonalAverage class. --- sasdata/data_util/new_manipulations.py | 584 +++++++++++++++++-------- 1 file changed, 413 insertions(+), 171 deletions(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index 82caf57..b5ee734 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -1,6 +1,3 @@ -from abc import ABC, abstractmethod -from typing import Union - import numpy as np from sasdata.dataloader.data_info import Data1D, Data2D @@ -13,6 +10,8 @@ def weights_for_interval(array, l_bound, u_bound, interval_type='half-open'): fractional values instead. These will depend on how close the array value is to being within the interval defined. """ + # These checks could be modified to return fractional bin weights. + # The last value in the binning range must be included for to pass utests if interval_type == 'half-open': in_range = (l_bound <= array) & (array < u_bound) elif interval_type == 'closed': @@ -24,82 +23,129 @@ def weights_for_interval(array, l_bound, u_bound, interval_type='half-open'): return np.asarray(in_range, dtype=int) -class Binning: +class DirectionalAverage: """ - TODO - add docstring + TODO - write a docstring """ - def __init__(self, min_value, max_value, nbins, base=None): + def __init__(self, major_data, minor_data, major_lims=None, + minor_lims=None, nbins=100): """ """ - self.minimum = min_value - self.maximum = max_value + if any(not isinstance(data, (list, np.ndarray)) for + data in (major_data, minor_data)): + msg = "Must provide major & minor coordinate arrays for binning." + raise ValueError(msg) + if any(lims is not None and len(lims) != 2 for + lims in (major_lims, minor_lims)): + msg = "Limits arrays must have 2 elements or be NoneType" + raise ValueError(msg) + if not isinstance(nbins, int): + msg = "Parameter 'nbins' must be an integer" + raise TypeError(msg) + + self.major_data = np.asarray(major_data) + self.minor_data = np.asarray(minor_data) + if major_lims is None: + self.major_lims = (self.major_data.min(), self.major_data.max()) + else: + self.major_lims = major_lims + if minor_lims is None: + self.minor_lims = (self.minor_data.min(), self.minor_data.max()) + else: + self.minor_lims = minor_lims self.nbins = nbins - self.base = base - self.bin_width = (max_value - min_value) / nbins - def get_index(self, value: float) -> int: + @property + def bin_width(self): """ + Return the bin width """ - if self.base: - numerator = (np.log(value) - np.log(self.minimum)) \ - / np.log(self.base) - denominator = (np.log(self.maximum) - np.log(self.minimum)) \ - / np.log(self.base) - else: - numerator = value - self.minimum - denominator = self.maximum - self.minimum + return (self.major_lims[1] - self.major_lims[0]) / self.nbins + + def get_bin_interval(self, bin_number): + """ + Return the upper and lower limits defining a given bin + """ + bin_start = self.major_lims[0] + bin_number * self.bin_width + bin_end = self.major_lims[0] + (bin_number + 1) * self.bin_width + + return bin_start, bin_end + def get_bin_index(self, value): + """ + """ + numerator = value - self.major_lims[0] + denominator = self.major_lims[1] - self.major_lims[0] bin_index = int(np.floor(self.nbins * numerator / denominator)) - # Bins are indexed from 0 to nbins-1, so this check protects against - # out-of-range indices when value == maximum. + # Bins are indexed from 0 to nbins-1, so tihs check protects against + # out-of-range indices when value == self.major_lims[1] if bin_index == self.nbins: bin_index -= 1 return bin_index - def get_interval(self, bin_number: int) -> float: + def compute_weights(self): """ """ - start = self.minimum + self.bin_width * bin_number - stop = self.minimum + self.bin_width * (bin_number + 1) + major_weights = np.zeros((self.nbins, self.major_data.size)) + for m in range(self.nbins): + # Include the value at the end of the binning range, but in + # general use half-open intervals so each value begins in only + # one bin. + if m == self.nbins - 1: + interval = 'closed' + else: + interval = 'half-open' + bin_start, bin_end = self.get_bin_interval(bin_number=m) + major_weights[m] = weights_for_interval(array=self.major_data, + l_bound=bin_start, + u_bound=bin_end, + interval_type=interval) + minor_weights = weights_for_interval(array=self.minor_data, + l_bound=self.minor_lims[0], + u_bound=self.minor_lims[1], + interval_type='closed') + return major_weights * minor_weights - return start, stop + def __call__(self, data, err_data): + """ + """ + weights = self.compute_weights() + x_axis_values = np.sum(weights * self.major_data, axis=1) + intensity = np.sum(weights * data, axis=1) + errs_squared = np.sum((weights * err_data)**2, axis=1) + bin_counts = np.sum(weights, axis=1) -class CartesianROI(ABC): + errors = np.sqrt(errs_squared) + x_axis_values /= bin_counts + intensity /= bin_counts + errors /= bin_counts + + finite = (np.isfinite(x_axis_values) & np.isfinite(intensity)) + if not finite.any(): + msg = "Average Error: No points inside ROI to average..." + raise ValueError(msg) + + return x_axis_values[finite], intensity[finite], errors[finite] + + +class GenericROI: """ - Base class for manipulators with a rectangular region of interest. + TODO - add docstring """ - def __init__(self, qx_min: float = 0, qx_max: float = 0, - qy_min: float = 0, qy_max: float = 0) -> None: + def __init__(self): """ - TODO - add docstring """ - - # Units A^-1 - self.qx_min = qx_min - self.qx_max = qx_max - self.qy_min = qy_min - self.qy_max = qy_max - - # Define data related variables self.data = None self.err_data = None + self.q_data = None self.qx_data = None self.qy_data = None - self.mask_data = None - - @abstractmethod - def __call__(self, data2d: Data2D = None) -> Union[float, Data1D]: - """ - TODO - add docstring - """ - return - # This method might be better placed in a parent class def validate_and_assign_data(self, data2d: Data2D = None) -> None: """ Check that the data supplied valid and assign data variables. @@ -111,94 +157,70 @@ def validate_and_assign_data(self, data2d: Data2D = None) -> None: msg = f"Invalid number of detectors: {len(data2d.detector)}" raise ValueError(msg) - finite_data = np.isfinite(data2d.data) - self.data = data2d.data[finite_data] - self.err_data = data2d.err_data[finite_data] - self.qx_data = data2d.qx_data[finite_data] - self.qy_data = data2d.qy_data[finite_data] - self.mask_data = data2d.mask[finite_data] + # Only use data which is finite and not masked off + valid_data = np.isfinite(data2d.data) & data2d.mask + + self.data = data2d.data[valid_data] + self.err_data = data2d.err_data[valid_data] + self.q_data = data2d.q_data[valid_data] + self.qx_data = data2d.qx_data[valid_data] + self.qy_data = data2d.qy_data[valid_data] # No points should have zero error, if they do then assume the error is - # the square root of the data. + # the square root of the data. This code was added to replicate + # previous functionality. It's a bit dodgy, so feel free to remove. self.err_data[self.err_data == 0] = \ np.sqrt(np.abs(self.data[self.err_data == 0])) - @property - def roi_mask(self): + +class CartesianROI(GenericROI): + """ + Base class for manipulators with a rectangular region of interest. + """ + + def __init__(self, qx_min: float = 0, qx_max: float = 0, + qy_min: float = 0, qy_max: float = 0) -> None: """ - Return a boolean array listing the elements of self.data which are - inside the ROI. This property should only be accessed after - CartesianROI has been called. + TODO - add docstring """ - if any(data is None for data in [self.qx_data, self.qy_data, - self.mask_data]): - raise RuntimeError - - within_x_lims = (self.qx_data >= self.qx_min) & \ - (self.qx_data <= self.qx_max) - within_y_lims = (self.qy_data >= self.qy_min) & \ - (self.qy_data <= self.qy_max) - # Don't return masked-off data - return within_x_lims & within_y_lims & self.mask_data + super().__init__() + # Units A^-1 + self.qx_min = qx_min + self.qx_max = qx_max + self.qy_min = qy_min + self.qy_max = qy_max -class PolarROI(ABC): +class PolarROI(GenericROI): """ Base class for manipulators whose ROI is defined with polar coordinates. """ - def __init__(self, r_min: float = 0, r_max: float = 1000, + def __init__(self, r_min: float, r_max: float, phi_min: float = 0, phi_max: float = 2*np.pi) -> None: """ TODO - add docstring """ + super().__init__() + self.phi_data = None + + if r_min >= r_max: + msg = "Minimum radius cannot be greater than maximum radius." + raise ValueError(msg) # Units A^-1 for radii, radians for angles self.r_min = r_min self.r_max = r_max self.phi_min = phi_min self.phi_max = phi_max - # Define data related variables - self.data = None - self.err_data = None - self.q_data = None - self.qx_data = None - self.qy_data = None - self.mask_data = None - - @abstractmethod - def __call__(self, data2d: Data2D = None) -> Union[float, Data1D]: - """ - TODO - add docstring - """ - return - - # This method might be better placed in a parent class def validate_and_assign_data(self, data2d: Data2D = None) -> None: """ Check that the data supplied valid and assign data variables. """ - if not isinstance(data2d, Data2D): - msg = "Data supplied must be of type Data2D." - raise TypeError(msg) - if len(data2d.detector) > 1: - msg = f"Invalid number of detectors: {len(data2d.detector)}" - raise ValueError(msg) - - finite_data = np.isfinite(data2d.data) - self.data = data2d.data[finite_data] - self.err_data = data2d.err_data[finite_data] - self.q_data = data2d.q_data[finite_data] - self.qx_data = data2d.qx_data[finite_data] - self.qy_data = data2d.qy_data[finite_data] - self.mask_data = data2d.mask[finite_data] - - # No points should have zero error, if they do then assume the error is - # the square root of the data. - self.err_data[self.err_data == 0] = \ - np.sqrt(np.abs(self.data[self.err_data == 0])) + super().validate_and_assign_data(data2d) + self.phi_data = np.arctan2(self.qy_data, self.qx_data) class Boxsum(CartesianROI): @@ -208,12 +230,15 @@ class Boxsum(CartesianROI): def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0) -> None: + """ + TODO - add docstring + """ super().__init__(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max) def __call__(self, data2d: Data2D = None) -> float: """ - Placeholder + TODO - add docstring """ self.validate_and_assign_data(data2d) total_sum, error, count = self._sum() @@ -226,7 +251,15 @@ def _sum(self) -> float: """ # Currently the weights are binary, but could be fractional in future - weights = self.roi_mask.astype(int) + x_weights = weights_for_interval(array=self.qx_data, + l_bound=self.qx_min, + u_bound=self.qx_max, + interval_type='closed') + y_weights = weights_for_interval(array=self.qy_data, + l_bound=self.qy_min, + u_bound=self.qy_max, + interval_type='closed') + weights = x_weights * y_weights data = weights * self.data err_squared = weights * weights * self.err_data * self.err_data @@ -245,6 +278,9 @@ class Boxavg(Boxsum): def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0) -> None: + """ + TODO - add docstring + """ super().__init__(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max) @@ -258,107 +294,313 @@ def __call__(self, data2d: Data2D) -> float: return (total_sum / count), (error / count) -class _Slab(CartesianROI): +class SlabX(CartesianROI): """ - Compute average I(Q) for a region of interest + Compute average I(Qx) for a region of interest """ def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0, nbins: int = 100, fold: bool = False): + """ + TODO - add docstring + """ super().__init__(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max) self.nbins = nbins self.fold = fold def __call__(self, data2d: Data2D = None) -> Data1D: - pass + """ + Compute average I(Qx) for a region of interest + :param data2d: Data2D object + :return: Data1D object + """ + self.validate_and_assign_data(data2d) + + if self.fold: + major_lims = (0, self.qx_max) + self.qx_data = np.abs(self.qx_data) + else: + major_lims = (self.qx_min, self.qx_max) + minor_lims = (self.qy_min, self.qy_max) + + directional_average = DirectionalAverage(major_data=self.qx_data, + minor_data=self.qy_data, + major_lims=major_lims, + minor_lims=minor_lims, + nbins=self.nbins) + qx_data, intensity, error = \ + directional_average(data=self.data, err_data=self.err_data) + + return Data1D(x=qx_data, y=intensity, dy=error) + + +class SlabY(CartesianROI): + """ + Compute average I(Qy) for a region of interest + """ - def _avg(self, data2d: Data2D, major_axis: str) -> Data1D: + def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, + qy_max: float = 0, nbins: int = 100, fold: bool = False): """ TODO - add docstring """ + super().__init__(qx_min=qx_min, qx_max=qx_max, + qy_min=qy_min, qy_max=qy_max) + self.nbins = nbins + self.fold = fold + + def __call__(self, data2d: Data2D = None) -> Data1D: + """ + Compute average I(Qy) for a region of interest + :param data2d: Data2D object + :return: Data1D object + """ self.validate_and_assign_data(data2d) - if major_axis == 'x': - q_major = self.qx_data - q_minor = self.qy_data - minor_lims = (self.qy_min, self.qy_max) - binning = Binning(min_value=0 if self.fold else self.qx_min, - max_value=self.qx_max, nbins=self.nbins) - elif major_axis == 'y': - q_major = self.qy_data - q_minor = self.qx_data - minor_lims = (self.qx_min, self.qx_max) - binning = Binning(min_value=0 if self.fold else self.qy_min, - max_value=self.qy_max, nbins=self.nbins) + if self.fold: + major_lims = (0, self.qy_max) + self.qy_data = np.abs(self.qy_data) else: - msg = f"Unrecognised axis: {major_axis}" - raise ValueError(msg) + major_lims = (self.qy_min, self.qy_max) + minor_lims = (self.qx_min, self.qx_max) - if self.fold: - q_major = np.abs(q_major) + directional_average = DirectionalAverage(major_data=self.qy_data, + minor_data=self.qx_data, + major_lims=major_lims, + minor_lims=minor_lims, + nbins=self.nbins) + qy_data, intensity, error = \ + directional_average(data=self.data, err_data=self.err_data) - major_weights = np.zeros((self.nbins, q_major.size)) - for m in range(self.nbins): - # Include the value at the end of the binning range, but otherwise - # use half-open intervals so each value belongs in only one bin. - if m == self.nbins - 1: - interval = 'closed' - else: - interval = 'half-open' - bin_start, bin_end = binning.get_interval(bin_number=m) - major_weights[m] \ - = weights_for_interval(array=q_major, l_bound=bin_start, - u_bound=bin_end, - interval_type=interval) - minor_weights = weights_for_interval(array=q_minor, - l_bound=minor_lims[0], - u_bound=minor_lims[1], - interval_type='closed') - weights = major_weights * minor_weights + return Data1D(x=qy_data, y=intensity, dy=error) - q_values = np.sum(weights * q_major, axis=1) - intensity = np.sum(weights * self.data, axis=1) - errs_squared = np.sum((weights * self.err_data)**2, axis=1) - bin_counts = np.sum(weights, axis=1) - errors = np.sqrt(errs_squared) - q_values /= bin_counts - intensity /= bin_counts - errors /= bin_counts +class CircularAverage(PolarROI): + """ + Perform circular averaging on 2D data - finite = (np.isfinite(q_values) & np.isfinite(intensity)) - if not finite.any(): - msg = "Average Error: No points inside ROI to average..." - raise ValueError(msg) + The data returned is the distribution of counts + as a function of Q + """ + + def __init__(self, r_min: float, r_max: float, nbins: int = 100) -> None: + """ + TODO - add docstring + """ + super().__init__(r_min=r_min, r_max=r_max, phi_min=0, phi_max=2*np.pi) + self.nbins = nbins + + def __call__(self, data2d: Data2D = None) -> Data1D: + """ + TODO - add docstring + """ + self.validate_and_assign_data(data2d) + + # Averaging takes place between radial limits + major_lims = (self.r_min, self.r_max) + # Average over the full angular range + directional_average = DirectionalAverage(major_data=self.q_data, + minor_data=self.phi_data, + major_lims=major_lims, + minor_lims=None, + nbins=self.nbins) + q_data, intensity, error = \ + directional_average(data=self.data, err_data=self.err_data) - return Data1D(x=q_values[finite], y=intensity[finite], dy=errors[finite]) + return Data1D(x=q_data, y=intensity, dy=error) -class SlabX(_Slab): +class Ring(PolarROI): """ - Compute average I(Qx) for a region of interest + Defines a ring on a 2D data set. + The ring is defined by r_min, r_max, and + the position of the center of the ring. + + The data returned is the distribution of counts + around the ring as a function of phi. + + Phi_min and phi_max should be defined between 0 and 2*pi + in anti-clockwise starting from the x- axis on the left-hand side """ + def __init__(self, r_min: float, r_max: float, nbins: int = 100) -> None: + """ + TODO - add docstring + """ + super().__init__(r_min=r_min, r_max=r_max, phi_min=0, phi_max=2*np.pi) + self.nbins = nbins + def __call__(self, data2d: Data2D = None) -> Data1D: """ - Compute average I(Qx) for a region of interest - :param data2d: Data2D object - :return: Data1D object + TODO - add docstring """ - return self._avg(data2d, 'x') + self.validate_and_assign_data(data2d) + # Averaging takes place between radial limits + minor_lims = (self.r_min, self.r_max) + # Average over the full angular range + directional_average = DirectionalAverage(major_data=self.phi_data, + minor_data=self.q_data, + major_lims=None, + minor_lims=minor_lims, + nbins=self.nbins) + phi_data, intensity, error = \ + directional_average(data=self.data, err_data=self.err_data) -class SlabY(_Slab): + return Data1D(x=phi_data, y=intensity, dy=error) + + +class SectorQ(PolarROI): """ - Compute average I(Qy) for a region of interest + Sector average as a function of Q for both wings. setting the _Sector.fold + attribute determines whether or not the two sectors are averaged together + (folded over) or separate. In the case of separate (not folded), the + qs for the "minor wing" are arbitrarily set to a negative value. + I(Q) is returned and the data is averaged over phi. + + A sector is defined by r_min, r_max, phi_min, phi_max. + where r_min, r_max, phi_min, phi_max >0. + The number of bins in Q also has to be defined. """ + def __init__(self, r_min: float, r_max: float, phi_min: float, + phi_max: float, nbins: int = 100, fold: bool = True) -> None: + """ + """ + super().__init__(r_min=r_min, r_max=r_max, + phi_min=phi_min, phi_max=phi_max) + self.nbins = nbins + self.fold = fold + def __call__(self, data2d: Data2D = None) -> Data1D: """ - Compute average I(Qy) for a region of interest - :param data2d: Data2D object - :return: Data1D object """ - return self._avg(data2d, 'y') + self.validate_and_assign_data(data2d) + + # Transform all angles to the range [0,2π), where phi_min is at zero. + # We won't need to convert back later because we're plotting against Q. + phi_offset = self.phi_min + self.phi_min = 0.0 + self.phi_max = (self.phi_max - phi_offset) % (2 * np.pi) + self.phi_data = (self.phi_data - phi_offset) % (2 * np.pi) + + major_lims = (self.r_min, self.r_max) + minor_lims = (self.phi_min, self.phi_max) + # Secondary region of interest covers angles on opposite side of origin + minor_lims_alt = (self.phi_min + np.pi, self.phi_max + np.pi) + + primary_region = DirectionalAverage(major_data=self.q_data, + minor_data=self.phi_data, + major_lims=major_lims, + minor_lims=minor_lims, + nbins=self.nbins) + secondary_region = DirectionalAverage(major_data=self.q_data, + minor_data=self.phi_data, + major_lims=major_lims, + minor_lims=minor_lims_alt, + nbins=self.nbins) + + primary_q, primary_I, primary_err = \ + primary_region(data=self.data, err_data=self.err_data) + secondary_q, secondary_I, secondary_err = \ + secondary_region(data=self.data, err_data=self.err_data) + + if self.fold: + # Combining the two regions requires re-binning; the q value + # arrays may be unequal lengths, or the indices may correspond to + # different q values. To average the results from >2 ROIs you would + # need to generalise this process. + combined_q = np.zeros(self.nbins) + average_intensity = np.zeros(self.nbins) + combined_err = np.zeros(self.nbins) + bin_counts = np.zeros(self.nbins) + for old_index, q_val in enumerate(primary_q): + old_index = int(old_index) + new_index = primary_region.get_bin_index(q_val) + combined_q[new_index] += q_val + average_intensity[new_index] += primary_I[old_index] + combined_err[new_index] += primary_err[old_index] ** 2 + bin_counts[new_index] += 1 + for old_index, q_val in enumerate(secondary_q): + old_index = int(old_index) + new_index = secondary_region.get_bin_index(q_val) + combined_q[new_index] += q_val + average_intensity[new_index] += secondary_I[old_index] + combined_err[new_index] += secondary_err[old_index] ** 2 + bin_counts[new_index] += 1 + + combined_q /= bin_counts + average_intensity /= bin_counts + combined_err = np.sqrt(combined_err) / bin_counts + + finite = (np.isfinite(combined_q) & np.isfinite(average_intensity)) + + data1d = Data1D(x=combined_q[finite], y=average_intensity[finite], + dy=combined_err[finite]) + else: + combined_q = np.append(np.flip(-1 * secondary_q), primary_q) + combined_intensity = np.append(np.flip(secondary_I), primary_I) + combined_error = np.append(np.flip(secondary_err), primary_err) + data1d = Data1D(x=combined_q, y=combined_intensity, + dy=combined_error) + + return data1d + + +class SectorPhi(PolarROI): + """ + Sector average as a function of phi. + I(phi) is return and the data is averaged over Q. + + A sector is defined by r_min, r_max, phi_min, phi_max. + The number of bin in phi also has to be defined. + """ + + def __init__(self, r_min: float, r_max: float, phi_min: float, + phi_max: float, nbins: int = 100) -> None: + """ + TODO - add docstring + """ + super().__init__(r_min=r_min, r_max=r_max, + phi_min=phi_min, phi_max=phi_max) + self.nbins = nbins + + def __call__(self, data2d: Data2D = None) -> Data1D: + """ + TODO - add docstring + """ + self.validate_and_assign_data(data2d) + + # Transform all angles to the range [0,2π), where phi_min is at zero. + # Remember to transform back afterwards + phi_offset = self.phi_min + self.phi_min = 0.0 + self.phi_max = (self.phi_max - phi_offset) % (2 * np.pi) + self.phi_data = (self.phi_data - phi_offset) % (2 * np.pi) + + # Averaging takes place between angular and radial limits + # When phi_max and phi_min have the same angle, ROI is a full circle. + if self.phi_max == 0: + major_lims = None + else: + major_lims = (self.phi_min, self.phi_max) + minor_lims = (self.r_min, self.r_max) + + directional_average = DirectionalAverage(major_data=self.phi_data, + minor_data=self.q_data, + major_lims=major_lims, + minor_lims=minor_lims, + nbins=self.nbins) + phi_data, intensity, error = \ + directional_average(data=self.data, err_data=self.err_data) + + # Convert angular data back to the original phi range + phi_data += phi_offset + # In the old manipulations.py, we also had this shift to plot the data + # at the centre of the bins. I'm not sure why it's only angular binning + # which gets this treatment. + phi_data += directional_average.bin_width / 2 + + return Data1D(x=phi_data, y=intensity, dy=error) From b3829a4d83c2ab7173f139a9a99c6049e1f9a900 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Thu, 7 Sep 2023 11:21:49 +0100 Subject: [PATCH 06/33] Updated the unit tests to suit the new_manipulations.py implementation. --- .../utest_averaging_analytical.py | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/test/sasdataloader/utest_averaging_analytical.py b/test/sasdataloader/utest_averaging_analytical.py index 3b2f3ee..6b42400 100644 --- a/test/sasdataloader/utest_averaging_analytical.py +++ b/test/sasdataloader/utest_averaging_analytical.py @@ -11,8 +11,9 @@ from scipy import integrate from sasdata.dataloader import data_info -from sasdata.data_util.manipulations import CircularAverage, Ring, SectorQ, SectorPhi -from sasdata.data_util.new_manipulations import SlabX, SlabY, Boxsum, Boxavg +from sasdata.data_util.new_manipulations import (SlabX, SlabY, Boxsum, Boxavg, + CircularAverage, Ring, + SectorQ, SectorPhi) class MatrixToData2D: @@ -655,18 +656,13 @@ def test_circularaverage_init(self): """ r_min = 1 r_max = 2 - bin_width = 0.001 - # nbins = 100 + nbins = 100 - circ_object = CircularAverage(r_min=r_min, r_max=r_max, - bin_width=bin_width) - # circ_object = CircularAverage(r_min=r_min, r_max=r_max, - # nbins=nbins) + circ_object = CircularAverage(r_min=r_min, r_max=r_max, nbins=nbins) self.assertEqual(circ_object.r_min, r_min) self.assertEqual(circ_object.r_max, r_max) - self.assertEqual(circ_object.bin_width, bin_width) - # self.assertEqual(circ_object.nbins, nbins) + self.assertEqual(circ_object.nbins, nbins) def test_circularaverage_dq_retrieval(self): """ @@ -705,11 +701,7 @@ def test_circularaverage_check_valid_radii(self): """ Test that CircularAverage raises ValueError when r_min > r_max """ - test_data = np.ones([100, 100]) - averager_data = MatrixToData2D(test_data) - - circ_object = CircularAverage(r_min=0.1, r_max=0.05) - self.assertRaises(ValueError, circ_object, averager_data.data) + self.assertRaises(ValueError, CircularAverage, r_min=0.1, r_max=0.05) def test_circularaverage_no_points_to_average(self): """ @@ -727,20 +719,25 @@ def test_circularaverage_averages_circularly(self): """ Test that CircularAverage can calculate a circular average correctly. """ - test_data = CircularTestingMatrix(frequency=2, major_axis='Q') + test_data = CircularTestingMatrix(frequency=2, matrix_size=201, + major_axis='Q') averager_data = MatrixToData2D(test_data.matrix) # Test the ability to average over a subsection of the data r_min = averager_data.qmax * 0.25 r_max = averager_data.qmax * 0.75 - circ_object = CircularAverage(r_min=r_min, r_max=r_max) + nbins = test_data.matrix_size + circ_object = CircularAverage(r_min=r_min, r_max=r_max, nbins=nbins) data1d = circ_object(averager_data.data) expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max) actual_area = integrate.trapezoid(data1d.y, data1d.x) - self.assertAlmostEqual(actual_area, expected_area, 3) + # This used to be able to pass with a precision of 3 d.p. with the old + # manipulations.py - I'm not sure why it doesn't anymore. + # This is still a good level of precision compared to the others though + self.assertAlmostEqual(actual_area, expected_area, 2) # TODO - also check the errors are being calculated correctly @@ -766,8 +763,7 @@ def test_ring_init(self): self.assertEqual(ring_object.r_min, r_min) self.assertEqual(ring_object.r_max, r_max) - # TODO - replace nbins_phi with nbins for consitency - self.assertEqual(ring_object.nbins_phi, nbins) + self.assertEqual(ring_object.nbins, nbins) def test_ring_non_plottable_data(self): """ @@ -804,7 +800,7 @@ def test_ring_averages_azimuthally(self): # Test the ability to average over a subsection of the data r_min = 0.25 * averager_data.qmax r_max = 0.75 * averager_data.qmax - nbins = int(test_data.matrix_size / 2) + nbins = test_data.matrix_size // 2 ring_object = Ring(r_min=r_min, r_max=r_max, nbins=nbins) data1d = ring_object(averager_data.data) @@ -835,17 +831,19 @@ def test_sectorq_init(self): phi_min = 0 phi_max = np.pi nbins = 100 - base = 10 + # base = 10 + # sector_object = SectorQ(r_min=r_min, r_max=r_max, phi_min=phi_min, + # phi_max=phi_max, nbins=nbins, base=base) sector_object = SectorQ(r_min=r_min, r_max=r_max, phi_min=phi_min, - phi_max=phi_max, nbins=nbins, base=base) + phi_max=phi_max, nbins=nbins) self.assertEqual(sector_object.r_min, r_min) self.assertEqual(sector_object.r_max, r_max) self.assertEqual(sector_object.phi_min, phi_min) self.assertEqual(sector_object.phi_max, phi_max) self.assertEqual(sector_object.nbins, nbins) - self.assertEqual(sector_object.base, base) + # self.assertEqual(sector_object.base, base) def test_sectorq_non_plottable_data(self): """ @@ -869,8 +867,8 @@ def test_sectorq_averaging_without_fold(self): phi_max = 5*np.pi/6 nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable - wedge_object = SectorQ(r_min=r_min, r_max=r_max, phi_min=phi_min + np.pi, - phi_max=phi_max + np.pi, nbins=nbins) + wedge_object = SectorQ(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins) # Explicitly set fold to False - results span full +/- range wedge_object.fold = False data1d = wedge_object(averager_data.data) @@ -943,17 +941,19 @@ def test_sectorphi_init(self): phi_min = 0 phi_max = np.pi nbins = 100 - base = 10 + # base = 10 + # sector_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min, + # phi_max=phi_max, nbins=nbins, base=base) sector_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min, - phi_max=phi_max, nbins=nbins, base=base) + phi_max=phi_max, nbins=nbins) self.assertEqual(sector_object.r_min, r_min) self.assertEqual(sector_object.r_max, r_max) self.assertEqual(sector_object.phi_min, phi_min) self.assertEqual(sector_object.phi_max, phi_max) self.assertEqual(sector_object.nbins, nbins) - self.assertEqual(sector_object.base, base) + # self.assertEqual(sector_object.base, base) def test_sectorphi_non_plottable_data(self): """ @@ -977,8 +977,8 @@ def test_sectorphi_averaging(self): phi_max = 5*np.pi/6 nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable - wedge_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min + np.pi, - phi_max=phi_max + np.pi, nbins=nbins) + wedge_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins) data1d = wedge_object(averager_data.data) expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max, From 2503d8162aaab38a7217799922b6a529b6fd5231 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 8 Sep 2023 10:03:15 +0100 Subject: [PATCH 07/33] Added dedicated WedgeQ and WedgePhi classes, plus corresponding unit test. The old SectorPhi now links to WedgePhi. --- sasdata/data_util/new_manipulations.py | 70 +++++++++++- .../utest_averaging_analytical.py | 105 +++++++++++++----- 2 files changed, 143 insertions(+), 32 deletions(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index b5ee734..a12f941 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -26,6 +26,10 @@ def weights_for_interval(array, l_bound, u_bound, interval_type='half-open'): class DirectionalAverage: """ TODO - write a docstring + + Note that the old version of manipulations.py had an option for logarithmic + binning which was only used by SectorQ. This functionality is never called + upon by SasView however, so I haven't implemented it here (yet). """ def __init__(self, major_data, minor_data, major_lims=None, @@ -548,19 +552,58 @@ def __call__(self, data2d: Data2D = None) -> Data1D: return data1d -class SectorPhi(PolarROI): +class WedgeQ(PolarROI): + """ + TODO - add docstring """ - Sector average as a function of phi. - I(phi) is return and the data is averaged over Q. - A sector is defined by r_min, r_max, phi_min, phi_max. - The number of bin in phi also has to be defined. + def __init__(self, r_min: float, r_max: float, phi_min: float, + phi_max: float, nbins: int = 100) -> None: + """ + """ + super().__init__(r_min=r_min, r_max=r_max, + phi_min=phi_min, phi_max=phi_max) + self.nbins = nbins + + def __call__(self, data2d: Data2D = None) -> Data1D: + """ + """ + self.validate_and_assign_data(data2d) + + # Transform all angles to the range [0,2π), where phi_min is at zero. + # We won't need to convert back later because we're plotting against Q. + phi_offset = self.phi_min + self.phi_min = 0.0 + self.phi_max = (self.phi_max - phi_offset) % (2 * np.pi) + self.phi_data = (self.phi_data - phi_offset) % (2 * np.pi) + + # Averaging takes place between radial and angular limits + major_lims = (self.r_min, self.r_max) + # When phi_max and phi_min have the same angle, ROI is a full circle. + if self.phi_max == 0: + minor_lims = None + else: + minor_lims = (self.phi_min, self.phi_max) + + directional_average = DirectionalAverage(major_data=self.q_data, + minor_data=self.phi_data, + major_lims=major_lims, + minor_lims=minor_lims, + nbins=self.nbins) + q_data, intensity, error = \ + directional_average(data=self.data, err_data=self.err_data) + + return Data1D(x=q_data, y=intensity, dy=error) + + +class WedgePhi(PolarROI): + """ + TODO - add docstring """ def __init__(self, r_min: float, r_max: float, phi_min: float, phi_max: float, nbins: int = 100) -> None: """ - TODO - add docstring """ super().__init__(r_min=r_min, r_max=r_max, phi_min=phi_min, phi_max=phi_max) @@ -604,3 +647,18 @@ def __call__(self, data2d: Data2D = None) -> Data1D: return Data1D(x=phi_data, y=intensity, dy=error) + +class SectorPhi(WedgePhi): + """ + Sector average as a function of phi. + I(phi) is return and the data is averaged over Q. + + A sector is defined by r_min, r_max, phi_min, phi_max. + The number of bin in phi also has to be defined. + """ + + # This class has only been kept around in case users are using it in + # scripts, SectorPhi was never used by SasView. The functionality is now in + # use through WedgeSlicer.py, so the rewritten version of this class has + # been named WedgePhi. + diff --git a/test/sasdataloader/utest_averaging_analytical.py b/test/sasdataloader/utest_averaging_analytical.py index 6b42400..358dd33 100644 --- a/test/sasdataloader/utest_averaging_analytical.py +++ b/test/sasdataloader/utest_averaging_analytical.py @@ -13,7 +13,7 @@ from sasdata.dataloader import data_info from sasdata.data_util.new_manipulations import (SlabX, SlabY, Boxsum, Boxavg, CircularAverage, Ring, - SectorQ, SectorPhi) + SectorQ, WedgeQ, WedgePhi) class MatrixToData2D: @@ -815,7 +815,7 @@ def test_ring_averages_azimuthally(self): class SectorQTests(unittest.TestCase): """ - This class contains the tests for the _Sector class from manipulations.py + This class contains the tests for the SectorQ class from manipulations.py On the sasview side, this includes SectorSlicer and WedgeSlicer. The parameters frequency, r_min, r_max, phi_min and phi_max are largely @@ -826,8 +826,8 @@ def test_sectorq_init(self): """ Test that SectorQ's __init__ method does what it's supposed to. """ - r_min = 1 - r_max = 2 + r_min = 0 + r_max = 1 phi_min = 0 phi_max = np.pi nbins = 100 @@ -861,7 +861,7 @@ def test_sectorq_averaging_without_fold(self): major_axis='Q') averager_data = MatrixToData2D(test_data.matrix) - r_min = 0.1 * averager_data.qmax + r_min = 0 r_max = 0.9 * averager_data.qmax phi_min = np.pi/6 phi_max = 5*np.pi/6 @@ -895,7 +895,7 @@ def test_sectorq_averaging_with_fold(self): major_axis='Q') averager_data = MatrixToData2D(test_data.matrix) - r_min = 0.1 * averager_data.qmax + r_min = 0 r_max = 0.9 * averager_data.qmax phi_min = np.pi/6 phi_max = 5*np.pi/6 @@ -923,18 +923,71 @@ def test_sectorq_averaging_with_fold(self): self.assertAlmostEqual(actual_area, expected_area, 1) -class SectorPhiTests(unittest.TestCase): +class WedgeQTests(unittest.TestCase): """ - This class contains the tests for the SectorPhi class from manipulations.py - On the sasview side, this includes SectorSlicer and WedgeSlicer. + This class contains the tests for the WedgeQ class from manipulations.py The parameters frequency, r_min, r_max, phi_min and phi_max are largely arbitrary, and the tests should pass if any sane value is used for them. """ - def test_sectorphi_init(self): + def test_wedgeq_init(self): """ - Test that SectorPhi's __init__ method does what it's supposed to. + Test that WedgeQ's __init__ method does what it's supposed to. + """ + r_min = 1 + r_max = 2 + phi_min = 0 + phi_max = np.pi + nbins = 10 + + wedge_object = WedgeQ(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins) + + self.assertEqual(wedge_object.r_min, r_min) + self.assertEqual(wedge_object.r_max, r_max) + self.assertEqual(wedge_object.phi_min, phi_min) + self.assertEqual(wedge_object.phi_max, phi_max) + self.assertEqual(wedge_object.nbins, nbins) + + def test_wedgeq_averaging(self): + """ + Test WedgeQ can average correctly, when all of min/max r & phi params + are specified and have their expected form. + """ + test_data = CircularTestingMatrix(frequency=3, matrix_size=201, + major_axis='Q') + averager_data = MatrixToData2D(test_data.matrix) + + r_min = 0.1 * averager_data.qmax + r_max = 0.9 * averager_data.qmax + phi_min = np.pi/6 + phi_max = 5*np.pi/6 + nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable + + wedge_object = WedgeQ(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins) + data1d = wedge_object(averager_data.data) + + expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max, + phi_min=phi_min, + phi_max=phi_max) + actual_area = integrate.simpson(data1d.y, data1d.x) + + self.assertAlmostEqual(actual_area, expected_area, 1) + + +class WedgePhiTests(unittest.TestCase): + """ + This class contains the tests for the WedgePhi class from manipulations.py + + The parameters frequency, r_min, r_max, phi_min and phi_max are largely + arbitrary, and the tests should pass if any sane value is used for them. + """ + + def test_wedgephi_init(self): + """ + Test that WedgePhi's __init__ method does what it's supposed to. """ r_min = 1 r_max = 2 @@ -943,29 +996,29 @@ def test_sectorphi_init(self): nbins = 100 # base = 10 - # sector_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min, + # wedge_object = WedgePhi(r_min=r_min, r_max=r_max, phi_min=phi_min, # phi_max=phi_max, nbins=nbins, base=base) - sector_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min, - phi_max=phi_max, nbins=nbins) + wedge_object = WedgePhi(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins) - self.assertEqual(sector_object.r_min, r_min) - self.assertEqual(sector_object.r_max, r_max) - self.assertEqual(sector_object.phi_min, phi_min) - self.assertEqual(sector_object.phi_max, phi_max) - self.assertEqual(sector_object.nbins, nbins) - # self.assertEqual(sector_object.base, base) + self.assertEqual(wedge_object.r_min, r_min) + self.assertEqual(wedge_object.r_max, r_max) + self.assertEqual(wedge_object.phi_min, phi_min) + self.assertEqual(wedge_object.phi_max, phi_max) + self.assertEqual(wedge_object.nbins, nbins) + # self.assertEqual(wedge_object.base, base) - def test_sectorphi_non_plottable_data(self): + def test_wedgephi_non_plottable_data(self): """ Test that RuntimeError is raised if the data supplied isn't plottable """ # Implementing this test can wait pass - def test_sectorphi_averaging(self): + def test_wedgephi_averaging(self): """ - Test _Sector can average correctly with a major axis of phi, when all - of min/max r & phi params are specified and have their expected form. + Test WedgePhi can average correctly, when all of min/max r & phi params + are specified and have their expected form. """ test_data = CircularTestingMatrix(frequency=1, matrix_size=201, major_axis='Phi') @@ -977,8 +1030,8 @@ def test_sectorphi_averaging(self): phi_max = 5*np.pi/6 nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable - wedge_object = SectorPhi(r_min=r_min, r_max=r_max, phi_min=phi_min, - phi_max=phi_max, nbins=nbins) + wedge_object = WedgePhi(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins) data1d = wedge_object(averager_data.data) expected_area = test_data.area_under_region(r_min=r_min, r_max=r_max, From c2ee6bd71865bba220870bbb13aee514fd0f6de1 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Sun, 17 Sep 2023 13:11:26 +0100 Subject: [PATCH 08/33] Added documentation to new manipulations module --- sasdata/data_util/new_manipulations.py | 421 +++++++++++++++++++------ 1 file changed, 329 insertions(+), 92 deletions(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index a12f941..1c7de22 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -1,3 +1,7 @@ +""" +This module contains various data processors used by Sasview's slicers. +""" + import numpy as np from sasdata.dataloader.data_info import Data1D, Data2D @@ -5,13 +9,23 @@ def weights_for_interval(array, l_bound, u_bound, interval_type='half-open'): """ + Weight coordinate data by position relative to a specified interval. + + :param array: the array for which the weights are calculated + :param l_bound: value defining the lower limit of the region of interest + :param u_bound: value defining the upper limit of the region of interest + :param interval_type: determines whether the value defined by u_bound is + included within the interval. + If and when fractional binning is implemented (ask Lucas), this function will be changed so that instead of outputting zeros and ones, it gives fractional values instead. These will depend on how close the array value is to being within the interval defined. """ - # These checks could be modified to return fractional bin weights. - # The last value in the binning range must be included for to pass utests + + # Whether the endpoint should be included depends on circumstance. + # Half-open is used when binning the major axis (except for the final bin) + # and closed used for the minor axis and the final bin of the major axis. if interval_type == 'half-open': in_range = (l_bound <= array) & (array < u_bound) elif interval_type == 'closed': @@ -25,19 +39,43 @@ def weights_for_interval(array, l_bound, u_bound, interval_type='half-open'): class DirectionalAverage: """ - TODO - write a docstring + Average along one coordinate axis of 2D data and return data for a 1D plot. + This can also be thought of as a projection onto the major axis: 2D -> 1D. + + This class operates on a decomposed Data2D object, and returns data needed + to construct a Data1D object. The class is instantiated with two arrays of + orthogonal coordinate data (depending on the coordinate system, these may + have undergone some pre-processing) and two corresponding two-element + tuples/lists defining the lower and upper limits on the Region of Interest + (ROI) for each coordinate axis. One of these axes is averaged along, and + the other is divided into bins and becomes the dependent variable of the + eventual 1D plot. These are called the minor and major axes respectively. + When a class instance is called, it is passed the intensity and error data + from the original Data2D object. These should not have undergone any + coordinate system dependent pre-processing. Note that the old version of manipulations.py had an option for logarithmic binning which was only used by SectorQ. This functionality is never called upon by SasView however, so I haven't implemented it here (yet). """ - def __init__(self, major_data, minor_data, major_lims=None, + def __init__(self, major_axis=None, minor_axis=None, major_lims=None, minor_lims=None, nbins=100): """ + Set up direction of averaging, limits on the ROI, & the number of bins. + + :param major_axis: Coordinate data for axis onto which the 2D data is + projected. + :param minor_axis: Coordinate data for the axis perpendicular to the + major axis. + :param major_lims: Lower and upper bounds of the ROI along the major + axis. Given as a 2 element tuple/list. + :param minor_lims: Lower and upper bounds of the ROI along the minor + axis. Given as a 2 element tuple/list. + :param nbins: The number of bins the major axis is divided up into. """ - if any(not isinstance(data, (list, np.ndarray)) for - data in (major_data, minor_data)): + if any(not isinstance(coordinate_data, (list, np.ndarray)) for + coordinate_data in (major_axis, minor_axis)): msg = "Must provide major & minor coordinate arrays for binning." raise ValueError(msg) if any(lims is not None and len(lims) != 2 for @@ -48,14 +86,15 @@ def __init__(self, major_data, minor_data, major_lims=None, msg = "Parameter 'nbins' must be an integer" raise TypeError(msg) - self.major_data = np.asarray(major_data) - self.minor_data = np.asarray(minor_data) + self.major_axis = np.asarray(major_axis) + self.minor_axis = np.asarray(minor_axis) + # In some cases all values from a given axis are part of the ROI. if major_lims is None: - self.major_lims = (self.major_data.min(), self.major_data.max()) + self.major_lims = (self.major_axis.min(), self.major_axis.max()) else: self.major_lims = major_lims if minor_lims is None: - self.minor_lims = (self.minor_data.min(), self.minor_data.max()) + self.minor_lims = (self.minor_axis.min(), self.minor_axis.max()) else: self.minor_lims = minor_lims self.nbins = nbins @@ -63,13 +102,15 @@ def __init__(self, major_data, minor_data, major_lims=None, @property def bin_width(self): """ - Return the bin width + Return the bin width based on the range of the major axis and nbins """ return (self.major_lims[1] - self.major_lims[0]) / self.nbins def get_bin_interval(self, bin_number): """ - Return the upper and lower limits defining a given bin + Return the upper and lower limits defining a bin, given its index. + + :param bin_number: The index of the bin (between 0 and self.nbins - 1) """ bin_start = self.major_lims[0] + bin_number * self.bin_width bin_end = self.major_lims[0] + (bin_number + 1) * self.bin_width @@ -78,6 +119,9 @@ def get_bin_interval(self, bin_number): def get_bin_index(self, value): """ + Return the index of the bin to which the supplied value belongs. + + :param value: A coordinate value from somewhere along the major axis. """ numerator = value - self.major_lims[0] denominator = self.major_lims[1] - self.major_lims[0] @@ -92,22 +136,26 @@ def get_bin_index(self, value): def compute_weights(self): """ + Return weights array for the contribution of each datapoint to each bin + + Each row of the weights array corresponds to the bin with the same + index. """ - major_weights = np.zeros((self.nbins, self.major_data.size)) + major_weights = np.zeros((self.nbins, self.major_axis.size)) for m in range(self.nbins): # Include the value at the end of the binning range, but in - # general use half-open intervals so each value begins in only + # general use half-open intervals so each value belongs in only # one bin. if m == self.nbins - 1: interval = 'closed' else: interval = 'half-open' bin_start, bin_end = self.get_bin_interval(bin_number=m) - major_weights[m] = weights_for_interval(array=self.major_data, + major_weights[m] = weights_for_interval(array=self.major_axis, l_bound=bin_start, u_bound=bin_end, interval_type=interval) - minor_weights = weights_for_interval(array=self.minor_data, + minor_weights = weights_for_interval(array=self.minor_axis, l_bound=self.minor_lims[0], u_bound=self.minor_lims[1], interval_type='closed') @@ -115,10 +163,14 @@ def compute_weights(self): def __call__(self, data, err_data): """ + Compute the directional average of the supplied intensity & error data. + + :param data: intensity data from the origninal Data2D object. + :param err_data: the corresponding errors for the intensity data. """ weights = self.compute_weights() - x_axis_values = np.sum(weights * self.major_data, axis=1) + x_axis_values = np.sum(weights * self.major_axis, axis=1) intensity = np.sum(weights * data, axis=1) errs_squared = np.sum((weights * err_data)**2, axis=1) bin_counts = np.sum(weights, axis=1) @@ -138,11 +190,16 @@ def __call__(self, data, err_data): class GenericROI: """ - TODO - add docstring + Base class used to set up the data from a Data2D object for processing. + This class performs any coordinate system independent setup and validation. """ def __init__(self): """ + Assign the variables used to label the properties of the Data2D object. + + In classes inheriting from GenericROI, the variables used to define the + boundaries of the Region Of Interest are also set up during __init__. """ self.data = None self.err_data = None @@ -152,8 +209,13 @@ def __init__(self): def validate_and_assign_data(self, data2d: Data2D = None) -> None: """ - Check that the data supplied valid and assign data variables. + Check that the data supplied is valid and assign data to variables. + This method must be executed before any further data processing happens + + :param data2d: A Data2D object which is the target of a child class' + data manipulations. """ + # Check that the supplied data2d is valid and usable. if not isinstance(data2d, Data2D): msg = "Data supplied must be of type Data2D." raise TypeError(msg) @@ -164,6 +226,8 @@ def validate_and_assign_data(self, data2d: Data2D = None) -> None: # Only use data which is finite and not masked off valid_data = np.isfinite(data2d.data) & data2d.mask + # Assign properties of the Data2D object to variables for reference + # during data processing. self.data = data2d.data[valid_data] self.err_data = data2d.err_data[valid_data] self.q_data = data2d.q_data[valid_data] @@ -179,17 +243,23 @@ def validate_and_assign_data(self, data2d: Data2D = None) -> None: class CartesianROI(GenericROI): """ - Base class for manipulators with a rectangular region of interest. + Base class for data manipulators with a Cartesian (rectangular) ROI. """ def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0) -> None: """ - TODO - add docstring + Assign the variables used to label the properties of the Data2D object. + Also establish the upper and lower bounds defining the ROI. + + The units of these parameters are A^-1 + :param qx_min: Lower bound of the ROI along the Q_x direction. + :param qx_max: Upper bound of the ROI along the Q_x direction. + :param qy_min: Lower bound of the ROI along the Q_y direction. + :param qy_max: Upper bound of the ROI along the Q_y direction. """ super().__init__() - # Units A^-1 self.qx_min = qx_min self.qx_max = qx_max self.qy_min = qy_min @@ -198,13 +268,22 @@ def __init__(self, qx_min: float = 0, qx_max: float = 0, class PolarROI(GenericROI): """ - Base class for manipulators whose ROI is defined with polar coordinates. + Base class for data manipulators with a polar ROI. """ def __init__(self, r_min: float, r_max: float, phi_min: float = 0, phi_max: float = 2*np.pi) -> None: """ - TODO - add docstring + Assign the variables used to label the properties of the Data2D object. + Also establish the upper and lower bounds defining the ROI. + + The units are A^-1 for radial parameters, and radians for anglar ones. + :param r_min: Lower bound of the ROI along the Q direction. + :param r_max: Upper bound of the ROI along the Q direction. + :param phi_min: Lower bound of the ROI along the Phi direction. + :param phi_max: Upper bound of the ROI along the Phi direction. + + Note that Phi is measured anti-clockwise from the positive x-axis. """ super().__init__() @@ -222,27 +301,42 @@ def __init__(self, r_min: float, r_max: float, def validate_and_assign_data(self, data2d: Data2D = None) -> None: """ Check that the data supplied valid and assign data variables. + This method must be executed before any further data processing happens + + :param data2d: A Data2D object which is the target of a child class' + data manipulations. """ + + # Most validation and pre-processing is taken care of by GenericROI. super().validate_and_assign_data(data2d) + # Phi data can be calculated from the Cartesian Q coordinates. self.phi_data = np.arctan2(self.qy_data, self.qx_data) class Boxsum(CartesianROI): """ - Perform the sum of counts in a 2D region of interest. + Compute the sum of the intensity within a rectangular Region Of Interest. """ def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0) -> None: """ - TODO - add docstring + Set up the Region of Interest and its boundaries. + + The units of these parameters are A^-1 + :param qx_min: Lower bound of the ROI along the Q_x direction. + :param qx_max: Upper bound of the ROI along the Q_x direction. + :param qy_min: Lower bound of the ROI along the Q_y direction. + :param qy_max: Upper bound of the ROI along the Q_y direction. """ super().__init__(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max) def __call__(self, data2d: Data2D = None) -> float: """ - TODO - add docstring + Coordinate data processing operations and return the results. + + :param data2d: The Data2D object for which the sum is calculated. """ self.validate_and_assign_data(data2d) total_sum, error, count = self._sum() @@ -251,7 +345,9 @@ def __call__(self, data2d: Data2D = None) -> float: def _sum(self) -> float: """ - TODO - add docstring + Determine which data are inside the ROI and compute their sum. + Also calculate the error on this calculation and the total number of + datapoints in the region. """ # Currently the weights are binary, but could be fractional in future @@ -266,6 +362,8 @@ def _sum(self) -> float: weights = x_weights * y_weights data = weights * self.data + # Not certain that the weights should be squared here, I'm just copying + # how it was done in the old manipulations.py err_squared = weights * weights * self.err_data * self.err_data total_sum = np.sum(data) @@ -277,20 +375,28 @@ def _sum(self) -> float: class Boxavg(Boxsum): """ - Perform the average of counts in a 2D region of interest. + Compute the average intensity within a rectangular Region Of Interest. """ def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0) -> None: """ - TODO - add docstring + Set up the Region of Interest and its boundaries. + + The units of these parameters are A^-1 + :param qx_min: Lower bound of the ROI along the Q_x direction. + :param qx_max: Upper bound of the ROI along the Q_x direction. + :param qy_min: Lower bound of the ROI along the Q_y direction. + :param qy_max: Upper bound of the ROI along the Q_y direction. """ super().__init__(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max) def __call__(self, data2d: Data2D) -> float: """ - TODO - add docstring + Coordinate data processing operations and return the results. + + :param data2d: The Data2D object for which the average is calculated. """ self.validate_and_assign_data(data2d) total_sum, error, count = super()._sum() @@ -300,13 +406,30 @@ def __call__(self, data2d: Data2D) -> float: class SlabX(CartesianROI): """ - Compute average I(Qx) for a region of interest + Average I(Q_x, Q_y) along the y direction (within a ROI), giving I(Q_x). + + This class is initialised by specifying the boundaries of the ROI and is + called by supplying a Data2D object. It returns a Data1D object. + The averaging process can also be thought of as projecting 2D -> 1D. + + There also exists the option to "fold" the ROI, where Q data on opposite + sides of the origin but with equal magnitudes are averaged together, + resulting in a 1D plot with only positive Q values shown. """ def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0, nbins: int = 100, fold: bool = False): """ - TODO - add docstring + Set up the ROI boundaries, the binning of the output 1D data, and fold. + + The units of these parameters are A^-1 + :param qx_min: Lower bound of the ROI along the Q_x direction. + :param qx_max: Upper bound of the ROI along the Q_x direction. + :param qy_min: Lower bound of the ROI along the Q_y direction. + :param qy_max: Upper bound of the ROI along the Q_y direction. + :param nbins: The number of bins data is sorted into along Q_x. + :param fold: Whether the two halves of the ROI along Q_x should be + folded together during averaging. """ super().__init__(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max) @@ -315,12 +438,18 @@ def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, def __call__(self, data2d: Data2D = None) -> Data1D: """ - Compute average I(Qx) for a region of interest - :param data2d: Data2D object - :return: Data1D object + Compute the 1D average of 2D data, projecting along the Q_x axis. + + :param data2d: The Data2D object for which the average is computed. + :return: Data1D object for plotting. """ self.validate_and_assign_data(data2d) + # SlabX is used by SasView's BoxInteractorX, which is designed so that + # the ROI is always centred on the origin. If this ever changes, then + # the behaviour of fold here will also need to change. Perhaps we could + # apply a transformation to the data like the one used in WedgePhi. + if self.fold: major_lims = (0, self.qx_max) self.qx_data = np.abs(self.qx_data) @@ -328,8 +457,8 @@ def __call__(self, data2d: Data2D = None) -> Data1D: major_lims = (self.qx_min, self.qx_max) minor_lims = (self.qy_min, self.qy_max) - directional_average = DirectionalAverage(major_data=self.qx_data, - minor_data=self.qy_data, + directional_average = DirectionalAverage(major_axis=self.qx_data, + minor_axis=self.qy_data, major_lims=major_lims, minor_lims=minor_lims, nbins=self.nbins) @@ -341,13 +470,30 @@ def __call__(self, data2d: Data2D = None) -> Data1D: class SlabY(CartesianROI): """ - Compute average I(Qy) for a region of interest + Average I(Q_x, Q_y) along the x direction (within a ROI), giving I(Q_y). + + This class is initialised by specifying the boundaries of the ROI and is + called by supplying a Data2D object. It returns a Data1D object. + The averaging process can also be thought of as projecting 2D -> 1D. + + There also exists the option to "fold" the ROI, where Q data on opposite + sides of the origin but with equal magnitudes are averaged together, + resulting in a 1D plot with only positive Q values shown. """ def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, qy_max: float = 0, nbins: int = 100, fold: bool = False): """ - TODO - add docstring + Set up the ROI boundaries, the binning of the output 1D data, and fold. + + The units of these parameters are A^-1 + :param qx_min: Lower bound of the ROI along the Q_x direction. + :param qx_max: Upper bound of the ROI along the Q_x direction. + :param qy_min: Lower bound of the ROI along the Q_y direction. + :param qy_max: Upper bound of the ROI along the Q_y direction. + :param nbins: The number of bins data is sorted into along Q_y. + :param fold: Whether the two halves of the ROI along Q_y should be + folded together during averaging. """ super().__init__(qx_min=qx_min, qx_max=qx_max, qy_min=qy_min, qy_max=qy_max) @@ -356,12 +502,18 @@ def __init__(self, qx_min: float = 0, qx_max: float = 0, qy_min: float = 0, def __call__(self, data2d: Data2D = None) -> Data1D: """ - Compute average I(Qy) for a region of interest - :param data2d: Data2D object - :return: Data1D object + Compute the 1D average of 2D data, projecting along the Q_y axis. + + :param data2d: The Data2D object for which the average is computed. + :return: Data1D object for plotting. """ self.validate_and_assign_data(data2d) + # SlabY is used by SasView's BoxInteractorY, which is designed so that + # the ROI is always centred on the origin. If this ever changes, then + # the behaviour of fold here will also need to change. Perhaps we could + # apply a transformation to the data like the one used in WedgePhi. + if self.fold: major_lims = (0, self.qy_max) self.qy_data = np.abs(self.qy_data) @@ -369,8 +521,8 @@ def __call__(self, data2d: Data2D = None) -> Data1D: major_lims = (self.qy_min, self.qy_max) minor_lims = (self.qx_min, self.qx_max) - directional_average = DirectionalAverage(major_data=self.qy_data, - minor_data=self.qx_data, + directional_average = DirectionalAverage(major_axis=self.qy_data, + minor_axis=self.qx_data, major_lims=major_lims, minor_lims=minor_lims, nbins=self.nbins) @@ -382,30 +534,41 @@ def __call__(self, data2d: Data2D = None) -> Data1D: class CircularAverage(PolarROI): """ - Perform circular averaging on 2D data + Calculate I(|Q|) by circularly averaging 2D data between 2 radial limits. - The data returned is the distribution of counts - as a function of Q + This class is initialised by specifying lower and upper limits on the + magnitude of Q values to consider during the averaging, though currently + SasView always calls this class using the full range of data. When called, + this class is supplied with a Data2D object. It returns a Data1D object + where intensity is given as a function of Q only. """ def __init__(self, r_min: float, r_max: float, nbins: int = 100) -> None: """ - TODO - add docstring + Set up the lower and upper radial limits as well as the number of bins. + + The units are A^-1 for the radial parameters. + :param r_min: Lower limit for |Q| values to use during averaging. + :param r_max: Upper limit for |Q| values to use during averaging. + :param nbins: The number of bins data is sorted into along |Q| the axis """ - super().__init__(r_min=r_min, r_max=r_max, phi_min=0, phi_max=2*np.pi) + super().__init__(r_min=r_min, r_max=r_max) self.nbins = nbins def __call__(self, data2d: Data2D = None) -> Data1D: """ - TODO - add docstring + Compute the 1D average of 2D data, projecting along the Q axis. + + :param data2d: The Data2D object for which the average is computed. + :return: Data1D object for plotting. """ self.validate_and_assign_data(data2d) # Averaging takes place between radial limits major_lims = (self.r_min, self.r_max) - # Average over the full angular range - directional_average = DirectionalAverage(major_data=self.q_data, - minor_data=self.phi_data, + # minor_lims is None because a full-circle angular range is used + directional_average = DirectionalAverage(major_axis=self.q_data, + minor_axis=self.phi_data, major_lims=major_lims, minor_lims=None, nbins=self.nbins) @@ -417,35 +580,41 @@ def __call__(self, data2d: Data2D = None) -> Data1D: class Ring(PolarROI): """ - Defines a ring on a 2D data set. - The ring is defined by r_min, r_max, and - the position of the center of the ring. + Calculate I(φ) by radially averaging 2D data between 2 radial limits. - The data returned is the distribution of counts - around the ring as a function of phi. - - Phi_min and phi_max should be defined between 0 and 2*pi - in anti-clockwise starting from the x- axis on the left-hand side + This class is initialised by specifying lower and upper limits on the + magnitude of Q values to consider during the averaging. When called, + this class is supplied with a Data2D object. It returns a Data1D object. + This Data1D object gives intensity as a function of the angle from the + positive x-axis, φ, only. """ def __init__(self, r_min: float, r_max: float, nbins: int = 100) -> None: """ - TODO - add docstring + Set up the lower and upper radial limits as well as the number of bins. + + The units are A^-1 for the radial parameters. + :param r_min: Lower limit for |Q| values to use during averaging. + :param r_max: Upper limit for |Q| values to use during averaging. + :param nbins: The number of bins data is sorted into along Phi the axis """ - super().__init__(r_min=r_min, r_max=r_max, phi_min=0, phi_max=2*np.pi) + super().__init__(r_min=r_min, r_max=r_max) self.nbins = nbins def __call__(self, data2d: Data2D = None) -> Data1D: """ - TODO - add docstring + Compute the 1D average of 2D data, projecting along the Phi axis. + + :param data2d: The Data2D object for which the average is computed. + :return: Data1D object for plotting. """ self.validate_and_assign_data(data2d) # Averaging takes place between radial limits minor_lims = (self.r_min, self.r_max) - # Average over the full angular range - directional_average = DirectionalAverage(major_data=self.phi_data, - minor_data=self.q_data, + # major_lims is None because a full-circle angular range is used + directional_average = DirectionalAverage(major_axis=self.phi_data, + minor_axis=self.q_data, major_lims=None, minor_lims=minor_lims, nbins=self.nbins) @@ -457,20 +626,39 @@ def __call__(self, data2d: Data2D = None) -> Data1D: class SectorQ(PolarROI): """ - Sector average as a function of Q for both wings. setting the _Sector.fold - attribute determines whether or not the two sectors are averaged together - (folded over) or separate. In the case of separate (not folded), the - qs for the "minor wing" are arbitrarily set to a negative value. - I(Q) is returned and the data is averaged over phi. - - A sector is defined by r_min, r_max, phi_min, phi_max. - where r_min, r_max, phi_min, phi_max >0. - The number of bins in Q also has to be defined. + Project I(Q, φ) data onto I(Q) within a region defined by Cartesian limits. + + The projection is computed by averaging together datapoints with the same + angle φ (so long as they are within the ROI), measured anticlockwise from + the positive x-axis. + + This class is initialised by specifying lower and upper limits on both the + magnitude of Q and the angle φ. These four parameters specify the primary + Region Of Interest, however there is a secondary ROI with the same |Q| + values on the opposite side of the origin (φ + π). How this secondary ROI + is treated depends on the value of the `fold` parameter. If fold is set to + True, data on opposite sides of the origin are averaged together and the + results are plotted against positive values of Q. If fold is set to False, + the data from the two regions are graphed separeately, with the secondary + ROI data labelled using negative Q values. + + When called, this class is supplied with a Data2D object. It returns a + Data1D object where intensity is given as a function of Q only. """ def __init__(self, r_min: float, r_max: float, phi_min: float, phi_max: float, nbins: int = 100, fold: bool = True) -> None: """ + Set up the ROI boundaries, the binning of the output 1D data, and fold. + + The units are A^-1 for radial parameters, and radians for anglar ones. + :param r_min: Lower limit for |Q| values to use during averaging. + :param r_max: Upper limit for |Q| values to use during averaging. + :param phi_min: Lower limit for φ values (in the primary ROI). + :param phi_max: Upper limit for φ values (in the primary ROI). + :param nbins: The number of bins data is sorted into along the |Q| axis + :param fold: Whether the primary and secondary ROIs should be folded + together during averaging. """ super().__init__(r_min=r_min, r_max=r_max, phi_min=phi_min, phi_max=phi_max) @@ -479,10 +667,15 @@ def __init__(self, r_min: float, r_max: float, phi_min: float, def __call__(self, data2d: Data2D = None) -> Data1D: """ + Compute the 1D average of 2D data, projecting along the Q_y axis. + + :param data2d: The Data2D object for which the average is computed. + :return: Data1D object for plotting. """ self.validate_and_assign_data(data2d) - # Transform all angles to the range [0,2π), where phi_min is at zero. + # Transform all angles to the range [0,2π) where phi_min is at zero, + # eliminating errors when the ROI straddles the 2π -> 0 discontinuity. # We won't need to convert back later because we're plotting against Q. phi_offset = self.phi_min self.phi_min = 0.0 @@ -494,13 +687,13 @@ def __call__(self, data2d: Data2D = None) -> Data1D: # Secondary region of interest covers angles on opposite side of origin minor_lims_alt = (self.phi_min + np.pi, self.phi_max + np.pi) - primary_region = DirectionalAverage(major_data=self.q_data, - minor_data=self.phi_data, + primary_region = DirectionalAverage(major_axis=self.q_data, + minor_axis=self.phi_data, major_lims=major_lims, minor_lims=minor_lims, nbins=self.nbins) - secondary_region = DirectionalAverage(major_data=self.q_data, - minor_data=self.phi_data, + secondary_region = DirectionalAverage(major_axis=self.q_data, + minor_axis=self.phi_data, major_lims=major_lims, minor_lims=minor_lims_alt, nbins=self.nbins) @@ -543,6 +736,7 @@ def __call__(self, data2d: Data2D = None) -> Data1D: data1d = Data1D(x=combined_q[finite], y=average_intensity[finite], dy=combined_err[finite]) else: + # The secondary ROI is labelled with negative Q values. combined_q = np.append(np.flip(-1 * secondary_q), primary_q) combined_intensity = np.append(np.flip(secondary_I), primary_I) combined_error = np.append(np.flip(secondary_err), primary_err) @@ -554,12 +748,29 @@ def __call__(self, data2d: Data2D = None) -> Data1D: class WedgeQ(PolarROI): """ - TODO - add docstring + Project I(Q, φ) data onto I(Q) within a region defined by Cartesian limits. + + The projection is computed by averaging together datapoints with the same + angle φ (so long as they are within the ROI), measured anticlockwise from + the positive x-axis. + + This class is initialised by specifying lower and upper limits on both the + magnitude of Q and the angle φ. + When called, this class is supplied with a Data2D object. It returns a + Data1D object where intensity is given as a function of Q only. """ def __init__(self, r_min: float, r_max: float, phi_min: float, phi_max: float, nbins: int = 100) -> None: """ + Set up the ROI boundaries, and the binning of the output 1D data. + + The units are A^-1 for radial parameters, and radians for anglar ones. + :param r_min: Lower limit for |Q| values to use during averaging. + :param r_max: Upper limit for |Q| values to use during averaging. + :param phi_min: Lower limit for φ values (in the primary ROI). + :param phi_max: Upper limit for φ values (in the primary ROI). + :param nbins: The number of bins data is sorted into along the |Q| axis """ super().__init__(r_min=r_min, r_max=r_max, phi_min=phi_min, phi_max=phi_max) @@ -567,10 +778,15 @@ def __init__(self, r_min: float, r_max: float, phi_min: float, def __call__(self, data2d: Data2D = None) -> Data1D: """ + Compute the 1D average of 2D data, projecting along the Q_y axis. + + :param data2d: The Data2D object for which the average is computed. + :return: Data1D object for plotting. """ self.validate_and_assign_data(data2d) - # Transform all angles to the range [0,2π), where phi_min is at zero. + # Transform all angles to the range [0,2π) where phi_min is at zero, + # eliminating errors when the ROI straddles the 2π -> 0 discontinuity. # We won't need to convert back later because we're plotting against Q. phi_offset = self.phi_min self.phi_min = 0.0 @@ -585,8 +801,8 @@ def __call__(self, data2d: Data2D = None) -> Data1D: else: minor_lims = (self.phi_min, self.phi_max) - directional_average = DirectionalAverage(major_data=self.q_data, - minor_data=self.phi_data, + directional_average = DirectionalAverage(major_axis=self.q_data, + minor_axis=self.phi_data, major_lims=major_lims, minor_lims=minor_lims, nbins=self.nbins) @@ -598,12 +814,29 @@ def __call__(self, data2d: Data2D = None) -> Data1D: class WedgePhi(PolarROI): """ - TODO - add docstring + Project I(Q, φ) data onto I(φ) within a region defined by Cartesian limits. + + The projection is computed by averaging together datapoints with the same + Q value (so long as they are within the ROI). + + This class is initialised by specifying lower and upper limits on both the + magnitude of Q and the angle φ, measured anticlockwise from the positive + x-axis. + When called, this class is supplied with a Data2D object. It returns a + Data1D object where intensity is given as a function of Q only. """ def __init__(self, r_min: float, r_max: float, phi_min: float, phi_max: float, nbins: int = 100) -> None: """ + Set up the ROI boundaries, and the binning of the output 1D data. + + The units are A^-1 for radial parameters, and radians for anglar ones. + :param r_min: Lower limit for |Q| values to use during averaging. + :param r_max: Upper limit for |Q| values to use during averaging. + :param phi_min: Lower limit for φ values to use during averaging. + :param phi_max: Upper limit for φ values to use during averaging. + :param nbins: The number of bins data is sorted into along the φ axis. """ super().__init__(r_min=r_min, r_max=r_max, phi_min=phi_min, phi_max=phi_max) @@ -611,12 +844,16 @@ def __init__(self, r_min: float, r_max: float, phi_min: float, def __call__(self, data2d: Data2D = None) -> Data1D: """ - TODO - add docstring + Compute the 1D average of 2D data, projecting along the Q_y axis. + + :param data2d: The Data2D object for which the average is computed. + :return: Data1D object for plotting. """ self.validate_and_assign_data(data2d) - # Transform all angles to the range [0,2π), where phi_min is at zero. - # Remember to transform back afterwards + # Transform all angles to the range [0,2π) where phi_min is at zero, + # eliminating errors when the ROI straddles the 2π -> 0 discontinuity. + # Remember to transform back afterwards as we're plotting against phi. phi_offset = self.phi_min self.phi_min = 0.0 self.phi_max = (self.phi_max - phi_offset) % (2 * np.pi) @@ -630,8 +867,8 @@ def __call__(self, data2d: Data2D = None) -> Data1D: major_lims = (self.phi_min, self.phi_max) minor_lims = (self.r_min, self.r_max) - directional_average = DirectionalAverage(major_data=self.phi_data, - minor_data=self.q_data, + directional_average = DirectionalAverage(major_axis=self.phi_data, + minor_axis=self.q_data, major_lims=major_lims, minor_lims=minor_lims, nbins=self.nbins) From fb0da1ad5a1797c0bc54fdc265d61fb32f692619 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Sun, 17 Sep 2023 18:37:14 +0100 Subject: [PATCH 09/33] Replaced python logical_and with numpy logical_and for speed --- sasdata/data_util/new_manipulations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index 1c7de22..6fa3e6b 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -27,9 +27,9 @@ def weights_for_interval(array, l_bound, u_bound, interval_type='half-open'): # Half-open is used when binning the major axis (except for the final bin) # and closed used for the minor axis and the final bin of the major axis. if interval_type == 'half-open': - in_range = (l_bound <= array) & (array < u_bound) + in_range = np.logical_and(l_bound <= array, array < u_bound) elif interval_type == 'closed': - in_range = (l_bound <= array) & (array <= u_bound) + in_range = np.logical_and(l_bound <= array, array <= u_bound) else: msg = f"Unrecognised interval_type: {interval_type}" raise ValueError(msg) @@ -127,7 +127,7 @@ def get_bin_index(self, value): denominator = self.major_lims[1] - self.major_lims[0] bin_index = int(np.floor(self.nbins * numerator / denominator)) - # Bins are indexed from 0 to nbins-1, so tihs check protects against + # Bins are indexed from 0 to nbins-1, so this check protects against # out-of-range indices when value == self.major_lims[1] if bin_index == self.nbins: bin_index -= 1 From 1bcd84f12f28caad4bd9dff662dce0463c51a179 Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Sun, 17 Sep 2023 18:39:14 +0100 Subject: [PATCH 10/33] Removed some superfluous logical_and checks. Both arrays should have been identical anyway --- sasdata/data_util/new_manipulations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index 6fa3e6b..af2f758 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -180,7 +180,7 @@ def __call__(self, data, err_data): intensity /= bin_counts errors /= bin_counts - finite = (np.isfinite(x_axis_values) & np.isfinite(intensity)) + finite = np.isfinite(intensity) if not finite.any(): msg = "Average Error: No points inside ROI to average..." raise ValueError(msg) @@ -731,7 +731,7 @@ def __call__(self, data2d: Data2D = None) -> Data1D: average_intensity /= bin_counts combined_err = np.sqrt(combined_err) / bin_counts - finite = (np.isfinite(combined_q) & np.isfinite(average_intensity)) + finite = np.isfinite(average_intensity) data1d = Data1D(x=combined_q[finite], y=average_intensity[finite], dy=combined_err[finite]) From 78df64bbd9cfddbc2e5cf17b496f4a85d8e5d93d Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Mon, 18 Sep 2023 10:53:31 +0100 Subject: [PATCH 11/33] Added scipy to dependencies --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 184860a..3eb7a9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ lxml # Calculation numpy +scipy # Unit testing pytest From 980fafed49cb3cd0d93f5ff46c0164076e716d0f Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Mon, 18 Sep 2023 11:20:22 +0100 Subject: [PATCH 12/33] Forgot to remove 'angles + np.pi' from SectorQ call, no longer needed since rewrite. --- test/sasdataloader/utest_averaging_analytical.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/sasdataloader/utest_averaging_analytical.py b/test/sasdataloader/utest_averaging_analytical.py index 358dd33..58842d0 100644 --- a/test/sasdataloader/utest_averaging_analytical.py +++ b/test/sasdataloader/utest_averaging_analytical.py @@ -901,9 +901,8 @@ def test_sectorq_averaging_with_fold(self): phi_max = 5*np.pi/6 nbins = int(test_data.matrix_size * np.sqrt(2)/4) # usually reliable - wedge_object = SectorQ(r_min=r_min, r_max=r_max, - phi_min=phi_min + np.pi, - phi_max=phi_max + np.pi, nbins=nbins) + wedge_object = SectorQ(r_min=r_min, r_max=r_max, phi_min=phi_min, + phi_max=phi_max, nbins=nbins) # Explicitly set fold to True - points either side of 0,0 are averaged wedge_object.fold = True data1d = wedge_object(averager_data.data) From cb2752179c55b3d2ca44771192db59591b6f2c5f Mon Sep 17 00:00:00 2001 From: Ellis Hewins Date: Fri, 22 Sep 2023 18:10:48 +0100 Subject: [PATCH 13/33] Added unit tests for DirectionalAverage class --- sasdata/data_util/new_manipulations.py | 4 + .../utest_averaging_analytical.py | 153 +++++++++++++++++- 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index af2f758..572f3d4 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -88,7 +88,11 @@ def __init__(self, major_axis=None, minor_axis=None, major_lims=None, self.major_axis = np.asarray(major_axis) self.minor_axis = np.asarray(minor_axis) + if self.major_axis.size != self.minor_axis.size: + msg = "Major and minor axes must have same length" + raise ValueError(msg) # In some cases all values from a given axis are part of the ROI. + # An alternative approach may be needed for fractional weights. if major_lims is None: self.major_lims = (self.major_axis.min(), self.major_axis.max()) else: diff --git a/test/sasdataloader/utest_averaging_analytical.py b/test/sasdataloader/utest_averaging_analytical.py index 58842d0..53ce39a 100644 --- a/test/sasdataloader/utest_averaging_analytical.py +++ b/test/sasdataloader/utest_averaging_analytical.py @@ -13,7 +13,8 @@ from sasdata.dataloader import data_info from sasdata.data_util.new_manipulations import (SlabX, SlabY, Boxsum, Boxavg, CircularAverage, Ring, - SectorQ, WedgeQ, WedgePhi) + SectorQ, WedgeQ, WedgePhi, + DirectionalAverage) class MatrixToData2D: @@ -1041,5 +1042,155 @@ def test_wedgephi_averaging(self): self.assertAlmostEqual(actual_area, expected_area, 1) +class DirectionalAverageValidationTests(unittest.TestCase): + """ + This class tests DirectionalAverage's data validation checks. + """ + + def test_missing_coordinate_data(self): + """ + Ensure a ValueError is raised if no axis data is supplied. + """ + self.assertRaises(ValueError, DirectionalAverage, + major_axis=None, minor_axis=None) + + def test_inappropriate_limits_arrays(self): + """ + Ensure a ValueError is raised if the wrong number of limits is suppied. + """ + self.assertRaises(ValueError, DirectionalAverage, major_axis=[], + minor_axis=[], major_lims=[], minor_lims=[]) + + def test_nbins_not_int(self): + """ + Ensure a TypeError is raised if the parameter nbins is not an integer. + """ + self.assertRaises(TypeError, DirectionalAverage, major_axis=[], + minor_axis=[], nbins=10.0) + + def test_axes_unequal_lengths(self): + """ + Ensure ValueError is raised if the major and minor axes don't match. + """ + self.assertRaises(ValueError, DirectionalAverage, major_axis=[0, 1, 2], + minor_axis=[3, 4]) + + def test_no_limits_on_an_axis(self): + """ + Ensure correct behaviour when there are no limits provided. + The min. and max. values from major/minor_axis are taken as the limits. + """ + dir_avg = DirectionalAverage(major_axis=[1, 2, 3], + minor_axis=[4, 5, 6]) + self.assertEqual(dir_avg.major_lims, (1, 3)) + self.assertEqual(dir_avg.minor_lims, (4, 6)) + + +class DirectionalAverageFunctionalityTests(unittest.TestCase): + """ + Placeholder + """ + + def setUp(self): + """ + Setup for the DirectionalAverageFunctionalityTests tests. + """ + + # 21 bins, with spacing 0.1 + self.qx_data = np.linspace(-1, 1, 21) + self.qy_data = self.qx_data + x, y = np.meshgrid(self.qx_data, self.qy_data) + # quadratic in x, linear in y + data = x * x * y + self.data2d = MatrixToData2D(data) + + # ROI is the first quadrant only. Same limits for both axes. + self.lims = (0.0, 1.0) + self.in_roi = (self.lims[0] <= self.qx_data) & \ + (self.qx_data <= self.lims[1]) + self.nbins = int(np.sum(self.in_roi)) + # Note that the bin width is less than the spacing of the datapoints, + # because we're insisting that there be as many bins as datapoints. + self.bin_width = (self.lims[1] - self.lims[0]) / self.nbins + + self.directional_average = \ + DirectionalAverage(major_axis=self.data2d.data.qx_data, + minor_axis=self.data2d.data.qy_data, + major_lims=self.lims, + minor_lims=self.lims, nbins=self.nbins) + + def test_bin_width(self): + """ + Test that the bin width is calculated correctly. + """ + self.assertAlmostEqual(self.directional_average.bin_width, + self.bin_width) + + def test_get_bin_interval(self): + """ + Test that the get_bin_interval method works correctly. + """ + for b in range(self.nbins): + bin_start, bin_end = self.directional_average.get_bin_interval(b) + expected_bin_start = self.lims[0] + b * self.bin_width + expected_bin_end = self.lims[0] + (b + 1) * self.bin_width + self.assertAlmostEqual(bin_start, expected_bin_start, 10) + self.assertAlmostEqual(bin_end, expected_bin_end, 10) + + def test_get_bin_index(self): + """ + Test that the get_bin_index method works correctly. + """ + # use values at the edges of bins, and values in the middles + values = np.linspace(self.lims[0], self.lims[1], self.nbins * 2) + expected_indices = np.repeat(np.arange(self.nbins), 2) + for n, v in enumerate(values): + self.assertAlmostEqual(self.directional_average.get_bin_index(v), + expected_indices[n], 10) + + def test_binary_weights(self): + """ + Test weights are calculated correctly when the bins & ROI are aligned. + When aligned perfectly, the weights should be ones and zeros only. + + Variations on this test will be needed once fractional weighting is + possible. These should have ROIs which do not line up perfectly with + the bins. + """ + + # I think this test needs mocks, it'd be very complex otherwise. + # I'm struggling to come up with a test for this one. + pass + + def test_directional_averaging(self): + """ + Test that a directinal average is computed correctly. + + Variations on this test will be needed once fractional weighting is + possible. These should have ROIs which do not line up perfectly with + the bins. + """ + x_axis_values, intensity, errors = \ + self.directional_average(data=self.data2d.data.data, + err_data=self.data2d.data.err_data) + + expected_x = self.qx_data[self.in_roi] + expected_intensity = np.mean(self.qy_data[self.in_roi]) * expected_x**2 + + np.testing.assert_array_almost_equal(x_axis_values, expected_x, 10) + np.testing.assert_array_almost_equal(intensity, expected_intensity, 10) + # TODO - also implement check for correct errors + + def test_no_points_in_roi(self): + """ + Test that ValueError is raised if there were on points in the ROI. + """ + # move the region of interest to outside the range of the data + self.directional_average.major_lims = (2, 3) + self.directional_average.minor_lims = (2, 3) + self.assertRaises(ValueError, self.directional_average, + self.data2d.data.data, self.data2d.data.err_data) + + if __name__ == '__main__': unittest.main() From f13fad470d86442a8d62c24100058c4b24ba43a5 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 16 Oct 2023 10:03:28 -0400 Subject: [PATCH 14/33] Move averaging tests from data loader to manipulations folder --- test/{sasdataloader => sasmanipulations}/utest_averaging.py | 0 .../utest_averaging_analytical.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename test/{sasdataloader => sasmanipulations}/utest_averaging.py (100%) rename test/{sasdataloader => sasmanipulations}/utest_averaging_analytical.py (100%) diff --git a/test/sasdataloader/utest_averaging.py b/test/sasmanipulations/utest_averaging.py similarity index 100% rename from test/sasdataloader/utest_averaging.py rename to test/sasmanipulations/utest_averaging.py diff --git a/test/sasdataloader/utest_averaging_analytical.py b/test/sasmanipulations/utest_averaging_analytical.py similarity index 100% rename from test/sasdataloader/utest_averaging_analytical.py rename to test/sasmanipulations/utest_averaging_analytical.py From 7b46314dbc48d935e425061978474950cf277b93 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 16 Oct 2023 11:08:59 -0400 Subject: [PATCH 15/33] Move files used in averaging tests --- test/sasmanipulations/data/MAR07232_rest.h5 | Bin 0 -> 544104 bytes test/sasmanipulations/data/avg_testdata.txt | 21 ++++++++++++++++++ .../data/ring_testdata.txt | 0 .../data/sectorphi_testdata.txt | 0 .../data/sectorq_testdata.txt | 0 .../data/slabx_testdata.txt | 0 .../data/slaby_testdata.txt | 0 7 files changed, 21 insertions(+) create mode 100644 test/sasmanipulations/data/MAR07232_rest.h5 create mode 100644 test/sasmanipulations/data/avg_testdata.txt rename test/{sasdataloader => sasmanipulations}/data/ring_testdata.txt (100%) rename test/{sasdataloader => sasmanipulations}/data/sectorphi_testdata.txt (100%) rename test/{sasdataloader => sasmanipulations}/data/sectorq_testdata.txt (100%) rename test/{sasdataloader => sasmanipulations}/data/slabx_testdata.txt (100%) rename test/{sasdataloader => sasmanipulations}/data/slaby_testdata.txt (100%) diff --git a/test/sasmanipulations/data/MAR07232_rest.h5 b/test/sasmanipulations/data/MAR07232_rest.h5 new file mode 100644 index 0000000000000000000000000000000000000000..053eecaf64d449f3b8328ff7ea02a4f3b7962978 GIT binary patch literal 544104 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%0D2~ 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 literal 0 HcmV?d00001 diff --git a/test/sasmanipulations/data/avg_testdata.txt b/test/sasmanipulations/data/avg_testdata.txt new file mode 100644 index 0000000..3fd5dde --- /dev/null +++ b/test/sasmanipulations/data/avg_testdata.txt @@ -0,0 +1,21 @@ +0.00019987186878 -0.01196215 0.148605728355 +0.000453772721237 0.02091606 0.23372601 +0.000750492390439 -0.01337855 0.17169562 +0.00103996394336 0.03062 0.13136407 +0.0013420198959 0.0811008333333 0.10681163 +0.001652061869 0.167022288372 0.10098903 +0.00196086470492 27.5554711176 0.7350533 +0.00226262401224 105.031578947 1.35744586624 +0.00256734439716 82.1791776119 1.10749938588 +0.0028637128388 54.714657971 0.890486416264 +0.00315257408712 36.8455584416 0.691746880003 +0.00344644126616 24.8938701149 0.534917225468 +0.00374248202229 16.5905619565 0.424655384023 +0.00404393067437 11.4714217925 0.328969543128 +0.004346317814 8.05405805556 0.273083524998 +0.00465162170627 5.5823291129 0.21217630209 +0.00495449803049 4.2574845082 0.186808495528 +0.00525641407066 3.30448963768 0.154743584955 +0.00555735057365 2.6995389781 0.140373302568 +0.00585577429002 2.03298512 0.116418358232 + diff --git a/test/sasdataloader/data/ring_testdata.txt b/test/sasmanipulations/data/ring_testdata.txt similarity index 100% rename from test/sasdataloader/data/ring_testdata.txt rename to test/sasmanipulations/data/ring_testdata.txt diff --git a/test/sasdataloader/data/sectorphi_testdata.txt b/test/sasmanipulations/data/sectorphi_testdata.txt similarity index 100% rename from test/sasdataloader/data/sectorphi_testdata.txt rename to test/sasmanipulations/data/sectorphi_testdata.txt diff --git a/test/sasdataloader/data/sectorq_testdata.txt b/test/sasmanipulations/data/sectorq_testdata.txt similarity index 100% rename from test/sasdataloader/data/sectorq_testdata.txt rename to test/sasmanipulations/data/sectorq_testdata.txt diff --git a/test/sasdataloader/data/slabx_testdata.txt b/test/sasmanipulations/data/slabx_testdata.txt similarity index 100% rename from test/sasdataloader/data/slabx_testdata.txt rename to test/sasmanipulations/data/slabx_testdata.txt diff --git a/test/sasdataloader/data/slaby_testdata.txt b/test/sasmanipulations/data/slaby_testdata.txt similarity index 100% rename from test/sasdataloader/data/slaby_testdata.txt rename to test/sasmanipulations/data/slaby_testdata.txt From f7950c4c8eda491020891ec9cdffeccdf980eb60 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 16 Oct 2023 11:09:37 -0400 Subject: [PATCH 16/33] Create and apply interval type enum to remove hard-coded strings --- sasdata/data_util/new_manipulations.py | 84 +++++++++++++------------- 1 file changed, 43 insertions(+), 41 deletions(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index 572f3d4..8b11662 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -1,40 +1,44 @@ """ This module contains various data processors used by Sasview's slicers. """ - +from enum import StrEnum, auto import numpy as np from sasdata.dataloader.data_info import Data1D, Data2D -def weights_for_interval(array, l_bound, u_bound, interval_type='half-open'): - """ - Weight coordinate data by position relative to a specified interval. - - :param array: the array for which the weights are calculated - :param l_bound: value defining the lower limit of the region of interest - :param u_bound: value defining the upper limit of the region of interest - :param interval_type: determines whether the value defined by u_bound is - included within the interval. - - If and when fractional binning is implemented (ask Lucas), this function - will be changed so that instead of outputting zeros and ones, it gives - fractional values instead. These will depend on how close the array value - is to being within the interval defined. - """ +class IntervalType(StrEnum): + HALF_OPEN = auto() + CLOSED = auto() + + def weights_for_interval(self, array, l_bound, u_bound): + """ + Weight coordinate data by position relative to a specified interval. - # Whether the endpoint should be included depends on circumstance. - # Half-open is used when binning the major axis (except for the final bin) - # and closed used for the minor axis and the final bin of the major axis. - if interval_type == 'half-open': - in_range = np.logical_and(l_bound <= array, array < u_bound) - elif interval_type == 'closed': - in_range = np.logical_and(l_bound <= array, array <= u_bound) - else: - msg = f"Unrecognised interval_type: {interval_type}" - raise ValueError(msg) + :param array: the array for which the weights are calculated + :param l_bound: value defining the lower limit of the region of interest + :param u_bound: value defining the upper limit of the region of interest + :param interval_type: determines whether the value defined by u_bound is + included within the interval. + + If and when fractional binning is implemented (ask Lucas), this function + will be changed so that instead of outputting zeros and ones, it gives + fractional values instead. These will depend on how close the array value + is to being within the interval defined. + """ + + # Whether the endpoint should be included depends on circumstance. + # Half-open is used when binning the major axis (except for the final bin) + # and closed used for the minor axis and the final bin of the major axis. + if self.name.lower() == 'half_open': + in_range = np.logical_and(l_bound <= array, array < u_bound) + elif self.name.lower() == 'closed': + in_range = np.logical_and(l_bound <= array, array <= u_bound) + else: + msg = f"Unrecognised interval_type: {self.name}" + raise ValueError(msg) - return np.asarray(in_range, dtype=int) + return np.asarray(in_range, dtype=int) class DirectionalAverage: @@ -146,23 +150,22 @@ def compute_weights(self): index. """ major_weights = np.zeros((self.nbins, self.major_axis.size)) + closed = IntervalType.CLOSED for m in range(self.nbins): # Include the value at the end of the binning range, but in # general use half-open intervals so each value belongs in only # one bin. if m == self.nbins - 1: - interval = 'closed' + interval = closed else: - interval = 'half-open' + interval = IntervalType.HALF_OPEN bin_start, bin_end = self.get_bin_interval(bin_number=m) - major_weights[m] = weights_for_interval(array=self.major_axis, + major_weights[m] = interval.weights_for_interval(array=self.major_axis, l_bound=bin_start, - u_bound=bin_end, - interval_type=interval) - minor_weights = weights_for_interval(array=self.minor_axis, + u_bound=bin_end) + minor_weights = closed.weights_for_interval(array=self.minor_axis, l_bound=self.minor_lims[0], - u_bound=self.minor_lims[1], - interval_type='closed') + u_bound=self.minor_lims[1]) return major_weights * minor_weights def __call__(self, data, err_data): @@ -355,14 +358,13 @@ def _sum(self) -> float: """ # Currently the weights are binary, but could be fractional in future - x_weights = weights_for_interval(array=self.qx_data, + interval = IntervalType.CLOSED + x_weights = interval.weights_for_interval(array=self.qx_data, l_bound=self.qx_min, - u_bound=self.qx_max, - interval_type='closed') - y_weights = weights_for_interval(array=self.qy_data, + u_bound=self.qx_max) + y_weights = interval.weights_for_interval(array=self.qy_data, l_bound=self.qy_min, - u_bound=self.qy_max, - interval_type='closed') + u_bound=self.qy_max) weights = x_weights * y_weights data = weights * self.data From 17bb08c9e41d2711eaf72d4328f831868958df4c Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 16 Oct 2023 11:48:41 -0400 Subject: [PATCH 17/33] Allow for non-linear bin spacings in the directional averaging --- sasdata/data_util/new_manipulations.py | 34 +++++++++++++++++--------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/new_manipulations.py index 8b11662..64713cd 100644 --- a/sasdata/data_util/new_manipulations.py +++ b/sasdata/data_util/new_manipulations.py @@ -106,24 +106,33 @@ def __init__(self, major_axis=None, minor_axis=None, major_lims=None, else: self.minor_lims = minor_lims self.nbins = nbins + # Assume a linear spacing for now, but allow for log, fibonacci, etc. implementations in the future + # Add one to bin because this is for the limits, not centroids. + self.bin_limits = np.linspace(self.major_lims[0], self.major_lims[1], self.nbins + 1) @property - def bin_width(self): - """ - Return the bin width based on the range of the major axis and nbins + def bin_widths(self) -> np.ndarray: + """Return a numpy array of all bin widths, regardless of the point spacings.""" + return np.asarray([self.bin_width_n(i) for i in range(0, self.nbins)]) + + def bin_width_n(self, bin_number: int) -> float: + """Calculate the bin width for the nth bin. + :param bin_number: The starting array index of the bin between 0 and self.nbins - 1. + :return: The bin width, as a float. """ - return (self.major_lims[1] - self.major_lims[0]) / self.nbins + lower, upper = self.get_bin_interval(bin_number) + return upper - lower - def get_bin_interval(self, bin_number): + def get_bin_interval(self, bin_number: int) -> (float, float): """ - Return the upper and lower limits defining a bin, given its index. + Return the lower and upper limits defining a bin, given its index. :param bin_number: The index of the bin (between 0 and self.nbins - 1) + :return: A tuple of the interval limits as (lower, upper). """ - bin_start = self.major_lims[0] + bin_number * self.bin_width - bin_end = self.major_lims[0] + (bin_number + 1) * self.bin_width - - return bin_start, bin_end + # Ensure bin_number is an integer and not a float or a string representation + bin_number = int(bin_number) + return self.bin_limits[bin_number], self.bin_limits[bin_number+1] def get_bin_index(self, value): """ @@ -859,7 +868,7 @@ def __call__(self, data2d: Data2D = None) -> Data1D: # Transform all angles to the range [0,2π) where phi_min is at zero, # eliminating errors when the ROI straddles the 2π -> 0 discontinuity. - # Remember to transform back afterwards as we're plotting against phi. + # Remember to transform back afterward as we're plotting against phi. phi_offset = self.phi_min self.phi_min = 0.0 self.phi_max = (self.phi_max - phi_offset) % (2 * np.pi) @@ -886,7 +895,8 @@ def __call__(self, data2d: Data2D = None) -> Data1D: # In the old manipulations.py, we also had this shift to plot the data # at the centre of the bins. I'm not sure why it's only angular binning # which gets this treatment. - phi_data += directional_average.bin_width / 2 + # TODO: Update this once non-linear binning options are implemented + phi_data += directional_average.bin_widths / 2 return Data1D(x=phi_data, y=intensity, dy=error) From a95eee0a4d4864876be8ab2ac76ddee8204ef133 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 16 Oct 2023 11:49:07 -0400 Subject: [PATCH 18/33] Update unit tests to account for new bin widths --- test/sasmanipulations/utest_averaging_analytical.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/sasmanipulations/utest_averaging_analytical.py b/test/sasmanipulations/utest_averaging_analytical.py index 53ce39a..9b78835 100644 --- a/test/sasmanipulations/utest_averaging_analytical.py +++ b/test/sasmanipulations/utest_averaging_analytical.py @@ -1123,8 +1123,7 @@ def test_bin_width(self): """ Test that the bin width is calculated correctly. """ - self.assertAlmostEqual(self.directional_average.bin_width, - self.bin_width) + self.assertAlmostEqual(np.average(self.directional_average.bin_widths), self.bin_width) def test_get_bin_interval(self): """ From b41b7f60dfdfd41d78e47b2b03b44c1fdef26687 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 16 Oct 2023 12:49:39 -0400 Subject: [PATCH 19/33] Rename manipulations_new to averaging and update internal references to match --- sasdata/data_util/{new_manipulations.py => averaging.py} | 0 test/sasmanipulations/utest_averaging_analytical.py | 6 ++---- 2 files changed, 2 insertions(+), 4 deletions(-) rename sasdata/data_util/{new_manipulations.py => averaging.py} (100%) diff --git a/sasdata/data_util/new_manipulations.py b/sasdata/data_util/averaging.py similarity index 100% rename from sasdata/data_util/new_manipulations.py rename to sasdata/data_util/averaging.py diff --git a/test/sasmanipulations/utest_averaging_analytical.py b/test/sasmanipulations/utest_averaging_analytical.py index 9b78835..e6f09d8 100644 --- a/test/sasmanipulations/utest_averaging_analytical.py +++ b/test/sasmanipulations/utest_averaging_analytical.py @@ -11,10 +11,8 @@ from scipy import integrate from sasdata.dataloader import data_info -from sasdata.data_util.new_manipulations import (SlabX, SlabY, Boxsum, Boxavg, - CircularAverage, Ring, - SectorQ, WedgeQ, WedgePhi, - DirectionalAverage) +from sasdata.data_util.averaging import (SlabX, SlabY, Boxsum, Boxavg, CircularAverage, Ring, + SectorQ, WedgeQ, WedgePhi, DirectionalAverage) class MatrixToData2D: From d7996ef031282118d1d9cc31cfa428b4d25c6afe Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 16 Oct 2023 12:50:37 -0400 Subject: [PATCH 20/33] Add deprecation warning that is triggered on import of manipulations.py --- sasdata/data_util/manipulations.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sasdata/data_util/manipulations.py b/sasdata/data_util/manipulations.py index 4212c20..504ebfb 100644 --- a/sasdata/data_util/manipulations.py +++ b/sasdata/data_util/manipulations.py @@ -22,9 +22,12 @@ import math import numpy as np from typing import Optional, Union +from warnings import warn from sasdata.dataloader.data_info import Data1D, Data2D +warn("sasdata.data_util.manipulations is deprecated and replaced by sasdata.data_util.averaging.", + DeprecationWarning, stacklevel=2) def position_and_wavelength_to_q(dx: float, dy: float, detector_distance: float, wavelength: float) -> float: """ From 06da0600e812f1f67dad54a395ae036d7c11f438 Mon Sep 17 00:00:00 2001 From: krzywon Date: Mon, 16 Oct 2023 13:14:58 -0400 Subject: [PATCH 21/33] Use Enum instead of StrEnum to ensure backwards compatibility with python versions 3.8 and 3.9 --- sasdata/data_util/averaging.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sasdata/data_util/averaging.py b/sasdata/data_util/averaging.py index 64713cd..c3f8a07 100644 --- a/sasdata/data_util/averaging.py +++ b/sasdata/data_util/averaging.py @@ -1,13 +1,13 @@ """ This module contains various data processors used by Sasview's slicers. """ -from enum import StrEnum, auto +from enum import Enum, auto import numpy as np from sasdata.dataloader.data_info import Data1D, Data2D -class IntervalType(StrEnum): +class IntervalType(Enum): HALF_OPEN = auto() CLOSED = auto() From 715a8d6c2e54105ff29cb7784f0b04770f088687 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Tue, 24 Oct 2023 17:14:52 +0100 Subject: [PATCH 22/33] Grammar --- sasdata/data_util/manipulations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sasdata/data_util/manipulations.py b/sasdata/data_util/manipulations.py index 504ebfb..bb9152d 100644 --- a/sasdata/data_util/manipulations.py +++ b/sasdata/data_util/manipulations.py @@ -199,7 +199,7 @@ def get_dq_data(data2d: Data2D) -> np.array: Get the dq for resolution averaging The pinholes and det. pix contribution present in both direction of the 2D which must be subtracted when - converting to 1D: dq_overlap should calculated ideally at + converting to 1D: dq_overlap should be calculated ideally at q = 0. Note This method works on only pinhole geometry. Extrapolate dqx(r) and dqy(phi) at q = 0, and take an average. ''' From 71560b9f4b6fc4f30e0d272331e8963e20e0c7a9 Mon Sep 17 00:00:00 2001 From: krzywon Date: Tue, 24 Oct 2023 15:24:24 -0400 Subject: [PATCH 23/33] Move 2D data restructure function to data_info where it is more semantically relevant --- sasdata/data_util/manipulations.py | 38 ------------------------------ sasdata/dataloader/data_info.py | 38 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/sasdata/data_util/manipulations.py b/sasdata/data_util/manipulations.py index 504ebfb..ab1745f 100644 --- a/sasdata/data_util/manipulations.py +++ b/sasdata/data_util/manipulations.py @@ -235,44 +235,6 @@ def get_dq_data(data2d: Data2D) -> np.array: dq_data = np.sqrt(dqx_data**2 + dqx_data**2) return dq_data -################################################################################ - - -def reader2D_converter(data2d: Optional[Data2D] = None) -> Data2D: - """ - convert old 2d format opened by IhorReader or danse_reader - to new Data2D format - This is mainly used by the Readers - - :param data2d: 2d array of Data2D object - :return: 1d arrays of Data2D object - - """ - if data2d.data is None or data2d.x_bins is None or data2d.y_bins is None: - raise ValueError("Can't convert this data: data=None...") - new_x = np.tile(data2d.x_bins, (len(data2d.y_bins), 1)) - new_y = np.tile(data2d.y_bins, (len(data2d.x_bins), 1)) - new_y = new_y.swapaxes(0, 1) - - new_data = data2d.data.flatten() - qx_data = new_x.flatten() - qy_data = new_y.flatten() - q_data = np.sqrt(qx_data * qx_data + qy_data * qy_data) - if data2d.err_data is None or np.any(data2d.err_data <= 0): - new_err_data = np.sqrt(np.abs(new_data)) - else: - new_err_data = data2d.err_data.flatten() - mask = np.ones(len(new_data), dtype=bool) - - output = data2d - output.data = new_data - output.err_data = new_err_data - output.qx_data = qx_data - output.qy_data = qy_data - output.q_data = q_data - output.mask = mask - - return output ################################################################################ diff --git a/sasdata/dataloader/data_info.py b/sasdata/dataloader/data_info.py index 2135928..cfa0b93 100644 --- a/sasdata/dataloader/data_info.py +++ b/sasdata/dataloader/data_info.py @@ -22,6 +22,7 @@ import math from math import fabs import copy +from typing import Optional import numpy as np @@ -1235,6 +1236,43 @@ def _perform_union(self, other): return result +def reader2D_converter(data2d: Optional[Data2D] = None) -> Data2D: + """ + convert old 2d format opened by IhorReader or danse_reader + to new Data2D format + This is mainly used by the Readers + + :param data2d: 2d array of Data2D object + :return: 1d arrays of Data2D object + + """ + if data2d.data is None or data2d.x_bins is None or data2d.y_bins is None: + raise ValueError("Can't convert this data: data=None...") + new_x = np.tile(data2d.x_bins, (len(data2d.y_bins), 1)) + new_y = np.tile(data2d.y_bins, (len(data2d.x_bins), 1)) + new_y = new_y.swapaxes(0, 1) + + new_data = data2d.data.flatten() + qx_data = new_x.flatten() + qy_data = new_y.flatten() + q_data = np.sqrt(qx_data * qx_data + qy_data * qy_data) + if data2d.err_data is None or np.any(data2d.err_data <= 0): + new_err_data = np.sqrt(np.abs(new_data)) + else: + new_err_data = data2d.err_data.flatten() + mask = np.ones(len(new_data), dtype=bool) + + output = data2d + output.data = new_data + output.err_data = new_err_data + output.qx_data = qx_data + output.qy_data = qy_data + output.q_data = q_data + output.mask = mask + + return output + + def combine_data_info_with_plottable(data, datainfo): """ A function that combines the DataInfo data in self.current_datainto with a From 4020cefe3e5a1d174587a6ef35bf6cb47cf453b0 Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 25 Oct 2023 12:17:04 -0400 Subject: [PATCH 24/33] Revert removal of reader2d_converter from manipulations --- sasdata/data_util/manipulations.py | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/sasdata/data_util/manipulations.py b/sasdata/data_util/manipulations.py index bd152a0..bb9152d 100644 --- a/sasdata/data_util/manipulations.py +++ b/sasdata/data_util/manipulations.py @@ -235,6 +235,44 @@ def get_dq_data(data2d: Data2D) -> np.array: dq_data = np.sqrt(dqx_data**2 + dqx_data**2) return dq_data +################################################################################ + + +def reader2D_converter(data2d: Optional[Data2D] = None) -> Data2D: + """ + convert old 2d format opened by IhorReader or danse_reader + to new Data2D format + This is mainly used by the Readers + + :param data2d: 2d array of Data2D object + :return: 1d arrays of Data2D object + + """ + if data2d.data is None or data2d.x_bins is None or data2d.y_bins is None: + raise ValueError("Can't convert this data: data=None...") + new_x = np.tile(data2d.x_bins, (len(data2d.y_bins), 1)) + new_y = np.tile(data2d.y_bins, (len(data2d.x_bins), 1)) + new_y = new_y.swapaxes(0, 1) + + new_data = data2d.data.flatten() + qx_data = new_x.flatten() + qy_data = new_y.flatten() + q_data = np.sqrt(qx_data * qx_data + qy_data * qy_data) + if data2d.err_data is None or np.any(data2d.err_data <= 0): + new_err_data = np.sqrt(np.abs(new_data)) + else: + new_err_data = data2d.err_data.flatten() + mask = np.ones(len(new_data), dtype=bool) + + output = data2d + output.data = new_data + output.err_data = new_err_data + output.qx_data = qx_data + output.qy_data = qy_data + output.q_data = q_data + output.mask = mask + + return output ################################################################################ From 31ed62af2ee1b401821593e405d49d7285c612c0 Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 25 Oct 2023 12:20:58 -0400 Subject: [PATCH 25/33] Update deprecation messages --- sasdata/data_util/manipulations.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sasdata/data_util/manipulations.py b/sasdata/data_util/manipulations.py index bb9152d..b6d720d 100644 --- a/sasdata/data_util/manipulations.py +++ b/sasdata/data_util/manipulations.py @@ -26,8 +26,9 @@ from sasdata.dataloader.data_info import Data1D, Data2D -warn("sasdata.data_util.manipulations is deprecated and replaced by sasdata.data_util.averaging.", - DeprecationWarning, stacklevel=2) +warn("sasdata.data_util.manipulations is deprecated. Unless otherwise noted, update your import to " + "sasdata.data_util.averaging.", DeprecationWarning, stacklevel=2) + def position_and_wavelength_to_q(dx: float, dy: float, detector_distance: float, wavelength: float) -> float: """ @@ -248,6 +249,8 @@ def reader2D_converter(data2d: Optional[Data2D] = None) -> Data2D: :return: 1d arrays of Data2D object """ + warn("reader2D_converter should be imported in the future sasdata.dataloader.data_info.", + DeprecationWarning, stacklevel=2) if data2d.data is None or data2d.x_bins is None or data2d.y_bins is None: raise ValueError("Can't convert this data: data=None...") new_x = np.tile(data2d.x_bins, (len(data2d.y_bins), 1)) From 85842d7bdb50c36c220fb5230863d2de71127c4f Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 25 Oct 2023 12:57:59 -0400 Subject: [PATCH 26/33] Port RingCut from manipulations to averaging --- sasdata/data_util/averaging.py | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/sasdata/data_util/averaging.py b/sasdata/data_util/averaging.py index c3f8a07..ec45c71 100644 --- a/sasdata/data_util/averaging.py +++ b/sasdata/data_util/averaging.py @@ -915,3 +915,38 @@ class SectorPhi(WedgePhi): # use through WedgeSlicer.py, so the rewritten version of this class has # been named WedgePhi. +################################################################################ + + +class Ringcut(PolarROI): + """ + Defines a ring on a 2D data set. + The ring is defined by r_min, r_max, and + the position of the center of the ring. + + The data returned is the region inside the ring + + Phi_min and phi_max should be defined between 0 and 2*pi + in anti-clockwise starting from the x- axis on the left-hand side + """ + + def __init__(self, r_min: float = 0.0, r_max: float = 0.0, phi_min: float = 0.0, phi_max: float = 2*np.pi): + super().__init__(r_min, r_max, phi_min, phi_max) + + def __call__(self, data2D: Data2D) -> np.ndarray[bool]: + """ + Apply the ring to the data set. + Returns the angular distribution for a given q range + + :param data2D: Data2D object + + :return: index array in the range + """ + super().validate_and_assign_data(data2D) + + # Get data + q_data = np.sqrt(self.qx_data * self.qx_data + self.qy_data * self.qy_data) + + # check whether each data point is inside ROI + out = (self.r_min <= q_data) & (self.r_max >= q_data) + return out From b92576ca15772bd1ae2ae30b70f7588a4a7a1c11 Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 25 Oct 2023 12:58:33 -0400 Subject: [PATCH 27/33] Port Boxcut from manipulations to averaging --- sasdata/data_util/averaging.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/sasdata/data_util/averaging.py b/sasdata/data_util/averaging.py index ec45c71..f63aae8 100644 --- a/sasdata/data_util/averaging.py +++ b/sasdata/data_util/averaging.py @@ -950,3 +950,27 @@ def __call__(self, data2D: Data2D) -> np.ndarray[bool]: # check whether each data point is inside ROI out = (self.r_min <= q_data) & (self.r_max >= q_data) return out + + +class Boxcut(CartesianROI): + """ + Find a rectangular 2D region of interest. + """ + + def __init__(self, x_min: float = 0.0, x_max: float = 0.0, y_min: float = 0.0, y_max: float = 0.0): + super().__init__(x_min, x_max, y_min, y_max) + + def __call__(self, data2D: Data2D) -> np.ndarray[bool]: + """ + Find a rectangular 2D region of interest where data points inside the ROI are True, and False otherwise + + :param data2D: Data2D object + :return: mask, 1d array (len = len(data)) + """ + super().validate_and_assign_data(data2D) + + # check whether each data point is inside ROI + outx = (self.qx_min <= self.qx_data) & (self.qx_max > self.qx_data) + outy = (self.qy_min <= self.qy_data) & (self.qy_max > self.qy_data) + + return outx & outy From 3e81f2d742fbbdbbf7954aa15355b2b7154477f3 Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 25 Oct 2023 12:58:56 -0400 Subject: [PATCH 28/33] Update documentation in manipulations to point to new test location --- sasdata/data_util/manipulations.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/sasdata/data_util/manipulations.py b/sasdata/data_util/manipulations.py index b6d720d..12f1c34 100644 --- a/sasdata/data_util/manipulations.py +++ b/sasdata/data_util/manipulations.py @@ -1,12 +1,11 @@ """ Data manipulations for 2D data sets. -Using the meta data information, various types of averaging -are performed in Q-space +Using the meta data information, various types of averaging are performed in Q-space To test this module use: ``` cd test -PYTHONPATH=../src/ python2 -m sasdataloader.test.utest_averaging DataInfoTests.test_sectorphi_quarter +PYTHONPATH=../src/ python2 -m sasmanipulations.test.utest_averaging DataInfoTests.test_sectorphi_quarter ``` """ ##################################################################### From 01f4daa53f3b28abd12f0d34c9b3f2fa92d8ab5b Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 25 Oct 2023 17:20:55 -0400 Subject: [PATCH 29/33] Move Sectorcut to averaging from manipulations --- sasdata/data_util/averaging.py | 49 ++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/sasdata/data_util/averaging.py b/sasdata/data_util/averaging.py index f63aae8..c065e23 100644 --- a/sasdata/data_util/averaging.py +++ b/sasdata/data_util/averaging.py @@ -974,3 +974,52 @@ def __call__(self, data2D: Data2D) -> np.ndarray[bool]: outy = (self.qy_min <= self.qy_data) & (self.qy_max > self.qy_data) return outx & outy + + +class Sectorcut(PolarROI): + """ + Defines a sector (major + minor) region on a 2D data set. + The sector is defined by phi_min, phi_max, + where phi_min and phi_max are defined by the right + and left lines wrt central line. + + Phi_min and phi_max are given in units of radian + and (phi_max-phi_min) should not be larger than pi + """ + + def __init__(self, phi_min: float = 0.0, phi_max: float = np.pi): + super().__init__(0, np.inf, phi_min, phi_max) + + def __call__(self, data2D: Data2D) -> np.ndarray[bool]: + """ + Find a rectangular 2D region of interest where data points inside the ROI are True, and False otherwise + + :param data2D: Data2D object + :return: mask, 1d array (len = len(data)) + """ + super().validate_and_assign_data(data2D) + + phi_offset = self.phi_min + self.phi_min = 0.0 + self.phi_max = (self.phi_max - phi_offset) % (2 * np.pi) + self.phi_data = (self.phi_data - phi_offset) % (2 * np.pi) + + phi_min_major, phi_max_major = (self.r_min, self.r_max) + phi_min_minor, phi_max_minor = (self.phi_min, self.phi_max) + + if phi_min_major > phi_max_major: + out_major = (phi_min_major <= self.phi_data) + \ + (phi_max_major > self.phi_data) + else: + out_major = (phi_min_major <= self.phi_data) & ( + phi_max_major > self.phi_data) + + if phi_min_minor > phi_max_minor: + out_minor = (phi_min_minor <= self.phi_data) + \ + (phi_max_minor >= self.phi_data) + else: + out_minor = (phi_min_minor <= self.phi_data) & \ + (phi_max_minor >= self.phi_data) + out = out_major & out_minor + + return out From f8d7cbfa9b4cd33beff95ebde7a366a2024c5cf7 Mon Sep 17 00:00:00 2001 From: krzywon Date: Wed, 25 Oct 2023 17:21:51 -0400 Subject: [PATCH 30/33] Use unmasked data for masking purposes --- sasdata/data_util/averaging.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sasdata/data_util/averaging.py b/sasdata/data_util/averaging.py index c065e23..163d07a 100644 --- a/sasdata/data_util/averaging.py +++ b/sasdata/data_util/averaging.py @@ -944,8 +944,8 @@ def __call__(self, data2D: Data2D) -> np.ndarray[bool]: """ super().validate_and_assign_data(data2D) - # Get data - q_data = np.sqrt(self.qx_data * self.qx_data + self.qy_data * self.qy_data) + # Calculate q_data using unmasked qx_data and qy_data + q_data = np.sqrt(data2D.qx_data * data2D.qx_data + data2D.qy_data * data2D.qy_data) # check whether each data point is inside ROI out = (self.r_min <= q_data) & (self.r_max >= q_data) @@ -970,8 +970,8 @@ def __call__(self, data2D: Data2D) -> np.ndarray[bool]: super().validate_and_assign_data(data2D) # check whether each data point is inside ROI - outx = (self.qx_min <= self.qx_data) & (self.qx_max > self.qx_data) - outy = (self.qy_min <= self.qy_data) & (self.qy_max > self.qy_data) + outx = (self.qx_min <= data2D.qx_data) & (self.qx_max > data2D.qx_data) + outy = (self.qy_min <= data2D.qy_data) & (self.qy_max > data2D.qy_data) return outx & outy From f657a53e49691fca4a860a998e21578080d39afe Mon Sep 17 00:00:00 2001 From: krzywon Date: Thu, 26 Oct 2023 15:37:36 -0400 Subject: [PATCH 31/33] Fix issue where sector cut only masked one half of region --- sasdata/data_util/averaging.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/sasdata/data_util/averaging.py b/sasdata/data_util/averaging.py index 163d07a..fb0379b 100644 --- a/sasdata/data_util/averaging.py +++ b/sasdata/data_util/averaging.py @@ -999,27 +999,27 @@ def __call__(self, data2D: Data2D) -> np.ndarray[bool]: """ super().validate_and_assign_data(data2D) + # Ensure unmasked data is used for the phi_data calculation to ensure data sizes match + self.phi_data = np.arctan2(data2D.qy_data, data2D.qx_data) + # Calculate q_data using unmasked qx_data and qy_data to ensure data sizes match + q_data = np.sqrt(data2D.qx_data * data2D.qx_data + data2D.qy_data * data2D.qy_data) + phi_offset = self.phi_min self.phi_min = 0.0 self.phi_max = (self.phi_max - phi_offset) % (2 * np.pi) self.phi_data = (self.phi_data - phi_offset) % (2 * np.pi) + phi_shifted = self.phi_data - np.pi - phi_min_major, phi_max_major = (self.r_min, self.r_max) - phi_min_minor, phi_max_minor = (self.phi_min, self.phi_max) + # Determine angular bounds for both upper and lower half of image + phi_min_angle, phi_max_angle = (self.phi_min, self.phi_max) - if phi_min_major > phi_max_major: - out_major = (phi_min_major <= self.phi_data) + \ - (phi_max_major > self.phi_data) - else: - out_major = (phi_min_major <= self.phi_data) & ( - phi_max_major > self.phi_data) + # Determine regions of interest + out_radial = (self.r_min <= q_data) & (self.r_max > q_data) + out_upper = (phi_min_angle <= self.phi_data) & (phi_max_angle >= self.phi_data) + out_lower = (phi_min_angle <= phi_shifted) & (phi_max_angle >= phi_shifted) - if phi_min_minor > phi_max_minor: - out_minor = (phi_min_minor <= self.phi_data) + \ - (phi_max_minor >= self.phi_data) - else: - out_minor = (phi_min_minor <= self.phi_data) & \ - (phi_max_minor >= self.phi_data) - out = out_major & out_minor + upper_roi = out_radial & out_upper + lower_roi = out_radial & out_lower + out = upper_roi | lower_roi return out From 8edf31049f3e790e19d24195774e0eccf5e1ec98 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Wed, 6 Dec 2023 14:51:43 +0000 Subject: [PATCH 32/33] Type hinting --- sasdata/data_util/averaging.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/sasdata/data_util/averaging.py b/sasdata/data_util/averaging.py index fb0379b..c2c1e6b 100644 --- a/sasdata/data_util/averaging.py +++ b/sasdata/data_util/averaging.py @@ -4,6 +4,9 @@ from enum import Enum, auto import numpy as np +from numpy.typing import ArrayLike +from typing import Optional + from sasdata.dataloader.data_info import Data1D, Data2D @@ -18,8 +21,6 @@ def weights_for_interval(self, array, l_bound, u_bound): :param array: the array for which the weights are calculated :param l_bound: value defining the lower limit of the region of interest :param u_bound: value defining the upper limit of the region of interest - :param interval_type: determines whether the value defined by u_bound is - included within the interval. If and when fractional binning is implemented (ask Lucas), this function will be changed so that instead of outputting zeros and ones, it gives @@ -63,8 +64,12 @@ class DirectionalAverage: upon by SasView however, so I haven't implemented it here (yet). """ - def __init__(self, major_axis=None, minor_axis=None, major_lims=None, - minor_lims=None, nbins=100): + def __init__(self, + major_axis: ArrayLike, + minor_axis: ArrayLike, + major_lims: Optional[tuple[float, float]]=None, + minor_lims: Optional[tuple[float, float]]=None, + nbins: int=100): """ Set up direction of averaging, limits on the ROI, & the number of bins. @@ -78,14 +83,17 @@ def __init__(self, major_axis=None, minor_axis=None, major_lims=None, axis. Given as a 2 element tuple/list. :param nbins: The number of bins the major axis is divided up into. """ - if any(not isinstance(coordinate_data, (list, np.ndarray)) for + + if any(not hasattr(coordinate_data, "__array__") for coordinate_data in (major_axis, minor_axis)): msg = "Must provide major & minor coordinate arrays for binning." raise ValueError(msg) + if any(lims is not None and len(lims) != 2 for lims in (major_lims, minor_lims)): msg = "Limits arrays must have 2 elements or be NoneType" raise ValueError(msg) + if not isinstance(nbins, int): msg = "Parameter 'nbins' must be an integer" raise TypeError(msg) @@ -677,6 +685,7 @@ def __init__(self, r_min: float, r_max: float, phi_min: float, """ super().__init__(r_min=r_min, r_max=r_max, phi_min=phi_min, phi_max=phi_max) + self.nbins = nbins self.fold = fold From 30f8bf0187c76bc68c7c6ad2f7e1cd4b6d606143 Mon Sep 17 00:00:00 2001 From: lucas-wilkins Date: Thu, 7 Dec 2023 10:15:13 +0000 Subject: [PATCH 33/33] Fix for first bug --- sasdata/data_util/averaging.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/sasdata/data_util/averaging.py b/sasdata/data_util/averaging.py index c2c1e6b..7f0597a 100644 --- a/sasdata/data_util/averaging.py +++ b/sasdata/data_util/averaging.py @@ -67,9 +67,9 @@ class DirectionalAverage: def __init__(self, major_axis: ArrayLike, minor_axis: ArrayLike, - major_lims: Optional[tuple[float, float]]=None, - minor_lims: Optional[tuple[float, float]]=None, - nbins: int=100): + major_lims: Optional[tuple[float, float]] = None, + minor_lims: Optional[tuple[float, float]] = None, + nbins: int = 100): """ Set up direction of averaging, limits on the ROI, & the number of bins. @@ -95,8 +95,12 @@ def __init__(self, raise ValueError(msg) if not isinstance(nbins, int): - msg = "Parameter 'nbins' must be an integer" - raise TypeError(msg) + # TODO: Make classes that depend on this provide ints, its quite a thing to fix though + try: + nbins = int(nbins) + except: + msg = f"Parameter 'nbins' must should be convertable to an integer via int(), got type {type(nbins)} (={nbins})" + raise TypeError(msg) self.major_axis = np.asarray(major_axis) self.minor_axis = np.asarray(minor_axis) @@ -685,7 +689,7 @@ def __init__(self, r_min: float, r_max: float, phi_min: float, """ super().__init__(r_min=r_min, r_max=r_max, phi_min=phi_min, phi_max=phi_max) - + self.nbins = nbins self.fold = fold