def test_optical_density_histogram(self): """ Test the optical density calculation is correct and stuffed with numpy.inf when the intensity is zero. """ bins = (200, 60) size = np.array([[-1, 1], [-1, 1]]) * 30 * u.cm intensity_results = cpr.synthetic_radiograph(self.sim_results, size=size, bins=bins) od_results = cpr.synthetic_radiograph(self.sim_results, size=size, bins=bins, optical_density=True) assert np.allclose(intensity_results[0], od_results[0]) assert np.allclose(intensity_results[1], od_results[1]) intensity = intensity_results[2] zero_mask = intensity == 0 i0 = np.mean(intensity[~zero_mask]) od = -np.log10(intensity / i0) assert np.allclose(od[~zero_mask], od_results[2][~zero_mask]) assert np.all(np.isposinf(od_results[2][zero_mask]))
def test_warns(self): """ Test warning when less than half the particles reach the detector plane. """ sim_results = self.sim_results.copy() sim_results["nparticles"] *= 3 with pytest.warns(RuntimeWarning): cpr.synthetic_radiograph(sim_results)
def run_1D_example(name): """ Run a simulation through an example with parameters optimized to sum up to a lineout along x. The goal is to run a relatively fast sim with a quasi-1D field grid that can then be summed to get good enough statistics to use as a test. """ grid = _test_grid(name, L=1 * u.mm, num=50) # Cartesian source = (0 * u.mm, -10 * u.mm, 0 * u.mm) detector = (0 * u.mm, 200 * u.mm, 0 * u.mm) # Expect warnings because these fields aren't well-behaved at the edges with pytest.warns( RuntimeWarning, match="Fields should go to zero at edges of grid to avoid "): sim = cpr.Tracker(grid, source, detector, verbose=False) sim.create_particles(1e4, 3 * u.MeV, max_theta=0.1 * u.deg) sim.run() size = np.array([[-1, 1], [-1, 1]]) * 10 * u.cm bins = [200, 60] hax, vax, values = cpr.synthetic_radiograph(sim, size=size, bins=bins) values = np.mean(values[:, 20:40], axis=1) return hax, values
def test_intensity_histogram(self, args, kwargs, expected): """Test several valid use cases.""" results = cpr.synthetic_radiograph(*args, **kwargs) assert len(results) == 3 x = results[0] assert isinstance(x, u.Quantity) assert x.unit == u.m assert x.shape == (expected["bins"][0], ) assert np.isclose(np.min(x), expected["xrange"][0], rtol=1e4) assert np.isclose(np.max(x), expected["xrange"][1], rtol=1e4) y = results[1] assert isinstance(y, u.Quantity) assert y.unit == u.m assert y.shape == (expected["bins"][1], ) assert np.isclose(np.min(y), expected["yrange"][0], rtol=1e4) assert np.isclose(np.max(y), expected["yrange"][1], rtol=1e4) histogram = results[2] assert isinstance(histogram, np.ndarray) assert histogram.shape == expected["bins"]
def test_add_wire_mesh(): # ************************************************************ # Test various input configurations # ************************************************************ # Test a circular mesh run_mesh_example(extent=1 * u.mm) # Test providing hdir run_mesh_example(mesh_hdir=np.array([0.5, 0, 0.5])) # Test providing hdir and vdir run_mesh_example(mesh_hdir=np.array([0.5, 0, 0.5]), mesh_vdir=np.array([0, 0.1, 1])) # ************************************************************ # Test invalid inputs # ************************************************************ # Test invalid extent (too many elements) with pytest.raises(ValueError): run_mesh_example(extent=(1 * u.mm, 2 * u.mm, 3 * u.mm)) # Test wire mesh completely blocks all particles (in this case because # the wire diameter is absurdly large) with pytest.raises(ValueError): run_mesh_example(wire_diameter=5 * u.mm) # Test if wire mesh is not between the source and object with pytest.raises(ValueError): run_mesh_example(location=np.array([0, 3, 0]) * u.mm) # ************************************************************ # Test that mesh is the right size in the detector plane, and that # the wire spacing images correctly. # This is actually a good overall test of the whole proton radiography # particle tracing algorithm. # ************************************************************ loc = np.array([0, -2, 0]) * u.mm extent = (1 * u.mm, 1 * u.mm) wire_diameter = 30 * u.um nwires = 9 sim = run_mesh_example( problem="empty", nparticles=1e5, location=loc, extent=extent, wire_diameter=wire_diameter, nwires=nwires, ) # Calculate the width that the grid SHOULD have on the image plane src_to_mesh = np.linalg.norm(loc.si.value - sim.source) mesh_to_det = np.linalg.norm(sim.detector - loc.si.value) mag = 1 + mesh_to_det / src_to_mesh true_width = mag * extent[0].to(u.mm).value true_spacing = true_width / (nwires - 1) # Create a synthetic radiograph size = np.array([[-1, 1], [-1, 1]]) * 2 * u.cm bins = [100, 50] # Expect a warning because many particles are off the radiograph # (Chose max_theta so corners are covered) with pytest.warns(RuntimeWarning): h, v, i = cpr.synthetic_radiograph(sim, size=size, bins=bins) # Sum up the vertical direction line = np.sum(i, axis=1) # Determine the points that are on gridlines: where 1/line is above the # median by a lot ind = np.argwhere(1 / line > 2 * np.median(1 / line)) hwhere = h.to(u.mm).value[ind] measured_width = np.max(hwhere) - np.min(hwhere) # Calculate the max spatial frequency (should be close to the grid spacing) dx = np.abs(size[0][1] - size[0][0]).to(u.mm).value / bins[0] fnyquist = int(bins[0] / 2) freqs = np.fft.fftfreq(h.size, d=dx) freqs = freqs[:fnyquist] # Calculate the positive frequency power spectrum pspect = np.abs(np.fft.fft(1 / line))**2 pspect = pspect[:fnyquist] pspect = np.where(np.abs(freqs) < 0.1, 0, pspect) # Mask the low frequencies # Measured spacing is the inverse of the maximum spatial frequency measured_spacing = 1 / freqs[np.argmax(pspect)] # This test is somewhat tricky, so here's a matplotlib plot # that can be uncommented for debugging """ fig, ax = plt.subplots(nrows=3, figsize=(4,15)) ax[0].pcolormesh(h.to(u.mm).value, v.to(u.mm).value, i.T, cmap='Blues_r') ax[0].set_aspect('equal') ax[0].axvline(x=np.max(hwhere), color='red') ax[0].axvline(x=np.min(hwhere), color='red') ax[1].plot(h.to(u.mm).value, 1/line) ax[1].axhline(y=np.median(1/line)) ax[1].axvline(x=np.max(hwhere), color='red') ax[1].axvline(x=np.min(hwhere), color='red') ax[2].plot(freqs, pspect) """ # Verify that the edges of the mesh are imaged correctly assert np.isclose(measured_width, true_width, 1) # Verify that the spacing is correct by checking the FFT assert np.isclose(measured_spacing, true_spacing, 0.5)
def test_gaussian_sphere_analytical_comparison(): """ This test runs a known example problem and compares to a theoretical model for small deflections. Still under construction (comparing the actual form of the radiograph is possible but tricky to implement). """ # The Gaussian sphere problem for small deflection potentials # is solved in Kugland2012relation, and the equations referenced # below are from that paper. # https://doi.org/10.1063/1.4750234 a = (1 * u.mm / 3).to(u.mm).value phi0 = 1.4e5 W = 15e6 l = 10 L = 200 # Define and run the problem # Setting b to be much larger than the problem so that the field is not # cut off at the edges. This is required to be directly # comparable to the theoretical result. grid = _test_grid( "electrostatic_gaussian_sphere", num=100, phi0=phi0 * u.V, a=a * u.mm, b=20 * u.mm, ) source = (0 * u.mm, -l * u.mm, 0 * u.mm) detector = (0 * u.mm, L * u.mm, 0 * u.mm) with pytest.warns( RuntimeWarning, match="Fields should go to zero at edges of grid to avoid "): sim = cpr.Tracker(grid, source, detector, verbose=False) sim.create_particles(1e3, W * u.eV, max_theta=12 * u.deg) sim.run() size = np.array([[-1, 1], [-1, 1]]) * 4 * u.cm bins = [100, 100] h, v, i = cpr.synthetic_radiograph(sim, size=size, bins=bins) h = h.to(u.mm).value / sim.mag v = v.to(u.mm).value / sim.mag r0 = h # Calculate a lineout across the center of the plane (y=0) v0 = np.argmin(np.abs(v)) line = np.mean(i[:, v0 - 6:v0 + 6], axis=1) # Zero the edge of the radiograph line += -np.mean(line) line *= 1 / np.max(np.abs(line)) # Calculate the theoretical deflection angles (Eq. 28) theory = phi0 / W * np.sqrt(np.pi) * (r0 / a) * np.exp(-((r0 / a)**2)) max_deflection = np.max(np.abs(theory)) mu = np.sqrt(np.pi) * (phi0 / W) * (l / a) # sim_mu = sim.max_deflection.to(u.rad).value*(l/a) # Calculate the theoretical inversion (Eq. 31 ) theory_deflect = -2 * mu * (1 - (r0 / a)**2) * np.exp(-((r0 / a)**2)) theory_deflect *= 1 / np.max(np.abs(theory_deflect)) # Uncomment for debug """ print(f"Theory max deflection: {max_deflection:.6f}") print(f"Theory mu: {mu:.3f}") print(f"Sim max deflection: {sim.max_deflection.to(u.rad).value:.6f}") print(f"Sim mu: {sim_mu:.3f}") import matplotlib.pyplot as plt print(f"Theory max deflection: {max_deflection:.6f}") print(f"Theory mu: {mu:.3f}") print(f"Sim max deflection: {sim.max_deflection.to(u.rad).value:.6f}") print(f"Sim mu: {sim_mu:.3f}") fig, ax = plt.subplots() ax.pcolormesh(h, v, i.T, shading='auto', cmap='Blues_r') ax.set_aspect('equal') fig, ax = plt.subplots() ax.plot(h, line ) ax.plot(h, theory_deflect) """ assert np.isclose(max_deflection, sim.max_deflection.to(u.rad).value, atol=1e-3)
def test_raises(self, args, kwargs, _raises): """Test scenarios the raise an Exception.""" with pytest.raises(_raises): cpr.synthetic_radiograph(*args, **kwargs)
def test_input_validation(): """ Intentionally raise a number of errors. """ # ************************************************************************ # During initialization # ************************************************************************ grid = _test_grid("electrostatic_gaussian_sphere") source = (-10 * u.mm, 90 * u.deg, 45 * u.deg) detector = (100 * u.mm, 90 * u.deg, 45 * u.deg) # Check that an error is raised when an input grid has a nan or infty value # First check NaN Ex = grid["E_x"] Ex[0, 0, 0] = np.nan * u.V / u.m grid.add_quantities(E_x=Ex) with pytest.raises(ValueError): sim = cpr.Tracker(grid, source, detector, verbose=False) Ex[0, 0, 0] = 0 * u.V / u.m Ex[0, 0, 0] = np.inf * u.V / u.m # Reset element for the rest of the tests grid.add_quantities(E_x=Ex) with pytest.raises(ValueError): sim = cpr.Tracker(grid, source, detector, verbose=False) Ex[0, 0, 0] = 0 * u.V / u.m # Check what happens if a value is large relative to the rest of the array Ex[0, 0, 0] = 0.5 * np.max(Ex) grid.add_quantities(E_x=Ex) # with pytest.raises(ValueError): with pytest.warns(RuntimeWarning): sim = cpr.Tracker(grid, source, detector, verbose=False) Ex[0, 0, 0] = 0 * u.V / u.m # Raise error when source-to-detector vector doesn't pass through the # field grid source_bad = (10 * u.mm, -10 * u.mm, 0 * u.mm) detector_bad = (10 * u.mm, 100 * u.mm, 0 * u.mm) with pytest.raises(ValueError): sim = cpr.Tracker(grid, source_bad, detector_bad, verbose=False) # Test raises warning when one (or more) of the required fields is missing grid_bad = CartesianGrid(-1 * u.mm, 1 * u.mm, num=50) with pytest.warns(RuntimeWarning, match="is not specified for the provided grid."): sim = cpr.Tracker(grid_bad, source, detector, verbose=True) # ************************************************************************ # During create_particles # ************************************************************************ sim = cpr.Tracker(grid, source, detector, verbose=False) sim.create_particles(1e3, 15 * u.MeV, max_theta=0.99 * np.pi / 2 * u.rad) # ************************************************************************ # During runtime # ************************************************************************ sim = cpr.Tracker(grid, source, detector, verbose=False) sim.create_particles(1e3, 15 * u.MeV) # Test an invalid field weighting keyword with pytest.raises(ValueError): sim.run(field_weighting="not a valid field weighting") # ************************************************************************ # During runtime # ************************************************************************ # SYNTHETIC RADIOGRAPH ERRORS sim.run() # Choose a very small synthetic radiograph size that misses most of the # particles with pytest.warns( RuntimeWarning, match="of the particles are shown on this synthetic radiograph."): size = np.array([[-1, 1], [-1, 1]]) * 1 * u.mm hax, vax, values = cpr.synthetic_radiograph(sim, size=size)