Source code for zapmenot.shield

'''
ZapMeNot - a point kernel photon shielding library
Copyright (C) 2019-2025  C. Alan Ford

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
'''

import abc
import math
import numbers
import copy

import numpy as np

from . import material, ray

import importlib
pyvista_spec = importlib.util.find_spec("pyvista")
pyvista_found = pyvista_spec is not None
if pyvista_found:
    import pyvista

# -----------------------------------------------------------


[docs] class Shield(abc.ABC): """Abtract class to model a photon shield. Parameters ---------- material_name : :obj:`material.Material`, optional Shield material type density : float, optional Material density in g/cm3 **kwargs Arbitrary keyword arguments. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield ''' def __init__(self, material_name=None, density=None, **kwargs): # the material name is validated by the Material class self.material = material.Material(material_name) if density is not None: if not isinstance(density, numbers.Number): raise ValueError("Invalid density: " + str(density)) self.material.density = density super().__init__(**kwargs)
[docs] @abc.abstractmethod def is_infinite(self): """Returns true if any dimension is infinite, false otherwise """
[docs] @abc.abstractmethod def is_hollow(self): """Returns true if the body is annular or hollow, false otherwise """
@abc.abstractmethod def _get_crossing_length(self, a_ray): """Calculates the linear intersection length of a ray and the shield Parameters ---------- a_ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. """ if not isinstance(a_ray, ray.FiniteLengthRay): raise ValueError("Invalid ray object")
[docs] @abc.abstractmethod def get_crossing_mfp(self, a_ray, photon_energy): """Calculates the mfp equivalent if a ray intersects the shield Parameters ---------- a_ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. photon_energy : float The photon energy in MeV """ if not isinstance(a_ray, ray.FiniteLengthRay): raise ValueError("Invalid ray object") if not isinstance(photon_energy, numbers.Number): raise ValueError("Invalid photon energy")
@staticmethod def _line_plane_collision(plane_normal, plane_point, ray_origin, ray_normal, epsilon=1e-6): """Calculates the distance from the ray origin to the intersection with a plane Parameters ---------- plane_normal : :class:`numpy.ndarray` A vector normal to the plane plane_point : :class:`numpy.ndarray` Vector location of an arbitrary point on the plane ray_origin : :class:`numpy.ndarray` The vector location of the ray origin ray_normal : :class:`numpy.ndarray` The vector normal of the ray photon_energy : float The photon energy in MeV Notes ----- This work is based on <https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-plane-and-ray-disk-intersection> """ ndotu = plane_normal.dot(ray_normal) if abs(ndotu) < epsilon: return None w = plane_point - ray_origin t = w.dot(plane_normal)/ndotu return t @staticmethod def _ray_sphere_intersection(ray, sphere): # based on # https://viclw17.github.io/2018/07/16/raytracing-ray-sphere-intersection a = np.dot(ray._dir, ray._dir) b = 2 * np.dot(ray._dir, ray._origin - sphere.center) c = np.dot(ray._origin-sphere.center, ray._origin - sphere.center) - sphere.radius**2 discriminant = b**2 - 4*a*c if discriminant <= 0: # sphere is missed or tangent return 0 root = np.sqrt(discriminant) t0 = (-b - root)/(2*a) t1 = (-b + root)/(2*a) big_list = [] for a_length in [t0, t1]: if (a_length >= 0) and (a_length <= ray._length): big_list.append(a_length) if len(big_list) != 2: # if not 2 intersections, look for ray endpoints inside the sphere if sphere.contains(ray._origin): big_list.append(0) if sphere.contains(ray._end): big_list.append(ray._length) if len(big_list) == 0: # ray misses the sphere return 0 if len(big_list) != 2: # this shouldn't occur raise ValueError("Sphere doesn't have 2 crossings") return abs(big_list[1]-big_list[0])
# -----------------------------------------------------------
[docs] class SemiInfiniteXSlab(Shield): """A semi-infinite slab shield perpendicular to the X axis. Parameters ---------- material_name : :obj:`material.Material` Shield material type x_start : float X axis location of the inner edge of the shield. x_end : float X axis location of the outer edge of the shield. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield x_start : float X axis location of the inner edge of the shield. x_end : float X axis location of the outer edge of the shield. ''' def __init__(self, material_name, x_start, x_end, density=None): super().__init__(material_name=material_name, density=density) self.x_start = x_start self.x_end = x_end
[docs] def is_infinite(self): """Returns true if any dimension is infinite, false otherwise """ return True
[docs] def is_hollow(self): """Returns true if the body is annular or hollow, false otherwise """ return False
def _get_crossing_length(self, ray): """Calculates the linear intersection length of a ray and the shield Parameters ---------- ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. """ super()._get_crossing_length(ray) # validate the arguments ray_origin = ray._origin ray_unit_vector = ray._dir plane_normal = np.array([1, 0, 0]) # get length to one crossing point plane_point = np.array([self.x_start, 0, 0]) first_length = self._line_plane_collision( plane_normal, plane_point, ray_origin, ray_unit_vector) if first_length is None: # ray is parallel to plane return 0 # get length to second crossing point plane_point = np.array([self.x_end, 0, 0]) second_length = self._line_plane_collision( plane_normal, plane_point, ray_origin, ray_unit_vector) if second_length is None: # ray is parallel to plane return 0 if (first_length < 0 and second_length < 0): # ray starts and ends entirely on one side of the shield return 0 if (first_length > ray._length and second_length > ray._length): # ray starts and ends entirely on one side of the shield return 0 # remainder of cases have some sort of partial or full crossing t0 = min(first_length, second_length) t1 = max(first_length, second_length) if ((t0 < 0) and (t1 > ray._length)): # ray is intirely within the slab return ray._length if ((t0 < 0) and (t1 < ray._length)): # ray start in slab and crosses out return t1 if ((t0 > 0) and (t1 > ray._length)): # ray starts outside slab and ends inside slab return ray._length - t0 # we are left with a full crossing return t1 - t0
[docs] def get_crossing_mfp(self, ray, photon_energy): """Calculates the mfp equivalent if a ray intersects the shield Parameters ---------- ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. photon_energy : float The photon energy in MeV """ # validate the arguments super().get_crossing_mfp(ray, photon_energy) distance = self._get_crossing_length(ray) return self.material.get_mfp(photon_energy, distance)
[docs] def draw(self): """Creates a display object Returns ------- :class:`pyvista.PolyData` A box object representing the slab shield. """ if pyvista_found: return pyvista.Box(bounds=(self.x_start, self.x_end, -1000, 1000, -1000, 1000))
def _projection(self, x, y, z): # project a point onto the surface of the infinite shield # this is a semi-infinite slab, with a finite X width, # so return two x values at the specified y and z return [(self.x_start, y, z), (self.x_end, y, z)]
# -----------------------------------------------------------
[docs] class Sphere(Shield): """A spherical shield. Parameters ---------- material_name : :obj:`material.Material` Shield material type sphere_center : list list of floats (x, y, and z coordinates). sphere_radius : float radius of the shield. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield center : list list of floats (x, y, and z coordinates). radius : float radius of the sphere. ''' def __init__(self, material_name, sphere_center, sphere_radius, density=None, **kwargs): '''Initialize material composition and location of the spherical shield''' super().__init__(material_name=material_name, density=density) self.center = np.array(sphere_center) self.radius = np.array(sphere_radius)
[docs] def is_infinite(self): '''Returns true if any dimension is infinite, false otherwise ''' return False
[docs] def is_hollow(self): """Returns true if the body is annular or hollow, false otherwise """ return False
[docs] def get_crossing_mfp(self, ray, photon_energy): '''returns the crossing mfp''' super().get_crossing_mfp(ray, photon_energy) # validate the arguments distance = self._get_crossing_length(ray) return self.material.get_mfp(photon_energy, distance)
def _get_crossing_length(self, ray): return self._ray_sphere_intersection(ray, self)
[docs] def contains(self, point): ''' Returns true if the point is contained within the sphere, otherwise false ''' ray = point - self.center if np.dot(ray, ray) > self.radius**2: return False return True
[docs] def draw(self): """Creates a display object Returns ------- :class:`pyvista.PolyData` A Sphere object representing the sphere shield. """ if pyvista_found: return pyvista.Sphere(radius=self.radius, center=self.center)
# -----------------------------------------------------------
[docs] class Shell(Shield): """A shell that surrounds a spherical shield or source""" def __init__(self, material_name, sphere, thickness, density=None, **kwargs): '''Initialize material composition and location of the shell''' super().__init__(material_name=material_name, density=density) if thickness <= 0: raise ValueError("Shell has zero or negative thickness") if not isinstance(sphere, Sphere): raise ValueError("Shell must contain a spherical shield or source") self.inner_sphere = copy.deepcopy(sphere) self.outer_sphere = Sphere(material_name, sphere.center, sphere.radius + thickness, density)
[docs] def is_infinite(self): '''Returns true if any dimension is infinite, false otherwise ''' return False
[docs] def is_hollow(self): """Returns true if the body is annular or hollow, false otherwise """ return True
[docs] def get_crossing_mfp(self, ray, photon_energy): '''returns the crossing mfp''' super().get_crossing_mfp(ray, photon_energy) # validate the arguments distance = self._get_crossing_length(ray) return self.outer_sphere.material.get_mfp(photon_energy, distance)
def _get_crossing_length(self, ray): outer_surface_crossing = self._ray_sphere_intersection(ray, self.outer_sphere) inner_surface_crossing = self._ray_sphere_intersection(ray, self.inner_sphere) crossing_length = outer_surface_crossing - inner_surface_crossing if crossing_length < 0: crossing_length = 0 return crossing_length
[docs] def contains(self, point): ''' Returns true if the point is contained within the shell, otherwise false ''' if self.outer_sphere.contains(point) and not self.inner_sphere.contains(point): return True else: return False
[docs] def draw(self): """Creates a display object Returns ------- :class:`pyvista.PolyData` A Sphere object representing the sphere shield. """ if pyvista_found: sphere_a = pyvista.Sphere(radius= self.outer_sphere.radius, center=self.outer_sphere.center) sphere_b = pyvista.Sphere(radius= self.inner_sphere.radius, center=self.inner_sphere.center) sphere_b.flip_faces(inplace=True) shell = sphere_a.merge(sphere_b) return shell
# -----------------------------------------------------------
[docs] class Box(Shield): """A rectangular polyhedron shield. All sides of the box shield must be axis-aligned. Parameters ---------- material_name : :obj:`material.Material` Shield material type box_center : :obj:`list` X, Y, and Z coordinates of the box center. box_dimensions : :obj:`list` X, Y, and Z dimensions of the box. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class:material.Material Material properties of the shield box_center : :class:numpy.ndarray Vector location of the center of the box in cartesian coordiantes. box_dimensions : :class:numpy.ndarray Vector holding the dimensions of the box. ''' def __init__(self, material_name, box_center, box_dimensions, density=None): super().__init__(material_name=material_name, density=density) self.box_center = np.array(box_center) self.box_dimensions = np.array(box_dimensions)
[docs] def is_infinite(self): """Returns true if any dimension is infinite, false otherwise """ return False
[docs] def is_hollow(self): """Returns true if the body is annular or hollow, false otherwise """ return False
[docs] def get_crossing_mfp(self, ray, photon_energy): """Calculates the mfp equivalent if a ray intersects the shield Parameters ---------- ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. photon_energy : float The photon energy in MeV """ # validate the arguments super().get_crossing_mfp(ray, photon_energy) distance = self._get_crossing_length(ray) return self.material.get_mfp(photon_energy, distance)
def _get_crossing_length(self, ray): """Calculates the linear intersection length of a ray and the shield Parameters ---------- ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. """ super()._get_crossing_length(ray) # validate the arguments crossings = self._intersect_axis_aligned_box(ray) # two crossings indicates a full-shield crossing # one crossing indicates that either (common) the source is # in the shield or (uncommon) the dose point is in the # shield # zero crossings can indicate that either both source and # dose points are in the shield or that the shield is # missed entirely if len(crossings) != 2: # check for start/end of ray within the box if self._contains(ray._origin): crossings.insert(0, ray._origin) if self._contains(np.array(ray._end)): crossings.append(np.array(ray._end)) if len(crossings) == 0: # likely it's a complete miss return 0 if len(crossings) != 2: # shouldn't ever get here raise ValueError("Shield doesn't have 2 crossings") # let numpy do the heavy lifting return np.linalg.norm(crossings[0]-crossings[1]) def _contains(self, point): """Determines if the shield contains a point Parameters ---------- point : :obj:`list` The X, Y, and Z cartesian coordinates of a point. Returns ------- boolean True if the box contains the point, false otherwise """ x = point[0] y = point[1] z = point[2] xmin = self.box_center[0]-self.box_dimensions[0]/2 xmax = self.box_center[0]+self.box_dimensions[0]/2 ymin = self.box_center[1]-self.box_dimensions[1]/2 ymax = self.box_center[1]+self.box_dimensions[1]/2 zmin = self.box_center[2]-self.box_dimensions[2]/2 zmax = self.box_center[2]+self.box_dimensions[2]/2 if (xmin <= x and x <= xmax and ymin <= y and y <= ymax and zmin <= z and z <= zmax): return True return False def _intersect_axis_aligned_box(self, ray): """Calculates a list of point where a ray intersects the axis-aligned box Parameters ---------- ray : :obj:`ray.Ray` A ray object that may intersect the box. Returns ------- :obj:`list` List of vector locations of intersection points. These will include the ray endpoints if they are located within the shield. """ 'returns 0, 1, or 2 points of intersection' results = [] bounds = [self.box_center - (self.box_dimensions/2), self.box_center + (self.box_dimensions/2)] tmin = (bounds[ray._sign[0]][0] - ray._origin[0]) * ray._invdir[0] tmax = (bounds[1-ray._sign[0]][0] - ray._origin[0]) * ray._invdir[0] tymin = (bounds[ray._sign[1]][1] - ray._origin[1]) * ray._invdir[1] tymax = (bounds[1-ray._sign[1]][1] - ray._origin[1]) * ray._invdir[1] if (tmin > tymax) or (tymin > tmax): return results if tymin > tmin: tmin = tymin if tymax < tmax: tmax = tymax tzmin = (bounds[ray._sign[2]][2] - ray._origin[2]) * ray._invdir[2] tzmax = (bounds[1-ray._sign[2]][2] - ray._origin[2]) * ray._invdir[2] if ((tmin > tzmax) or (tzmin > tmax)): return results if tzmin > tmin: tmin = tzmin if tzmax < tmax: tmax = tzmax if (tmin >= 0) and (tmin <= ray._length): results.append(ray._origin + ray._dir*tmin) if (tmax >= 0) and (tmax <= ray._length): results.append(ray._origin + ray._dir*tmax) return results
[docs] def draw(self): """Creates a display object Returns ------- :class:`pyvista.PolyData` A box object representing the box shield. """ if pyvista_found: xmin = self.box_center[0]-self.box_dimensions[0]/2 xmax = self.box_center[0]+self.box_dimensions[0]/2 ymin = self.box_center[1]-self.box_dimensions[1]/2 ymax = self.box_center[1]+self.box_dimensions[1]/2 zmin = self.box_center[2]-self.box_dimensions[2]/2 zmax = self.box_center[2]+self.box_dimensions[2]/2 return pyvista.Box(bounds=(xmin, xmax, ymin, ymax, zmin, zmax))
# -----------------------------------------------------------
[docs] class InfiniteAnnulus(Shield): """An annular shield of infinite length Parameters ---------- material_name : :obj:`material.Material` Shield material type cylinder_origin : :obj:`list` X, Y, and Z coordinates of the point on the cylinder centerline. cylinder_axis : :obj:`list` X, Y, and Z vector components of the cylinder axis. cylinder_inner_radius : float Radius of the annulus inner surface. cylinder_outer_radius : float Radius of the annulus outer surface. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield inner_radius : float Radius of the annulus inner surface. outer_radius : float Radius of the annulus outer surface. origin : :class:`numpy.ndarray` Vector location of a point on the annulus centerline. dir : :class:`numpy.ndarray` Vector normal of the annulus centerline. ''' def __init__(self, material_name, cylinder_origin, cylinder_axis, cylinder_inner_radius, cylinder_outer_radius, density=None): super().__init__(material_name=material_name, density=density) self.inner_radius = cylinder_inner_radius self.outer_radius = cylinder_outer_radius self.origin = np.array(cylinder_origin) axis = np.array(cylinder_axis) self.dir = axis/np.linalg.norm(axis)
[docs] def is_infinite(self): """Returns true if any dimension is infinite, false otherwise """ return True
[docs] def is_hollow(self): """Returns true if the body is annular or hollow, false otherwise """ return True
[docs] def get_crossing_mfp(self, ray, photon_energy): """Calculates the mfp equivalent if a ray intersects the shield Parameters ---------- ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. photon_energy : float The photon energy in MeV """ # validate the arguments super().get_crossing_mfp(ray, photon_energy) distance = self._get_crossing_length(ray) return self.material.get_mfp(photon_energy, distance)
def _get_crossing_length(self, ray): """Calculates the linear intersection length of a ray and the shield Parameters ---------- ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. """ super()._get_crossing_length(ray) # validate the arguments # get a list of crossing points crossings = self._intersect(ray) # zero crossings can indicate that either both source and # dose points are in the shield or that the shield is # missed entirely # one crossing indicates that either (common) the source is # in the shield or (uncommon) the dose point is in the # shield # two crossings indicates a single in/out or out/in crossing # four crossings indicate a full-shield crossing if len(crossings) not in [2, 4]: if self._contains(ray._origin): crossings.append(0) if self._contains(np.array(ray._end)): crossings.append(ray._length) if len(crossings) == 0: return 0 if len(crossings) not in [2, 4]: raise ValueError("Shield doesn't have valid crossings") crossings.sort() if len(crossings) == 2: return crossings[1]-crossings[0] # let numpy do the heavy lifting return (crossings[1]-crossings[0]) + (crossings[3] - crossings[2]) def _contains(self, point): """Determines if the shield contains a point Parameters ---------- point : :obj:`list` The X, Y, and Z cartesian coordinates of a point. Returns ------- boolean True if the shield contains the point, false otherwise """ # determine scalar projection of point on cylinder centerline rando = np.dot(point-self.origin, self.dir) # check the radial distance from cylinder centerline parto = (self.origin+self.dir*rando) - point if (np.dot(parto, parto) < self.inner_radius**2) or \ (np.dot(parto, parto) > self.outer_radius**2): return False return True def _intersect(self, ray): """Calculates a list of points where a ray intersects the shield Parameters ---------- ray : :obj:`ray.Ray` A ray object that may intersect the box. Returns ------- :obj:`list` List of distances along the ray, measured from the ray origin, where the ray intersects the annulus. Will include the ray endpoints if they are within the annular shield. Notes ----- This work is based on <https://mrl.nyu.edu/~dzorin/rend05/lecture2.pdf> and <https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-plane-and-ray-disk-intersection> """ results = [] for radius in [self.inner_radius, self.outer_radius]: deltap = ray._origin - self.origin part1 = ray._dir - (np.dot(ray._dir, self.dir)*self.dir) part2 = deltap - (np.dot(deltap, self.dir)*self.dir) a = np.dot(part1, part1) b = 2*np.dot(part1, part2) c = np.dot(part2, part2) - radius**2 zoro = b**2 - 4*a*c if zoro > 0: # roots are real, then are two intersections on an # "infinite" cylinder meo = math.sqrt(zoro) t1 = (-b + meo)/(2*a) t2 = (-b - meo)/(2*a) # check to see if the intersections occur in the finite # length of the cylinder for t in [t1, t2]: # discard line/cylinder intersections outside of the # length of the ray if t >= 0 and t <= ray._length: results.append(t) return results
[docs] def draw(self): """Creates a display object Returns ------- :class:`pyvista.PolyData` A boolean object representing the annular cylinder shield. """ if pyvista_found: # define an imaginary bottom of the shield at a distance # of -2000 from the origin bottom = self.dir*(-2000.) disc = pyvista.Disc(center=(bottom[0], bottom[1], bottom[2]), normal=self.dir, inner=self.inner_radius, outer=self.outer_radius, c_res=50) cyl1 = disc.extrude(self.dir*4000, capping=True) return cyl1
def _projection(self, x, y, z): # TODO: generalize this by using a degenerate cylinder # # project a point onto the surface of the infinite shield # this is a unconstrained annulus. # Given the range of possible geometries, this # routine will return a single x,y,z tuple representing # the interesection of the annulus axis with the plane of # the point. Three possible intersections with the x, y, # and z planes of the point. The closest point will # be returned. This should permit the annulus to be displayed # without overly extending the region displayed. normal = [1, 0, 0] plane_point = [x, y, z] intersection_list = [] t = self._line_plane_collision(np.array(normal), np.array(plane_point), self.origin, self.dir) if t is not None: intersection_list.append(self.origin + self.dir*t) normal = [0, 1, 0] t = self._line_plane_collision(np.array(normal), np.array(plane_point), self.origin, self.dir) if t is not None: intersection_list.append(self.origin + self.dir*t) normal = [0, 0, 1] t = self._line_plane_collision(np.array(normal), np.array(plane_point), self.origin, self.dir) if t is not None: intersection_list.append(self.origin + self.dir*t) min_collision_distance = None for intersection in intersection_list: distance = np.linalg.norm(intersection - plane_point) if min_collision_distance is None or \ (distance < min_collision_distance): min_collision_distance = distance collision_point = intersection fakeElipsisRadius = 2 * self.outer_radius # generate a bounding box centered at "center" and # a width of 2*outer_radius return [collision_point - [fakeElipsisRadius, 0, 0], collision_point + [fakeElipsisRadius, 0, 0], collision_point - [0, fakeElipsisRadius, 0], collision_point + [0, fakeElipsisRadius, 0], collision_point - [0, 0, fakeElipsisRadius], collision_point + [0, 0, fakeElipsisRadius]]
# -----------------------------------------------------------
[docs] class YAlignedInfiniteAnnulus(InfiniteAnnulus): """An annular shield of infinite length aligned with the Y axis Parameters ---------- material_name : :obj:`material.Material` Shield material type cylinder_center : :obj:`list` X, Y, and Z coordinates of the point on the cylinder centerline. cylinder_inner_radius : float Radius of the annulus inner surface. cylinder_outer_radius : float Radius of the annulus outer surface. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield inner_radius : float Radius of the annulus inner surface. outer_radius : float Radius of the annulus outer surface. origin : :class:`numpy.ndarray` Vector location of a point on the annulus centerline. dir : :class:`numpy.ndarray` Vector normal of the annulus centerline. ''' def __init__(self, material_name, cylinder_center, cylinder_inner_radius, cylinder_outer_radius, density=None): super().__init__(material_name=material_name, density=density, cylinder_origin=cylinder_center, cylinder_inner_radius=cylinder_inner_radius, cylinder_outer_radius=cylinder_outer_radius, cylinder_axis=[0, 1, 0])
# -----------------------------------------------------------
[docs] class XAlignedInfiniteAnnulus(InfiniteAnnulus): """An annular shield of infinite length aligned with the X axis Parameters ---------- material_name : :obj:`material.Material` Shield material type cylinder_center : :obj:`list` X, Y, and Z coordinates of the point on the cylinder centerline. cylinder_inner_radius : float Radius of the annulus inner surface. cylinder_outer_radius : float Radius of the annulus outer surface. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield inner_radius : float Radius of the annulus inner surface. outer_radius : float Radius of the annulus outer surface. origin : :class:`numpy.ndarray` Vector location of a point on the annulus centerline. dir : :class:`numpy.ndarray` Vector normal of the annulus centerline. ''' def __init__(self, material_name, cylinder_center, cylinder_inner_radius, cylinder_outer_radius, density=None): super().__init__(material_name=material_name, density=density, cylinder_origin=cylinder_center, cylinder_inner_radius=cylinder_inner_radius, cylinder_outer_radius=cylinder_outer_radius, cylinder_axis=[1, 0, 0])
# -----------------------------------------------------------
[docs] class ZAlignedInfiniteAnnulus(InfiniteAnnulus): """An annular shield of infinite length aligned with the Z axis Parameters ---------- material_name : :obj:`material.Material` Shield material type cylinder_center : :obj:`list` X, Y, and Z coordinates of the point on the cylinder centerline. cylinder_inner_radius : float Radius of the annulus inner surface. cylinder_outer_radius : float Radius of the annulus outer surface. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield inner_radius : float Radius of the annulus inner surface. outer_radius : float Radius of the annulus outer surface. origin : :class:`numpy.ndarray` Vector location of a point on the annulus centerline. dir : :class:`numpy.ndarray` Vector normal of the annulus centerline. ''' def __init__(self, material_name, cylinder_center, cylinder_inner_radius, cylinder_outer_radius, density=None): super().__init__(material_name=material_name, density=density, cylinder_origin=cylinder_center, cylinder_inner_radius=cylinder_inner_radius, cylinder_outer_radius=cylinder_outer_radius, cylinder_axis=[0, 0, 1])
# -----------------------------------------------------------
[docs] class CappedCylinder(Shield): """A cylindrical shield of finite length Parameters ---------- material_name : :obj:`material.Material` Shield material type cylinder_start : :obj:`list` X, Y, and Z coordinates of the center of one cylinder end. cylinder_end : :obj:`list` X, Y, and Z coordinates of the center of another cylinder end. cylinder_radius : float Radius of the cylinder. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield radius : float Radius of the cylinder. origin : :class:`numpy.ndarray` Vector location corresponding to `cylinder_start`. end : :class:`numpy.ndarray` Vector location corresponding to `cylinder_end`. length : float Length of the cylinder. dir : :class:`numpy.ndarray` Vector normal of the cylinder centerline. ''' def __init__(self, material_name, cylinder_start, cylinder_end, cylinder_radius, density=None): super().__init__(material_name=material_name, density=density) self.radius = cylinder_radius self.origin = np.array(cylinder_start) self.end = np.array(cylinder_end) self.length = np.linalg.norm(self.end - self.origin) self.dir = (self.end - self.origin)/self.length
[docs] def is_infinite(self): """Returns true if any dimension is infinite, false otherwise """ return False
[docs] def is_hollow(self): """Returns true if the body is annular or hollow, false otherwise """ return False
[docs] def get_crossing_mfp(self, ray, photon_energy): """Calculates the mfp equivalent if a ray intersects the shield Parameters ---------- ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. photon_energy : float The photon energy in MeV """ # validate the arguments super().get_crossing_mfp(ray, photon_energy) distance = self._get_crossing_length(ray) return self.material.get_mfp(photon_energy, distance)
def _get_crossing_length(self, ray): """Calculates the linear intersection length of a ray and the shield Parameters ---------- ray : :class:`ray.FiniteLengthRay` The finite length ray that is checked for intersections with the shield. """ super()._get_crossing_length(ray) # validate the arguments # get a list of crossing points crossings = self._intersect(ray) # two crossings indicates a full-shield crossing # one crossing indicates that either (common) the source is # in the shield or (uncommon) the dose point is in the # shield # zero crossings can indicate that either both source and # dose points are in the shield or that the shield is # missed entirely if len(crossings) != 2: # check for start/end of ray within the cylinder if self._contains(ray._origin): crossings.insert(0, ray._origin) if self._contains(np.array(ray._end)): crossings.append(np.array(ray._end)) if len(crossings) == 0: # likely it's a complete miss return 0 if len(crossings) != 2: # shouldn't ever get here raise ValueError("Shield doesn't have 2 crossings") # let numpy do the heavy lifting return np.linalg.norm(crossings[0]-crossings[1]) def _contains(self, point): """Determines if the shield contains a point Parameters ---------- point : :obj:`list` The X, Y, and Z cartesian coordinates of a point. Returns ------- boolean True if the shield contains the point, false otherwise """ # determine scalar projection of point on cylinder centerline rando = np.dot(point-self.origin, self.dir) if rando < 0 or rando > self.length: return False # check the radial distance from cylinder centerline parto = (self.origin+self.dir*rando) - point if np.dot(parto, parto) > self.radius**2: return False return True def _intersect(self, ray): """Calculates a list of points where a ray intersects the shield Parameters ---------- ray : :obj:`ray.Ray` A ray object that may intersect the box. Returns ------- :obj:`list` List of points along the ray where the ray intersects the annulus. Will include the ray endpoints if they are within the annular shield. Notes ----- This work is based on <https://mrl.nyu.edu/~dzorin/rend05/lecture2.pdf> and <https://www.scratchapixel.com/lessons/3d-basic-rendering/minimal-ray-tracer-rendering-simple-shapes/ray-plane-and-ray-disk-intersection> """ results = [] # test for deltap = ray._origin - self.origin part1 = ray._dir - (np.dot(ray._dir, self.dir)*self.dir) part2 = deltap - (np.dot(deltap, self.dir)*self.dir) a = np.dot(part1, part1) b = 2*np.dot(part1, part2) c = np.dot(part2, part2) - self.radius**2 zoro = b**2 - 4*a*c if zoro > 0: # roots are real, then are two intersections on an # "infinite" cylinder meo = math.sqrt(zoro) t1 = (-b + meo)/(2*a) t2 = (-b - meo)/(2*a) # check to see if the intersections occur in the finite length # of the cylinder for t in [t1, t2]: # discard line/cylinder intersections outside of the # length of the ray if t >= 0 and t <= ray._length: intersection = ray._origin + ray._dir*t loc = np.dot(intersection-self.origin, self.dir) # keep only intersections within the finite length # of the cylinder if loc >= 0 and loc < self.length: results.append(intersection) # check to see if there are intersections on the caps for disk_center in [self.origin, self.end]: t = self._line_plane_collision( self.dir, disk_center, ray._origin, ray._dir) if t is not None and t >= 0 and t <= ray._length: point = ray._origin + ray._dir*t radial = point - disk_center if radial.dot(radial) < self.radius**2: results.append(point) return results
[docs] def draw(self): """Creates a display object Returns ------- :class:`pyvista.PolyData` A cylinder object representing the capped cylinder shield. """ if pyvista_found: center = (self.origin + self.end) / 2 return pyvista.Cylinder(center=(center[0], center[1], center[2]), direction=self.dir, height=self.length, radius=self.radius)
# -----------------------------------------------------------
[docs] class YAlignedCylinder(CappedCylinder): """A cylindrical shield of finite length aligned with the Y axis Parameters ---------- material_name : :obj:`material.Material` Shield material type cylinder_center : :obj:`list` X, Y, and Z coordinates of the center of the cylinder. cylinder_length : float The length of the cylinder. cylinder_radius : float Radius of the cylinder. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield radius : float Radius of the cylinder. origin : :class:`numpy.ndarray` Vector location corresponding to `cylinder_start`. end : :class:`numpy.ndarray` Vector location corresponding to `cylinder_end`. length : float Length of the cylinder. dir : :class:`numpy.ndarray` Vector normal of the cylinder centerline. ''' def __init__(self, material_name, cylinder_center, cylinder_length, cylinder_radius, density=None): cylinder_start = [cylinder_center[0], cylinder_center[1]-cylinder_length/2, cylinder_center[2]] cylinder_end = [cylinder_center[0], cylinder_center[1] + cylinder_length/2, cylinder_center[2]] super().__init__(material_name=material_name, density=density, cylinder_start=cylinder_start, cylinder_end=cylinder_end, cylinder_radius=cylinder_radius)
# -----------------------------------------------------------
[docs] class XAlignedCylinder(CappedCylinder): """A cylindrical shield of finite length aligned with the X axis Parameters ---------- material_name : :obj:`material.Material` Shield material type cylinder_center : :obj:`list` X, Y, and Z coordinates of the center of the cylinder. cylinder_length : float The length of the cylinder. cylinder_radius : float Radius of the cylinder. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield radius : float Radius of the cylinder. origin : :class:`numpy.ndarray` Vector location corresponding to `cylinder_start`. end : :class:`numpy.ndarray` Vector location corresponding to `cylinder_end`. length : float Length of the cylinder. dir : :class:`numpy.ndarray` Vector normal of the cylinder centerline. ''' def __init__(self, material_name, cylinder_center, cylinder_length, cylinder_radius, density=None): cylinder_start = [cylinder_center[0]-cylinder_length / 2, cylinder_center[1], cylinder_center[2]] cylinder_end = [cylinder_center[0]+cylinder_length / 2, cylinder_center[1], cylinder_center[2]] super().__init__(material_name=material_name, density=density, cylinder_start=cylinder_start, cylinder_end=cylinder_end, cylinder_radius=cylinder_radius)
# -----------------------------------------------------------
[docs] class ZAlignedCylinder(CappedCylinder): """A cylindrical shield of finite length aligned with the Z axis Parameters ---------- material_name : :obj:`material.Material` Shield material type cylinder_center : :obj:`list` X, Y, and Z coordinates of the center of the cylinder. cylinder_length : float The length of the cylinder. cylinder_radius : float Radius of the cylinder. density : float, optional Material density in g/cm3. """ ''' Attributes ---------- material : :class: `material.Material` Material properties of the shield radius : float Radius of the cylinder. origin : :class:`numpy.ndarray` Vector location corresponding to `cylinder_start`. end : :class:`numpy.ndarray` Vector location corresponding to `cylinder_end`. length : float Length of the cylinder. dir : :class:`numpy.ndarray` Vector normal of the cylinder centerline. ''' def __init__(self, material_name, cylinder_center, cylinder_length, cylinder_radius, density=None): cylinder_start = [cylinder_center[0], cylinder_center[1], cylinder_center[2]-cylinder_length/2] cylinder_end = [cylinder_center[0], cylinder_center[1], cylinder_center[2]+cylinder_length/2] super().__init__(material_name=material_name, density=density, cylinder_start=cylinder_start, cylinder_end=cylinder_end, cylinder_radius=cylinder_radius)