I managed to write a cylindrical shell function in triso.py that worked! Havn’t fully error checked it for pushing to main, but here it is for anyone interested:

class _CylindricalShell(_Container):

“”"Cylindrical shell container in which to pack spheres.

```
Parameters
----------
length : float
Length of the cylindrical container.
radius : float
Outer radius of the cylindrical shell container.
inner_radius : float
Inner radius of the cylindrical shell container.
axis : string
Axis along which the length of the cylinder is aligned.
sphere_radius : float
Radius of spheres to be packed in container.
center : Iterable of float
Cartesian coordinates of the center of the container. Default is
(0., 0., 0.)
Attributes
----------
length : float
Length of the cylindrical container.
radius : float
Outer radius of the cylindrical shell container.
inner_radius : float
Inner radius of the cylindrical shell container.
axis : string
Axis along which the length of the cylinder is aligned.
sphere_radius : float
Radius of spheres to be packed in container.
center : list of float
Cartesian coordinates of the center of the container. Default is
(0., 0., 0.)
shift : list of int
Rolled indices of the x-, y-, and z- coordinates of a sphere so the
configuration is aligned with the correct axis. No shift corresponds to
a cylinder along the z-axis.
cell_length : list of float
Length in x-, y-, and z- directions of each cell in mesh overlaid on
domain.
limits : list of float
Maximum radial distance and minimum and maximum distance in the
direction parallel to the axis where sphere center can be placed.
volume : float
Volume of the container.
"""
def __init__(self, length, radius, inner_radius, axis, sphere_radius, center=(0., 0., 0.)):
super().__init__(sphere_radius, center)
self._shift = None
self.length = length
self.radius = radius
self.inner_radius = inner_radius
self.axis = axis
@property
def length(self):
return self._length
@length.setter
def length(self, length):
self._length = float(length)
self._limits = None
self._cell_length = None
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, radius):
self._radius = float(radius)
self._limits = None
self._cell_length = None
@property
def inner_radius(self):
return self._inner_radius
@inner_radius.setter
def inner_radius(self, inner_radius):
self._inner_radius = float(inner_radius)
self._limits = None
self._cell_length = None
@property
def axis(self):
return self._axis
@axis.setter
def axis(self, axis):
self._axis = axis
self._shift = None
@property
def shift(self):
if self._shift is None:
if self.axis == 'x':
self._shift = [1, 2, 0]
elif self.axis == 'y':
self._shift = [2, 0, 1]
else:
self._shift = [0, 1, 2]
return self._shift
@property
def limits(self):
if self._limits is None:
z0 = self.center[self.shift[2]]
z = self.length/2
sphere_r = self.sphere_radius
r_max = self.radius - sphere_r
if self.inner_radius == 0:
r_min = 0
else:
r_min = self.inner_radius + sphere_r
self._limits = [[z0 - z + sphere_r, r_min], [z0 + z - sphere_r, r_max]] # min and max [z, r] pairs
return self._limits
@limits.setter
def limits(self, limits):
self._limits = limits
@property # leaving this the same as for the cylinder.
def cell_length(self):
if self._cell_length is None:
h = 4*self.sphere_radius
i, j, k = self.shift
self._cell_length = [None]*3
self._cell_length[i] = 2*self.radius/int(2*self.radius/h)
self._cell_length[j] = 2*self.radius/int(2*self.radius/h)
self._cell_length[k] = self.length/int(self.length/h)
return self._cell_length
@property
def volume(self):
return self.length*pi*(self.radius**2 - self.inner_radius**2)
@classmethod
def from_region(self, region, sphere_radius):
check_type('region', region, openmc.Region)
# Assume the simplest case where the cylinder shell volume is the
# intersection of the half-spaces of two cylinders and two planes
if not isinstance(region, openmc.Intersection):
raise ValueError
if any(not isinstance(node, openmc.Halfspace) for node in region):
raise ValueError
if len(region) != 4: # should be two cylinders and two planes
raise ValueError
# Identify the axis that the cylinder lies along
axis = region[0].surface.type[0]
# Make sure the region is composed of a cylinder and two planes on the
# same axis
count = Counter(node.surface.type for node in region)
if count[axis + '-cylinder'] != 2 or count[axis + '-plane'] != 2:
raise ValueError
# Sort the half-spaces by surface type
cyl1, cyl2, p1, p2 = sorted(region, key=lambda x: x.surface.type)
# order the cylinders correctly (cyl1 should be smaller than cyl2)
if cyl1.surface.r > cyl2.surface.r:
cyl1, cyl2 = cyl2, cyl1
# check to make sure cylinders are centered correctly
radius = cyl2.surface.r
inner_radius = cyl1.surface.r
center_check = (cyl1.surface.x0, cyl1.surface.y0, cyl1.surface.z0)
if center_check != (cyl2.surface.x0, cyl2.surface.y0, cyl2.surface.z0):
raise ValueError
# Calculate the parameters for a cylinder along the x-axis
if axis == 'x':
if p1.surface.x0 > p2.surface.x0:
p1, p2 = p2, p1
length = p2.surface.x0 - p1.surface.x0
center = (p1.surface.x0 + length/2, cyl1.surface.y0, cyl1.surface.z0) # TODO cyl1 over cyl2 chosen for no reason
# Calculate the parameters for a cylinder along the y-axis
elif axis == 'y':
if p1.surface.y0 > p2.surface.y0:
p1, p2 = p2, p1
length = p2.surface.y0 - p1.surface.y0
center = (cyl1.surface.x0, p1.surface.y0 + length/2, cyl1.surface.z0) # TODO cyl1 over cyl2 chosen for no reason
# Calculate the parameters for a cylinder along the z-axis
else:
if p1.surface.z0 > p2.surface.z0:
p1, p2 = p2, p1
length = p2.surface.z0 - p1.surface.z0
center = (cyl1.surface.x0, cyl1.surface.y0, p1.surface.z0 + length/2)
# Make sure the half-spaces are on the correct side of the surfaces
if cyl1.side != '+' or cyl2.side != '-' or p1.side != '+' or p2.side != '-':
raise ValueError
# the region is a cylindrical shell, so create container
print('Wow you just made a cylinderical shell!')
return _CylindricalShell(length, radius, inner_radius, axis, sphere_radius, center)
def random_point(self):
ll, ul = self.limits
r = sqrt(uniform(ll[1]**2, ul[1]**2)) # modified to start at the lower radius
t = uniform(0, 2*pi)
i, j, k = self.shift
p = [None]*3
p[i] = r*cos(t) + self.center[i]
p[j] = r*sin(t) + self.center[j]
p[k] = uniform(ll[0], ul[0])
return p
def repel_spheres(self, p, q, d, d_new):
# Moving each sphere distance 's' away from the other along the line
# joining the sphere centers will ensure their final distance is
# equal to the outer diameter
s = (d_new - d)/2
v = (p - q)/d
p += s*v
q -= s*v
# Enforce the rigid boundary by moving each sphere back along the
# surface normal until it is completely within the container if it
# overlaps the surface
ll, ul = self.limits
c = self.center
i, j, k = self.shift
r = sqrt((p[i] - c[i])**2 + (p[j] - c[j])**2)
if r > ul[1]:
p[i] = (p[i] - c[i])*ul[1]/r + c[i] # cartesian "x and y" (after shift)
p[j] = (p[j] - c[j])*ul[1]/r + c[j]
elif r < ll[1]:
p[i] = (p[i] - c[i])*ll[1]/r + c[i] # new section for inner cylinder cartesian "x and y" (after shift)
p[j] = (p[j] - c[j])*ll[1]/r + c[j]
p[k] = np.clip(p[k], ll[0], ul[0])
r = sqrt((q[i] - c[i])**2 + (q[j] - c[j])**2)
if r > ul[1]:
q[i] = (q[i] - c[i])*ul[1]/r + c[i]
q[j] = (q[j] - c[j])*ul[1]/r + c[j]
elif r < ll[1]:
q[i] = (q[i] - c[i])*ll[1]/r + c[i]
q[j] = (q[j] - c[j])*ll[1]/r + c[j]
q[k] = np.clip(q[k], ll[0], ul[0])
```