From 4a4a4dde7572e51838ea608885891dceea8bd1bf Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Thu, 7 Nov 2024 11:08:23 +1300 Subject: [PATCH 01/43] Add cap mesh --- src/scaffoldmaker/scaffolds.py | 4 + src/scaffoldmaker/utils/capmesh.py | 1932 ++++++++++++++++++++ src/scaffoldmaker/utils/eft_utils.py | 91 +- src/scaffoldmaker/utils/networkmesh.py | 25 +- src/scaffoldmaker/utils/tubenetworkmesh.py | 116 ++ tests/test_capmesh.py | 402 ++++ 6 files changed, 2567 insertions(+), 3 deletions(-) create mode 100644 src/scaffoldmaker/utils/capmesh.py create mode 100644 tests/test_capmesh.py diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 6c35deb2..998946e4 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -41,6 +41,8 @@ from scaffoldmaker.meshtypes.meshtype_3d_musclefusiform1 import MeshType_3d_musclefusiform1 from scaffoldmaker.meshtypes.meshtype_3d_ostium1 import MeshType_3d_ostium1 from scaffoldmaker.meshtypes.meshtype_3d_ostium2 import MeshType_3d_ostium2 +from scaffoldmaker.meshtypes.meshtype_3d_renal_capsule1 import MeshType_3d_renal_capsule1 +from scaffoldmaker.meshtypes.meshtype_3d_renal_pelvis1 import MeshType_3d_renal_pelvis1 from scaffoldmaker.meshtypes.meshtype_3d_smallintestine1 import MeshType_3d_smallintestine1 from scaffoldmaker.meshtypes.meshtype_3d_solidcylinder1 import MeshType_3d_solidcylinder1 from scaffoldmaker.meshtypes.meshtype_3d_solidsphere1 import MeshType_3d_solidsphere1 @@ -102,6 +104,8 @@ def __init__(self): MeshType_3d_musclefusiform1, MeshType_3d_ostium1, MeshType_3d_ostium2, + MeshType_3d_renal_capsule1, + MeshType_3d_renal_pelvis1, MeshType_3d_smallintestine1, MeshType_3d_solidcylinder1, MeshType_3d_solidsphere1, diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py new file mode 100644 index 00000000..540085fd --- /dev/null +++ b/src/scaffoldmaker/utils/capmesh.py @@ -0,0 +1,1932 @@ +import math + +from cmlibs.maths.vectorops import magnitude, sub, add, set_magnitude, normalize, rotate_vector_around_vector, cross, \ + angle, mult, div +from cmlibs.zinc.element import Element +from cmlibs.zinc.node import Node +from scaffoldmaker.utils.eft_utils import determineCubicHermiteSerendipityEft, \ + addTricubicHermiteSerendipityEftParameterScaling +from scaffoldmaker.utils.eftfactory_tricubichermite import eftfactory_tricubichermite +from scaffoldmaker.utils.interpolation import smoothCubicHermiteDerivativesLoop, smoothCubicHermiteDerivativesLine, \ + sampleCubicHermiteCurves, interpolateSampleCubicHermite +from scaffoldmaker.utils.spheremesh import calculate_arc_length, local_to_global_coordinates, spherical_to_cartesian, \ + calculate_azimuth + + +class CapMesh: + """ + Cap mesh generator. + """ + + def __init__(self, elementsCountAround, elementsCountCoreBoxMajor, elementsCountCoreBoxMinor, + elementsCountThroughShell, elementsCountTransition, networkPathParameters, tubeBoxCoordinates, + tubeTransitionCoordinates, tubeShellCoordinates, isCap, isCore): + """ + :param elementsCountAround: Number of elements around this segment. + :param elementsCountCoreBoxMajor: Number of elements across core box major axis. + :param elementsCountCoreBoxMinor: Number of elements across core box minor axis. + :param elementsCountThroughShell: Number of elements between inner and outer tube. + :param elementsCountTransition: Number of elements across transition zone between core box elements and + rim elements. + :param networkPathParameters: List containing path parameters of a tube network. + :param tubeBoxCoordinates: List of coordinates and derivatives for nodes that form tube box elements. + :param tubeTransitionCoordinates: List of coordinates and derivatives for nodes that form tube transition elements. + :param tubeShellCoordinates: List of coordinates and derivatives for nodes that form tube rim elements. + :param isCap: List [startCap, endCap] with boolean values. True if the tube segment requires a cap at the + start of a segment, or at the end of a segment, respectively. [True, True] if the segment requires cap at both + ends. + :param isCore: True for tube network with a solid core, False for regular tube network. + """ + self._isCap = isCap + self._isCore = isCore + + self._elementsCountAround = elementsCountAround + self._elementsCountCoreBoxMajor = elementsCountCoreBoxMajor + self._elementsCountCoreBoxMinor = elementsCountCoreBoxMinor + self._elementsCountThroughShell = elementsCountThroughShell + self._elementsCountTransition = elementsCountTransition + + self._networkPathParameters = networkPathParameters + self._tubeBoxCoordinates = tubeBoxCoordinates # tube box coordinates + self._tubeTransitionCoordinates = tubeTransitionCoordinates # tube transition coordinates + self._tubeShellCoordinates = tubeShellCoordinates # tube rim coordinates + + self._boxExtCoordinates = None + # coordinates and derivatives for box nodes extended from the tube segment + # list[startCap, endCap][x, d1, d2, d3][nAcrossMajor][nAcrossMinor] + self._transitionExtCoordinates = None + self._shellExtCoordinates = None + # coordinates and derivatives for shell nodes extended from the tube segment + # list[startCap, endCap][x, d1, d2, d3][nThroughWall][nAround] + self._boxExtNodeIds = None + self._rimExtNodeIds = None + + self._boxCoordinates = None + # list[startCap, endCap][[x, d1, d2, d3][nAcrossMajor][nAcrossMinor] + self._shellCoordinates = None + # list[startCap, endCap][x, d1, d2, d3][nThroughWall][apex, rim][nAround] if the tube is without the solid core. + # list[startCap, endCap][[x, d1, d2, d3][nThroughWall][nAcrossMajor][nAcrossMinor] if the tube is with the core. + self._startCapNodeIds = None + # capNodeIds that form the cap at the start of a tube segment. + # list[nThroughWall][apex, rim] if the tube is without the core. + # list[nThroughWall][nAcrossMajor][nAcrossMinor] if the tube is with the core. + self._endCapNodeIds = None + # capNodeIds that form the cap at the end of a tube segment. + # list structure is identical to startCapNodeIds. + self._startCapElementIds = None + # elementIds that form the cap at the start of a tube segment. + # list[nThroughWall][apex, rim] if the tube is without the core. + # list[box, rim]: [box] and [rim] sublists have different structures. + # [box][core, transition, shield][nAcrossMajor][nAcrossMinor] + # [rim][base, shield][nAround] + self._endCapElementIds = None + # elementIds that form the cap at the end of a tube segment. + + def _extendTubeEnds(self, isStartCap=True): + """ + Add additional tube sections with smaller element size along the tube at either ends of the tube with smaller + D2 derivatives. This function is to minimise the effect of large difference in D2 derivatives between the cap + mesh and the tube mesh. + :param isStartCap:True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + + layoutD1 = self._networkPathParameters[0][1][idx] + outerRadius = self._calculateOuterShellRadius(isStartCap) + # segmentLength = magnitude(sub(self._networkPathParameters[0][0][0], self._networkPathParameters[0][0][1])) + # ext = segmentLength * 0.05 + ext = outerRadius / 2 ## may need another method to calculate extension + unitVector = normalize(layoutD1) + signValue = -1 if isStartCap else 1 + + coreBoxMajorNodesCount = self._elementsCountCoreBoxMajor + 1 + coreBoxMinorNodesCount = self._elementsCountCoreBoxMinor + 1 + + boxCoordinates, transitionCoordinates, shellCoordinates = [], [], [] + for nx in range(4): + boxCoordinates.append([]) + transitionCoordinates.append([]) + shellCoordinates.append([]) + if self._isCore: + for m in range(coreBoxMajorNodesCount): + boxCoordinates[nx].append([]) + for n in range(coreBoxMinorNodesCount): + boxCoordinates[nx][m].append([]) + if self._elementsCountTransition > 1: + for n3 in range(self._elementsCountTransition - 1): + transitionCoordinates[nx].append([]) + for n3 in range(self._elementsCountThroughShell + 1): + shellCoordinates[nx].append([]) + + if self._isCore: + for m in range(coreBoxMajorNodesCount): + xList, d2List = [], [] + x = self._tubeBoxCoordinates[0][idx][m] + for n in range(coreBoxMinorNodesCount): + tx = add(x[n], set_magnitude(unitVector, ext * signValue)) + td2 = mult(sub(tx, x[n]), signValue) + xList.append(tx) + d2List.append(td2) + boxCoordinates[0][m] = xList + boxCoordinates[1][m] = self._tubeBoxCoordinates[1][idx][m] + boxCoordinates[2][m] = d2List + boxCoordinates[3][m] = self._tubeBoxCoordinates[3][idx][m] + + if self._elementsCountTransition > 1: + for n3 in range(self._elementsCountTransition - 1): + xList, d2List = [], [] + x = self._tubeTransitionCoordinates[0][idx][n3] + for nx in range(self._elementsCountAround): + tx = add(x[nx], set_magnitude(unitVector, ext * signValue)) + td2 = mult(sub(tx, x[nx]), signValue) + xList.append(tx) + d2List.append(td2) + transitionCoordinates[0][n3] = xList + transitionCoordinates[1][n3] = self._tubeTransitionCoordinates[1][idx][n3] + transitionCoordinates[2][n3] = d2List + transitionCoordinates[3][n3] = self._tubeTransitionCoordinates[3][idx][n3] + + for n3 in range(self._elementsCountThroughShell + 1): + xList, d2List = [], [] + x = self._tubeShellCoordinates[0][idx][n3] + for nx in range(self._elementsCountAround): + tx = add(x[nx], set_magnitude(unitVector, ext * signValue)) + td2 = mult(sub(tx, x[nx]), signValue) + xList.append(tx) + d2List.append(td2) + shellCoordinates[0][n3] = xList + shellCoordinates[1][n3] = self._tubeShellCoordinates[1][idx][n3] + shellCoordinates[2][n3] = d2List + shellCoordinates[3][n3] = self._tubeShellCoordinates[3][idx][n3] + + if self._isCore: + self._boxExtCoordinates = [None, None] if self._boxExtCoordinates is None else self._boxExtCoordinates + self._boxExtCoordinates[idx] = boxCoordinates + if self._elementsCountTransition > 1: + self._transitionExtCoordinates = [None, None] if self._transitionExtCoordinates is None \ + else self._transitionExtCoordinates + self._transitionExtCoordinates[idx] = transitionCoordinates + + self._shellExtCoordinates = [None, None] if self._shellExtCoordinates is None else self._shellExtCoordinates + self._shellExtCoordinates[idx] = shellCoordinates + + def _remapCapCoordinates(self, isStartCap=True): + """ + Remap box and rim coordinates of the cap nodes based on the scale of tube extension. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + + layoutD1 = self._networkPathParameters[0][1][idx] + outerRadius = self._calculateOuterShellRadius(isStartCap) + ext = outerRadius / 2 + # segmentLength = magnitude(sub(self._networkPathParameters[0][0][0], self._networkPathParameters[0][0][1])) + # ext = segmentLength * 0.05 + unitVector = normalize(layoutD1) + signValue = -1 if isStartCap else 1 + + coreBoxMajorNodesCount = self._getNodesCountCoreBoxMajor() + coreBoxMinorNodesCount = self._getNodesCountCoreBoxMinor() + nodesCountRim = self._getNodesCountRim() + + if self._isCore: + for m in range(coreBoxMajorNodesCount): + xList = [] + x = self._boxCoordinates[idx][0][m] + for n in range(coreBoxMinorNodesCount): + tx = add(x[n], set_magnitude(unitVector, ext * signValue)) + xList.append(tx) + self._boxCoordinates[idx][0][m] = xList + for n3 in range(nodesCountRim): + for m in range(coreBoxMajorNodesCount): + xList = [] + x = self._shellCoordinates[idx][0][n3][m] + for n in range(coreBoxMinorNodesCount): + tx = add(x[n], set_magnitude(unitVector, ext * signValue)) + xList.append(tx) + self._shellCoordinates[idx][0][n3][m] = xList + + def _determineCapCoordinatesWithoutCore(self, isStartCap=True): + """ + Calculates coordinates and derivatives for the cap elements. It first calculates the coordinates for the apex + nodes, and then calculates the coordinates for rim nodes on the shell surface. + Used when the solid core is inactive. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + self._shellCoordinates = [None, None] if self._shellCoordinates is None else self._shellCoordinates + + idx = 0 if isStartCap else -1 + signValue = 1 if isStartCap else -1 + pathParameters = self._networkPathParameters[idx] + + outerRadius = self._calculateOuterShellRadius(isStartCap) + shellThickness = self._calculateShellThickness(isStartCap) + # segmentLength = magnitude(sub(self._networkPathParameters[0][0][0], self._networkPathParameters[0][0][1])) + # ext = segmentLength * -0.05 + ext = -(outerRadius / 2) ## may need another method to calculate extension + centre = add(pathParameters[0][idx], set_magnitude(pathParameters[1][idx], ext * signValue)) + + outerWidth = outerLength = outerRadius + innerWidth = innerLength = outerRadius - shellThickness + + elementLengthRatioEquatorApex = 1.0 + lengthRatio = 1.0 + + bOuter = 2.0 / (1.0 + elementLengthRatioEquatorApex / lengthRatio) + aOuter = 1.0 - bOuter + bInner = 2.0 / (1.0 + elementLengthRatioEquatorApex / lengthRatio) + aInner = 1.0 - bInner + + elementsCountUp = 2 + radiansPerElementAround = 2.0 * math.pi / self._elementsCountAround + positionOuterArray = [(0, 0)] * elementsCountUp + positionInnerArray = [(0, 0)] * elementsCountUp + radiansUpOuterArray = [0] * elementsCountUp + radiansUpInnerArray = [0] * elementsCountUp + vector2OuterArray = [(0, 0)] * elementsCountUp + vector2InnerArray = [(0, 0)] * elementsCountUp + + for n2 in range(2): + xi = n2 * 2 / (2 * elementsCountUp) + nxiOuter = aOuter * xi * xi + bOuter * xi + dnxiOuter = 2.0 * aOuter * xi + bOuter + radiansUpOuterArray[n2] = radiansUpOuter = nxiOuter * math.pi * 0.5 + dRadiansUpOuter = dnxiOuter * math.pi / (2 * elementsCountUp) + cosRadiansUpOuter = math.cos(radiansUpOuter) + sinRadiansUpOuter = math.sin(radiansUpOuter) + positionOuterArray[n2] = [outerWidth * sinRadiansUpOuter, -outerLength * cosRadiansUpOuter] + vector2OuterArray[n2] = (outerWidth * cosRadiansUpOuter * dRadiansUpOuter, + outerLength * sinRadiansUpOuter * dRadiansUpOuter) + + nxiInner = aInner * xi * xi + bInner * xi + dnxiInner = 2.0 * aInner * xi + bInner + radiansUpInnerArray[n2] = radiansUpInner = nxiInner * math.pi * 0.5 + dRadiansUpInner = dnxiInner * math.pi / (2 * elementsCountUp) + cosRadiansUpInner = math.cos(radiansUpInner) + sinRadiansUpInner = math.sin(radiansUpInner) + positionInnerArray[n2] = [innerWidth * sinRadiansUpInner, -innerLength * cosRadiansUpInner] + vector2InnerArray[n2] = (innerWidth * cosRadiansUpInner * dRadiansUpInner, + innerLength * sinRadiansUpInner * dRadiansUpInner) + + xList, d1List, d2List, d3List, rList = [], [], [], [], [] + elementsCountThroughShell = self._elementsCountThroughShell + for n3 in range(elementsCountThroughShell + 1): + for lst in [xList, d1List, d2List, d3List]: + lst.append([]) + for n2 in range(2): + n3_fraction = n3 / elementsCountThroughShell + positionOuter = positionOuterArray[n2] + positionInner = positionInnerArray[n2] + position = [positionOuter[0] * n3_fraction + positionInner[0] * (1.0 - n3_fraction), + positionOuter[1] * n3_fraction + positionInner[1] * (1.0 - n3_fraction)] + vector2Outer = vector2OuterArray[n2] + vector2Inner = vector2InnerArray[n2] + vector2 = [vector2Outer[0] * n3_fraction + vector2Inner[0] * (1.0 - n3_fraction), + vector2Outer[1] * n3_fraction + vector2Inner[1] * (1.0 - n3_fraction)] + vector3 = [(positionOuter[0] - positionInner[0]) / elementsCountThroughShell, + (positionOuter[1] - positionInner[1]) / elementsCountThroughShell] + # calculate coordinates + if n2 == 0: # apex + x = apex = add(pathParameters[0][idx], + set_magnitude(pathParameters[1][idx], (position[1] + ext) * signValue )) + d1 = set_magnitude(pathParameters[4][idx], vector2[0] * signValue) + d2 = set_magnitude(pathParameters[2][idx], vector2[0]) + d3 = set_magnitude(pathParameters[1][idx], vector3[1] * signValue) + for lst, value in zip([xList, d1List, d2List, d3List], [x, d1, d2, d3]): + lst[-1].append(value) + else: + refAxis = normalize(pathParameters[4][idx]) + rotateAngle = n2 * (math.pi / 2) / elementsCountUp * signValue + radius = innerLength + (outerLength - innerLength) * n3 / elementsCountThroughShell + rList.append(radius) + tx = rotate_vector_around_vector(normalize(sub(apex, centre)), refAxis, -rotateAngle) + tx = set_magnitude(tx, radius) + for n1 in range(self._elementsCountAround): + radiansAround = n1 * radiansPerElementAround + cosRadiansAround = math.cos(radiansAround) + sinRadiansAround = math.sin(radiansAround) + + rx = rotate_vector_around_vector(tx, pathParameters[1][idx], radiansAround) + rx = add(rx, centre) + d1 = [0.0, position[0] * -sinRadiansAround * radiansPerElementAround * signValue, + position[0] * cosRadiansAround * radiansPerElementAround * signValue] + d2 = [vector2[1], vector2[0] * cosRadiansAround, + vector2[0] * sinRadiansAround] + d3 = [vector3[1], vector3[0] * cosRadiansAround * signValue, + vector3[0] * sinRadiansAround * signValue] + for lst, value in zip([xList, d1List, d2List, d3List], [rx, d1, d2, d3]): + lst[-1].append(value) + + xCoordinates = [[] for _ in range(4)] + for n, value in zip(range(4), [xList, d1List, d2List, d3List]): + for n3 in range(self._elementsCountThroughShell + 1): + xCoordinates[n].append([]) + xCoordinates[n][n3] = [value[n3][0], value[n3][1:]] + self._shellCoordinates[idx] = xCoordinates + + # transform sphere to spheroid + for n3 in range(elementsCountThroughShell + 1): + radii = self._getTubeRadii(centre, n3, idx) + oRadii = [1.0, rList[n3], rList[n3]] + ratio = self._getRatioBetweenTwoRadii(radii, oRadii) + self._sphereToSpheroid(n3, ratio, centre, isStartCap) + # smooth derivatives + for n3 in range(elementsCountThroughShell + 1): + xList = self._shellCoordinates[idx][0] + d1List = self._shellCoordinates[idx][1] + sd1 = smoothCubicHermiteDerivativesLoop(xList[n3][1], d1List[n3][1]) + d1List[n3][1] = sd1 + for n1 in range(self._elementsCountAround): + radiansAround = n1 * radiansPerElementAround + x = xList[n3][1][n1] + xStart, xEnd = xList[n3][0], self._shellExtCoordinates[idx][0][n3][n1] + nx = [xStart, x, xEnd] if isStartCap else [xEnd, x, xStart] + d2List = self._shellCoordinates[idx][2] + d2Start = rotate_vector_around_vector(d2List[n3][0], pathParameters[1][idx], radiansAround) + d2Start = set_magnitude(d2Start, magnitude(d2List[n3][0]) * signValue) + d2End = set_magnitude(self._shellExtCoordinates[idx][2][n3][n1], + magnitude(self._shellExtCoordinates[idx][1][n3][n1])) + d2 = d2List[n3][1][n1] + nd = [d2Start, d2, d2End] if isStartCap else [d2End, d2, d2Start] + sd2 = smoothCubicHermiteDerivativesLine(nx, nd, fixStartDerivative=True, fixEndDerivative=True) + d2List[n3][1][n1] = sd2[1] + for n1 in range(self._elementsCountAround): + xList = self._shellCoordinates[idx][0] + d3List = self._shellCoordinates[idx][3] + nx = [xList[n3][1][n1] for n3 in range(elementsCountThroughShell + 1)] + nd = [d3List[n3][1][n1] for n3 in range(elementsCountThroughShell + 1)] + sd3 = smoothCubicHermiteDerivativesLine(nx, nd) + for n3 in range(elementsCountThroughShell + 1): + d3List[n3][1][n1] = sd3[n3] + + def _determineCapCoordinatesWithCore(self, isStartCap=True): + """ + Blackbox function for calculating coordinates and derivatives for the cap elements. + It first calculates the coordinates for shell nodes, then calculates for box nodes. + nodes, and then calculates the coordinates for rim nodes on the shell surface. + Used when the solid core is active. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + centre = self._networkPathParameters[0][0][idx] + + self._extendTubeEnds(isStartCap) # extend tube end + # shell nodes + nodesCountRim = self._getNodesCountRim() + for n3 in range(nodesCountRim): + ox = self._getRimExtCoordinatesAround(n3, isStartCap)[0] + radius = self._calculateRadius(ox) + radii = self._getTubeRadii(centre, n3, idx) # radii for spheroid + oRadii = [1.0, radius, radius] # original radii used to create the sphere + ratio = self._getRatioBetweenTwoRadii(radii, oRadii) + # ratio between original radii for the sphere and the new radii for spheroid + self._calculateMajorAndMinorNodesCoordinates(n3, centre, ratio, isStartCap) + self._calculateShellQuadruplePoints(n3, centre, radius, isStartCap) + self._calculateShellRegularNodeCoordinates(n3, centre, isStartCap) + self._sphereToSpheroid(n3, ratio, centre, isStartCap) + self._determineShellDerivatives(isStartCap) + # box nodes + self._calculateBoxQuadruplePoints(centre, isStartCap) + self._calculateBoxMajorAndMinorNodes(isStartCap) + self._determineBoxDerivatives(isStartCap) + + self._remapCapCoordinates(isStartCap) + self._extendTubeEnds(isStartCap) + + def _createShellCoordinatesList(self): + """ + Creates an empty list for storing rim coordinates. Only applies when the solid core is active. + """ + self._shellCoordinates = [] if self._shellCoordinates is None else self._shellCoordinates + elementsCountRim = self._getElementsCountRim() + for s in range(2): + self._shellCoordinates.append([] if self._isCap[s] else None) + if self._shellCoordinates[s] is not None: + for nx in range(4): + self._shellCoordinates[s].append([]) + for n3 in range(elementsCountRim): + self._shellCoordinates[s][nx].append([]) + self._shellCoordinates[s][nx][n3] = [[] for _ in range(self._elementsCountCoreBoxMajor + 1)] + for m in range(self._elementsCountCoreBoxMajor + 1): + self._shellCoordinates[s][nx][n3][m] = \ + [None for _ in range(self._elementsCountCoreBoxMinor + 1)] + + def _calculateOuterShellRadius(self, isStartCap=True): + """ + Calculates the radius of an outer shell. It takes the average of a half-distance between two opposing nodes on + the outer shell of a tube segment. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: Radius of the cap shell. + """ + idx = 0 if isStartCap else -1 + ox = self._tubeShellCoordinates[0][idx][-1] + radii = [] + for i in range(self._elementsCountAround // 2): + j = i + self._elementsCountAround // 2 + r = magnitude(sub(ox[i], ox[j])) / 2 + radii.append(r) + return sum(radii) / len(radii) + + def _calculateRadius(self, ox): + """ + Calculates the radius of a shell. It takes the average of a half-distance between two opposing nodes around a + tube segment. + :param ox: Coordinates of shell nodes around a tube segment. + :return: Radius of the cap shell. + """ + radii = [] + for i in range(self._elementsCountAround // 2): + j = i + self._elementsCountAround // 2 + r = magnitude(sub(ox[i], ox[j])) / 2 + radii.append(r) + return sum(radii) / len(radii) + + def _calculateShellThickness(self, isStartCap=True): + """ + Calculates the thickness of a shell, based on the thickness of a tube segment at either ends. + It takes the average of a distance between the outer and the inner node pair around the rim of a tube segment. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: Thickness of the cap shell. + """ + idx = 0 if isStartCap else -1 + ix = self._tubeShellCoordinates[0][idx][0] + ox = self._tubeShellCoordinates[0][idx][-1] + + shellThicknesses = [] + for i in range(self._elementsCountAround): + thickness = magnitude(sub(ox[i], ix[i])) + shellThicknesses.append(thickness) + return sum(shellThicknesses) / len(shellThicknesses) + + def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio, isStartCap=True): + """ + Calculates coordinates and derivatives for major and minor axis nodes on the surface of a cap shell by rotating + the major and minor axis nodes on the rim of a tube segment. + :param n3: Node index from inner to outer rim. + :param centre: Centre coordinates of a tube segment at either ends. + :param ratio: List of ratios between original circular radii and new radii if the tube is non-circular. + [x-axis, major axis, minor axis]. The values should equal 1.0 if the tube cross-section is circular. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + layoutD2 = self._networkPathParameters[0][2][idx] + layoutD3 = self._networkPathParameters[0][4][idx] + + elementsCountAcrossMajor = self._elementsCountCoreBoxMajor + 2 + elementsCountAcrossMinor = self._elementsCountCoreBoxMinor + 2 + + refAxis = normalize(layoutD2) + rotateAngle = (math.pi / elementsCountAcrossMinor) if isStartCap else \ + -(math.pi / elementsCountAcrossMinor) + minorAxisNodesCoordinates = [[], []] # [startCap, endCap] + n1 = self._elementsCountAround * 3 // 4 + ix = self._getTubeRimCoordinates(n1, idx, n3) + for n in range(1, elementsCountAcrossMinor): + for nx in [0, 1]: + vi = sub(ix[nx], centre) if nx == 0 else mult(ix[nx], -1) + vi = div(vi, ratio[2]) + vr = rotate_vector_around_vector(vi, refAxis, n * rotateAngle) + vr = add(vr, centre) if nx == 0 else vr + minorAxisNodesCoordinates[nx].append(vr) + + refAxis = normalize(layoutD3) + rotateAngle = (math.pi / elementsCountAcrossMajor) if isStartCap else \ + -(math.pi / elementsCountAcrossMajor) + majorAxisNodesCoordinates = [[], []] # [startCap, endCap] + ix = self._getTubeRimCoordinates(0, idx, n3) + for m in range(1,elementsCountAcrossMajor): + for nx in [0, 1]: # [x, d1] + vi = sub(ix[nx], centre) if nx == 0 else ix[nx] + vi = div(vi, ratio[1]) + vr = rotate_vector_around_vector(vi, refAxis, m * rotateAngle) + vr = add(vr, centre) if nx == 0 else vr + majorAxisNodesCoordinates[nx].append(vr) + + midMajorIndex = elementsCountAcrossMajor // 2 - 1 + midMinorIndex = elementsCountAcrossMinor // 2 - 1 + for n in range(elementsCountAcrossMinor - 1): + for i in [0, 1]: + nx = [0, 1][i] + if self._shellCoordinates[idx][nx][n3][midMajorIndex][n] is None: + self._shellCoordinates[idx][nx][n3][midMajorIndex][n] = minorAxisNodesCoordinates[i][n] + for m in range(elementsCountAcrossMajor - 1): + for i in [0, 1]: + nx = [0, 2][i] + if self._shellCoordinates[idx][nx][n3][m][midMinorIndex] is None: + self._shellCoordinates[idx][nx][n3][m][midMinorIndex] = majorAxisNodesCoordinates[i][m] + + # derivatives + for n in range(elementsCountAcrossMinor - 1): + if self._shellCoordinates[idx][2][n3][midMajorIndex][n] is None: + self._shellCoordinates[idx][2][n3][midMajorIndex][n] = majorAxisNodesCoordinates[1][midMajorIndex] + tx = self._shellCoordinates[idx][0][n3][midMajorIndex] + td2 = self._shellCoordinates[idx][2][n3][midMajorIndex] + self._shellCoordinates[idx][2][n3][midMajorIndex] = smoothCubicHermiteDerivativesLine(tx, td2) + + for m in range(elementsCountAcrossMajor - 1): + if self._shellCoordinates[idx][1][n3][m][midMinorIndex] is None: + self._shellCoordinates[idx][1][n3][m][midMinorIndex] = minorAxisNodesCoordinates[1][midMinorIndex] + tx = [self._shellCoordinates[idx][0][n3][m][midMinorIndex] for m in range(elementsCountAcrossMajor - 1)] + td1 = [self._shellCoordinates[idx][1][n3][m][midMinorIndex] for m in range(elementsCountAcrossMajor - 1)] + sd1 = smoothCubicHermiteDerivativesLine(tx, td1) + for m in range(elementsCountAcrossMajor - 1): + self._shellCoordinates[idx][1][n3][m][midMinorIndex] = sd1[m] + + def _calculateShellQuadruplePoints(self, n3, centre, radius, isStartCap=True): + """ + Calculate coordinates and derivatives of the quadruple point on the surface, where 3 hex elements merge. + :param n3: Node index from inner to outer rim. + :param centre: Centre coordinates of a tube segment at either ends. + :param radius: Shell radius. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + + layoutD1 = self._networkPathParameters[0][1][idx] + layoutD2 = self._networkPathParameters[0][2][idx] + layoutD3 = self._networkPathParameters[0][4][idx] + + axesList = [[mult(layoutD1, -1), layoutD2, mult(layoutD3, -1)], + [layoutD2, mult(layoutD1, -1), layoutD3], + [mult(layoutD2, -1), mult(layoutD1, -1), mult(layoutD3, -1)], + [mult(layoutD1, -1), mult(layoutD2, -1), layoutD3]] + + elementsCountUp = 2 + elementsCountAcrossMajor = self._elementsCountCoreBoxMajor + 2 + elementsCountAcrossMinor = self._elementsCountCoreBoxMinor + 2 + counter = 0 + signValue = 1 if isStartCap else -1 + for m in [0, -1]: + for n in [0, -1]: + if m == n: + elementsCount = [elementsCountUp, elementsCountAcrossMajor // 2, elementsCountAcrossMinor // 2] + else: + elementsCount = [elementsCountAcrossMajor // 2, elementsCountUp, elementsCountAcrossMinor // 2] + + n1z, n3z = elementsCount[1], elementsCount[2] + n1y, n3y = n1z - 1, n3z - 1 + + elementsAroundEllipse12 = elementsCount[0] + elementsCount[1] - 2 + radiansAroundEllipse12 = math.pi / 2 + radiansPerElementAroundEllipse12 = radiansAroundEllipse12 / elementsAroundEllipse12 + elementsAroundEllipse13 = elementsCount[0] + elementsCount[2] - 2 + radiansAroundEllipse13 = math.pi / 2 + radiansPerElementAroundEllipse13 = radiansAroundEllipse13 / elementsAroundEllipse13 + + theta_2 = n3y * radiansPerElementAroundEllipse13 + theta_3 = n1y * radiansPerElementAroundEllipse12 + phi_3 = calculate_azimuth(theta_3, theta_2) + ratio = 1 + local_x = spherical_to_cartesian(radius, theta_3, ratio * phi_3 + (1 - ratio) * math.pi / 2) + c = counter if isStartCap else -(counter + 1) + axes = [mult(axis, signValue) for axis in axesList[c]] + x = local_to_global_coordinates(local_x, axes, centre) + self._shellCoordinates[idx][0][n3][m][n] = x + counter += 1 + + def _calculateShellRegularNodeCoordinates(self, n3, centre, isStartCap=True): + """ + Calculate coordinates and derivatives of all other shell nodes on the cap surface. + :param n3: Node index from inner to outer rim. + :param centre: Centre coordinates of a tube segment at either ends. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + elementsCountAcrossMajor = self._elementsCountCoreBoxMajor + 2 + elementsCountAcrossMinor = self._elementsCountCoreBoxMinor + 2 + midMajorIndex = elementsCountAcrossMajor // 2 - 1 + midMinorIndex = elementsCountAcrossMinor // 2 - 1 + for m in range(self._elementsCountCoreBoxMajor + 1): + elementsOut = self._elementsCountCoreBoxMinor // 2 + for n in [0, -1]: + x1 = self._shellCoordinates[idx][0][n3][m][n] + x2 = self._shellCoordinates[idx][0][n3][m][midMinorIndex] + if x1 is None: + continue + else: + if n == 0: + nx, nd1 = self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) + else: + nx, nd1 = self._sampleCurvesOnSphere(x2, x1, centre, elementsOut) + nRange = [n + 1, midMinorIndex] if n == 0 else \ + [midMinorIndex + 1, self._elementsCountCoreBoxMinor] + for c in range(nRange[0], nRange[1]): + self._shellCoordinates[idx][0][n3][m][c] = nx[c % (len(nx) - 1)] + self._shellCoordinates[idx][1][n3][m][c] = nd1[c % (len(nd1) - 1)] + self._shellCoordinates[idx][2][n3][m][c] = [0, 0, 0] + + for n in range(self._elementsCountCoreBoxMinor + 1): + elementsOut = self._elementsCountCoreBoxMajor // 2 + for m in [0, -1]: + x1 = self._shellCoordinates[idx][0][n3][m][n] + x2 = self._shellCoordinates[idx][0][n3][midMajorIndex][n] + if x1 is None: + continue + else: + if m == 0: + nx, nd2 = self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) + else: + nx, nd2 = self._sampleCurvesOnSphere(x2, x1, centre, elementsOut) + mRange = [m + 1, midMajorIndex] if m == 0 else [midMajorIndex + 1, + self._elementsCountCoreBoxMajor] + for c in range(mRange[0], mRange[1]): + self._shellCoordinates[idx][0][n3][c][n] = nx[c % (len(nx) - 1)] + self._shellCoordinates[idx][1][n3][c][n] = [0, 0, 0] + self._shellCoordinates[idx][2][n3][c][n] = nd2[c % (len(nd2) - 1)] + + def _sphereToSpheroid(self, n3, ratio, centre, isStartCap=True): + """ + Transform the sphere to ellipsoid using the radius in each direction. + :param n3: Node index from inner to outer rim. + :param ratio: List of ratios between original circular radii and new radii if the tube is non-circular. + [x-axis, major axis, minor axis]. The values should equal 1.0 if the tube cross-section is circular. + :param centre: Centre coordinates of a tube segment at either ends. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + + # rotation angles to use in case when the cap is tilted from xyz axes. + layoutD2 = normalize(self._networkPathParameters[0][2][idx]) + layoutD3 = normalize(self._networkPathParameters[0][4][idx]) + thetaD2 = angle(layoutD2, [0.0, 1.0, 0.0]) + thetaD3 = angle(layoutD3, [0.0, 0.0, 1.0]) + + mCount = self._elementsCountCoreBoxMajor + 1 if self._isCore else 1 + nCount = self._elementsCountCoreBoxMinor + 1 if self._isCore else self._elementsCountAround + for m in range(mCount): + mp = m if self._isCore else 1 + for n in range(nCount): + btx = self._shellCoordinates[idx][0][n3][mp][n] + btx = sub(btx, centre) + btx = rotate_vector_around_vector(btx, layoutD3, thetaD2) + btx = rotate_vector_around_vector(btx, layoutD2, thetaD3) + btx = [ratio[c] * btx[c] for c in range(3)] + btx = rotate_vector_around_vector(btx, layoutD3, -thetaD2) + btx = rotate_vector_around_vector(btx, layoutD2, -thetaD3) + self._shellCoordinates[idx][0][n3][mp][n] = add(btx, centre) + + def _getRatioBetweenTwoRadii(self, radii, oRadii): + """ + Calculates the ratio between the original radius of a sphere and the new radius of an ellipsoid. + :param radii: List of new radius in each direction. + :param oRadii: List of original radius in each direction. + :return: List of ratio between two radii in x, y, and z-direction. + """ + return [radii[c] / oRadii[c] for c in range(3)] + + def _getTubeRadii(self, centre, n3, idx): + """ + Calculates the radius of a tube segment in major axis and minor axis. + :param centre: + :param n3: Node index from inner to outer rim. + :param idx: 0 if calculating for the start segment, -1 if calculating for the end segment. + :return: List of radii in major and minor axes. The radius in x-direction is set to 1.0 by default because the + radius in this direction is constant. + """ + n1m, n1n = 0, self._elementsCountAround // 4 + ixm = self._getTubeRimCoordinates(n1m, idx, n3)[0] + ixn = self._getTubeRimCoordinates(n1n, idx, n3)[0] + majorRadius, minorRadius = magnitude(sub(ixm, centre)), magnitude(sub(ixn, centre)) + + return [1.0, majorRadius, minorRadius] + + def _determineShellDerivatives(self, isStartCap=True): + """ + Compute d1, d2, and d3 derivatives for the shell nodes. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + nodesCountRim = self._getNodesCountRim() + for n3 in range(nodesCountRim): + for m in [0, -1]: + for n in [0, -1]: + # initialise derivatives for quadruple points + mp = m + 1 if m == 0 else m - 1 + np = n + 1 if n == 0 else n - 1 + d1 = sub(self._shellCoordinates[idx][0][n3][mp][n], self._shellCoordinates[idx][0][n3][m][n]) + self._shellCoordinates[idx][1][n3][m][n] = mult(d1, -1) if m == -1 else d1 + d2 = sub(self._shellCoordinates[idx][0][n3][m][np], self._shellCoordinates[idx][0][n3][m][n]) + self._shellCoordinates[idx][2][n3][m][n] = mult(d2, -1) if n == -1 else d2 + + for n3 in range(nodesCountRim): + for m in range(self._elementsCountCoreBoxMajor + 1): + signValue = 1 if isStartCap else -1 + tx = self._shellCoordinates[idx][0][n3][m] + td2 = self._shellCoordinates[idx][2][n3][m] + sd2 = smoothCubicHermiteDerivativesLine(tx, td2) + for n in range(self._elementsCountCoreBoxMinor + 1): + self._shellCoordinates[idx][2][n3][m][n] = mult(sd2[n], signValue) + for n in range(self._elementsCountCoreBoxMinor + 1): + tx = [self._shellCoordinates[idx][0][n3][m][n] for m in range(self._elementsCountCoreBoxMajor + 1)] + td1 = [self._shellCoordinates[idx][1][n3][m][n] for m in range(self._elementsCountCoreBoxMajor + 1)] + sd1 = smoothCubicHermiteDerivativesLine(tx, td1) + for m in range(self._elementsCountCoreBoxMajor + 1): + self._shellCoordinates[idx][1][n3][m][n] = sd1[m] + + for m in range(self._elementsCountCoreBoxMajor + 1): + for n in range(self._elementsCountCoreBoxMinor + 1): + otx = self._shellCoordinates[idx][0][-1][m][n] + itx = self._shellCoordinates[idx][0][0][m][n] + shellFactor = 1.0 / self._elementsCountThroughShell + sd3 = mult(sub(otx, itx), shellFactor) + for n3 in range(nodesCountRim): + self._shellCoordinates[idx][3][n3][m][n] = sd3 + + def _calculateBoxQuadruplePoints(self, centre, isStartCap=True): + """ + Calculate coordinates and derivatives of the quadruple point for the box elements, where 3 hex elements merge. + :param centre: Centre coordinates of a tube segment at either ends. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + capBoxCoordinates = [] + for nx in range(4): + capBoxCoordinates.append([]) + capBoxCoordinates[nx] = [[] for _ in range(self._elementsCountCoreBoxMajor + 1)] + for m in range(self._elementsCountCoreBoxMajor + 1): + capBoxCoordinates[nx][m] = [None for _ in range(self._elementsCountCoreBoxMinor + 1)] + + boxCoordinates = self._tubeBoxCoordinates[0][idx] + rimCoordinates = self._tubeTransitionCoordinates[0][idx][0] if self._elementsCountTransition > 1 \ + else self._tubeShellCoordinates[0][idx][0] + capCoordinates = self._shellCoordinates[idx][0][0] + + elementsCountAround = self._elementsCountAround + nodesCountAcrossMinorHalf = len(boxCoordinates[0]) // 2 + triplePointIndexesList = [] + for n in range(0, elementsCountAround, elementsCountAround // 2): + triplePointIndexesList.append((n - nodesCountAcrossMinorHalf) % elementsCountAround) + triplePointIndexesList.append(n + nodesCountAcrossMinorHalf) + triplePointIndexesList[-2], triplePointIndexesList[-1] = triplePointIndexesList[-1], triplePointIndexesList[-2] + + counter = 0 + for m in [0, -1]: + for n in [0, -1]: + tpIndex = triplePointIndexesList[counter] + x1 = boxCoordinates[m][n] + x2 = rimCoordinates[tpIndex] + x3 = capCoordinates[m][n] + + ts = magnitude(sub(x1, x2)) + ra = sub(x3, centre) + radius = magnitude(ra) + local_x = mult(ra, (1 - ts / radius)) + x = add(local_x, centre) + capBoxCoordinates[0][m][n] = x + capBoxCoordinates[1][m][n] = self._tubeBoxCoordinates[1][idx][m][n] + capBoxCoordinates[3][m][n] = self._tubeBoxCoordinates[3][idx][m][n] + counter += 1 + + if self._boxCoordinates is None: + self._boxCoordinates = [] + self._boxCoordinates = [None] * 2 + self._boxCoordinates[idx] = capBoxCoordinates + + def _calculateBoxMajorAndMinorNodes(self, isStartCap=True): + """ + Calculate coordinates and derivatives for box nodes along the central major and minor axes. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + midMajorIndex = self._elementsCountCoreBoxMajor // 2 + midMinorIndex = self._elementsCountCoreBoxMinor // 2 + + # box side nodes + for m in [0, -1]: + nx, nd1, nd3 = [], [], [] + for n in [0, -1]: + nx += [self._boxCoordinates[idx][0][m][n]] + nd1 += [self._boxCoordinates[idx][1][m][n]] + nd3 += [self._boxCoordinates[idx][3][m][n]] + tx, td3, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd3, self._elementsCountCoreBoxMinor, arcLengthDerivatives=True) + td1 = interpolateSampleCubicHermite(nd1, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] + for n in range(1, self._elementsCountCoreBoxMinor): + self._boxCoordinates[idx][0][m][n] = tx[n] + self._boxCoordinates[idx][1][m][n] = td1[n] + self._boxCoordinates[idx][3][m][n] = td3[n] + + for n in [0, -1]: + nx, nd1, nd3 = [], [], [] + for m in [0, -1]: + nx += [self._boxCoordinates[idx][0][m][n]] + nd1 += [self._boxCoordinates[idx][1][m][n]] + nd3 += [self._boxCoordinates[idx][3][m][n]] + tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, arcLengthDerivatives=True) + td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] + for m in range(1, self._elementsCountCoreBoxMajor): + self._boxCoordinates[idx][0][m][n] = tx[m] + self._boxCoordinates[idx][1][m][n] = td1[m] + self._boxCoordinates[idx][3][m][n] = td3[m] + + # box major and minor nodes + nx, nd1, nd3 = [], [], [] + for n in [0, -1]: + nx += [self._boxCoordinates[idx][0][midMajorIndex][n]] + nd1 += [self._boxCoordinates[idx][1][midMajorIndex][n]] + nd3 += [self._boxCoordinates[idx][3][midMajorIndex][n]] + tx, td3, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd3, self._elementsCountCoreBoxMinor, arcLengthDerivatives=True) + td1 = interpolateSampleCubicHermite(nd1, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] + for n in range(1, self._elementsCountCoreBoxMinor): + self._boxCoordinates[idx][0][midMajorIndex][n] = tx[n] + self._boxCoordinates[idx][1][midMajorIndex][n] = td1[n] + self._boxCoordinates[idx][3][midMajorIndex][n] = td3[n] + + nx, nd1 = [], [] + for m in [0, -1]: + nx += [self._boxCoordinates[idx][0][m][midMinorIndex]] + nd1 += [self._boxCoordinates[idx][1][m][midMinorIndex]] + tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, arcLengthDerivatives=True) + td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] + for m in range(1, self._elementsCountCoreBoxMajor): + self._boxCoordinates[idx][0][m][midMinorIndex] = tx[m] + self._boxCoordinates[idx][1][m][midMinorIndex] = td1[m] + self._boxCoordinates[idx][3][m][midMinorIndex] = td3[m] + + # remaining nodes + for m in range(self._elementsCountCoreBoxMajor): + for n in range(self._elementsCountCoreBoxMinor): + if self._boxCoordinates[idx][0][m][n] is None: + nx = [self._boxCoordinates[idx][0][0][n], + self._boxCoordinates[idx][0][midMajorIndex][n], + self._boxCoordinates[idx][0][-1][n]] + nd1 = [self._boxCoordinates[idx][1][0][n], + self._boxCoordinates[idx][1][midMajorIndex][n], + self._boxCoordinates[idx][1][-1][n]] + nd3 = [self._boxCoordinates[idx][3][0][n], + self._boxCoordinates[idx][3][midMajorIndex][n], + self._boxCoordinates[idx][3][-1][n]] + tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, + arcLengthDerivatives=True) + td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 3, pe, pxi, psf)[0] + for mi in range(1, self._elementsCountCoreBoxMajor): + self._boxCoordinates[idx][0][mi][n] = tx[mi] + self._boxCoordinates[idx][1][mi][n] = td1[mi] + self._boxCoordinates[idx][3][mi][n] = td3[mi] + + # smooth derivatives + for m in range(1, self._elementsCountCoreBoxMajor): + nx = self._boxCoordinates[idx][0][m] + nd3 = self._boxCoordinates[idx][3][m] + sd3 = smoothCubicHermiteDerivativesLine(nx, nd3) + for n in range(1, self._elementsCountCoreBoxMinor): + self._boxCoordinates[idx][3][m][n] = sd3[n] + for n in range(self._elementsCountCoreBoxMinor): + nx = [self._boxCoordinates[idx][0][m][n] for m in range(self._elementsCountCoreBoxMajor + 1)] + nd1 = [self._boxCoordinates[idx][1][m][n] for m in range(self._elementsCountCoreBoxMajor + 1)] + sd1 = smoothCubicHermiteDerivativesLine(nx, nd1) + for m in range(1, self._elementsCountCoreBoxMajor): + self._boxCoordinates[idx][1][m][n] = sd1[m] + + def _determineBoxDerivatives(self, isStartCap=True): + """ + Calculate d2 derivatives of box nodes. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + signValue = 1 if isStartCap else -1 + for m in range(self._elementsCountCoreBoxMajor + 1): + for n in range(self._elementsCountCoreBoxMinor + 1): + otx = self._tubeBoxCoordinates[0][idx][m][n] + itx = self._boxCoordinates[idx][0][m][n] + d2 = sub(otx, itx) + self._boxCoordinates[idx][2][m][n] = mult(d2, signValue) + + def _createBoundaryNodeIdsList(self, nodeIds): + """ + Creates a list (in a circular format similar to other rim node id lists) of box node ids that are + located at the boundary of the box component. + This list is used to easily stitch inner rim nodes with box nodes. + :param nodeIds: List of box node ids to be rearranged. + :return: A list of box node ids stored in a circular format, and a lookup list that translates indexes used in + boxBoundaryNodeIds list to indexes that can be used in boxCoordinates list. + """ + capBoundaryNodeIds, capBoundaryNodeToBoxId = [], [] + boxElementsCountRow = self._elementsCountCoreBoxMajor + 1 + boxElementsCountColumn = self._elementsCountCoreBoxMinor + 1 + for n3 in range(boxElementsCountRow): + if n3 == 0 or n3 == boxElementsCountRow - 1: + ids = nodeIds[n3] if n3 == 0 else nodeIds[n3][::-1] + n1List = list(range(boxElementsCountColumn)) if n3 == 0 else ( + list(range(boxElementsCountColumn - 1, -1, -1))) + capBoundaryNodeIds += [ids[c] for c in range(boxElementsCountColumn)] + for n1 in n1List: + capBoundaryNodeToBoxId.append([n3, n1]) + else: + for n1 in [-1, 0]: + capBoundaryNodeIds.append(nodeIds[n3][n1]) + capBoundaryNodeToBoxId.append([n3, n1]) + + start = self._elementsCountCoreBoxMajor - 2 + idx = self._elementsCountCoreBoxMinor + 2 + for n in range(int(start), -1, -1): + capBoundaryNodeIds.append(capBoundaryNodeIds.pop(idx + 2 * n)) + capBoundaryNodeToBoxId.append(capBoundaryNodeToBoxId.pop(idx + 2 * n)) + + nloop = self._elementsCountCoreBoxMinor // 2 + for _ in range(nloop): + capBoundaryNodeIds.insert(len(capBoundaryNodeIds), capBoundaryNodeIds.pop(0)) + capBoundaryNodeToBoxId.insert(len(capBoundaryNodeToBoxId), capBoundaryNodeToBoxId.pop(0)) + + return capBoundaryNodeIds, capBoundaryNodeToBoxId + + def _getBoxCoordinates(self, m, n, isStartCap=True): + """ + :param m: Index along the major axis. + :param n: Index along the minor axis. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: x[], d1[], d2[], and d3[] of box nodes in the cap mesh. + """ + idx = 0 if isStartCap else -1 + return (self._boxCoordinates[idx][0][m][n], + self._boxCoordinates[idx][1][m][n], + self._boxCoordinates[idx][2][m][n], + self._boxCoordinates[idx][3][m][n]) + + def _getBoxExtCoordinates(self, m, n, isStartCap=True): + """ + :param m: Index along the major axis. + :param n: Index along the minor axis. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: x[], d1[], d2[], and d3[] of box nodes extended from the tube segment. + """ + idx = 0 if isStartCap else -1 + return (self._boxExtCoordinates[idx][0][m][n], + self._boxExtCoordinates[idx][1][m][n], + self._boxExtCoordinates[idx][2][m][n], + self._boxExtCoordinates[idx][3][m][n]) + + def _getRimExtCoordinates(self, n1, n3, isStartCap=True): + """ + :param n1: Index around rim. + :param n3: Index from inner to outer rim. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: x[], d1[], d2[], and d3[] of rim nodes extended from the tube segment. + """ + idx = 0 if isStartCap else -1 + transitionNodeCount = (len(self._transitionExtCoordinates[idx][0]) + if (self._transitionExtCoordinates and self._transitionExtCoordinates[idx]) else 0) + + if n3 < transitionNodeCount: + return (self._transitionExtCoordinates[idx][0][n3][n1], + self._transitionExtCoordinates[idx][1][n3][n1], + self._transitionExtCoordinates[idx][2][n3][n1], + self._transitionExtCoordinates[idx][3][n3][n1]) + sn3 = n3 - transitionNodeCount + return (self._shellExtCoordinates[idx][0][sn3][n1], + self._shellExtCoordinates[idx][1][sn3][n1], + self._shellExtCoordinates[idx][2][sn3][n1], + self._shellExtCoordinates[idx][3][sn3][n1]) + + def _getRimExtCoordinatesAround(self, n3, isStartCap=True): + """ + :param n3: Index from inner to outer rim. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: x[], d1[], d2[], and d3[] of rim nodes extended from the tube segment. + """ + idx = 0 if isStartCap else -1 + transitionNodeCount = (len(self._transitionExtCoordinates[idx][0]) + if (self._transitionExtCoordinates and self._transitionExtCoordinates[idx]) else 0) + + if n3 < transitionNodeCount: + return (self._transitionExtCoordinates[idx][0][n3], + self._transitionExtCoordinates[idx][1][n3], + self._transitionExtCoordinates[idx][2][n3], + self._transitionExtCoordinates[idx][3][n3]) + sn3 = n3 - transitionNodeCount + return (self._shellExtCoordinates[idx][0][sn3], + self._shellExtCoordinates[idx][1][sn3], + self._shellExtCoordinates[idx][2][sn3], + self._shellExtCoordinates[idx][3][sn3]) + + def _getRimCoordinatesWithCore(self, m, n, n3, isStartCap=True): + """ + Get coordinates and derivatives for cap rim. Only applies when core option is active. + :param m: Index across major axis. + :param n: Index across minor axis. + :param n3: Index along the tube. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: cap rim coordinates and derivatives for points at n3, m and n. + """ + idx = 0 if isStartCap else -1 + return (self._shellCoordinates[idx][0][n3][m][n], + self._shellCoordinates[idx][1][n3][m][n], + self._shellCoordinates[idx][2][n3][m][n], + self._shellCoordinates[idx][3][n3][m][n]) + + def _getTubeBoxCoordinates(self, m, n, isStartCap=True): + """ + Get coordinates and derivatives for tube box. + :param m: Index across major axis. + :param n: Index across minor axis. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: Tube box coordinates and derivatives for points at n2, m and n. + """ + idx = 0 if isStartCap else -1 + return (self._tubeBoxCoordinates[0][idx][m][n], + self._tubeBoxCoordinates[1][idx][m][n], + self._tubeBoxCoordinates[2][idx][m][n], + self._tubeBoxCoordinates[3][idx][m][n]) + + def _getTubeRimCoordinates(self, n1, n2, n3): + """ + Get rim parameters at a point. + :param n1: Node index around. + :param n2: Node index along segment. + :param n3: Node index from inner to outer rim. + :param isCore: + :return: Tube rim coordinates and derivatives for points at n1, n2 and n3. + """ + transitionNodeCount = (len(self._tubeTransitionCoordinates[0][0]) + if (self._tubeTransitionCoordinates and self._tubeTransitionCoordinates[0]) else 0) + if n3 < transitionNodeCount and self._isCore: + return (self._tubeTransitionCoordinates[0][n2][n3][n1], + self._tubeTransitionCoordinates[1][n2][n3][n1], + self._tubeTransitionCoordinates[2][n2][n3][n1], + self._tubeTransitionCoordinates[3][n2][n3][n1]) + sn3 = n3 - transitionNodeCount + return [self._tubeShellCoordinates[0][n2][sn3][n1], + self._tubeShellCoordinates[1][n2][sn3][n1], + self._tubeShellCoordinates[2][n2][sn3][n1], + self._tubeShellCoordinates[3][n2][sn3][n1]] + + def _getTriplePointIndexes(self): + """ + Get a node ID at triple points (special four corners) of the solid core. + :return: A list of circular (n1) indexes used to identify triple points. + """ + elementsCountAround = self._elementsCountAround + nodesCountAcrossMinorHalf = self._getNodesCountCoreBoxMinor() // 2 + triplePointIndexesList = [] + + for n in range(0, elementsCountAround, elementsCountAround // 2): + triplePointIndexesList.append(n + nodesCountAcrossMinorHalf) + triplePointIndexesList.append((n - nodesCountAcrossMinorHalf) % elementsCountAround) + + return triplePointIndexesList + + def _getTriplePointLocation(self, e1, isStartCap=True): + """ + Determines the location of a specific triple point relative to the solid core box. + There are four locations: Top left (location = 1); top right (location = -1); bottom left (location = 2); + and bottom right (location = -2). Location is None if not located at any of the four specified locations. + :return: Location identifier. + """ + em = self._elementsCountCoreBoxMinor // 2 + eM = self._elementsCountCoreBoxMajor // 2 + ec = self._elementsCountAround // 4 + + lftColumnElements = list(range(0, ec - eM)) + list(range(3 * ec + eM, self._elementsCountAround)) + topRowElements = list(range(ec - eM, ec + eM)) + rhtColumnElements = list((range(2 * ec - em, 2 * ec + em))) + btmRowElements = list(range(3 * ec - eM, 3 * ec + eM)) + + ni = len(lftColumnElements) // 2 + if e1 == topRowElements[0] or e1 == lftColumnElements[ni - 1]: + location = 1 # "TopLeft" + elif e1 == topRowElements[-1] or e1 == rhtColumnElements[0]: + location = -1 # "TopRight" + elif e1 == btmRowElements[-1] or e1 == lftColumnElements[ni]: + location = 2 # "BottomLeft" + elif e1 == btmRowElements[0] or e1 == rhtColumnElements[-1]: + location = -2 # "BottomRight" + else: + location = 0 + + if not isStartCap and location != 0: + location = location + 1 if location > 0 else location - 1 + if abs(location) > 2: + location = location - 2 if location > 0 else location + 2 + + return location + + def _getBoxBoundaryLocation(self, m, n): + """ + Determines: 1. Where the node is located on the box boundary. There are four side locations: Top, bottom, left, + and right; and 2. the location of a triple point relative to the solid core box. There are four locations: + Top left, top right, bottom left, and bottom right. + Location is 0 if not located at any of the four specified locations. + :return: Side location identifier and triple point location identifier. + """ + mEnd = self._getNodesCountCoreBoxMajor() - 1 + nEnd = self._getNodesCountCoreBoxMinor() - 1 + + m = m + self._getNodesCountCoreBoxMajor() if m < 0 else m + n = n + self._getNodesCountCoreBoxMinor() if n < 0 else n + + if n == nEnd and 0 < m < mEnd: + location = 1 # "Top" + elif n == 0 and 0 < m < mEnd: + location = -1 # "Bottom" + elif m == 0 and 0 < n < nEnd: + location = 2 # "Left" + elif m == mEnd and 0 < n < nEnd: + location = -2 # "Right" + else: + location = 0 + + tpLocation = 0 + if location == 0: + if m == 0 and n == nEnd: + tpLocation = 1 # Top Left + elif m == mEnd and n == nEnd: + tpLocation = -1 # Top Right + elif m == 0 and n == 0: + tpLocation = 2 # Bottom Left + elif m == mEnd and n == 0: + tpLocation = -2 # Bottom Right + + return location, tpLocation + + def _getNodesCountCoreBoxMajor(self): + idx = -1 if self._boxCoordinates[0] is None else 0 + return len(self._boxCoordinates[idx][0]) + + def _getNodesCountCoreBoxMinor(self): + idx = -1 if self._boxCoordinates[0] is None else 0 + return len(self._boxCoordinates[idx][0][0]) + + def _getNodesCountRim(self): + nodesCountRim = self._elementsCountThroughShell + self._elementsCountTransition + return nodesCountRim + + def _getElementsCountRim(self): + elementsCountRim = max(1, self._elementsCountThroughShell) + if self._isCore: + elementsCountRim += self._elementsCountTransition + return elementsCountRim + + def _sampleCurvesOnSphere(self, x1, x2, origin, elementsOut): + """ + Sample coordinates and d1 derivatives of + :param x1, x2: Coordinates of points 1 and 2 on the spherical surface of cap mesh. + :param origin: Centre point coordinates. + :param elementsOut: The number of elements required between points 1 and 2. + :return: Lists of sampled x and d1 between points 1 and 2. + """ + r1, r2 = sub(x1, origin), sub(x2, origin) + deltax = sub(r2, r1) + normal = cross(r1, deltax) + theta = angle(r1, r2) + anglePerElement = theta / elementsOut + arcLengthPerElement = calculate_arc_length(x1, x2, origin) / elementsOut + + nx, nd1 = [], [] + for n1 in range(elementsOut + 1): + radiansAcross = n1 * anglePerElement + r = rotate_vector_around_vector(r1, normal, radiansAcross) + x = add(r, origin) + d1 = set_magnitude(cross(normal, r), arcLengthPerElement) + nx.append(x) + nd1.append(d1) + + return nx, nd1 + + def _generateNodesWithoutCore(self, generateData, isStartCap=True): + """ + Blackbox function for generating cap nodes. Used only when the tube segment does not have a core. + :param generateData: TubeNetworkMeshGenerateData class object. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + coordinates = generateData.getCoordinates() + fieldcache = generateData.getFieldcache() + nodes = generateData.getNodes() + nodetemplate = generateData.getNodetemplate() + + nodesCountShell = len(self._tubeShellCoordinates[0][0]) + capNodeIds = [] + idx = 0 if isStartCap else -1 + for n2 in range(2): + capNodeIds.append([]) + for n3 in range(nodesCountShell): + capNodeIds[n2].append([]) + if n2 == 0: # apex + rx = self._shellCoordinates[idx][0][n3][n2] + rd1 = self._shellCoordinates[idx][1][n3][n2] + rd2 = self._shellCoordinates[idx][2][n3][n2] + rd3 = self._shellCoordinates[idx][3][n3][n2] + + nodeIdentifier = generateData.nextNodeIdentifier() + node = nodes.createNode(nodeIdentifier, nodetemplate) + fieldcache.setNode(node) + for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, + Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], + [rx, rd1, rd2, rd3]): + coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) + capNodeIds[n2][n3].append(nodeIdentifier) + else: + for n1 in range(self._elementsCountAround): + rx = self._shellCoordinates[idx][0][n3][n2][n1] + rd1 = self._shellCoordinates[idx][1][n3][n2][n1] + rd2 = self._shellCoordinates[idx][2][n3][n2][n1] + rd3 = self._shellCoordinates[idx][3][n3][n2][n1] + + nodeIdentifier = generateData.nextNodeIdentifier() + node = nodes.createNode(nodeIdentifier, nodetemplate) + fieldcache.setNode(node) + for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, + Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], + [rx, rd1, rd2, rd3]): + coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) + capNodeIds[n2][n3].append(nodeIdentifier) + + if isStartCap: + self._startCapNodeIds = capNodeIds + else: + self._endCapNodeIds = capNodeIds + + def _generateNodesWithCore(self, generateData, isStartCap): + """ + Blackbox function for generating cap nodes. Used only when the tube segment has a core. + :param generateData: TubeNetworkMeshGenerateData class object. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + coordinates = generateData.getCoordinates() + fieldcache = generateData.getFieldcache() + nodes = generateData.getNodes() + nodetemplate = generateData.getNodetemplate() + + nodesCountCoreBoxMajor = self._getNodesCountCoreBoxMajor() + nodesCountCoreBoxMinor = self._getNodesCountCoreBoxMinor() + nloop = self._getNodesCountRim() + 1 + capNodeIds = [] + idx = 0 if isStartCap else -1 + for n3 in range(nloop): + if n3 == 0: + capNodeIds.append([]) + for m in range(nodesCountCoreBoxMajor): + capNodeIds[n3].append([]) + for n in range(nodesCountCoreBoxMinor): + rx = self._boxCoordinates[idx][0][m][n] + rd1 = self._boxCoordinates[idx][1][m][n] + rd2 = self._boxCoordinates[idx][2][m][n] + rd3 = self._boxCoordinates[idx][3][m][n] + nodeIdentifier = generateData.nextNodeIdentifier() + node = nodes.createNode(nodeIdentifier, nodetemplate) + fieldcache.setNode(node) + for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, + Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], [rx, rd1, rd2, rd3]): + coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) + capNodeIds[n3][m].append(nodeIdentifier) + else: + capNodeIds.append([]) + for m in range(nodesCountCoreBoxMajor): + n3p = n3 - 1 + capNodeIds[n3].append([]) + for n in range(nodesCountCoreBoxMinor): + rx = self._shellCoordinates[idx][0][n3p][m][n] + rd1 = self._shellCoordinates[idx][1][n3p][m][n] + rd2 = self._shellCoordinates[idx][2][n3p][m][n] + rd3 = self._shellCoordinates[idx][3][n3p][m][n] + + nodeIdentifier = generateData.nextNodeIdentifier() + node = nodes.createNode(nodeIdentifier, nodetemplate) + fieldcache.setNode(node) + for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, + Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], + [rx, rd1, rd2, rd3]): + coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) + capNodeIds[n3][m].append(nodeIdentifier) + + if isStartCap: + self._startCapNodeIds = capNodeIds + else: + self._endCapNodeIds = capNodeIds + + def _generateExtendedTubeNodes(self, generateData, isStartCap=True): + """ + Blackbox function for generating tube nodes extended from the original tube segment. + :param generateData: TubeNetworkMeshGenerateData class object. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + coordinates = generateData.getCoordinates() + fieldcache = generateData.getFieldcache() + nodes = generateData.getNodes() + nodetemplate = generateData.getNodetemplate() + + # create core box nodes + self._boxExtNodeIds = [None, None] if self._boxExtNodeIds is None else self._boxExtNodeIds + if self._isCore: + self._boxExtNodeIds[idx] = [] + nodesCountCoreBoxMajor = self._getNodesCountCoreBoxMajor() + nodesCountAcrossMinor = self._getNodesCountCoreBoxMinor() + for n3 in range(nodesCountCoreBoxMajor): + self._boxExtNodeIds[idx].append([]) + rx = self._boxExtCoordinates[idx][0][n3] + rd1 = self._boxExtCoordinates[idx][1][n3] + rd2 = self._boxExtCoordinates[idx][2][n3] + rd3 = self._boxExtCoordinates[idx][3][n3] + for n1 in range(nodesCountAcrossMinor): + nodeIdentifier = generateData.nextNodeIdentifier() + node = nodes.createNode(nodeIdentifier, nodetemplate) + fieldcache.setNode(node) + for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, + Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], + [rx[n1], rd1[n1], rd2[n1], rd3[n1]]): + coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) + self._boxExtNodeIds[idx][n3].append(nodeIdentifier) + + # create rim nodes and transition nodes (if there are more than 1 layer of transition) + nodesCountRim = self._getNodesCountRim() + elementsCountTransition = self._elementsCountTransition + self._rimExtNodeIds = [None, None] if self._rimExtNodeIds is None else self._rimExtNodeIds + self._rimExtNodeIds[idx] = [] + for n3 in range(nodesCountRim): + n3p = n3 - (elementsCountTransition - 1) if self._isCore else n3 + if self._isCore and elementsCountTransition > 1 and n3 < (elementsCountTransition - 1): + # transition coordinates + rx = self._transitionExtCoordinates[idx][0][n3] + rd1 = self._transitionExtCoordinates[idx][1][n3] + rd2 = self._transitionExtCoordinates[idx][2][n3] + rd3 = self._transitionExtCoordinates[idx][3][n3] + else: + # rim coordinates + rx = self._shellExtCoordinates[idx][0][n3p] + rd1 = self._shellExtCoordinates[idx][1][n3p] + rd2 = self._shellExtCoordinates[idx][2][n3p] + rd3 = self._shellExtCoordinates[idx][3][n3p] + ringNodeIds = [] + for n1 in range(self._elementsCountAround): + nodeIdentifier = generateData.nextNodeIdentifier() + node = nodes.createNode(nodeIdentifier, nodetemplate) + fieldcache.setNode(node) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, rx[n1]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, rd1[n1]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, rd2[n1]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, rd3[n1]) + ringNodeIds.append(nodeIdentifier) + self._rimExtNodeIds[idx].append(ringNodeIds) + + def _generateElementsWithoutCore(self, generateData, elementsCountRim, tubeRimNodeIds, annotationMeshGroups, + isStartCap=True): + """ + Blackbox function for generating cap elements. Used only when the tube segment does not have a core. + :param generateData: TubeNetworkMeshGenerateData class object. + :param elementsCountRim: Number of elements through the rim. + :param tubeRimNodeIds: List of tube rim nodes. + :param annotationMeshGroups: List of all annotated mesh groups. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + coordinates = generateData.getCoordinates() + mesh = generateData.getMesh() + elementtemplateStd, eftStd = generateData.getStandardElementtemplate() + eftfactory = eftfactory_tricubichermite(mesh, False) + + if isStartCap: + capNodeIds = self._startCapNodeIds + self._startCapElementIds = [] if self._startCapElementIds is None else self._startCapElementIds + else: + capNodeIds = self._endCapNodeIds + self._endCapElementIds = [] if self._endCapElementIds is None else self._endCapElementIds + capElementIds = [] + for e3 in range(elementsCountRim): + capElementIds.append([]) + for e2 in range(2): + capElementIds[e3].append([]) + if e2 == 0: + for e1 in range(self._elementsCountAround): + e1p = (e1 + 1) % self._elementsCountAround + nids = [] + for n3 in [e3, e3 + 1]: + if isStartCap: + nids += [capNodeIds[0][n3][0], capNodeIds[1][n3][e1], capNodeIds[1][n3][e1p]] + else: + nids += [capNodeIds[1][n3][e1], capNodeIds[1][n3][e1p], capNodeIds[0][n3][0]] + elementIdentifier = generateData.nextElementIdentifier() + va, vb = e1, (e1 + 1) % self._elementsCountAround + if isStartCap: + eftCap = eftfactory.createEftShellPoleBottom(va * 100, vb * 100) + else: + eftCap = eftfactory.createEftShellPoleTop(va * 100, vb * 100) + elementtemplateCap = mesh.createElementtemplate() + elementtemplateCap.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplateCap.defineField(coordinates, -1, eftCap) + element = mesh.createElement(elementIdentifier, elementtemplateCap) + element.setNodesByIdentifier(eftCap, nids) + + # set general linear map coefficients + radiansPerElementAround = math.pi * 2.0 / self._elementsCountAround + radiansAround = e1 * radiansPerElementAround if isStartCap else math.pi + e1 * radiansPerElementAround + radiansAroundNext = ((e1 + 1) % radiansPerElementAround) * radiansPerElementAround if isStartCap \ + else math.pi + ((e1 + 1) % self._elementsCountAround) * radiansPerElementAround + scalefactors = [ + 1.0, + math.sin(radiansAround), math.cos(radiansAround), radiansPerElementAround, + math.sin(radiansAroundNext), math.cos(radiansAroundNext), radiansPerElementAround, + math.sin(radiansAround), math.cos(radiansAround), radiansPerElementAround, + math.sin(radiansAroundNext), math.cos(radiansAroundNext), radiansPerElementAround + ] + if not isStartCap: + for s in [0, 1, 4, 7, 10]: + scalefactors[s] *= -1 + element.setScaleFactors(eftCap, scalefactors) + # for annotationMeshGroup in annotationMeshGroups: + # annotationMeshGroup.addElement(element) + capElementIds[e3][e2].append(elementIdentifier) + else: + idx = 0 if isStartCap else -1 + for e1 in range(self._elementsCountAround): + e1p = (e1 + 1) % self._elementsCountAround + nids = [] + for n3 in [e3, e3 + 1]: + nids += [capNodeIds[e2][n3][e1], capNodeIds[e2][n3][e1p], + self._rimExtNodeIds[idx][n3][e1], self._rimExtNodeIds[idx][n3][e1p]] + if not isStartCap: + for a in [nids]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplateStd) + element.setNodesByIdentifier(eftStd, nids) + capElementIds[e3][e2].append(elementIdentifier) + + if isStartCap: + self._startCapElementIds = capElementIds + else: + self._endCapElementIds = capElementIds + + def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isStartCap=True): + """ + Blackbox function for generating cap elements. Used only when the tube segment has a core. + :param generateData: TubeNetworkMeshGenerateData class object. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + elementsCountAround = self._elementsCountAround + elementsCountCoreBoxMinor = self._elementsCountCoreBoxMinor + elementsCountCoreBoxMajor = self._elementsCountCoreBoxMajor + elementsCountRim = self._getElementsCountRim() + + coordinates = generateData.getCoordinates() + mesh = generateData.getMesh() + elementtemplateStd, eftStd = generateData.getStandardElementtemplate() + + if isStartCap: + capNodeIds = self._startCapNodeIds + self._startCapElementIds = [] if self._startCapElementIds is None else self._startCapElementIds + else: + capNodeIds = self._endCapNodeIds + self._endCapElementIds = [] if self._endCapElementIds is None else self._endCapElementIds + boxExtNodeIds = self._boxExtNodeIds[idx] + rimExtNodeIds = self._rimExtNodeIds[idx] + triplePointIndexesList = self._getTriplePointIndexes() + + capElementIds = [] + boxBoundaryNodeIds, boxBoundaryNodeToCapIndex = [], [] + rimBoundaryNodeIds, rimBoundaryNodeToCapIndex = [], [] + for n2 in range(elementsCountRim + 1): + if n2 == 0: + boxBoundaryNodeIds.append(self._createBoundaryNodeIdsList(capNodeIds[n2])[0]) + boxBoundaryNodeToCapIndex.append(self._createBoundaryNodeIdsList(capNodeIds[n2])[1]) + else: + rimBoundaryNodeIds.append(self._createBoundaryNodeIdsList(capNodeIds[n2])[0]) + rimBoundaryNodeToCapIndex.append(self._createBoundaryNodeIdsList(capNodeIds[n2])[1]) + + # box & shield + # box + boxElementIds = [] + for e3 in range(elementsCountCoreBoxMajor): + boxElementIds.append([]) + e3p = e3 + 1 + for e1 in range(elementsCountCoreBoxMinor): + nids, nodeParameters, nodeLayouts = [], [], [] + for n1 in [e1, e1 + 1]: + nids += [capNodeIds[0][e3][n1], capNodeIds[0][e3p][n1], + boxExtNodeIds[e3][n1], boxExtNodeIds[e3p][n1]] + if not isStartCap: + for a in [nids]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplateStd) + element.setNodesByIdentifier(eftStd, nids) + boxElementIds[e3].append(elementIdentifier) + capElementIds.append(boxElementIds) + + # box shield elements (elements joining the box and the shell elements) + nodeLayoutCapTransition = generateData.getNodeLayoutCapTransition() + boxshieldElementIds = [] + for e3 in range(elementsCountCoreBoxMajor): + boxshieldElementIds.append([]) + e3p = e3 + 1 + for e1 in range(elementsCountCoreBoxMinor): + nids, nodeParameters, nodeLayouts = [], [], [] + elementIdentifier = generateData.nextElementIdentifier() + for n1 in [e1, e1 + 1]: + for n3 in [e3, e3p]: + nids += [capNodeIds[1][n3][n1]] + nodeParameter = self._getRimCoordinatesWithCore(n3, n1, 0, isStartCap) + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayoutCapTransition) + for n3 in [e3, e3p]: + boxLocation, tpLocation = self._getBoxBoundaryLocation(n3, n1) + nid = capNodeIds[0][n3][n1] + nids += [nid] + nodeParameter = self._getBoxCoordinates(n3, n1, isStartCap) + nodeParameters.append(nodeParameter) + nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(boxLocation, isStartCap) + nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation) + nodeLayoutTransitionTriplePoint = generateData.getNodeLayoutTransitionTriplePoint(tpLocation) + if nid in boxBoundaryNodeIds[0]: + nodeLayouts.append(nodeLayoutCapBoxShield if tpLocation == 0 else nodeLayoutCapBoxShieldTriplePoint) + else: + nodeLayouts.append(None) + if not isStartCap: + for a in [nids, nodeParameters, nodeLayouts]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + elementtemplate = mesh.createElementtemplate() + elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplate.defineField(coordinates, -1, eft) + + element = mesh.createElement(elementIdentifier, elementtemplate) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) + boxshieldElementIds[e3].append(elementIdentifier) + capElementIds.append(boxshieldElementIds) + + # shield + for e3 in range(elementsCountRim - 1): + e3p = e3 + 1 + shieldElementIds = [] + for e2 in range(self._elementsCountCoreBoxMajor): + e2p = e2 + 1 + shieldElementIds.append([]) + for e1 in range(elementsCountCoreBoxMinor): + e1p = e1 + 1 + nids = [] + for n3 in [e3p, e3p + 1]: + nids += [capNodeIds[n3][e2][e1], capNodeIds[n3][e2p][e1], + capNodeIds[n3][e2][e1p], capNodeIds[n3][e2p][e1p]] + if not isStartCap: + for a in [nids]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplateStd) + element.setNodesByIdentifier(eftStd, nids) + shieldElementIds[e2].append(elementIdentifier) + capElementIds.append(shieldElementIds) + + if isStartCap: + self._startCapElementIds.append(capElementIds) + else: + self._endCapElementIds.append(capElementIds) + + # rim + capElementIds = [] + # box transition + ringElementIds = [] + boxExtBoundaryNodeIds, boxExtBoundaryNodestoBoxIds = self._createBoundaryNodeIdsList(boxExtNodeIds) + for e1 in range(elementsCountAround): + nids, nodeParameters, nodeLayouts = [], [], [] + n1p = (e1 + 1) % self._elementsCountAround + boxLocation = self._getTriplePointLocation(e1) + shellLocation = self._getTriplePointLocation(e1, isStartCap) + nodeLayoutTransition = generateData.getNodeLayoutTransition() + nodeLayoutCapTransition = generateData.getNodeLayoutCapTransition() + nodeLayoutTransitionTriplePoint = generateData.getNodeLayoutTransitionTriplePoint(boxLocation) + nodeLayoutCapShellTransitionTriplePoint = generateData.getNodeLayoutCapShellTriplePoint(shellLocation) + + elementIdentifier = generateData.nextElementIdentifier() + for n3 in [0, 1]: + for n1 in [e1, n1p]: + nid = boxBoundaryNodeIds[n3][n1] if n3 == 0 else rimBoundaryNodeIds[n3 -1][n1] + nids += [nid] + mi, ni = boxBoundaryNodeToCapIndex[n3][n1] if n3 == 0 else rimBoundaryNodeToCapIndex[n3 - 1][n1] + location, tpLocation = self._getBoxBoundaryLocation(mi, ni) + nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(location, isStartCap) + nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation) + if n3 == 0: + nodeParameter = self._getBoxCoordinates(mi, ni, isStartCap) + nodeLayout = nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList else ( + nodeLayoutCapBoxShield) + else: + nodeParameter = self._getRimCoordinatesWithCore(mi, ni, 0, isStartCap) + nodeLayout = nodeLayoutCapShellTransitionTriplePoint if n1 in triplePointIndexesList else ( + nodeLayoutCapTransition) + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayout) + for n1 in [e1, n1p]: + if n3 == 0: + nid = boxExtBoundaryNodeIds[n1] + mi, ni = boxExtBoundaryNodestoBoxIds[n1] + nodeParameter = self._getBoxExtCoordinates(mi, ni, isStartCap) + else: + nid = rimExtNodeIds[0][n1] + nodeParameter = self._getRimExtCoordinates(n1, 0, isStartCap) + nids += [nid] + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList and n3 == 0 + else nodeLayoutTransition) + if not isStartCap: + for a in [nids, nodeParameters, nodeLayouts]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + # print("nids", nids) + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + if self._elementsCountTransition == 1: + eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( + eft, scalefactors, nodeParameters, nids, coreBoundaryScalingMode) + elementtemplate = mesh.createElementtemplate() + elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplate.defineField(coordinates, -1, eft) + + element = mesh.createElement(elementIdentifier, elementtemplate) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) + ringElementIds.append(elementIdentifier) + capElementIds.append(ringElementIds) + + # shell + triplePointIndexesList = self._getTriplePointIndexes() + for e3 in range(elementsCountRim - 1): + rimElementIds = [] + for e1 in range(self._elementsCountAround): + nids, nodeParameters, nodeLayouts = [], [], [] + e1p = (e1 + 1) % self._elementsCountAround + location = self._getTriplePointLocation(e1, isStartCap) + nodeLayoutCapTransition = generateData.getNodeLayoutCapTransition() + nodeLayoutCapShellTriplePoint = generateData.getNodeLayoutCapShellTriplePoint(location) + for n3 in [e3, e3 + 1]: + for n1 in [e1, e1p]: + nids += [rimBoundaryNodeIds[n3][n1]] + mi, ni = rimBoundaryNodeToCapIndex[n3][n1] + nodeParameter = self._getRimCoordinatesWithCore(mi, ni, n3, isStartCap) + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayoutCapShellTriplePoint if n1 in triplePointIndexesList else + nodeLayoutCapTransition) + for n1 in [e1, e1p]: + nids += [rimExtNodeIds[n3][n1]] + nodeParameter = self._getRimExtCoordinates(n1, n3, isStartCap) + nodeParameters.append(nodeParameter) + nodeLayouts.append(None) + if not isStartCap: + for a in [nids, nodeParameters, nodeLayouts]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + elementIdentifier = generateData.nextElementIdentifier() + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + elementtemplate = mesh.createElementtemplate() + elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplate.defineField(coordinates, -1, eft) + element = mesh.createElement(elementIdentifier, elementtemplate) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) + rimElementIds.append(elementIdentifier) + capElementIds.append(rimElementIds) + + if isStartCap: + self._startCapElementIds.append(capElementIds) + else: + self._endCapElementIds.append(capElementIds) + + def _generateExtendedTubeElements(self, generateData, tubeBoxNodeIds, tubeRimNodeIds, coreBoundaryScalingMode, + isStartCap=True): + """ + Blackbox function for generating extended tube elements. + :param generateData: TubeNetworkMeshGenerateData class object. + :param tubeBoxNodeIds: List of tube box nodes. + :param tubeRimNodeIds: List of tube rim nodes. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + """ + idx = 0 if isStartCap else -1 + + coordinates = generateData.getCoordinates() + mesh = generateData.getMesh() + elementtemplateStd, eftStd = generateData.getStandardElementtemplate() + boxExtElementIds, rimExtElementIds = [], [] + + boxExtNodeIds = self._boxExtNodeIds[idx] + rimExtNodeIds = self._rimExtNodeIds[idx] + + if self._isCore: + boxExtBoundaryNodeIds, boxExtBoundaryNodesToBoxIds = self._createBoundaryNodeIdsList(boxExtNodeIds) + tubeBoxBoundaryNodeIds, tubeBoxBoundaryNodesToBoxIds = self._createBoundaryNodeIdsList(tubeBoxNodeIds[idx]) + # create box elements + for e3 in range(self._elementsCountCoreBoxMajor): + e3p = e3 + 1 + for e1 in range(self._elementsCountCoreBoxMinor): + nids = [] + for n1 in [e1, e1 + 1]: + nids += [boxExtNodeIds[e3][n1], boxExtNodeIds[e3p][n1], + tubeBoxNodeIds[idx][e3][n1], tubeBoxNodeIds[idx][e3p][n1]] + if not isStartCap: + for a in [nids]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplateStd) + element.setNodesByIdentifier(eftStd, nids) + # for annotationMeshGroup in annotationMeshGroups: + # annotationMeshGroup.addElement(element) + boxExtElementIds.append(elementIdentifier) + + # create core transition elements first layer after box + triplePointIndexesList = self._getTriplePointIndexes() + ringExtElementIds = [] + for e1 in range(self._elementsCountAround): + nids, nodeParameters, nodeLayouts = [], [], [] + n1p = (e1 + 1) % self._elementsCountAround + location = self._getTriplePointLocation(e1) + nodeLayoutTransition = generateData.getNodeLayoutTransition() + nodeLayoutTransitionTriplePoint = generateData.getNodeLayoutTransitionTriplePoint(location) + for n2 in [0, 1]: + for n1 in [e1, n1p]: + if n2 == 0: + nid = boxExtBoundaryNodeIds[n1] + mi, ni = boxExtBoundaryNodesToBoxIds[n1] + nodeParameter = self._getBoxExtCoordinates(mi, ni, isStartCap) + nodeLayout = nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList \ + else nodeLayoutTransition + else: + nid = tubeBoxBoundaryNodeIds[n1] + mi, ni = tubeBoxBoundaryNodesToBoxIds[n1] + nodeParameter = self._getTubeBoxCoordinates(mi, ni, isStartCap) + nodeLayout = nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList \ + else nodeLayoutTransition + nids += [nid] + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayout) + if not isStartCap: + for a in [nids, nodeParameters, nodeLayouts]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + for n2 in [0, 1]: + for n1 in [e1, n1p]: + if n2 == 0: + nid = rimExtNodeIds[0][n1] + nodeParameter = self._getRimExtCoordinates(n1, 0, isStartCap) + nodeLayout = None + else: + nid = tubeRimNodeIds[idx][0][n1] + nodeParameter = self._getTubeRimCoordinates(n1, idx, 0) + nodeLayout = None + nids += [nid] + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayout) + if not isStartCap: + for a in [nids, nodeParameters, nodeLayouts]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + if self._elementsCountTransition == 1: + eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( + eft, scalefactors, nodeParameters, nids, coreBoundaryScalingMode) + elementtemplate = mesh.createElementtemplate() + elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplate.defineField(coordinates, -1, eft) + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplate) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) + # for annotationMeshGroup in annotationMeshGroups: + # annotationMeshGroup.addElement(element) + ringExtElementIds.append(elementIdentifier) + + # create regular rim elements - all elements outside first transition layer + elementsCountRim = self._getElementsCountRim() + elementsCountRimRegular = elementsCountRim - 1 if self._isCore else elementsCountRim + for e3 in range(elementsCountRimRegular): + ringExtElementIds = [] + lastTransition = self._isCore and (e3 == (self._elementsCountTransition - 2)) + for e1 in range(self._elementsCountAround): + elementtemplate, eft = elementtemplateStd, eftStd + n1p = (e1 + 1) % self._elementsCountAround + nids = [] + for n3 in [e3, e3 + 1]: + nids += [rimExtNodeIds[n3][e1], rimExtNodeIds[n3][n1p], + tubeRimNodeIds[idx][n3][e1], tubeRimNodeIds[idx][n3][n1p]] + if not isStartCap: + for a in [nids]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + elementIdentifier = generateData.nextElementIdentifier() + scalefactors = [] + if lastTransition: + # get node parameters for computing scale factors + nodeParameters = [] + for n3 in (e3, e3 + 1): + for n2 in (0, 1): + for n1 in (e1, n1p): + if n2 == 0: + nodeParameter = self._getRimExtCoordinates(n1, n3, isStartCap) + else: + nodeParameter = self._getTubeRimCoordinates(n1, idx, n3) + nodeParameters.append(nodeParameter) + if not isStartCap: + for a in [nodeParameters]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + eft = generateData.createElementfieldtemplate() + eft, scalefactors = addTricubicHermiteSerendipityEftParameterScaling( + eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3) + elementtemplateTransition = mesh.createElementtemplate() + elementtemplateTransition.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplateTransition.defineField(coordinates, -1, eft) + elementtemplate = elementtemplateTransition + element = mesh.createElement(elementIdentifier, elementtemplate) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) + # for annotationMeshGroup in annotationMeshGroups: + # annotationMeshGroup.addElement(element) + ringExtElementIds.append(elementIdentifier) + # self._rimElementIds[e2].append(ringExtElementIds) + + def sampleCoordinates(self): + """ + Sample cap coordinates. + """ + if self._isCore: + self._createShellCoordinatesList() + if self._isCap[0]: + self._determineCapCoordinatesWithCore(isStartCap=True) + if self._isCap[1]: + self._determineCapCoordinatesWithCore(isStartCap=False) + else: + if self._isCap[0]: + self._extendTubeEnds(isStartCap=True) + self._determineCapCoordinatesWithoutCore(isStartCap=True) + if self._isCap[1]: + self._extendTubeEnds(isStartCap=False) + self._determineCapCoordinatesWithoutCore(isStartCap=False) + + def generateNodes(self, generateData, isStartCap=True, isCore=False): + """ + Blackbox function for generating cap and extended tube nodes. + :param generateData: Class object from TubeNetworkMeshGenerateData. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :param isCore: True for generating a solid core inside the tube, False for regular tube network. + """ + if isCore: + if isStartCap: + self._generateNodesWithCore(generateData, isStartCap) + self._generateExtendedTubeNodes(generateData, isStartCap) + else: + self._generateExtendedTubeNodes(generateData, isStartCap) + self._generateNodesWithCore(generateData, isStartCap) + else: + if isStartCap: + self._generateNodesWithoutCore(generateData, isStartCap) + self._generateExtendedTubeNodes(generateData, isStartCap) + else: + self._generateExtendedTubeNodes(generateData, isStartCap) + self._generateNodesWithoutCore(generateData, isStartCap) + + def generateElements(self, generateData, elementsCountRim, tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups, + coreBoundaryScalingMode, isStartCap=True, isCore=False): + """ + Blackbox function for generating cap and extended tube elements. + :param generateData: TubeNetworkMeshGenerateData class object. + :param elementsCountRim: Number of elements through the rim. + :param tubeBoxNodeIds: List of tube box nodes. + :param tubeRimNodeIds: List of tube rim nodes. + :param annotationMeshGroups: List of all annotated mesh groups. + :param coreBoundaryScalingMode: Mode 1 to set scale factors, 2 to add version 2 to d3 for the boundary nodes + and assigning values to that version equal to the scale factors x version 1. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :param isCore: True for generating a solid core inside the tube, False for regular tube network. + """ + if isCore: + self._generateElementsWithCore(generateData, coreBoundaryScalingMode, isStartCap) + self._generateExtendedTubeElements(generateData, tubeBoxNodeIds, tubeRimNodeIds, coreBoundaryScalingMode, + isStartCap) + else: + self._generateElementsWithoutCore( + generateData, elementsCountRim, tubeRimNodeIds, annotationMeshGroups, isStartCap) + self._generateExtendedTubeElements(generateData, tubeBoxNodeIds, tubeRimNodeIds, coreBoundaryScalingMode, + isStartCap) diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index d2d3e822..e62e2570 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -688,6 +688,29 @@ def __init__(self): [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]]) self.nodeLayoutsBifurcation6WayTriplePoint = {} + self._nodeLayoutCapShellTriplePointTopLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [-1.0, 1.0, 0.0]]) + self._nodeLayoutCapShellTriplePointTopRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0]]) + self._nodeLayoutCapShellTriplePointBottomLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [-1.0, -1.0, 0.0]]) + self._nodeLayoutCapShellTriplePointBottomRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, -1.0, 0.0]]) + + self._nodeLayoutCapBoxShield = None + + self._nodeLayoutCapBoxShieldTriplePointTopLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, 0.0, 1.0]]) + self._nodeLayoutCapBoxShieldTriplePointTopRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 0.0, 1.0]]) + + self._nodeLayoutCapBoxShieldTriplePointBottomLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 0.0, -1.0]]) + + self._nodeLayoutCapBoxShieldTriplePointBottomRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]]) + + def getNodeLayoutRegularPermuted(self, d3Defined, limitDirections=None): """ Get node layout for permutations of +/- d1, d2, d3, optionally limiting some directions. @@ -813,7 +836,7 @@ def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajor [0.0, 0.0, 1.0], [0.0, 1.0, -1.0]]) nodeLayoutBifurcationCoreTransitionMMM = HermiteNodeLayout( [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], - [0.0, 0.0, 11.0], [-1.0, -1.0, -1.0]]) + [0.0, 0.0, 1.0], [-1.0, -1.0, -1.0]]) nodeLayouts = ( nodeLayoutBifurcationCoreTransitionPOP, nodeLayoutBifurcationCoreTransitionOPP, @@ -839,6 +862,72 @@ def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajor return self._nodeLayoutBifurcationCoreTransitionBottomGeneral return nodeLayouts[layoutIndex] + def getNodeLayoutCapTransition(self): + """ + Get node layout for transition elements between the core box elements and the shell elements in the cap mesh. + :return: HermiteNodeLayout. + """ + return self._nodeLayoutRegularPermuted_d3Defined + + def getNodeLayoutCapShellTriplePoint(self): + """ + Get node layout for triple-point corners of shell elements in the cap mesh. There are four corners + (Top Left, Top Right, Bottom Left, and Bottom Right) each with its specific node layout. + :return: HermiteNodeLayout. + """ + nodeLayouts = [self._nodeLayoutCapShellTriplePointTopLeft, self._nodeLayoutCapShellTriplePointTopRight, + self._nodeLayoutCapShellTriplePointBottomLeft, self._nodeLayoutCapShellTriplePointBottomRight] + return nodeLayouts + + def getNodeLayoutCapBoxShield(self, isStartCap=True): + """ + Get node layout for elements between the core box and the shield in the cap mesh. There are four types + (Top, Bottom, Left, and Right) each with its specific node layout. The node layout also changes direction in + the y-axis depending on whether the cap is at the start or the end of a tube segment. + :return: HermiteNodeLayout. + """ + if isStartCap: + nodeLayoutCapBoxShieldTop = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, -1.0, 1.0]]) + nodeLayoutCapBoxShieldBottom = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, -1.0]]) + nodeLayoutCapBoxShieldLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [-1.0, -1.0, 0.0]]) + nodeLayoutCapBoxShieldRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [1.0, -1.0, 0.0]]) + else: + nodeLayoutCapBoxShieldTop = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [0.0, 1.0, 1.0]]) + nodeLayoutCapBoxShieldBottom = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, -1.0]]) + nodeLayoutCapBoxShieldLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [-1.0, 1.0, 0.0]]) + nodeLayoutCapBoxShieldRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [1.0, 1.0, 0.0]]) + + self._nodeLayoutCapBoxShield = [nodeLayoutCapBoxShieldTop, nodeLayoutCapBoxShieldBottom, + nodeLayoutCapBoxShieldLeft, nodeLayoutCapBoxShieldRight] + + return self._nodeLayoutCapBoxShield + + def getNodeLayoutCapBoxShieldTriplePoint(self): + """ + Get node layout for triple-point corners of core box elements in the cap mesh. There are four corners + (Top Left, Top Right, Bottom Left, and Bottom Right) each with its specific node layout. + :return: HermiteNodeLayout. + """ + nodeLayouts = [self._nodeLayoutCapBoxShieldTriplePointTopLeft, self._nodeLayoutCapBoxShieldTriplePointTopRight, + self._nodeLayoutCapBoxShieldTriplePointBottomLeft, self._nodeLayoutCapBoxShieldTriplePointBottomRight] + + # limitDirections = [None, None, None] + # nodeLayouts[0] = HermiteNodeLayout(None, nodeLayouts[0], limitDirections) + # nodeLayout = nodeLayouts[0] + # permutations = nodeLayout.getPermutations() + # print("permutations", permutations) + + return nodeLayouts + + def determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts): """ Determine the bicubic or tricubic Hermite serendipity element field template for diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index 3d4f4f10..e469851d 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -101,7 +101,7 @@ class NetworkSegment: Describes a segment of a network between junctions as a sequence of nodes with node derivative versions. """ - def __init__(self, networkNodes: list, nodeVersions: list): + def __init__(self, networkNodes: list, nodeVersions: list, isCap: list): """ :param networkNodes: List of NetworkNodes from start to end. Must be at least 2. :param nodeVersions: List of node versions to use for derivatives at network nodes. @@ -109,6 +109,7 @@ def __init__(self, networkNodes: list, nodeVersions: list): assert isinstance(networkNodes, list) and (len(networkNodes) > 1) and (len(nodeVersions) == len(networkNodes)) self._networkNodes = networkNodes self._nodeVersions = nodeVersions + self._isCap = isCap self._elementIdentifiers = [None] * (len(networkNodes) - 1) for networkNode in networkNodes[1:-1]: networkNode.setInteriorSegment(self) @@ -159,6 +160,12 @@ def isCyclic(self): """ return False # not implemented, assume not cyclic + def isCap(self): + """ + :return: True if the segment requires a cap mesh, False if not. + """ + return self._isCap + def split(self, splitNetworkNode): """ Split segment to finish at splitNetworkNode, returning remainder as a new NetworkSegment. @@ -207,6 +214,20 @@ def build(self, structureString): self._networkSegments = [] sequenceStrings = structureString.split(",") for sequenceString in sequenceStrings: + # check if the node requires a cap at the end + if not sequenceString[0].isnumeric() or not sequenceString[-1].isnumeric(): + try: + isStartCap = True if sequenceString[0] == "(" else False + isEndCap = True if sequenceString[-1] == ")" else False + sequenceString = sequenceString[1:] if isStartCap else sequenceString + sequenceString = sequenceString[:-1] if isEndCap else sequenceString + except ValueError: + print("Network mesh: Skipping invalid cap sequence", sequenceString, file=sys.stderr) + continue + else: + isStartCap = isEndCap = False + isCap = [isStartCap, isEndCap] + nodeIdentifiers = [] nodeVersions = [] nodeVersionStrings = sequenceString.split("-") @@ -240,7 +261,7 @@ def build(self, structureString): sequenceNodes.append(networkNode) sequenceVersions.append(nodeVersion) if (len(sequenceNodes) > 1) and (existingNetworkNode or (nodeIdentifier == nodeIdentifiers[-1])): - networkSegment = NetworkSegment(sequenceNodes, sequenceVersions) + networkSegment = NetworkSegment(sequenceNodes, sequenceVersions, isCap) self._networkSegments.append(networkSegment) sequenceNodes = sequenceNodes[-1:] sequenceVersions = sequenceVersions[-1:] diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index f41e6770..6d44b09f 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -4,6 +4,7 @@ from cmlibs.maths.vectorops import add, cross, dot, magnitude, mult, normalize, set_magnitude, sub, rejection from cmlibs.zinc.element import Element, Elementbasis from cmlibs.zinc.node import Node +from scaffoldmaker.utils.capmesh import CapMesh from scaffoldmaker.utils.eft_utils import ( addTricubicHermiteSerendipityEftParameterScaling, determineCubicHermiteSerendipityEft, HermiteNodeLayoutManager) from scaffoldmaker.utils.interpolation import ( @@ -70,6 +71,14 @@ def __init__(self, region, meshDimension, coordinateFieldName="coordinates", d3Defined, limitDirections=[None, [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]], None]) self._nodeLayoutTransitionTriplePoint = None + self._nodeLayoutCapTransition = self._nodeLayoutManager.getNodeLayoutCapTransition() + + self._nodeLayoutCapShellTriplePoint = None + + self._nodeLayoutCapBoxShield = None + self._nodeLayoutCapBoxShieldTriplePoint = None + + # annotation groups created if core: self._coreGroup = None self._shellGroup = None @@ -88,6 +97,9 @@ def createElementfieldtemplate(self): """ return self._mesh.createElementfieldtemplate(self._elementbasis) + def getCapElementtemplate(self): + return self._capElementtemplate + def getNodeLayout6Way(self): return self._nodeLayout6Way @@ -146,6 +158,77 @@ def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajor return self._nodeLayoutManager.getNodeLayoutBifurcation6WayTriplePoint( segmentsIn, sequence, maxMajorSegment, top) + def getNodeLayoutCapTransition(self): + """ + Node layout for generating cap shell transition elements, excluding at triple points. + """ + return self._nodeLayoutCapTransition + + def getNodeLayoutCapShellTriplePoint(self, location): + """ + Special node layout for generating cap shell transition elements at triple points. + There are four layouts specific to each corner of the core box: Top left (location = 1); + top right (location = -1); bottom left (location = 2); and bottom right (location = -2). + :param location: Location identifier identifying four corners of solid core box. + :return: Node layout. + """ + nodeLayouts = self._nodeLayoutManager.getNodeLayoutCapShellTriplePoint() + assert location in [1, -1, 2, -2, 0] + if location == 1: # "Top Left" + nodeLayout = nodeLayouts[0] + elif location == -1: # "Top Right" + nodeLayout = nodeLayouts[1] + elif location == 2: # "Bottom Left" + nodeLayout = nodeLayouts[2] + elif location == -2: # "Bottom Right" + nodeLayout = nodeLayouts[3] + else: + nodeLayout = self._nodeLayoutCapTransition + + self._nodeLayoutCapShellTriplePoint = nodeLayout + return self._nodeLayoutCapShellTriplePoint + + def getNodeLayoutCapBoxShield(self, location, isStartCap=True): + """ + + """ + nodeLayouts = self._nodeLayoutManager.getNodeLayoutCapBoxShield(isStartCap) + assert location in [1, -1, 2, -2, 0] + if location == 1: # "Top" + nodeLayout = nodeLayouts[0] + elif location == -1: # "Bottom" + nodeLayout = nodeLayouts[1] + elif location == 2: # "Left" + nodeLayout = nodeLayouts[2] + elif location == -2: # "Right" + nodeLayout = nodeLayouts[3] + else: + nodeLayout = None + + self._nodeLayoutCapBoxShield = nodeLayout + return self._nodeLayoutCapBoxShield + + def getNodeLayoutCapBoxShieldTriplePoint(self, location): + """ + + """ + nodeLayouts = self._nodeLayoutManager.getNodeLayoutCapBoxShieldTriplePoint() + assert location in [1, -1, 2, -2, 0] + if location == 1: # "Top Left" + nodeLayout = nodeLayouts[0] + elif location == -1: # "Top Right" + nodeLayout = nodeLayouts[1] + elif location == 2: # "Bottom Left" + nodeLayout = nodeLayouts[2] + elif location == -2: # "Bottom Right" + nodeLayout = nodeLayouts[3] + else: + nodeLayout = self._nodeLayoutCapBoxShield + + self._nodeLayoutCapBoxShieldTriplePoint = nodeLayout + return self._nodeLayoutCapBoxShieldTriplePoint + + def getNodetemplate(self): return self._nodetemplate @@ -281,6 +364,10 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem # [nAlong][nAcrossMajor][nAcrossMinor] format. self._boxElementIds = None # [along][major][minor] + self._networkPathParameters = pathParametersList + self._isCap = networkSegment.isCap() + self._capmesh = None + def getCoreBoundaryScalingMode(self): return self._coreBoundaryScalingMode @@ -511,6 +598,14 @@ def sample(self, fixedElementsCountAlong, targetElementLength): # sample coordinates for the solid core self._sampleCoreCoordinates(elementsCountAlong) + if self._isCap: + # sample coordinates for the cap mesh at the ends of a tube segment + self._capmesh = CapMesh(self._elementsCountAround, self._elementsCountCoreBoxMajor, + self._elementsCountCoreBoxMinor, self._elementsCountThroughShell, + self._elementsCountTransition, self._networkPathParameters, self._boxCoordinates, + self._transitionCoordinates, self._rimCoordinates, self._isCap, self._isCore) + self._capmesh.sampleCoordinates() + def _sampleCoreCoordinates(self, elementsCountAlong): """ Black box function for sampling coordinates for the solid core. @@ -1519,6 +1614,12 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): self._rimNodeIds[n2] = rimNodeIds continue + capmesh = self._capmesh + # create cap nodes at the start section of a tube segment + if self._isCap[0] and n2 == 0: + isStartCap = True + capmesh.generateNodes(generateData, isStartCap, self._isCore) + # create core box nodes if self._boxCoordinates: self._boxNodeIds[n2] = [] if self._boxNodeIds[n2] is None else self._boxNodeIds[n2] @@ -1570,6 +1671,11 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): ringNodeIds.append(nodeIdentifier) self._rimNodeIds[n2].append(ringNodeIds) + # create cap nodes at the end section of a tube segment + if self._isCap[-1] and n2 == elementsCountAlong: + isStartCap = False + self._endCapNodeIds = capmesh.generateNodes(generateData, isStartCap, self._isCore) + # create a new list containing box node ids are located at the boundary if self._isCore: self._boxBoundaryNodeIds, self._boxBoundaryNodeToBoxId = ( @@ -1586,6 +1692,16 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): self._boxElementIds[e2] = [] self._rimElementIds[e2] = [] e2p = e2 + 1 + # create cap elements + if self._isCap[0] and e2 == 0: + isStartCap = True + capmesh.generateElements(generateData, elementsCountRim, self._boxNodeIds, self._rimNodeIds, + annotationMeshGroups, self._coreBoundaryScalingMode, isStartCap, self._isCore) + elif self._isCap[-1] and e2 == (elementsCountAlong - endSkipCount - 1): + isStartCap = False + capmesh.generateElements(generateData, elementsCountRim, self._boxNodeIds, self._rimNodeIds, + annotationMeshGroups, self._coreBoundaryScalingMode, isStartCap, self._isCore) + if self._isCore: # create box elements elementsCountAcrossMinor = self.getCoreBoxMinorNodesCount() - 1 diff --git a/tests/test_capmesh.py b/tests/test_capmesh.py new file mode 100644 index 00000000..bb61805a --- /dev/null +++ b/tests/test_capmesh.py @@ -0,0 +1,402 @@ +import unittest + +from cmlibs.utils.zinc.finiteelement import evaluateFieldNodesetRange +from cmlibs.utils.zinc.general import ChangeManager +from cmlibs.utils.zinc.group import identifier_ranges_to_string, mesh_group_add_identifier_ranges, \ + mesh_group_to_identifier_ranges +from cmlibs.zinc.context import Context +from cmlibs.zinc.element import Element +from cmlibs.zinc.field import Field +from cmlibs.zinc.result import RESULT_OK +from scaffoldmaker.annotation.annotationgroup import findAnnotationGroupByName +from scaffoldmaker.meshtypes.meshtype_3d_tubenetwork1 import MeshType_3d_tubenetwork1 +from scaffoldmaker.scaffoldpackage import ScaffoldPackage + +from testutils import assertAlmostEqualList + +class CapScaffoldTestCase(unittest.TestCase): + + def test_3d_cap_tube_network_default(self): + """ + Test default 3-D tube network with cap at both ends is generated correctly. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Default") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + # change the network layout to have cap at both ends of the tube + networkLayoutSettings["Structure"] = "(1-2)" + self.assertEqual("(1-2)",networkLayoutSettings["Structure"]) + + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(13, len(settings)) + self.assertEqual(8, settings["Number of elements around"]) + self.assertEqual(1, settings["Number of elements through shell"]) + self.assertEqual([0], settings["Annotation numbers of elements around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) + self.assertFalse(settings["Use linear through shell"]) + self.assertTrue(settings["Use outer trim surfaces"]) + self.assertFalse(settings["Show trim surfaces"]) + self.assertFalse(settings["Core"]) + self.assertEqual(2, settings["Number of elements across core box minor"]) + self.assertEqual(1, settings["Number of elements across core transition"]) + self.assertEqual([0], settings["Annotation numbers of elements across core box minor"]) + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + + fieldmodule = region.getFieldmodule() + + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual(8 * 4 + 8 * 3 * 2, mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual((8 * 5 + 8 * 2 * 2 + 2) * 2, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [-0.150, -0.100, -0.100], X_TOL) + assertAlmostEqualList(self, maximums, [1.150, 0.100, 0.100], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + isExteriorXi3_0 = fieldmodule.createFieldAnd( + isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_0)) + isExteriorXi3_1 = fieldmodule.createFieldAnd( + isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + outerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + outerSurfaceAreaField.setNumbersOfPoints(4) + result, outerSurfaceArea = outerSurfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + innerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_0, coordinates, mesh2d) + innerSurfaceAreaField.setNumbersOfPoints(4) + result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.01475819159418598, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 0.83785503770891, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 0.6527066474646166, delta=X_TOL) + + def test_3d_cap_tube_network_default_core(self): + """ + Test default 3-D tube network with cap at both ends and with a solid core is generated correctly. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Default") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + # change the network layout to have cap at both ends of the tube + networkLayoutSettings["Structure"] = "(1-2)" + self.assertEqual("(1-2)",networkLayoutSettings["Structure"]) + + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(13, len(settings)) + self.assertEqual(8, settings["Number of elements around"]) + self.assertEqual(1, settings["Number of elements through shell"]) + self.assertEqual([0], settings["Annotation numbers of elements around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) + self.assertFalse(settings["Use linear through shell"]) + self.assertTrue(settings["Use outer trim surfaces"]) + self.assertFalse(settings["Show trim surfaces"]) + self.assertFalse(settings["Core"]) + self.assertEqual(2, settings["Number of elements across core box minor"]) + self.assertEqual(1, settings["Number of elements across core transition"]) + self.assertEqual([0], settings["Annotation numbers of elements across core box minor"]) + settings["Core"] = True + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + + fieldmodule = region.getFieldmodule() + + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual((8 + 8 + 4) * 4 + ((8 + 8 + 4) * 2 + 4 + 4) * 2, mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual((8 + 8 + 9) * 5 + ((8 + 8 + 9) + 9 * 3) * 2, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [-0.150, -0.100, -0.100], X_TOL) + assertAlmostEqualList(self, maximums, [1.150, 0.100, 0.100], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) + surfaceAreaField.setNumbersOfPoints(4) + result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.03928467254863209, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 0.8736482362813334, delta=X_TOL) + + + + def test_3d_cap_tube_network_bifurcation(self): + """ + Test bifurcation 3-D tube network with cap at both ends is generated correctly. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Bifurcation") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + # change the network layout to have cap at both ends of the tube + networkLayoutSettings["Structure"] = "(1-2.1,2.2-3),2.3-4)" + self.assertEqual("(1-2.1,2.2-3),2.3-4)",networkLayoutSettings["Structure"]) + + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(13, len(settings)) + self.assertEqual(8, settings["Number of elements around"]) + self.assertEqual(1, settings["Number of elements through shell"]) + self.assertEqual([0], settings["Annotation numbers of elements around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) + self.assertFalse(settings["Use linear through shell"]) + self.assertTrue(settings["Use outer trim surfaces"]) + self.assertFalse(settings["Show trim surfaces"]) + self.assertFalse(settings["Core"]) + self.assertEqual(2, settings["Number of elements across core box minor"]) + self.assertEqual(1, settings["Number of elements across core transition"]) + self.assertEqual([0], settings["Annotation numbers of elements across core box minor"]) + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + + fieldmodule = region.getFieldmodule() + + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual((8 * 4 + 8 * 3) * 3 , mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual((8 * 4 * 3 + 3 * 3 + 2) * 2 + (8 * 2 * 2 + 2) * 3, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [-0.1500000000000000, -0.6246941344953365, -0.1000000000000000], X_TOL) + assertAlmostEqualList(self, maximums, [2.1433222518126906, 0.6276801844614515, 0.1000000000000000], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + isExteriorXi3_0 = fieldmodule.createFieldAnd( + isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_0)) + isExteriorXi3_1 = fieldmodule.createFieldAnd( + isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + outerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + outerSurfaceAreaField.setNumbersOfPoints(4) + result, outerSurfaceArea = outerSurfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + innerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_0, coordinates, mesh2d) + innerSurfaceAreaField.setNumbersOfPoints(4) + result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.04024808736450315, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.251027047006869, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 1.7914675669958757, delta=X_TOL) + + def test_3d_tube_network_bifurcation_core(self): + """ + Test bifurcation 3-D tube network with solid core and cap at both ends is generated correctly. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Bifurcation") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + # change the network layout to have cap at both ends of the tube + networkLayoutSettings["Structure"] = "(1-2.1,2.2-3),2.3-4)" + self.assertEqual("(1-2.1,2.2-3),2.3-4)",networkLayoutSettings["Structure"]) + + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(13, len(settings)) + self.assertEqual(8, settings["Number of elements around"]) + self.assertEqual(1, settings["Number of elements through shell"]) + self.assertEqual([0], settings["Annotation numbers of elements around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) + self.assertFalse(settings["Use linear through shell"]) + self.assertFalse(settings["Show trim surfaces"]) + self.assertFalse(settings["Core"]) + self.assertEqual(2, settings["Number of elements across core box minor"]) + self.assertEqual(1, settings["Number of elements across core transition"]) + self.assertEqual([0], settings["Annotation numbers of elements across core box minor"]) + settings["Core"] = True + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + + fieldmodule = region.getFieldmodule() + + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual((8 * 4 * 3) * 2 + (4 * 4 * 3) +((8 + 8 + 4) * 2 + 4 + 4) * 3, mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual((8 * 4 * 3 + 3 * 3 + 2) * 2 + (9 * 4 * 3 + 3 * 4) + ((8 + 8 + 9) + 9 * 3) * 3, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [-0.1500000000000000, -0.6172290095800493, -0.1000000000000000], X_TOL) + assertAlmostEqualList(self, maximums, [2.1395896893550477, 0.6172290095800493, 0.1000000000000000], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) + surfaceAreaField.setNumbersOfPoints(4) + result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.11101973283867012, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.3035471266966363, delta=X_TOL) + + def test_3d_tube_network_converging_bifurcation_core(self): + """ + Test converging bifurcation 3-D tube network with solid core and 12, 12, 8 elements around. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Bifurcation") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + # change the network layout to have cap at both ends of the tube + networkLayoutSettings["Structure"] = "(1-3.1,(2-3.2,3.3-4)" + self.assertEqual("(1-3.1,(2-3.2,3.3-4)",networkLayoutSettings["Structure"]) + + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(13, len(settings)) + self.assertEqual(8, settings["Number of elements around"]) + self.assertEqual(1, settings["Number of elements through shell"]) + self.assertEqual([0], settings["Annotation numbers of elements around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) + self.assertFalse(settings["Use linear through shell"]) + self.assertFalse(settings["Show trim surfaces"]) + self.assertFalse(settings["Core"]) + self.assertEqual(2, settings["Number of elements across core box minor"]) + self.assertEqual(1, settings["Number of elements across core transition"]) + self.assertEqual([0], settings["Annotation numbers of elements across core box minor"]) + settings["Core"] = True + settings["Number of elements around"] = 12 + settings["Annotation numbers of elements around"] = [8] + + context = Context("Test") + region = context.getDefaultRegion() + + # add a user-defined annotation group to network layout to vary elements count around. Must generate first + tmpRegion = region.createRegion() + tmpFieldmodule = tmpRegion.getFieldmodule() + networkLayoutScaffoldPackage.generate(tmpRegion) + + annotationGroup1 = networkLayoutScaffoldPackage.createUserAnnotationGroup(("segment 3", "SEGMENT:3")) + group = annotationGroup1.getGroup() + mesh1d = tmpFieldmodule.findMeshByDimension(1) + meshGroup = group.createMeshGroup(mesh1d) + mesh_group_add_identifier_ranges(meshGroup, [[3, 3]]) + self.assertEqual(1, meshGroup.getSize()) + self.assertEqual(1, annotationGroup1.getDimension()) + identifier_ranges_string = identifier_ranges_to_string(mesh_group_to_identifier_ranges(meshGroup)) + self.assertEqual("3", identifier_ranges_string) + networkLayoutScaffoldPackage.updateUserAnnotationGroups() + + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + annotationGroups = scaffoldPackage.getAnnotationGroups() + self.assertEqual(3, len(annotationGroups)) + self.assertTrue(findAnnotationGroupByName(annotationGroups, "core") is not None) + self.assertTrue(findAnnotationGroupByName(annotationGroups, "shell") is not None) + + fieldmodule = region.getFieldmodule() + + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual(544, mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(680, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + # check annotation group transferred to 3D tube + annotationGroup = findAnnotationGroupByName(annotationGroups, "segment 3") + self.assertTrue(annotationGroup is not None) + self.assertEqual("SEGMENT:3", annotationGroup.getId()) + self.assertEqual(80, annotationGroup.getMeshGroup(fieldmodule.findMeshByDimension(3)).getSize()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [-0.14453769545049167, -0.6221797157655231, -0.1000000000000000], X_TOL) + assertAlmostEqualList(self, maximums, [2.15, 0.6221795584199485, 0.1000000000000000], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) + surfaceAreaField.setNumbersOfPoints(4) + result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.11060465010614413, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.3003361436808776, delta=X_TOL) + +if __name__ == "__main__": + unittest.main() From 9ee7a9da9c81cc1a64d21830f68315834eaa93cd Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 13 Nov 2024 14:03:32 +1300 Subject: [PATCH 02/43] Fix triple point box transition EFT --- src/scaffoldmaker/utils/capmesh.py | 68 ++++++++-------- src/scaffoldmaker/utils/eft_utils.py | 90 +++++++++++++--------- src/scaffoldmaker/utils/tubenetworkmesh.py | 4 +- 3 files changed, 90 insertions(+), 72 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 540085fd..42e619ff 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -734,11 +734,12 @@ def _determineShellDerivatives(self, isStartCap=True): for m in range(self._elementsCountCoreBoxMajor + 1): self._shellCoordinates[idx][1][n3][m][n] = sd1[m] + elementsCountRim = self._elementsCountThroughShell + self._elementsCountTransition - 1 for m in range(self._elementsCountCoreBoxMajor + 1): for n in range(self._elementsCountCoreBoxMinor + 1): otx = self._shellCoordinates[idx][0][-1][m][n] itx = self._shellCoordinates[idx][0][0][m][n] - shellFactor = 1.0 / self._elementsCountThroughShell + shellFactor = 1.0 / elementsCountRim sd3 = mult(sub(otx, itx), shellFactor) for n3 in range(nodesCountRim): self._shellCoordinates[idx][3][n3][m][n] = sd3 @@ -902,8 +903,12 @@ def _determineBoxDerivatives(self, isStartCap=True): for n in range(self._elementsCountCoreBoxMinor + 1): otx = self._tubeBoxCoordinates[0][idx][m][n] itx = self._boxCoordinates[idx][0][m][n] - d2 = sub(otx, itx) - self._boxCoordinates[idx][2][m][n] = mult(d2, signValue) + d2 = mult(sub(otx, itx), signValue) + self._boxCoordinates[idx][2][m][n] = d2 + d1 = self._boxCoordinates[idx][1][m][n] + d3 = self._boxCoordinates[idx][3][m][n] + self._boxCoordinates[idx][1][m][n] = set_magnitude(d1, magnitude(d2)) + self._boxCoordinates[idx][3][m][n] = set_magnitude(d3, magnitude(d2)) def _createBoundaryNodeIdsList(self, nodeIds): """ @@ -952,10 +957,10 @@ def _getBoxCoordinates(self, m, n, isStartCap=True): :return: x[], d1[], d2[], and d3[] of box nodes in the cap mesh. """ idx = 0 if isStartCap else -1 - return (self._boxCoordinates[idx][0][m][n], + return [self._boxCoordinates[idx][0][m][n], self._boxCoordinates[idx][1][m][n], self._boxCoordinates[idx][2][m][n], - self._boxCoordinates[idx][3][m][n]) + self._boxCoordinates[idx][3][m][n]] def _getBoxExtCoordinates(self, m, n, isStartCap=True): """ @@ -966,10 +971,10 @@ def _getBoxExtCoordinates(self, m, n, isStartCap=True): :return: x[], d1[], d2[], and d3[] of box nodes extended from the tube segment. """ idx = 0 if isStartCap else -1 - return (self._boxExtCoordinates[idx][0][m][n], + return [self._boxExtCoordinates[idx][0][m][n], self._boxExtCoordinates[idx][1][m][n], self._boxExtCoordinates[idx][2][m][n], - self._boxExtCoordinates[idx][3][m][n]) + self._boxExtCoordinates[idx][3][m][n]] def _getRimExtCoordinates(self, n1, n3, isStartCap=True): """ @@ -984,15 +989,15 @@ def _getRimExtCoordinates(self, n1, n3, isStartCap=True): if (self._transitionExtCoordinates and self._transitionExtCoordinates[idx]) else 0) if n3 < transitionNodeCount: - return (self._transitionExtCoordinates[idx][0][n3][n1], + return [self._transitionExtCoordinates[idx][0][n3][n1], self._transitionExtCoordinates[idx][1][n3][n1], self._transitionExtCoordinates[idx][2][n3][n1], - self._transitionExtCoordinates[idx][3][n3][n1]) + self._transitionExtCoordinates[idx][3][n3][n1]] sn3 = n3 - transitionNodeCount - return (self._shellExtCoordinates[idx][0][sn3][n1], + return [self._shellExtCoordinates[idx][0][sn3][n1], self._shellExtCoordinates[idx][1][sn3][n1], self._shellExtCoordinates[idx][2][sn3][n1], - self._shellExtCoordinates[idx][3][sn3][n1]) + self._shellExtCoordinates[idx][3][sn3][n1]] def _getRimExtCoordinatesAround(self, n3, isStartCap=True): """ @@ -1027,10 +1032,10 @@ def _getRimCoordinatesWithCore(self, m, n, n3, isStartCap=True): :return: cap rim coordinates and derivatives for points at n3, m and n. """ idx = 0 if isStartCap else -1 - return (self._shellCoordinates[idx][0][n3][m][n], + return [self._shellCoordinates[idx][0][n3][m][n], self._shellCoordinates[idx][1][n3][m][n], self._shellCoordinates[idx][2][n3][m][n], - self._shellCoordinates[idx][3][n3][m][n]) + self._shellCoordinates[idx][3][n3][m][n]] def _getTubeBoxCoordinates(self, m, n, isStartCap=True): """ @@ -1042,10 +1047,10 @@ def _getTubeBoxCoordinates(self, m, n, isStartCap=True): :return: Tube box coordinates and derivatives for points at n2, m and n. """ idx = 0 if isStartCap else -1 - return (self._tubeBoxCoordinates[0][idx][m][n], + return [self._tubeBoxCoordinates[0][idx][m][n], self._tubeBoxCoordinates[1][idx][m][n], self._tubeBoxCoordinates[2][idx][m][n], - self._tubeBoxCoordinates[3][idx][m][n]) + self._tubeBoxCoordinates[3][idx][m][n]] def _getTubeRimCoordinates(self, n1, n2, n3): """ @@ -1059,10 +1064,10 @@ def _getTubeRimCoordinates(self, n1, n2, n3): transitionNodeCount = (len(self._tubeTransitionCoordinates[0][0]) if (self._tubeTransitionCoordinates and self._tubeTransitionCoordinates[0]) else 0) if n3 < transitionNodeCount and self._isCore: - return (self._tubeTransitionCoordinates[0][n2][n3][n1], + return [self._tubeTransitionCoordinates[0][n2][n3][n1], self._tubeTransitionCoordinates[1][n2][n3][n1], self._tubeTransitionCoordinates[2][n2][n3][n1], - self._tubeTransitionCoordinates[3][n2][n3][n1]) + self._tubeTransitionCoordinates[3][n2][n3][n1]] sn3 = n3 - transitionNodeCount return [self._tubeShellCoordinates[0][n2][sn3][n1], self._tubeShellCoordinates[1][n2][sn3][n1], @@ -1486,6 +1491,9 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta mesh = generateData.getMesh() elementtemplateStd, eftStd = generateData.getStandardElementtemplate() + nodeLayoutTransition = generateData.getNodeLayoutTransition() + nodeLayoutCapTransition = generateData.getNodeLayoutCapTransition() + if isStartCap: capNodeIds = self._startCapNodeIds self._startCapElementIds = [] if self._startCapElementIds is None else self._startCapElementIds @@ -1529,7 +1537,6 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta capElementIds.append(boxElementIds) # box shield elements (elements joining the box and the shell elements) - nodeLayoutCapTransition = generateData.getNodeLayoutCapTransition() boxshieldElementIds = [] for e3 in range(elementsCountCoreBoxMajor): boxshieldElementIds.append([]) @@ -1550,8 +1557,7 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta nodeParameter = self._getBoxCoordinates(n3, n1, isStartCap) nodeParameters.append(nodeParameter) nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(boxLocation, isStartCap) - nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation) - nodeLayoutTransitionTriplePoint = generateData.getNodeLayoutTransitionTriplePoint(tpLocation) + nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation, isStartCap) if nid in boxBoundaryNodeIds[0]: nodeLayouts.append(nodeLayoutCapBoxShield if tpLocation == 0 else nodeLayoutCapBoxShieldTriplePoint) else: @@ -1564,7 +1570,6 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta elementtemplate = mesh.createElementtemplate() elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplate.defineField(coordinates, -1, eft) - element = mesh.createElement(elementIdentifier, elementtemplate) element.setNodesByIdentifier(eft, nids) if scalefactors: @@ -1610,12 +1615,8 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta n1p = (e1 + 1) % self._elementsCountAround boxLocation = self._getTriplePointLocation(e1) shellLocation = self._getTriplePointLocation(e1, isStartCap) - nodeLayoutTransition = generateData.getNodeLayoutTransition() - nodeLayoutCapTransition = generateData.getNodeLayoutCapTransition() nodeLayoutTransitionTriplePoint = generateData.getNodeLayoutTransitionTriplePoint(boxLocation) nodeLayoutCapShellTransitionTriplePoint = generateData.getNodeLayoutCapShellTriplePoint(shellLocation) - - elementIdentifier = generateData.nextElementIdentifier() for n3 in [0, 1]: for n1 in [e1, n1p]: nid = boxBoundaryNodeIds[n3][n1] if n3 == 0 else rimBoundaryNodeIds[n3 -1][n1] @@ -1623,10 +1624,10 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta mi, ni = boxBoundaryNodeToCapIndex[n3][n1] if n3 == 0 else rimBoundaryNodeToCapIndex[n3 - 1][n1] location, tpLocation = self._getBoxBoundaryLocation(mi, ni) nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(location, isStartCap) - nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation) + nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation, isStartCap) if n3 == 0: nodeParameter = self._getBoxCoordinates(mi, ni, isStartCap) - nodeLayout = nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList else ( + nodeLayout = nodeLayoutCapBoxShieldTriplePoint if n1 in triplePointIndexesList else ( nodeLayoutCapBoxShield) else: nodeParameter = self._getRimCoordinatesWithCore(mi, ni, 0, isStartCap) @@ -1650,15 +1651,14 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta for a in [nids, nodeParameters, nodeLayouts]: a[-4], a[-2] = a[-2], a[-4] a[-3], a[-1] = a[-1], a[-3] - # print("nids", nids) eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) - if self._elementsCountTransition == 1: - eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( - eft, scalefactors, nodeParameters, nids, coreBoundaryScalingMode) + # if self._elementsCountTransition == 1: + # eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( + # eft, scalefactors, nodeParameters, nids, coreBoundaryScalingMode) elementtemplate = mesh.createElementtemplate() elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplate.defineField(coordinates, -1, eft) - + elementIdentifier = generateData.nextElementIdentifier() element = mesh.createElement(elementIdentifier, elementtemplate) element.setNodesByIdentifier(eft, nids) if scalefactors: @@ -1850,8 +1850,8 @@ def _generateExtendedTubeElements(self, generateData, tubeBoxNodeIds, tubeRimNod a[-4], a[-2] = a[-2], a[-4] a[-3], a[-1] = a[-1], a[-3] eft = generateData.createElementfieldtemplate() - eft, scalefactors = addTricubicHermiteSerendipityEftParameterScaling( - eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3) + # eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( + # eft, scalefactors, nodeParameters, nids, coreBoundaryScalingMode) elementtemplateTransition = mesh.createElementtemplate() elementtemplateTransition.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplateTransition.defineField(coordinates, -1, eft) diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index e62e2570..7eae53e5 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -22,12 +22,13 @@ def getEftTermScaling(eft, functionIndex, termIndex): scaleFactorCount, scaleFactorIndexes = eft.getTermScaling(functionIndex, termIndex, 1) if scaleFactorCount < 0: if eft.getNumberOfLocalScaleFactors() > 0: - print('getEftTermScaling function', functionIndex, ' term', termIndex, ' scaleFactorCount ', scaleFactorCount) + print('getEftTermScaling function', functionIndex, ' term', termIndex, ' scaleFactorCount ', + scaleFactorCount) return [] if scaleFactorCount == 0: return [] if scaleFactorCount == 1: - return [ scaleFactorIndexes ] + return [scaleFactorIndexes] scaleFactorCount, scaleFactorIndexes = eft.getTermScaling(functionIndex, termIndex, scaleFactorCount) return scaleFactorIndexes @@ -41,7 +42,8 @@ def mapEftFunction1Node1Term(eft, function, localNode, valueLabel, version, scal eft.setTermScaling(function, 1, scaleFactors) -def mapEftFunction1Node2Terms(eft, function, localNode, valueLabel1, version1, scaleFactors1, valueLabel2, version2, scaleFactors2): +def mapEftFunction1Node2Terms(eft, function, localNode, valueLabel1, version1, scaleFactors1, valueLabel2, version2, + scaleFactors2): ''' Set function of eft to sum 2 terms the respective valueLabels, versions from localNode with scaleFactors ''' @@ -62,7 +64,8 @@ def remapEftLocalNodes(eft, newNodeCount, localNodeIndexes): for f in range(1, functionCount + 1): termCount = eft.getFunctionNumberOfTerms(f) for t in range(1, termCount + 1): - eft.setTermNodeParameter(f, t, localNodeIndexes[eft.getTermLocalNodeIndex(f, t) - 1], eft.getTermNodeValueLabel(f, t), eft.getTermNodeVersion(f, t)) + eft.setTermNodeParameter(f, t, localNodeIndexes[eft.getTermLocalNodeIndex(f, t) - 1], + eft.getTermNodeValueLabel(f, t), eft.getTermNodeVersion(f, t)) eft.setNumberOfLocalNodes(newNodeCount) @@ -130,7 +133,7 @@ def remapEftNodeValueLabelsVersion(eft, localNodeIndexes, valueLabels, version): valueLabel = eft.getTermNodeValueLabel(f, t) if (localNodeIndex in localNodeIndexes) and (valueLabel in valueLabels): result = eft.setTermNodeParameter(f, t, localNodeIndex, valueLabel, version) - #print('remap result', result) + # print('remap result', result) def remapEftNodeValueLabelWithNodes(eft, localNodeIndex, fromValueLabel, expressionTerms): @@ -145,7 +148,8 @@ def remapEftNodeValueLabelWithNodes(eft, localNodeIndex, fromValueLabel, express functionCount = eft.getNumberOfFunctions() for f in range(1, functionCount + 1): if eft.getFunctionNumberOfTerms(f) == 1: - if (eft.getTermLocalNodeIndex(f, 1) == localNodeIndex) and (eft.getTermNodeValueLabel(f, 1) == fromValueLabel) and (not getEftTermScaling(eft, f, 1)): + if (eft.getTermLocalNodeIndex(f, 1) == localNodeIndex) and ( + eft.getTermNodeValueLabel(f, 1) == fromValueLabel) and (not getEftTermScaling(eft, f, 1)): termCount = len(expressionTerms) eft.setFunctionNumberOfTerms(f, termCount) version = eft.getTermNodeVersion(f, 1) @@ -242,7 +246,7 @@ def createEftElementSurfaceLayer(elementIn, eftIn, eftfactory, eftStd, removeNod eft = eftStd scalefactors = None scaleFactorCountIn = eftIn.getNumberOfLocalScaleFactors() - scaleFactorsUsed = [False]*scaleFactorCountIn # flag scale factors used on top surface of element + scaleFactorsUsed = [False] * scaleFactorCountIn # flag scale factors used on top surface of element functionCountIn = eftIn.getNumberOfFunctions() halfFunctionCountIn = functionCountIn // 2 elementBasis = eftfactory.getElementbasis() @@ -268,7 +272,7 @@ def createEftElementSurfaceLayer(elementIn, eftIn, eftfactory, eftStd, removeNod scalingCount, scalingIndexes = eftIn.getTermScaling(fIn, t, 0) if scalingCount > 0: scalingIndexes = eftIn.getTermScaling(fIn, t, scalingCount)[1] - #if (scalingCount > 1) or (scalingIndexes != 1): + # if (scalingCount > 1) or (scalingIndexes != 1): # print("Scaling", scalingIndexes) if scalingCount == 1: scalingIndexes = [scalingIndexes] @@ -302,7 +306,7 @@ def createEftElementSurfaceLayer(elementIn, eftIn, eftfactory, eftStd, removeNod else: newt = t assert noRemap or (newt > 0), "Element " + str(elementIn.getIdentifier()) + " f " + str(f) + \ - " terms " + str(termCountIn) + " terms " + str(termCountIn) if True in scaleFactorsUsed: # get used scale factors, reorder indexes result, scalefactorsIn = elementIn.getScaleFactors(eftIn, scaleFactorCountIn) @@ -474,9 +478,9 @@ def determineTricubicHermiteEft(mesh, nodeParameters, nodeDerivativeFixedWeights functionNumber = n * 8 + crossDerivativeLabels[cd] derivativeIndexes = \ [0, 1] if (cd == 0) else \ - [0, 2] if (cd == 1) else \ - [1, 2] if (cd == 2) else \ - [0, 1, 2] + [0, 2] if (cd == 1) else \ + [1, 2] if (cd == 2) else \ + [0, 1, 2] sign = 1.0 crossDerivativeLabel = Node.VALUE_LABEL_VALUE for ed in derivativeIndexes: @@ -537,6 +541,9 @@ def _determinePermutations(self): # permutations.append((directions[j], directions[i])) else: normDir = [normalize(dir) for dir in self._directions] + # print("directions", self._directions) + # print("normDir", normDir) + # print("directionsCount", directionsCount) for i in range(directionsCount - 2): for j in range(i + 1, directionsCount - 1): if dot(normDir[i], normDir[j]) < -0.9: @@ -566,6 +573,7 @@ def _determinePermutations(self): permutations.append((self._directions[ii], self._directions[jj], self._directions[kk])) permutations.append((self._directions[jj], self._directions[kk], self._directions[ii])) permutations.append((self._directions[kk], self._directions[ii], self._directions[jj])) + # print("permutations", permutations) return permutations def getComplexity(self): @@ -606,11 +614,13 @@ def getDerivativeWeightsList(self, nodeDeltas, nodeDerivatives, localNodeIndex): inwardNodeDeltas = [[-d for d in nodeDeltas[i]] if flips[i] else nodeDeltas[i] for i in swizzleIndexes] derivativeWeightsList = None greatestSimilarity = -1.0 + # print("permutations", self._permutations) for permutation in self._permutations: # skip permutations using directions not in any supplied limitDirections skipPermutation = False limitIndex = 0 for limitDirections in self._limitDirections: + # print("limitDirections", limitDirections) if limitDirections: weights = permutation[swizzleIndexes[limitIndex]] if flips[limitIndex]: @@ -641,9 +651,12 @@ def getDerivativeWeightsList(self, nodeDeltas, nodeDerivatives, localNodeIndex): cosineSimilarity = dot(derivative, delta) / (magDerivative * magDelta) # magnitudeSimilarity = math.exp(-math.fabs((magDerivative - magDelta) / magDelta)) similarity += cosineSimilarity # * magnitudeSimilarity + # print("similarity", similarity) if similarity > greatestSimilarity: greatestSimilarity = similarity derivativeWeightsList = permutation + # print("swizzleIndexes", swizzleIndexes) + # print("derivativeWeightsList", derivativeWeightsList) finalWeightsList = [ [-w for w in derivativeWeightsList[swizzleIndexes[i]]] if flips[i] else derivativeWeightsList[swizzleIndexes[i]] for i in range(derivativesPerNode) @@ -687,7 +700,6 @@ def __init__(self): self._nodeLayoutTriplePointBottomRight = HermiteNodeLayout( [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]]) self.nodeLayoutsBifurcation6WayTriplePoint = {} - self._nodeLayoutCapShellTriplePointTopLeft = HermiteNodeLayout( [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [-1.0, 1.0, 0.0]]) self._nodeLayoutCapShellTriplePointTopRight = HermiteNodeLayout( @@ -696,20 +708,8 @@ def __init__(self): [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [-1.0, -1.0, 0.0]]) self._nodeLayoutCapShellTriplePointBottomRight = HermiteNodeLayout( [[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, -1.0, 0.0]]) - self._nodeLayoutCapBoxShield = None - - self._nodeLayoutCapBoxShieldTriplePointTopLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, 0.0, 1.0]]) - self._nodeLayoutCapBoxShieldTriplePointTopRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 0.0, 1.0]]) - - self._nodeLayoutCapBoxShieldTriplePointBottomLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 0.0, -1.0]]) - - self._nodeLayoutCapBoxShieldTriplePointBottomRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]]) - + self._nodeLayoutCapBoxShieldTriplePoint = None def getNodeLayoutRegularPermuted(self, d3Defined, limitDirections=None): """ @@ -884,6 +884,8 @@ def getNodeLayoutCapBoxShield(self, isStartCap=True): Get node layout for elements between the core box and the shield in the cap mesh. There are four types (Top, Bottom, Left, and Right) each with its specific node layout. The node layout also changes direction in the y-axis depending on whether the cap is at the start or the end of a tube segment. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. :return: HermiteNodeLayout. """ if isStartCap: @@ -910,22 +912,38 @@ def getNodeLayoutCapBoxShield(self, isStartCap=True): return self._nodeLayoutCapBoxShield - def getNodeLayoutCapBoxShieldTriplePoint(self): + def getNodeLayoutCapBoxShieldTriplePoint(self, isStartCap=True): """ Get node layout for triple-point corners of core box elements in the cap mesh. There are four corners (Top Left, Top Right, Bottom Left, and Bottom Right) each with its specific node layout. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. :return: HermiteNodeLayout. """ - nodeLayouts = [self._nodeLayoutCapBoxShieldTriplePointTopLeft, self._nodeLayoutCapBoxShieldTriplePointTopRight, - self._nodeLayoutCapBoxShieldTriplePointBottomLeft, self._nodeLayoutCapBoxShieldTriplePointBottomRight] + if isStartCap: + nodeLayoutCapBoxShieldTriplePointTopLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, -1.0, 1.0]]) + nodeLayoutCapBoxShieldTriplePointTopRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, -1.0, 1.0]]) + nodeLayoutCapBoxShieldTriplePointBottomLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, -1.0, -1.0]]) + nodeLayoutCapBoxShieldTriplePointBottomRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, -1.0, -1.0]]) + else: + nodeLayoutCapBoxShieldTriplePointTopLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, 1.0, 1.0]]) + nodeLayoutCapBoxShieldTriplePointTopRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]) + nodeLayoutCapBoxShieldTriplePointBottomLeft = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 1.0, -1.0]]) + nodeLayoutCapBoxShieldTriplePointBottomRight = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, -1.0]]) - # limitDirections = [None, None, None] - # nodeLayouts[0] = HermiteNodeLayout(None, nodeLayouts[0], limitDirections) - # nodeLayout = nodeLayouts[0] - # permutations = nodeLayout.getPermutations() - # print("permutations", permutations) + self._nodeLayoutCapBoxShieldTriplePoint = \ + [nodeLayoutCapBoxShieldTriplePointTopLeft, nodeLayoutCapBoxShieldTriplePointTopRight, + nodeLayoutCapBoxShieldTriplePointBottomLeft, nodeLayoutCapBoxShieldTriplePointBottomRight] - return nodeLayouts + return self._nodeLayoutCapBoxShieldTriplePoint def determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts): @@ -1009,7 +1027,7 @@ def determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts): nodeParameters[n][1], nodeParameters[n][2], nodeParameters[n][3] if d3Defined else None] - derivativeWeightsList =\ + derivativeWeightsList = \ nodeLayout.getDerivativeWeightsList(deltas[n], nodeDerivatives, n) if nodeLayout else None for ed in range(derivativesPerNode): if nodeLayout: diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 6d44b09f..6854685d 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -208,11 +208,11 @@ def getNodeLayoutCapBoxShield(self, location, isStartCap=True): self._nodeLayoutCapBoxShield = nodeLayout return self._nodeLayoutCapBoxShield - def getNodeLayoutCapBoxShieldTriplePoint(self, location): + def getNodeLayoutCapBoxShieldTriplePoint(self, location, isStartCap=True): """ """ - nodeLayouts = self._nodeLayoutManager.getNodeLayoutCapBoxShieldTriplePoint() + nodeLayouts = self._nodeLayoutManager.getNodeLayoutCapBoxShieldTriplePoint(isStartCap) assert location in [1, -1, 2, -2, 0] if location == 1: # "Top Left" nodeLayout = nodeLayouts[0] From 428c3084fef43af50cf51b46aaf47de7346f2b87 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 15 Nov 2024 15:03:43 +1300 Subject: [PATCH 03/43] Fix mismatched scalefactors between the tube mesh and the cap mesh --- src/scaffoldmaker/utils/capmesh.py | 372 +++++++++------------ src/scaffoldmaker/utils/eft_utils.py | 9 +- src/scaffoldmaker/utils/tubenetworkmesh.py | 36 +- tests/test_capmesh.py | 16 +- 4 files changed, 195 insertions(+), 238 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 42e619ff..82c8bde4 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -4,8 +4,7 @@ angle, mult, div from cmlibs.zinc.element import Element from cmlibs.zinc.node import Node -from scaffoldmaker.utils.eft_utils import determineCubicHermiteSerendipityEft, \ - addTricubicHermiteSerendipityEftParameterScaling +from scaffoldmaker.utils.eft_utils import determineCubicHermiteSerendipityEft from scaffoldmaker.utils.eftfactory_tricubichermite import eftfactory_tricubichermite from scaffoldmaker.utils.interpolation import smoothCubicHermiteDerivativesLoop, smoothCubicHermiteDerivativesLine, \ sampleCubicHermiteCurves, interpolateSampleCubicHermite @@ -51,6 +50,9 @@ def __init__(self, elementsCountAround, elementsCountCoreBoxMajor, elementsCount self._tubeTransitionCoordinates = tubeTransitionCoordinates # tube transition coordinates self._tubeShellCoordinates = tubeShellCoordinates # tube rim coordinates + self._isStartCap = None + self._generateData = None + self._boxExtCoordinates = None # coordinates and derivatives for box nodes extended from the tube segment # list[startCap, endCap][x, d1, d2, d3][nAcrossMajor][nAcrossMinor] @@ -82,21 +84,17 @@ def __init__(self, elementsCountAround, elementsCountCoreBoxMajor, elementsCount self._endCapElementIds = None # elementIds that form the cap at the end of a tube segment. - def _extendTubeEnds(self, isStartCap=True): + def _extendTubeEnds(self): """ Add additional tube sections with smaller element size along the tube at either ends of the tube with smaller D2 derivatives. This function is to minimise the effect of large difference in D2 derivatives between the cap mesh and the tube mesh. - :param isStartCap:True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ + isStartCap = self._isStartCap idx = 0 if isStartCap else -1 layoutD1 = self._networkPathParameters[0][1][idx] - outerRadius = self._calculateOuterShellRadius(isStartCap) - # segmentLength = magnitude(sub(self._networkPathParameters[0][0][0], self._networkPathParameters[0][0][1])) - # ext = segmentLength * 0.05 - ext = outerRadius / 2 ## may need another method to calculate extension + ext = self._getExtensionLength() unitVector = normalize(layoutD1) signValue = -1 if isStartCap else 1 @@ -171,19 +169,15 @@ def _extendTubeEnds(self, isStartCap=True): self._shellExtCoordinates = [None, None] if self._shellExtCoordinates is None else self._shellExtCoordinates self._shellExtCoordinates[idx] = shellCoordinates - def _remapCapCoordinates(self, isStartCap=True): + def _remapCapCoordinates(self): """ Remap box and rim coordinates of the cap nodes based on the scale of tube extension. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ + isStartCap = self._isStartCap idx = 0 if isStartCap else -1 layoutD1 = self._networkPathParameters[0][1][idx] - outerRadius = self._calculateOuterShellRadius(isStartCap) - ext = outerRadius / 2 - # segmentLength = magnitude(sub(self._networkPathParameters[0][0][0], self._networkPathParameters[0][0][1])) - # ext = segmentLength * 0.05 + ext = self._getExtensionLength() unitVector = normalize(layoutD1) signValue = -1 if isStartCap else 1 @@ -208,27 +202,23 @@ def _remapCapCoordinates(self, isStartCap=True): xList.append(tx) self._shellCoordinates[idx][0][n3][m] = xList - def _determineCapCoordinatesWithoutCore(self, isStartCap=True): + def _determineCapCoordinatesWithoutCore(self): """ Calculates coordinates and derivatives for the cap elements. It first calculates the coordinates for the apex nodes, and then calculates the coordinates for rim nodes on the shell surface. Used when the solid core is inactive. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ self._shellCoordinates = [None, None] if self._shellCoordinates is None else self._shellCoordinates + isStartCap = self._isStartCap idx = 0 if isStartCap else -1 signValue = 1 if isStartCap else -1 pathParameters = self._networkPathParameters[idx] - outerRadius = self._calculateOuterShellRadius(isStartCap) - shellThickness = self._calculateShellThickness(isStartCap) - # segmentLength = magnitude(sub(self._networkPathParameters[0][0][0], self._networkPathParameters[0][0][1])) - # ext = segmentLength * -0.05 - ext = -(outerRadius / 2) ## may need another method to calculate extension - centre = add(pathParameters[0][idx], set_magnitude(pathParameters[1][idx], ext * signValue)) - + outerRadius = self._getOuterShellRadius() + shellThickness = self._getShellThickness() + ext = self._getExtensionLength() + centre = add(pathParameters[0][idx], set_magnitude(pathParameters[1][idx], ext * -signValue)) outerWidth = outerLength = outerRadius innerWidth = innerLength = outerRadius - shellThickness @@ -291,7 +281,7 @@ def _determineCapCoordinatesWithoutCore(self, isStartCap=True): # calculate coordinates if n2 == 0: # apex x = apex = add(pathParameters[0][idx], - set_magnitude(pathParameters[1][idx], (position[1] + ext) * signValue )) + set_magnitude(pathParameters[1][idx], (position[1] - ext) * signValue)) d1 = set_magnitude(pathParameters[4][idx], vector2[0] * signValue) d2 = set_magnitude(pathParameters[2][idx], vector2[0]) d3 = set_magnitude(pathParameters[1][idx], vector3[1] * signValue) @@ -332,7 +322,7 @@ def _determineCapCoordinatesWithoutCore(self, isStartCap=True): radii = self._getTubeRadii(centre, n3, idx) oRadii = [1.0, rList[n3], rList[n3]] ratio = self._getRatioBetweenTwoRadii(radii, oRadii) - self._sphereToSpheroid(n3, ratio, centre, isStartCap) + self._sphereToSpheroid(n3, ratio, centre) # smooth derivatives for n3 in range(elementsCountThroughShell + 1): xList = self._shellCoordinates[idx][0] @@ -362,40 +352,39 @@ def _determineCapCoordinatesWithoutCore(self, isStartCap=True): for n3 in range(elementsCountThroughShell + 1): d3List[n3][1][n1] = sd3[n3] - def _determineCapCoordinatesWithCore(self, isStartCap=True): + def _determineCapCoordinatesWithCore(self, s): """ Blackbox function for calculating coordinates and derivatives for the cap elements. It first calculates the coordinates for shell nodes, then calculates for box nodes. nodes, and then calculates the coordinates for rim nodes on the shell surface. Used when the solid core is active. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. + :param s: Index for isCap list. 0 indicates start cap and 1 indicates end cap. """ + self._isStartCap = isStartCap = True if self._isCap[0] and s == 0 else False idx = 0 if isStartCap else -1 centre = self._networkPathParameters[0][0][idx] - self._extendTubeEnds(isStartCap) # extend tube end + self._extendTubeEnds() # extend tube end # shell nodes nodesCountRim = self._getNodesCountRim() for n3 in range(nodesCountRim): - ox = self._getRimExtCoordinatesAround(n3, isStartCap)[0] - radius = self._calculateRadius(ox) + ox = self._getRimExtCoordinatesAround(n3)[0] + radius = self._getRadius(ox) radii = self._getTubeRadii(centre, n3, idx) # radii for spheroid oRadii = [1.0, radius, radius] # original radii used to create the sphere ratio = self._getRatioBetweenTwoRadii(radii, oRadii) # ratio between original radii for the sphere and the new radii for spheroid - self._calculateMajorAndMinorNodesCoordinates(n3, centre, ratio, isStartCap) - self._calculateShellQuadruplePoints(n3, centre, radius, isStartCap) - self._calculateShellRegularNodeCoordinates(n3, centre, isStartCap) - self._sphereToSpheroid(n3, ratio, centre, isStartCap) - self._determineShellDerivatives(isStartCap) + self._calculateMajorAndMinorNodesCoordinates(n3, centre, ratio) + self._calculateShellQuadruplePoints(n3, centre, radius) + self._calculateShellRegularNodeCoordinates(n3, centre) + self._sphereToSpheroid(n3, ratio, centre) + self._determineShellDerivatives() # box nodes - self._calculateBoxQuadruplePoints(centre, isStartCap) - self._calculateBoxMajorAndMinorNodes(isStartCap) - self._determineBoxDerivatives(isStartCap) + self._calculateBoxQuadruplePoints(centre) + self._calculateBoxMajorAndMinorNodes() + self._determineBoxDerivatives() - self._remapCapCoordinates(isStartCap) - self._extendTubeEnds(isStartCap) + self._remapCapCoordinates() def _createShellCoordinatesList(self): """ @@ -415,15 +404,13 @@ def _createShellCoordinatesList(self): self._shellCoordinates[s][nx][n3][m] = \ [None for _ in range(self._elementsCountCoreBoxMinor + 1)] - def _calculateOuterShellRadius(self, isStartCap=True): + def _getOuterShellRadius(self): """ Calculates the radius of an outer shell. It takes the average of a half-distance between two opposing nodes on the outer shell of a tube segment. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. :return: Radius of the cap shell. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 ox = self._tubeShellCoordinates[0][idx][-1] radii = [] for i in range(self._elementsCountAround // 2): @@ -432,7 +419,7 @@ def _calculateOuterShellRadius(self, isStartCap=True): radii.append(r) return sum(radii) / len(radii) - def _calculateRadius(self, ox): + def _getRadius(self, ox): """ Calculates the radius of a shell. It takes the average of a half-distance between two opposing nodes around a tube segment. @@ -446,15 +433,13 @@ def _calculateRadius(self, ox): radii.append(r) return sum(radii) / len(radii) - def _calculateShellThickness(self, isStartCap=True): + def _getShellThickness(self): """ Calculates the thickness of a shell, based on the thickness of a tube segment at either ends. It takes the average of a distance between the outer and the inner node pair around the rim of a tube segment. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. :return: Thickness of the cap shell. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 ix = self._tubeShellCoordinates[0][idx][0] ox = self._tubeShellCoordinates[0][idx][-1] @@ -464,7 +449,15 @@ def _calculateShellThickness(self, isStartCap=True): shellThicknesses.append(thickness) return sum(shellThicknesses) / len(shellThicknesses) - def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio, isStartCap=True): + def _getExtensionLength(self): + """ + Calculates the length of extended tube segment. Currently set to half of the outer tube radius. + :return: Length of extended tube segment. + """ + outerRadius = self._getOuterShellRadius() + return outerRadius / 2 + + def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio): """ Calculates coordinates and derivatives for major and minor axis nodes on the surface of a cap shell by rotating the major and minor axis nodes on the rim of a tube segment. @@ -472,10 +465,8 @@ def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio, isStartCap= :param centre: Centre coordinates of a tube segment at either ends. :param ratio: List of ratios between original circular radii and new radii if the tube is non-circular. [x-axis, major axis, minor axis]. The values should equal 1.0 if the tube cross-section is circular. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 layoutD2 = self._networkPathParameters[0][2][idx] layoutD3 = self._networkPathParameters[0][4][idx] @@ -483,7 +474,7 @@ def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio, isStartCap= elementsCountAcrossMinor = self._elementsCountCoreBoxMinor + 2 refAxis = normalize(layoutD2) - rotateAngle = (math.pi / elementsCountAcrossMinor) if isStartCap else \ + rotateAngle = (math.pi / elementsCountAcrossMinor) if self._isStartCap else \ -(math.pi / elementsCountAcrossMinor) minorAxisNodesCoordinates = [[], []] # [startCap, endCap] n1 = self._elementsCountAround * 3 // 4 @@ -497,7 +488,7 @@ def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio, isStartCap= minorAxisNodesCoordinates[nx].append(vr) refAxis = normalize(layoutD3) - rotateAngle = (math.pi / elementsCountAcrossMajor) if isStartCap else \ + rotateAngle = (math.pi / elementsCountAcrossMajor) if self._isStartCap else \ -(math.pi / elementsCountAcrossMajor) majorAxisNodesCoordinates = [[], []] # [startCap, endCap] ix = self._getTubeRimCoordinates(0, idx, n3) @@ -539,16 +530,14 @@ def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio, isStartCap= for m in range(elementsCountAcrossMajor - 1): self._shellCoordinates[idx][1][n3][m][midMinorIndex] = sd1[m] - def _calculateShellQuadruplePoints(self, n3, centre, radius, isStartCap=True): + def _calculateShellQuadruplePoints(self, n3, centre, radius): """ Calculate coordinates and derivatives of the quadruple point on the surface, where 3 hex elements merge. :param n3: Node index from inner to outer rim. :param centre: Centre coordinates of a tube segment at either ends. :param radius: Shell radius. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 layoutD1 = self._networkPathParameters[0][1][idx] layoutD2 = self._networkPathParameters[0][2][idx] @@ -563,7 +552,7 @@ def _calculateShellQuadruplePoints(self, n3, centre, radius, isStartCap=True): elementsCountAcrossMajor = self._elementsCountCoreBoxMajor + 2 elementsCountAcrossMinor = self._elementsCountCoreBoxMinor + 2 counter = 0 - signValue = 1 if isStartCap else -1 + signValue = 1 if self._isStartCap else -1 for m in [0, -1]: for n in [0, -1]: if m == n: @@ -586,21 +575,19 @@ def _calculateShellQuadruplePoints(self, n3, centre, radius, isStartCap=True): phi_3 = calculate_azimuth(theta_3, theta_2) ratio = 1 local_x = spherical_to_cartesian(radius, theta_3, ratio * phi_3 + (1 - ratio) * math.pi / 2) - c = counter if isStartCap else -(counter + 1) + c = counter if self._isStartCap else -(counter + 1) axes = [mult(axis, signValue) for axis in axesList[c]] x = local_to_global_coordinates(local_x, axes, centre) self._shellCoordinates[idx][0][n3][m][n] = x counter += 1 - def _calculateShellRegularNodeCoordinates(self, n3, centre, isStartCap=True): + def _calculateShellRegularNodeCoordinates(self, n3, centre): """ Calculate coordinates and derivatives of all other shell nodes on the cap surface. :param n3: Node index from inner to outer rim. :param centre: Centre coordinates of a tube segment at either ends. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 elementsCountAcrossMajor = self._elementsCountCoreBoxMajor + 2 elementsCountAcrossMinor = self._elementsCountCoreBoxMinor + 2 midMajorIndex = elementsCountAcrossMajor // 2 - 1 @@ -643,17 +630,15 @@ def _calculateShellRegularNodeCoordinates(self, n3, centre, isStartCap=True): self._shellCoordinates[idx][1][n3][c][n] = [0, 0, 0] self._shellCoordinates[idx][2][n3][c][n] = nd2[c % (len(nd2) - 1)] - def _sphereToSpheroid(self, n3, ratio, centre, isStartCap=True): + def _sphereToSpheroid(self, n3, ratio, centre): """ Transform the sphere to ellipsoid using the radius in each direction. :param n3: Node index from inner to outer rim. :param ratio: List of ratios between original circular radii and new radii if the tube is non-circular. [x-axis, major axis, minor axis]. The values should equal 1.0 if the tube cross-section is circular. :param centre: Centre coordinates of a tube segment at either ends. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 # rotation angles to use in case when the cap is tilted from xyz axes. layoutD2 = normalize(self._networkPathParameters[0][2][idx]) @@ -700,13 +685,11 @@ def _getTubeRadii(self, centre, n3, idx): return [1.0, majorRadius, minorRadius] - def _determineShellDerivatives(self, isStartCap=True): + def _determineShellDerivatives(self): """ Compute d1, d2, and d3 derivatives for the shell nodes. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 nodesCountRim = self._getNodesCountRim() for n3 in range(nodesCountRim): for m in [0, -1]: @@ -721,7 +704,7 @@ def _determineShellDerivatives(self, isStartCap=True): for n3 in range(nodesCountRim): for m in range(self._elementsCountCoreBoxMajor + 1): - signValue = 1 if isStartCap else -1 + signValue = 1 if self._isStartCap else -1 tx = self._shellCoordinates[idx][0][n3][m] td2 = self._shellCoordinates[idx][2][n3][m] sd2 = smoothCubicHermiteDerivativesLine(tx, td2) @@ -744,14 +727,12 @@ def _determineShellDerivatives(self, isStartCap=True): for n3 in range(nodesCountRim): self._shellCoordinates[idx][3][n3][m][n] = sd3 - def _calculateBoxQuadruplePoints(self, centre, isStartCap=True): + def _calculateBoxQuadruplePoints(self, centre): """ Calculate coordinates and derivatives of the quadruple point for the box elements, where 3 hex elements merge. :param centre: Centre coordinates of a tube segment at either ends. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 capBoxCoordinates = [] for nx in range(4): capBoxCoordinates.append([]) @@ -795,13 +776,11 @@ def _calculateBoxQuadruplePoints(self, centre, isStartCap=True): self._boxCoordinates = [None] * 2 self._boxCoordinates[idx] = capBoxCoordinates - def _calculateBoxMajorAndMinorNodes(self, isStartCap=True): + def _calculateBoxMajorAndMinorNodes(self): """ Calculate coordinates and derivatives for box nodes along the central major and minor axes. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 midMajorIndex = self._elementsCountCoreBoxMajor // 2 midMinorIndex = self._elementsCountCoreBoxMinor // 2 @@ -891,14 +870,12 @@ def _calculateBoxMajorAndMinorNodes(self, isStartCap=True): for m in range(1, self._elementsCountCoreBoxMajor): self._boxCoordinates[idx][1][m][n] = sd1[m] - def _determineBoxDerivatives(self, isStartCap=True): + def _determineBoxDerivatives(self): """ Calculate d2 derivatives of box nodes. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 - signValue = 1 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 + signValue = 1 if self._isStartCap else -1 for m in range(self._elementsCountCoreBoxMajor + 1): for n in range(self._elementsCountCoreBoxMinor + 1): otx = self._tubeBoxCoordinates[0][idx][m][n] @@ -948,43 +925,37 @@ def _createBoundaryNodeIdsList(self, nodeIds): return capBoundaryNodeIds, capBoundaryNodeToBoxId - def _getBoxCoordinates(self, m, n, isStartCap=True): + def _getBoxCoordinates(self, m, n): """ :param m: Index along the major axis. :param n: Index along the minor axis. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. :return: x[], d1[], d2[], and d3[] of box nodes in the cap mesh. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 return [self._boxCoordinates[idx][0][m][n], self._boxCoordinates[idx][1][m][n], self._boxCoordinates[idx][2][m][n], self._boxCoordinates[idx][3][m][n]] - def _getBoxExtCoordinates(self, m, n, isStartCap=True): + def _getBoxExtCoordinates(self, m, n): """ :param m: Index along the major axis. :param n: Index along the minor axis. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. :return: x[], d1[], d2[], and d3[] of box nodes extended from the tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 return [self._boxExtCoordinates[idx][0][m][n], self._boxExtCoordinates[idx][1][m][n], self._boxExtCoordinates[idx][2][m][n], self._boxExtCoordinates[idx][3][m][n]] - def _getRimExtCoordinates(self, n1, n3, isStartCap=True): + def _getRimExtCoordinates(self, n1, n3): """ :param n1: Index around rim. :param n3: Index from inner to outer rim. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. :return: x[], d1[], d2[], and d3[] of rim nodes extended from the tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 transitionNodeCount = (len(self._transitionExtCoordinates[idx][0]) if (self._transitionExtCoordinates and self._transitionExtCoordinates[idx]) else 0) @@ -999,54 +970,48 @@ def _getRimExtCoordinates(self, n1, n3, isStartCap=True): self._shellExtCoordinates[idx][2][sn3][n1], self._shellExtCoordinates[idx][3][sn3][n1]] - def _getRimExtCoordinatesAround(self, n3, isStartCap=True): + def _getRimExtCoordinatesAround(self, n3): """ :param n3: Index from inner to outer rim. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. :return: x[], d1[], d2[], and d3[] of rim nodes extended from the tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 transitionNodeCount = (len(self._transitionExtCoordinates[idx][0]) if (self._transitionExtCoordinates and self._transitionExtCoordinates[idx]) else 0) if n3 < transitionNodeCount: - return (self._transitionExtCoordinates[idx][0][n3], + return [self._transitionExtCoordinates[idx][0][n3], self._transitionExtCoordinates[idx][1][n3], self._transitionExtCoordinates[idx][2][n3], - self._transitionExtCoordinates[idx][3][n3]) + self._transitionExtCoordinates[idx][3][n3]] sn3 = n3 - transitionNodeCount - return (self._shellExtCoordinates[idx][0][sn3], + return [self._shellExtCoordinates[idx][0][sn3], self._shellExtCoordinates[idx][1][sn3], self._shellExtCoordinates[idx][2][sn3], - self._shellExtCoordinates[idx][3][sn3]) + self._shellExtCoordinates[idx][3][sn3]] - def _getRimCoordinatesWithCore(self, m, n, n3, isStartCap=True): + def _getRimCoordinatesWithCore(self, m, n, n3): """ Get coordinates and derivatives for cap rim. Only applies when core option is active. :param m: Index across major axis. :param n: Index across minor axis. :param n3: Index along the tube. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. :return: cap rim coordinates and derivatives for points at n3, m and n. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 return [self._shellCoordinates[idx][0][n3][m][n], self._shellCoordinates[idx][1][n3][m][n], self._shellCoordinates[idx][2][n3][m][n], self._shellCoordinates[idx][3][n3][m][n]] - def _getTubeBoxCoordinates(self, m, n, isStartCap=True): + def _getTubeBoxCoordinates(self, m, n): """ Get coordinates and derivatives for tube box. :param m: Index across major axis. :param n: Index across minor axis. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. :return: Tube box coordinates and derivatives for points at n2, m and n. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 return [self._tubeBoxCoordinates[0][idx][m][n], self._tubeBoxCoordinates[1][idx][m][n], self._tubeBoxCoordinates[2][idx][m][n], @@ -1058,7 +1023,6 @@ def _getTubeRimCoordinates(self, n1, n2, n3): :param n1: Node index around. :param n2: Node index along segment. :param n3: Node index from inner to outer rim. - :param isCore: :return: Tube rim coordinates and derivatives for points at n1, n2 and n3. """ transitionNodeCount = (len(self._tubeTransitionCoordinates[0][0]) @@ -1089,11 +1053,12 @@ def _getTriplePointIndexes(self): return triplePointIndexesList - def _getTriplePointLocation(self, e1, isStartCap=True): + def _getTriplePointLocation(self, e1, isShell=False): """ Determines the location of a specific triple point relative to the solid core box. There are four locations: Top left (location = 1); top right (location = -1); bottom left (location = 2); and bottom right (location = -2). Location is None if not located at any of the four specified locations. + :param isShell: True if the triple point is located on the shell layer, False if located on the core box. :return: Location identifier. """ em = self._elementsCountCoreBoxMinor // 2 @@ -1117,7 +1082,7 @@ def _getTriplePointLocation(self, e1, isStartCap=True): else: location = 0 - if not isStartCap and location != 0: + if isShell and not self._isStartCap and location != 0: location = location + 1 if location > 0 else location - 1 if abs(location) > 2: location = location - 2 if location > 0 else location + 2 @@ -1206,13 +1171,11 @@ def _sampleCurvesOnSphere(self, x1, x2, origin, elementsOut): return nx, nd1 - def _generateNodesWithoutCore(self, generateData, isStartCap=True): + def _generateNodesWithoutCore(self): """ Blackbox function for generating cap nodes. Used only when the tube segment does not have a core. - :param generateData: TubeNetworkMeshGenerateData class object. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ + generateData = self._generateData coordinates = generateData.getCoordinates() fieldcache = generateData.getFieldcache() nodes = generateData.getNodes() @@ -1220,7 +1183,7 @@ def _generateNodesWithoutCore(self, generateData, isStartCap=True): nodesCountShell = len(self._tubeShellCoordinates[0][0]) capNodeIds = [] - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 for n2 in range(2): capNodeIds.append([]) for n3 in range(nodesCountShell): @@ -1255,18 +1218,16 @@ def _generateNodesWithoutCore(self, generateData, isStartCap=True): coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) capNodeIds[n2][n3].append(nodeIdentifier) - if isStartCap: + if self._isStartCap: self._startCapNodeIds = capNodeIds else: self._endCapNodeIds = capNodeIds - def _generateNodesWithCore(self, generateData, isStartCap): + def _generateNodesWithCore(self): """ Blackbox function for generating cap nodes. Used only when the tube segment has a core. - :param generateData: TubeNetworkMeshGenerateData class object. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ + generateData = self._generateData coordinates = generateData.getCoordinates() fieldcache = generateData.getFieldcache() nodes = generateData.getNodes() @@ -1276,7 +1237,7 @@ def _generateNodesWithCore(self, generateData, isStartCap): nodesCountCoreBoxMinor = self._getNodesCountCoreBoxMinor() nloop = self._getNodesCountRim() + 1 capNodeIds = [] - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 for n3 in range(nloop): if n3 == 0: capNodeIds.append([]) @@ -1314,19 +1275,17 @@ def _generateNodesWithCore(self, generateData, isStartCap): coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) capNodeIds[n3][m].append(nodeIdentifier) - if isStartCap: + if self._isStartCap: self._startCapNodeIds = capNodeIds else: self._endCapNodeIds = capNodeIds - def _generateExtendedTubeNodes(self, generateData, isStartCap=True): + def _generateExtendedTubeNodes(self): """ Blackbox function for generating tube nodes extended from the original tube segment. - :param generateData: TubeNetworkMeshGenerateData class object. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ - idx = 0 if isStartCap else -1 + idx = 0 if self._isStartCap else -1 + generateData = self._generateData coordinates = generateData.getCoordinates() fieldcache = generateData.getFieldcache() nodes = generateData.getNodes() @@ -1385,21 +1344,18 @@ def _generateExtendedTubeNodes(self, generateData, isStartCap=True): ringNodeIds.append(nodeIdentifier) self._rimExtNodeIds[idx].append(ringNodeIds) - def _generateElementsWithoutCore(self, generateData, elementsCountRim, tubeRimNodeIds, annotationMeshGroups, - isStartCap=True): + def _generateElementsWithoutCore(self, elementsCountRim, annotationMeshGroups): """ Blackbox function for generating cap elements. Used only when the tube segment does not have a core. - :param generateData: TubeNetworkMeshGenerateData class object. :param elementsCountRim: Number of elements through the rim. - :param tubeRimNodeIds: List of tube rim nodes. :param annotationMeshGroups: List of all annotated mesh groups. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ + generateData = self._generateData coordinates = generateData.getCoordinates() mesh = generateData.getMesh() elementtemplateStd, eftStd = generateData.getStandardElementtemplate() eftfactory = eftfactory_tricubichermite(mesh, False) + isStartCap = self._isStartCap if isStartCap: capNodeIds = self._startCapNodeIds @@ -1474,19 +1430,18 @@ def _generateElementsWithoutCore(self, generateData, elementsCountRim, tubeRimNo else: self._endCapElementIds = capElementIds - def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isStartCap=True): + def _generateElementsWithCore(self): """ Blackbox function for generating cap elements. Used only when the tube segment has a core. - :param generateData: TubeNetworkMeshGenerateData class object. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ + isStartCap = self._isStartCap idx = 0 if isStartCap else -1 elementsCountAround = self._elementsCountAround elementsCountCoreBoxMinor = self._elementsCountCoreBoxMinor elementsCountCoreBoxMajor = self._elementsCountCoreBoxMajor elementsCountRim = self._getElementsCountRim() + generateData = self._generateData coordinates = generateData.getCoordinates() mesh = generateData.getMesh() elementtemplateStd, eftStd = generateData.getStandardElementtemplate() @@ -1524,8 +1479,7 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta for e1 in range(elementsCountCoreBoxMinor): nids, nodeParameters, nodeLayouts = [], [], [] for n1 in [e1, e1 + 1]: - nids += [capNodeIds[0][e3][n1], capNodeIds[0][e3p][n1], - boxExtNodeIds[e3][n1], boxExtNodeIds[e3p][n1]] + nids += [capNodeIds[0][e3][n1], capNodeIds[0][e3p][n1], boxExtNodeIds[e3][n1], boxExtNodeIds[e3p][n1]] if not isStartCap: for a in [nids]: a[-4], a[-2] = a[-2], a[-4] @@ -1547,14 +1501,14 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta for n1 in [e1, e1 + 1]: for n3 in [e3, e3p]: nids += [capNodeIds[1][n3][n1]] - nodeParameter = self._getRimCoordinatesWithCore(n3, n1, 0, isStartCap) + nodeParameter = self._getRimCoordinatesWithCore(n3, n1, 0) nodeParameters.append(nodeParameter) nodeLayouts.append(nodeLayoutCapTransition) for n3 in [e3, e3p]: boxLocation, tpLocation = self._getBoxBoundaryLocation(n3, n1) nid = capNodeIds[0][n3][n1] nids += [nid] - nodeParameter = self._getBoxCoordinates(n3, n1, isStartCap) + nodeParameter = self._getBoxCoordinates(n3, n1) nodeParameters.append(nodeParameter) nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(boxLocation, isStartCap) nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation, isStartCap) @@ -1614,7 +1568,7 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta nids, nodeParameters, nodeLayouts = [], [], [] n1p = (e1 + 1) % self._elementsCountAround boxLocation = self._getTriplePointLocation(e1) - shellLocation = self._getTriplePointLocation(e1, isStartCap) + shellLocation = self._getTriplePointLocation(e1, isShell=True) nodeLayoutTransitionTriplePoint = generateData.getNodeLayoutTransitionTriplePoint(boxLocation) nodeLayoutCapShellTransitionTriplePoint = generateData.getNodeLayoutCapShellTriplePoint(shellLocation) for n3 in [0, 1]: @@ -1626,11 +1580,11 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(location, isStartCap) nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation, isStartCap) if n3 == 0: - nodeParameter = self._getBoxCoordinates(mi, ni, isStartCap) + nodeParameter = self._getBoxCoordinates(mi, ni) nodeLayout = nodeLayoutCapBoxShieldTriplePoint if n1 in triplePointIndexesList else ( nodeLayoutCapBoxShield) else: - nodeParameter = self._getRimCoordinatesWithCore(mi, ni, 0, isStartCap) + nodeParameter = self._getRimCoordinatesWithCore(mi, ni, 0) nodeLayout = nodeLayoutCapShellTransitionTriplePoint if n1 in triplePointIndexesList else ( nodeLayoutCapTransition) nodeParameters.append(nodeParameter) @@ -1639,10 +1593,10 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta if n3 == 0: nid = boxExtBoundaryNodeIds[n1] mi, ni = boxExtBoundaryNodestoBoxIds[n1] - nodeParameter = self._getBoxExtCoordinates(mi, ni, isStartCap) + nodeParameter = self._getBoxExtCoordinates(mi, ni) else: nid = rimExtNodeIds[0][n1] - nodeParameter = self._getRimExtCoordinates(n1, 0, isStartCap) + nodeParameter = self._getRimExtCoordinates(n1, 0) nids += [nid] nodeParameters.append(nodeParameter) nodeLayouts.append(nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList and n3 == 0 @@ -1652,9 +1606,6 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta a[-4], a[-2] = a[-2], a[-4] a[-3], a[-1] = a[-1], a[-3] eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) - # if self._elementsCountTransition == 1: - # eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( - # eft, scalefactors, nodeParameters, nids, coreBoundaryScalingMode) elementtemplate = mesh.createElementtemplate() elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplate.defineField(coordinates, -1, eft) @@ -1673,20 +1624,20 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta for e1 in range(self._elementsCountAround): nids, nodeParameters, nodeLayouts = [], [], [] e1p = (e1 + 1) % self._elementsCountAround - location = self._getTriplePointLocation(e1, isStartCap) + location = self._getTriplePointLocation(e1, isShell=True) nodeLayoutCapTransition = generateData.getNodeLayoutCapTransition() nodeLayoutCapShellTriplePoint = generateData.getNodeLayoutCapShellTriplePoint(location) for n3 in [e3, e3 + 1]: for n1 in [e1, e1p]: nids += [rimBoundaryNodeIds[n3][n1]] mi, ni = rimBoundaryNodeToCapIndex[n3][n1] - nodeParameter = self._getRimCoordinatesWithCore(mi, ni, n3, isStartCap) + nodeParameter = self._getRimCoordinatesWithCore(mi, ni, n3) nodeParameters.append(nodeParameter) nodeLayouts.append(nodeLayoutCapShellTriplePoint if n1 in triplePointIndexesList else nodeLayoutCapTransition) for n1 in [e1, e1p]: nids += [rimExtNodeIds[n3][n1]] - nodeParameter = self._getRimExtCoordinates(n1, n3, isStartCap) + nodeParameter = self._getRimExtCoordinates(n1, n3) nodeParameters.append(nodeParameter) nodeLayouts.append(None) if not isStartCap: @@ -1710,18 +1661,16 @@ def _generateElementsWithCore(self, generateData, coreBoundaryScalingMode, isSta else: self._endCapElementIds.append(capElementIds) - def _generateExtendedTubeElements(self, generateData, tubeBoxNodeIds, tubeRimNodeIds, coreBoundaryScalingMode, - isStartCap=True): + def _generateExtendedTubeElements(self, tubeBoxNodeIds, tubeRimNodeIds): """ Blackbox function for generating extended tube elements. - :param generateData: TubeNetworkMeshGenerateData class object. :param tubeBoxNodeIds: List of tube box nodes. :param tubeRimNodeIds: List of tube rim nodes. - :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end - of a tube segment. """ + isStartCap = self._isStartCap idx = 0 if isStartCap else -1 - + scalingMode = 3 if isStartCap else 4 + generateData = self._generateData coordinates = generateData.getCoordinates() mesh = generateData.getMesh() elementtemplateStd, eftStd = generateData.getStandardElementtemplate() @@ -1766,18 +1715,15 @@ def _generateExtendedTubeElements(self, generateData, tubeBoxNodeIds, tubeRimNod if n2 == 0: nid = boxExtBoundaryNodeIds[n1] mi, ni = boxExtBoundaryNodesToBoxIds[n1] - nodeParameter = self._getBoxExtCoordinates(mi, ni, isStartCap) - nodeLayout = nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList \ - else nodeLayoutTransition + nodeParameter = self._getBoxExtCoordinates(mi, ni) else: nid = tubeBoxBoundaryNodeIds[n1] mi, ni = tubeBoxBoundaryNodesToBoxIds[n1] - nodeParameter = self._getTubeBoxCoordinates(mi, ni, isStartCap) - nodeLayout = nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList \ - else nodeLayoutTransition + nodeParameter = self._getTubeBoxCoordinates(mi, ni) nids += [nid] nodeParameters.append(nodeParameter) - nodeLayouts.append(nodeLayout) + nodeLayouts.append(nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList else + nodeLayoutTransition) if not isStartCap: for a in [nids, nodeParameters, nodeLayouts]: a[-4], a[-2] = a[-2], a[-4] @@ -1786,15 +1732,13 @@ def _generateExtendedTubeElements(self, generateData, tubeBoxNodeIds, tubeRimNod for n1 in [e1, n1p]: if n2 == 0: nid = rimExtNodeIds[0][n1] - nodeParameter = self._getRimExtCoordinates(n1, 0, isStartCap) - nodeLayout = None + nodeParameter = self._getRimExtCoordinates(n1, 0) else: nid = tubeRimNodeIds[idx][0][n1] nodeParameter = self._getTubeRimCoordinates(n1, idx, 0) - nodeLayout = None nids += [nid] nodeParameters.append(nodeParameter) - nodeLayouts.append(nodeLayout) + nodeLayouts.append(None) if not isStartCap: for a in [nids, nodeParameters, nodeLayouts]: a[-4], a[-2] = a[-2], a[-4] @@ -1802,7 +1746,7 @@ def _generateExtendedTubeElements(self, generateData, tubeBoxNodeIds, tubeRimNod eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) if self._elementsCountTransition == 1: eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( - eft, scalefactors, nodeParameters, nids, coreBoundaryScalingMode) + eft, scalefactors, nodeParameters, nids, scalingMode) elementtemplate = mesh.createElementtemplate() elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplate.defineField(coordinates, -1, eft) @@ -1841,7 +1785,7 @@ def _generateExtendedTubeElements(self, generateData, tubeBoxNodeIds, tubeRimNod for n2 in (0, 1): for n1 in (e1, n1p): if n2 == 0: - nodeParameter = self._getRimExtCoordinates(n1, n3, isStartCap) + nodeParameter = self._getRimExtCoordinates(n1, n3) else: nodeParameter = self._getTubeRimCoordinates(n1, idx, n3) nodeParameters.append(nodeParameter) @@ -1850,8 +1794,8 @@ def _generateExtendedTubeElements(self, generateData, tubeBoxNodeIds, tubeRimNod a[-4], a[-2] = a[-2], a[-4] a[-3], a[-1] = a[-1], a[-3] eft = generateData.createElementfieldtemplate() - # eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( - # eft, scalefactors, nodeParameters, nids, coreBoundaryScalingMode) + eft, scalefactors = generateData.resolveEftCoreBoundaryScaling( + eft, scalefactors, nodeParameters, nids, scalingMode) elementtemplateTransition = mesh.createElementtemplate() elementtemplateTransition.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplateTransition.defineField(coordinates, -1, eft) @@ -1871,17 +1815,16 @@ def sampleCoordinates(self): """ if self._isCore: self._createShellCoordinatesList() - if self._isCap[0]: - self._determineCapCoordinatesWithCore(isStartCap=True) - if self._isCap[1]: - self._determineCapCoordinatesWithCore(isStartCap=False) - else: - if self._isCap[0]: - self._extendTubeEnds(isStartCap=True) - self._determineCapCoordinatesWithoutCore(isStartCap=True) - if self._isCap[1]: - self._extendTubeEnds(isStartCap=False) - self._determineCapCoordinatesWithoutCore(isStartCap=False) + for s in range(2): + self._isStartCap = True if self._isCap[0] and s == 0 else False + if self._isCap[s]: + if self._isCore: + self._determineCapCoordinatesWithCore(s) + else: + self._extendTubeEnds() + self._determineCapCoordinatesWithoutCore() + else: + continue def generateNodes(self, generateData, isStartCap=True, isCore=False): """ @@ -1891,26 +1834,19 @@ def generateNodes(self, generateData, isStartCap=True, isCore=False): of a tube segment. :param isCore: True for generating a solid core inside the tube, False for regular tube network. """ - if isCore: - if isStartCap: - self._generateNodesWithCore(generateData, isStartCap) - self._generateExtendedTubeNodes(generateData, isStartCap) - else: - self._generateExtendedTubeNodes(generateData, isStartCap) - self._generateNodesWithCore(generateData, isStartCap) + self._isStartCap = isStartCap + self._generateData = generateData + if isStartCap: + self._generateNodesWithCore() if isCore else self._generateNodesWithoutCore() + self._generateExtendedTubeNodes() else: - if isStartCap: - self._generateNodesWithoutCore(generateData, isStartCap) - self._generateExtendedTubeNodes(generateData, isStartCap) - else: - self._generateExtendedTubeNodes(generateData, isStartCap) - self._generateNodesWithoutCore(generateData, isStartCap) + self._generateExtendedTubeNodes() + self._generateNodesWithCore() if isCore else self._generateNodesWithoutCore() - def generateElements(self, generateData, elementsCountRim, tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups, - coreBoundaryScalingMode, isStartCap=True, isCore=False): + def generateElements(self, elementsCountRim, tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups, + isStartCap=True, isCore=False): """ Blackbox function for generating cap and extended tube elements. - :param generateData: TubeNetworkMeshGenerateData class object. :param elementsCountRim: Number of elements through the rim. :param tubeBoxNodeIds: List of tube box nodes. :param tubeRimNodeIds: List of tube rim nodes. @@ -1921,12 +1857,10 @@ def generateElements(self, generateData, elementsCountRim, tubeBoxNodeIds, tubeR of a tube segment. :param isCore: True for generating a solid core inside the tube, False for regular tube network. """ + self._isStartCap = isStartCap if isCore: - self._generateElementsWithCore(generateData, coreBoundaryScalingMode, isStartCap) - self._generateExtendedTubeElements(generateData, tubeBoxNodeIds, tubeRimNodeIds, coreBoundaryScalingMode, - isStartCap) + self._generateElementsWithCore() + self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds) else: - self._generateElementsWithoutCore( - generateData, elementsCountRim, tubeRimNodeIds, annotationMeshGroups, isStartCap) - self._generateExtendedTubeElements(generateData, tubeBoxNodeIds, tubeRimNodeIds, coreBoundaryScalingMode, - isStartCap) + self._generateElementsWithoutCore(elementsCountRim, annotationMeshGroups) + self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds) diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index 7eae53e5..a09b5345 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -1121,7 +1121,8 @@ def addTricubicHermiteSerendipityEftParameterScaling(eft, scalefactors, nodePara :param localNodeIndexes: Local node indexes to scale value label at. Currently must be in [5, 6, 7, 8]. :param valueLabel: Single value label to scale. Currently only implemened for D_DS3. :param version: Set to a number > 1 to use that derivative version instead of scale factors, and client is - responsible for assigning that derivative version in proportion to the returned scale factors. + responsible for assigning that derivative version in proportion to the returned scale factors. Versions 3 and 4 + are used when the cap mesh is active, each representing the version used for start cap and end cap, respectively. :return: Modified eft, final scalefactors, add scalefactors (multiplying the affected derivatives; these are included in final scalefactors if version == 1, otherwise client must use these to scale versioned derivatives). """ @@ -1143,5 +1144,11 @@ def addTricubicHermiteSerendipityEftParameterScaling(eft, scalefactors, nodePara newScalefactors = scalefactors + addScalefactors if scalefactors else addScalefactors addScaleEftNodesValueLabel(eft, localNodeIndexes, Node.VALUE_LABEL_D_DS3, 3) return eft, newScalefactors, addScalefactors + elif version in [3, 4]: + addScalefactors = addScalefactors[-2:] if version == 3 else addScalefactors[:2] + localNodeIndexes = [7, 8] if version == 3 else [5, 6] + newScalefactors = scalefactors + addScalefactors if scalefactors else addScalefactors + addScaleEftNodesValueLabel(eft, localNodeIndexes, Node.VALUE_LABEL_D_DS3, 3) + return eft, newScalefactors, addScalefactors remapEftNodeValueLabelsVersion(eft, localNodeIndexes, [valueLabel], version) return eft, scalefactors, addScalefactors diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 6854685d..bead4bd5 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -190,7 +190,13 @@ def getNodeLayoutCapShellTriplePoint(self, location): def getNodeLayoutCapBoxShield(self, location, isStartCap=True): """ - + Special node layout for generating cap box-shield transition elements. + There are four layouts relative to the core box: Top (location = 1); bottom (location = -1); + left (location = 2); and right (location = -2). + :param location: Location identifier identifying four corners of solid core box. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: Node layout. """ nodeLayouts = self._nodeLayoutManager.getNodeLayoutCapBoxShield(isStartCap) assert location in [1, -1, 2, -2, 0] @@ -210,7 +216,13 @@ def getNodeLayoutCapBoxShield(self, location, isStartCap=True): def getNodeLayoutCapBoxShieldTriplePoint(self, location, isStartCap=True): """ - + Special node layout for generating cap box-shield transition elements at triple points. + There are four layouts relative to the core box: Top left (location = 1); top right (location = -1); + bottom left (location = 2); and bottom right (location = -2). + :param location: Location identifier identifying four corners of solid core box. + :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end + of a tube segment. + :return: Node layout. """ nodeLayouts = self._nodeLayoutManager.getNodeLayoutCapBoxShieldTriplePoint(isStartCap) assert location in [1, -1, 2, -2, 0] @@ -228,7 +240,6 @@ def getNodeLayoutCapBoxShieldTriplePoint(self, location, isStartCap=True): self._nodeLayoutCapBoxShieldTriplePoint = nodeLayout return self._nodeLayoutCapBoxShieldTriplePoint - def getNodetemplate(self): return self._nodetemplate @@ -282,10 +293,12 @@ def resolveEftCoreBoundaryScaling(self, eft, scalefactors, nodeParameters, nodeI x, d1, d2, d3 each with 3 components. :param nodeIdentifiers: List over 8 3-D local nodes giving global node identifiers. :param mode: 1 to set scale factors, 2 to add version 2 to d3 for the boundary nodes and assigning - values to that version equal to the scale factors x version 1. + values to that version equal to the scale factors x version 1. Modes 3 and 4 are used for cap mesh, which are + similar to mode 1, but the local node indexes are limited to [7, 8] for mode 3 (start cap) and [5, 6] for mode + 4 (end cap). :return: New eft, new scalefactors. """ - assert mode in (1, 2) + assert mode in (1, 2, 3, 4) eft, scalefactors, addScalefactors = addTricubicHermiteSerendipityEftParameterScaling( eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3, version=mode) if mode == 2: @@ -369,6 +382,8 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem self._capmesh = None def getCoreBoundaryScalingMode(self): + modes = (1, 2, 3, 4) if self._isCap else (1, 2) + assert self._coreBoundaryScalingMode in modes return self._coreBoundaryScalingMode def getElementsCountAround(self): @@ -1695,12 +1710,12 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create cap elements if self._isCap[0] and e2 == 0: isStartCap = True - capmesh.generateElements(generateData, elementsCountRim, self._boxNodeIds, self._rimNodeIds, - annotationMeshGroups, self._coreBoundaryScalingMode, isStartCap, self._isCore) + capmesh.generateElements(elementsCountRim, self._boxNodeIds, self._rimNodeIds, + annotationMeshGroups, isStartCap, self._isCore) elif self._isCap[-1] and e2 == (elementsCountAlong - endSkipCount - 1): isStartCap = False - capmesh.generateElements(generateData, elementsCountRim, self._boxNodeIds, self._rimNodeIds, - annotationMeshGroups, self._coreBoundaryScalingMode, isStartCap, self._isCore) + capmesh.generateElements(elementsCountRim, self._boxNodeIds, self._rimNodeIds, + annotationMeshGroups, isStartCap, self._isCore) if self._isCore: # create box elements @@ -1751,6 +1766,9 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplate.defineField(coordinates, -1, eft) elementIdentifier = generateData.nextElementIdentifier() + if elementIdentifier in [53, 54]: + print("self._coreBoundaryScalingMode", self._coreBoundaryScalingMode) + print("tube scalefactors", scalefactors) element = mesh.createElement(elementIdentifier, elementtemplate) element.setNodesByIdentifier(eft, nids) if scalefactors: diff --git a/tests/test_capmesh.py b/tests/test_capmesh.py index bb61805a..c2b71268 100644 --- a/tests/test_capmesh.py +++ b/tests/test_capmesh.py @@ -26,7 +26,7 @@ def test_3d_cap_tube_network_default(self): networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() # change the network layout to have cap at both ends of the tube networkLayoutSettings["Structure"] = "(1-2)" - self.assertEqual("(1-2)",networkLayoutSettings["Structure"]) + self.assertEqual("(1-2)", networkLayoutSettings["Structure"]) self.assertTrue(networkLayoutSettings["Define inner coordinates"]) self.assertEqual(13, len(settings)) @@ -156,10 +156,8 @@ def test_3d_cap_tube_network_default_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.03928467254863209, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 0.8736482362813334, delta=X_TOL) - - + self.assertAlmostEqual(volume, 0.03930782850767879, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 0.8285291049289928, delta=X_TOL) def test_3d_cap_tube_network_bifurcation(self): """ @@ -300,8 +298,8 @@ def test_3d_tube_network_bifurcation_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.11101973283867012, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.3035471266966363, delta=X_TOL) + self.assertAlmostEqual(volume, 0.11105681500696916, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.2359565854658427, delta=X_TOL) def test_3d_tube_network_converging_bifurcation_core(self): """ @@ -395,8 +393,8 @@ def test_3d_tube_network_converging_bifurcation_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.11060465010614413, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.3003361436808776, delta=X_TOL) + self.assertAlmostEqual(volume, 0.1106252097801163, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.233433190529325, delta=X_TOL) if __name__ == "__main__": unittest.main() From 8375deae5eae7f228194e068efb86217ec38d271 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 22 Nov 2024 10:53:30 +1300 Subject: [PATCH 04/43] Refactor code --- src/scaffoldmaker/utils/capmesh.py | 307 ++++++++------------- src/scaffoldmaker/utils/tubenetworkmesh.py | 3 - 2 files changed, 113 insertions(+), 197 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 82c8bde4..2418f74e 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -1,3 +1,7 @@ +""" +Specialisation of Tube Network Mesh for building 3-D cap mesh. +""" + import math from cmlibs.maths.vectorops import magnitude, sub, add, set_magnitude, normalize, rotate_vector_around_vector, cross, \ @@ -410,13 +414,9 @@ def _getOuterShellRadius(self): the outer shell of a tube segment. :return: Radius of the cap shell. """ - idx = 0 if self._isStartCap else -1 - ox = self._tubeShellCoordinates[0][idx][-1] - radii = [] - for i in range(self._elementsCountAround // 2): - j = i + self._elementsCountAround // 2 - r = magnitude(sub(ox[i], ox[j])) / 2 - radii.append(r) + ox = self._tubeShellCoordinates[0][0][-1] if self._isStartCap else self._tubeShellCoordinates[0][-1][-1] + radii = [magnitude(sub(ox[i], ox[i + self._elementsCountAround // 2])) / 2 for i in + range(self._elementsCountAround // 2)] return sum(radii) / len(radii) def _getRadius(self, ox): @@ -426,11 +426,8 @@ def _getRadius(self, ox): :param ox: Coordinates of shell nodes around a tube segment. :return: Radius of the cap shell. """ - radii = [] - for i in range(self._elementsCountAround // 2): - j = i + self._elementsCountAround // 2 - r = magnitude(sub(ox[i], ox[j])) / 2 - radii.append(r) + radii = [magnitude(sub(ox[i], ox[i + self._elementsCountAround // 2])) / 2 for i in + range(self._elementsCountAround // 2)] return sum(radii) / len(radii) def _getShellThickness(self): @@ -439,14 +436,9 @@ def _getShellThickness(self): It takes the average of a distance between the outer and the inner node pair around the rim of a tube segment. :return: Thickness of the cap shell. """ - idx = 0 if self._isStartCap else -1 - ix = self._tubeShellCoordinates[0][idx][0] - ox = self._tubeShellCoordinates[0][idx][-1] - - shellThicknesses = [] - for i in range(self._elementsCountAround): - thickness = magnitude(sub(ox[i], ix[i])) - shellThicknesses.append(thickness) + ix = self._tubeShellCoordinates[0][0][0] if self._isStartCap else self._tubeShellCoordinates[0][-1][0] + ox = self._tubeShellCoordinates[0][0][-1] if self._isStartCap else self._tubeShellCoordinates[0][-1][-1] + shellThicknesses = [magnitude(sub(ox[i], ix[i])) for i in range(self._elementsCountAround)] return sum(shellThicknesses) / len(shellThicknesses) def _getExtensionLength(self): @@ -551,35 +543,26 @@ def _calculateShellQuadruplePoints(self, n3, centre, radius): elementsCountUp = 2 elementsCountAcrossMajor = self._elementsCountCoreBoxMajor + 2 elementsCountAcrossMinor = self._elementsCountCoreBoxMinor + 2 - counter = 0 signValue = 1 if self._isStartCap else -1 - for m in [0, -1]: - for n in [0, -1]: - if m == n: - elementsCount = [elementsCountUp, elementsCountAcrossMajor // 2, elementsCountAcrossMinor // 2] - else: - elementsCount = [elementsCountAcrossMajor // 2, elementsCountUp, elementsCountAcrossMinor // 2] - - n1z, n3z = elementsCount[1], elementsCount[2] - n1y, n3y = n1z - 1, n3z - 1 - - elementsAroundEllipse12 = elementsCount[0] + elementsCount[1] - 2 - radiansAroundEllipse12 = math.pi / 2 - radiansPerElementAroundEllipse12 = radiansAroundEllipse12 / elementsAroundEllipse12 - elementsAroundEllipse13 = elementsCount[0] + elementsCount[2] - 2 - radiansAroundEllipse13 = math.pi / 2 - radiansPerElementAroundEllipse13 = radiansAroundEllipse13 / elementsAroundEllipse13 - - theta_2 = n3y * radiansPerElementAroundEllipse13 - theta_3 = n1y * radiansPerElementAroundEllipse12 - phi_3 = calculate_azimuth(theta_3, theta_2) - ratio = 1 - local_x = spherical_to_cartesian(radius, theta_3, ratio * phi_3 + (1 - ratio) * math.pi / 2) - c = counter if self._isStartCap else -(counter + 1) - axes = [mult(axis, signValue) for axis in axesList[c]] - x = local_to_global_coordinates(local_x, axes, centre) - self._shellCoordinates[idx][0][n3][m][n] = x - counter += 1 + for counter, (m, n) in enumerate([(0, 0), (0, -1), (-1, 0), (-1, -1)]): + elementsCount = ( + [elementsCountUp, elementsCountAcrossMajor // 2, elementsCountAcrossMinor // 2] if m == n else + [elementsCountAcrossMajor // 2, elementsCountUp, elementsCountAcrossMinor // 2] + ) + + radiansPerElementAroundEllipse12 = math.pi / (2 * (elementsCount[0] + elementsCount[1] - 2)) + radiansPerElementAroundEllipse13 = math.pi / (2 * (elementsCount[0] + elementsCount[2] - 2)) + + theta_2 = (elementsCount[2] - 1) * radiansPerElementAroundEllipse13 + theta_3 = (elementsCount[1] - 1) * radiansPerElementAroundEllipse12 + phi_3 = calculate_azimuth(theta_3, theta_2) + + local_x = spherical_to_cartesian(radius, theta_3, phi_3) + c = counter if self._isStartCap else -(counter + 1) + axes = [mult(axis, signValue) for axis in axesList[c]] + x = local_to_global_coordinates(local_x, axes, centre) + + self._shellCoordinates[idx][0][n3][m][n] = x def _calculateShellRegularNodeCoordinates(self, n3, centre): """ @@ -592,43 +575,38 @@ def _calculateShellRegularNodeCoordinates(self, n3, centre): elementsCountAcrossMinor = self._elementsCountCoreBoxMinor + 2 midMajorIndex = elementsCountAcrossMajor // 2 - 1 midMinorIndex = elementsCountAcrossMinor // 2 - 1 + + elementsOut = self._elementsCountCoreBoxMinor // 2 for m in range(self._elementsCountCoreBoxMajor + 1): - elementsOut = self._elementsCountCoreBoxMinor // 2 for n in [0, -1]: x1 = self._shellCoordinates[idx][0][n3][m][n] x2 = self._shellCoordinates[idx][0][n3][m][midMinorIndex] if x1 is None: continue - else: - if n == 0: - nx, nd1 = self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) - else: - nx, nd1 = self._sampleCurvesOnSphere(x2, x1, centre, elementsOut) - nRange = [n + 1, midMinorIndex] if n == 0 else \ - [midMinorIndex + 1, self._elementsCountCoreBoxMinor] - for c in range(nRange[0], nRange[1]): - self._shellCoordinates[idx][0][n3][m][c] = nx[c % (len(nx) - 1)] - self._shellCoordinates[idx][1][n3][m][c] = nd1[c % (len(nd1) - 1)] - self._shellCoordinates[idx][2][n3][m][c] = [0, 0, 0] - + nx, nd1 = (self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) if n == 0 else + self._sampleCurvesOnSphere(x2, x1, centre, elementsOut)) + start, end = ((n + 1, midMinorIndex) if n == 0 else (midMinorIndex + 1, self._elementsCountCoreBoxMinor)) + for c in range(start, end): + idx_c = c % (len(nx) - 1) + self._shellCoordinates[idx][0][n3][m][c] = nx[idx_c] + self._shellCoordinates[idx][1][n3][m][c] = nd1[idx_c] + self._shellCoordinates[idx][2][n3][m][c] = [0, 0, 0] + + elementsOut = self._elementsCountCoreBoxMajor // 2 for n in range(self._elementsCountCoreBoxMinor + 1): - elementsOut = self._elementsCountCoreBoxMajor // 2 for m in [0, -1]: x1 = self._shellCoordinates[idx][0][n3][m][n] x2 = self._shellCoordinates[idx][0][n3][midMajorIndex][n] if x1 is None: continue - else: - if m == 0: - nx, nd2 = self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) - else: - nx, nd2 = self._sampleCurvesOnSphere(x2, x1, centre, elementsOut) - mRange = [m + 1, midMajorIndex] if m == 0 else [midMajorIndex + 1, - self._elementsCountCoreBoxMajor] - for c in range(mRange[0], mRange[1]): - self._shellCoordinates[idx][0][n3][c][n] = nx[c % (len(nx) - 1)] - self._shellCoordinates[idx][1][n3][c][n] = [0, 0, 0] - self._shellCoordinates[idx][2][n3][c][n] = nd2[c % (len(nd2) - 1)] + nx, nd2 = (self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) if m == 0 else + self._sampleCurvesOnSphere(x2, x1, centre, elementsOut)) + start, end = ((m + 1, midMajorIndex) if m == 0 else (midMajorIndex + 1, self._elementsCountCoreBoxMajor)) + for c in range(start, end): + idx_c = c % (len(nx) - 1) + self._shellCoordinates[idx][0][n3][c][n] = nx[idx_c] + self._shellCoordinates[idx][1][n3][c][n] = [0, 0, 0] + self._shellCoordinates[idx][2][n3][c][n] = nd2[idx_c] def _sphereToSpheroid(self, n3, ratio, centre): """ @@ -648,16 +626,21 @@ def _sphereToSpheroid(self, n3, ratio, centre): mCount = self._elementsCountCoreBoxMajor + 1 if self._isCore else 1 nCount = self._elementsCountCoreBoxMinor + 1 if self._isCore else self._elementsCountAround + # process shell coordinates for m in range(mCount): mp = m if self._isCore else 1 for n in range(nCount): btx = self._shellCoordinates[idx][0][n3][mp][n] btx = sub(btx, centre) - btx = rotate_vector_around_vector(btx, layoutD3, thetaD2) - btx = rotate_vector_around_vector(btx, layoutD2, thetaD3) + # apply forward transformations + for vec, theta in [(layoutD3, thetaD2), (layoutD2, thetaD3)]: + btx = rotate_vector_around_vector(btx, vec, theta) + # scale by ratios btx = [ratio[c] * btx[c] for c in range(3)] - btx = rotate_vector_around_vector(btx, layoutD3, -thetaD2) - btx = rotate_vector_around_vector(btx, layoutD2, -thetaD3) + # apply inverse transformations + for vec, theta in [(layoutD2, -thetaD3), (layoutD3, -thetaD2)]: + btx = rotate_vector_around_vector(btx, vec, theta) + # update shell coordinates self._shellCoordinates[idx][0][n3][mp][n] = add(btx, centre) def _getRatioBetweenTwoRadii(self, radii, oRadii): @@ -679,10 +662,8 @@ def _getTubeRadii(self, centre, n3, idx): radius in this direction is constant. """ n1m, n1n = 0, self._elementsCountAround // 4 - ixm = self._getTubeRimCoordinates(n1m, idx, n3)[0] - ixn = self._getTubeRimCoordinates(n1n, idx, n3)[0] - majorRadius, minorRadius = magnitude(sub(ixm, centre)), magnitude(sub(ixn, centre)) - + ixm, ixn = (self._getTubeRimCoordinates(n, idx, n3)[0] for n in [n1m, n1n]) + majorRadius, minorRadius = (magnitude(sub(coord, centre)) for coord in [ixm, ixn]) return [1.0, majorRadius, minorRadius] def _determineShellDerivatives(self): @@ -753,26 +734,18 @@ def _calculateBoxQuadruplePoints(self, centre): triplePointIndexesList.append(n + nodesCountAcrossMinorHalf) triplePointIndexesList[-2], triplePointIndexesList[-1] = triplePointIndexesList[-1], triplePointIndexesList[-2] - counter = 0 - for m in [0, -1]: - for n in [0, -1]: - tpIndex = triplePointIndexesList[counter] - x1 = boxCoordinates[m][n] - x2 = rimCoordinates[tpIndex] - x3 = capCoordinates[m][n] - - ts = magnitude(sub(x1, x2)) - ra = sub(x3, centre) - radius = magnitude(ra) - local_x = mult(ra, (1 - ts / radius)) - x = add(local_x, centre) - capBoxCoordinates[0][m][n] = x - capBoxCoordinates[1][m][n] = self._tubeBoxCoordinates[1][idx][m][n] - capBoxCoordinates[3][m][n] = self._tubeBoxCoordinates[3][idx][m][n] - counter += 1 + for counter, (m, n) in enumerate([(0, 0), (0, -1), (-1, 0), (-1, -1)]): + tpIndex = triplePointIndexesList[counter] + x1, x2, x3 = boxCoordinates[m][n], rimCoordinates[tpIndex], capCoordinates[m][n] + ts = magnitude(sub(x1, x2)) + ra = sub(x3, centre) + radius = magnitude(ra) + local_x = mult(ra, (1 - ts / radius)) + capBoxCoordinates[0][m][n] = add(local_x, centre) + capBoxCoordinates[1][m][n] = self._tubeBoxCoordinates[1][idx][m][n] + capBoxCoordinates[3][m][n] = self._tubeBoxCoordinates[3][idx][m][n] if self._boxCoordinates is None: - self._boxCoordinates = [] self._boxCoordinates = [None] * 2 self._boxCoordinates[idx] = capBoxCoordinates @@ -786,24 +759,19 @@ def _calculateBoxMajorAndMinorNodes(self): # box side nodes for m in [0, -1]: - nx, nd1, nd3 = [], [], [] - for n in [0, -1]: - nx += [self._boxCoordinates[idx][0][m][n]] - nd1 += [self._boxCoordinates[idx][1][m][n]] - nd3 += [self._boxCoordinates[idx][3][m][n]] + nx = [self._boxCoordinates[idx][0][m][n] for n in [0, -1]] + nd1 = [self._boxCoordinates[idx][1][m][n] for n in [0, -1]] + nd3 = [self._boxCoordinates[idx][3][m][n] for n in [0, -1]] tx, td3, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd3, self._elementsCountCoreBoxMinor, arcLengthDerivatives=True) td1 = interpolateSampleCubicHermite(nd1, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] for n in range(1, self._elementsCountCoreBoxMinor): self._boxCoordinates[idx][0][m][n] = tx[n] self._boxCoordinates[idx][1][m][n] = td1[n] self._boxCoordinates[idx][3][m][n] = td3[n] - for n in [0, -1]: - nx, nd1, nd3 = [], [], [] - for m in [0, -1]: - nx += [self._boxCoordinates[idx][0][m][n]] - nd1 += [self._boxCoordinates[idx][1][m][n]] - nd3 += [self._boxCoordinates[idx][3][m][n]] + nx = [self._boxCoordinates[idx][0][m][n] for m in [0, -1]] + nd1 = [self._boxCoordinates[idx][1][m][n] for m in [0, -1]] + nd3 = [self._boxCoordinates[idx][3][m][n] for m in [0, -1]] tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, arcLengthDerivatives=True) td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] for m in range(1, self._elementsCountCoreBoxMajor): @@ -812,11 +780,9 @@ def _calculateBoxMajorAndMinorNodes(self): self._boxCoordinates[idx][3][m][n] = td3[m] # box major and minor nodes - nx, nd1, nd3 = [], [], [] - for n in [0, -1]: - nx += [self._boxCoordinates[idx][0][midMajorIndex][n]] - nd1 += [self._boxCoordinates[idx][1][midMajorIndex][n]] - nd3 += [self._boxCoordinates[idx][3][midMajorIndex][n]] + nx = [self._boxCoordinates[idx][0][midMajorIndex][n] for n in [0, -1]] + nd1 = [self._boxCoordinates[idx][1][midMajorIndex][n] for n in [0, -1]] + nd3 = [self._boxCoordinates[idx][3][midMajorIndex][n] for n in [0, -1]] tx, td3, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd3, self._elementsCountCoreBoxMinor, arcLengthDerivatives=True) td1 = interpolateSampleCubicHermite(nd1, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] for n in range(1, self._elementsCountCoreBoxMinor): @@ -824,10 +790,8 @@ def _calculateBoxMajorAndMinorNodes(self): self._boxCoordinates[idx][1][midMajorIndex][n] = td1[n] self._boxCoordinates[idx][3][midMajorIndex][n] = td3[n] - nx, nd1 = [], [] - for m in [0, -1]: - nx += [self._boxCoordinates[idx][0][m][midMinorIndex]] - nd1 += [self._boxCoordinates[idx][1][m][midMinorIndex]] + nx = [self._boxCoordinates[idx][0][m][midMinorIndex] for m in [0, -1]] + nd1 = [self._boxCoordinates[idx][1][m][midMinorIndex] for m in [0, -1]] tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, arcLengthDerivatives=True) td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] for m in range(1, self._elementsCountCoreBoxMajor): @@ -839,17 +803,10 @@ def _calculateBoxMajorAndMinorNodes(self): for m in range(self._elementsCountCoreBoxMajor): for n in range(self._elementsCountCoreBoxMinor): if self._boxCoordinates[idx][0][m][n] is None: - nx = [self._boxCoordinates[idx][0][0][n], - self._boxCoordinates[idx][0][midMajorIndex][n], - self._boxCoordinates[idx][0][-1][n]] - nd1 = [self._boxCoordinates[idx][1][0][n], - self._boxCoordinates[idx][1][midMajorIndex][n], - self._boxCoordinates[idx][1][-1][n]] - nd3 = [self._boxCoordinates[idx][3][0][n], - self._boxCoordinates[idx][3][midMajorIndex][n], - self._boxCoordinates[idx][3][-1][n]] - tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, - arcLengthDerivatives=True) + nx = [self._boxCoordinates[idx][0][i][n] for i in [0, midMajorIndex, -1]] + nd1 = [self._boxCoordinates[idx][1][i][n] for i in [0, midMajorIndex, -1]] + nd3 = [self._boxCoordinates[idx][3][i][n] for i in [0, midMajorIndex, -1]] + tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, arcLengthDerivatives=True) td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 3, pe, pxi, psf)[0] for mi in range(1, self._elementsCountCoreBoxMajor): self._boxCoordinates[idx][0][mi][n] = tx[mi] @@ -882,10 +839,8 @@ def _determineBoxDerivatives(self): itx = self._boxCoordinates[idx][0][m][n] d2 = mult(sub(otx, itx), signValue) self._boxCoordinates[idx][2][m][n] = d2 - d1 = self._boxCoordinates[idx][1][m][n] - d3 = self._boxCoordinates[idx][3][m][n] - self._boxCoordinates[idx][1][m][n] = set_magnitude(d1, magnitude(d2)) - self._boxCoordinates[idx][3][m][n] = set_magnitude(d3, magnitude(d2)) + self._boxCoordinates[idx][1][m][n] = set_magnitude(self._boxCoordinates[idx][1][m][n], magnitude(d2)) + self._boxCoordinates[idx][3][m][n] = set_magnitude(self._boxCoordinates[idx][3][m][n], magnitude(d2)) def _createBoundaryNodeIdsList(self, nodeIds): """ @@ -1103,27 +1058,19 @@ def _getBoxBoundaryLocation(self, m, n): m = m + self._getNodesCountCoreBoxMajor() if m < 0 else m n = n + self._getNodesCountCoreBoxMinor() if n < 0 else n - if n == nEnd and 0 < m < mEnd: - location = 1 # "Top" - elif n == 0 and 0 < m < mEnd: - location = -1 # "Bottom" - elif m == 0 and 0 < n < nEnd: - location = 2 # "Left" - elif m == mEnd and 0 < n < nEnd: - location = -2 # "Right" + if 0 < m < mEnd: + location = 1 if n == nEnd else -1 if n == 0 else 0 + elif 0 < n < nEnd: + location = 2 if m == 0 else -2 if m == mEnd else 0 else: location = 0 tpLocation = 0 if location == 0: - if m == 0 and n == nEnd: - tpLocation = 1 # Top Left - elif m == mEnd and n == nEnd: - tpLocation = -1 # Top Right - elif m == 0 and n == 0: - tpLocation = 2 # Bottom Left - elif m == mEnd and n == 0: - tpLocation = -2 # Bottom Right + if n == nEnd: + tpLocation = 1 if m == 0 else -1 # Top Left or Top Right + elif n == 0: + tpLocation = 2 if m == 0 else -2 # Bottom Left or Bottom Right return location, tpLocation @@ -1164,10 +1111,8 @@ def _sampleCurvesOnSphere(self, x1, x2, origin, elementsOut): for n1 in range(elementsOut + 1): radiansAcross = n1 * anglePerElement r = rotate_vector_around_vector(r1, normal, radiansAcross) - x = add(r, origin) - d1 = set_magnitude(cross(normal, r), arcLengthPerElement) - nx.append(x) - nd1.append(d1) + nx.append(add(r, origin)) + nd1.append(set_magnitude(cross(normal, r), arcLengthPerElement)) return nx, nd1 @@ -1189,11 +1134,7 @@ def _generateNodesWithoutCore(self): for n3 in range(nodesCountShell): capNodeIds[n2].append([]) if n2 == 0: # apex - rx = self._shellCoordinates[idx][0][n3][n2] - rd1 = self._shellCoordinates[idx][1][n3][n2] - rd2 = self._shellCoordinates[idx][2][n3][n2] - rd3 = self._shellCoordinates[idx][3][n3][n2] - + rx, rd1, rd2, rd3 = (self._shellCoordinates[idx][i][n3][n2] for i in range(4)) nodeIdentifier = generateData.nextNodeIdentifier() node = nodes.createNode(nodeIdentifier, nodetemplate) fieldcache.setNode(node) @@ -1204,11 +1145,7 @@ def _generateNodesWithoutCore(self): capNodeIds[n2][n3].append(nodeIdentifier) else: for n1 in range(self._elementsCountAround): - rx = self._shellCoordinates[idx][0][n3][n2][n1] - rd1 = self._shellCoordinates[idx][1][n3][n2][n1] - rd2 = self._shellCoordinates[idx][2][n3][n2][n1] - rd3 = self._shellCoordinates[idx][3][n3][n2][n1] - + rx, rd1, rd2, rd3 = (self._shellCoordinates[idx][i][n3][n2][n1] for i in range(4)) nodeIdentifier = generateData.nextNodeIdentifier() node = nodes.createNode(nodeIdentifier, nodetemplate) fieldcache.setNode(node) @@ -1244,10 +1181,7 @@ def _generateNodesWithCore(self): for m in range(nodesCountCoreBoxMajor): capNodeIds[n3].append([]) for n in range(nodesCountCoreBoxMinor): - rx = self._boxCoordinates[idx][0][m][n] - rd1 = self._boxCoordinates[idx][1][m][n] - rd2 = self._boxCoordinates[idx][2][m][n] - rd3 = self._boxCoordinates[idx][3][m][n] + rx, rd1, rd2, rd3 = (self._boxCoordinates[idx][i][m][n] for i in range(4)) nodeIdentifier = generateData.nextNodeIdentifier() node = nodes.createNode(nodeIdentifier, nodetemplate) fieldcache.setNode(node) @@ -1261,11 +1195,7 @@ def _generateNodesWithCore(self): n3p = n3 - 1 capNodeIds[n3].append([]) for n in range(nodesCountCoreBoxMinor): - rx = self._shellCoordinates[idx][0][n3p][m][n] - rd1 = self._shellCoordinates[idx][1][n3p][m][n] - rd2 = self._shellCoordinates[idx][2][n3p][m][n] - rd3 = self._shellCoordinates[idx][3][n3p][m][n] - + rx, rd1, rd2, rd3 = (self._shellCoordinates[idx][i][n3p][m][n] for i in range(4)) nodeIdentifier = generateData.nextNodeIdentifier() node = nodes.createNode(nodeIdentifier, nodetemplate) fieldcache.setNode(node) @@ -1299,10 +1229,7 @@ def _generateExtendedTubeNodes(self): nodesCountAcrossMinor = self._getNodesCountCoreBoxMinor() for n3 in range(nodesCountCoreBoxMajor): self._boxExtNodeIds[idx].append([]) - rx = self._boxExtCoordinates[idx][0][n3] - rd1 = self._boxExtCoordinates[idx][1][n3] - rd2 = self._boxExtCoordinates[idx][2][n3] - rd3 = self._boxExtCoordinates[idx][3][n3] + rx, rd1, rd2, rd3 = [self._boxExtCoordinates[idx][i][n3] for i in range(4)] for n1 in range(nodesCountAcrossMinor): nodeIdentifier = generateData.nextNodeIdentifier() node = nodes.createNode(nodeIdentifier, nodetemplate) @@ -1319,28 +1246,20 @@ def _generateExtendedTubeNodes(self): self._rimExtNodeIds = [None, None] if self._rimExtNodeIds is None else self._rimExtNodeIds self._rimExtNodeIds[idx] = [] for n3 in range(nodesCountRim): - n3p = n3 - (elementsCountTransition - 1) if self._isCore else n3 - if self._isCore and elementsCountTransition > 1 and n3 < (elementsCountTransition - 1): - # transition coordinates - rx = self._transitionExtCoordinates[idx][0][n3] - rd1 = self._transitionExtCoordinates[idx][1][n3] - rd2 = self._transitionExtCoordinates[idx][2][n3] - rd3 = self._transitionExtCoordinates[idx][3][n3] - else: - # rim coordinates - rx = self._shellExtCoordinates[idx][0][n3p] - rd1 = self._shellExtCoordinates[idx][1][n3p] - rd2 = self._shellExtCoordinates[idx][2][n3p] - rd3 = self._shellExtCoordinates[idx][3][n3p] + n3p = n3 - (elementsCountTransition - 1) + tx = self._transitionExtCoordinates[idx] if self._isCore and elementsCountTransition > 1 and n3 < ( + elementsCountTransition - 1) else self._shellExtCoordinates[idx] + rx, rd1, rd2, rd3 = [tx[i][n3 if self._isCore and elementsCountTransition > 1 and n3 < ( + elementsCountTransition - 1) else n3p] for i in range(4)] ringNodeIds = [] for n1 in range(self._elementsCountAround): nodeIdentifier = generateData.nextNodeIdentifier() node = nodes.createNode(nodeIdentifier, nodetemplate) fieldcache.setNode(node) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, rx[n1]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, rd1[n1]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, rd2[n1]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, rd3[n1]) + for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, + Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], + [rx[n1], rd1[n1], rd2[n1], rd3[n1]]): + coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) ringNodeIds.append(nodeIdentifier) self._rimExtNodeIds[idx].append(ringNodeIds) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index bead4bd5..53822c6e 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -1766,9 +1766,6 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplate.defineField(coordinates, -1, eft) elementIdentifier = generateData.nextElementIdentifier() - if elementIdentifier in [53, 54]: - print("self._coreBoundaryScalingMode", self._coreBoundaryScalingMode) - print("tube scalefactors", scalefactors) element = mesh.createElement(elementIdentifier, elementtemplate) element.setNodesByIdentifier(eft, nids) if scalefactors: From 6d64fe23becccb16ae0c2dc45d1477b289a22397 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 22 Nov 2024 14:12:00 +1300 Subject: [PATCH 05/43] Remove unused scaffold imports in scaffolds file --- src/scaffoldmaker/scaffolds.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 998946e4..6c35deb2 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -41,8 +41,6 @@ from scaffoldmaker.meshtypes.meshtype_3d_musclefusiform1 import MeshType_3d_musclefusiform1 from scaffoldmaker.meshtypes.meshtype_3d_ostium1 import MeshType_3d_ostium1 from scaffoldmaker.meshtypes.meshtype_3d_ostium2 import MeshType_3d_ostium2 -from scaffoldmaker.meshtypes.meshtype_3d_renal_capsule1 import MeshType_3d_renal_capsule1 -from scaffoldmaker.meshtypes.meshtype_3d_renal_pelvis1 import MeshType_3d_renal_pelvis1 from scaffoldmaker.meshtypes.meshtype_3d_smallintestine1 import MeshType_3d_smallintestine1 from scaffoldmaker.meshtypes.meshtype_3d_solidcylinder1 import MeshType_3d_solidcylinder1 from scaffoldmaker.meshtypes.meshtype_3d_solidsphere1 import MeshType_3d_solidsphere1 @@ -104,8 +102,6 @@ def __init__(self): MeshType_3d_musclefusiform1, MeshType_3d_ostium1, MeshType_3d_ostium2, - MeshType_3d_renal_capsule1, - MeshType_3d_renal_pelvis1, MeshType_3d_smallintestine1, MeshType_3d_solidcylinder1, MeshType_3d_solidsphere1, From 6691f8ff316af2b185fd2164ef2cfe6f550ea84a Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 20 Dec 2024 09:54:37 +1300 Subject: [PATCH 06/43] Add renal capsule scaffold --- src/scaffoldmaker/annotation/kidney_terms.py | 25 ++ .../meshtypes/meshtype_3d_renal_capsule1.py | 358 ++++++++++++++++++ src/scaffoldmaker/scaffolds.py | 5 +- tests/test_renalcapsule.py | 120 ++++++ 4 files changed, 507 insertions(+), 1 deletion(-) create mode 100644 src/scaffoldmaker/annotation/kidney_terms.py create mode 100644 src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py create mode 100644 tests/test_renalcapsule.py diff --git a/src/scaffoldmaker/annotation/kidney_terms.py b/src/scaffoldmaker/annotation/kidney_terms.py new file mode 100644 index 00000000..ca228b88 --- /dev/null +++ b/src/scaffoldmaker/annotation/kidney_terms.py @@ -0,0 +1,25 @@ +""" +Common resource for kidney annotation terms. +""" + +# convention: preferred name, preferred id, followed by any other ids and alternative names +kidney_terms = [ + ("core", ""), + ("renal capsule", "", ""), + ("renal pelvis", "UBERON:0001224", "ILX:0723968"), + ("major calyx", "UBERON:0001226", "ILX:0730785"), + ("minor calyx", "UBERON:0001227", "ILX:0730473"), + ("renal pyramid", "UBERON:0004200", "ILX:0727514"), + ("shell", "") + ] + +def get_kidney_term(name : str): + """ + Find term by matching name to any identifier held for a term. + Raise exception if name not found. + :return ( preferred name, preferred id ) + """ + for term in kidney_terms: + if name in term: + return (term[0], term[1]) + raise NameError("Kidney annotation term '" + name + "' not found.") diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py new file mode 100644 index 00000000..685542b6 --- /dev/null +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py @@ -0,0 +1,358 @@ +""" +Generates a 3D renal capsule using tube network mesh. +""" +import math + +from cmlibs.maths.vectorops import mult, set_magnitude, cross +from cmlibs.utils.zinc.field import find_or_create_field_coordinates +from cmlibs.zinc.field import Field + +from scaffoldmaker.annotation.annotationgroup import AnnotationGroup +from scaffoldmaker.annotation.kidney_terms import get_kidney_term +from scaffoldmaker.meshtypes.meshtype_1d_network_layout1 import MeshType_1d_network_layout1 +from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base +from scaffoldmaker.scaffoldpackage import ScaffoldPackage +from scaffoldmaker.utils.interpolation import smoothCubicHermiteDerivativesLine, sampleCubicHermiteCurves, \ + smoothCurveSideCrossDerivatives +from scaffoldmaker.utils.networkmesh import NetworkMesh +from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData +from cmlibs.zinc.node import Node + + +class MeshType_1d_renal_capsule_network_layout1(MeshType_1d_network_layout1): + """ + Defines renal capsule network layout. + """ + + @classmethod + def getName(cls): + return "1D Renal Capsule Network Layout 1" + + @classmethod + def getParameterSetNames(cls): + return ["Default"] + + @classmethod + def getDefaultOptions(clscls, parameterSetName="Default"): + options = {} + options["Base parameter set"] = "Human 1" if (parameterSetName == "Default") else parameterSetName + options["Structure"] = "(1-2-3-4-5)" + options["Define inner coordinates"] = True + options["Renal capsule length"] = 1.0 + options["Renal capsule diameter"] = 1.5 + options["Renal capsule bend angle degrees"] = 10 + options["Inner proportion default"] = 0.8 + return options + + @classmethod + def getOrderedOptionNames(cls): + return [ + "Renal capsule length", + "Renal capsule diameter", + "Renal capsule bend angle degrees", + "Inner proportion default" + ] + + @classmethod + def checkOptions(cls, options): + dependentChanges = False + for key in [ + "Renal capsule length", + "Renal capsule diameter", + "Renal capsule bend angle degrees", + "Inner proportion default" + ]: + pass + + return dependentChanges + + @classmethod + def generateBaseMesh(cls, region, options): + """ + Generate the unrefined mesh. + :param region: Zinc region to define model in. Must be empty. + :param options: Dict containing options. See getDefaultOptions(). + :return [] empty list of AnnotationGroup, NetworkMesh + """ + # parameters + structure = options["Structure"] + capsuleLength = options["Renal capsule length"] + capsuleRadius = 0.5 * options["Renal capsule diameter"] + capsuleBendAngle = options["Renal capsule bend angle degrees"] + innerProportionDefault = options["Inner proportion default"] + + networkMesh = NetworkMesh(structure) + networkMesh.create1DLayoutMesh(region) + + fieldmodule = region.getFieldmodule() + mesh = fieldmodule.findMeshByDimension(1) + + # set up element annotations + renalCapsuleGroup = AnnotationGroup(region, get_kidney_term("renal capsule")) + annotationGroups = [renalCapsuleGroup] + + renalCapsuleGroup = renalCapsuleGroup.getMeshGroup(mesh) + elementIdentifier = 1 + capsuleElementsCount = 4 + meshGroups = [renalCapsuleGroup] + for e in range(capsuleElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + + # set coordinates (outer) + fieldcache = fieldmodule.createFieldcache() + coordinates = find_or_create_field_coordinates(fieldmodule) + # need to ensure inner coordinates are at least defined: + cls.defineInnerCoordinates(region, coordinates, options, networkMesh, innerProportion=innerProportionDefault) + innerCoordinates = find_or_create_field_coordinates(fieldmodule, "inner coordinates") + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + + # renal capsule + nodeIdentifier = 1 + halfCapsuleLength = 0.5 * capsuleLength + capsuleScale = capsuleLength / capsuleElementsCount + bendAngleRadians = math.radians(capsuleBendAngle) + sinBendAngle = math.sin(bendAngleRadians) + cosBendAngle = math.cos(bendAngleRadians) + sinCurveAngle = math.sin(3 * bendAngleRadians) + mx = [0.0, 0.0, 0.0] + d1 = [capsuleScale, 0.0, 0.0] + d3 = [0.0, 0.0, capsuleRadius] + id3 = mult(d3, innerProportionDefault) + + tx = halfCapsuleLength * -cosBendAngle + ty = halfCapsuleLength * -sinBendAngle + sx = [tx, ty, 0.0] + ex = [-tx, ty, 0.0] + sd1 = mult([1.0, sinCurveAngle, 0.0], capsuleScale) + ed1 = [sd1[0], -sd1[1], sd1[2]] + nx, nd1 = sampleCubicHermiteCurves([sx, mx, ex], [sd1, d1, ed1], capsuleElementsCount)[0:2] + nd1 = smoothCubicHermiteDerivativesLine(nx, nd1) + + sd2_list = [] + sd3_list = [] + sNodeIdentifiers = [] + for e in range(capsuleElementsCount + 1): + sNodeIdentifiers.append(nodeIdentifier) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + sd2 = set_magnitude(cross(d3, nd1[e]), capsuleRadius) + sid2 = mult(sd2, innerProportionDefault) + sd2_list.append(sd2) + sd3_list.append(d3) + for field, derivatives in ((coordinates, (nd1[e], sd2, d3)), (innerCoordinates, (nd1[e], sid2, id3))): + setNodeFieldParameters(field, fieldcache, nx[e], *derivatives) + nodeIdentifier += 1 + + sd12 = smoothCurveSideCrossDerivatives(nx, nd1, [sd2_list])[0] + sd13 = smoothCurveSideCrossDerivatives(nx, nd1, [sd3_list])[0] + for e in range(capsuleElementsCount + 1): + node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) + fieldcache.setNode(node) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12[e]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sd13[e]) + sid12 = mult(sd12[e], innerProportionDefault) + sid13 = mult(sd13[e], innerProportionDefault) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sid12) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sid13) + + return annotationGroups, networkMesh + + +class MeshType_3d_renal_capsule1(Scaffold_base): + """ + Generates a 3-D renal capsule. + """ + + @classmethod + def getName(cls): + return "3D Renal Capsule 1" + + @classmethod + def getParameterSetNames(cls): + return [ + "Default", + "Human 1" + ] + + @classmethod + def getDefaultOptions(cls, parameterSetName='Default'): + options = {} + useParameterSetName = "Human 1" if (parameterSetName == "Default") else parameterSetName + options["Base parameter set"] = useParameterSetName + options["Renal capsule network layout"] = ScaffoldPackage(MeshType_1d_renal_capsule_network_layout1) + options["Elements count around"] = 12 + options["Elements count through shell"] = 1 + options["Annotation elements counts around"] = [0] + options["Target element density along longest segment"] = 4.0 + options["Number of elements across core box minor"] = 2 + options["Number of elements across core transition"] = 1 + options["Annotation numbers of elements across core box minor"] = [0] + return options + + @classmethod + def getOrderedOptionNames(cls): + optionNames = [ + "Renal capsule network layout", + "Elements count around", + "Elements count through shell", + "Annotation elements counts around", + "Target element density along longest segment", + "Number of elements across core box minor", + "Number of elements across core transition", + "Annotation numbers of elements across core box minor"] + return optionNames + + @classmethod + def getOptionValidScaffoldTypes(cls, optionName): + if optionName == "Renal capsule network layout": + return [MeshType_1d_renal_capsule_network_layout1] + return [] + + + @classmethod + def getOptionScaffoldPackage(cls, optionName, scaffoldType, parameterSetName=None): + """ + :param parameterSetName: Name of valid parameter set for option Scaffold, or None for default. + :return: ScaffoldPackage. + """ + if parameterSetName: + assert parameterSetName in cls.getOptionScaffoldTypeParameterSetNames(optionName, scaffoldType), \ + "Invalid parameter set " + str(parameterSetName) + " for scaffold " + str(scaffoldType.getName()) + \ + " in option " + str(optionName) + " of scaffold " + cls.getName() + if optionName == "Renal capsule network layout": + if not parameterSetName: + parameterSetName = "Default" + return ScaffoldPackage(MeshType_1d_renal_capsule_network_layout1, defaultParameterSetName=parameterSetName) + assert False, cls.__name__ + ".getOptionScaffoldPackage: Option " + optionName + " is not a scaffold" + + @classmethod + def checkOptions(cls, options): + dependentChanges = False + if (options["Renal capsule network layout"].getScaffoldType() not in + cls.getOptionValidScaffoldTypes("Renal capsule network layout")): + options["Renal capsule network layout"] = ScaffoldPackage(MeshType_1d_renal_capsule_network_layout1) + + minElementsCountAround = None + for key in [ + "Elements count around" + ]: + if options[key] < 8: + options[key] = 8 + elif options[key] % 4: + options[key] += 4 - (options[key] % 4) + if (minElementsCountAround is None) or (options[key] < minElementsCountAround): + minElementsCountAround = options[key] + + if options["Number of elements through shell"] < 0: + options["Number of elements through shell"] = 1 + + if options["Number of elements across core transition"] < 1: + options["Number of elements across core transition"] = 1 + + maxElementsCountCoreBoxMinor = minElementsCountAround // 2 - 2 + for key in [ + "Number of elements across core box minor" + ]: + if options[key] < 2: + options[key] = 2 + elif options[key] > maxElementsCountCoreBoxMinor: + options[key] = maxElementsCountCoreBoxMinor + dependentChanges = True + elif options[key] % 2: + options[key] += options[key] % 2 + + annotationElementsCountsAround = options["Annotation elements counts around"] + if len(annotationElementsCountsAround) == 0: + options["Annotation elements count around"] = [0] + else: + for i in range(len(annotationElementsCountsAround)): + if annotationElementsCountsAround[i] <= 0: + annotationElementsCountsAround[i] = 0 + elif annotationElementsCountsAround[i] < 4: + annotationElementsCountsAround[i] = 4 + + if options["Target element density along longest segment"] < 1.0: + options["Target element density along longest segment"] = 1.0 + return dependentChanges + + @classmethod + def generateBaseMesh(cls, region, options): + """ + Generate the base hermite-bilinear mesh. See also generateMesh(). + :param region: Zinc region to define model in. Must be empty. + :param options: Dict containing options. See getDefaultOptions(). + :return: list of AnnotationGroup, None + """ + networkLayout = options["Renal capsule network layout"] + layoutRegion = region.createRegion() + networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters + layoutAnnotationGroups = networkLayout.getAnnotationGroups() + networkMesh = networkLayout.getConstructionObject() + + tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( + networkMesh, + targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], + defaultElementsCountAround=options["Elements count around"], + elementsCountThroughShell=options["Elements count through shell"], + layoutAnnotationGroups=layoutAnnotationGroups, + isCore=True, + elementsCountTransition=options["Number of elements across core transition"], + defaultElementsCountCoreBoxMinor=options["Number of elements across core box minor"], + annotationElementsCountsCoreBoxMinor=options["Annotation numbers of elements across core box minor"] + ) + + tubeNetworkMeshBuilder.build() + generateData = TubeNetworkMeshGenerateData( + region, 3, + isLinearThroughShell=False) + tubeNetworkMeshBuilder.generateMesh(generateData) + annotationGroups = generateData.getAnnotationGroups() + + return annotationGroups, None + +def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None): + """ + Assign node field parameters x, d1, d2, d3 of field. + :param field: Field parameters to assign. + :param fieldcache: Fieldcache with node set. + :param x: Parameters to set for Node.VALUE_LABEL_VALUE. + :param d1: Parameters to set for Node.VALUE_LABEL_D_DS1. + :param d2: Parameters to set for Node.VALUE_LABEL_D_DS2. + :param d3: Parameters to set for Node.VALUE_LABEL_D_DS3. + :param d12: Optional parameters to set for Node.VALUE_LABEL_D2_DS1DS2. + :param d13: Optional parameters to set for Node.VALUE_LABEL_D2_DS1DS3. + :return: + """ + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + if d12: + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, d12) + if d13: + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, d13) + + +def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=None, d13=None): + """ + Assign node field parameters d1, d2, d3 of field. + :param field: Field to assign parameters of. + :param fieldcache: Fieldcache with node set. + :param version: Version of d1, d2, d3 >= 1. + :param d1: Parameters to set for Node.VALUE_LABEL_D_DS1. + :param d2: Parameters to set for Node.VALUE_LABEL_D_DS2. + :param d3: Parameters to set for Node.VALUE_LABEL_D_DS3. + :param d12: Optional parameters to set for Node.VALUE_LABEL_D2_DS1DS2. + :param d13: Optional parameters to set for Node.VALUE_LABEL_D2_DS1DS3. + :return: + """ + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, d1) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, d2) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, d3) + if d12: + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, d12) + if d13: + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, d13) diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 6c35deb2..7f9d5952 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -41,6 +41,7 @@ from scaffoldmaker.meshtypes.meshtype_3d_musclefusiform1 import MeshType_3d_musclefusiform1 from scaffoldmaker.meshtypes.meshtype_3d_ostium1 import MeshType_3d_ostium1 from scaffoldmaker.meshtypes.meshtype_3d_ostium2 import MeshType_3d_ostium2 +from scaffoldmaker.meshtypes.meshtype_3d_renal_capsule1 import MeshType_3d_renal_capsule1, MeshType_1d_renal_capsule_network_layout1 from scaffoldmaker.meshtypes.meshtype_3d_smallintestine1 import MeshType_3d_smallintestine1 from scaffoldmaker.meshtypes.meshtype_3d_solidcylinder1 import MeshType_3d_solidcylinder1 from scaffoldmaker.meshtypes.meshtype_3d_solidsphere1 import MeshType_3d_solidsphere1 @@ -102,6 +103,7 @@ def __init__(self): MeshType_3d_musclefusiform1, MeshType_3d_ostium1, MeshType_3d_ostium2, + MeshType_3d_renal_capsule1, MeshType_3d_smallintestine1, MeshType_3d_solidcylinder1, MeshType_3d_solidsphere1, @@ -120,7 +122,8 @@ def __init__(self): MeshType_3d_wholebody2 ] self._allPrivateScaffoldTypes = [ - MeshType_1d_human_body_network_layout1 + MeshType_1d_human_body_network_layout1, + MeshType_1d_renal_capsule_network_layout1 ] def findScaffoldTypeByName(self, name): diff --git a/tests/test_renalcapsule.py b/tests/test_renalcapsule.py new file mode 100644 index 00000000..1f81ecfc --- /dev/null +++ b/tests/test_renalcapsule.py @@ -0,0 +1,120 @@ +import math +import unittest + +from cmlibs.utils.zinc.finiteelement import evaluateFieldNodesetRange +from cmlibs.utils.zinc.general import ChangeManager + +from cmlibs.zinc.context import Context +from cmlibs.zinc.field import Field +from cmlibs.zinc.result import RESULT_OK + +from scaffoldmaker.annotation.annotationgroup import getAnnotationGroupForTerm +from scaffoldmaker.annotation.kidney_terms import get_kidney_term +from scaffoldmaker.meshtypes.meshtype_3d_renal_capsule1 import MeshType_3d_renal_capsule1 + + +from testutils import assertAlmostEqualList + + +class RenalCapsulecaffoldTestCase(unittest.TestCase): + + def test_renalcapsule(self): + """ + Test creation of renal capsule scaffold. + """ + scaffold = MeshType_3d_renal_capsule1 + parameterSetNames = scaffold.getParameterSetNames() + self.assertEqual(parameterSetNames, ["Default", "Human 1"]) + options = scaffold.getDefaultOptions("Human 1") + + self.assertEqual(9, len(options)) + self.assertEqual(12, options["Elements count around"]) + self.assertEqual(1, options["Elements count through shell"]) + self.assertEqual([0], options["Annotation elements counts around"]) + self.assertEqual(4.0, options["Target element density along longest segment"]) + self.assertEqual(2, options["Number of elements across core box minor"]) + self.assertEqual(1, options["Number of elements across core transition"]) + self.assertEqual([0], options["Annotation numbers of elements across core box minor"]) + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + annotationGroups = scaffold.generateMesh(region, options)[0] + self.assertEqual(3, len(annotationGroups)) + + fieldmodule = region.getFieldmodule() + self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual(288, mesh3d.getSize()) + mesh2d = fieldmodule.findMeshByDimension(2) + self.assertEqual(920, mesh2d.getSize()) + mesh1d = fieldmodule.findMeshByDimension(1) + self.assertEqual(994, mesh1d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(363, nodes.getSize()) + datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) + self.assertEqual(0, datapoints.getSize()) + + # Check coordinates range, volume + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + tol = 1.0E-4 + assertAlmostEqualList(self, minimums, [-1.5818268645898947, -0.9278353405057089, -0.75], tol) + assertAlmostEqualList(self, maximums, [1.5818290319384656, 0.7499999999986053, 0.75], tol) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(3) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) + surfaceAreaField.setNumbersOfPoints(4) + result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 4.847649908739891, delta=tol) + self.assertAlmostEqual(surfaceArea, 15.287447927204916, delta=tol) + + # check some annotation groups: + + expectedSizes3d = { + "core": (80, 1.1338066143026762), + "shell": (48, 0.6376433780623295), + "renal capsule": (128, 1.7714499923650102) + } + for name in expectedSizes3d: + term = get_kidney_term(name) + annotationGroup = getAnnotationGroupForTerm(annotationGroups, term) + size = annotationGroup.getMeshGroup(mesh3d).getSize() + self.assertEqual(expectedSizes3d[name][0], size, name) + volumeMeshGroup = annotationGroup.getMeshGroup(mesh3d) + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, volumeMeshGroup) + volumeField.setNumbersOfPoints(4) + fieldcache = fieldmodule.createFieldcache() + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) + + expectedSizes2d = { + "shell": (204, 13.489465438804439), + "renal capsule": (440, 26.801531053920638) + } + for name in expectedSizes2d: + term = get_kidney_term(name) + annotationGroup = getAnnotationGroupForTerm(annotationGroups, term) + size = annotationGroup.getMeshGroup(mesh2d).getSize() + self.assertEqual(expectedSizes2d[name][0], size, name) + surfaceMeshGroup = annotationGroup.getMeshGroup(mesh2d) + surfaceAreaField = fieldmodule.createFieldMeshIntegral(one, coordinates, surfaceMeshGroup) + surfaceAreaField.setNumbersOfPoints(4) + fieldcache = fieldmodule.createFieldcache() + result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + self.assertAlmostEqual(surfaceArea, expectedSizes2d[name][1], delta=tol) From 733a6d520a7bce736d9c4d357e32d0f199c254f1 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 16 Jun 2025 11:04:10 +1200 Subject: [PATCH 07/43] Update checkOptions --- .../meshtypes/meshtype_3d_renal_capsule1.py | 70 ++++++++++++------- 1 file changed, 46 insertions(+), 24 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py index 685542b6..d34c67d9 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py @@ -235,34 +235,26 @@ def checkOptions(cls, options): cls.getOptionValidScaffoldTypes("Renal capsule network layout")): options["Renal capsule network layout"] = ScaffoldPackage(MeshType_1d_renal_capsule_network_layout1) - minElementsCountAround = None - for key in [ - "Elements count around" - ]: - if options[key] < 8: - options[key] = 8 - elif options[key] % 4: - options[key] += 4 - (options[key] % 4) - if (minElementsCountAround is None) or (options[key] < minElementsCountAround): - minElementsCountAround = options[key] + if options["Elements count around"] < 8: + options["Elements count around"] = 8 + elif options["Elements count around"] % 4: + options["Elements count around"] += 4 - (options["Elements count around"] % 4) - if options["Number of elements through shell"] < 0: - options["Number of elements through shell"] = 1 + if options["Elements count through shell"] < 1: + options["Elements count through shell"] = 1 if options["Number of elements across core transition"] < 1: options["Number of elements across core transition"] = 1 + minElementsCountAround = options["Elements count around"] maxElementsCountCoreBoxMinor = minElementsCountAround // 2 - 2 - for key in [ - "Number of elements across core box minor" - ]: - if options[key] < 2: - options[key] = 2 - elif options[key] > maxElementsCountCoreBoxMinor: - options[key] = maxElementsCountCoreBoxMinor - dependentChanges = True - elif options[key] % 2: - options[key] += options[key] % 2 + if options["Number of elements across core box minor"] < 2: + options["Number of elements across core box minor"] = 2 + elif options["Number of elements across core box minor"] > maxElementsCountCoreBoxMinor: + options["Number of elements across core box minor"] = maxElementsCountCoreBoxMinor + dependentChanges = True + elif options["Number of elements across core box minor"] % 2: + options["Number of elements across core box minor"] += options["Number of elements across core box minor"] % 2 annotationElementsCountsAround = options["Annotation elements counts around"] if len(annotationElementsCountsAround) == 0: @@ -271,8 +263,38 @@ def checkOptions(cls, options): for i in range(len(annotationElementsCountsAround)): if annotationElementsCountsAround[i] <= 0: annotationElementsCountsAround[i] = 0 - elif annotationElementsCountsAround[i] < 4: - annotationElementsCountsAround[i] = 4 + else: + if annotationElementsCountsAround[i] < 8: + annotationElementsCountsAround[i] = 8 + elif annotationElementsCountsAround[i] % 4: + annotationElementsCountsAround[i] += 4 - (annotationElementsCountsAround[i] % 4) + if annotationElementsCountsAround[i] < minElementsCountAround: + minElementsCountAround = annotationElementsCountsAround[i] + + annotationCoreBoxMinorCounts = options["Annotation numbers of elements across core box minor"] + if len(annotationCoreBoxMinorCounts) == 0: + annotationCoreBoxMinorCounts = options["Annotation numbers of elements across core box minor"] = [0] + if len(annotationCoreBoxMinorCounts) > len(annotationElementsCountsAround): + annotationCoreBoxMinorCounts = options["Annotation numbers of elements across core box minor"] = \ + annotationCoreBoxMinorCounts[:len(annotationElementsCountsAround)] + dependentChanges = True + for i in range(len(annotationCoreBoxMinorCounts)): + aroundCount = annotationElementsCountsAround[i] if annotationElementsCountsAround[i] \ + else options["Number of elements around"] + maxCoreBoxMinorCount = aroundCount // 2 - 2 + if annotationCoreBoxMinorCounts[i] <= 0: + annotationCoreBoxMinorCounts[i] = 0 + # this may reduce the default + if maxCoreBoxMinorCount < options["Number of elements across core box minor"]: + options["Number of elements across core box minor"] = maxCoreBoxMinorCount + dependentChanges = True + elif annotationCoreBoxMinorCounts[i] < 2: + annotationCoreBoxMinorCounts[i] = 2 + elif annotationCoreBoxMinorCounts[i] > maxCoreBoxMinorCount: + annotationCoreBoxMinorCounts[i] = maxCoreBoxMinorCount + dependentChanges = True + elif annotationCoreBoxMinorCounts[i] % 2: + annotationCoreBoxMinorCounts[i] += 1 if options["Target element density along longest segment"] < 1.0: options["Target element density along longest segment"] = 1.0 From 63dc46c9375501b739dec034f16f4aa586332fb1 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 16 Jun 2025 11:51:28 +1200 Subject: [PATCH 08/43] Add getLayoutStructure to 1D Renal Capsule Network --- .../meshtypes/meshtype_3d_renal_capsule1.py | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py index d34c67d9..c5aaa8af 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py @@ -33,11 +33,11 @@ def getParameterSetNames(cls): return ["Default"] @classmethod - def getDefaultOptions(clscls, parameterSetName="Default"): + def getDefaultOptions(cls, parameterSetName="Default"): options = {} options["Base parameter set"] = "Human 1" if (parameterSetName == "Default") else parameterSetName - options["Structure"] = "(1-2-3-4-5)" options["Define inner coordinates"] = True + options["Elements count along"] = 2 options["Renal capsule length"] = 1.0 options["Renal capsule diameter"] = 1.5 options["Renal capsule bend angle degrees"] = 10 @@ -47,6 +47,7 @@ def getDefaultOptions(clscls, parameterSetName="Default"): @classmethod def getOrderedOptionNames(cls): return [ + "Elements count along", "Renal capsule length", "Renal capsule diameter", "Renal capsule bend angle degrees", @@ -57,6 +58,7 @@ def getOrderedOptionNames(cls): def checkOptions(cls, options): dependentChanges = False for key in [ + "Elements count along", "Renal capsule length", "Renal capsule diameter", "Renal capsule bend angle degrees", @@ -75,7 +77,8 @@ def generateBaseMesh(cls, region, options): :return [] empty list of AnnotationGroup, NetworkMesh """ # parameters - structure = options["Structure"] + structure = options["Structure"] = cls.getLayoutStructure(options) + capsuleElementsCount = options["Elements count along"] capsuleLength = options["Renal capsule length"] capsuleRadius = 0.5 * options["Renal capsule diameter"] capsuleBendAngle = options["Renal capsule bend angle degrees"] @@ -93,7 +96,6 @@ def generateBaseMesh(cls, region, options): renalCapsuleGroup = renalCapsuleGroup.getMeshGroup(mesh) elementIdentifier = 1 - capsuleElementsCount = 4 meshGroups = [renalCapsuleGroup] for e in range(capsuleElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) @@ -160,6 +162,18 @@ def generateBaseMesh(cls, region, options): return annotationGroups, networkMesh + @classmethod + def getLayoutStructure(cls, options): + """ + Generate 1D layout structure based on the number of elements count along. + :param options: Dict containing options. See getDefaultOptions(). + :return string version of the 1D layout structure + """ + nodesCountAlong = options["Elements count along"] + 1 + assert nodesCountAlong > 1 + + return f"({'-'.join(str(i) for i in range(1, nodesCountAlong + 1))})" + class MeshType_3d_renal_capsule1(Scaffold_base): """ @@ -280,7 +294,7 @@ def checkOptions(cls, options): dependentChanges = True for i in range(len(annotationCoreBoxMinorCounts)): aroundCount = annotationElementsCountsAround[i] if annotationElementsCountsAround[i] \ - else options["Number of elements around"] + else options["Elements count around"] maxCoreBoxMinorCount = aroundCount // 2 - 2 if annotationCoreBoxMinorCounts[i] <= 0: annotationCoreBoxMinorCounts[i] = 0 @@ -296,8 +310,8 @@ def checkOptions(cls, options): elif annotationCoreBoxMinorCounts[i] % 2: annotationCoreBoxMinorCounts[i] += 1 - if options["Target element density along longest segment"] < 1.0: - options["Target element density along longest segment"] = 1.0 + if options["Target element density along longest segment"] < 2.0: + options["Target element density along longest segment"] = 2.0 return dependentChanges @classmethod From a855a3508156d84ff915490dcde3df14a30e1140 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 16 Jun 2025 12:00:02 +1200 Subject: [PATCH 09/43] Update unit test --- tests/test_renalcapsule.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_renalcapsule.py b/tests/test_renalcapsule.py index 1f81ecfc..da0db4de 100644 --- a/tests/test_renalcapsule.py +++ b/tests/test_renalcapsule.py @@ -60,8 +60,8 @@ def test_renalcapsule(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [-1.5818268645898947, -0.9278353405057089, -0.75], tol) - assertAlmostEqualList(self, maximums, [1.5818290319384656, 0.7499999999986053, 0.75], tol) + assertAlmostEqualList(self, minimums, [-1.583346623141804, -0.9520066012170885, -0.75], tol) + assertAlmostEqualList(self, maximums, [1.583349401938375, 0.7499999999986053, 0.75], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -79,15 +79,15 @@ def test_renalcapsule(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 4.847649908739891, delta=tol) - self.assertAlmostEqual(surfaceArea, 15.287447927204916, delta=tol) + self.assertAlmostEqual(volume, 4.844335733470136, delta=tol) + self.assertAlmostEqual(surfaceArea, 15.289023987470623, delta=tol) # check some annotation groups: expectedSizes3d = { - "core": (80, 1.1338066143026762), - "shell": (48, 0.6376433780623295), - "renal capsule": (128, 1.7714499923650102) + "core": (80, 1.1315894674812224), + "shell": (48, 0.636434417387064), + "renal capsule": (128, 1.76802388486829) } for name in expectedSizes3d: term = get_kidney_term(name) @@ -103,8 +103,8 @@ def test_renalcapsule(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "shell": (204, 13.489465438804439), - "renal capsule": (440, 26.801531053920638) + "shell": (204, 13.48337379199411), + "renal capsule": (440, 26.78248128640177) } for name in expectedSizes2d: term = get_kidney_term(name) From ae262792efad3852bc9aff3acc64cc0f845a0cb7 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 16 Jun 2025 12:41:49 +1200 Subject: [PATCH 10/43] Update checkOptions for 1D Renal Capsule Network --- .../meshtypes/meshtype_3d_renal_capsule1.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py index c5aaa8af..17116e67 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py @@ -58,13 +58,24 @@ def getOrderedOptionNames(cls): def checkOptions(cls, options): dependentChanges = False for key in [ - "Elements count along", "Renal capsule length", - "Renal capsule diameter", - "Renal capsule bend angle degrees", - "Inner proportion default" + "Renal capsule diameter" ]: - pass + if options[key] < 0.1: + options[key] = 0.1 + + if options["Elements count along"] < 2: + options["Elements count along"] = 2 + + if options["Renal capsule bend angle degrees"] < 0.0: + options["Renal capsule bend angle degrees"] = 0.0 + elif options["Renal capsule bend angle degrees"] > 30.0: + options["Renal capsule bend angle degrees"] = 30.0 + + if options["Inner proportion default"] < 0.1: + options["Inner proportion default"] = 0.1 + elif options["Inner proportion default"] > 0.9: + options["Inner proportion default"] = 0.9 return dependentChanges From db7aea6c615295443ca4a6bd42a0c65b67f877b9 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 15 Jul 2025 12:48:16 +1200 Subject: [PATCH 11/43] Add annotations to the cap mesh --- src/scaffoldmaker/utils/capmesh.py | 292 +++++++++++++++++---- src/scaffoldmaker/utils/tubenetworkmesh.py | 51 +++- 2 files changed, 283 insertions(+), 60 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 2418f74e..07187bf3 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -22,8 +22,7 @@ class CapMesh: """ def __init__(self, elementsCountAround, elementsCountCoreBoxMajor, elementsCountCoreBoxMinor, - elementsCountThroughShell, elementsCountTransition, networkPathParameters, tubeBoxCoordinates, - tubeTransitionCoordinates, tubeShellCoordinates, isCap, isCore): + elementsCountThroughShell, elementsCountTransition, networkPathParameters, isCap, isCore): """ :param elementsCountAround: Number of elements around this segment. :param elementsCountCoreBoxMajor: Number of elements across core box major axis. @@ -32,9 +31,6 @@ def __init__(self, elementsCountAround, elementsCountCoreBoxMajor, elementsCount :param elementsCountTransition: Number of elements across transition zone between core box elements and rim elements. :param networkPathParameters: List containing path parameters of a tube network. - :param tubeBoxCoordinates: List of coordinates and derivatives for nodes that form tube box elements. - :param tubeTransitionCoordinates: List of coordinates and derivatives for nodes that form tube transition elements. - :param tubeShellCoordinates: List of coordinates and derivatives for nodes that form tube rim elements. :param isCap: List [startCap, endCap] with boolean values. True if the tube segment requires a cap at the start of a segment, or at the end of a segment, respectively. [True, True] if the segment requires cap at both ends. @@ -50,9 +46,9 @@ def __init__(self, elementsCountAround, elementsCountCoreBoxMajor, elementsCount self._elementsCountTransition = elementsCountTransition self._networkPathParameters = networkPathParameters - self._tubeBoxCoordinates = tubeBoxCoordinates # tube box coordinates - self._tubeTransitionCoordinates = tubeTransitionCoordinates # tube transition coordinates - self._tubeShellCoordinates = tubeShellCoordinates # tube rim coordinates + self._tubeBoxCoordinates = None # tube box coordinates + self._tubeTransitionCoordinates = None # tube transition coordinates + self._tubeShellCoordinates = None # tube rim coordinates self._isStartCap = None self._generateData = None @@ -87,6 +83,12 @@ def __init__(self, elementsCountAround, elementsCountCoreBoxMajor, elementsCount # [rim][base, shield][nAround] self._endCapElementIds = None # elementIds that form the cap at the end of a tube segment. + self._startExtElementIds = None + self._endExtElementIds = None + + # annotation groups created if core: + self._coreGroup = None + self._shellGroup = None def _extendTubeEnds(self): """ @@ -206,7 +208,7 @@ def _remapCapCoordinates(self): xList.append(tx) self._shellCoordinates[idx][0][n3][m] = xList - def _determineCapCoordinatesWithoutCore(self): + def _sampleCapCoordinatesWithoutCore(self): """ Calculates coordinates and derivatives for the cap elements. It first calculates the coordinates for the apex nodes, and then calculates the coordinates for rim nodes on the shell surface. @@ -356,7 +358,7 @@ def _determineCapCoordinatesWithoutCore(self): for n3 in range(elementsCountThroughShell + 1): d3List[n3][1][n1] = sd3[n3] - def _determineCapCoordinatesWithCore(self, s): + def _sampleCapCoordinatesWithCore(self, s): """ Blackbox function for calculating coordinates and derivatives for the cap elements. It first calculates the coordinates for shell nodes, then calculates for box nodes. @@ -368,14 +370,14 @@ def _determineCapCoordinatesWithCore(self, s): idx = 0 if isStartCap else -1 centre = self._networkPathParameters[0][0][idx] - self._extendTubeEnds() # extend tube end + self._extendTubeEnds() # extend tube end # shell nodes nodesCountRim = self._getNodesCountRim() for n3 in range(nodesCountRim): ox = self._getRimExtCoordinatesAround(n3)[0] radius = self._getRadius(ox) - radii = self._getTubeRadii(centre, n3, idx) # radii for spheroid - oRadii = [1.0, radius, radius] # original radii used to create the sphere + radii = self._getTubeRadii(centre, n3, idx) # radii for spheroid + oRadii = [1.0, radius, radius] # original radii used to create the sphere ratio = self._getRatioBetweenTwoRadii(radii, oRadii) # ratio between original radii for the sphere and the new radii for spheroid self._calculateMajorAndMinorNodesCoordinates(n3, centre, ratio) @@ -468,7 +470,7 @@ def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio): refAxis = normalize(layoutD2) rotateAngle = (math.pi / elementsCountAcrossMinor) if self._isStartCap else \ -(math.pi / elementsCountAcrossMinor) - minorAxisNodesCoordinates = [[], []] # [startCap, endCap] + minorAxisNodesCoordinates = [[], []] # [startCap, endCap] n1 = self._elementsCountAround * 3 // 4 ix = self._getTubeRimCoordinates(n1, idx, n3) for n in range(1, elementsCountAcrossMinor): @@ -482,10 +484,10 @@ def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio): refAxis = normalize(layoutD3) rotateAngle = (math.pi / elementsCountAcrossMajor) if self._isStartCap else \ -(math.pi / elementsCountAcrossMajor) - majorAxisNodesCoordinates = [[], []] # [startCap, endCap] + majorAxisNodesCoordinates = [[], []] # [startCap, endCap] ix = self._getTubeRimCoordinates(0, idx, n3) - for m in range(1,elementsCountAcrossMajor): - for nx in [0, 1]: # [x, d1] + for m in range(1, elementsCountAcrossMajor): + for nx in [0, 1]: # [x, d1] vi = sub(ix[nx], centre) if nx == 0 else ix[nx] vi = div(vi, ratio[1]) vr = rotate_vector_around_vector(vi, refAxis, m * rotateAngle) @@ -585,7 +587,8 @@ def _calculateShellRegularNodeCoordinates(self, n3, centre): continue nx, nd1 = (self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) if n == 0 else self._sampleCurvesOnSphere(x2, x1, centre, elementsOut)) - start, end = ((n + 1, midMinorIndex) if n == 0 else (midMinorIndex + 1, self._elementsCountCoreBoxMinor)) + start, end = ( + (n + 1, midMinorIndex) if n == 0 else (midMinorIndex + 1, self._elementsCountCoreBoxMinor)) for c in range(start, end): idx_c = c % (len(nx) - 1) self._shellCoordinates[idx][0][n3][m][c] = nx[idx_c] @@ -601,7 +604,8 @@ def _calculateShellRegularNodeCoordinates(self, n3, centre): continue nx, nd2 = (self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) if m == 0 else self._sampleCurvesOnSphere(x2, x1, centre, elementsOut)) - start, end = ((m + 1, midMajorIndex) if m == 0 else (midMajorIndex + 1, self._elementsCountCoreBoxMajor)) + start, end = ( + (m + 1, midMajorIndex) if m == 0 else (midMajorIndex + 1, self._elementsCountCoreBoxMajor)) for c in range(start, end): idx_c = c % (len(nx) - 1) self._shellCoordinates[idx][0][n3][c][n] = nx[idx_c] @@ -1186,7 +1190,8 @@ def _generateNodesWithCore(self): node = nodes.createNode(nodeIdentifier, nodetemplate) fieldcache.setNode(node) for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, - Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], [rx, rd1, rd2, rd3]): + Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], + [rx, rd1, rd2, rd3]): coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) capNodeIds[n3][m].append(nodeIdentifier) else: @@ -1248,9 +1253,9 @@ def _generateExtendedTubeNodes(self): for n3 in range(nodesCountRim): n3p = n3 - (elementsCountTransition - 1) tx = self._transitionExtCoordinates[idx] if self._isCore and elementsCountTransition > 1 and n3 < ( - elementsCountTransition - 1) else self._shellExtCoordinates[idx] + elementsCountTransition - 1) else self._shellExtCoordinates[idx] rx, rd1, rd2, rd3 = [tx[i][n3 if self._isCore and elementsCountTransition > 1 and n3 < ( - elementsCountTransition - 1) else n3p] for i in range(4)] + elementsCountTransition - 1) else n3p] for i in range(4)] ringNodeIds = [] for n1 in range(self._elementsCountAround): nodeIdentifier = generateData.nextNodeIdentifier() @@ -1324,8 +1329,8 @@ def _generateElementsWithoutCore(self, elementsCountRim, annotationMeshGroups): for s in [0, 1, 4, 7, 10]: scalefactors[s] *= -1 element.setScaleFactors(eftCap, scalefactors) - # for annotationMeshGroup in annotationMeshGroups: - # annotationMeshGroup.addElement(element) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) capElementIds[e3][e2].append(elementIdentifier) else: idx = 0 if isStartCap else -1 @@ -1349,7 +1354,7 @@ def _generateElementsWithoutCore(self, elementsCountRim, annotationMeshGroups): else: self._endCapElementIds = capElementIds - def _generateElementsWithCore(self): + def _generateElementsWithCore(self, annotationMeshGroups): """ Blackbox function for generating cap elements. Used only when the tube segment has a core. """ @@ -1398,7 +1403,8 @@ def _generateElementsWithCore(self): for e1 in range(elementsCountCoreBoxMinor): nids, nodeParameters, nodeLayouts = [], [], [] for n1 in [e1, e1 + 1]: - nids += [capNodeIds[0][e3][n1], capNodeIds[0][e3p][n1], boxExtNodeIds[e3][n1], boxExtNodeIds[e3p][n1]] + nids += [capNodeIds[0][e3][n1], capNodeIds[0][e3p][n1], + boxExtNodeIds[e3][n1], boxExtNodeIds[e3p][n1]] if not isStartCap: for a in [nids]: a[-4], a[-2] = a[-2], a[-4] @@ -1406,6 +1412,8 @@ def _generateElementsWithCore(self): elementIdentifier = generateData.nextElementIdentifier() element = mesh.createElement(elementIdentifier, elementtemplateStd) element.setNodesByIdentifier(eftStd, nids) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) boxElementIds[e3].append(elementIdentifier) capElementIds.append(boxElementIds) @@ -1432,7 +1440,8 @@ def _generateElementsWithCore(self): nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(boxLocation, isStartCap) nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation, isStartCap) if nid in boxBoundaryNodeIds[0]: - nodeLayouts.append(nodeLayoutCapBoxShield if tpLocation == 0 else nodeLayoutCapBoxShieldTriplePoint) + nodeLayouts.append( + nodeLayoutCapBoxShield if tpLocation == 0 else nodeLayoutCapBoxShieldTriplePoint) else: nodeLayouts.append(None) if not isStartCap: @@ -1447,6 +1456,8 @@ def _generateElementsWithCore(self): element.setNodesByIdentifier(eft, nids) if scalefactors: element.setScaleFactors(eft, scalefactors) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) boxshieldElementIds[e3].append(elementIdentifier) capElementIds.append(boxshieldElementIds) @@ -1470,6 +1481,8 @@ def _generateElementsWithCore(self): elementIdentifier = generateData.nextElementIdentifier() element = mesh.createElement(elementIdentifier, elementtemplateStd) element.setNodesByIdentifier(eftStd, nids) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) shieldElementIds[e2].append(elementIdentifier) capElementIds.append(shieldElementIds) @@ -1492,7 +1505,7 @@ def _generateElementsWithCore(self): nodeLayoutCapShellTransitionTriplePoint = generateData.getNodeLayoutCapShellTriplePoint(shellLocation) for n3 in [0, 1]: for n1 in [e1, n1p]: - nid = boxBoundaryNodeIds[n3][n1] if n3 == 0 else rimBoundaryNodeIds[n3 -1][n1] + nid = boxBoundaryNodeIds[n3][n1] if n3 == 0 else rimBoundaryNodeIds[n3 - 1][n1] nids += [nid] mi, ni = boxBoundaryNodeToCapIndex[n3][n1] if n3 == 0 else rimBoundaryNodeToCapIndex[n3 - 1][n1] location, tpLocation = self._getBoxBoundaryLocation(mi, ni) @@ -1533,6 +1546,8 @@ def _generateElementsWithCore(self): element.setNodesByIdentifier(eft, nids) if scalefactors: element.setScaleFactors(eft, scalefactors) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) ringElementIds.append(elementIdentifier) capElementIds.append(ringElementIds) @@ -1572,6 +1587,8 @@ def _generateElementsWithCore(self): element.setNodesByIdentifier(eft, nids) if scalefactors: element.setScaleFactors(eft, scalefactors) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) rimElementIds.append(elementIdentifier) capElementIds.append(rimElementIds) @@ -1580,7 +1597,7 @@ def _generateElementsWithCore(self): else: self._endCapElementIds.append(capElementIds) - def _generateExtendedTubeElements(self, tubeBoxNodeIds, tubeRimNodeIds): + def _generateExtendedTubeElements(self, tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups): """ Blackbox function for generating extended tube elements. :param tubeBoxNodeIds: List of tube box nodes. @@ -1598,11 +1615,18 @@ def _generateExtendedTubeElements(self, tubeBoxNodeIds, tubeRimNodeIds): boxExtNodeIds = self._boxExtNodeIds[idx] rimExtNodeIds = self._rimExtNodeIds[idx] + if isStartCap: + self._startExtElementIds = [] if self._startExtElementIds is None else self._startExtElementIds + else: + self._endExtElementIds = [] if self._endExtElementIds is None else self._endExtElementIds + if self._isCore: boxExtBoundaryNodeIds, boxExtBoundaryNodesToBoxIds = self._createBoundaryNodeIdsList(boxExtNodeIds) tubeBoxBoundaryNodeIds, tubeBoxBoundaryNodesToBoxIds = self._createBoundaryNodeIdsList(tubeBoxNodeIds[idx]) # create box elements + boxElementIds = [] for e3 in range(self._elementsCountCoreBoxMajor): + boxElementIds.append([]) e3p = e3 + 1 for e1 in range(self._elementsCountCoreBoxMinor): nids = [] @@ -1616,13 +1640,14 @@ def _generateExtendedTubeElements(self, tubeBoxNodeIds, tubeRimNodeIds): elementIdentifier = generateData.nextElementIdentifier() element = mesh.createElement(elementIdentifier, elementtemplateStd) element.setNodesByIdentifier(eftStd, nids) - # for annotationMeshGroup in annotationMeshGroups: - # annotationMeshGroup.addElement(element) - boxExtElementIds.append(elementIdentifier) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + boxElementIds[e3].append(elementIdentifier) + boxExtElementIds.append(boxElementIds) # create core transition elements first layer after box triplePointIndexesList = self._getTriplePointIndexes() - ringExtElementIds = [] + boxElementIds = [] for e1 in range(self._elementsCountAround): nids, nodeParameters, nodeLayouts = [], [], [] n1p = (e1 + 1) % self._elementsCountAround @@ -1674,15 +1699,20 @@ def _generateExtendedTubeElements(self, tubeBoxNodeIds, tubeRimNodeIds): element.setNodesByIdentifier(eft, nids) if scalefactors: element.setScaleFactors(eft, scalefactors) - # for annotationMeshGroup in annotationMeshGroups: - # annotationMeshGroup.addElement(element) - ringExtElementIds.append(elementIdentifier) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + boxElementIds.append(elementIdentifier) + boxExtElementIds.append(boxElementIds) # create regular rim elements - all elements outside first transition layer elementsCountRim = self._getElementsCountRim() elementsCountRimRegular = elementsCountRim - 1 if self._isCore else elementsCountRim + nTransition = elementsCountRimRegular - self._elementsCountThroughShell for e3 in range(elementsCountRimRegular): - ringExtElementIds = [] + if e3 < nTransition: + boxElementIds = [] + else: + rimExtElementIds.append([]) lastTransition = self._isCore and (e3 == (self._elementsCountTransition - 2)) for e1 in range(self._elementsCountAround): elementtemplate, eft = elementtemplateStd, eftStd @@ -1723,25 +1753,40 @@ def _generateExtendedTubeElements(self, tubeBoxNodeIds, tubeRimNodeIds): element.setNodesByIdentifier(eft, nids) if scalefactors: element.setScaleFactors(eft, scalefactors) - # for annotationMeshGroup in annotationMeshGroups: - # annotationMeshGroup.addElement(element) - ringExtElementIds.append(elementIdentifier) - # self._rimElementIds[e2].append(ringExtElementIds) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + if e3 < nTransition: + boxElementIds.append(elementIdentifier) + else: + rimExtElementIds[-1].append(elementIdentifier) + if e3 < nTransition: + boxExtElementIds.append(boxElementIds) + + if isStartCap: + self._startExtElementIds.append(boxExtElementIds) + self._startExtElementIds.append(rimExtElementIds) + else: + self._endExtElementIds.append(boxExtElementIds) + self._endExtElementIds.append(rimExtElementIds) - def sampleCoordinates(self): + def sampleCoordinates(self, tubeBoxCoordinates, tubeTransitionCoordinates, tubeShellCoordinates): """ Sample cap coordinates. """ + self._tubeBoxCoordinates = tubeBoxCoordinates # tube box coordinates + self._tubeTransitionCoordinates = tubeTransitionCoordinates # tube transition coordinates + self._tubeShellCoordinates = tubeShellCoordinates # tube rim coordinates + if self._isCore: self._createShellCoordinatesList() for s in range(2): self._isStartCap = True if self._isCap[0] and s == 0 else False if self._isCap[s]: if self._isCore: - self._determineCapCoordinatesWithCore(s) + self._sampleCapCoordinatesWithCore(s) else: self._extendTubeEnds() - self._determineCapCoordinatesWithoutCore() + self._sampleCapCoordinatesWithoutCore() else: continue @@ -1770,16 +1815,165 @@ def generateElements(self, elementsCountRim, tubeBoxNodeIds, tubeRimNodeIds, ann :param tubeBoxNodeIds: List of tube box nodes. :param tubeRimNodeIds: List of tube rim nodes. :param annotationMeshGroups: List of all annotated mesh groups. - :param coreBoundaryScalingMode: Mode 1 to set scale factors, 2 to add version 2 to d3 for the boundary nodes - and assigning values to that version equal to the scale factors x version 1. :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end of a tube segment. :param isCore: True for generating a solid core inside the tube, False for regular tube network. """ self._isStartCap = isStartCap if isCore: - self._generateElementsWithCore() - self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds) + self._generateElementsWithCore(annotationMeshGroups) + self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) else: self._generateElementsWithoutCore(elementsCountRim, annotationMeshGroups) - self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds) + self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) + + def _addElementsFromIdentifiers(self, mesh, meshGroup, identifiers, tRange=None, mRange=None, nRange=None, + e2Range=None): + """ + Adds elements to the mesh group based on a structured list of element identifiers. + :param mesh: The master mesh object used to look up elements by identifier. + :param meshGroup: Zinc MeshGroup to add elements to. + :param identifiers: A nested list of element identifiers. + :param tRange: Range of transition indices. + :param mRange: Range along the major axis. + :param nRange: Range along the minor axis. + :param e2Range: Range around the tube, for ring/circular indexing. + """ + for t in tRange: + if mRange is not None and nRange is not None: + for m in mRange: + for n in nRange: + elementIdentifier = identifiers[t][m][n] + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroup.addElement(element) + elif e2Range is not None: + for e2 in e2Range: + elementIdentifier = identifiers[t][e2] + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroup.addElement(element) + + def addBoxElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None): + """ + Add ranges of box elements to mesh group. + :param meshGroup: Zinc MeshGroup to add elements to. + :param e1Range: Range between start and limit element indexes in major / d2 direction. + :param e2Range: Range between start and limit element indexes around the tube. + :param e3Range: Range between start and limit element indexes in minor / d3 direction. + """ + elementsCountAround = self._elementsCountAround + elementsCountCoreBoxMajor = self._elementsCountCoreBoxMajor + elementsCountCoreBoxMinor = self._elementsCountCoreBoxMinor + elementsCountTransition = self._elementsCountTransition + + mesh = meshGroup.getMasterMesh() + e1Range = range(elementsCountCoreBoxMajor) if e1Range is None else e1Range + e2Range = range(elementsCountAround) if e2Range is None else e2Range + e3Range = range(elementsCountCoreBoxMinor) if e3Range is None else e3Range + + for i, isCap in enumerate(self._isCap): + if not isCap: + continue + + isStart = (i == 0) + capElementIds = self._startCapElementIds if isStart else self._endCapElementIds + extElementIds = self._startExtElementIds if isStart else self._endExtElementIds + + # Cap base block (structured m, t, n) + self._addElementsFromIdentifiers(mesh, meshGroup, capElementIds[0], + tRange=range(elementsCountTransition + 1), mRange=e1Range, nRange=e3Range) + + # Cap tube elements (t, e2) + self._addElementsFromIdentifiers(mesh, meshGroup, capElementIds[1], + tRange=range(elementsCountTransition), e2Range=e2Range) + + # Extension base block (just one layer at t=0) + self._addElementsFromIdentifiers(mesh, meshGroup, extElementIds[0][0:1], + tRange=[0], mRange=e1Range, nRange=e3Range) + + # Extension tube (t > 0, e2) + self._addElementsFromIdentifiers(mesh, meshGroup, extElementIds[0], + tRange=range(1, elementsCountTransition + 1), e2Range=e2Range) + + def addShellElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None): + """ + Add ranges of shell elements to mesh group. + :param meshGroup: Zinc MeshGroup to add elements to. + :param e1Range: Range between start and limit element indexes in major / d2 direction. + :param e2Range: Range between start and limit element indexes around the tube. + :param e3Range: Range between start and limit element indexes in minor / d3 direction. + """ + elementsCountAround = self._elementsCountAround + elementsCountCoreBoxMajor = self._elementsCountCoreBoxMajor + elementsCountCoreBoxMinor = self._elementsCountCoreBoxMinor + elementsCountTransition = self._elementsCountTransition + elementsCountThroughShell = self._elementsCountThroughShell + + mesh = meshGroup.getMasterMesh() + e1Range = range(elementsCountCoreBoxMajor) if e1Range is None else e1Range + e2Range = range(elementsCountAround) if e2Range is None else e2Range + e3Range = range(elementsCountCoreBoxMinor) if e3Range is None else e3Range + + for i, isCap in enumerate(self._isCap): + if not isCap: + continue + + isStart = (i == 0) + capElementIds = self._startCapElementIds if isStart else self._endCapElementIds + extElementIds = self._startExtElementIds if isStart else self._endExtElementIds + + tCapShellRange = range(elementsCountTransition + 1, elementsCountTransition + 1 + elementsCountThroughShell) + self._addElementsFromIdentifiers(mesh, meshGroup, capElementIds[0], + tRange=tCapShellRange, mRange=e1Range, nRange=e3Range) + + tTubeShellRange = range(elementsCountTransition, elementsCountTransition + elementsCountThroughShell) + self._addElementsFromIdentifiers(mesh, meshGroup, capElementIds[1], tRange=tTubeShellRange, e2Range=e2Range) + + tExtShellRange = range(elementsCountThroughShell) + self._addElementsFromIdentifiers(mesh, meshGroup, extElementIds[1], tRange=tExtShellRange, e2Range=e2Range) + + def addAllElementsToMeshGroup(self, meshGroup): + """ + Add all elements in the segment to mesh group. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + self.addBoxElementsToMeshGroup(meshGroup) + self.addShellElementsToMeshGroup(meshGroup) + + def addSideD2ElementsToMeshGroup(self, side: bool, meshGroup): + """ + Add elements to the mesh group on side of +d2 or -d2, often matching left and right. + Only works with even numbers around and phase starting at +d2. + :param side: False for +d2 direction, True for -d2 direction. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + e2Start = (self._elementsCountAround // 4) if side else -((self._elementsCountAround + 2) // 4) + e2Limit = e2Start + (self._elementsCountAround // 2) + e2Limit = e2Limit + 1 if (self._elementsCountAround % 4) == 2 else e2Limit + e2Range = range(e2Start, e2Limit) + if self._isCore: + e1Start = (self._elementsCountCoreBoxMajor // 2) if side else 0 + e1Limit = self._elementsCountCoreBoxMajor if side else ((self._elementsCountCoreBoxMajor + 1) // 2) + e1Range = range(e1Start, e1Limit) + self.addBoxElementsToMeshGroup(meshGroup, e1Range=e1Range, e2Range=e2Range) + else: + e1Range = None + self.addShellElementsToMeshGroup(meshGroup, e1Range=e1Range, e2Range=e2Range) + + def addSideD3ElementsToMeshGroup(self, side: bool, meshGroup): + """ + Add elements to the mesh group on side of +d3 or -d3, often matching anterior/ventral and posterior/dorsal. + Only works with even numbers around and phase starting at +d2. + :param side: False for +d3 direction, True for -d3 direction. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + e2Start = (self._elementsCountAround // 2) if side else 0 + e2Limit = e2Start + (self._elementsCountAround // 2) + e2Range = range(e2Start, e2Limit) + if self._isCore: + e3Start = 0 if side else (self._elementsCountCoreBoxMinor // 2) + e3Limit = ((self._elementsCountCoreBoxMinor + 1) // 2) if side else self._elementsCountCoreBoxMinor + e3Range = range(e3Start, e3Limit) + self.addBoxElementsToMeshGroup(meshGroup, e2Range=e2Range, e3Range=e3Range) + else: + e3Range = None + self.addShellElementsToMeshGroup(meshGroup, e2Range=e2Range, e3Range=e3Range) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 53822c6e..a0368fd9 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -87,6 +87,7 @@ def __init__(self, region, meshDimension, coordinateFieldName="coordinates", self._rightGroup = None self._dorsalGroup = None self._ventralGroup = None + self._renalCapsuleGroup = None def getStandardElementtemplate(self): return self._standardElementtemplate, self._standardEft @@ -279,6 +280,11 @@ def getVentralMeshGroup(self): self._ventralGroup = self.getOrCreateAnnotationGroup(("ventral", "")) return self._ventralGroup.getMeshGroup(self._mesh) + def getRenalCapsuleMeshGroup(self): + if not self._renalCapsuleGroup: + self._renalCapsuleGroup = self.getOrCreateAnnotationGroup(("renal capsule", "")) + return self._renalCapsuleGroup.getMeshGroup(self._mesh) + def getNewTrimAnnotationGroup(self): self._trimAnnotationGroupCount += 1 return self.getOrCreateAnnotationGroup(("trim surface " + "{:03d}".format(self._trimAnnotationGroupCount), "")) @@ -379,7 +385,11 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem self._networkPathParameters = pathParametersList self._isCap = networkSegment.isCap() - self._capmesh = None + if self._isCap: + self._capMesh = CapMesh(self._elementsCountAround, self._elementsCountCoreBoxMajor, + self._elementsCountCoreBoxMinor, self._elementsCountThroughShell, + self._elementsCountTransition, self._networkPathParameters, + self._isCap, self._isCore) def getCoreBoundaryScalingMode(self): modes = (1, 2, 3, 4) if self._isCap else (1, 2) @@ -395,6 +405,12 @@ def getRawTubeCoordinates(self, pathIndex=0): def getIsCore(self): return self._isCore + def getIsCap(self): + return self._isCap + + def getCapMesh(self): + return self._capMesh + def getElementsCountCoreBoxMajor(self): return self._elementsCountCoreBoxMajor @@ -615,11 +631,7 @@ def sample(self, fixedElementsCountAlong, targetElementLength): if self._isCap: # sample coordinates for the cap mesh at the ends of a tube segment - self._capmesh = CapMesh(self._elementsCountAround, self._elementsCountCoreBoxMajor, - self._elementsCountCoreBoxMinor, self._elementsCountThroughShell, - self._elementsCountTransition, self._networkPathParameters, self._boxCoordinates, - self._transitionCoordinates, self._rimCoordinates, self._isCap, self._isCore) - self._capmesh.sampleCoordinates() + self._capMesh.sampleCoordinates(self._boxCoordinates, self._transitionCoordinates, self._rimCoordinates) def _sampleCoreCoordinates(self, elementsCountAlong): """ @@ -1629,11 +1641,11 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): self._rimNodeIds[n2] = rimNodeIds continue - capmesh = self._capmesh + capMesh = self._capMesh # create cap nodes at the start section of a tube segment if self._isCap[0] and n2 == 0: isStartCap = True - capmesh.generateNodes(generateData, isStartCap, self._isCore) + capMesh.generateNodes(generateData, isStartCap, self._isCore) # create core box nodes if self._boxCoordinates: @@ -1689,7 +1701,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create cap nodes at the end section of a tube segment if self._isCap[-1] and n2 == elementsCountAlong: isStartCap = False - self._endCapNodeIds = capmesh.generateNodes(generateData, isStartCap, self._isCore) + self._endCapNodeIds = capMesh.generateNodes(generateData, isStartCap, self._isCore) # create a new list containing box node ids are located at the boundary if self._isCore: @@ -1710,11 +1722,11 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create cap elements if self._isCap[0] and e2 == 0: isStartCap = True - capmesh.generateElements(elementsCountRim, self._boxNodeIds, self._rimNodeIds, + capMesh.generateElements(elementsCountRim, self._boxNodeIds, self._rimNodeIds, annotationMeshGroups, isStartCap, self._isCore) elif self._isCap[-1] and e2 == (elementsCountAlong - endSkipCount - 1): isStartCap = False - capmesh.generateElements(elementsCountRim, self._boxNodeIds, self._rimNodeIds, + capMesh.generateElements(elementsCountRim, self._boxNodeIds, self._rimNodeIds, annotationMeshGroups, isStartCap, self._isCore) if self._isCore: @@ -3346,8 +3358,14 @@ def generateMesh(self, generateData): shellMeshGroup = generateData.getShellMeshGroup() for networkSegment in self._networkMesh.getNetworkSegments(): segment = self._segments[networkSegment] + segmentCaps = segment.getIsCap() segment.addCoreElementsToMeshGroup(coreMeshGroup) segment.addShellElementsToMeshGroup(shellMeshGroup) + for isCap in segmentCaps: + if isCap: + capMesh = segment.getCapMesh() + capMesh.addBoxElementsToMeshGroup(coreMeshGroup) + capMesh.addShellElementsToMeshGroup(shellMeshGroup) class BodyTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): @@ -3368,6 +3386,11 @@ def generateMesh(self, generateData): ventralMeshGroup = generateData.getVentralMeshGroup() for networkSegment in self._networkMesh.getNetworkSegments(): segment = self._segments[networkSegment] + segmentCaps = segment.getIsCap() + if True in segmentCaps: + capMesh = segment.getCapMesh() + else: + capMesh = None annotationTerms = segment.getAnnotationTerms() for annotationTerm in annotationTerms: if "left" in annotationTerm[0]: @@ -3380,8 +3403,14 @@ def generateMesh(self, generateData): # segment on main axis segment.addSideD2ElementsToMeshGroup(False, leftMeshGroup) segment.addSideD2ElementsToMeshGroup(True, rightMeshGroup) + if capMesh: + capMesh.addSideD2ElementsToMeshGroup(False, leftMeshGroup) + capMesh.addSideD2ElementsToMeshGroup(True, rightMeshGroup) segment.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) segment.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) + if capMesh: + capMesh.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) + capMesh.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) class TubeEllipseGenerator: From 2bf888d4f4fd90f360ab095a34cf965189aa3bd7 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 15 Jul 2025 13:17:48 +1200 Subject: [PATCH 12/43] Replace renal capsule to kidney capsule in kidney terms --- src/scaffoldmaker/annotation/kidney_terms.py | 4 ++-- src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/annotation/kidney_terms.py b/src/scaffoldmaker/annotation/kidney_terms.py index ca228b88..241ccfe8 100644 --- a/src/scaffoldmaker/annotation/kidney_terms.py +++ b/src/scaffoldmaker/annotation/kidney_terms.py @@ -5,10 +5,10 @@ # convention: preferred name, preferred id, followed by any other ids and alternative names kidney_terms = [ ("core", ""), - ("renal capsule", "", ""), - ("renal pelvis", "UBERON:0001224", "ILX:0723968"), + ("kidney capsule", "UBERON:0002015", "ILX:0733912"), ("major calyx", "UBERON:0001226", "ILX:0730785"), ("minor calyx", "UBERON:0001227", "ILX:0730473"), + ("renal pelvis", "UBERON:0001224", "ILX:0723968"), ("renal pyramid", "UBERON:0004200", "ILX:0727514"), ("shell", "") ] diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py index 17116e67..2d897f06 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py @@ -102,7 +102,7 @@ def generateBaseMesh(cls, region, options): mesh = fieldmodule.findMeshByDimension(1) # set up element annotations - renalCapsuleGroup = AnnotationGroup(region, get_kidney_term("renal capsule")) + renalCapsuleGroup = AnnotationGroup(region, get_kidney_term("kidney capsule")) annotationGroups = [renalCapsuleGroup] renalCapsuleGroup = renalCapsuleGroup.getMeshGroup(mesh) From 88a47cceabc59526da4f74f705d571bbb271cb18 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 15 Jul 2025 13:18:53 +1200 Subject: [PATCH 13/43] Update unit test for renal capsule scaffold --- tests/test_renalcapsule.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_renalcapsule.py b/tests/test_renalcapsule.py index da0db4de..33f944f4 100644 --- a/tests/test_renalcapsule.py +++ b/tests/test_renalcapsule.py @@ -85,9 +85,9 @@ def test_renalcapsule(self): # check some annotation groups: expectedSizes3d = { - "core": (80, 1.1315894674812224), - "shell": (48, 0.636434417387064), - "renal capsule": (128, 1.76802388486829) + "core": (176, 2.880428953529323), + "shell": (112, 1.9640545714444255), + "kidney capsule": (288, 4.844483524973759) } for name in expectedSizes3d: term = get_kidney_term(name) @@ -103,8 +103,8 @@ def test_renalcapsule(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "shell": (204, 13.48337379199411), - "renal capsule": (440, 26.78248128640177) + "shell": (448, 37.8673195697525), + "kidney capsule": (920, 66.45790167672409) } for name in expectedSizes2d: term = get_kidney_term(name) From cca6e03ddbac4075349418d5682f3a2236ffa049 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 15 Jul 2025 14:18:54 +1200 Subject: [PATCH 14/43] Update unit test for cap mesh --- tests/test_capmesh.py | 2 +- tests/test_renalcapsule.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_capmesh.py b/tests/test_capmesh.py index c2b71268..f794454e 100644 --- a/tests/test_capmesh.py +++ b/tests/test_capmesh.py @@ -369,7 +369,7 @@ def test_3d_tube_network_converging_bifurcation_core(self): annotationGroup = findAnnotationGroupByName(annotationGroups, "segment 3") self.assertTrue(annotationGroup is not None) self.assertEqual("SEGMENT:3", annotationGroup.getId()) - self.assertEqual(80, annotationGroup.getMeshGroup(fieldmodule.findMeshByDimension(3)).getSize()) + self.assertEqual(128, annotationGroup.getMeshGroup(fieldmodule.findMeshByDimension(3)).getSize()) X_TOL = 1.0E-6 diff --git a/tests/test_renalcapsule.py b/tests/test_renalcapsule.py index 33f944f4..4c9f5c14 100644 --- a/tests/test_renalcapsule.py +++ b/tests/test_renalcapsule.py @@ -118,3 +118,6 @@ def test_renalcapsule(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) self.assertAlmostEqual(surfaceArea, expectedSizes2d[name][1], delta=tol) + +if __name__ == "__main__": + unittest.main() From d8e9864031fdf4978ab7d4c9cbf126318df5b7ee Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 18 Jul 2025 10:29:43 +1200 Subject: [PATCH 15/43] Improve derivatives at cap-tube joint --- src/scaffoldmaker/utils/capmesh.py | 125 +++++++++++++++++++-- src/scaffoldmaker/utils/tubenetworkmesh.py | 51 ++++++++- 2 files changed, 168 insertions(+), 8 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 07187bf3..1d3d76ed 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -11,7 +11,7 @@ from scaffoldmaker.utils.eft_utils import determineCubicHermiteSerendipityEft from scaffoldmaker.utils.eftfactory_tricubichermite import eftfactory_tricubichermite from scaffoldmaker.utils.interpolation import smoothCubicHermiteDerivativesLoop, smoothCubicHermiteDerivativesLine, \ - sampleCubicHermiteCurves, interpolateSampleCubicHermite + sampleCubicHermiteCurves, interpolateSampleCubicHermite, DerivativeScalingMode from scaffoldmaker.utils.spheremesh import calculate_arc_length, local_to_global_coordinates, spherical_to_cartesian, \ calculate_azimuth @@ -391,6 +391,7 @@ def _sampleCapCoordinatesWithCore(self, s): self._determineBoxDerivatives() self._remapCapCoordinates() + self._smoothDerivatives() def _createShellCoordinatesList(self): """ @@ -668,6 +669,12 @@ def _getTubeRadii(self, centre, n3, idx): n1m, n1n = 0, self._elementsCountAround // 4 ixm, ixn = (self._getTubeRimCoordinates(n, idx, n3)[0] for n in [n1m, n1n]) majorRadius, minorRadius = (magnitude(sub(coord, centre)) for coord in [ixm, ixn]) + # if majorRadius > minorRadius: + # xRadius = majorRadius / minorRadius + # elif majorRadius < minorRadius: + # xRadius = minorRadius / majorRadius + # else: + # xRadius = 1.0 return [1.0, majorRadius, minorRadius] def _determineShellDerivatives(self): @@ -818,17 +825,17 @@ def _calculateBoxMajorAndMinorNodes(self): self._boxCoordinates[idx][3][mi][n] = td3[mi] # smooth derivatives - for m in range(1, self._elementsCountCoreBoxMajor): + for m in range(self._elementsCountCoreBoxMajor + 1): nx = self._boxCoordinates[idx][0][m] nd3 = self._boxCoordinates[idx][3][m] sd3 = smoothCubicHermiteDerivativesLine(nx, nd3) - for n in range(1, self._elementsCountCoreBoxMinor): + for n in range(self._elementsCountCoreBoxMinor + 1): self._boxCoordinates[idx][3][m][n] = sd3[n] - for n in range(self._elementsCountCoreBoxMinor): + for n in range(self._elementsCountCoreBoxMinor + 1): nx = [self._boxCoordinates[idx][0][m][n] for m in range(self._elementsCountCoreBoxMajor + 1)] nd1 = [self._boxCoordinates[idx][1][m][n] for m in range(self._elementsCountCoreBoxMajor + 1)] sd1 = smoothCubicHermiteDerivativesLine(nx, nd1) - for m in range(1, self._elementsCountCoreBoxMajor): + for m in range(self._elementsCountCoreBoxMajor + 1): self._boxCoordinates[idx][1][m][n] = sd1[m] def _determineBoxDerivatives(self): @@ -843,8 +850,110 @@ def _determineBoxDerivatives(self): itx = self._boxCoordinates[idx][0][m][n] d2 = mult(sub(otx, itx), signValue) self._boxCoordinates[idx][2][m][n] = d2 - self._boxCoordinates[idx][1][m][n] = set_magnitude(self._boxCoordinates[idx][1][m][n], magnitude(d2)) - self._boxCoordinates[idx][3][m][n] = set_magnitude(self._boxCoordinates[idx][3][m][n], magnitude(d2)) + + def _smoothDerivatives(self): + """ + Smooths derivatives to eliminate zero Jacobian contours at the cap-tube joint. + """ + nodesCountCoreBoxMajor = self._getNodesCountCoreBoxMajor() + nodesCountCoreBoxMinor = self._getNodesCountCoreBoxMinor() + nodesCountRim = self._getNodesCountRim() + nloop = self._getNodesCountRim() + + idx = 0 if self._isStartCap else -1 + if self._isCap[idx]: + boxCoordinates = self._boxCoordinates[idx] + boxExtCoordinates = self._boxExtCoordinates[idx] + shellCoordinates = self._shellCoordinates[idx] + tubeBoxCoordinates = self._tubeBoxCoordinates + boxBoundaryNodeToBoxId = self._createBoxBoundaryNodeIdsList() + + # smooth derivatives along d2 for box + for m in range(nodesCountCoreBoxMajor): + for n in range(nodesCountCoreBoxMinor): + nx = [boxCoordinates[0][m][n], boxExtCoordinates[0][m][n], tubeBoxCoordinates[0][idx][m][n]] + nd2 = [boxCoordinates[2][m][n], boxExtCoordinates[2][m][n], tubeBoxCoordinates[2][idx][m][n]] + sd2 = smoothCubicHermiteDerivativesLine(nx, nd2, fixAllDirections=True, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) + boxCoordinates[2][m][n] = sd2[0] + boxExtCoordinates[2][m][n] = sd2[1] + + # smooth derivatives along d3 for shell + for m in range(nodesCountCoreBoxMajor): + for n in range(nodesCountCoreBoxMinor): + bx = boxCoordinates[0][m][n] + bd3 = sub(shellCoordinates[0][0][m][n], bx) + nx = [bx] + [shellCoordinates[0][n3][m][n] for n3 in range(nloop)] + nd3 = [bd3] + [shellCoordinates[3][n3][m][n] for n3 in range(nloop)] + sd3 = smoothCubicHermiteDerivativesLine(nx, nd3, fixAllDirections=True, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) + for n3 in range(nloop): + shellCoordinates[3][n3][m][n] = sd3[n3 + 1] + + # smooth transition/shell ext coordinates along d2 + for n2 in range(self._elementsCountAround): + m, n = boxBoundaryNodeToBoxId[n2] + for n3 in range(nodesCountRim - 1): + tx = self._getRimExtCoordinatesAround(n3)[0][n2] + bx = shellCoordinates[0][n3][m][n] + bd2 = sub(bx, tx) + nx = [bx, tx] + nd2 = [bd2, self._getRimExtCoordinatesAround(n3)[2][n2]] + sd2 = smoothCubicHermiteDerivativesLine(nx, nd2, fixAllDirections=True, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) + if self._elementsCountTransition - 1 > n3: + self._transitionExtCoordinates[idx][2][n3][n2] = sd2[1] + else: + n3p = n3 - 1 - self._elementsCountTransition if self._elementsCountTransition > 1 else n3 + self._shellExtCoordinates[idx][2][n3p][n2] = sd2[1] + + # smooth shell ext coordinates along d3 + for n2 in range(self._elementsCountAround): + m, n = boxBoundaryNodeToBoxId[n2] + bx = boxCoordinates[0][m][n] + tx = [self._getRimExtCoordinatesAround(n3)[0][n2] for n3 in range(nodesCountRim)] + bd3 = sub(bx, tx[0]) + td3 = [self._getRimExtCoordinatesAround(n3)[3][n2] for n3 in range(nodesCountRim)] + nx = [bx] + tx + nd3 = [bd3] + td3 + sd3 = smoothCubicHermiteDerivativesLine(nx, nd3, fixAllDirections=True, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) + self._shellExtCoordinates[idx][3][0][n2] = sd3[1] + + def _createBoxBoundaryNodeIdsList(self): + """ + Creates a list (in a circular format similar to other rim node id lists) of core box node ids that are + located at the boundary of the core. This list is used to easily stitch inner rim nodes with box nodes. + Used specifically for solid core at the junction. + :return: A list of box node ids stored in a circular format, and a lookup list that translates indexes used in + boxBoundaryNodeIds list to indexes that can be used in boxCoordinates list. + """ + boxBoundaryNodeToBoxId = [] + elementsCountCoreBoxMajor = self._elementsCountCoreBoxMajor + elementsCountCoreBoxMinor = self._elementsCountCoreBoxMinor + coreBoxMajorNodesCount = elementsCountCoreBoxMajor + 1 + coreBoxMinorNodesCount = elementsCountCoreBoxMinor + 1 + + for n3 in range(coreBoxMajorNodesCount): + if n3 == 0 or n3 == coreBoxMajorNodesCount - 1: + n1List = list(range(coreBoxMinorNodesCount)) if n3 == 0 else ( + list(range(coreBoxMinorNodesCount - 1, -1, -1))) + for n1 in n1List: + boxBoundaryNodeToBoxId.append([n3, n1]) + else: + for n1 in [-1, 0]: + boxBoundaryNodeToBoxId.append([n3, n1]) + + start = elementsCountCoreBoxMajor - 2 + idx = elementsCountCoreBoxMinor + 2 + for n in range(int(start), -1, -1): + boxBoundaryNodeToBoxId.append(boxBoundaryNodeToBoxId.pop(idx + 2 * n)) + + nloop = elementsCountCoreBoxMinor // 2 + for _ in range(nloop): + boxBoundaryNodeToBoxId.insert(len(boxBoundaryNodeToBoxId), boxBoundaryNodeToBoxId.pop(0)) + + return boxBoundaryNodeToBoxId def _createBoundaryNodeIdsList(self, nodeIds): """ @@ -1790,6 +1899,8 @@ def sampleCoordinates(self, tubeBoxCoordinates, tubeTransitionCoordinates, tubeS else: continue + return self._boxExtCoordinates, self._transitionExtCoordinates, self._shellExtCoordinates + def generateNodes(self, generateData, isStartCap=True, isCore=False): """ Blackbox function for generating cap and extended tube nodes. diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index a0368fd9..c6217fc3 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -383,6 +383,10 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem # [nAlong][nAcrossMajor][nAcrossMinor] format. self._boxElementIds = None # [along][major][minor] + self._boxExtCoordinates = None + self._transitionExtCoordinates = None + self.rimExtCoordinates = None + self._networkPathParameters = pathParametersList self._isCap = networkSegment.isCap() if self._isCap: @@ -631,7 +635,10 @@ def sample(self, fixedElementsCountAlong, targetElementLength): if self._isCap: # sample coordinates for the cap mesh at the ends of a tube segment - self._capMesh.sampleCoordinates(self._boxCoordinates, self._transitionCoordinates, self._rimCoordinates) + self._boxExtCoordinates, self._transitionExtCoordinates, self.rimExtCoordinates = ( + self._capMesh.sampleCoordinates(self._boxCoordinates, self._transitionCoordinates, self._rimCoordinates)) + + self._smoothD2DerivativesAtCapTubeJoint() def _sampleCoreCoordinates(self, elementsCountAlong): """ @@ -1148,6 +1155,48 @@ def _createBoxBoundaryNodeIdsList(self, startSkipCount=None, endSkipCount=None): return boxBoundaryNodeIds, boxBoundaryNodeToBoxId + def _smoothD2DerivativesAtCapTubeJoint(self): + """ + Smooths D2 derivatives at the joint where the cap and the tube surfaces join to eliminate zero Jacobian contours. + """ + for i in [0, -1]: + if not self._isCap[i]: + continue + + capCoordinates = self._boxExtCoordinates[i] + for m in range(self._elementsCountCoreBoxMajor + 1): + for n in range(self._elementsCountCoreBoxMinor + 1): + nx = [capCoordinates[0][m][n], self._boxCoordinates[0][i][m][n]] + nd2 = [capCoordinates[2][m][n], self._boxCoordinates[2][i][m][n]] + if i == -1: + nx.reverse() + nd2.reverse() + sd2 = smoothCubicHermiteDerivativesLine(nx, nd2) + self._boxCoordinates[2][i][m][n] = sd2[1] + + capCoordinates = self.rimExtCoordinates[i] + for n3 in range(self._elementsCountThroughShell + 1): + for n1 in range(self._elementsCountAround): + nx = [capCoordinates[0][n3][n1], self._rimCoordinates[0][i][n3][n1]] + nd2 = [capCoordinates[2][n3][n1], self._rimCoordinates[2][i][n3][n1]] + if i == -1: + nx.reverse() + nd2.reverse() + sd2 = smoothCubicHermiteDerivativesLine(nx, nd2) + self._rimCoordinates[2][i][n3][n1] = sd2[1] + + if self._transitionExtCoordinates is not None: + capCoordinates = self._transitionExtCoordinates[i] + for n3 in range(self._elementsCountTransition - 1): + for n1 in range(self._elementsCountAround): + nx = [capCoordinates[0][n3][n1], self._transitionCoordinates[0][i][n3][n1]] + nd2 = [capCoordinates[2][n3][n1], self._transitionCoordinates[2][i][n3][n1]] + if i == -1: + nx.reverse() + nd2.reverse() + sd2 = smoothCubicHermiteDerivativesLine(nx, nd2) + self._transitionCoordinates[2][i][n3][n1] = sd2[1] + @classmethod def blendSampledCoordinates(cls, segment1, nodeIndexAlong1, segment2, nodeIndexAlong2): nodesCountAround = segment1._elementsCountAround From f1dbf2ba076f012d65992669208763d2717f48b3 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 21 Jul 2025 10:44:32 +1200 Subject: [PATCH 16/43] Improve smoothing of cap shell layer --- src/scaffoldmaker/utils/capmesh.py | 60 +++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 1d3d76ed..546d6362 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -391,7 +391,7 @@ def _sampleCapCoordinatesWithCore(self, s): self._determineBoxDerivatives() self._remapCapCoordinates() - self._smoothDerivatives() + self._smoothDerivatives(ratio) def _createShellCoordinatesList(self): """ @@ -475,7 +475,7 @@ def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio): n1 = self._elementsCountAround * 3 // 4 ix = self._getTubeRimCoordinates(n1, idx, n3) for n in range(1, elementsCountAcrossMinor): - for nx in [0, 1]: + for nx in [0, 1]: # x coord and d1 derivatives vi = sub(ix[nx], centre) if nx == 0 else mult(ix[nx], -1) vi = div(vi, ratio[2]) vr = rotate_vector_around_vector(vi, refAxis, n * rotateAngle) @@ -628,6 +628,9 @@ def _sphereToSpheroid(self, n3, ratio, centre): layoutD3 = normalize(self._networkPathParameters[0][4][idx]) thetaD2 = angle(layoutD2, [0.0, 1.0, 0.0]) thetaD3 = angle(layoutD3, [0.0, 0.0, 1.0]) + # + # if layoutD2[0] < 0.0: + # thetaD2 *= -1.0 mCount = self._elementsCountCoreBoxMajor + 1 if self._isCore else 1 nCount = self._elementsCountCoreBoxMinor + 1 if self._isCore else self._elementsCountAround @@ -669,13 +672,12 @@ def _getTubeRadii(self, centre, n3, idx): n1m, n1n = 0, self._elementsCountAround // 4 ixm, ixn = (self._getTubeRimCoordinates(n, idx, n3)[0] for n in [n1m, n1n]) majorRadius, minorRadius = (magnitude(sub(coord, centre)) for coord in [ixm, ixn]) - # if majorRadius > minorRadius: - # xRadius = majorRadius / minorRadius - # elif majorRadius < minorRadius: - # xRadius = minorRadius / majorRadius - # else: - # xRadius = 1.0 - return [1.0, majorRadius, minorRadius] + xRadius = 1.0 + if majorRadius > minorRadius: + xRadius = math.cbrt(majorRadius / minorRadius) + elif majorRadius < minorRadius: + xRadius = math.cbrt(minorRadius / majorRadius) + return [xRadius, majorRadius, minorRadius] def _determineShellDerivatives(self): """ @@ -851,7 +853,7 @@ def _determineBoxDerivatives(self): d2 = mult(sub(otx, itx), signValue) self._boxCoordinates[idx][2][m][n] = d2 - def _smoothDerivatives(self): + def _smoothDerivatives(self, ratio): """ Smooths derivatives to eliminate zero Jacobian contours at the cap-tube joint. """ @@ -867,6 +869,7 @@ def _smoothDerivatives(self): shellCoordinates = self._shellCoordinates[idx] tubeBoxCoordinates = self._tubeBoxCoordinates boxBoundaryNodeToBoxId = self._createBoxBoundaryNodeIdsList() + midMajorIndex = self._elementsCountCoreBoxMajor // 2 # smooth derivatives along d2 for box for m in range(nodesCountCoreBoxMajor): @@ -878,6 +881,43 @@ def _smoothDerivatives(self): boxCoordinates[2][m][n] = sd2[0] boxExtCoordinates[2][m][n] = sd2[1] + # smooth derivatives along d2 for shell + if ratio != [1.0, 1.0, 1.0]: + for n3 in range(nloop): + for m in range(nodesCountCoreBoxMajor): + cx = [shellCoordinates[0][n3][m][n] for n in range(nodesCountCoreBoxMinor)] + cd2 = [shellCoordinates[2][n3][m][n] for n in range(nodesCountCoreBoxMinor)] + + start = -self._elementsCountCoreBoxMinor // 2 - m + end = self._elementsCountCoreBoxMinor // 2 + m + rimIndex = [start, end] if idx == 0 else [end, start] + rimExt = self._getRimExtCoordinatesAround(n3) + ex = [rimExt[0][n2] for n2 in rimIndex] + ed2 = [rimExt[2][n2] for n2 in rimIndex] + + if idx == -1: + cx.reverse() + cd2.reverse() + + nx = [ex[0]] + cx + [ex[1]] + if idx == 0: + nd2 = [mult(ed2[0], -1.0)] + cd2 + [ed2[1]] + else: + nd2 = [ed2[0]] + cd2 + [mult(ed2[1], -1.0)] + + if m == midMajorIndex: + sd2 = smoothCubicHermiteDerivativesLine(nx, nd2, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN)[1:-1] + else: + sd2 = smoothCubicHermiteDerivativesLine(nx, nd2, fixAllDirections=True, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN)[1:-1] + + if idx == -1: + sd2.reverse() + + for n in range(nodesCountCoreBoxMinor): + shellCoordinates[2][n3][m][n] = sd2[n] + # smooth derivatives along d3 for shell for m in range(nodesCountCoreBoxMajor): for n in range(nodesCountCoreBoxMinor): From a5d968dbc73019432618c1bc479fcd5b9670f5b7 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 21 Jul 2025 11:00:00 +1200 Subject: [PATCH 17/43] Fix mesh alignment between tube body and end caps --- src/scaffoldmaker/utils/capmesh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 546d6362..0bb6bdbe 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -628,9 +628,9 @@ def _sphereToSpheroid(self, n3, ratio, centre): layoutD3 = normalize(self._networkPathParameters[0][4][idx]) thetaD2 = angle(layoutD2, [0.0, 1.0, 0.0]) thetaD3 = angle(layoutD3, [0.0, 0.0, 1.0]) - # - # if layoutD2[0] < 0.0: - # thetaD2 *= -1.0 + + if layoutD2[0] < 0.0: + thetaD2 *= -1.0 mCount = self._elementsCountCoreBoxMajor + 1 if self._isCore else 1 nCount = self._elementsCountCoreBoxMinor + 1 if self._isCore else self._elementsCountAround From f3e197d24e1bd54796ce0637843a1ae3c361cff1 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 21 Jul 2025 15:13:10 +1200 Subject: [PATCH 18/43] Fix calculation of apex coordinates for cap without core --- src/scaffoldmaker/utils/capmesh.py | 19 +++++++++++++++---- src/scaffoldmaker/utils/tubenetworkmesh.py | 19 ++++++++++++++----- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 0bb6bdbe..efebabbc 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -632,13 +632,18 @@ def _sphereToSpheroid(self, n3, ratio, centre): if layoutD2[0] < 0.0: thetaD2 *= -1.0 - mCount = self._elementsCountCoreBoxMajor + 1 if self._isCore else 1 + mCount = self._elementsCountCoreBoxMajor + 1 if self._isCore else 2 nCount = self._elementsCountCoreBoxMinor + 1 if self._isCore else self._elementsCountAround # process shell coordinates for m in range(mCount): - mp = m if self._isCore else 1 for n in range(nCount): - btx = self._shellCoordinates[idx][0][n3][mp][n] + if not self._isCore and m == 0: + if n > 0: + continue + else: + btx = self._shellCoordinates[idx][0][n3][m] + else: + btx = self._shellCoordinates[idx][0][n3][m][n] btx = sub(btx, centre) # apply forward transformations for vec, theta in [(layoutD3, thetaD2), (layoutD2, thetaD3)]: @@ -649,7 +654,13 @@ def _sphereToSpheroid(self, n3, ratio, centre): for vec, theta in [(layoutD2, -thetaD3), (layoutD3, -thetaD2)]: btx = rotate_vector_around_vector(btx, vec, theta) # update shell coordinates - self._shellCoordinates[idx][0][n3][mp][n] = add(btx, centre) + if not self._isCore and m == 0: + if n > 0: + continue + else: + self._shellCoordinates[idx][0][n3][m] = add(btx, centre) + else: + self._shellCoordinates[idx][0][n3][m][n] = add(btx, centre) def _getRatioBetweenTwoRadii(self, radii, oRadii): """ diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index c6217fc3..ed008925 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -637,7 +637,7 @@ def sample(self, fixedElementsCountAlong, targetElementLength): # sample coordinates for the cap mesh at the ends of a tube segment self._boxExtCoordinates, self._transitionExtCoordinates, self.rimExtCoordinates = ( self._capMesh.sampleCoordinates(self._boxCoordinates, self._transitionCoordinates, self._rimCoordinates)) - + # if self._isCore: self._smoothD2DerivativesAtCapTubeJoint() def _sampleCoreCoordinates(self, elementsCountAlong): @@ -1159,10 +1159,7 @@ def _smoothD2DerivativesAtCapTubeJoint(self): """ Smooths D2 derivatives at the joint where the cap and the tube surfaces join to eliminate zero Jacobian contours. """ - for i in [0, -1]: - if not self._isCap[i]: - continue - + def smoothBoxDerivatives(): capCoordinates = self._boxExtCoordinates[i] for m in range(self._elementsCountCoreBoxMajor + 1): for n in range(self._elementsCountCoreBoxMinor + 1): @@ -1174,6 +1171,7 @@ def _smoothD2DerivativesAtCapTubeJoint(self): sd2 = smoothCubicHermiteDerivativesLine(nx, nd2) self._boxCoordinates[2][i][m][n] = sd2[1] + def smoothRimDerivatives(): capCoordinates = self.rimExtCoordinates[i] for n3 in range(self._elementsCountThroughShell + 1): for n1 in range(self._elementsCountAround): @@ -1197,6 +1195,17 @@ def _smoothD2DerivativesAtCapTubeJoint(self): sd2 = smoothCubicHermiteDerivativesLine(nx, nd2) self._transitionCoordinates[2][i][n3][n1] = sd2[1] + for i in [0, -1]: + if not self._isCap[i]: + continue + + if self._isCore: + smoothBoxDerivatives() + smoothRimDerivatives() + else: + smoothRimDerivatives() + + @classmethod def blendSampledCoordinates(cls, segment1, nodeIndexAlong1, segment2, nodeIndexAlong2): nodesCountAround = segment1._elementsCountAround From df3b121559ab21c537f9af4b877800f9ab3a900e Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 21 Jul 2025 15:48:04 +1200 Subject: [PATCH 19/43] Update unit tests for cap mesh and renal capsule scaffold --- tests/test_capmesh.py | 28 ++++++++++++++-------------- tests/test_renalcapsule.py | 14 +++++++------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/tests/test_capmesh.py b/tests/test_capmesh.py index f794454e..1bea2e0b 100644 --- a/tests/test_capmesh.py +++ b/tests/test_capmesh.py @@ -18,7 +18,7 @@ class CapScaffoldTestCase(unittest.TestCase): def test_3d_cap_tube_network_default(self): """ - Test default 3-D tube network with cap at both ends is generated correctly. + Test default 3-D tube network with cap at both ends without core is generated correctly. """ scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Default") settings = scaffoldPackage.getScaffoldSettings() @@ -88,9 +88,9 @@ def test_3d_cap_tube_network_default(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.01475819159418598, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 0.83785503770891, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 0.6527066474646166, delta=X_TOL) + self.assertAlmostEqual(volume, 0.014512692292910195, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 0.8242090308803719, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 0.6417898420017868, delta=X_TOL) def test_3d_cap_tube_network_default_core(self): """ @@ -156,8 +156,8 @@ def test_3d_cap_tube_network_default_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.03930782850767879, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 0.8285291049289928, delta=X_TOL) + self.assertAlmostEqual(volume, 0.03862588577889281, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 0.8148830981004554, delta=X_TOL) def test_3d_cap_tube_network_bifurcation(self): """ @@ -204,7 +204,7 @@ def test_3d_cap_tube_network_bifurcation(self): minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) assertAlmostEqualList(self, minimums, [-0.1500000000000000, -0.6246941344953365, -0.1000000000000000], X_TOL) - assertAlmostEqualList(self, maximums, [2.1433222518126906, 0.6276801844614515, 0.1000000000000000], X_TOL) + assertAlmostEqualList(self, maximums, [2.1433222518126906, 0.6246941344953366, 0.1000000000000000], X_TOL) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -231,9 +231,9 @@ def test_3d_cap_tube_network_bifurcation(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.04024808736450315, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 2.251027047006869, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 1.7914675669958757, delta=X_TOL) + self.assertAlmostEqual(volume, 0.03975060276203082, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.2227971026955844, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 1.7686359946575902, delta=X_TOL) def test_3d_tube_network_bifurcation_core(self): """ @@ -298,8 +298,8 @@ def test_3d_tube_network_bifurcation_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.11105681500696916, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.2359565854658427, delta=X_TOL) + self.assertAlmostEqual(volume, 0.1096893209534004, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.2086099452749344, delta=X_TOL) def test_3d_tube_network_converging_bifurcation_core(self): """ @@ -393,8 +393,8 @@ def test_3d_tube_network_converging_bifurcation_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.1106252097801163, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.233433190529325, delta=X_TOL) + self.assertAlmostEqual(volume, 0.10923734449979247, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.205681406661578, delta=X_TOL) if __name__ == "__main__": unittest.main() diff --git a/tests/test_renalcapsule.py b/tests/test_renalcapsule.py index 4c9f5c14..00a13675 100644 --- a/tests/test_renalcapsule.py +++ b/tests/test_renalcapsule.py @@ -79,15 +79,15 @@ def test_renalcapsule(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 4.844335733470136, delta=tol) - self.assertAlmostEqual(surfaceArea, 15.289023987470623, delta=tol) + self.assertAlmostEqual(volume, 4.8399524698282725, delta=tol) + self.assertAlmostEqual(surfaceArea, 15.277545756184905, delta=tol) # check some annotation groups: expectedSizes3d = { - "core": (176, 2.880428953529323), - "shell": (112, 1.9640545714444255), - "kidney capsule": (288, 4.844483524973759) + "core": (176, 2.8838746320298183), + "shell": (112, 1.9560431675458367), + "kidney capsule": (288, 4.839917799575645) } for name in expectedSizes3d: term = get_kidney_term(name) @@ -103,8 +103,8 @@ def test_renalcapsule(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "shell": (448, 37.8673195697525), - "kidney capsule": (920, 66.45790167672409) + "shell": (448, 37.84060276636123), + "kidney capsule": (920, 66.4453245855002) } for name in expectedSizes2d: term = get_kidney_term(name) From ea1a7210cfbb7a025368087fbfe4b1b638b947f4 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 21 Jul 2025 16:45:30 +1200 Subject: [PATCH 20/43] Replace math.cbrt with math.pow --- src/scaffoldmaker/utils/capmesh.py | 6 +++--- tests/test_capmesh.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index efebabbc..78cb2002 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -229,7 +229,7 @@ def _sampleCapCoordinatesWithoutCore(self): innerWidth = innerLength = outerRadius - shellThickness elementLengthRatioEquatorApex = 1.0 - lengthRatio = 1.0 + lengthRatio = 2.0 bOuter = 2.0 / (1.0 + elementLengthRatioEquatorApex / lengthRatio) aOuter = 1.0 - bOuter @@ -685,9 +685,9 @@ def _getTubeRadii(self, centre, n3, idx): majorRadius, minorRadius = (magnitude(sub(coord, centre)) for coord in [ixm, ixn]) xRadius = 1.0 if majorRadius > minorRadius: - xRadius = math.cbrt(majorRadius / minorRadius) + xRadius = math.pow((majorRadius / minorRadius), 1/3) elif majorRadius < minorRadius: - xRadius = math.cbrt(minorRadius / majorRadius) + xRadius = math.pow((minorRadius / majorRadius), 1/3) return [xRadius, majorRadius, minorRadius] def _determineShellDerivatives(self): diff --git a/tests/test_capmesh.py b/tests/test_capmesh.py index 1bea2e0b..acb5de02 100644 --- a/tests/test_capmesh.py +++ b/tests/test_capmesh.py @@ -88,9 +88,9 @@ def test_3d_cap_tube_network_default(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.014512692292910195, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 0.8242090308803719, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 0.6417898420017868, delta=X_TOL) + self.assertAlmostEqual(volume, 0.014526098773694766, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 0.8247195017487451, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 0.6421069263444478, delta=X_TOL) def test_3d_cap_tube_network_default_core(self): """ @@ -231,9 +231,9 @@ def test_3d_cap_tube_network_bifurcation(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.03975060276203082, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 2.2227971026955844, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 1.7686359946575902, delta=X_TOL) + self.assertAlmostEqual(volume, 0.039770715282355665, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.2235629407464303, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 1.7691117020959595, delta=X_TOL) def test_3d_tube_network_bifurcation_core(self): """ From 908d0f6a02181e1beed96ac5ef36e960c13b561f Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 18 Aug 2025 09:36:55 +1200 Subject: [PATCH 21/43] Resolve merge conflicts when merging main --- src/scaffoldmaker/scaffolds.py | 3 ++- src/scaffoldmaker/utils/networkmesh.py | 3 +-- src/scaffoldmaker/utils/tubenetworkmesh.py | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index d153e689..c8f6ce5f 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -44,7 +44,8 @@ from scaffoldmaker.meshtypes.meshtype_3d_nerve1 import MeshType_3d_nerve1 from scaffoldmaker.meshtypes.meshtype_3d_ostium1 import MeshType_3d_ostium1 from scaffoldmaker.meshtypes.meshtype_3d_ostium2 import MeshType_3d_ostium2 -from scaffoldmaker.meshtypes.meshtype_3d_renal_capsule1 import MeshType_3d_renal_capsule1, MeshType_1d_renal_capsule_network_layout1 +from scaffoldmaker.meshtypes.meshtype_3d_renal_capsule1 import MeshType_1d_renal_capsule_network_layout1, \ + MeshType_3d_renal_capsule1 from scaffoldmaker.meshtypes.meshtype_3d_smallintestine1 import MeshType_3d_smallintestine1 from scaffoldmaker.meshtypes.meshtype_3d_solidcylinder1 import MeshType_3d_solidcylinder1 from scaffoldmaker.meshtypes.meshtype_3d_solidsphere1 import MeshType_3d_solidsphere1 diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index ae616785..ccae9a2a 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -280,8 +280,7 @@ def build(self, structureString): sequenceNodes.append(networkNode) sequenceVersions.append(nodeVersion) if (len(sequenceNodes) > 1) and (existingNetworkNode or (nodeIdentifier == nodeIdentifiers[-1])): - networkSegment = NetworkSegment(sequenceNodes, sequenceVersions, isCap) - networkSegment = NetworkSegment(sequenceNodes, sequenceVersions, isPatch) + networkSegment = NetworkSegment(sequenceNodes, sequenceVersions, isCap, isPatch) self._networkSegments.append(networkSegment) sequenceNodes = sequenceNodes[-1:] sequenceVersions = sequenceVersions[-1:] diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 1a012bee..82d99ceb 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -4,6 +4,8 @@ from cmlibs.maths.vectorops import add, cross, dot, magnitude, mult, normalize, set_magnitude, sub, rejection from cmlibs.zinc.element import Element, Elementbasis from cmlibs.zinc.node import Node + +from scaffoldmaker.utils.capmesh import CapMesh from scaffoldmaker.utils.eft_utils import ( addTricubicHermiteSerendipityEftParameterScaling, determineCubicHermiteSerendipityEft, HermiteNodeLayoutManager) from scaffoldmaker.utils.interpolation import ( From 878594d5adaa1047a0fbcae260b6b040bc5e16fa Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 20 Aug 2025 13:19:46 +1200 Subject: [PATCH 22/43] Replace cap without core case to use cap with core without box elements --- src/scaffoldmaker/utils/capmesh.py | 924 +++++++-------------- src/scaffoldmaker/utils/tubenetworkmesh.py | 44 +- tests/test_capmesh.py | 20 +- 3 files changed, 347 insertions(+), 641 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 78cb2002..d7769e5d 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -9,9 +9,8 @@ from cmlibs.zinc.element import Element from cmlibs.zinc.node import Node from scaffoldmaker.utils.eft_utils import determineCubicHermiteSerendipityEft -from scaffoldmaker.utils.eftfactory_tricubichermite import eftfactory_tricubichermite -from scaffoldmaker.utils.interpolation import smoothCubicHermiteDerivativesLoop, smoothCubicHermiteDerivativesLine, \ - sampleCubicHermiteCurves, interpolateSampleCubicHermite, DerivativeScalingMode +from scaffoldmaker.utils.interpolation import smoothCubicHermiteDerivativesLine, sampleCubicHermiteCurves, \ + interpolateSampleCubicHermite, DerivativeScalingMode from scaffoldmaker.utils.spheremesh import calculate_arc_length, local_to_global_coordinates, spherical_to_cartesian, \ calculate_azimuth @@ -66,18 +65,15 @@ def __init__(self, elementsCountAround, elementsCountCoreBoxMajor, elementsCount self._boxCoordinates = None # list[startCap, endCap][[x, d1, d2, d3][nAcrossMajor][nAcrossMinor] self._shellCoordinates = None - # list[startCap, endCap][x, d1, d2, d3][nThroughWall][apex, rim][nAround] if the tube is without the solid core. - # list[startCap, endCap][[x, d1, d2, d3][nThroughWall][nAcrossMajor][nAcrossMinor] if the tube is with the core. + # list[startCap, endCap][[x, d1, d2, d3][nThroughWall][nAcrossMajor][nAcrossMinor]. self._startCapNodeIds = None # capNodeIds that form the cap at the start of a tube segment. - # list[nThroughWall][apex, rim] if the tube is without the core. - # list[nThroughWall][nAcrossMajor][nAcrossMinor] if the tube is with the core. + # list[nThroughWall][nAcrossMajor][nAcrossMinor]. self._endCapNodeIds = None # capNodeIds that form the cap at the end of a tube segment. # list structure is identical to startCapNodeIds. self._startCapElementIds = None # elementIds that form the cap at the start of a tube segment. - # list[nThroughWall][apex, rim] if the tube is without the core. # list[box, rim]: [box] and [rim] sublists have different structures. # [box][core, transition, shield][nAcrossMajor][nAcrossMinor] # [rim][base, shield][nAround] @@ -112,44 +108,42 @@ def _extendTubeEnds(self): boxCoordinates.append([]) transitionCoordinates.append([]) shellCoordinates.append([]) - if self._isCore: - for m in range(coreBoxMajorNodesCount): - boxCoordinates[nx].append([]) - for n in range(coreBoxMinorNodesCount): - boxCoordinates[nx][m].append([]) - if self._elementsCountTransition > 1: - for n3 in range(self._elementsCountTransition - 1): - transitionCoordinates[nx].append([]) + for m in range(coreBoxMajorNodesCount): + boxCoordinates[nx].append([]) + for n in range(coreBoxMinorNodesCount): + boxCoordinates[nx][m].append([]) + if self._elementsCountTransition > 1: + for n3 in range(self._elementsCountTransition - 1): + transitionCoordinates[nx].append([]) for n3 in range(self._elementsCountThroughShell + 1): shellCoordinates[nx].append([]) - if self._isCore: - for m in range(coreBoxMajorNodesCount): + for m in range(coreBoxMajorNodesCount): + xList, d2List = [], [] + x = self._tubeBoxCoordinates[0][idx][m] + for n in range(coreBoxMinorNodesCount): + tx = add(x[n], set_magnitude(unitVector, ext * signValue)) + td2 = mult(sub(tx, x[n]), signValue) + xList.append(tx) + d2List.append(td2) + boxCoordinates[0][m] = xList + boxCoordinates[1][m] = self._tubeBoxCoordinates[1][idx][m] + boxCoordinates[2][m] = d2List + boxCoordinates[3][m] = self._tubeBoxCoordinates[3][idx][m] + + if self._elementsCountTransition > 1: + for n3 in range(self._elementsCountTransition - 1): xList, d2List = [], [] - x = self._tubeBoxCoordinates[0][idx][m] - for n in range(coreBoxMinorNodesCount): - tx = add(x[n], set_magnitude(unitVector, ext * signValue)) - td2 = mult(sub(tx, x[n]), signValue) + x = self._tubeTransitionCoordinates[0][idx][n3] + for nx in range(self._elementsCountAround): + tx = add(x[nx], set_magnitude(unitVector, ext * signValue)) + td2 = mult(sub(tx, x[nx]), signValue) xList.append(tx) d2List.append(td2) - boxCoordinates[0][m] = xList - boxCoordinates[1][m] = self._tubeBoxCoordinates[1][idx][m] - boxCoordinates[2][m] = d2List - boxCoordinates[3][m] = self._tubeBoxCoordinates[3][idx][m] - - if self._elementsCountTransition > 1: - for n3 in range(self._elementsCountTransition - 1): - xList, d2List = [], [] - x = self._tubeTransitionCoordinates[0][idx][n3] - for nx in range(self._elementsCountAround): - tx = add(x[nx], set_magnitude(unitVector, ext * signValue)) - td2 = mult(sub(tx, x[nx]), signValue) - xList.append(tx) - d2List.append(td2) - transitionCoordinates[0][n3] = xList - transitionCoordinates[1][n3] = self._tubeTransitionCoordinates[1][idx][n3] - transitionCoordinates[2][n3] = d2List - transitionCoordinates[3][n3] = self._tubeTransitionCoordinates[3][idx][n3] + transitionCoordinates[0][n3] = xList + transitionCoordinates[1][n3] = self._tubeTransitionCoordinates[1][idx][n3] + transitionCoordinates[2][n3] = d2List + transitionCoordinates[3][n3] = self._tubeTransitionCoordinates[3][idx][n3] for n3 in range(self._elementsCountThroughShell + 1): xList, d2List = [], [] @@ -164,13 +158,12 @@ def _extendTubeEnds(self): shellCoordinates[2][n3] = d2List shellCoordinates[3][n3] = self._tubeShellCoordinates[3][idx][n3] - if self._isCore: - self._boxExtCoordinates = [None, None] if self._boxExtCoordinates is None else self._boxExtCoordinates - self._boxExtCoordinates[idx] = boxCoordinates - if self._elementsCountTransition > 1: - self._transitionExtCoordinates = [None, None] if self._transitionExtCoordinates is None \ - else self._transitionExtCoordinates - self._transitionExtCoordinates[idx] = transitionCoordinates + self._boxExtCoordinates = [None, None] if self._boxExtCoordinates is None else self._boxExtCoordinates + self._boxExtCoordinates[idx] = boxCoordinates + if self._elementsCountTransition > 1: + self._transitionExtCoordinates = [None, None] if self._transitionExtCoordinates is None \ + else self._transitionExtCoordinates + self._transitionExtCoordinates[idx] = transitionCoordinates self._shellExtCoordinates = [None, None] if self._shellExtCoordinates is None else self._shellExtCoordinates self._shellExtCoordinates[idx] = shellCoordinates @@ -191,186 +184,45 @@ def _remapCapCoordinates(self): coreBoxMinorNodesCount = self._getNodesCountCoreBoxMinor() nodesCountRim = self._getNodesCountRim() - if self._isCore: + for m in range(coreBoxMajorNodesCount): + xList = [] + x = self._boxCoordinates[idx][0][m] + for n in range(coreBoxMinorNodesCount): + tx = add(x[n], set_magnitude(unitVector, ext * signValue)) + xList.append(tx) + self._boxCoordinates[idx][0][m] = xList + for n3 in range(nodesCountRim): for m in range(coreBoxMajorNodesCount): xList = [] - x = self._boxCoordinates[idx][0][m] + x = self._shellCoordinates[idx][0][n3][m] for n in range(coreBoxMinorNodesCount): tx = add(x[n], set_magnitude(unitVector, ext * signValue)) xList.append(tx) - self._boxCoordinates[idx][0][m] = xList - for n3 in range(nodesCountRim): - for m in range(coreBoxMajorNodesCount): - xList = [] - x = self._shellCoordinates[idx][0][n3][m] - for n in range(coreBoxMinorNodesCount): - tx = add(x[n], set_magnitude(unitVector, ext * signValue)) - xList.append(tx) - self._shellCoordinates[idx][0][n3][m] = xList - - def _sampleCapCoordinatesWithoutCore(self): - """ - Calculates coordinates and derivatives for the cap elements. It first calculates the coordinates for the apex - nodes, and then calculates the coordinates for rim nodes on the shell surface. - Used when the solid core is inactive. - """ - self._shellCoordinates = [None, None] if self._shellCoordinates is None else self._shellCoordinates - - isStartCap = self._isStartCap - idx = 0 if isStartCap else -1 - signValue = 1 if isStartCap else -1 - pathParameters = self._networkPathParameters[idx] - - outerRadius = self._getOuterShellRadius() - shellThickness = self._getShellThickness() - ext = self._getExtensionLength() - centre = add(pathParameters[0][idx], set_magnitude(pathParameters[1][idx], ext * -signValue)) - outerWidth = outerLength = outerRadius - innerWidth = innerLength = outerRadius - shellThickness - - elementLengthRatioEquatorApex = 1.0 - lengthRatio = 2.0 + self._shellCoordinates[idx][0][n3][m] = xList - bOuter = 2.0 / (1.0 + elementLengthRatioEquatorApex / lengthRatio) - aOuter = 1.0 - bOuter - bInner = 2.0 / (1.0 + elementLengthRatioEquatorApex / lengthRatio) - aInner = 1.0 - bInner - - elementsCountUp = 2 - radiansPerElementAround = 2.0 * math.pi / self._elementsCountAround - positionOuterArray = [(0, 0)] * elementsCountUp - positionInnerArray = [(0, 0)] * elementsCountUp - radiansUpOuterArray = [0] * elementsCountUp - radiansUpInnerArray = [0] * elementsCountUp - vector2OuterArray = [(0, 0)] * elementsCountUp - vector2InnerArray = [(0, 0)] * elementsCountUp - - for n2 in range(2): - xi = n2 * 2 / (2 * elementsCountUp) - nxiOuter = aOuter * xi * xi + bOuter * xi - dnxiOuter = 2.0 * aOuter * xi + bOuter - radiansUpOuterArray[n2] = radiansUpOuter = nxiOuter * math.pi * 0.5 - dRadiansUpOuter = dnxiOuter * math.pi / (2 * elementsCountUp) - cosRadiansUpOuter = math.cos(radiansUpOuter) - sinRadiansUpOuter = math.sin(radiansUpOuter) - positionOuterArray[n2] = [outerWidth * sinRadiansUpOuter, -outerLength * cosRadiansUpOuter] - vector2OuterArray[n2] = (outerWidth * cosRadiansUpOuter * dRadiansUpOuter, - outerLength * sinRadiansUpOuter * dRadiansUpOuter) - - nxiInner = aInner * xi * xi + bInner * xi - dnxiInner = 2.0 * aInner * xi + bInner - radiansUpInnerArray[n2] = radiansUpInner = nxiInner * math.pi * 0.5 - dRadiansUpInner = dnxiInner * math.pi / (2 * elementsCountUp) - cosRadiansUpInner = math.cos(radiansUpInner) - sinRadiansUpInner = math.sin(radiansUpInner) - positionInnerArray[n2] = [innerWidth * sinRadiansUpInner, -innerLength * cosRadiansUpInner] - vector2InnerArray[n2] = (innerWidth * cosRadiansUpInner * dRadiansUpInner, - innerLength * sinRadiansUpInner * dRadiansUpInner) - - xList, d1List, d2List, d3List, rList = [], [], [], [], [] - elementsCountThroughShell = self._elementsCountThroughShell - for n3 in range(elementsCountThroughShell + 1): - for lst in [xList, d1List, d2List, d3List]: - lst.append([]) - for n2 in range(2): - n3_fraction = n3 / elementsCountThroughShell - positionOuter = positionOuterArray[n2] - positionInner = positionInnerArray[n2] - position = [positionOuter[0] * n3_fraction + positionInner[0] * (1.0 - n3_fraction), - positionOuter[1] * n3_fraction + positionInner[1] * (1.0 - n3_fraction)] - vector2Outer = vector2OuterArray[n2] - vector2Inner = vector2InnerArray[n2] - vector2 = [vector2Outer[0] * n3_fraction + vector2Inner[0] * (1.0 - n3_fraction), - vector2Outer[1] * n3_fraction + vector2Inner[1] * (1.0 - n3_fraction)] - vector3 = [(positionOuter[0] - positionInner[0]) / elementsCountThroughShell, - (positionOuter[1] - positionInner[1]) / elementsCountThroughShell] - # calculate coordinates - if n2 == 0: # apex - x = apex = add(pathParameters[0][idx], - set_magnitude(pathParameters[1][idx], (position[1] - ext) * signValue)) - d1 = set_magnitude(pathParameters[4][idx], vector2[0] * signValue) - d2 = set_magnitude(pathParameters[2][idx], vector2[0]) - d3 = set_magnitude(pathParameters[1][idx], vector3[1] * signValue) - for lst, value in zip([xList, d1List, d2List, d3List], [x, d1, d2, d3]): - lst[-1].append(value) - else: - refAxis = normalize(pathParameters[4][idx]) - rotateAngle = n2 * (math.pi / 2) / elementsCountUp * signValue - radius = innerLength + (outerLength - innerLength) * n3 / elementsCountThroughShell - rList.append(radius) - tx = rotate_vector_around_vector(normalize(sub(apex, centre)), refAxis, -rotateAngle) - tx = set_magnitude(tx, radius) - for n1 in range(self._elementsCountAround): - radiansAround = n1 * radiansPerElementAround - cosRadiansAround = math.cos(radiansAround) - sinRadiansAround = math.sin(radiansAround) - - rx = rotate_vector_around_vector(tx, pathParameters[1][idx], radiansAround) - rx = add(rx, centre) - d1 = [0.0, position[0] * -sinRadiansAround * radiansPerElementAround * signValue, - position[0] * cosRadiansAround * radiansPerElementAround * signValue] - d2 = [vector2[1], vector2[0] * cosRadiansAround, - vector2[0] * sinRadiansAround] - d3 = [vector3[1], vector3[0] * cosRadiansAround * signValue, - vector3[0] * sinRadiansAround * signValue] - for lst, value in zip([xList, d1List, d2List, d3List], [rx, d1, d2, d3]): - lst[-1].append(value) - - xCoordinates = [[] for _ in range(4)] - for n, value in zip(range(4), [xList, d1List, d2List, d3List]): - for n3 in range(self._elementsCountThroughShell + 1): - xCoordinates[n].append([]) - xCoordinates[n][n3] = [value[n3][0], value[n3][1:]] - self._shellCoordinates[idx] = xCoordinates - - # transform sphere to spheroid - for n3 in range(elementsCountThroughShell + 1): - radii = self._getTubeRadii(centre, n3, idx) - oRadii = [1.0, rList[n3], rList[n3]] - ratio = self._getRatioBetweenTwoRadii(radii, oRadii) - self._sphereToSpheroid(n3, ratio, centre) - # smooth derivatives - for n3 in range(elementsCountThroughShell + 1): - xList = self._shellCoordinates[idx][0] - d1List = self._shellCoordinates[idx][1] - sd1 = smoothCubicHermiteDerivativesLoop(xList[n3][1], d1List[n3][1]) - d1List[n3][1] = sd1 - for n1 in range(self._elementsCountAround): - radiansAround = n1 * radiansPerElementAround - x = xList[n3][1][n1] - xStart, xEnd = xList[n3][0], self._shellExtCoordinates[idx][0][n3][n1] - nx = [xStart, x, xEnd] if isStartCap else [xEnd, x, xStart] - d2List = self._shellCoordinates[idx][2] - d2Start = rotate_vector_around_vector(d2List[n3][0], pathParameters[1][idx], radiansAround) - d2Start = set_magnitude(d2Start, magnitude(d2List[n3][0]) * signValue) - d2End = set_magnitude(self._shellExtCoordinates[idx][2][n3][n1], - magnitude(self._shellExtCoordinates[idx][1][n3][n1])) - d2 = d2List[n3][1][n1] - nd = [d2Start, d2, d2End] if isStartCap else [d2End, d2, d2Start] - sd2 = smoothCubicHermiteDerivativesLine(nx, nd, fixStartDerivative=True, fixEndDerivative=True) - d2List[n3][1][n1] = sd2[1] - for n1 in range(self._elementsCountAround): - xList = self._shellCoordinates[idx][0] - d3List = self._shellCoordinates[idx][3] - nx = [xList[n3][1][n1] for n3 in range(elementsCountThroughShell + 1)] - nd = [d3List[n3][1][n1] for n3 in range(elementsCountThroughShell + 1)] - sd3 = smoothCubicHermiteDerivativesLine(nx, nd) - for n3 in range(elementsCountThroughShell + 1): - d3List[n3][1][n1] = sd3[n3] - - def _sampleCapCoordinatesWithCore(self, s): + def _sampleCapCoordinates(self, s): """ Blackbox function for calculating coordinates and derivatives for the cap elements. It first calculates the coordinates for shell nodes, then calculates for box nodes. nodes, and then calculates the coordinates for rim nodes on the shell surface. - Used when the solid core is active. :param s: Index for isCap list. 0 indicates start cap and 1 indicates end cap. """ + + def _getRatioBetweenTwoRadii(rList, oList): + """ + Calculates the ratio between the original radius of a sphere and the new radius of an ellipsoid. + :param rList: List of new radius in each direction. + :param oList: List of original radius in each direction. + :return: List of ratio between two radii in x, y, and z-direction. + """ + return [rList[c] / oList[c] for c in range(3)] + self._isStartCap = isStartCap = True if self._isCap[0] and s == 0 else False idx = 0 if isStartCap else -1 centre = self._networkPathParameters[0][0][idx] self._extendTubeEnds() # extend tube end + # shell nodes nodesCountRim = self._getNodesCountRim() for n3 in range(nodesCountRim): @@ -378,13 +230,14 @@ def _sampleCapCoordinatesWithCore(self, s): radius = self._getRadius(ox) radii = self._getTubeRadii(centre, n3, idx) # radii for spheroid oRadii = [1.0, radius, radius] # original radii used to create the sphere - ratio = self._getRatioBetweenTwoRadii(radii, oRadii) + ratio = _getRatioBetweenTwoRadii(radii, oRadii) # ratio between original radii for the sphere and the new radii for spheroid self._calculateMajorAndMinorNodesCoordinates(n3, centre, ratio) self._calculateShellQuadruplePoints(n3, centre, radius) self._calculateShellRegularNodeCoordinates(n3, centre) self._sphereToSpheroid(n3, ratio, centre) self._determineShellDerivatives() + # box nodes self._calculateBoxQuadruplePoints(centre) self._calculateBoxMajorAndMinorNodes() @@ -433,17 +286,6 @@ def _getRadius(self, ox): range(self._elementsCountAround // 2)] return sum(radii) / len(radii) - def _getShellThickness(self): - """ - Calculates the thickness of a shell, based on the thickness of a tube segment at either ends. - It takes the average of a distance between the outer and the inner node pair around the rim of a tube segment. - :return: Thickness of the cap shell. - """ - ix = self._tubeShellCoordinates[0][0][0] if self._isStartCap else self._tubeShellCoordinates[0][-1][0] - ox = self._tubeShellCoordinates[0][0][-1] if self._isStartCap else self._tubeShellCoordinates[0][-1][-1] - shellThicknesses = [magnitude(sub(ox[i], ix[i])) for i in range(self._elementsCountAround)] - return sum(shellThicknesses) / len(shellThicknesses) - def _getExtensionLength(self): """ Calculates the length of extended tube segment. Currently set to half of the outer tube radius. @@ -475,7 +317,7 @@ def _calculateMajorAndMinorNodesCoordinates(self, n3, centre, ratio): n1 = self._elementsCountAround * 3 // 4 ix = self._getTubeRimCoordinates(n1, idx, n3) for n in range(1, elementsCountAcrossMinor): - for nx in [0, 1]: # x coord and d1 derivatives + for nx in [0, 1]: # x coord and d1 derivatives vi = sub(ix[nx], centre) if nx == 0 else mult(ix[nx], -1) vi = div(vi, ratio[2]) vr = rotate_vector_around_vector(vi, refAxis, n * rotateAngle) @@ -586,8 +428,8 @@ def _calculateShellRegularNodeCoordinates(self, n3, centre): x2 = self._shellCoordinates[idx][0][n3][m][midMinorIndex] if x1 is None: continue - nx, nd1 = (self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) if n == 0 else - self._sampleCurvesOnSphere(x2, x1, centre, elementsOut)) + nx, nd1 = (sampleCurvesOnSphere(x1, x2, centre, elementsOut) if n == 0 else + sampleCurvesOnSphere(x2, x1, centre, elementsOut)) start, end = ( (n + 1, midMinorIndex) if n == 0 else (midMinorIndex + 1, self._elementsCountCoreBoxMinor)) for c in range(start, end): @@ -603,8 +445,8 @@ def _calculateShellRegularNodeCoordinates(self, n3, centre): x2 = self._shellCoordinates[idx][0][n3][midMajorIndex][n] if x1 is None: continue - nx, nd2 = (self._sampleCurvesOnSphere(x1, x2, centre, elementsOut) if m == 0 else - self._sampleCurvesOnSphere(x2, x1, centre, elementsOut)) + nx, nd2 = (sampleCurvesOnSphere(x1, x2, centre, elementsOut) if m == 0 else + sampleCurvesOnSphere(x2, x1, centre, elementsOut)) start, end = ( (m + 1, midMajorIndex) if m == 0 else (midMajorIndex + 1, self._elementsCountCoreBoxMajor)) for c in range(start, end): @@ -632,18 +474,12 @@ def _sphereToSpheroid(self, n3, ratio, centre): if layoutD2[0] < 0.0: thetaD2 *= -1.0 - mCount = self._elementsCountCoreBoxMajor + 1 if self._isCore else 2 - nCount = self._elementsCountCoreBoxMinor + 1 if self._isCore else self._elementsCountAround + mCount = self._elementsCountCoreBoxMajor + 1 + nCount = self._elementsCountCoreBoxMinor + 1 # process shell coordinates for m in range(mCount): for n in range(nCount): - if not self._isCore and m == 0: - if n > 0: - continue - else: - btx = self._shellCoordinates[idx][0][n3][m] - else: - btx = self._shellCoordinates[idx][0][n3][m][n] + btx = self._shellCoordinates[idx][0][n3][m][n] btx = sub(btx, centre) # apply forward transformations for vec, theta in [(layoutD3, thetaD2), (layoutD2, thetaD3)]: @@ -653,23 +489,7 @@ def _sphereToSpheroid(self, n3, ratio, centre): # apply inverse transformations for vec, theta in [(layoutD2, -thetaD3), (layoutD3, -thetaD2)]: btx = rotate_vector_around_vector(btx, vec, theta) - # update shell coordinates - if not self._isCore and m == 0: - if n > 0: - continue - else: - self._shellCoordinates[idx][0][n3][m] = add(btx, centre) - else: - self._shellCoordinates[idx][0][n3][m][n] = add(btx, centre) - - def _getRatioBetweenTwoRadii(self, radii, oRadii): - """ - Calculates the ratio between the original radius of a sphere and the new radius of an ellipsoid. - :param radii: List of new radius in each direction. - :param oRadii: List of original radius in each direction. - :return: List of ratio between two radii in x, y, and z-direction. - """ - return [radii[c] / oRadii[c] for c in range(3)] + self._shellCoordinates[idx][0][n3][m][n] = add(btx, centre) def _getTubeRadii(self, centre, n3, idx): """ @@ -685,9 +505,9 @@ def _getTubeRadii(self, centre, n3, idx): majorRadius, minorRadius = (magnitude(sub(coord, centre)) for coord in [ixm, ixn]) xRadius = 1.0 if majorRadius > minorRadius: - xRadius = math.pow((majorRadius / minorRadius), 1/3) + xRadius = math.pow((majorRadius / minorRadius), 1 / 3) elif majorRadius < minorRadius: - xRadius = math.pow((minorRadius / majorRadius), 1/3) + xRadius = math.pow((minorRadius / majorRadius), 1 / 3) return [xRadius, majorRadius, minorRadius] def _determineShellDerivatives(self): @@ -786,7 +606,8 @@ def _calculateBoxMajorAndMinorNodes(self): nx = [self._boxCoordinates[idx][0][m][n] for n in [0, -1]] nd1 = [self._boxCoordinates[idx][1][m][n] for n in [0, -1]] nd3 = [self._boxCoordinates[idx][3][m][n] for n in [0, -1]] - tx, td3, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd3, self._elementsCountCoreBoxMinor, arcLengthDerivatives=True) + tx, td3, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd3, self._elementsCountCoreBoxMinor, + arcLengthDerivatives=True) td1 = interpolateSampleCubicHermite(nd1, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] for n in range(1, self._elementsCountCoreBoxMinor): self._boxCoordinates[idx][0][m][n] = tx[n] @@ -796,7 +617,8 @@ def _calculateBoxMajorAndMinorNodes(self): nx = [self._boxCoordinates[idx][0][m][n] for m in [0, -1]] nd1 = [self._boxCoordinates[idx][1][m][n] for m in [0, -1]] nd3 = [self._boxCoordinates[idx][3][m][n] for m in [0, -1]] - tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, arcLengthDerivatives=True) + tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, + arcLengthDerivatives=True) td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] for m in range(1, self._elementsCountCoreBoxMajor): self._boxCoordinates[idx][0][m][n] = tx[m] @@ -807,7 +629,8 @@ def _calculateBoxMajorAndMinorNodes(self): nx = [self._boxCoordinates[idx][0][midMajorIndex][n] for n in [0, -1]] nd1 = [self._boxCoordinates[idx][1][midMajorIndex][n] for n in [0, -1]] nd3 = [self._boxCoordinates[idx][3][midMajorIndex][n] for n in [0, -1]] - tx, td3, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd3, self._elementsCountCoreBoxMinor, arcLengthDerivatives=True) + tx, td3, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd3, self._elementsCountCoreBoxMinor, + arcLengthDerivatives=True) td1 = interpolateSampleCubicHermite(nd1, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] for n in range(1, self._elementsCountCoreBoxMinor): self._boxCoordinates[idx][0][midMajorIndex][n] = tx[n] @@ -816,7 +639,8 @@ def _calculateBoxMajorAndMinorNodes(self): nx = [self._boxCoordinates[idx][0][m][midMinorIndex] for m in [0, -1]] nd1 = [self._boxCoordinates[idx][1][m][midMinorIndex] for m in [0, -1]] - tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, arcLengthDerivatives=True) + tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, + arcLengthDerivatives=True) td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 2, pe, pxi, psf)[0] for m in range(1, self._elementsCountCoreBoxMajor): self._boxCoordinates[idx][0][m][midMinorIndex] = tx[m] @@ -830,7 +654,8 @@ def _calculateBoxMajorAndMinorNodes(self): nx = [self._boxCoordinates[idx][0][i][n] for i in [0, midMajorIndex, -1]] nd1 = [self._boxCoordinates[idx][1][i][n] for i in [0, midMajorIndex, -1]] nd3 = [self._boxCoordinates[idx][3][i][n] for i in [0, midMajorIndex, -1]] - tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, arcLengthDerivatives=True) + tx, td1, pe, pxi, psf = sampleCubicHermiteCurves(nx, nd1, self._elementsCountCoreBoxMajor, + arcLengthDerivatives=True) td3 = interpolateSampleCubicHermite(nd3, [[0.0, 0.0, 0.0]] * 3, pe, pxi, psf)[0] for mi in range(1, self._elementsCountCoreBoxMajor): self._boxCoordinates[idx][0][mi][n] = tx[mi] @@ -881,7 +706,6 @@ def _smoothDerivatives(self, ratio): tubeBoxCoordinates = self._tubeBoxCoordinates boxBoundaryNodeToBoxId = self._createBoxBoundaryNodeIdsList() midMajorIndex = self._elementsCountCoreBoxMajor // 2 - # smooth derivatives along d2 for box for m in range(nodesCountCoreBoxMajor): for n in range(nodesCountCoreBoxMinor): @@ -918,10 +742,12 @@ def _smoothDerivatives(self, ratio): if m == midMajorIndex: sd2 = smoothCubicHermiteDerivativesLine(nx, nd2, - magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN)[1:-1] + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN)[ + 1:-1] else: sd2 = smoothCubicHermiteDerivativesLine(nx, nd2, fixAllDirections=True, - magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN)[1:-1] + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN)[ + 1:-1] if idx == -1: sd2.reverse() @@ -930,16 +756,17 @@ def _smoothDerivatives(self, ratio): shellCoordinates[2][n3][m][n] = sd2[n] # smooth derivatives along d3 for shell - for m in range(nodesCountCoreBoxMajor): - for n in range(nodesCountCoreBoxMinor): - bx = boxCoordinates[0][m][n] - bd3 = sub(shellCoordinates[0][0][m][n], bx) - nx = [bx] + [shellCoordinates[0][n3][m][n] for n3 in range(nloop)] - nd3 = [bd3] + [shellCoordinates[3][n3][m][n] for n3 in range(nloop)] - sd3 = smoothCubicHermiteDerivativesLine(nx, nd3, fixAllDirections=True, - magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - for n3 in range(nloop): - shellCoordinates[3][n3][m][n] = sd3[n3 + 1] + if self._isCore: + for m in range(nodesCountCoreBoxMajor): + for n in range(nodesCountCoreBoxMinor): + bx = boxCoordinates[0][m][n] + bd3 = sub(shellCoordinates[0][0][m][n], bx) + nx = [bx] + [shellCoordinates[0][n3][m][n] for n3 in range(nloop)] + nd3 = [bd3] + [shellCoordinates[3][n3][m][n] for n3 in range(nloop)] + sd3 = smoothCubicHermiteDerivativesLine(nx, nd3, fixAllDirections=True, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) + for n3 in range(nloop): + shellCoordinates[3][n3][m][n] = sd3[n3 + 1] # smooth transition/shell ext coordinates along d2 for n2 in range(self._elementsCountAround): @@ -1247,86 +1074,15 @@ def _getNodesCountCoreBoxMinor(self): return len(self._boxCoordinates[idx][0][0]) def _getNodesCountRim(self): - nodesCountRim = self._elementsCountThroughShell + self._elementsCountTransition - return nodesCountRim + return self._elementsCountThroughShell + self._elementsCountTransition def _getElementsCountRim(self): - elementsCountRim = max(1, self._elementsCountThroughShell) - if self._isCore: - elementsCountRim += self._elementsCountTransition + elementsCountRim = max(1, self._elementsCountThroughShell) + self._elementsCountTransition return elementsCountRim - def _sampleCurvesOnSphere(self, x1, x2, origin, elementsOut): - """ - Sample coordinates and d1 derivatives of - :param x1, x2: Coordinates of points 1 and 2 on the spherical surface of cap mesh. - :param origin: Centre point coordinates. - :param elementsOut: The number of elements required between points 1 and 2. - :return: Lists of sampled x and d1 between points 1 and 2. - """ - r1, r2 = sub(x1, origin), sub(x2, origin) - deltax = sub(r2, r1) - normal = cross(r1, deltax) - theta = angle(r1, r2) - anglePerElement = theta / elementsOut - arcLengthPerElement = calculate_arc_length(x1, x2, origin) / elementsOut - - nx, nd1 = [], [] - for n1 in range(elementsOut + 1): - radiansAcross = n1 * anglePerElement - r = rotate_vector_around_vector(r1, normal, radiansAcross) - nx.append(add(r, origin)) - nd1.append(set_magnitude(cross(normal, r), arcLengthPerElement)) - - return nx, nd1 - - def _generateNodesWithoutCore(self): + def _generateNodes(self): """ - Blackbox function for generating cap nodes. Used only when the tube segment does not have a core. - """ - generateData = self._generateData - coordinates = generateData.getCoordinates() - fieldcache = generateData.getFieldcache() - nodes = generateData.getNodes() - nodetemplate = generateData.getNodetemplate() - - nodesCountShell = len(self._tubeShellCoordinates[0][0]) - capNodeIds = [] - idx = 0 if self._isStartCap else -1 - for n2 in range(2): - capNodeIds.append([]) - for n3 in range(nodesCountShell): - capNodeIds[n2].append([]) - if n2 == 0: # apex - rx, rd1, rd2, rd3 = (self._shellCoordinates[idx][i][n3][n2] for i in range(4)) - nodeIdentifier = generateData.nextNodeIdentifier() - node = nodes.createNode(nodeIdentifier, nodetemplate) - fieldcache.setNode(node) - for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, - Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], - [rx, rd1, rd2, rd3]): - coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) - capNodeIds[n2][n3].append(nodeIdentifier) - else: - for n1 in range(self._elementsCountAround): - rx, rd1, rd2, rd3 = (self._shellCoordinates[idx][i][n3][n2][n1] for i in range(4)) - nodeIdentifier = generateData.nextNodeIdentifier() - node = nodes.createNode(nodeIdentifier, nodetemplate) - fieldcache.setNode(node) - for nodeValue, rValue in zip([Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, - Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3], - [rx, rd1, rd2, rd3]): - coordinates.setNodeParameters(fieldcache, -1, nodeValue, 1, rValue) - capNodeIds[n2][n3].append(nodeIdentifier) - - if self._isStartCap: - self._startCapNodeIds = capNodeIds - else: - self._endCapNodeIds = capNodeIds - - def _generateNodesWithCore(self): - """ - Blackbox function for generating cap nodes. Used only when the tube segment has a core. + Blackbox function for generating cap nodes. """ generateData = self._generateData coordinates = generateData.getCoordinates() @@ -1342,6 +1098,9 @@ def _generateNodesWithCore(self): for n3 in range(nloop): if n3 == 0: capNodeIds.append([]) + if not self._isCore: + continue + for m in range(nodesCountCoreBoxMajor): capNodeIds[n3].append([]) for n in range(nodesCountCoreBoxMinor): @@ -1414,8 +1173,10 @@ def _generateExtendedTubeNodes(self): n3p = n3 - (elementsCountTransition - 1) tx = self._transitionExtCoordinates[idx] if self._isCore and elementsCountTransition > 1 and n3 < ( elementsCountTransition - 1) else self._shellExtCoordinates[idx] - rx, rd1, rd2, rd3 = [tx[i][n3 if self._isCore and elementsCountTransition > 1 and n3 < ( - elementsCountTransition - 1) else n3p] for i in range(4)] + rx, rd1, rd2, rd3 = [ + tx[i][n3 if elementsCountTransition > 1 and n3 < (elementsCountTransition - 1) else n3p] for i in + range(4)] + ringNodeIds = [] for n1 in range(self._elementsCountAround): nodeIdentifier = generateData.nextNodeIdentifier() @@ -1428,93 +1189,7 @@ def _generateExtendedTubeNodes(self): ringNodeIds.append(nodeIdentifier) self._rimExtNodeIds[idx].append(ringNodeIds) - def _generateElementsWithoutCore(self, elementsCountRim, annotationMeshGroups): - """ - Blackbox function for generating cap elements. Used only when the tube segment does not have a core. - :param elementsCountRim: Number of elements through the rim. - :param annotationMeshGroups: List of all annotated mesh groups. - """ - generateData = self._generateData - coordinates = generateData.getCoordinates() - mesh = generateData.getMesh() - elementtemplateStd, eftStd = generateData.getStandardElementtemplate() - eftfactory = eftfactory_tricubichermite(mesh, False) - isStartCap = self._isStartCap - - if isStartCap: - capNodeIds = self._startCapNodeIds - self._startCapElementIds = [] if self._startCapElementIds is None else self._startCapElementIds - else: - capNodeIds = self._endCapNodeIds - self._endCapElementIds = [] if self._endCapElementIds is None else self._endCapElementIds - capElementIds = [] - for e3 in range(elementsCountRim): - capElementIds.append([]) - for e2 in range(2): - capElementIds[e3].append([]) - if e2 == 0: - for e1 in range(self._elementsCountAround): - e1p = (e1 + 1) % self._elementsCountAround - nids = [] - for n3 in [e3, e3 + 1]: - if isStartCap: - nids += [capNodeIds[0][n3][0], capNodeIds[1][n3][e1], capNodeIds[1][n3][e1p]] - else: - nids += [capNodeIds[1][n3][e1], capNodeIds[1][n3][e1p], capNodeIds[0][n3][0]] - elementIdentifier = generateData.nextElementIdentifier() - va, vb = e1, (e1 + 1) % self._elementsCountAround - if isStartCap: - eftCap = eftfactory.createEftShellPoleBottom(va * 100, vb * 100) - else: - eftCap = eftfactory.createEftShellPoleTop(va * 100, vb * 100) - elementtemplateCap = mesh.createElementtemplate() - elementtemplateCap.setElementShapeType(Element.SHAPE_TYPE_CUBE) - elementtemplateCap.defineField(coordinates, -1, eftCap) - element = mesh.createElement(elementIdentifier, elementtemplateCap) - element.setNodesByIdentifier(eftCap, nids) - - # set general linear map coefficients - radiansPerElementAround = math.pi * 2.0 / self._elementsCountAround - radiansAround = e1 * radiansPerElementAround if isStartCap else math.pi + e1 * radiansPerElementAround - radiansAroundNext = ((e1 + 1) % radiansPerElementAround) * radiansPerElementAround if isStartCap \ - else math.pi + ((e1 + 1) % self._elementsCountAround) * radiansPerElementAround - scalefactors = [ - 1.0, - math.sin(radiansAround), math.cos(radiansAround), radiansPerElementAround, - math.sin(radiansAroundNext), math.cos(radiansAroundNext), radiansPerElementAround, - math.sin(radiansAround), math.cos(radiansAround), radiansPerElementAround, - math.sin(radiansAroundNext), math.cos(radiansAroundNext), radiansPerElementAround - ] - if not isStartCap: - for s in [0, 1, 4, 7, 10]: - scalefactors[s] *= -1 - element.setScaleFactors(eftCap, scalefactors) - for annotationMeshGroup in annotationMeshGroups: - annotationMeshGroup.addElement(element) - capElementIds[e3][e2].append(elementIdentifier) - else: - idx = 0 if isStartCap else -1 - for e1 in range(self._elementsCountAround): - e1p = (e1 + 1) % self._elementsCountAround - nids = [] - for n3 in [e3, e3 + 1]: - nids += [capNodeIds[e2][n3][e1], capNodeIds[e2][n3][e1p], - self._rimExtNodeIds[idx][n3][e1], self._rimExtNodeIds[idx][n3][e1p]] - if not isStartCap: - for a in [nids]: - a[-4], a[-2] = a[-2], a[-4] - a[-3], a[-1] = a[-1], a[-3] - elementIdentifier = generateData.nextElementIdentifier() - element = mesh.createElement(elementIdentifier, elementtemplateStd) - element.setNodesByIdentifier(eftStd, nids) - capElementIds[e3][e2].append(elementIdentifier) - - if isStartCap: - self._startCapElementIds = capElementIds - else: - self._endCapElementIds = capElementIds - - def _generateElementsWithCore(self, annotationMeshGroups): + def _generateElements(self, annotationMeshGroups): """ Blackbox function for generating cap elements. Used only when the tube segment has a core. """ @@ -1548,6 +1223,9 @@ def _generateElementsWithCore(self, annotationMeshGroups): rimBoundaryNodeIds, rimBoundaryNodeToCapIndex = [], [] for n2 in range(elementsCountRim + 1): if n2 == 0: + if not self._isCore: + continue + boxBoundaryNodeIds.append(self._createBoundaryNodeIdsList(capNodeIds[n2])[0]) boxBoundaryNodeToCapIndex.append(self._createBoundaryNodeIdsList(capNodeIds[n2])[1]) else: @@ -1555,71 +1233,73 @@ def _generateElementsWithCore(self, annotationMeshGroups): rimBoundaryNodeToCapIndex.append(self._createBoundaryNodeIdsList(capNodeIds[n2])[1]) # box & shield - # box - boxElementIds = [] - for e3 in range(elementsCountCoreBoxMajor): - boxElementIds.append([]) - e3p = e3 + 1 - for e1 in range(elementsCountCoreBoxMinor): - nids, nodeParameters, nodeLayouts = [], [], [] - for n1 in [e1, e1 + 1]: - nids += [capNodeIds[0][e3][n1], capNodeIds[0][e3p][n1], - boxExtNodeIds[e3][n1], boxExtNodeIds[e3p][n1]] - if not isStartCap: - for a in [nids]: - a[-4], a[-2] = a[-2], a[-4] - a[-3], a[-1] = a[-1], a[-3] - elementIdentifier = generateData.nextElementIdentifier() - element = mesh.createElement(elementIdentifier, elementtemplateStd) - element.setNodesByIdentifier(eftStd, nids) - for annotationMeshGroup in annotationMeshGroups: - annotationMeshGroup.addElement(element) - boxElementIds[e3].append(elementIdentifier) - capElementIds.append(boxElementIds) + if self._isCore: + # box + boxElementIds = [] + for e3 in range(elementsCountCoreBoxMajor): + boxElementIds.append([]) + e3p = e3 + 1 + for e1 in range(elementsCountCoreBoxMinor): + nids, nodeParameters, nodeLayouts = [], [], [] + for n1 in [e1, e1 + 1]: + nids += [capNodeIds[0][e3][n1], capNodeIds[0][e3p][n1], + boxExtNodeIds[e3][n1], boxExtNodeIds[e3p][n1]] + if not isStartCap: + for a in [nids]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplateStd) + element.setNodesByIdentifier(eftStd, nids) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + boxElementIds[e3].append(elementIdentifier) + capElementIds.append(boxElementIds) - # box shield elements (elements joining the box and the shell elements) - boxshieldElementIds = [] - for e3 in range(elementsCountCoreBoxMajor): - boxshieldElementIds.append([]) - e3p = e3 + 1 - for e1 in range(elementsCountCoreBoxMinor): - nids, nodeParameters, nodeLayouts = [], [], [] - elementIdentifier = generateData.nextElementIdentifier() - for n1 in [e1, e1 + 1]: - for n3 in [e3, e3p]: - nids += [capNodeIds[1][n3][n1]] - nodeParameter = self._getRimCoordinatesWithCore(n3, n1, 0) - nodeParameters.append(nodeParameter) - nodeLayouts.append(nodeLayoutCapTransition) - for n3 in [e3, e3p]: - boxLocation, tpLocation = self._getBoxBoundaryLocation(n3, n1) - nid = capNodeIds[0][n3][n1] - nids += [nid] - nodeParameter = self._getBoxCoordinates(n3, n1) - nodeParameters.append(nodeParameter) - nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(boxLocation, isStartCap) - nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation, isStartCap) - if nid in boxBoundaryNodeIds[0]: - nodeLayouts.append( - nodeLayoutCapBoxShield if tpLocation == 0 else nodeLayoutCapBoxShieldTriplePoint) - else: - nodeLayouts.append(None) - if not isStartCap: - for a in [nids, nodeParameters, nodeLayouts]: - a[-4], a[-2] = a[-2], a[-4] - a[-3], a[-1] = a[-1], a[-3] - eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) - elementtemplate = mesh.createElementtemplate() - elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) - elementtemplate.defineField(coordinates, -1, eft) - element = mesh.createElement(elementIdentifier, elementtemplate) - element.setNodesByIdentifier(eft, nids) - if scalefactors: - element.setScaleFactors(eft, scalefactors) - for annotationMeshGroup in annotationMeshGroups: - annotationMeshGroup.addElement(element) - boxshieldElementIds[e3].append(elementIdentifier) - capElementIds.append(boxshieldElementIds) + # box shield elements (elements joining the box and the shell elements) + boxshieldElementIds = [] + for e3 in range(elementsCountCoreBoxMajor): + boxshieldElementIds.append([]) + e3p = e3 + 1 + for e1 in range(elementsCountCoreBoxMinor): + nids, nodeParameters, nodeLayouts = [], [], [] + elementIdentifier = generateData.nextElementIdentifier() + for n1 in [e1, e1 + 1]: + for n3 in [e3, e3p]: + nids += [capNodeIds[1][n3][n1]] + nodeParameter = self._getRimCoordinatesWithCore(n3, n1, 0) + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayoutCapTransition) + for n3 in [e3, e3p]: + boxLocation, tpLocation = self._getBoxBoundaryLocation(n3, n1) + nid = capNodeIds[0][n3][n1] + nids += [nid] + nodeParameter = self._getBoxCoordinates(n3, n1) + nodeParameters.append(nodeParameter) + nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(boxLocation, isStartCap) + nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint( + tpLocation, isStartCap) + if nid in boxBoundaryNodeIds[0]: + nodeLayouts.append( + nodeLayoutCapBoxShield if tpLocation == 0 else nodeLayoutCapBoxShieldTriplePoint) + else: + nodeLayouts.append(None) + if not isStartCap: + for a in [nids, nodeParameters, nodeLayouts]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + elementtemplate = mesh.createElementtemplate() + elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplate.defineField(coordinates, -1, eft) + element = mesh.createElement(elementIdentifier, elementtemplate) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + boxshieldElementIds[e3].append(elementIdentifier) + capElementIds.append(boxshieldElementIds) # shield for e3 in range(elementsCountRim - 1): @@ -1653,63 +1333,65 @@ def _generateElementsWithCore(self, annotationMeshGroups): # rim capElementIds = [] - # box transition - ringElementIds = [] - boxExtBoundaryNodeIds, boxExtBoundaryNodestoBoxIds = self._createBoundaryNodeIdsList(boxExtNodeIds) - for e1 in range(elementsCountAround): - nids, nodeParameters, nodeLayouts = [], [], [] - n1p = (e1 + 1) % self._elementsCountAround - boxLocation = self._getTriplePointLocation(e1) - shellLocation = self._getTriplePointLocation(e1, isShell=True) - nodeLayoutTransitionTriplePoint = generateData.getNodeLayoutTransitionTriplePoint(boxLocation) - nodeLayoutCapShellTransitionTriplePoint = generateData.getNodeLayoutCapShellTriplePoint(shellLocation) - for n3 in [0, 1]: - for n1 in [e1, n1p]: - nid = boxBoundaryNodeIds[n3][n1] if n3 == 0 else rimBoundaryNodeIds[n3 - 1][n1] - nids += [nid] - mi, ni = boxBoundaryNodeToCapIndex[n3][n1] if n3 == 0 else rimBoundaryNodeToCapIndex[n3 - 1][n1] - location, tpLocation = self._getBoxBoundaryLocation(mi, ni) - nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(location, isStartCap) - nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint(tpLocation, isStartCap) - if n3 == 0: - nodeParameter = self._getBoxCoordinates(mi, ni) - nodeLayout = nodeLayoutCapBoxShieldTriplePoint if n1 in triplePointIndexesList else ( - nodeLayoutCapBoxShield) - else: - nodeParameter = self._getRimCoordinatesWithCore(mi, ni, 0) - nodeLayout = nodeLayoutCapShellTransitionTriplePoint if n1 in triplePointIndexesList else ( - nodeLayoutCapTransition) - nodeParameters.append(nodeParameter) - nodeLayouts.append(nodeLayout) - for n1 in [e1, n1p]: - if n3 == 0: - nid = boxExtBoundaryNodeIds[n1] - mi, ni = boxExtBoundaryNodestoBoxIds[n1] - nodeParameter = self._getBoxExtCoordinates(mi, ni) - else: - nid = rimExtNodeIds[0][n1] - nodeParameter = self._getRimExtCoordinates(n1, 0) - nids += [nid] - nodeParameters.append(nodeParameter) - nodeLayouts.append(nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList and n3 == 0 - else nodeLayoutTransition) - if not isStartCap: - for a in [nids, nodeParameters, nodeLayouts]: - a[-4], a[-2] = a[-2], a[-4] - a[-3], a[-1] = a[-1], a[-3] - eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) - elementtemplate = mesh.createElementtemplate() - elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) - elementtemplate.defineField(coordinates, -1, eft) - elementIdentifier = generateData.nextElementIdentifier() - element = mesh.createElement(elementIdentifier, elementtemplate) - element.setNodesByIdentifier(eft, nids) - if scalefactors: - element.setScaleFactors(eft, scalefactors) - for annotationMeshGroup in annotationMeshGroups: - annotationMeshGroup.addElement(element) - ringElementIds.append(elementIdentifier) - capElementIds.append(ringElementIds) + if self._isCore: + # box transition + ringElementIds = [] + boxExtBoundaryNodeIds, boxExtBoundaryNodestoBoxIds = self._createBoundaryNodeIdsList(boxExtNodeIds) + for e1 in range(elementsCountAround): + nids, nodeParameters, nodeLayouts = [], [], [] + n1p = (e1 + 1) % self._elementsCountAround + boxLocation = self._getTriplePointLocation(e1) + shellLocation = self._getTriplePointLocation(e1, isShell=True) + nodeLayoutTransitionTriplePoint = generateData.getNodeLayoutTransitionTriplePoint(boxLocation) + nodeLayoutCapShellTransitionTriplePoint = generateData.getNodeLayoutCapShellTriplePoint(shellLocation) + for n3 in [0, 1]: + for n1 in [e1, n1p]: + nid = boxBoundaryNodeIds[n3][n1] if n3 == 0 else rimBoundaryNodeIds[n3 - 1][n1] + nids += [nid] + mi, ni = boxBoundaryNodeToCapIndex[n3][n1] if n3 == 0 else rimBoundaryNodeToCapIndex[n3 - 1][n1] + location, tpLocation = self._getBoxBoundaryLocation(mi, ni) + nodeLayoutCapBoxShield = generateData.getNodeLayoutCapBoxShield(location, isStartCap) + nodeLayoutCapBoxShieldTriplePoint = generateData.getNodeLayoutCapBoxShieldTriplePoint( + tpLocation, isStartCap) + if n3 == 0: + nodeParameter = self._getBoxCoordinates(mi, ni) + nodeLayout = nodeLayoutCapBoxShieldTriplePoint if n1 in triplePointIndexesList else ( + nodeLayoutCapBoxShield) + else: + nodeParameter = self._getRimCoordinatesWithCore(mi, ni, 0) + nodeLayout = nodeLayoutCapShellTransitionTriplePoint if n1 in triplePointIndexesList else ( + nodeLayoutCapTransition) + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayout) + for n1 in [e1, n1p]: + if n3 == 0: + nid = boxExtBoundaryNodeIds[n1] + mi, ni = boxExtBoundaryNodestoBoxIds[n1] + nodeParameter = self._getBoxExtCoordinates(mi, ni) + else: + nid = rimExtNodeIds[0][n1] + nodeParameter = self._getRimExtCoordinates(n1, 0) + nids += [nid] + nodeParameters.append(nodeParameter) + nodeLayouts.append(nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList and n3 == 0 + else nodeLayoutTransition) + if not isStartCap: + for a in [nids, nodeParameters, nodeLayouts]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + elementtemplate = mesh.createElementtemplate() + elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplate.defineField(coordinates, -1, eft) + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplate) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + ringElementIds.append(elementIdentifier) + capElementIds.append(ringElementIds) # shell triplePointIndexesList = self._getTriplePointIndexes() @@ -1866,7 +1548,7 @@ def _generateExtendedTubeElements(self, tubeBoxNodeIds, tubeRimNodeIds, annotati # create regular rim elements - all elements outside first transition layer elementsCountRim = self._getElementsCountRim() - elementsCountRimRegular = elementsCountRim - 1 if self._isCore else elementsCountRim + elementsCountRimRegular = elementsCountRim - 1 nTransition = elementsCountRimRegular - self._elementsCountThroughShell for e3 in range(elementsCountRimRegular): if e3 < nTransition: @@ -1937,82 +1619,44 @@ def sampleCoordinates(self, tubeBoxCoordinates, tubeTransitionCoordinates, tubeS self._tubeTransitionCoordinates = tubeTransitionCoordinates # tube transition coordinates self._tubeShellCoordinates = tubeShellCoordinates # tube rim coordinates - if self._isCore: - self._createShellCoordinatesList() + self._createShellCoordinatesList() for s in range(2): self._isStartCap = True if self._isCap[0] and s == 0 else False if self._isCap[s]: - if self._isCore: - self._sampleCapCoordinatesWithCore(s) - else: - self._extendTubeEnds() - self._sampleCapCoordinatesWithoutCore() + self._sampleCapCoordinates(s) else: continue return self._boxExtCoordinates, self._transitionExtCoordinates, self._shellExtCoordinates - def generateNodes(self, generateData, isStartCap=True, isCore=False): + def generateNodes(self, generateData, isStartCap=True): """ Blackbox function for generating cap and extended tube nodes. :param generateData: Class object from TubeNetworkMeshGenerateData. :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end of a tube segment. - :param isCore: True for generating a solid core inside the tube, False for regular tube network. """ self._isStartCap = isStartCap self._generateData = generateData if isStartCap: - self._generateNodesWithCore() if isCore else self._generateNodesWithoutCore() + self._generateNodes() self._generateExtendedTubeNodes() else: self._generateExtendedTubeNodes() - self._generateNodesWithCore() if isCore else self._generateNodesWithoutCore() + self._generateNodes() - def generateElements(self, elementsCountRim, tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups, - isStartCap=True, isCore=False): + def generateElements(self, tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups, isStartCap=True): """ Blackbox function for generating cap and extended tube elements. - :param elementsCountRim: Number of elements through the rim. :param tubeBoxNodeIds: List of tube box nodes. :param tubeRimNodeIds: List of tube rim nodes. :param annotationMeshGroups: List of all annotated mesh groups. :param isStartCap: True if generating a cap mesh at the start of a tube segment, False if generating at the end of a tube segment. - :param isCore: True for generating a solid core inside the tube, False for regular tube network. """ self._isStartCap = isStartCap - if isCore: - self._generateElementsWithCore(annotationMeshGroups) - self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) - else: - self._generateElementsWithoutCore(elementsCountRim, annotationMeshGroups) - self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) - - def _addElementsFromIdentifiers(self, mesh, meshGroup, identifiers, tRange=None, mRange=None, nRange=None, - e2Range=None): - """ - Adds elements to the mesh group based on a structured list of element identifiers. - :param mesh: The master mesh object used to look up elements by identifier. - :param meshGroup: Zinc MeshGroup to add elements to. - :param identifiers: A nested list of element identifiers. - :param tRange: Range of transition indices. - :param mRange: Range along the major axis. - :param nRange: Range along the minor axis. - :param e2Range: Range around the tube, for ring/circular indexing. - """ - for t in tRange: - if mRange is not None and nRange is not None: - for m in mRange: - for n in nRange: - elementIdentifier = identifiers[t][m][n] - element = mesh.findElementByIdentifier(elementIdentifier) - meshGroup.addElement(element) - elif e2Range is not None: - for e2 in e2Range: - elementIdentifier = identifiers[t][e2] - element = mesh.findElementByIdentifier(elementIdentifier) - meshGroup.addElement(element) + self._generateElements(annotationMeshGroups) + self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) def addBoxElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None): """ @@ -2041,20 +1685,16 @@ def addBoxElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Ran extElementIds = self._startExtElementIds if isStart else self._endExtElementIds # Cap base block (structured m, t, n) - self._addElementsFromIdentifiers(mesh, meshGroup, capElementIds[0], - tRange=range(elementsCountTransition + 1), mRange=e1Range, nRange=e3Range) + addElementsFromIdentifiers(mesh, meshGroup, capElementIds[0], tRange=range(elementsCountTransition + 1), mRange=e1Range, nRange=e3Range) # Cap tube elements (t, e2) - self._addElementsFromIdentifiers(mesh, meshGroup, capElementIds[1], - tRange=range(elementsCountTransition), e2Range=e2Range) + addElementsFromIdentifiers(mesh, meshGroup, capElementIds[1], tRange=range(elementsCountTransition), e2Range=e2Range) # Extension base block (just one layer at t=0) - self._addElementsFromIdentifiers(mesh, meshGroup, extElementIds[0][0:1], - tRange=[0], mRange=e1Range, nRange=e3Range) + addElementsFromIdentifiers(mesh, meshGroup, extElementIds[0][0:1], tRange=[0], mRange=e1Range, nRange=e3Range) # Extension tube (t > 0, e2) - self._addElementsFromIdentifiers(mesh, meshGroup, extElementIds[0], - tRange=range(1, elementsCountTransition + 1), e2Range=e2Range) + addElementsFromIdentifiers(mesh, meshGroup, extElementIds[0], tRange=range(1, elementsCountTransition + 1), e2Range=e2Range) def addShellElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None): """ @@ -2084,14 +1724,13 @@ def addShellElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3R extElementIds = self._startExtElementIds if isStart else self._endExtElementIds tCapShellRange = range(elementsCountTransition + 1, elementsCountTransition + 1 + elementsCountThroughShell) - self._addElementsFromIdentifiers(mesh, meshGroup, capElementIds[0], - tRange=tCapShellRange, mRange=e1Range, nRange=e3Range) + addElementsFromIdentifiers(mesh, meshGroup, capElementIds[0], tRange=tCapShellRange, mRange=e1Range, nRange=e3Range) tTubeShellRange = range(elementsCountTransition, elementsCountTransition + elementsCountThroughShell) - self._addElementsFromIdentifiers(mesh, meshGroup, capElementIds[1], tRange=tTubeShellRange, e2Range=e2Range) + addElementsFromIdentifiers(mesh, meshGroup, capElementIds[1], tRange=tTubeShellRange, e2Range=e2Range) tExtShellRange = range(elementsCountThroughShell) - self._addElementsFromIdentifiers(mesh, meshGroup, extElementIds[1], tRange=tExtShellRange, e2Range=e2Range) + addElementsFromIdentifiers(mesh, meshGroup, extElementIds[1], tRange=tExtShellRange, e2Range=e2Range) def addAllElementsToMeshGroup(self, meshGroup): """ @@ -2139,3 +1778,54 @@ def addSideD3ElementsToMeshGroup(self, side: bool, meshGroup): else: e3Range = None self.addShellElementsToMeshGroup(meshGroup, e2Range=e2Range, e3Range=e3Range) + + +def sampleCurvesOnSphere(x1, x2, origin, elementsOut): + """ + Sample coordinates and d1 derivatives of + :param x1: Coordinates of point 1 on the spherical surface of cap mesh. + :param x2: Coordinates of point 2 on the spherical surface of cap mesh. + :param origin: Centre point coordinates. + :param elementsOut: The number of elements required between points 1 and 2. + :return: Lists of sampled x and d1 between points 1 and 2. + """ + r1, r2 = sub(x1, origin), sub(x2, origin) + deltax = sub(r2, r1) + normal = cross(r1, deltax) + theta = angle(r1, r2) + anglePerElement = theta / elementsOut + arcLengthPerElement = calculate_arc_length(x1, x2, origin) / elementsOut + + nx, nd1 = [], [] + for n1 in range(elementsOut + 1): + radiansAcross = n1 * anglePerElement + r = rotate_vector_around_vector(r1, normal, radiansAcross) + nx.append(add(r, origin)) + nd1.append(set_magnitude(cross(normal, r), arcLengthPerElement)) + + return nx, nd1 + + +def addElementsFromIdentifiers(mesh, meshGroup, identifiers, tRange=None, mRange=None, nRange=None, e2Range=None): + """ + Adds elements to the mesh group based on a structured list of element identifiers. + :param mesh: The master mesh object used to look up elements by identifier. + :param meshGroup: Zinc MeshGroup to add elements to. + :param identifiers: A nested list of element identifiers. + :param tRange: Range of transition indices. + :param mRange: Range along the major axis. + :param nRange: Range along the minor axis. + :param e2Range: Range around the tube, for ring/circular indexing. + """ + for t in tRange: + if mRange is not None and nRange is not None: + for m in mRange: + for n in nRange: + elementIdentifier = identifiers[t][m][n] + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroup.addElement(element) + elif e2Range is not None: + for e2 in e2Range: + elementIdentifier = identifiers[t][e2] + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroup.addElement(element) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 82d99ceb..fc301db2 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -414,6 +414,9 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem self._networkPathParameters = pathParametersList self._isCap = networkSegment.isCap() if self._isCap: + if not self._isCore: + self._getNumberOfBoxElements() + self._elementsCountTransition = 1 self._capMesh = CapMesh(self._elementsCountAround, self._elementsCountCoreBoxMajor, self._elementsCountCoreBoxMinor, self._elementsCountThroughShell, self._elementsCountTransition, self._networkPathParameters, @@ -657,9 +660,9 @@ def sample(self, fixedElementsCountAlong, targetElementLength): self._rimElementIds = [None] * elementsCountAlong self._boxElementIds = [None] * elementsCountAlong - if self._isCore: - # sample coordinates for the solid core - self._sampleCoreCoordinates(elementsCountAlong) + # if self._isCore: + # # sample coordinates for the solid core + self._sampleCoreCoordinates(elementsCountAlong) if self._isCap: # sample coordinates for the cap mesh at the ends of a tube segment @@ -668,6 +671,19 @@ def sample(self, fixedElementsCountAlong, targetElementLength): # if self._isCore: self._smoothD2DerivativesAtCapTubeJoint() + def _getNumberOfBoxElements(self): + """ + Calculates the number of core box elements required to form a square-looking shield mesh. + Only used when the tube is without a core. + """ + # assert + half = self._elementsCountAround // 4 + if half % 2 == 0: + self._elementsCountCoreBoxMajor = self._elementsCountCoreBoxMinor = half + else: + self._elementsCountCoreBoxMinor = half - 1 if (half - 1) % 2 == 0 else half - 3 + self._elementsCountCoreBoxMajor = half + 1 if (half + 1) % 2 == 0 else half + 3 + def _sampleCoreCoordinates(self, elementsCountAlong): """ Black box function for sampling coordinates for the solid core. @@ -1731,11 +1747,10 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): capMesh = self._capMesh # create cap nodes at the start section of a tube segment if self._isCap[0] and n2 == 0: - isStartCap = True - capMesh.generateNodes(generateData, isStartCap, self._isCore) + capMesh.generateNodes(generateData, isStartCap=True) # create core box nodes - if self._boxCoordinates: + if self._isCore: self._boxNodeIds[n2] = [] if self._boxNodeIds[n2] is None else self._boxNodeIds[n2] coreBoxMajorNodesCount = self.getCoreBoxMajorNodesCount() coreBoxMinorNodesCount = self.getCoreBoxMinorNodesCount() @@ -1787,8 +1802,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create cap nodes at the end section of a tube segment if self._isCap[-1] and n2 == elementsCountAlong: - isStartCap = False - self._endCapNodeIds = capMesh.generateNodes(generateData, isStartCap, self._isCore) + self._endCapNodeIds = capMesh.generateNodes(generateData, isStartCap=False) # create a new list containing box node ids are located at the boundary if self._isCore: @@ -1808,13 +1822,9 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): e2p = e2 + 1 # create cap elements if self._isCap[0] and e2 == 0: - isStartCap = True - capMesh.generateElements(elementsCountRim, self._boxNodeIds, self._rimNodeIds, - annotationMeshGroups, isStartCap, self._isCore) + capMesh.generateElements(self._boxNodeIds, self._rimNodeIds, annotationMeshGroups, isStartCap=True) elif self._isCap[-1] and e2 == (elementsCountAlong - endSkipCount - 1): - isStartCap = False - capMesh.generateElements(elementsCountRim, self._boxNodeIds, self._rimNodeIds, - annotationMeshGroups, isStartCap, self._isCore) + capMesh.generateElements(self._boxNodeIds, self._rimNodeIds, annotationMeshGroups, isStartCap=False) if self._isCore: # create box elements @@ -4254,6 +4264,12 @@ def createSegment(self, networkSegment): self._layoutNodes, self._layoutInnerCoordinates, pathValueLabels, networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions())) elementsCountAround = self._defaultElementsCountAround + if any(networkSegment.isCap()) and not self._isCore: + if elementsCountAround < 8: + elementsCountAround = 8 + elif elementsCountAround % 4: + elementsCountAround += 4 - self._defaultElementsCountAround % 4 + elementsCountCoreBoxMinor = self._defaultElementsCountCoreBoxMinor coreBoundaryScalingMode = self._defaultCoreBoundaryScalingMode diff --git a/tests/test_capmesh.py b/tests/test_capmesh.py index acb5de02..b407414d 100644 --- a/tests/test_capmesh.py +++ b/tests/test_capmesh.py @@ -51,7 +51,7 @@ def test_3d_cap_tube_network_default(self): fieldmodule = region.getFieldmodule() mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(8 * 4 + 8 * 3 * 2, mesh3d.getSize()) + self.assertEqual(8 * 4 + (8 * 2 + 4) * 2, mesh3d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) self.assertEqual((8 * 5 + 8 * 2 * 2 + 2) * 2, nodes.getSize()) coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() @@ -88,9 +88,9 @@ def test_3d_cap_tube_network_default(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.014526098773694766, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 0.8247195017487451, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 0.6421069263444478, delta=X_TOL) + self.assertAlmostEqual(volume, 0.014392962237699036, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 0.8148830981004552, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 0.6329726542126899, delta=X_TOL) def test_3d_cap_tube_network_default_core(self): """ @@ -194,7 +194,7 @@ def test_3d_cap_tube_network_bifurcation(self): fieldmodule = region.getFieldmodule() mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual((8 * 4 + 8 * 3) * 3 , mesh3d.getSize()) + self.assertEqual((8 * 4 + 8 * 2 + 4) * 3 , mesh3d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) self.assertEqual((8 * 4 * 3 + 3 * 3 + 2) * 2 + (8 * 2 * 2 + 2) * 3, nodes.getSize()) coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() @@ -203,8 +203,8 @@ def test_3d_cap_tube_network_bifurcation(self): X_TOL = 1.0E-6 minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - assertAlmostEqualList(self, minimums, [-0.1500000000000000, -0.6246941344953365, -0.1000000000000000], X_TOL) - assertAlmostEqualList(self, maximums, [2.1433222518126906, 0.6246941344953366, 0.1000000000000000], X_TOL) + assertAlmostEqualList(self, minimums, [-0.1500000000000000, -0.6172290095800493, -0.1000000000000000], X_TOL) + assertAlmostEqualList(self, maximums, [2.1395896893550477, 0.6172290095800493, 0.1000000000000000], X_TOL) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -231,9 +231,9 @@ def test_3d_cap_tube_network_bifurcation(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.039770715282355665, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 2.2235629407464303, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 1.7691117020959595, delta=X_TOL) + self.assertAlmostEqual(volume, 0.039564164189831115, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.2086099452749344, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 1.755307334843727, delta=X_TOL) def test_3d_tube_network_bifurcation_core(self): """ From 07d9fcb940e4c80bc22f6f20dd6e2f86b6968c80 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 20 Aug 2025 13:37:26 +1200 Subject: [PATCH 23/43] Fix issue with 2D tube network --- src/scaffoldmaker/utils/tubenetworkmesh.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index fc301db2..964ea58b 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -660,9 +660,9 @@ def sample(self, fixedElementsCountAlong, targetElementLength): self._rimElementIds = [None] * elementsCountAlong self._boxElementIds = [None] * elementsCountAlong - # if self._isCore: - # # sample coordinates for the solid core - self._sampleCoreCoordinates(elementsCountAlong) + # sample coordinates for the solid core + if self._dimension == 3: + self._sampleCoreCoordinates(elementsCountAlong) if self._isCap: # sample coordinates for the cap mesh at the ends of a tube segment From bb893423b0afb90b7121d07f38a51fce3d2e26f6 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 20 Aug 2025 15:15:40 +1200 Subject: [PATCH 24/43] Refactor isCap and isPatch code in networkmesh --- src/scaffoldmaker/utils/networkmesh.py | 41 +++++++++++--------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index ccae9a2a..6f875343 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -105,6 +105,7 @@ def __init__(self, networkNodes: list, nodeVersions: list, isCap: list, isPatch) """ :param networkNodes: List of NetworkNodes from start to end. Must be at least 2. :param nodeVersions: List of node versions to use for derivatives at network nodes. + :param isCap: List of boolean true if segment requires a cap at either ends. [Start, End] :param isPatch: True if segment at the other end of the junction requires a patch. """ assert isinstance(networkNodes, list) and (len(networkNodes) > 1) and (len(nodeVersions) == len(networkNodes)) @@ -222,31 +223,25 @@ def build(self, structureString): self._networkSegments = [] sequenceStrings = structureString.split(",") for sequenceString in sequenceStrings: - # check if the node requires a cap at the end - if not sequenceString[0].isnumeric() or not sequenceString[-1].isnumeric(): - try: - isStartCap = True if sequenceString[0] == "(" else False - isEndCap = True if sequenceString[-1] == ")" else False - sequenceString = sequenceString[1:] if isStartCap else sequenceString - sequenceString = sequenceString[:-1] if isEndCap else sequenceString - except ValueError: - print("Network mesh: Skipping invalid cap sequence", sequenceString, file=sys.stderr) - continue - else: - isStartCap = isEndCap = False + # check if the segment requires a cap or a patch + isStartCap = isEndCap = isPatch = False + try: + # Check and handle caps + if sequenceString.startswith("("): + isStartCap = True + sequenceString = sequenceString[1:] + if sequenceString.endswith(")"): + isEndCap = True + sequenceString = sequenceString[:-1] + # Check and handle patch + if sequenceString.startswith("#"): + isPatch = True + sequenceString = sequenceString[2:] + except (ValueError, IndexError): + print("Network mesh: Skipping invalid cap sequence", sequenceString, file=sys.stderr) + continue isCap = [isStartCap, isEndCap] - # check if segment is a patch - if not sequenceString[0].isnumeric(): - try: - isPatch = True if sequenceString[0] == "#" else False - sequenceString = sequenceString[2:] if isPatch else sequenceString - except ValueError: - print("Network mesh: Skipping invalid cap sequence", sequenceString, file=sys.stderr) - continue - else: - isPatch = False - nodeIdentifiers = [] nodeVersions = [] nodeVersionStrings = sequenceString.split("-") From aafbb7cbeadff6e19a20656dd2aaeedf1a1e1be8 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 27 Aug 2025 11:39:48 +1200 Subject: [PATCH 25/43] Fix issue with cap transition elements --- src/scaffoldmaker/utils/capmesh.py | 19 ++++++++++++------- src/scaffoldmaker/utils/eft_utils.py | 11 +++++++++-- src/scaffoldmaker/utils/tubenetworkmesh.py | 22 ++++++++++++---------- 3 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index d7769e5d..71ea7b1f 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -936,19 +936,19 @@ def _getRimExtCoordinatesAround(self, n3): self._shellExtCoordinates[idx][2][sn3], self._shellExtCoordinates[idx][3][sn3]] - def _getRimCoordinatesWithCore(self, m, n, n3): + def _getRimCoordinatesWithCore(self, m, n, n2): """ Get coordinates and derivatives for cap rim. Only applies when core option is active. :param m: Index across major axis. :param n: Index across minor axis. - :param n3: Index along the tube. + :param n2: Index along the tube. :return: cap rim coordinates and derivatives for points at n3, m and n. """ idx = 0 if self._isStartCap else -1 - return [self._shellCoordinates[idx][0][n3][m][n], - self._shellCoordinates[idx][1][n3][m][n], - self._shellCoordinates[idx][2][n3][m][n], - self._shellCoordinates[idx][3][n3][m][n]] + return [self._shellCoordinates[idx][0][n2][m][n], + self._shellCoordinates[idx][1][n2][m][n], + self._shellCoordinates[idx][2][n2][m][n], + self._shellCoordinates[idx][3][n2][m][n]] def _getTubeBoxCoordinates(self, m, n): """ @@ -1198,6 +1198,7 @@ def _generateElements(self, annotationMeshGroups): elementsCountAround = self._elementsCountAround elementsCountCoreBoxMinor = self._elementsCountCoreBoxMinor elementsCountCoreBoxMajor = self._elementsCountCoreBoxMajor + elementsCountTransition = self._elementsCountTransition elementsCountRim = self._getElementsCountRim() generateData = self._generateData @@ -1207,6 +1208,7 @@ def _generateElements(self, annotationMeshGroups): nodeLayoutTransition = generateData.getNodeLayoutTransition() nodeLayoutCapTransition = generateData.getNodeLayoutCapTransition() + nodeLayoutCapTransitionSpecial = generateData.getNodeLayoutCapTransition(isSpecial=True) if isStartCap: capNodeIds = self._startCapNodeIds @@ -1269,7 +1271,10 @@ def _generateElements(self, annotationMeshGroups): nids += [capNodeIds[1][n3][n1]] nodeParameter = self._getRimCoordinatesWithCore(n3, n1, 0) nodeParameters.append(nodeParameter) - nodeLayouts.append(nodeLayoutCapTransition) + if elementsCountTransition > 1: + nodeLayouts.append(nodeLayoutCapTransitionSpecial) + else: + nodeLayouts.append(nodeLayoutCapTransition) for n3 in [e3, e3p]: boxLocation, tpLocation = self._getBoxBoundaryLocation(n3, n1) nid = capNodeIds[0][n3][n1] diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index 1f01d316..b5151230 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -976,12 +976,19 @@ def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajor return self._nodeLayoutBifurcationCoreTransitionBottomGeneral return nodeLayouts[layoutIndex] - def getNodeLayoutCapTransition(self): + def getNodeLayoutCapTransition(self, isSpecial=False): """ Get node layout for transition elements between the core box elements and the shell elements in the cap mesh. + :param isSpecial: True if cap transition is a special case where the number of transition elements > 1 for + box shield elements. False for all other cases. :return: HermiteNodeLayout. """ - return self._nodeLayoutRegularPermuted_d3Defined + if isSpecial: + nodeLayoutCapTransition = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]]) + else: + nodeLayoutCapTransition = self._nodeLayoutRegularPermuted_d3Defined + return nodeLayoutCapTransition def getNodeLayoutCapShellTriplePoint(self): """ diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 964ea58b..3bae83db 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -82,10 +82,8 @@ def __init__(self, region, meshDimension, coordinateFieldName="coordinates", d3Defined, limitDirections=[None, [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]], None]) self._nodeLayoutTransitionTriplePoint = None - self._nodeLayoutCapTransition = self._nodeLayoutManager.getNodeLayoutCapTransition() - + self._nodeLayoutCapTransition = None self._nodeLayoutCapShellTriplePoint = None - self._nodeLayoutCapBoxShield = None self._nodeLayoutCapBoxShieldTriplePoint = None @@ -182,10 +180,14 @@ def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajor return self._nodeLayoutManager.getNodeLayoutBifurcation6WayTriplePoint( segmentsIn, sequence, maxMajorSegment, top) - def getNodeLayoutCapTransition(self): + def getNodeLayoutCapTransition(self, isSpecial=False): """ - Node layout for generating cap shell transition elements, excluding at triple points. + Node layout for generating cap transition elements, excluding at triple points. + :param isSpecial:True if cap transition is a special case where the number of transition elements > 1 for + box shield elements. False for all other cases. + :return: Node layout. """ + self._nodeLayoutCapTransition = self._nodeLayoutManager.getNodeLayoutCapTransition(isSpecial=isSpecial) return self._nodeLayoutCapTransition def getNodeLayoutCapShellTriplePoint(self, location): @@ -4327,11 +4329,11 @@ def generateMesh(self, generateData): segmentCaps = segment.getIsCap() segment.addCoreElementsToMeshGroup(coreMeshGroup) segment.addShellElementsToMeshGroup(shellMeshGroup) - for isCap in segmentCaps: - if isCap: - capMesh = segment.getCapMesh() - capMesh.addBoxElementsToMeshGroup(coreMeshGroup) - capMesh.addShellElementsToMeshGroup(shellMeshGroup) + # for isCap in segmentCaps: + # if isCap: + # capMesh = segment.getCapMesh() + # capMesh.addBoxElementsToMeshGroup(coreMeshGroup) + # capMesh.addShellElementsToMeshGroup(shellMeshGroup) class BodyTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): From 15f497a345a1473a31d7cafb81975112c3fea96f Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 27 Aug 2025 12:09:35 +1200 Subject: [PATCH 26/43] Fix issue with unit test for renal capsule scaffold --- src/scaffoldmaker/utils/tubenetworkmesh.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 3bae83db..b08f1948 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -4329,11 +4329,11 @@ def generateMesh(self, generateData): segmentCaps = segment.getIsCap() segment.addCoreElementsToMeshGroup(coreMeshGroup) segment.addShellElementsToMeshGroup(shellMeshGroup) - # for isCap in segmentCaps: - # if isCap: - # capMesh = segment.getCapMesh() - # capMesh.addBoxElementsToMeshGroup(coreMeshGroup) - # capMesh.addShellElementsToMeshGroup(shellMeshGroup) + for isCap in segmentCaps: + if isCap: + capMesh = segment.getCapMesh() + capMesh.addBoxElementsToMeshGroup(coreMeshGroup) + capMesh.addShellElementsToMeshGroup(shellMeshGroup) class BodyTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): From 9d0f2fba94ed4a8de9c10f9d8b0240fc3fc14c7a Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 27 Aug 2025 14:55:12 +1200 Subject: [PATCH 27/43] Refactor node layouts in eft_utils --- src/scaffoldmaker/utils/capmesh.py | 4 +- src/scaffoldmaker/utils/eft_utils.py | 93 ++++++++++------------------ 2 files changed, 34 insertions(+), 63 deletions(-) diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 71ea7b1f..09d575ef 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -1258,7 +1258,7 @@ def _generateElements(self, annotationMeshGroups): boxElementIds[e3].append(elementIdentifier) capElementIds.append(boxElementIds) - # box shield elements (elements joining the box and the shell elements) + # box shield elements (elements joining the box and the shield elements) boxshieldElementIds = [] for e3 in range(elementsCountCoreBoxMajor): boxshieldElementIds.append([]) @@ -1339,7 +1339,7 @@ def _generateElements(self, annotationMeshGroups): # rim capElementIds = [] if self._isCore: - # box transition + # box shell (elements between the box core and the outer rim) ringElementIds = [] boxExtBoundaryNodeIds, boxExtBoundaryNodestoBoxIds = self._createBoundaryNodeIdsList(boxExtNodeIds) for e1 in range(elementsCountAround): diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index b5151230..6b8222f4 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -732,27 +732,21 @@ def __init__(self): HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, -1.0, 1.0]]), HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, 1.0, 1.0]]), HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]])] - self._nodeLayoutTriplePointTopLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, 0.0, 1.0]]) - self._nodeLayoutTriplePointTopRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 0.0, 1.0]]) - self._nodeLayoutTriplePointBottomLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 0.0, -1.0]]) - self._nodeLayoutTriplePointBottomRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]]) + self._nodeLayoutTriplePoint = [ + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, 0.0, 1.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 0.0, 1.0]]), + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 0.0, -1.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]])] self._nodeLayoutTriplePoint23Front = HermiteNodeLayout( [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [0.0, 1.0, 1.0]]) self._nodeLayoutTriplePoint23Back = HermiteNodeLayout( [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, 0.0], [0.0, 1.0, -1.0]]) self.nodeLayoutsBifurcation6WayTriplePoint = {} - self._nodeLayoutCapShellTriplePointTopLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [-1.0, 1.0, 0.0]]) - self._nodeLayoutCapShellTriplePointTopRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0]]) - self._nodeLayoutCapShellTriplePointBottomLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [-1.0, -1.0, 0.0]]) - self._nodeLayoutCapShellTriplePointBottomRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, -1.0, 0.0]]) + self._nodeLayoutCapShellTriplePoint = [ + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [-1.0, 1.0, 0.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0]]), + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [-1.0, -1.0, 0.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [1.0, -1.0, 0.0]])] self._nodeLayoutCapBoxShield = None self._nodeLayoutCapBoxShieldTriplePoint = None @@ -870,9 +864,7 @@ def getNodeLayoutTriplePoint(self): Bottom Left, and Bottom Right) each with its specific node layout. :return: List of 4 HermiteNodeLayout. """ - nodeLayouts = [self._nodeLayoutTriplePointTopLeft, self._nodeLayoutTriplePointTopRight, - self._nodeLayoutTriplePointBottomLeft, self._nodeLayoutTriplePointBottomRight] - return nodeLayouts + return self._nodeLayoutTriplePoint def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajorSegment, top): """ @@ -996,9 +988,7 @@ def getNodeLayoutCapShellTriplePoint(self): (Top Left, Top Right, Bottom Left, and Bottom Right) each with its specific node layout. :return: HermiteNodeLayout. """ - nodeLayouts = [self._nodeLayoutCapShellTriplePointTopLeft, self._nodeLayoutCapShellTriplePointTopRight, - self._nodeLayoutCapShellTriplePointBottomLeft, self._nodeLayoutCapShellTriplePointBottomRight] - return nodeLayouts + return self._nodeLayoutCapShellTriplePoint def getNodeLayoutCapBoxShield(self, isStartCap=True): """ @@ -1010,26 +1000,17 @@ def getNodeLayoutCapBoxShield(self, isStartCap=True): :return: HermiteNodeLayout. """ if isStartCap: - nodeLayoutCapBoxShieldTop = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, -1.0, 1.0]]) - nodeLayoutCapBoxShieldBottom = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, -1.0]]) - nodeLayoutCapBoxShieldLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [-1.0, -1.0, 0.0]]) - nodeLayoutCapBoxShieldRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [1.0, -1.0, 0.0]]) + self._nodeLayoutCapBoxShield = [ + HermiteNodeLayout([[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0], [0.0, -1.0, 1.0]]), + HermiteNodeLayout([[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, -1.0, -1.0]]), + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [-1.0, -1.0, 0.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [1.0, -1.0, 0.0]])] else: - nodeLayoutCapBoxShieldTop = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [0.0, 1.0, 1.0]]) - nodeLayoutCapBoxShieldBottom = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, -1.0]]) - nodeLayoutCapBoxShieldLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [-1.0, 1.0, 0.0]]) - nodeLayoutCapBoxShieldRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [1.0, 1.0, 0.0]]) - - self._nodeLayoutCapBoxShield = [nodeLayoutCapBoxShieldTop, nodeLayoutCapBoxShieldBottom, - nodeLayoutCapBoxShieldLeft, nodeLayoutCapBoxShieldRight] + self._nodeLayoutCapBoxShield = [ + HermiteNodeLayout([[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, -1.0, 0.0], [0.0, 1.0, 1.0]]), + HermiteNodeLayout([[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, -1.0]]), + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [-1.0, 1.0, 0.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0, -1.0], [1.0, 1.0, 0.0]])] return self._nodeLayoutCapBoxShield @@ -1042,27 +1023,17 @@ def getNodeLayoutCapBoxShieldTriplePoint(self, isStartCap=True): :return: HermiteNodeLayout. """ if isStartCap: - nodeLayoutCapBoxShieldTriplePointTopLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, -1.0, 1.0]]) - nodeLayoutCapBoxShieldTriplePointTopRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, -1.0, 1.0]]) - nodeLayoutCapBoxShieldTriplePointBottomLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, -1.0, -1.0]]) - nodeLayoutCapBoxShieldTriplePointBottomRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, -1.0, -1.0]]) + self._nodeLayoutCapBoxShieldTriplePoint = [ + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, -1.0, 1.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, -1.0, 1.0]]), + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, -1.0, -1.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, -1.0, -1.0]])] else: - nodeLayoutCapBoxShieldTriplePointTopLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, 1.0, 1.0]]) - nodeLayoutCapBoxShieldTriplePointTopRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]) - nodeLayoutCapBoxShieldTriplePointBottomLeft = HermiteNodeLayout( - [[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 1.0, -1.0]]) - nodeLayoutCapBoxShieldTriplePointBottomRight = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, -1.0]]) - - self._nodeLayoutCapBoxShieldTriplePoint = \ - [nodeLayoutCapBoxShieldTriplePointTopLeft, nodeLayoutCapBoxShieldTriplePointTopRight, - nodeLayoutCapBoxShieldTriplePointBottomLeft, nodeLayoutCapBoxShieldTriplePointBottomRight] + self._nodeLayoutCapBoxShieldTriplePoint = [ + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [-1.0, 1.0, 1.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]), + HermiteNodeLayout([[1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 1.0, -1.0]]), + HermiteNodeLayout([[-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, -1.0]])] return self._nodeLayoutCapBoxShieldTriplePoint From 6717a259455ca9dec21bd3f732c64b9a67fd4227 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 9 Sep 2025 10:03:38 +1200 Subject: [PATCH 28/43] Add annotations to the kidney scaffold --- src/scaffoldmaker/annotation/kidney_terms.py | 14 +- .../meshtypes/meshtype_3d_renal_capsule1.py | 119 +++++++++++++-- src/scaffoldmaker/utils/capmesh.py | 46 ++++-- src/scaffoldmaker/utils/tubenetworkmesh.py | 137 ++++++++++++++++-- .../{test_renalcapsule.py => test_kidney.py} | 37 ++--- 5 files changed, 297 insertions(+), 56 deletions(-) rename tests/{test_renalcapsule.py => test_kidney.py} (80%) diff --git a/src/scaffoldmaker/annotation/kidney_terms.py b/src/scaffoldmaker/annotation/kidney_terms.py index 241ccfe8..d38ee327 100644 --- a/src/scaffoldmaker/annotation/kidney_terms.py +++ b/src/scaffoldmaker/annotation/kidney_terms.py @@ -4,13 +4,23 @@ # convention: preferred name, preferred id, followed by any other ids and alternative names kidney_terms = [ - ("core", ""), + ("anterior surface of kidney", "UBERON:0035368", "ILX:0724840"), + ("cortex of kidney", "UBERON:0001225", "ILX:0726853"), + ("dorsal surface of kidney", ""), + ("hilum of kidney", "UBERON:0008716", "ILX:0731719"), + ("kidney", "UBERON:0002113", "ILX:0735723"), ("kidney capsule", "UBERON:0002015", "ILX:0733912"), + ("lateral edge of kidney", ""), + ("lateral surface of kidney", ""), ("major calyx", "UBERON:0001226", "ILX:0730785"), + ("medial edge of kidney", ""), + ("medial surface of kidney", ""), ("minor calyx", "UBERON:0001227", "ILX:0730473"), + ("renal medulla", "UBERON:0000362", "ILX:0729114"), ("renal pelvis", "UBERON:0001224", "ILX:0723968"), ("renal pyramid", "UBERON:0004200", "ILX:0727514"), - ("shell", "") + ("posterior surface of kidney", "UBERON:0035471", "ILX:0724479"), + ("ventral surface of kidney", "") ] def get_kidney_term(name : str): diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py index 2d897f06..6b897851 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py @@ -7,7 +7,8 @@ from cmlibs.utils.zinc.field import find_or_create_field_coordinates from cmlibs.zinc.field import Field -from scaffoldmaker.annotation.annotationgroup import AnnotationGroup +from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm, \ + getAnnotationGroupForTerm, findAnnotationGroupByName from scaffoldmaker.annotation.kidney_terms import get_kidney_term from scaffoldmaker.meshtypes.meshtype_1d_network_layout1 import MeshType_1d_network_layout1 from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base @@ -15,7 +16,8 @@ from scaffoldmaker.utils.interpolation import smoothCubicHermiteDerivativesLine, sampleCubicHermiteCurves, \ smoothCurveSideCrossDerivatives from scaffoldmaker.utils.networkmesh import NetworkMesh -from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData +from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData, \ + KidneyTubeNetworkMeshBuilder from cmlibs.zinc.node import Node @@ -41,7 +43,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Renal capsule length"] = 1.0 options["Renal capsule diameter"] = 1.5 options["Renal capsule bend angle degrees"] = 10 - options["Inner proportion default"] = 0.8 + options["Inner proportion default"] = 0.6 return options @classmethod @@ -102,12 +104,12 @@ def generateBaseMesh(cls, region, options): mesh = fieldmodule.findMeshByDimension(1) # set up element annotations - renalCapsuleGroup = AnnotationGroup(region, get_kidney_term("kidney capsule")) - annotationGroups = [renalCapsuleGroup] + kidneyGroup = AnnotationGroup(region, get_kidney_term("kidney")) + annotationGroups = [kidneyGroup] - renalCapsuleGroup = renalCapsuleGroup.getMeshGroup(mesh) + kidneyMeshGroup = kidneyGroup.getMeshGroup(mesh) elementIdentifier = 1 - meshGroups = [renalCapsuleGroup] + meshGroups = [kidneyMeshGroup] for e in range(capsuleElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: @@ -208,10 +210,10 @@ def getDefaultOptions(cls, parameterSetName='Default'): useParameterSetName = "Human 1" if (parameterSetName == "Default") else parameterSetName options["Base parameter set"] = useParameterSetName options["Renal capsule network layout"] = ScaffoldPackage(MeshType_1d_renal_capsule_network_layout1) - options["Elements count around"] = 12 + options["Elements count around"] = 8 options["Elements count through shell"] = 1 options["Annotation elements counts around"] = [0] - options["Target element density along longest segment"] = 4.0 + options["Target element density along longest segment"] = 2.0 options["Number of elements across core box minor"] = 2 options["Number of elements across core transition"] = 1 options["Annotation numbers of elements across core box minor"] = [0] @@ -339,7 +341,7 @@ def generateBaseMesh(cls, region, options): layoutAnnotationGroups = networkLayout.getAnnotationGroups() networkMesh = networkLayout.getConstructionObject() - tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( + tubeNetworkMeshBuilder = KidneyTubeNetworkMeshBuilder( networkMesh, targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], defaultElementsCountAround=options["Elements count around"], @@ -358,8 +360,83 @@ def generateBaseMesh(cls, region, options): tubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() + # add kidney-specific annotation groups + fm = region.getFieldmodule() + mesh = generateData.getMesh() + + coreGroup = getAnnotationGroupForTerm(annotationGroups, ("core", "")).getGroup() + shellGroup = getAnnotationGroupForTerm(annotationGroups, ("shell", "")).getGroup() + openingGroup = getAnnotationGroupForTerm(annotationGroups, ("opening", "")).getGroup() + + hilumGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("hilum of kidney")) + hilumGroup.getMeshGroup(mesh).addElementsConditional(openingGroup) + + cortexGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("cortex of kidney")) + tempGroup = fm.createFieldSubtract(shellGroup, openingGroup) + cortexGroup.getMeshGroup(mesh).addElementsConditional(tempGroup) + + medullaGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("renal medulla")) + medullaGroup.getMeshGroup(mesh).addElementsConditional(coreGroup) + + for term in ["core", "shell", "opening"]: + annotationGroups.remove(findAnnotationGroupByName(annotationGroups, term)) + return annotationGroups, None + + @classmethod + def defineFaceAnnotations(cls, region, options, annotationGroups): + """ + Add face annotation groups from the highest dimension mesh. + Must have defined faces and added subelements for highest dimension groups. + :param region: Zinc region containing model. + :param options: Dict containing options. See getDefaultOptions(). + :param annotationGroups: List of annotation groups for top-level elements. + New face annotation groups are appended to this list. + """ + fm = region.getFieldmodule() + mesh1d = fm.findMeshByDimension(1) + mesh2d = fm.findMeshByDimension(2) + + is_exterior = fm.createFieldIsExterior() + kidneyGroup = getAnnotationGroupForTerm(annotationGroups, get_kidney_term("kidney")).getGroup() + + # surface groups + kidneyCapsuleGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("kidney capsule")) + kidneyCapsuleGroup.getMeshGroup(mesh2d).addElementsConditional(fm.createFieldAnd(kidneyGroup, is_exterior)) + + surfaceTypes = [ + "anterior", "posterior", + "lateral", "medial", + "dorsal", "ventral" + ] + + arb_group = {} + kidney_exterior = {} + for surfaceType in surfaceTypes: + group = getAnnotationGroupForTerm(annotationGroups, (surfaceType, "")) + group2d = group.getGroup() + group2d_exterior = fm.createFieldAnd(group2d, is_exterior) + + term = f"{surfaceType} surface of kidney" + surfaceGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(term)) + surfaceGroup.getMeshGroup(mesh2d).addElementsConditional(group2d_exterior) + + arb_group.update({surfaceType: group2d}) + kidney_exterior.update({term: group2d_exterior}) + + # edge groups + dorsalVentralBorderGroup = fm.createFieldAnd(fm.createFieldAnd(arb_group["dorsal"], arb_group["ventral"]), is_exterior) + + for surfaceType in ["lateral", "medial"]: + term = f"{surfaceType} edge of kidney" + edgeGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(term)) + edgeGroup.getMeshGroup(mesh1d).addElementsConditional(fm.createFieldAnd(arb_group[surfaceType], dorsalVentralBorderGroup)) + + # for term in ["lateral", "medial", "dorsal", "ventral"]: + # annotationGroups.remove(findAnnotationGroupByName(annotationGroups, term)) + + def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None): """ Assign node field parameters x, d1, d2, d3 of field. @@ -403,3 +480,25 @@ def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=N field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, d12) if d13: field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, d13) + + +def setHilumGroupThreshold(fm, coordinates, halfLength): + """ + Creates a field to identify lung base elements based on y-coordinate threshold. + Elements with y-coordinates below 45% of the rotated half-breadth are considered part of the lung base region for + annotation purposes. + :param fm: Field module used for creating and managing fields. + :param coordinates: The coordinate field. + :param halfLength: Half-length of tube. + :return is_within_threshold: True for elements between the positive and negative x-threshold. + """ + x_component = fm.createFieldComponent(coordinates, [1]) + x_threshold = 0.25 * halfLength + minus_one = fm.createFieldConstant(-1) + + x_threshold_field = fm.createFieldConstant(x_threshold) + is_less_than_threshold = fm.createFieldLessThan(x_component, x_threshold_field) + is_greater_than_threshold = fm.createFieldGreaterThan(x_component, x_threshold_field * minus_one) + is_within_threshold = fm.createFieldAnd(is_less_than_threshold, is_greater_than_threshold) + + return is_within_threshold diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 09d575ef..7f3926d4 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -1663,13 +1663,14 @@ def generateElements(self, tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups, self._generateElements(annotationMeshGroups) self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) - def addBoxElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None): + def addCapBoxElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None, mode=0): """ Add ranges of box elements to mesh group. :param meshGroup: Zinc MeshGroup to add elements to. :param e1Range: Range between start and limit element indexes in major / d2 direction. :param e2Range: Range between start and limit element indexes around the tube. :param e3Range: Range between start and limit element indexes in minor / d3 direction. + :param mode: """ elementsCountAround = self._elementsCountAround elementsCountCoreBoxMajor = self._elementsCountCoreBoxMajor @@ -1681,11 +1682,15 @@ def addBoxElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Ran e2Range = range(elementsCountAround) if e2Range is None else e2Range e3Range = range(elementsCountCoreBoxMinor) if e3Range is None else e3Range - for i, isCap in enumerate(self._isCap): + nloop = 2 if mode == 0 else 1 + for idx in range(nloop): + if mode > 0: + idx = 0 if mode == 1 else 1 + isCap = self._isCap[idx] if not isCap: continue - isStart = (i == 0) + isStart = (idx == 0) capElementIds = self._startCapElementIds if isStart else self._endCapElementIds extElementIds = self._startExtElementIds if isStart else self._endExtElementIds @@ -1701,13 +1706,14 @@ def addBoxElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Ran # Extension tube (t > 0, e2) addElementsFromIdentifiers(mesh, meshGroup, extElementIds[0], tRange=range(1, elementsCountTransition + 1), e2Range=e2Range) - def addShellElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None): + def addCapShellElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None, mode=0): """ Add ranges of shell elements to mesh group. :param meshGroup: Zinc MeshGroup to add elements to. :param e1Range: Range between start and limit element indexes in major / d2 direction. :param e2Range: Range between start and limit element indexes around the tube. :param e3Range: Range between start and limit element indexes in minor / d3 direction. + :mode: """ elementsCountAround = self._elementsCountAround elementsCountCoreBoxMajor = self._elementsCountCoreBoxMajor @@ -1720,11 +1726,15 @@ def addShellElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3R e2Range = range(elementsCountAround) if e2Range is None else e2Range e3Range = range(elementsCountCoreBoxMinor) if e3Range is None else e3Range - for i, isCap in enumerate(self._isCap): + nloop = 2 if mode == 0 else 1 + for idx in range(nloop): + if mode > 0: + idx = 0 if mode == 1 else 1 + isCap = self._isCap[idx] if not isCap: continue - isStart = (i == 0) + isStart = (idx == 0) capElementIds = self._startCapElementIds if isStart else self._endCapElementIds extElementIds = self._startExtElementIds if isStart else self._endExtElementIds @@ -1737,15 +1747,19 @@ def addShellElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3R tExtShellRange = range(elementsCountThroughShell) addElementsFromIdentifiers(mesh, meshGroup, extElementIds[1], tRange=tExtShellRange, e2Range=e2Range) - def addAllElementsToMeshGroup(self, meshGroup): + + def addCapSideD1ElementsToMeshGroup(self, side: bool, meshGroup): """ - Add all elements in the segment to mesh group. + Add elements to the mesh group on side of +d1 or -d1, often matching anterior and posterior. + :param side: False for +d1 direction, True for -d1 direction. :param meshGroup: Zinc MeshGroup to add elements to. """ - self.addBoxElementsToMeshGroup(meshGroup) - self.addShellElementsToMeshGroup(meshGroup) + mode = 1 if side else 2 + if self._isCore: + self.addCapBoxElementsToMeshGroup(meshGroup, mode=mode) + self.addCapShellElementsToMeshGroup(meshGroup, mode=mode) - def addSideD2ElementsToMeshGroup(self, side: bool, meshGroup): + def addCapSideD2ElementsToMeshGroup(self, side: bool, meshGroup): """ Add elements to the mesh group on side of +d2 or -d2, often matching left and right. Only works with even numbers around and phase starting at +d2. @@ -1760,12 +1774,12 @@ def addSideD2ElementsToMeshGroup(self, side: bool, meshGroup): e1Start = (self._elementsCountCoreBoxMajor // 2) if side else 0 e1Limit = self._elementsCountCoreBoxMajor if side else ((self._elementsCountCoreBoxMajor + 1) // 2) e1Range = range(e1Start, e1Limit) - self.addBoxElementsToMeshGroup(meshGroup, e1Range=e1Range, e2Range=e2Range) + self.addCapBoxElementsToMeshGroup(meshGroup, e1Range=e1Range, e2Range=e2Range) else: e1Range = None - self.addShellElementsToMeshGroup(meshGroup, e1Range=e1Range, e2Range=e2Range) + self.addCapShellElementsToMeshGroup(meshGroup, e1Range=e1Range, e2Range=e2Range) - def addSideD3ElementsToMeshGroup(self, side: bool, meshGroup): + def addCapSideD3ElementsToMeshGroup(self, side: bool, meshGroup): """ Add elements to the mesh group on side of +d3 or -d3, often matching anterior/ventral and posterior/dorsal. Only works with even numbers around and phase starting at +d2. @@ -1779,10 +1793,10 @@ def addSideD3ElementsToMeshGroup(self, side: bool, meshGroup): e3Start = 0 if side else (self._elementsCountCoreBoxMinor // 2) e3Limit = ((self._elementsCountCoreBoxMinor + 1) // 2) if side else self._elementsCountCoreBoxMinor e3Range = range(e3Start, e3Limit) - self.addBoxElementsToMeshGroup(meshGroup, e2Range=e2Range, e3Range=e3Range) + self.addCapBoxElementsToMeshGroup(meshGroup, e2Range=e2Range, e3Range=e3Range) else: e3Range = None - self.addShellElementsToMeshGroup(meshGroup, e2Range=e2Range, e3Range=e3Range) + self.addCapShellElementsToMeshGroup(meshGroup, e2Range=e2Range, e3Range=e3Range) def sampleCurvesOnSphere(x1, x2, origin, elementsOut): diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index b08f1948..1f02dd38 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -96,7 +96,14 @@ def __init__(self, region, meshDimension, coordinateFieldName="coordinates", self._rightGroup = None self._dorsalGroup = None self._ventralGroup = None + # annotations for the kidney scaffold: self._renalCapsuleGroup = None + self._lateralGroup = None + self._medialGroup = None + self._anteriorGroup = None + self._posteriorGroup = None + self._openingGroup = None + def getStandardElementtemplate(self): return self._standardElementtemplate, self._standardEft @@ -310,6 +317,31 @@ def getRenalCapsuleMeshGroup(self): self._renalCapsuleGroup = self.getOrCreateAnnotationGroup(("renal capsule", "")) return self._renalCapsuleGroup.getMeshGroup(self._mesh) + def getAnteriorMeshGroup(self): + if not self._anteriorGroup: + self._anteriorGroup = self.getOrCreateAnnotationGroup(("anterior", "")) + return self._anteriorGroup.getMeshGroup(self._mesh) + + def getPosteriorMeshGroup(self): + if not self._posteriorGroup: + self._posteriorGroup = self.getOrCreateAnnotationGroup(("posterior", "")) + return self._posteriorGroup.getMeshGroup(self._mesh) + + def getLateralMeshGroup(self): + if not self._lateralGroup: + self._lateralGroup = self.getOrCreateAnnotationGroup(("lateral", "")) + return self._lateralGroup.getMeshGroup(self._mesh) + + def getMedialMeshGroup(self): + if not self._medialGroup: + self._medialGroup = self.getOrCreateAnnotationGroup(("medial", "")) + return self._medialGroup.getMeshGroup(self._mesh) + + def getOpeningMeshGroup(self): + if not self._openingGroup: + self._openingGroup = self.getOrCreateAnnotationGroup(("opening", "")) + return self._openingGroup.getMeshGroup(self._mesh) + def getNewTrimAnnotationGroup(self): self._trimAnnotationGroupCount += 1 return self.getOrCreateAnnotationGroup(("trim surface " + "{:03d}".format(self._trimAnnotationGroupCount), "")) @@ -1562,19 +1594,23 @@ def setBoxElementId(self, e1, e2, e3, elementIdentifier): [None] * self._elementsCountCoreBoxMinor for _ in range(self._elementsCountCoreBoxMajor)] self._boxElementIds[e2][e3][e1] = elementIdentifier - def _addBoxElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGroup): + def _addBoxElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGroup, e2Start=None, e2Limit=None): """ Add ranges of box elements to mesh group. :param e1Start: Start element index in major / d2 direction. :param e1Limit: Limit element index in major / d2 direction. :param e3Start: Start element index in minor / d3 direction. :param e3Limit: Limit element index in minor / d3 direction. + :param e2Start: Start element index in d1 direction. If None, use 0 as default. + :param e2Limit: Limit element index in d1 direction. If None, use elementsCountAlong as default. :param meshGroup: Zinc MeshGroup to add elements to. """ # print("Add box elements", e1Start, e1Limit, e3Start, e3Limit, meshGroup.getName()) elementsCountAlong = self.getSampledElementsCountAlong() mesh = meshGroup.getMasterMesh() - for e2 in range(elementsCountAlong): + e2Start = 0 if e2Start is None else e2Start + e2Limit = elementsCountAlong if e2Limit is None else e2Limit + for e2 in range(e2Start, e2Limit): boxSlice = self._boxElementIds[e2] if boxSlice: # print(boxSlice[e1Start:e1Limit]) @@ -1583,19 +1619,23 @@ def _addBoxElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGro element = mesh.findElementByIdentifier(elementIdentifier) meshGroup.addElement(element) - def _addRimElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGroup): + def _addRimElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGroup, e2Start=None, e2Limit=None): """ Add ranges of rim elements to mesh group. :param e1Start: Start element index around. Can be negative which supports wrapping. :param e1Limit: Limit element index around. :param e3Start: Start element index rim. :param e3Limit: Limit element index rim. + :param e2Start: Start element index in d1 direction. If None, use 0 as default. + :param e2Limit: Limit element index in d1 direction. If None, use elementsCountAlong as default. :param meshGroup: Zinc MeshGroup to add elements to. """ # print("Add rim elements", e1Start, e1Limit, e3Start, e3Limit, meshGroup.getName()) elementsCountAlong = self.getSampledElementsCountAlong() mesh = meshGroup.getMasterMesh() - for e2 in range(elementsCountAlong): + e2Start = 0 if e2Start is None else e2Start + e2Limit = elementsCountAlong if e2Limit is None else e2Limit + for e2 in range(e2Start, e2Limit): rimSlice = self._rimElementIds[e2] if rimSlice: for elementIdentifiersList in rimSlice[e3Start:e3Limit]: @@ -1637,6 +1677,23 @@ def addAllElementsToMeshGroup(self, meshGroup): self.addCoreElementsToMeshGroup(meshGroup) self.addShellElementsToMeshGroup(meshGroup) + def addSideD1ElementsToMeshGroup(self, side: bool, meshGroup): + """ + Add elements to the mesh group on side of +d1 or -d1, often matching anterior and posterior. + :param side: False for +d1 direction, True for -d1 direction. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + elementsCountAlong = self.getSampledElementsCountAlong() + e2Start = 0 if side else elementsCountAlong // 2 + e2Limit = elementsCountAlong // 2 if side else elementsCountAlong + if self._isCore: + self._addBoxElementsToMeshGroup(0, self._elementsCountCoreBoxMajor, + 0, self._elementsCountCoreBoxMinor, meshGroup, + e2Start=e2Start, e2Limit=e2Limit) + self._addRimElementsToMeshGroup(0, self._elementsCountAround, + 0, self.getElementsCountRim(), meshGroup, + e2Start=e2Start, e2Limit=e2Limit) + def addSideD2ElementsToMeshGroup(self, side: bool, meshGroup): """ Add elements to the mesh group on side of +d2 or -d2, often matching left and right. @@ -1669,6 +1726,19 @@ def addSideD3ElementsToMeshGroup(self, side: bool, meshGroup): e1Limit = e1Start + (self._elementsCountAround // 2) self._addRimElementsToMeshGroup(e1Start, e1Limit, 0, self.getElementsCountRim(), meshGroup) + def addShellOpeningElementsToMeshGroup(self, e1Start, e1Limit, meshGroup): + """ + Add specific elements in the shell to mesh group. + :param e1Start: Start element index around. + :param e1Limit: Limit element index around. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + elementsCountRim = self.getElementsCountRim() + elementsCountShell = self._elementsCountThroughShell + e3ShellStart = elementsCountRim - elementsCountShell + + self._addRimElementsToMeshGroup(e1Start, e1Limit, e3ShellStart, elementsCountRim, meshGroup) + def getRimNodeIdsSlice(self, n2): """ Get slice of rim node IDs. @@ -4332,8 +4402,8 @@ def generateMesh(self, generateData): for isCap in segmentCaps: if isCap: capMesh = segment.getCapMesh() - capMesh.addBoxElementsToMeshGroup(coreMeshGroup) - capMesh.addShellElementsToMeshGroup(shellMeshGroup) + capMesh.addCapBoxElementsToMeshGroup(coreMeshGroup) + capMesh.addCapShellElementsToMeshGroup(shellMeshGroup) class BodyTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): @@ -4372,13 +4442,60 @@ def generateMesh(self, generateData): segment.addSideD2ElementsToMeshGroup(False, leftMeshGroup) segment.addSideD2ElementsToMeshGroup(True, rightMeshGroup) if capMesh: - capMesh.addSideD2ElementsToMeshGroup(False, leftMeshGroup) - capMesh.addSideD2ElementsToMeshGroup(True, rightMeshGroup) + capMesh.addCapSideD2ElementsToMeshGroup(False, leftMeshGroup) + capMesh.addCapSideD2ElementsToMeshGroup(True, rightMeshGroup) + segment.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) + segment.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) + if capMesh: + capMesh.addCapSideD3ElementsToMeshGroup(False, ventralMeshGroup) + capMesh.addCapSideD3ElementsToMeshGroup(True, dorsalMeshGroup) + + +class KidneyTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): + """ + Specialization of TubeNetworkMeshBuilder adding annotations for anterior, posterior, lateral, medial, and hilum regions. + """ + + def generateMesh(self, generateData): + super(KidneyTubeNetworkMeshBuilder, self).generateMesh(generateData) + # build anterior, posterior, lateral, medial annotation groups + anteriorMeshGroup = generateData.getAnteriorMeshGroup() + posteriorMeshGroup = generateData.getPosteriorMeshGroup() + lateralMeshGroup = generateData.getLateralMeshGroup() + medialMeshGroup = generateData.getMedialMeshGroup() + dorsalMeshGroup = generateData.getDorsalMeshGroup() + ventralMeshGroup = generateData.getVentralMeshGroup() + openingMeshGroup = generateData.getOpeningMeshGroup() + + elementsCountAround = self._defaultElementsCountAround + halfElementsCountAround = elementsCountAround // 2 + increment = max(1, elementsCountAround // 8) + + e1Start = halfElementsCountAround - increment + e1End = halfElementsCountAround + increment + + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + segmentCaps = segment.getIsCap() + if True in segmentCaps: + capMesh = segment.getCapMesh() + else: + capMesh = None + # segment on main axis + segment.addSideD1ElementsToMeshGroup(True, anteriorMeshGroup) + segment.addSideD1ElementsToMeshGroup(False, posteriorMeshGroup) + segment.addSideD2ElementsToMeshGroup(False, lateralMeshGroup) + segment.addSideD2ElementsToMeshGroup(True, medialMeshGroup) segment.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) segment.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) + segment.addShellOpeningElementsToMeshGroup(e1Start, e1End, openingMeshGroup) if capMesh: - capMesh.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) - capMesh.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) + capMesh.addCapSideD1ElementsToMeshGroup(True, anteriorMeshGroup) + capMesh.addCapSideD1ElementsToMeshGroup(False, posteriorMeshGroup) + capMesh.addCapSideD2ElementsToMeshGroup(False, lateralMeshGroup) + capMesh.addCapSideD2ElementsToMeshGroup(True, medialMeshGroup) + capMesh.addCapSideD3ElementsToMeshGroup(False, ventralMeshGroup) + capMesh.addCapSideD3ElementsToMeshGroup(True, dorsalMeshGroup) class TubeEllipseGenerator: diff --git a/tests/test_renalcapsule.py b/tests/test_kidney.py similarity index 80% rename from tests/test_renalcapsule.py rename to tests/test_kidney.py index 00a13675..02e98a56 100644 --- a/tests/test_renalcapsule.py +++ b/tests/test_kidney.py @@ -16,9 +16,9 @@ from testutils import assertAlmostEqualList -class RenalCapsulecaffoldTestCase(unittest.TestCase): +class KidneyScaffoldTestCase(unittest.TestCase): - def test_renalcapsule(self): + def test_kidney(self): """ Test creation of renal capsule scaffold. """ @@ -28,10 +28,10 @@ def test_renalcapsule(self): options = scaffold.getDefaultOptions("Human 1") self.assertEqual(9, len(options)) - self.assertEqual(12, options["Elements count around"]) + self.assertEqual(8, options["Elements count around"]) self.assertEqual(1, options["Elements count through shell"]) self.assertEqual([0], options["Annotation elements counts around"]) - self.assertEqual(4.0, options["Target element density along longest segment"]) + self.assertEqual(2.0, options["Target element density along longest segment"]) self.assertEqual(2, options["Number of elements across core box minor"]) self.assertEqual(1, options["Number of elements across core transition"]) self.assertEqual([0], options["Annotation numbers of elements across core box minor"]) @@ -40,18 +40,18 @@ def test_renalcapsule(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(3, len(annotationGroups)) + self.assertEqual(19, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(288, mesh3d.getSize()) + self.assertEqual(136, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(920, mesh2d.getSize()) + self.assertEqual(436, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(994, mesh1d.getSize()) + self.assertEqual(478, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(363, nodes.getSize()) + self.assertEqual(179, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -60,8 +60,8 @@ def test_renalcapsule(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [-1.583346623141804, -0.9520066012170885, -0.75], tol) - assertAlmostEqualList(self, maximums, [1.583349401938375, 0.7499999999986053, 0.75], tol) + assertAlmostEqualList(self, minimums, [-1.5508832466803322, -0.9195143646416805, -0.75], tol) + assertAlmostEqualList(self, maximums, [1.5508832466803322, 0.7499999999986053, 0.75], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -79,15 +79,15 @@ def test_renalcapsule(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 4.8399524698282725, delta=tol) - self.assertAlmostEqual(surfaceArea, 15.277545756184905, delta=tol) + self.assertAlmostEqual(volume, 4.815115457307255, delta=tol) + self.assertAlmostEqual(surfaceArea, 15.219510760440258, delta=tol) # check some annotation groups: expectedSizes3d = { - "core": (176, 2.8838746320298183), - "shell": (112, 1.9560431675458367), - "kidney capsule": (288, 4.839917799575645) + "renal medulla": (80, 1.4917652106228603), + "cortex of kidney": (52, 3.147755999580444), + "kidney": (136, 4.81517966709125) } for name in expectedSizes3d: term = get_kidney_term(name) @@ -103,8 +103,9 @@ def test_renalcapsule(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "shell": (448, 37.84060276636123), - "kidney capsule": (920, 66.4453245855002) + "kidney capsule": (56, 15.219510760440258), + "anterior surface of kidney": (28, 7.609755380220144), + "posterior surface of kidney": (28, 7.609755380220144) } for name in expectedSizes2d: term = get_kidney_term(name) From b6067f83a3eb5dd694d754b5602e9ea7186b62bf Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 9 Sep 2025 10:51:02 +1200 Subject: [PATCH 29/43] Rename Renal Capsule scaffold to Kidney scaffold --- ...nal_capsule1.py => meshtype_3d_kidney1.py} | 94 +++++++++---------- src/scaffoldmaker/scaffolds.py | 7 +- tests/test_kidney.py | 4 +- 3 files changed, 52 insertions(+), 53 deletions(-) rename src/scaffoldmaker/meshtypes/{meshtype_3d_renal_capsule1.py => meshtype_3d_kidney1.py} (88%) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py similarity index 88% rename from src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py rename to src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index 6b897851..afa6f803 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_capsule1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -1,5 +1,5 @@ """ -Generates a 3D renal capsule using tube network mesh. +Generates a 3D kidney using tube network mesh. """ import math @@ -21,14 +21,14 @@ from cmlibs.zinc.node import Node -class MeshType_1d_renal_capsule_network_layout1(MeshType_1d_network_layout1): +class MeshType_1d_kidney_network_layout1(MeshType_1d_network_layout1): """ - Defines renal capsule network layout. + Defines kidney network layout. """ @classmethod def getName(cls): - return "1D Renal Capsule Network Layout 1" + return "1D Kidney Network Layout 1" @classmethod def getParameterSetNames(cls): @@ -40,9 +40,9 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Base parameter set"] = "Human 1" if (parameterSetName == "Default") else parameterSetName options["Define inner coordinates"] = True options["Elements count along"] = 2 - options["Renal capsule length"] = 1.0 - options["Renal capsule diameter"] = 1.5 - options["Renal capsule bend angle degrees"] = 10 + options["Kidney length"] = 1.0 + options["Kidney diameter"] = 1.5 + options["Kidney bend angle degrees"] = 10 options["Inner proportion default"] = 0.6 return options @@ -50,9 +50,9 @@ def getDefaultOptions(cls, parameterSetName="Default"): def getOrderedOptionNames(cls): return [ "Elements count along", - "Renal capsule length", - "Renal capsule diameter", - "Renal capsule bend angle degrees", + "Kidney length", + "Kidney diameter", + "Kidney bend angle degrees", "Inner proportion default" ] @@ -60,8 +60,8 @@ def getOrderedOptionNames(cls): def checkOptions(cls, options): dependentChanges = False for key in [ - "Renal capsule length", - "Renal capsule diameter" + "Kidney length", + "Kidney diameter" ]: if options[key] < 0.1: options[key] = 0.1 @@ -69,10 +69,10 @@ def checkOptions(cls, options): if options["Elements count along"] < 2: options["Elements count along"] = 2 - if options["Renal capsule bend angle degrees"] < 0.0: - options["Renal capsule bend angle degrees"] = 0.0 - elif options["Renal capsule bend angle degrees"] > 30.0: - options["Renal capsule bend angle degrees"] = 30.0 + if options["Kidney bend angle degrees"] < 0.0: + options["Kidney bend angle degrees"] = 0.0 + elif options["Kidney bend angle degrees"] > 30.0: + options["Kidney bend angle degrees"] = 30.0 if options["Inner proportion default"] < 0.1: options["Inner proportion default"] = 0.1 @@ -91,10 +91,10 @@ def generateBaseMesh(cls, region, options): """ # parameters structure = options["Structure"] = cls.getLayoutStructure(options) - capsuleElementsCount = options["Elements count along"] - capsuleLength = options["Renal capsule length"] - capsuleRadius = 0.5 * options["Renal capsule diameter"] - capsuleBendAngle = options["Renal capsule bend angle degrees"] + kidneyElementsCount = options["Elements count along"] + kidneyLength = options["Kidney length"] + kidneyRadius = 0.5 * options["Kidney diameter"] + kidneyBendAngle = options["Kidney bend angle degrees"] innerProportionDefault = options["Inner proportion default"] networkMesh = NetworkMesh(structure) @@ -110,7 +110,7 @@ def generateBaseMesh(cls, region, options): kidneyMeshGroup = kidneyGroup.getMeshGroup(mesh) elementIdentifier = 1 meshGroups = [kidneyMeshGroup] - for e in range(capsuleElementsCount): + for e in range(kidneyElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) @@ -124,36 +124,36 @@ def generateBaseMesh(cls, region, options): innerCoordinates = find_or_create_field_coordinates(fieldmodule, "inner coordinates") nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - # renal capsule + # Kidney nodeIdentifier = 1 - halfCapsuleLength = 0.5 * capsuleLength - capsuleScale = capsuleLength / capsuleElementsCount - bendAngleRadians = math.radians(capsuleBendAngle) + halfKidneyLength = 0.5 * kidneyLength + kidneyScale = kidneyLength / kidneyElementsCount + bendAngleRadians = math.radians(kidneyBendAngle) sinBendAngle = math.sin(bendAngleRadians) cosBendAngle = math.cos(bendAngleRadians) sinCurveAngle = math.sin(3 * bendAngleRadians) mx = [0.0, 0.0, 0.0] - d1 = [capsuleScale, 0.0, 0.0] - d3 = [0.0, 0.0, capsuleRadius] + d1 = [kidneyScale, 0.0, 0.0] + d3 = [0.0, 0.0, kidneyRadius] id3 = mult(d3, innerProportionDefault) - tx = halfCapsuleLength * -cosBendAngle - ty = halfCapsuleLength * -sinBendAngle + tx = halfKidneyLength * -cosBendAngle + ty = halfKidneyLength * -sinBendAngle sx = [tx, ty, 0.0] ex = [-tx, ty, 0.0] - sd1 = mult([1.0, sinCurveAngle, 0.0], capsuleScale) + sd1 = mult([1.0, sinCurveAngle, 0.0], kidneyScale) ed1 = [sd1[0], -sd1[1], sd1[2]] - nx, nd1 = sampleCubicHermiteCurves([sx, mx, ex], [sd1, d1, ed1], capsuleElementsCount)[0:2] + nx, nd1 = sampleCubicHermiteCurves([sx, mx, ex], [sd1, d1, ed1], kidneyElementsCount)[0:2] nd1 = smoothCubicHermiteDerivativesLine(nx, nd1) sd2_list = [] sd3_list = [] sNodeIdentifiers = [] - for e in range(capsuleElementsCount + 1): + for e in range(kidneyElementsCount + 1): sNodeIdentifiers.append(nodeIdentifier) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - sd2 = set_magnitude(cross(d3, nd1[e]), capsuleRadius) + sd2 = set_magnitude(cross(d3, nd1[e]), kidneyRadius) sid2 = mult(sd2, innerProportionDefault) sd2_list.append(sd2) sd3_list.append(d3) @@ -163,7 +163,7 @@ def generateBaseMesh(cls, region, options): sd12 = smoothCurveSideCrossDerivatives(nx, nd1, [sd2_list])[0] sd13 = smoothCurveSideCrossDerivatives(nx, nd1, [sd3_list])[0] - for e in range(capsuleElementsCount + 1): + for e in range(kidneyElementsCount + 1): node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) fieldcache.setNode(node) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12[e]) @@ -188,14 +188,14 @@ def getLayoutStructure(cls, options): return f"({'-'.join(str(i) for i in range(1, nodesCountAlong + 1))})" -class MeshType_3d_renal_capsule1(Scaffold_base): +class MeshType_3d_kidney1(Scaffold_base): """ - Generates a 3-D renal capsule. + Generates a 3-D Kidney. """ @classmethod def getName(cls): - return "3D Renal Capsule 1" + return "3D Kidney 1" @classmethod def getParameterSetNames(cls): @@ -209,7 +209,7 @@ def getDefaultOptions(cls, parameterSetName='Default'): options = {} useParameterSetName = "Human 1" if (parameterSetName == "Default") else parameterSetName options["Base parameter set"] = useParameterSetName - options["Renal capsule network layout"] = ScaffoldPackage(MeshType_1d_renal_capsule_network_layout1) + options["Kidney network layout"] = ScaffoldPackage(MeshType_1d_kidney_network_layout1) options["Elements count around"] = 8 options["Elements count through shell"] = 1 options["Annotation elements counts around"] = [0] @@ -222,7 +222,7 @@ def getDefaultOptions(cls, parameterSetName='Default'): @classmethod def getOrderedOptionNames(cls): optionNames = [ - "Renal capsule network layout", + "Kidney network layout", "Elements count around", "Elements count through shell", "Annotation elements counts around", @@ -234,8 +234,8 @@ def getOrderedOptionNames(cls): @classmethod def getOptionValidScaffoldTypes(cls, optionName): - if optionName == "Renal capsule network layout": - return [MeshType_1d_renal_capsule_network_layout1] + if optionName == "Kidney network layout": + return [MeshType_1d_kidney_network_layout1] return [] @@ -249,18 +249,18 @@ def getOptionScaffoldPackage(cls, optionName, scaffoldType, parameterSetName=Non assert parameterSetName in cls.getOptionScaffoldTypeParameterSetNames(optionName, scaffoldType), \ "Invalid parameter set " + str(parameterSetName) + " for scaffold " + str(scaffoldType.getName()) + \ " in option " + str(optionName) + " of scaffold " + cls.getName() - if optionName == "Renal capsule network layout": + if optionName == "Kidney network layout": if not parameterSetName: parameterSetName = "Default" - return ScaffoldPackage(MeshType_1d_renal_capsule_network_layout1, defaultParameterSetName=parameterSetName) + return ScaffoldPackage(MeshType_1d_kidney_network_layout1, defaultParameterSetName=parameterSetName) assert False, cls.__name__ + ".getOptionScaffoldPackage: Option " + optionName + " is not a scaffold" @classmethod def checkOptions(cls, options): dependentChanges = False - if (options["Renal capsule network layout"].getScaffoldType() not in - cls.getOptionValidScaffoldTypes("Renal capsule network layout")): - options["Renal capsule network layout"] = ScaffoldPackage(MeshType_1d_renal_capsule_network_layout1) + if (options["Kidney network layout"].getScaffoldType() not in + cls.getOptionValidScaffoldTypes("Kidney network layout")): + options["Kidney network layout"] = ScaffoldPackage(MeshType_1d_kidney_network_layout1) if options["Elements count around"] < 8: options["Elements count around"] = 8 @@ -335,7 +335,7 @@ def generateBaseMesh(cls, region, options): :param options: Dict containing options. See getDefaultOptions(). :return: list of AnnotationGroup, None """ - networkLayout = options["Renal capsule network layout"] + networkLayout = options["Kidney network layout"] layoutRegion = region.createRegion() networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters layoutAnnotationGroups = networkLayout.getAnnotationGroups() diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 6ef1df98..2489398f 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -44,8 +44,7 @@ from scaffoldmaker.meshtypes.meshtype_3d_nerve1 import MeshType_3d_nerve1 from scaffoldmaker.meshtypes.meshtype_3d_ostium1 import MeshType_3d_ostium1 from scaffoldmaker.meshtypes.meshtype_3d_ostium2 import MeshType_3d_ostium2 -from scaffoldmaker.meshtypes.meshtype_3d_renal_capsule1 import MeshType_1d_renal_capsule_network_layout1, \ - MeshType_3d_renal_capsule1 +from scaffoldmaker.meshtypes.meshtype_3d_kidney1 import MeshType_1d_kidney_network_layout1, MeshType_3d_kidney1 from scaffoldmaker.meshtypes.meshtype_3d_smallintestine1 import MeshType_3d_smallintestine1 from scaffoldmaker.meshtypes.meshtype_3d_solidcylinder1 import MeshType_3d_solidcylinder1 from scaffoldmaker.meshtypes.meshtype_3d_solidsphere1 import MeshType_3d_solidsphere1 @@ -105,6 +104,7 @@ class Scaffolds(object): MeshType_3d_heartventricles3, MeshType_3d_heartventriclesbase1, MeshType_3d_heartventriclesbase2, + MeshType_3d_kidney1, MeshType_3d_lens1, MeshType_3d_lung1, MeshType_3d_lung2, @@ -113,7 +113,6 @@ class Scaffolds(object): MeshType_3d_nerve1, MeshType_3d_ostium1, MeshType_3d_ostium2, - MeshType_3d_renal_capsule1, MeshType_3d_smallintestine1, MeshType_3d_solidcylinder1, MeshType_3d_solidsphere1, @@ -136,7 +135,7 @@ class Scaffolds(object): MeshType_1d_human_body_network_layout1, MeshType_1d_human_spinal_nerve_network_layout1, MeshType_1d_human_trigeminal_nerve_network_layout1, - MeshType_1d_renal_capsule_network_layout1, + MeshType_1d_kidney_network_layout1, MeshType_1d_uterus_network_layout1 ] diff --git a/tests/test_kidney.py b/tests/test_kidney.py index 02e98a56..a7010e82 100644 --- a/tests/test_kidney.py +++ b/tests/test_kidney.py @@ -10,7 +10,7 @@ from scaffoldmaker.annotation.annotationgroup import getAnnotationGroupForTerm from scaffoldmaker.annotation.kidney_terms import get_kidney_term -from scaffoldmaker.meshtypes.meshtype_3d_renal_capsule1 import MeshType_3d_renal_capsule1 +from scaffoldmaker.meshtypes.meshtype_3d_kidney1 import MeshType_3d_kidney1 from testutils import assertAlmostEqualList @@ -22,7 +22,7 @@ def test_kidney(self): """ Test creation of renal capsule scaffold. """ - scaffold = MeshType_3d_renal_capsule1 + scaffold = MeshType_3d_kidney1 parameterSetNames = scaffold.getParameterSetNames() self.assertEqual(parameterSetNames, ["Default", "Human 1"]) options = scaffold.getDefaultOptions("Human 1") From 826cd0b67884bcd172656c5f69ffa1830201554c Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 9 Sep 2025 15:32:25 +1200 Subject: [PATCH 30/43] Update parameters for kidney dimensions --- .../meshtypes/meshtype_3d_kidney1.py | 37 ++++++++++++++----- tests/test_kidney.py | 20 +++++----- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index afa6f803..031e443e 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -41,7 +41,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Define inner coordinates"] = True options["Elements count along"] = 2 options["Kidney length"] = 1.0 - options["Kidney diameter"] = 1.5 + options["Kidney width"] = 0.5 + options["Kidney thickness"] = 0.3 options["Kidney bend angle degrees"] = 10 options["Inner proportion default"] = 0.6 return options @@ -51,7 +52,8 @@ def getOrderedOptionNames(cls): return [ "Elements count along", "Kidney length", - "Kidney diameter", + "Kidney width", + "Kidney thickness", "Kidney bend angle degrees", "Inner proportion default" ] @@ -61,7 +63,8 @@ def checkOptions(cls, options): dependentChanges = False for key in [ "Kidney length", - "Kidney diameter" + "Kidney width", + "Kidney thickness" ]: if options[key] < 0.1: options[key] = 0.1 @@ -93,7 +96,9 @@ def generateBaseMesh(cls, region, options): structure = options["Structure"] = cls.getLayoutStructure(options) kidneyElementsCount = options["Elements count along"] kidneyLength = options["Kidney length"] - kidneyRadius = 0.5 * options["Kidney diameter"] + halfKidneyLength = 0.5 * kidneyLength + halfKidneyWidth = 0.5 * options["Kidney width"] + halfKidneyThickness = 0.5 * options["Kidney thickness"] kidneyBendAngle = options["Kidney bend angle degrees"] innerProportionDefault = options["Inner proportion default"] @@ -126,19 +131,21 @@ def generateBaseMesh(cls, region, options): # Kidney nodeIdentifier = 1 - halfKidneyLength = 0.5 * kidneyLength - kidneyScale = kidneyLength / kidneyElementsCount + tubeRadius = cls.getTubeRadius(halfKidneyWidth, halfKidneyThickness) * (halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) + extensionLength = 0.5 * (halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) + halfLayoutLength = (halfKidneyLength - tubeRadius - extensionLength) + kidneyScale = 2 * halfLayoutLength / kidneyElementsCount bendAngleRadians = math.radians(kidneyBendAngle) sinBendAngle = math.sin(bendAngleRadians) cosBendAngle = math.cos(bendAngleRadians) sinCurveAngle = math.sin(3 * bendAngleRadians) mx = [0.0, 0.0, 0.0] d1 = [kidneyScale, 0.0, 0.0] - d3 = [0.0, 0.0, kidneyRadius] + d3 = [0.0, 0.0, halfKidneyThickness] id3 = mult(d3, innerProportionDefault) - tx = halfKidneyLength * -cosBendAngle - ty = halfKidneyLength * -sinBendAngle + tx = halfLayoutLength * -cosBendAngle + ty = halfLayoutLength * -sinBendAngle sx = [tx, ty, 0.0] ex = [-tx, ty, 0.0] sd1 = mult([1.0, sinCurveAngle, 0.0], kidneyScale) @@ -153,7 +160,7 @@ def generateBaseMesh(cls, region, options): sNodeIdentifiers.append(nodeIdentifier) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - sd2 = set_magnitude(cross(d3, nd1[e]), kidneyRadius) + sd2 = set_magnitude(cross(d3, nd1[e]), halfKidneyWidth) sid2 = mult(sd2, innerProportionDefault) sd2_list.append(sd2) sd3_list.append(d3) @@ -187,6 +194,16 @@ def getLayoutStructure(cls, options): return f"({'-'.join(str(i) for i in range(1, nodesCountAlong + 1))})" + @classmethod + def getTubeRadius(cls, majorRadius, minorRadius): + """ + + """ + if majorRadius > minorRadius: + return math.pow((majorRadius / minorRadius), 1 / 3) + elif majorRadius < minorRadius: + return math.pow((minorRadius / majorRadius), 1 / 3) + class MeshType_3d_kidney1(Scaffold_base): """ diff --git a/tests/test_kidney.py b/tests/test_kidney.py index a7010e82..19ca213c 100644 --- a/tests/test_kidney.py +++ b/tests/test_kidney.py @@ -60,8 +60,8 @@ def test_kidney(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [-1.5508832466803322, -0.9195143646416805, -0.75], tol) - assertAlmostEqualList(self, maximums, [1.5508832466803322, 0.7499999999986053, 0.75], tol) + assertAlmostEqualList(self, minimums, [-0.4815964169156838, -0.2983818034875962, -0.15], tol) + assertAlmostEqualList(self, maximums, [0.4815964169156838, 0.25, 0.15], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -79,15 +79,15 @@ def test_kidney(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 4.815115457307255, delta=tol) - self.assertAlmostEqual(surfaceArea, 15.219510760440258, delta=tol) + self.assertAlmostEqual(volume, 0.09936614241399494, delta=tol) + self.assertAlmostEqual(surfaceArea, 1.2392639560968692, delta=tol) # check some annotation groups: expectedSizes3d = { - "renal medulla": (80, 1.4917652106228603), - "cortex of kidney": (52, 3.147755999580444), - "kidney": (136, 4.81517966709125) + "renal medulla": (80, 0.030779129384780158), + "cortex of kidney": (52, 0.06381933765967172), + "kidney": (136, 0.0993682249715619) } for name in expectedSizes3d: term = get_kidney_term(name) @@ -103,9 +103,9 @@ def test_kidney(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "kidney capsule": (56, 15.219510760440258), - "anterior surface of kidney": (28, 7.609755380220144), - "posterior surface of kidney": (28, 7.609755380220144) + "kidney capsule": (56, 1.2392639560968692), + "anterior surface of kidney": (28, 0.6196319780484342), + "posterior surface of kidney": (28, 0.6196319780484342) } for name in expectedSizes2d: term = get_kidney_term(name) From 594b161a1d051a3b07b2e93839b7b58e66dfc45f Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 12 Sep 2025 10:52:51 +1200 Subject: [PATCH 31/43] Add left and right kidneys --- src/scaffoldmaker/annotation/kidney_terms.py | 2 + .../meshtypes/meshtype_3d_kidney1.py | 145 +++++++++++------- 2 files changed, 92 insertions(+), 55 deletions(-) diff --git a/src/scaffoldmaker/annotation/kidney_terms.py b/src/scaffoldmaker/annotation/kidney_terms.py index d38ee327..f0592262 100644 --- a/src/scaffoldmaker/annotation/kidney_terms.py +++ b/src/scaffoldmaker/annotation/kidney_terms.py @@ -12,6 +12,7 @@ ("kidney capsule", "UBERON:0002015", "ILX:0733912"), ("lateral edge of kidney", ""), ("lateral surface of kidney", ""), + ("left kidney", "UBERON:0004538", "ILX:0725163"), ("major calyx", "UBERON:0001226", "ILX:0730785"), ("medial edge of kidney", ""), ("medial surface of kidney", ""), @@ -19,6 +20,7 @@ ("renal medulla", "UBERON:0000362", "ILX:0729114"), ("renal pelvis", "UBERON:0001224", "ILX:0723968"), ("renal pyramid", "UBERON:0004200", "ILX:0727514"), + ("right kidney", "UBERON:0004539", "ILX:0735697"), ("posterior surface of kidney", "UBERON:0035471", "ILX:0724479"), ("ventral surface of kidney", "") ] diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index 031e443e..63bd1941 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -39,10 +39,13 @@ def getDefaultOptions(cls, parameterSetName="Default"): options = {} options["Base parameter set"] = "Human 1" if (parameterSetName == "Default") else parameterSetName options["Define inner coordinates"] = True + options["Left kidney"] = True + options["Right kidney"] = True options["Elements count along"] = 2 options["Kidney length"] = 1.0 options["Kidney width"] = 0.5 - options["Kidney thickness"] = 0.3 + options["Kidney thickness"] = 0.4 + options["Left-right kidney spacing"] = 1.0 options["Kidney bend angle degrees"] = 10 options["Inner proportion default"] = 0.6 return options @@ -50,10 +53,13 @@ def getDefaultOptions(cls, parameterSetName="Default"): @classmethod def getOrderedOptionNames(cls): return [ + "Left kidney", + "Right kidney", "Elements count along", "Kidney length", "Kidney width", "Kidney thickness", + "Left-right kidney spacing", "Kidney bend angle degrees", "Inner proportion default" ] @@ -72,6 +78,9 @@ def checkOptions(cls, options): if options["Elements count along"] < 2: options["Elements count along"] = 2 + if options["Left-right kidney spacing"] < 0.0: + options["Left-right kidney spacing"] = 0.0 + if options["Kidney bend angle degrees"] < 0.0: options["Kidney bend angle degrees"] = 0.0 elif options["Kidney bend angle degrees"] > 30.0: @@ -95,10 +104,13 @@ def generateBaseMesh(cls, region, options): # parameters structure = options["Structure"] = cls.getLayoutStructure(options) kidneyElementsCount = options["Elements count along"] + isLeftKidney = options["Left kidney"] + isRightKidney = options["Right kidney"] kidneyLength = options["Kidney length"] halfKidneyLength = 0.5 * kidneyLength halfKidneyWidth = 0.5 * options["Kidney width"] halfKidneyThickness = 0.5 * options["Kidney thickness"] + spacing = 0.5 * options["Left-right kidney spacing"] kidneyBendAngle = options["Kidney bend angle degrees"] innerProportionDefault = options["Inner proportion default"] @@ -110,16 +122,16 @@ def generateBaseMesh(cls, region, options): # set up element annotations kidneyGroup = AnnotationGroup(region, get_kidney_term("kidney")) - annotationGroups = [kidneyGroup] - kidneyMeshGroup = kidneyGroup.getMeshGroup(mesh) - elementIdentifier = 1 - meshGroups = [kidneyMeshGroup] - for e in range(kidneyElementsCount): - element = mesh.findElementByIdentifier(elementIdentifier) - for meshGroup in meshGroups: - meshGroup.addElement(element) - elementIdentifier += 1 + + leftKidneyGroup = AnnotationGroup(region, get_kidney_term("left kidney")) + leftKidneyMeshGroup = leftKidneyGroup.getMeshGroup(mesh) + + rightKidneyGroup = AnnotationGroup(region, get_kidney_term("right kidney")) + rightKidneyMeshGroup = rightKidneyGroup.getMeshGroup(mesh) + + annotationGroups = [kidneyGroup, leftKidneyGroup, rightKidneyGroup] + meshGroups = [kidneyMeshGroup, leftKidneyMeshGroup, rightKidneyMeshGroup] # set coordinates (outer) fieldcache = fieldmodule.createFieldcache() @@ -131,7 +143,9 @@ def generateBaseMesh(cls, region, options): # Kidney nodeIdentifier = 1 - tubeRadius = cls.getTubeRadius(halfKidneyWidth, halfKidneyThickness) * (halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) + elementIdentifier = 1 + tubeRadius = cls.getTubeRadius(halfKidneyWidth, halfKidneyThickness) * ( + halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) extensionLength = 0.5 * (halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) halfLayoutLength = (halfKidneyLength - tubeRadius - extensionLength) kidneyScale = 2 * halfLayoutLength / kidneyElementsCount @@ -139,46 +153,63 @@ def generateBaseMesh(cls, region, options): sinBendAngle = math.sin(bendAngleRadians) cosBendAngle = math.cos(bendAngleRadians) sinCurveAngle = math.sin(3 * bendAngleRadians) - mx = [0.0, 0.0, 0.0] - d1 = [kidneyScale, 0.0, 0.0] - d3 = [0.0, 0.0, halfKidneyThickness] - id3 = mult(d3, innerProportionDefault) - - tx = halfLayoutLength * -cosBendAngle - ty = halfLayoutLength * -sinBendAngle - sx = [tx, ty, 0.0] - ex = [-tx, ty, 0.0] - sd1 = mult([1.0, sinCurveAngle, 0.0], kidneyScale) - ed1 = [sd1[0], -sd1[1], sd1[2]] - nx, nd1 = sampleCubicHermiteCurves([sx, mx, ex], [sd1, d1, ed1], kidneyElementsCount)[0:2] - nd1 = smoothCubicHermiteDerivativesLine(nx, nd1) - - sd2_list = [] - sd3_list = [] - sNodeIdentifiers = [] - for e in range(kidneyElementsCount + 1): - sNodeIdentifiers.append(nodeIdentifier) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - sd2 = set_magnitude(cross(d3, nd1[e]), halfKidneyWidth) - sid2 = mult(sd2, innerProportionDefault) - sd2_list.append(sd2) - sd3_list.append(d3) - for field, derivatives in ((coordinates, (nd1[e], sd2, d3)), (innerCoordinates, (nd1[e], sid2, id3))): - setNodeFieldParameters(field, fieldcache, nx[e], *derivatives) - nodeIdentifier += 1 - - sd12 = smoothCurveSideCrossDerivatives(nx, nd1, [sd2_list])[0] - sd13 = smoothCurveSideCrossDerivatives(nx, nd1, [sd3_list])[0] - for e in range(kidneyElementsCount + 1): - node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) - fieldcache.setNode(node) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12[e]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sd13[e]) - sid12 = mult(sd12[e], innerProportionDefault) - sid13 = mult(sd13[e], innerProportionDefault) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sid12) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sid13) + + leftKidney, rightKidney = 0, 1 + kidneys = [kidney for show, kidney in [(isLeftKidney, leftKidney), (isRightKidney, rightKidney)] if show] + for kidney in kidneys: + spacing = spacing if kidney is leftKidney else -spacing + mx = [0.0, 0.0, 0.0] + d1 = [kidneyScale, 0.0, 0.0] + d3 = [0.0, 0.0, halfKidneyThickness] + id3 = mult(d3, innerProportionDefault) + + tx = halfLayoutLength * -cosBendAngle + ty = halfLayoutLength * -sinBendAngle + sx = [tx, ty, 0.0] if kidney is leftKidney else [tx, -ty, 0.0] + ex = [-tx, ty, 0.0] if kidney is leftKidney else [-tx, -ty, 0.0] + sd1 = mult([1.0, sinCurveAngle, 0.0], kidneyScale) + ed1 = [sd1[0], -sd1[1], sd1[2]] + nx, nd1 = sampleCubicHermiteCurves([sx, mx, ex], [sd1, d1, ed1], kidneyElementsCount)[0:2] + nd1 = smoothCubicHermiteDerivativesLine(nx, nd1) + for c in range(kidneyElementsCount + 1): + nx[c][1] += spacing + + sd2_list = [] + sd3_list = [] + sNodeIdentifiers = [] + for e in range(kidneyElementsCount + 1): + sNodeIdentifiers.append(nodeIdentifier) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + sd2 = set_magnitude(cross(d3, nd1[e]), halfKidneyWidth) + sid2 = mult(sd2, innerProportionDefault) + sd2_list.append(sd2) + sd3_list.append(d3) + for field, derivatives in ((coordinates, (nd1[e], sd2, d3)), (innerCoordinates, (nd1[e], sid2, id3))): + setNodeFieldParameters(field, fieldcache, nx[e], *derivatives) + nodeIdentifier += 1 + + sd12 = smoothCurveSideCrossDerivatives(nx, nd1, [sd2_list])[0] + sd13 = smoothCurveSideCrossDerivatives(nx, nd1, [sd3_list])[0] + for e in range(kidneyElementsCount + 1): + node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) + fieldcache.setNode(node) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12[e]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sd13[e]) + sid12 = mult(sd12[e], innerProportionDefault) + sid13 = mult(sd13[e], innerProportionDefault) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sid12) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sid13) + + # add annotations + for e in range(kidneyElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroups[0].addElement(element) + if kidney is leftKidney and isLeftKidney: + meshGroups[1].addElement(element) + if kidney is rightKidney and isRightKidney: + meshGroups[2].addElement(element) + elementIdentifier += 1 return annotationGroups, networkMesh @@ -189,10 +220,14 @@ def getLayoutStructure(cls, options): :param options: Dict containing options. See getDefaultOptions(). :return string version of the 1D layout structure """ - nodesCountAlong = options["Elements count along"] + 1 - assert nodesCountAlong > 1 - - return f"({'-'.join(str(i) for i in range(1, nodesCountAlong + 1))})" + nodes_count = options["Elements count along"] + 1 + assert nodes_count > 1 + + left = f"({'-'.join(map(str, range(1, nodes_count + 1)))})" + if options["Left kidney"] and options["Right kidney"]: + right = f"({'-'.join(map(str, range(nodes_count + 1, 2 * nodes_count + 1)))})" + return f"{left},{right}" + return left @classmethod def getTubeRadius(cls, majorRadius, minorRadius): From 3538c76bb2c3d851204e2ecc3025d21e01ae2326 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 16 Sep 2025 12:34:28 +1200 Subject: [PATCH 32/43] Update annotations for left and right kidneys --- src/scaffoldmaker/annotation/kidney_terms.py | 34 ++- .../meshtypes/meshtype_3d_kidney1.py | 216 ++++++++++++++---- src/scaffoldmaker/utils/tubenetworkmesh.py | 108 +++++++-- tests/test_kidney.py | 32 +-- 4 files changed, 312 insertions(+), 78 deletions(-) diff --git a/src/scaffoldmaker/annotation/kidney_terms.py b/src/scaffoldmaker/annotation/kidney_terms.py index f0592262..89119ede 100644 --- a/src/scaffoldmaker/annotation/kidney_terms.py +++ b/src/scaffoldmaker/annotation/kidney_terms.py @@ -5,24 +5,56 @@ # convention: preferred name, preferred id, followed by any other ids and alternative names kidney_terms = [ ("anterior surface of kidney", "UBERON:0035368", "ILX:0724840"), + ("anterior surface of left kidney", ""), + ("anterior surface of right kidney", ""), ("cortex of kidney", "UBERON:0001225", "ILX:0726853"), + ("cortex of left kidney", "ILX:0791219"), + ("cortex of right kidney", "ILX:0791182"), ("dorsal surface of kidney", ""), + ("dorsal surface of left kidney", ""), + ("dorsal surface of right kidney", ""), ("hilum of kidney", "UBERON:0008716", "ILX:0731719"), + ("hilum of left kidney", ""), + ("hilum of right kidney", ""), + ("inner medulla of left kidney", "ILX:0784932"), + ("inner medulla of right kidney", "ILX:0791193"), + ("juxtamedullary cortex", "UBERON:0005271", "ILX:0730126"), + ("juxtamedullary cortex surface of kidney", ""), + ("juxtamedullary cortex surface of left kidney", ""), + ("juxtamedullary cortex surface of right kidney", ""), ("kidney", "UBERON:0002113", "ILX:0735723"), ("kidney capsule", "UBERON:0002015", "ILX:0733912"), ("lateral edge of kidney", ""), + ("lateral edge of left kidney", ""), + ("lateral edge of right kidney", ""), ("lateral surface of kidney", ""), + ("lateral surface of left kidney", ""), + ("lateral surface of right kidney", ""), ("left kidney", "UBERON:0004538", "ILX:0725163"), + ("left kidney capsule", ""), ("major calyx", "UBERON:0001226", "ILX:0730785"), ("medial edge of kidney", ""), + ("medial edge of left kidney", ""), + ("medial edge of right kidney", ""), ("medial surface of kidney", ""), + ("medial surface of left kidney", ""), + ("medial surface of right kidney", ""), + ("medulla of left kidney", ""), + ("medulla of right kidney", ""), ("minor calyx", "UBERON:0001227", "ILX:0730473"), + ("outer medulla of left kidney", ""), + ("outer medulla of right kidney", ""), ("renal medulla", "UBERON:0000362", "ILX:0729114"), ("renal pelvis", "UBERON:0001224", "ILX:0723968"), ("renal pyramid", "UBERON:0004200", "ILX:0727514"), ("right kidney", "UBERON:0004539", "ILX:0735697"), + ("right kidney capsule", ""), ("posterior surface of kidney", "UBERON:0035471", "ILX:0724479"), - ("ventral surface of kidney", "") + ("posterior surface of left kidney", ""), + ("posterior surface of right kidney", ""), + ("ventral surface of kidney", ""), + ("ventral surface of left kidney", ""), + ("ventral surface of right kidney", "") ] def get_kidney_term(name : str): diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index 63bd1941..faac83fb 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -15,9 +15,9 @@ from scaffoldmaker.scaffoldpackage import ScaffoldPackage from scaffoldmaker.utils.interpolation import smoothCubicHermiteDerivativesLine, sampleCubicHermiteCurves, \ smoothCurveSideCrossDerivatives +from scaffoldmaker.utils.meshrefinement import MeshRefinement from scaffoldmaker.utils.networkmesh import NetworkMesh -from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData, \ - KidneyTubeNetworkMeshBuilder +from scaffoldmaker.utils.tubenetworkmesh import KidneyTubeNetworkMeshBuilder, TubeNetworkMeshGenerateData from cmlibs.zinc.node import Node @@ -26,6 +26,8 @@ class MeshType_1d_kidney_network_layout1(MeshType_1d_network_layout1): Defines kidney network layout. """ + showKidneys = [False, False] + @classmethod def getName(cls): return "1D Kidney Network Layout 1" @@ -91,6 +93,10 @@ def checkOptions(cls, options): elif options["Inner proportion default"] > 0.9: options["Inner proportion default"] = 0.9 + if not options["Left kidney"] and not options["Right kidney"]: + dependentChanges = True + options["Left kidney"] = True + return dependentChanges @classmethod @@ -99,7 +105,7 @@ def generateBaseMesh(cls, region, options): Generate the unrefined mesh. :param region: Zinc region to define model in. Must be empty. :param options: Dict containing options. See getDefaultOptions(). - :return [] empty list of AnnotationGroup, NetworkMesh + :return: [] empty list of AnnotationGroup, NetworkMesh """ # parameters structure = options["Structure"] = cls.getLayoutStructure(options) @@ -113,6 +119,7 @@ def generateBaseMesh(cls, region, options): spacing = 0.5 * options["Left-right kidney spacing"] kidneyBendAngle = options["Kidney bend angle degrees"] innerProportionDefault = options["Inner proportion default"] + cls.setShowKidneys(options) networkMesh = NetworkMesh(structure) networkMesh.create1DLayoutMesh(region) @@ -145,7 +152,7 @@ def generateBaseMesh(cls, region, options): nodeIdentifier = 1 elementIdentifier = 1 tubeRadius = cls.getTubeRadius(halfKidneyWidth, halfKidneyThickness) * ( - halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) + halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) extensionLength = 0.5 * (halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) halfLayoutLength = (halfKidneyLength - tubeRadius - extensionLength) kidneyScale = 2 * halfLayoutLength / kidneyElementsCount @@ -218,7 +225,7 @@ def getLayoutStructure(cls, options): """ Generate 1D layout structure based on the number of elements count along. :param options: Dict containing options. See getDefaultOptions(). - :return string version of the 1D layout structure + :return: string version of the 1D layout structure """ nodes_count = options["Elements count along"] + 1 assert nodes_count > 1 @@ -239,6 +246,15 @@ def getTubeRadius(cls, majorRadius, minorRadius): elif majorRadius < minorRadius: return math.pow((minorRadius / majorRadius), 1 / 3) + @classmethod + def getShowKidneys(cls): + return cls.showKidneys + + @classmethod + def setShowKidneys(cls, options): + cls.showKidneys[0] = True if options["Left kidney"] else False + cls.showKidneys[1] = True if options["Right kidney"] else False + class MeshType_3d_kidney1(Scaffold_base): """ @@ -269,6 +285,8 @@ def getDefaultOptions(cls, parameterSetName='Default'): options["Number of elements across core box minor"] = 2 options["Number of elements across core transition"] = 1 options["Annotation numbers of elements across core box minor"] = [0] + options["Refine"] = False + options["Refine number of elements"] = 4 return options @classmethod @@ -281,7 +299,10 @@ def getOrderedOptionNames(cls): "Target element density along longest segment", "Number of elements across core box minor", "Number of elements across core transition", - "Annotation numbers of elements across core box minor"] + "Annotation numbers of elements across core box minor", + "Refine", + "Refine number of elements" + ] return optionNames @classmethod @@ -290,7 +311,6 @@ def getOptionValidScaffoldTypes(cls, optionName): return [MeshType_1d_kidney_network_layout1] return [] - @classmethod def getOptionScaffoldPackage(cls, optionName, scaffoldType, parameterSetName=None): """ @@ -377,6 +397,9 @@ def checkOptions(cls, options): if options["Target element density along longest segment"] < 2.0: options["Target element density along longest segment"] = 2.0 + + if options['Refine number of elements'] < 1: + options['Refine number of elements'] = 1 return dependentChanges @classmethod @@ -392,8 +415,9 @@ def generateBaseMesh(cls, region, options): networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters layoutAnnotationGroups = networkLayout.getAnnotationGroups() networkMesh = networkLayout.getConstructionObject() + showKidneys = getShowKidneysSettings() - tubeNetworkMeshBuilder = KidneyTubeNetworkMeshBuilder( + kidneyTubeNetworkMeshBuilder = KidneyTubeNetworkMeshBuilder( networkMesh, targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], defaultElementsCountAround=options["Elements count around"], @@ -402,14 +426,15 @@ def generateBaseMesh(cls, region, options): isCore=True, elementsCountTransition=options["Number of elements across core transition"], defaultElementsCountCoreBoxMinor=options["Number of elements across core box minor"], - annotationElementsCountsCoreBoxMinor=options["Annotation numbers of elements across core box minor"] + annotationElementsCountsCoreBoxMinor=options["Annotation numbers of elements across core box minor"], + showKidneys=showKidneys ) - tubeNetworkMeshBuilder.build() + kidneyTubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( region, 3, isLinearThroughShell=False) - tubeNetworkMeshBuilder.generateMesh(generateData) + kidneyTubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() # add kidney-specific annotation groups @@ -436,6 +461,18 @@ def generateBaseMesh(cls, region, options): return annotationGroups, None + @classmethod + def refineMesh(cls, meshRefinement, options): + """ + Refine source mesh into separate region, with change of basis. + :param meshRefinement: MeshRefinement, which knows source and target region. + :param options: Dict containing options. See getDefaultOptions(). + """ + assert isinstance(meshRefinement, MeshRefinement) + refineElementsCount = options['Refine number of elements'] + meshRefinement.refineAllElementsCubeStandard3d(refineElementsCount, refineElementsCount, refineElementsCount) + + @classmethod def defineFaceAnnotations(cls, region, options, annotationGroups): """ @@ -446,47 +483,146 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): :param annotationGroups: List of annotation groups for top-level elements. New face annotation groups are appended to this list. """ + show_kidneys = getShowKidneysSettings() + + # Initialize field module and meshes fm = region.getFieldmodule() mesh1d = fm.findMeshByDimension(1) mesh2d = fm.findMeshByDimension(2) - + mesh3d = fm.findMeshByDimension(3) is_exterior = fm.createFieldIsExterior() - kidneyGroup = getAnnotationGroupForTerm(annotationGroups, get_kidney_term("kidney")).getGroup() - # surface groups - kidneyCapsuleGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("kidney capsule")) - kidneyCapsuleGroup.getMeshGroup(mesh2d).addElementsConditional(fm.createFieldAnd(kidneyGroup, is_exterior)) + # Get base kidney group + kidney_group = getAnnotationGroupForTerm(annotationGroups, get_kidney_term("kidney")).getGroup() - surfaceTypes = [ - "anterior", "posterior", - "lateral", "medial", - "dorsal", "ventral" - ] + # Create side groups and tracking dictionaries + side_groups = {} + side_kidney_groups = {"left": {}, "right": {}} + + for side in ["left", "right"]: + group = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(f"{side} kidney")) + side_groups[side] = group.getGroup() + side_kidney_groups[side][f"{side} kidney"] = group - arb_group = {} - kidney_exterior = {} - for surfaceType in surfaceTypes: - group = getAnnotationGroupForTerm(annotationGroups, (surfaceType, "")) - group2d = group.getGroup() - group2d_exterior = fm.createFieldAnd(group2d, is_exterior) + # Create kidney part groups (cortex, hilum, medulla) + create_kidney_part_groups(fm, mesh3d, annotationGroups, region, side_groups, side_kidney_groups) - term = f"{surfaceType} surface of kidney" - surfaceGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(term)) - surfaceGroup.getMeshGroup(mesh2d).addElementsConditional(group2d_exterior) + # Create capsule groups + create_capsule_groups(fm, mesh2d, annotationGroups, region, kidney_group, side_groups, side_kidney_groups, is_exterior) - arb_group.update({surfaceType: group2d}) - kidney_exterior.update({term: group2d_exterior}) + # Create surface and edge annotation groups + create_surface_and_edge_groups(fm, mesh2d, mesh1d, annotationGroups, region, side_groups, side_kidney_groups, is_exterior) - # edge groups - dorsalVentralBorderGroup = fm.createFieldAnd(fm.createFieldAnd(arb_group["dorsal"], arb_group["ventral"]), is_exterior) + # Remove groups based on kidney visibility settings + remove_hidden_kidney_groups(show_kidneys, side_kidney_groups, annotationGroups) - for surfaceType in ["lateral", "medial"]: - term = f"{surfaceType} edge of kidney" - edgeGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(term)) - edgeGroup.getMeshGroup(mesh1d).addElementsConditional(fm.createFieldAnd(arb_group[surfaceType], dorsalVentralBorderGroup)) - # for term in ["lateral", "medial", "dorsal", "ventral"]: - # annotationGroups.remove(findAnnotationGroupByName(annotationGroups, term)) +def getShowKidneysSettings(): + return MeshType_1d_kidney_network_layout1.getShowKidneys() + + +def create_kidney_part_groups(fm, mesh3d, annotationGroups, region, side_groups, side_kidney_groups): + """ + Create cortex, hilum, and medulla groups for each kidney side. + """ + kidney_parts = ["cortex", "hilum", "medulla"] + + for part in kidney_parts: + # Get the anatomical term for the part + arb_term = f"renal {part}" if part == "medulla" else f"{part} of kidney" + arb_group = getAnnotationGroupForTerm(annotationGroups, get_kidney_term(arb_term)).getGroup() + + # Create side-specific part groups + for side in ["left", "right"]: + part_term = f"{part} of {side} kidney" + part_group = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(part_term)) + part_group.getMeshGroup(mesh3d).addElementsConditional(fm.createFieldAnd(arb_group, side_groups[side])) + side_kidney_groups[side][part_term] = part_group + + +def create_capsule_groups(fm, mesh2d, annotationGroups, region, kidney_group, side_groups, side_kidney_groups, is_exterior): + """ + Create kidney capsule surface groups. + """ + # General kidney capsule group + kidney_capsule_group = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("kidney capsule")) + kidney_exterior = fm.createFieldAnd(kidney_group, is_exterior) + kidney_capsule_group.getMeshGroup(mesh2d).addElementsConditional(kidney_exterior) + + # Side-specific capsule groups + for side in ["left", "right"]: + capsule_term = f"{side} kidney capsule" + capsule_group = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(capsule_term)) + capsule_exterior = fm.createFieldAnd(side_groups[side], is_exterior) + capsule_group.getMeshGroup(mesh2d).addElementsConditional(capsule_exterior) + side_kidney_groups[side][capsule_term] = capsule_group + + +def create_surface_and_edge_groups(fm, mesh2d, mesh1d, annotationGroups, region, side_groups, side_kidney_groups, is_exterior): + """ + Create surface and edge annotation groups. + """ + surface_types = ["anterior", "posterior", "lateral", "medial", "dorsal", "ventral", "juxtamedullary cortex"] + surface_fields = {} + + # Create surface groups + for surface_type in surface_types: + if surface_type == "juxtamedullary cortex": + cortex_group = getAnnotationGroupForTerm(annotationGroups, get_kidney_term("cortex of kidney")).getGroup() + medulla_group = getAnnotationGroupForTerm(annotationGroups, get_kidney_term("renal medulla")).getGroup() + surface_exterior = fm.createFieldAnd(medulla_group, cortex_group) + else: + base_group = getAnnotationGroupForTerm(annotationGroups, (surface_type, "")) + surface_field = base_group.getGroup() + surface_exterior = fm.createFieldAnd(surface_field, is_exterior) + surface_fields[surface_type] = surface_field + + # General kidney surface group + general_term = f"{surface_type} surface of kidney" + general_surface_group = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(general_term)) + general_surface_group.getMeshGroup(mesh2d).addElementsConditional(surface_exterior) + + # Side-specific surface groups + for side in ["left", "right"]: + side_term = f"{surface_type} surface of {side} kidney" + side_surface_group = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(side_term)) + side_surface_field = fm.createFieldAnd(surface_exterior, side_groups[side]) + side_surface_group.getMeshGroup(mesh2d).addElementsConditional(side_surface_field) + side_kidney_groups[side][side_term] = side_surface_group + + # Create edge groups at dorsal-ventral intersection + dorsal_ventral_border = fm.createFieldAnd( + fm.createFieldAnd(surface_fields["dorsal"], surface_fields["ventral"]), is_exterior) + + edge_types = ["lateral", "medial"] + + for edge_type in edge_types: + # General edge group + edge_term = f"{edge_type} edge of kidney" + edge_field = fm.createFieldAnd(surface_fields[edge_type], dorsal_ventral_border) + general_edge_group = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(edge_term)) + general_edge_group.getMeshGroup(mesh1d).addElementsConditional(edge_field) + + # Side-specific edge groups + for side in ["left", "right"]: + side_edge_term = f"{edge_type} edge of {side} kidney" + side_edge_group = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term(side_edge_term)) + side_edge_field = fm.createFieldAnd(edge_field, side_groups[side]) + side_edge_group.getMeshGroup(mesh1d).addElementsConditional(side_edge_field) + side_kidney_groups[side][side_edge_term] = side_edge_group + + +def remove_hidden_kidney_groups(show_kidneys, side_kidney_groups, annotationGroups): + """ + Remove annotation groups for kidneys that should not be shown. + """ + if not show_kidneys[0]: # Left kidney + for group in side_kidney_groups["left"].values(): + annotationGroups.remove(group) + + if not show_kidneys[1]: # Right kidney + for group in side_kidney_groups["right"].values(): + annotationGroups.remove(group) def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None): diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 1f02dd38..135bc98e 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -4456,6 +4456,26 @@ class KidneyTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): Specialization of TubeNetworkMeshBuilder adding annotations for anterior, posterior, lateral, medial, and hilum regions. """ + def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, + layoutAnnotationGroups: list=[], annotationElementsCountsAlong: list=[], + defaultElementsCountAround: int=8, annotationElementsCountsAround: list=[], + elementsCountThroughShell: int=1, isCore=False, elementsCountTransition: int=1, + defaultElementsCountCoreBoxMinor: int=2, annotationElementsCountsCoreBoxMinor: list=[], + defaultCoreBoundaryScalingMode=1, annotationCoreBoundaryScalingMode=[], + useOuterTrimSurfaces=True, showKidneys=[]): + """ + Builds specialized continuous tube network meshes for kidney scaffold. + :param showKidneys: List of flags for showing left and/or right kidneys. + """ + super(KidneyTubeNetworkMeshBuilder, self).__init__( + networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups, annotationElementsCountsAlong, + defaultElementsCountAround, annotationElementsCountsAround, elementsCountThroughShell, isCore, + elementsCountTransition, defaultElementsCountCoreBoxMinor, annotationElementsCountsCoreBoxMinor, + defaultCoreBoundaryScalingMode, annotationCoreBoundaryScalingMode, useOuterTrimSurfaces) + + self._showKidneys = showKidneys + + def generateMesh(self, generateData): super(KidneyTubeNetworkMeshBuilder, self).generateMesh(generateData) # build anterior, posterior, lateral, medial annotation groups @@ -4471,31 +4491,77 @@ def generateMesh(self, generateData): halfElementsCountAround = elementsCountAround // 2 increment = max(1, elementsCountAround // 8) - e1Start = halfElementsCountAround - increment - e1End = halfElementsCountAround + increment + leftKidney, rightKidney = 0, 1 + # Kidney configuration mapping + kidney_configs = { + leftKidney: { + 'lateral_flag': False, + 'medial_flag': True, + 'e1_start': halfElementsCountAround - increment, + 'e1_end': halfElementsCountAround + increment + }, + rightKidney: { + 'lateral_flag': True, + 'medial_flag': False, + 'e1_start': -increment, + 'e1_end': increment + } + } + + def add_common_elements(mesh_obj, method_prefix=""): + """ + Add D1 and D3 elements that are common to all kidneys. + :param mesh_obj: The mesh object (segment or capMesh) to add elements to + :param method_prefix: Prefix for method names (e.g., "addCap" for capMesh methods) + """ + d1_method = f"{method_prefix}SideD1ElementsToMeshGroup" + d3_method = f"{method_prefix}SideD3ElementsToMeshGroup" + + getattr(mesh_obj, d1_method)(False, anteriorMeshGroup) + getattr(mesh_obj, d1_method)(True, posteriorMeshGroup) + getattr(mesh_obj, d3_method)(False, ventralMeshGroup) + getattr(mesh_obj, d3_method)(True, dorsalMeshGroup) + + def add_kidney_specific_elements(mesh_obj, kidney, method_prefix=""): + """ + Add kidney-specific D2 elements. + :param mesh_obj: The mesh object (segment or capMesh) to add elements to + :param kidney: Kidney identifier (leftKidney=0, rightKidney=1) + :param method_prefix: Prefix for method names (e.g., "addCap" for capMesh methods) + """ + if kidney not in kidney_configs: + return + + config = kidney_configs[kidney] + d2_method = f"{method_prefix}SideD2ElementsToMeshGroup" + + getattr(mesh_obj, d2_method)(config['lateral_flag'], lateralMeshGroup) + getattr(mesh_obj, d2_method)(config['medial_flag'], medialMeshGroup) + + # Shell opening elements only for segment (not capMesh) + if "Cap" not in method_prefix: + mesh_obj.addShellOpeningElementsToMeshGroup(config['e1_start'], config['e1_end'], openingMeshGroup) + + for kidney in [leftKidney, rightKidney]: + if not self._showKidneys[kidney]: + continue - for networkSegment in self._networkMesh.getNetworkSegments(): + idx = 0 if False in self._showKidneys else kidney + networkSegment = self._networkMesh.getNetworkSegments()[idx] segment = self._segments[networkSegment] segmentCaps = segment.getIsCap() - if True in segmentCaps: - capMesh = segment.getCapMesh() - else: - capMesh = None - # segment on main axis - segment.addSideD1ElementsToMeshGroup(True, anteriorMeshGroup) - segment.addSideD1ElementsToMeshGroup(False, posteriorMeshGroup) - segment.addSideD2ElementsToMeshGroup(False, lateralMeshGroup) - segment.addSideD2ElementsToMeshGroup(True, medialMeshGroup) - segment.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) - segment.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) - segment.addShellOpeningElementsToMeshGroup(e1Start, e1End, openingMeshGroup) + capMesh = segment.getCapMesh() if True in segmentCaps else None + + # Apply common elements to segment + add_common_elements(segment, "add") + + # Apply kidney-specific elements to segment + add_kidney_specific_elements(segment, kidney, "add") + + # Apply elements to capMesh if it exists if capMesh: - capMesh.addCapSideD1ElementsToMeshGroup(True, anteriorMeshGroup) - capMesh.addCapSideD1ElementsToMeshGroup(False, posteriorMeshGroup) - capMesh.addCapSideD2ElementsToMeshGroup(False, lateralMeshGroup) - capMesh.addCapSideD2ElementsToMeshGroup(True, medialMeshGroup) - capMesh.addCapSideD3ElementsToMeshGroup(False, ventralMeshGroup) - capMesh.addCapSideD3ElementsToMeshGroup(True, dorsalMeshGroup) + add_common_elements(capMesh, "addCap") + add_kidney_specific_elements(capMesh, kidney, "addCap") class TubeEllipseGenerator: diff --git a/tests/test_kidney.py b/tests/test_kidney.py index 19ca213c..f4113669 100644 --- a/tests/test_kidney.py +++ b/tests/test_kidney.py @@ -27,7 +27,7 @@ def test_kidney(self): self.assertEqual(parameterSetNames, ["Default", "Human 1"]) options = scaffold.getDefaultOptions("Human 1") - self.assertEqual(9, len(options)) + self.assertEqual(11, len(options)) self.assertEqual(8, options["Elements count around"]) self.assertEqual(1, options["Elements count through shell"]) self.assertEqual([0], options["Annotation elements counts around"]) @@ -40,18 +40,18 @@ def test_kidney(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(19, len(annotationGroups)) + self.assertEqual(45, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(136, mesh3d.getSize()) + self.assertEqual(136 * 2, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(436, mesh2d.getSize()) + self.assertEqual(436 * 2, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(478, mesh1d.getSize()) + self.assertEqual(478 * 2, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(179, nodes.getSize()) + self.assertEqual(179 * 2, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -60,8 +60,8 @@ def test_kidney(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [-0.4815964169156838, -0.2983818034875962, -0.15], tol) - assertAlmostEqualList(self, maximums, [0.4815964169156838, 0.25, 0.15], tol) + assertAlmostEqualList(self, minimums, [-0.47969110977192125, -0.75, -0.2], tol) + assertAlmostEqualList(self, maximums, [0.47969110977192125, 0.75, 0.2], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -79,15 +79,15 @@ def test_kidney(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.09936614241399494, delta=tol) - self.assertAlmostEqual(surfaceArea, 1.2392639560968692, delta=tol) + self.assertAlmostEqual(volume, 0.26271882980819067, delta=tol) + self.assertAlmostEqual(surfaceArea, 2.7967266004246665, delta=tol) # check some annotation groups: expectedSizes3d = { - "renal medulla": (80, 0.030779129384780158), - "cortex of kidney": (52, 0.06381933765967172), - "kidney": (136, 0.0993682249715619) + "renal medulla": (80 * 2, 0.08077413857088181), + "cortex of kidney": (52 * 2, 0.17260778416183756), + "kidney": (136 * 2, 0.2627234820829377) } for name in expectedSizes3d: term = get_kidney_term(name) @@ -103,9 +103,9 @@ def test_kidney(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "kidney capsule": (56, 1.2392639560968692), - "anterior surface of kidney": (28, 0.6196319780484342), - "posterior surface of kidney": (28, 0.6196319780484342) + "kidney capsule": (56 * 2, 2.7967266004246665), + "anterior surface of kidney": (28 * 2, 1.3983633002123297), + "posterior surface of kidney": (28 * 2, 1.3983633002123297) } for name in expectedSizes2d: term = get_kidney_term(name) From 13e131eaf9bd5f6850a4ba43de997a2e58a92160 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 16 Sep 2025 12:45:31 +1200 Subject: [PATCH 33/43] Update unit test --- tests/test_kidney.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_kidney.py b/tests/test_kidney.py index f4113669..d933ceac 100644 --- a/tests/test_kidney.py +++ b/tests/test_kidney.py @@ -40,7 +40,7 @@ def test_kidney(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(45, len(annotationGroups)) + self.assertEqual(48, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) From 4a030e919cfae2ce9c08e27b46e3011a2505995f Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Thu, 30 Oct 2025 15:30:28 +1300 Subject: [PATCH 34/43] Add point markers --- src/scaffoldmaker/annotation/kidney_terms.py | 4 + .../meshtypes/meshtype_3d_kidney1.py | 86 +++++++++++++++---- src/scaffoldmaker/utils/capmesh.py | 8 +- src/scaffoldmaker/utils/networkmesh.py | 6 ++ src/scaffoldmaker/utils/tubenetworkmesh.py | 8 +- tests/test_kidney.py | 10 +-- 6 files changed, 95 insertions(+), 27 deletions(-) diff --git a/src/scaffoldmaker/annotation/kidney_terms.py b/src/scaffoldmaker/annotation/kidney_terms.py index 89119ede..f9e535a2 100644 --- a/src/scaffoldmaker/annotation/kidney_terms.py +++ b/src/scaffoldmaker/annotation/kidney_terms.py @@ -16,6 +16,8 @@ ("hilum of kidney", "UBERON:0008716", "ILX:0731719"), ("hilum of left kidney", ""), ("hilum of right kidney", ""), + ("inferior pole of left kidney", "FMA:15609"), + ("inferior pole of right kidney", "FMA:15608"), ("inner medulla of left kidney", "ILX:0784932"), ("inner medulla of right kidney", "ILX:0791193"), ("juxtamedullary cortex", "UBERON:0005271", "ILX:0730126"), @@ -52,6 +54,8 @@ ("posterior surface of kidney", "UBERON:0035471", "ILX:0724479"), ("posterior surface of left kidney", ""), ("posterior surface of right kidney", ""), + ("superior pole of left kidney", "FMA:15607"), + ("superior pole of right kidney", "FMA:15606"), ("ventral surface of kidney", ""), ("ventral surface of left kidney", ""), ("ventral surface of right kidney", "") diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index faac83fb..f567273f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -4,7 +4,8 @@ import math from cmlibs.maths.vectorops import mult, set_magnitude, cross -from cmlibs.utils.zinc.field import find_or_create_field_coordinates +from cmlibs.utils.zinc.field import find_or_create_field_coordinates, findOrCreateFieldCoordinates +from cmlibs.utils.zinc.general import ChangeManager from cmlibs.zinc.field import Field from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm, \ @@ -278,9 +279,9 @@ def getDefaultOptions(cls, parameterSetName='Default'): useParameterSetName = "Human 1" if (parameterSetName == "Default") else parameterSetName options["Base parameter set"] = useParameterSetName options["Kidney network layout"] = ScaffoldPackage(MeshType_1d_kidney_network_layout1) - options["Elements count around"] = 8 - options["Elements count through shell"] = 1 - options["Annotation elements counts around"] = [0] + options["Number of elements around"] = 8 + options["Number of elements through shell"] = 1 + options["Annotation numbers of elements around"] = [0] options["Target element density along longest segment"] = 2.0 options["Number of elements across core box minor"] = 2 options["Number of elements across core transition"] = 1 @@ -293,9 +294,9 @@ def getDefaultOptions(cls, parameterSetName='Default'): def getOrderedOptionNames(cls): optionNames = [ "Kidney network layout", - "Elements count around", - "Elements count through shell", - "Annotation elements counts around", + "Number of elements around", + "Number of elements through shell", + "Annotation numbers of elements around", "Target element density along longest segment", "Number of elements across core box minor", "Number of elements across core transition", @@ -334,13 +335,13 @@ def checkOptions(cls, options): cls.getOptionValidScaffoldTypes("Kidney network layout")): options["Kidney network layout"] = ScaffoldPackage(MeshType_1d_kidney_network_layout1) - if options["Elements count around"] < 8: - options["Elements count around"] = 8 - elif options["Elements count around"] % 4: - options["Elements count around"] += 4 - (options["Elements count around"] % 4) + if options["Number of elements around"] < 8: + options["Number of elements around"] = 8 + elif options["Number of elements around"] % 4: + options["Number of elements around"] += 4 - (options["Elements count around"] % 4) - if options["Elements count through shell"] < 1: - options["Elements count through shell"] = 1 + if options["Number of elements through shell"] < 1: + options["Number of elements through shell"] = 1 if options["Number of elements across core transition"] < 1: options["Number of elements across core transition"] = 1 @@ -357,7 +358,7 @@ def checkOptions(cls, options): annotationElementsCountsAround = options["Annotation elements counts around"] if len(annotationElementsCountsAround) == 0: - options["Annotation elements count around"] = [0] + options["Annotation numbers of elements around"] = [0] else: for i in range(len(annotationElementsCountsAround)): if annotationElementsCountsAround[i] <= 0: @@ -379,7 +380,7 @@ def checkOptions(cls, options): dependentChanges = True for i in range(len(annotationCoreBoxMinorCounts)): aroundCount = annotationElementsCountsAround[i] if annotationElementsCountsAround[i] \ - else options["Elements count around"] + else options["Number of elements around"] maxCoreBoxMinorCount = aroundCount // 2 - 2 if annotationCoreBoxMinorCounts[i] <= 0: annotationCoreBoxMinorCounts[i] = 0 @@ -420,8 +421,8 @@ def generateBaseMesh(cls, region, options): kidneyTubeNetworkMeshBuilder = KidneyTubeNetworkMeshBuilder( networkMesh, targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], - defaultElementsCountAround=options["Elements count around"], - elementsCountThroughShell=options["Elements count through shell"], + defaultElementsCountAround=options["Number of elements around"], + elementsCountThroughShell=options["Number of elements through shell"], layoutAnnotationGroups=layoutAnnotationGroups, isCore=True, elementsCountTransition=options["Number of elements across core transition"], @@ -439,12 +440,17 @@ def generateBaseMesh(cls, region, options): # add kidney-specific annotation groups fm = region.getFieldmodule() + coordinates = findOrCreateFieldCoordinates(fm) mesh = generateData.getMesh() + nodes = fm.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) coreGroup = getAnnotationGroupForTerm(annotationGroups, ("core", "")).getGroup() shellGroup = getAnnotationGroupForTerm(annotationGroups, ("shell", "")).getGroup() openingGroup = getAnnotationGroupForTerm(annotationGroups, ("opening", "")).getGroup() + kidneyGroup = AnnotationGroup(region, get_kidney_term("kidney")) + kidneyNodesetGroup = kidneyGroup.getNodesetGroup(nodes) + hilumGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("hilum of kidney")) hilumGroup.getMeshGroup(mesh).addElementsConditional(openingGroup) @@ -458,6 +464,52 @@ def generateBaseMesh(cls, region, options): for term in ["core", "shell", "opening"]: annotationGroups.remove(findAnnotationGroupByName(annotationGroups, term)) + # marker points + leftSuperiorPoleGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("superior pole of left kidney")) + leftInferiorPoleGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("inferior pole of left kidney")) + + rightSuperiorPoleGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("superior pole of right kidney")) + rightInferiorPoleGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("inferior pole of right kidney")) + + markerList = [] + elementsCountAround = options["Number of elements around"] + elementsCountAlong = int(options["Target element density along longest segment"] + 2) # extra sections from the cap mesh + elementsCountThroughShell = options["Number of elements through shell"] + elementsCountCoreBoxMinor = options["Number of elements across core box minor"] + elementsCountCoreBoxMajor = (elementsCountAround // 2) - elementsCountCoreBoxMinor + elementsCountTransition = options["Number of elements across core transition"] + + box_count = elementsCountCoreBoxMinor * elementsCountCoreBoxMajor + depth = elementsCountThroughShell + elementsCountTransition + offset = elementsCountCoreBoxMajor // 2 * elementsCountCoreBoxMinor + elementsCountCoreBoxMinor // 2 + 1 + cap_count = box_count * (depth + 1) + elementsCountAround * depth + tube_section_count = box_count + elementsCountAround * depth + tube_count = tube_section_count * elementsCountAlong + kidney_elements_count = cap_count * 2 + tube_count if showKidneys[0] else 0 + + if showKidneys[0]: + idx = box_count * depth + offset + markerList.append({"group": leftSuperiorPoleGroup, "elementId": idx, "xi": [0.0, 0.0, 1.0]}) + + idx = cap_count + tube_count + (box_count * depth + offset) + markerList.append({"group": leftInferiorPoleGroup, "elementId": idx, "xi": [0.0, 1.0, 1.0]}) + + if showKidneys[1]: + idx = kidney_elements_count + box_count * depth + offset + markerList.append({"group": rightSuperiorPoleGroup, "elementId": idx, "xi": [0.0, 0.0, 1.0]}) + + idx = kidney_elements_count + cap_count + tube_count + (box_count * depth + offset) + markerList.append({"group": rightInferiorPoleGroup, "elementId": idx, "xi": [0.0, 1.0, 1.0]}) + + nodeIdentifier = generateData.nextNodeIdentifier() + for marker in markerList: + annotationGroup = marker["group"] + markerNode = annotationGroup.createMarkerNode( + nodeIdentifier, element=mesh.findElementByIdentifier(marker["elementId"]), xi=marker["xi"]) + annotationGroup.setMarkerMaterialCoordinates(coordinates) + kidneyNodesetGroup.addNode(markerNode) + nodeIdentifier += 1 + return annotationGroups, None diff --git a/src/scaffoldmaker/utils/capmesh.py b/src/scaffoldmaker/utils/capmesh.py index 7f3926d4..7c19501f 100644 --- a/src/scaffoldmaker/utils/capmesh.py +++ b/src/scaffoldmaker/utils/capmesh.py @@ -1660,8 +1660,12 @@ def generateElements(self, tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups, of a tube segment. """ self._isStartCap = isStartCap - self._generateElements(annotationMeshGroups) - self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) + if isStartCap: + self._generateElements(annotationMeshGroups) + self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) + else: + self._generateExtendedTubeElements(tubeBoxNodeIds, tubeRimNodeIds, annotationMeshGroups) + self._generateElements(annotationMeshGroups) def addCapBoxElementsToMeshGroup(self, meshGroup, e1Range=None, e2Range=None, e3Range=None, mode=0): """ diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index da43e223..be8f6f3e 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -473,6 +473,12 @@ def getCoordinates(self): """ return self._coordinates + def setCoordinates(self, coordinates): + """ + + """ + self._coordinates = coordinates + def getFieldcache(self): """ :return: Zinc Fieldcache for assigning field parameters with. diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 135bc98e..24998f6a 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -1892,11 +1892,9 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): self._boxElementIds[e2] = [] self._rimElementIds[e2] = [] e2p = e2 + 1 - # create cap elements + # create cap elements at the start of the tube if self._isCap[0] and e2 == 0: capMesh.generateElements(self._boxNodeIds, self._rimNodeIds, annotationMeshGroups, isStartCap=True) - elif self._isCap[-1] and e2 == (elementsCountAlong - endSkipCount - 1): - capMesh.generateElements(self._boxNodeIds, self._rimNodeIds, annotationMeshGroups, isStartCap=False) if self._isCore: # create box elements @@ -1994,6 +1992,10 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): ringElementIds.append(elementIdentifier) self._rimElementIds[e2].append(ringElementIds) + # create cap elements at the end of the tube + if self._isCap[-1] and e2 == (elementsCountAlong - endSkipCount - 1): + capMesh.generateElements(self._boxNodeIds, self._rimNodeIds, annotationMeshGroups, isStartCap=False) + def generateJunctionRimElements(self, junction, generateData): """ Generates rim elements for junction part of the segment after segment nodes and elements have been made. diff --git a/tests/test_kidney.py b/tests/test_kidney.py index d933ceac..bc86a586 100644 --- a/tests/test_kidney.py +++ b/tests/test_kidney.py @@ -28,9 +28,9 @@ def test_kidney(self): options = scaffold.getDefaultOptions("Human 1") self.assertEqual(11, len(options)) - self.assertEqual(8, options["Elements count around"]) - self.assertEqual(1, options["Elements count through shell"]) - self.assertEqual([0], options["Annotation elements counts around"]) + self.assertEqual(8, options["Number of elements around"]) + self.assertEqual(1, options["Number of elements through shell"]) + self.assertEqual([0], options["Annotation numbers of elements around"]) self.assertEqual(2.0, options["Target element density along longest segment"]) self.assertEqual(2, options["Number of elements across core box minor"]) self.assertEqual(1, options["Number of elements across core transition"]) @@ -40,7 +40,7 @@ def test_kidney(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(48, len(annotationGroups)) + self.assertEqual(52, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) @@ -51,7 +51,7 @@ def test_kidney(self): mesh1d = fieldmodule.findMeshByDimension(1) self.assertEqual(478 * 2, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(179 * 2, nodes.getSize()) + self.assertEqual(181 * 2, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) From 0d4cb0a70b41cff3c3982f59eb6478f6dedaea35 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Thu, 30 Oct 2025 15:34:47 +1300 Subject: [PATCH 35/43] Remove redundant function in networkmesh --- src/scaffoldmaker/utils/networkmesh.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index be8f6f3e..da43e223 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -473,12 +473,6 @@ def getCoordinates(self): """ return self._coordinates - def setCoordinates(self, coordinates): - """ - - """ - self._coordinates = coordinates - def getFieldcache(self): """ :return: Zinc Fieldcache for assigning field parameters with. From 32cd4abe0760c71049fd10cf8d9adc5560869b37 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 10 Nov 2025 09:47:03 +1300 Subject: [PATCH 36/43] Fix wrong option names --- src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index f567273f..94d528fe 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -338,7 +338,7 @@ def checkOptions(cls, options): if options["Number of elements around"] < 8: options["Number of elements around"] = 8 elif options["Number of elements around"] % 4: - options["Number of elements around"] += 4 - (options["Elements count around"] % 4) + options["Number of elements around"] += 4 - (options["Number of elements around"] % 4) if options["Number of elements through shell"] < 1: options["Number of elements through shell"] = 1 @@ -346,7 +346,7 @@ def checkOptions(cls, options): if options["Number of elements across core transition"] < 1: options["Number of elements across core transition"] = 1 - minElementsCountAround = options["Elements count around"] + minElementsCountAround = options["Number of elements around"] maxElementsCountCoreBoxMinor = minElementsCountAround // 2 - 2 if options["Number of elements across core box minor"] < 2: options["Number of elements across core box minor"] = 2 @@ -356,7 +356,7 @@ def checkOptions(cls, options): elif options["Number of elements across core box minor"] % 2: options["Number of elements across core box minor"] += options["Number of elements across core box minor"] % 2 - annotationElementsCountsAround = options["Annotation elements counts around"] + annotationElementsCountsAround = options["Annotation numbers of elements around"] if len(annotationElementsCountsAround) == 0: options["Annotation numbers of elements around"] = [0] else: From ff5deae94dee94646241eb9344f45c5233887dff Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Thu, 13 Nov 2025 13:37:14 +1300 Subject: [PATCH 37/43] Add a function for bending the kidney mesh --- .../meshtypes/meshtype_3d_kidney1.py | 125 ++++++++++++++---- tests/test_kidney.py | 22 +-- 2 files changed, 111 insertions(+), 36 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index 94d528fe..e9aa07ea 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -3,7 +3,7 @@ """ import math -from cmlibs.maths.vectorops import mult, set_magnitude, cross +from cmlibs.maths.vectorops import mult, set_magnitude, cross, rotate_about_z_axis from cmlibs.utils.zinc.field import find_or_create_field_coordinates, findOrCreateFieldCoordinates from cmlibs.utils.zinc.general import ChangeManager from cmlibs.zinc.field import Field @@ -21,6 +21,8 @@ from scaffoldmaker.utils.tubenetworkmesh import KidneyTubeNetworkMeshBuilder, TubeNetworkMeshGenerateData from cmlibs.zinc.node import Node +from scaffoldmaker.utils.zinc_utils import translate_nodeset_coordinates + class MeshType_1d_kidney_network_layout1(MeshType_1d_network_layout1): """ @@ -48,8 +50,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Kidney length"] = 1.0 options["Kidney width"] = 0.5 options["Kidney thickness"] = 0.4 - options["Left-right kidney spacing"] = 1.0 - options["Kidney bend angle degrees"] = 10 + options["Left-right kidney spacing"] = 0.0 options["Inner proportion default"] = 0.6 return options @@ -63,7 +64,6 @@ def getOrderedOptionNames(cls): "Kidney width", "Kidney thickness", "Left-right kidney spacing", - "Kidney bend angle degrees", "Inner proportion default" ] @@ -84,11 +84,6 @@ def checkOptions(cls, options): if options["Left-right kidney spacing"] < 0.0: options["Left-right kidney spacing"] = 0.0 - if options["Kidney bend angle degrees"] < 0.0: - options["Kidney bend angle degrees"] = 0.0 - elif options["Kidney bend angle degrees"] > 30.0: - options["Kidney bend angle degrees"] = 30.0 - if options["Inner proportion default"] < 0.1: options["Inner proportion default"] = 0.1 elif options["Inner proportion default"] > 0.9: @@ -118,7 +113,6 @@ def generateBaseMesh(cls, region, options): halfKidneyWidth = 0.5 * options["Kidney width"] halfKidneyThickness = 0.5 * options["Kidney thickness"] spacing = 0.5 * options["Left-right kidney spacing"] - kidneyBendAngle = options["Kidney bend angle degrees"] innerProportionDefault = options["Inner proportion default"] cls.setShowKidneys(options) @@ -152,15 +146,11 @@ def generateBaseMesh(cls, region, options): # Kidney nodeIdentifier = 1 elementIdentifier = 1 - tubeRadius = cls.getTubeRadius(halfKidneyWidth, halfKidneyThickness) * ( + capRadius = cls.getCapRadius(halfKidneyWidth, halfKidneyThickness) * ( halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) extensionLength = 0.5 * (halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) - halfLayoutLength = (halfKidneyLength - tubeRadius - extensionLength) + halfLayoutLength = (halfKidneyLength - capRadius - extensionLength) kidneyScale = 2 * halfLayoutLength / kidneyElementsCount - bendAngleRadians = math.radians(kidneyBendAngle) - sinBendAngle = math.sin(bendAngleRadians) - cosBendAngle = math.cos(bendAngleRadians) - sinCurveAngle = math.sin(3 * bendAngleRadians) leftKidney, rightKidney = 0, 1 kidneys = [kidney for show, kidney in [(isLeftKidney, leftKidney), (isRightKidney, rightKidney)] if show] @@ -171,11 +161,10 @@ def generateBaseMesh(cls, region, options): d3 = [0.0, 0.0, halfKidneyThickness] id3 = mult(d3, innerProportionDefault) - tx = halfLayoutLength * -cosBendAngle - ty = halfLayoutLength * -sinBendAngle - sx = [tx, ty, 0.0] if kidney is leftKidney else [tx, -ty, 0.0] - ex = [-tx, ty, 0.0] if kidney is leftKidney else [-tx, -ty, 0.0] - sd1 = mult([1.0, sinCurveAngle, 0.0], kidneyScale) + tx = halfLayoutLength + sx = [tx, 0.0, 0.0] if kidney is leftKidney else [tx, 0.0, 0.0] + ex = [-tx, 0.0, 0.0] if kidney is leftKidney else [-tx, 0.0, 0.0] + sd1 = mult([1.0, 0.0, 0.0], kidneyScale) ed1 = [sd1[0], -sd1[1], sd1[2]] nx, nd1 = sampleCubicHermiteCurves([sx, mx, ex], [sd1, d1, ed1], kidneyElementsCount)[0:2] nd1 = smoothCubicHermiteDerivativesLine(nx, nd1) @@ -238,14 +227,19 @@ def getLayoutStructure(cls, options): return left @classmethod - def getTubeRadius(cls, majorRadius, minorRadius): + def getCapRadius(cls, majorRadius, minorRadius): """ - + Calculate the radius of the cap mesh based on the major radius and the minor radius of tube cross-section. + :param majorRadius: The radius of a tube in the major-axis. + :param minorRadius: The radius of a tube in the minor-axis. + :return: Cap radius """ if majorRadius > minorRadius: return math.pow((majorRadius / minorRadius), 1 / 3) elif majorRadius < minorRadius: return math.pow((minorRadius / majorRadius), 1 / 3) + else: + return majorRadius @classmethod def getShowKidneys(cls): @@ -286,6 +280,8 @@ def getDefaultOptions(cls, parameterSetName='Default'): options["Number of elements across core box minor"] = 2 options["Number of elements across core transition"] = 1 options["Annotation numbers of elements across core box minor"] = [0] + options["Kidney spacing"] = 1.0 + options["Kidney curvature"] = 1.0 options["Refine"] = False options["Refine number of elements"] = 4 return options @@ -301,6 +297,8 @@ def getOrderedOptionNames(cls): "Number of elements across core box minor", "Number of elements across core transition", "Annotation numbers of elements across core box minor", + "Kidney spacing", + "Kidney curvature", "Refine", "Refine number of elements" ] @@ -417,6 +415,8 @@ def generateBaseMesh(cls, region, options): layoutAnnotationGroups = networkLayout.getAnnotationGroups() networkMesh = networkLayout.getConstructionObject() showKidneys = getShowKidneysSettings() + isLeftKidney = showKidneys[0] + isRightKidney = showKidneys[1] kidneyTubeNetworkMeshBuilder = KidneyTubeNetworkMeshBuilder( networkMesh, @@ -451,6 +451,12 @@ def generateBaseMesh(cls, region, options): kidneyGroup = AnnotationGroup(region, get_kidney_term("kidney")) kidneyNodesetGroup = kidneyGroup.getNodesetGroup(nodes) + leftKidneyGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("left kidney")) + leftKidneyNodesetGroup = leftKidneyGroup.getNodesetGroup(nodes) + + rightKidneyGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("right kidney")) + rightKidneyNodesetGroup = rightKidneyGroup.getNodesetGroup(nodes) + hilumGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_kidney_term("hilum of kidney")) hilumGroup.getMeshGroup(mesh).addElementsConditional(openingGroup) @@ -487,14 +493,14 @@ def generateBaseMesh(cls, region, options): tube_count = tube_section_count * elementsCountAlong kidney_elements_count = cap_count * 2 + tube_count if showKidneys[0] else 0 - if showKidneys[0]: + if isLeftKidney: idx = box_count * depth + offset markerList.append({"group": leftSuperiorPoleGroup, "elementId": idx, "xi": [0.0, 0.0, 1.0]}) idx = cap_count + tube_count + (box_count * depth + offset) markerList.append({"group": leftInferiorPoleGroup, "elementId": idx, "xi": [0.0, 1.0, 1.0]}) - if showKidneys[1]: + if isRightKidney: idx = kidney_elements_count + box_count * depth + offset markerList.append({"group": rightSuperiorPoleGroup, "elementId": idx, "xi": [0.0, 0.0, 1.0]}) @@ -510,6 +516,25 @@ def generateBaseMesh(cls, region, options): kidneyNodesetGroup.addNode(markerNode) nodeIdentifier += 1 + # transformation + leftKidney, rightKidney = 0, 1 + kidneys = [lung for show, lung in [(isLeftKidney, leftKidney), (isRightKidney, rightKidney)] if show] + for kidney in kidneys: + isLeft = True if kidney == leftKidney else False + isRight = True if kidney == rightKidney else False + spacing = -options["Kidney spacing"] / 2 if isLeft else options["Kidney spacing"] / 2 + curvature = options["Kidney curvature"] if isLeft else -options["Kidney curvature"] + + kidneyNodeset = leftKidneyNodesetGroup if isLeft else rightKidneyNodesetGroup + + if curvature != 0.0: + if isLeft: + bendKidneyMeshAroundZAxis(curvature, fm, coordinates, kidneyNodeset, stationaryPointXY=[-0.05, 0.0]) + if isRight: + bendKidneyMeshAroundZAxis(curvature, fm, coordinates, kidneyNodeset, stationaryPointXY=[0.05, 0.0]) + + translate_nodeset_coordinates(kidneyNodeset, coordinates, [0.0, spacing, 0.0]) + return annotationGroups, None @@ -677,6 +702,56 @@ def remove_hidden_kidney_groups(show_kidneys, side_kidney_groups, annotationGrou annotationGroups.remove(group) +def bendKidneyMeshAroundZAxis(curvature, fm, coordinates, kidneyNodeset, stationaryPointXY): + """ + Transform coordinates by bending with curvature about a centre point the radius in + x direction from stationaryPointXY. + :param curvature: 1/radius. Must be non-zero. + :param fm: Field module being worked with. + :param coordinates: The coordinate field, initially circular in y-z plane. + :param kidneyNodeset: Zinc NodesetGroup containing nodes to transform. + :param stationaryPointXY: Coordinates x, y which are not displaced by bending. + """ + rotateKidneyMeshAboutZAxis(90, fm, coordinates, kidneyNodeset) + + radius = 1.0 / curvature + scale = fm.createFieldConstant([-1.0, -curvature, -1.0]) + centreOffset = [stationaryPointXY[0] - radius, stationaryPointXY[1], 0.0] + centreOfCurvature = fm.createFieldConstant(centreOffset) + polarCoordinates = (centreOfCurvature - coordinates) * scale + polarCoordinates.setCoordinateSystemType(Field.COORDINATE_SYSTEM_TYPE_CYLINDRICAL_POLAR) + rcCoordinates = fm.createFieldCoordinateTransformation(polarCoordinates) + rcCoordinates.setCoordinateSystemType(Field.COORDINATE_SYSTEM_TYPE_RECTANGULAR_CARTESIAN) + newCoordinates = rcCoordinates + centreOfCurvature + + fieldassignment = coordinates.createFieldassignment(newCoordinates) + fieldassignment.setNodeset(kidneyNodeset) + fieldassignment.assign() + + rotateKidneyMeshAboutZAxis(-90, fm, coordinates, kidneyNodeset) + + +def rotateKidneyMeshAboutZAxis(rotateAngle, fm, coordinates, kidneyNodeset): + """ + Rotates the lung mesh coordinates about a specified axis using the right-hand rule. + :param rotateAngle: Angle of rotation in degrees. + :param fm: Field module being worked with. + :param coordinates: The coordinate field, initially circular in y-z plane. + :param kidneyNodeset: Zinc NodesetGroup containing nodes to transform. + :return: None + """ + rotateAngle = -math.radians(rotateAngle) # negative value due to right handed rule + rotateMatrix = fm.createFieldConstant([math.cos(rotateAngle), math.sin(rotateAngle), 0.0, + -math.sin(rotateAngle), math.cos(rotateAngle), 0.0, + 0.0, 0.0, 1.0]) + + rotated_coordinates = fm.createFieldMatrixMultiply(3, rotateMatrix, coordinates) + + fieldassignment = coordinates.createFieldassignment(rotated_coordinates) + fieldassignment.setNodeset(kidneyNodeset) + fieldassignment.assign() + + def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None): """ Assign node field parameters x, d1, d2, d3 of field. diff --git a/tests/test_kidney.py b/tests/test_kidney.py index bc86a586..3ee933ab 100644 --- a/tests/test_kidney.py +++ b/tests/test_kidney.py @@ -27,7 +27,7 @@ def test_kidney(self): self.assertEqual(parameterSetNames, ["Default", "Human 1"]) options = scaffold.getDefaultOptions("Human 1") - self.assertEqual(11, len(options)) + self.assertEqual(13, len(options)) self.assertEqual(8, options["Number of elements around"]) self.assertEqual(1, options["Number of elements through shell"]) self.assertEqual([0], options["Annotation numbers of elements around"]) @@ -60,8 +60,8 @@ def test_kidney(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [-0.47969110977192125, -0.75, -0.2], tol) - assertAlmostEqualList(self, maximums, [0.47969110977192125, 0.75, 0.2], tol) + assertAlmostEqualList(self, minimums, [-0.5137479110210048, -0.75, -0.2], tol) + assertAlmostEqualList(self, maximums, [0.5137479110210048, 0.75, 0.2], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -79,15 +79,15 @@ def test_kidney(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.26271882980819067, delta=tol) - self.assertAlmostEqual(surfaceArea, 2.7967266004246665, delta=tol) + self.assertAlmostEqual(volume, 0.27576984106019536, delta=tol) + self.assertAlmostEqual(surfaceArea, 2.9158676929090253, delta=tol) # check some annotation groups: expectedSizes3d = { - "renal medulla": (80 * 2, 0.08077413857088181), - "cortex of kidney": (52 * 2, 0.17260778416183756), - "kidney": (136 * 2, 0.2627234820829377) + "renal medulla": (80 * 2, 0.08481313137381906), + "cortex of kidney": (52 * 2, 0.1770664248818641), + "kidney": (136 * 2, 0.2757746910245236) } for name in expectedSizes3d: term = get_kidney_term(name) @@ -103,9 +103,9 @@ def test_kidney(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "kidney capsule": (56 * 2, 2.7967266004246665), - "anterior surface of kidney": (28 * 2, 1.3983633002123297), - "posterior surface of kidney": (28 * 2, 1.3983633002123297) + "kidney capsule": (56 * 2, 2.9158676929090253), + "anterior surface of kidney": (28 * 2, 1.4579338464549303), + "posterior surface of kidney": (28 * 2, 1.4579338464540916) } for name in expectedSizes2d: term = get_kidney_term(name) From 5ee3103dfab50a13d97949e413eecd0f8e97031a Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Thu, 13 Nov 2025 13:53:27 +1300 Subject: [PATCH 38/43] Remove kidney spacing option in the 1D layout --- src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index e9aa07ea..82616e3a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -50,7 +50,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Kidney length"] = 1.0 options["Kidney width"] = 0.5 options["Kidney thickness"] = 0.4 - options["Left-right kidney spacing"] = 0.0 options["Inner proportion default"] = 0.6 return options @@ -63,7 +62,6 @@ def getOrderedOptionNames(cls): "Kidney length", "Kidney width", "Kidney thickness", - "Left-right kidney spacing", "Inner proportion default" ] @@ -81,9 +79,6 @@ def checkOptions(cls, options): if options["Elements count along"] < 2: options["Elements count along"] = 2 - if options["Left-right kidney spacing"] < 0.0: - options["Left-right kidney spacing"] = 0.0 - if options["Inner proportion default"] < 0.1: options["Inner proportion default"] = 0.1 elif options["Inner proportion default"] > 0.9: @@ -112,7 +107,6 @@ def generateBaseMesh(cls, region, options): halfKidneyLength = 0.5 * kidneyLength halfKidneyWidth = 0.5 * options["Kidney width"] halfKidneyThickness = 0.5 * options["Kidney thickness"] - spacing = 0.5 * options["Left-right kidney spacing"] innerProportionDefault = options["Inner proportion default"] cls.setShowKidneys(options) @@ -155,7 +149,6 @@ def generateBaseMesh(cls, region, options): leftKidney, rightKidney = 0, 1 kidneys = [kidney for show, kidney in [(isLeftKidney, leftKidney), (isRightKidney, rightKidney)] if show] for kidney in kidneys: - spacing = spacing if kidney is leftKidney else -spacing mx = [0.0, 0.0, 0.0] d1 = [kidneyScale, 0.0, 0.0] d3 = [0.0, 0.0, halfKidneyThickness] @@ -168,8 +161,6 @@ def generateBaseMesh(cls, region, options): ed1 = [sd1[0], -sd1[1], sd1[2]] nx, nd1 = sampleCubicHermiteCurves([sx, mx, ex], [sd1, d1, ed1], kidneyElementsCount)[0:2] nd1 = smoothCubicHermiteDerivativesLine(nx, nd1) - for c in range(kidneyElementsCount + 1): - nx[c][1] += spacing sd2_list = [] sd3_list = [] From fe7b0349daa19cc48b4f3423e29d6f7bc8ddf68b Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Thu, 13 Nov 2025 15:11:47 +1300 Subject: [PATCH 39/43] Remove unused options in 1D layout --- .../meshtypes/meshtype_3d_kidney1.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index 82616e3a..e014143a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -46,11 +46,10 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Define inner coordinates"] = True options["Left kidney"] = True options["Right kidney"] = True - options["Elements count along"] = 2 options["Kidney length"] = 1.0 options["Kidney width"] = 0.5 options["Kidney thickness"] = 0.4 - options["Inner proportion default"] = 0.6 + options["Medulla to cortex proportion"] = 0.6 return options @classmethod @@ -58,11 +57,10 @@ def getOrderedOptionNames(cls): return [ "Left kidney", "Right kidney", - "Elements count along", "Kidney length", "Kidney width", "Kidney thickness", - "Inner proportion default" + "Medulla to cortex proportion" ] @classmethod @@ -76,13 +74,10 @@ def checkOptions(cls, options): if options[key] < 0.1: options[key] = 0.1 - if options["Elements count along"] < 2: - options["Elements count along"] = 2 - - if options["Inner proportion default"] < 0.1: - options["Inner proportion default"] = 0.1 - elif options["Inner proportion default"] > 0.9: - options["Inner proportion default"] = 0.9 + if options["Medulla to cortex proportion"] < 0.1: + options["Medulla to cortex proportion"] = 0.1 + elif options["Medulla to cortex proportion"] > 0.9: + options["Medulla to cortex proportion"] = 0.9 if not options["Left kidney"] and not options["Right kidney"]: dependentChanges = True @@ -100,14 +95,13 @@ def generateBaseMesh(cls, region, options): """ # parameters structure = options["Structure"] = cls.getLayoutStructure(options) - kidneyElementsCount = options["Elements count along"] isLeftKidney = options["Left kidney"] isRightKidney = options["Right kidney"] kidneyLength = options["Kidney length"] halfKidneyLength = 0.5 * kidneyLength halfKidneyWidth = 0.5 * options["Kidney width"] halfKidneyThickness = 0.5 * options["Kidney thickness"] - innerProportionDefault = options["Inner proportion default"] + innerProportionDefault = options["Medulla to cortex proportion"] cls.setShowKidneys(options) networkMesh = NetworkMesh(structure) @@ -140,6 +134,7 @@ def generateBaseMesh(cls, region, options): # Kidney nodeIdentifier = 1 elementIdentifier = 1 + kidneyElementsCount = 2 capRadius = cls.getCapRadius(halfKidneyWidth, halfKidneyThickness) * ( halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) extensionLength = 0.5 * (halfKidneyWidth * 0.45 + halfKidneyThickness * 0.55) @@ -208,7 +203,7 @@ def getLayoutStructure(cls, options): :param options: Dict containing options. See getDefaultOptions(). :return: string version of the 1D layout structure """ - nodes_count = options["Elements count along"] + 1 + nodes_count = 3 assert nodes_count > 1 left = f"({'-'.join(map(str, range(1, nodes_count + 1)))})" From 84fc3f8ccbed031ef716ef6ea421f583e4414f77 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 14 Nov 2025 10:15:18 +1300 Subject: [PATCH 40/43] Fix kidney orientation --- .../meshtypes/meshtype_3d_kidney1.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index e014143a..81b4dd06 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -150,10 +150,10 @@ def generateBaseMesh(cls, region, options): id3 = mult(d3, innerProportionDefault) tx = halfLayoutLength - sx = [tx, 0.0, 0.0] if kidney is leftKidney else [tx, 0.0, 0.0] - ex = [-tx, 0.0, 0.0] if kidney is leftKidney else [-tx, 0.0, 0.0] - sd1 = mult([1.0, 0.0, 0.0], kidneyScale) - ed1 = [sd1[0], -sd1[1], sd1[2]] + sx = [-tx, 0.0, 0.0] if kidney is leftKidney else [-tx, 0.0, 0.0] + ex = [tx, 0.0, 0.0] if kidney is leftKidney else [tx, 0.0, 0.0] + sd1 = mult([-1.0, 0.0, 0.0], kidneyScale) + ed1 = [-sd1[0], sd1[1], sd1[2]] nx, nd1 = sampleCubicHermiteCurves([sx, mx, ex], [sd1, d1, ed1], kidneyElementsCount)[0:2] nd1 = smoothCubicHermiteDerivativesLine(nx, nd1) @@ -508,16 +508,16 @@ def generateBaseMesh(cls, region, options): for kidney in kidneys: isLeft = True if kidney == leftKidney else False isRight = True if kidney == rightKidney else False - spacing = -options["Kidney spacing"] / 2 if isLeft else options["Kidney spacing"] / 2 - curvature = options["Kidney curvature"] if isLeft else -options["Kidney curvature"] + spacing = options["Kidney spacing"] / 2 if isLeft else -options["Kidney spacing"] / 2 + curvature = -options["Kidney curvature"] if isLeft else options["Kidney curvature"] kidneyNodeset = leftKidneyNodesetGroup if isLeft else rightKidneyNodesetGroup if curvature != 0.0: if isLeft: - bendKidneyMeshAroundZAxis(curvature, fm, coordinates, kidneyNodeset, stationaryPointXY=[-0.05, 0.0]) - if isRight: bendKidneyMeshAroundZAxis(curvature, fm, coordinates, kidneyNodeset, stationaryPointXY=[0.05, 0.0]) + if isRight: + bendKidneyMeshAroundZAxis(curvature, fm, coordinates, kidneyNodeset, stationaryPointXY=[-0.05, 0.0]) translate_nodeset_coordinates(kidneyNodeset, coordinates, [0.0, spacing, 0.0]) From 55ab34a3f1f37a17eaa703f4db4fd72f43b30ae4 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 14 Nov 2025 11:27:27 +1300 Subject: [PATCH 41/43] Change number of elements through shell to cortex --- src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index 81b4dd06..428605c0 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -260,7 +260,7 @@ def getDefaultOptions(cls, parameterSetName='Default'): options["Base parameter set"] = useParameterSetName options["Kidney network layout"] = ScaffoldPackage(MeshType_1d_kidney_network_layout1) options["Number of elements around"] = 8 - options["Number of elements through shell"] = 1 + options["Number of elements through cortex"] = 1 options["Annotation numbers of elements around"] = [0] options["Target element density along longest segment"] = 2.0 options["Number of elements across core box minor"] = 2 @@ -277,7 +277,7 @@ def getOrderedOptionNames(cls): optionNames = [ "Kidney network layout", "Number of elements around", - "Number of elements through shell", + "Number of elements through cortex", "Annotation numbers of elements around", "Target element density along longest segment", "Number of elements across core box minor", @@ -324,8 +324,8 @@ def checkOptions(cls, options): elif options["Number of elements around"] % 4: options["Number of elements around"] += 4 - (options["Number of elements around"] % 4) - if options["Number of elements through shell"] < 1: - options["Number of elements through shell"] = 1 + if options["Number of elements through cortex"] < 1: + options["Number of elements through cortex"] = 1 if options["Number of elements across core transition"] < 1: options["Number of elements across core transition"] = 1 @@ -408,7 +408,7 @@ def generateBaseMesh(cls, region, options): networkMesh, targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], defaultElementsCountAround=options["Number of elements around"], - elementsCountThroughShell=options["Number of elements through shell"], + elementsCountThroughShell=options["Number of elements through cortex"], layoutAnnotationGroups=layoutAnnotationGroups, isCore=True, elementsCountTransition=options["Number of elements across core transition"], @@ -466,7 +466,7 @@ def generateBaseMesh(cls, region, options): markerList = [] elementsCountAround = options["Number of elements around"] elementsCountAlong = int(options["Target element density along longest segment"] + 2) # extra sections from the cap mesh - elementsCountThroughShell = options["Number of elements through shell"] + elementsCountThroughShell = options["Number of elements through cortex"] elementsCountCoreBoxMinor = options["Number of elements across core box minor"] elementsCountCoreBoxMajor = (elementsCountAround // 2) - elementsCountCoreBoxMinor elementsCountTransition = options["Number of elements across core transition"] From 38ec23c60bef555ca699c25795f504c6bbcf0df9 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 14 Nov 2025 14:12:58 +1300 Subject: [PATCH 42/43] Update unit test --- tests/test_kidney.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_kidney.py b/tests/test_kidney.py index 3ee933ab..35d5e067 100644 --- a/tests/test_kidney.py +++ b/tests/test_kidney.py @@ -29,7 +29,7 @@ def test_kidney(self): self.assertEqual(13, len(options)) self.assertEqual(8, options["Number of elements around"]) - self.assertEqual(1, options["Number of elements through shell"]) + self.assertEqual(1, options["Number of elements through cortex"]) self.assertEqual([0], options["Annotation numbers of elements around"]) self.assertEqual(2.0, options["Target element density along longest segment"]) self.assertEqual(2, options["Number of elements across core box minor"]) From ea241f3af4cfe17239ff8bb3bcce1417f5022f87 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 18 Nov 2025 11:42:47 +1300 Subject: [PATCH 43/43] Clean up code and method descriptions --- src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py index 428605c0..be491615 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_kidney1.py @@ -3,9 +3,8 @@ """ import math -from cmlibs.maths.vectorops import mult, set_magnitude, cross, rotate_about_z_axis +from cmlibs.maths.vectorops import mult, set_magnitude, cross from cmlibs.utils.zinc.field import find_or_create_field_coordinates, findOrCreateFieldCoordinates -from cmlibs.utils.zinc.general import ChangeManager from cmlibs.zinc.field import Field from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm, \ @@ -504,7 +503,7 @@ def generateBaseMesh(cls, region, options): # transformation leftKidney, rightKidney = 0, 1 - kidneys = [lung for show, lung in [(isLeftKidney, leftKidney), (isRightKidney, rightKidney)] if show] + kidneys = [kidney for show, kidney in [(isLeftKidney, leftKidney), (isRightKidney, rightKidney)] if show] for kidney in kidneys: isLeft = True if kidney == leftKidney else False isRight = True if kidney == rightKidney else False @@ -719,7 +718,7 @@ def bendKidneyMeshAroundZAxis(curvature, fm, coordinates, kidneyNodeset, station def rotateKidneyMeshAboutZAxis(rotateAngle, fm, coordinates, kidneyNodeset): """ - Rotates the lung mesh coordinates about a specified axis using the right-hand rule. + Rotates the mesh coordinates about a specified axis using the right-hand rule. :param rotateAngle: Angle of rotation in degrees. :param fm: Field module being worked with. :param coordinates: The coordinate field, initially circular in y-z plane. @@ -785,9 +784,7 @@ def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=N def setHilumGroupThreshold(fm, coordinates, halfLength): """ - Creates a field to identify lung base elements based on y-coordinate threshold. - Elements with y-coordinates below 45% of the rotated half-breadth are considered part of the lung base region for - annotation purposes. + Creates a field to identify hilum elements based on x-coordinate threshold. :param fm: Field module used for creating and managing fields. :param coordinates: The coordinate field. :param halfLength: Half-length of tube.