From ad4e5065eef4a9906552b6655cca0389af88342f Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 16 Dec 2024 12:23:13 +1300 Subject: [PATCH 01/18] Add renal pelvis scaffold --- src/scaffoldmaker/annotation/kidney_terms.py | 24 + src/scaffoldmaker/annotation/ureter_terms.py | 19 + .../meshtypes/meshtype_3d_renal_pelvis1.py | 683 ++++++++++++++++++ src/scaffoldmaker/scaffolds.py | 6 +- src/scaffoldmaker/utils/tracksurface.py | 1 + src/scaffoldmaker/utils/tubenetworkmesh.py | 40 + tests/test_renalpelvis.py | 131 ++++ 7 files changed, 903 insertions(+), 1 deletion(-) create mode 100644 src/scaffoldmaker/annotation/kidney_terms.py create mode 100644 src/scaffoldmaker/annotation/ureter_terms.py create mode 100644 src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py create mode 100644 tests/test_renalpelvis.py diff --git a/src/scaffoldmaker/annotation/kidney_terms.py b/src/scaffoldmaker/annotation/kidney_terms.py new file mode 100644 index 00000000..a0b65cde --- /dev/null +++ b/src/scaffoldmaker/annotation/kidney_terms.py @@ -0,0 +1,24 @@ +""" +Common resource for kidney annotation terms. +""" + +# convention: preferred name, preferred id, followed by any other ids and alternative names +kidney_terms = [ + ("core", ""), + ("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/annotation/ureter_terms.py b/src/scaffoldmaker/annotation/ureter_terms.py new file mode 100644 index 00000000..5c052657 --- /dev/null +++ b/src/scaffoldmaker/annotation/ureter_terms.py @@ -0,0 +1,19 @@ +""" +Common resource for ureter annotation terms. +""" + +# convention: preferred name, preferred id, followed by any other ids and alternative names +ureter_terms = [ + ("ureter", "UBERON:0000056", "ILX:0728080") + ] + +def get_ureter_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 ureter_terms: + if name in term: + return (term[0], term[1]) + raise NameError("Body annotation term '" + name + "' not found.") \ No newline at end of file diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py new file mode 100644 index 00000000..a379d8d5 --- /dev/null +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -0,0 +1,683 @@ +""" +Generates a 3D renal pelvis using tube network mesh. +""" +import math + +from cmlibs.maths.vectorops import mult, cross, add, sub, set_magnitude +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.annotation.ureter_terms import get_ureter_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 sampleCubicHermiteCurves, smoothCubicHermiteDerivativesLine +from scaffoldmaker.utils.networkmesh import NetworkMesh +from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshGenerateData, RenalPelvisTubeNetworkMeshBuilder +from cmlibs.zinc.node import Node + + +class MeshType_1d_renal_pelvis_network_layout1(MeshType_1d_network_layout1): + """ + Defines renal pelvis network layout. + """ + + + @classmethod + def getName(cls): + return "1D Renal Pelvis Network Layout 1" + + @classmethod + def getParameterSetNames(cls): + return ["Default"] + + @classmethod + def getDefaultOptions(cls, parameterSetName="Default"): + options = {} + options["Base parameter set"] = "Human 1" if (parameterSetName == "Default") else parameterSetName + options["Structure"] = ( + "1-2, 2-3.1,3.2-4,3.3-5,3.4-6," + "4.2-7, 4.3-8, 6.2-9, 6.3-10," + "7.2-11, 7.3-12, 8.2-13, 8.3-14, 5.2-15, 5.3-16, 9.2-17, 9.3-18, 10.2-19, 10.3-20," + "11-21-22, 12-23-24, 13-25-26, 14-27-28, 15-29-30, 16-31-32, 17-33-34, 18-35-36, 19-37-38, 20-39-40") + options["Define inner coordinates"] = True + options["Ureter length"] = 3.0 + options["Ureter radius"] = 0.1 + options["Ureter bend angle degrees"] = 45 + options["Major calyx length"] = 0.6 + options["Major calyx radius"] = 0.1 + options["Major calyx angle degrees"] = 170 + options["Middle major calyx length"] = 0.4 + options["Major to bottom/top minor calyx length"] = 0.3 + options["Major to lower/upper minor calyx length"] = 0.3 + options["Bottom/top minor calyx length"] = 0.2 + options["Lower/upper minor calyx length"] = 0.2 + options["Minor calyx radius"] = 0.1 + options["Bottom/top minor calyx bifurcation angle degrees"] = 90 + options["Lower/upper minor calyx bifurcation angle degrees"] = 90 + options["Lower/upper minor calyx bend angle degrees"] = 10 + options["Renal pyramid length"] = 0.5 + options["Renal pyramid width"] = 0.5 + options["Inner proportion default"] = 0.8 + options["Inner proportion ureter"] = 0.7 + return options + + @classmethod + def getOrderedOptionNames(cls): + return [ + "Ureter length", + "Ureter radius", + "Ureter bend angle degrees", + "Major calyx length", + "Major calyx radius", + "Major calyx angle degrees", + "Middle major calyx length", + "Major to bottom/top minor calyx length", + "Major to lower/upper minor calyx length", + "Bottom/top minor calyx length", + "Lower/upper minor calyx length", + "Minor calyx radius", + "Bottom/top minor calyx bifurcation angle degrees", + "Lower/upper minor calyx bifurcation angle degrees", + "Lower/upper minor calyx bend angle degrees", + "Renal pyramid length", + "Renal pyramid width", + "Inner proportion default", + "Inner proportion ureter" + ] + + @classmethod + def checkOptions(cls, options): + dependentChanges = False + for key in [ + "Ureter length", + "Ureter radius", + "Ureter bend angle degrees", + "Major calyx length", + "Major calyx radius", + "Major calyx angle degrees", + "Middle major calyx length", + "Major to bottom/top minor calyx length", + "Major to lower/upper minor calyx length", + "Bottom/top minor calyx length", + "Lower/upper minor calyx length", + "Minor calyx radius", + "Bottom/top minor calyx bifurcation angle degrees", + "Lower/upper minor calyx bifurcation angle degrees", + "Lower/upper minor calyx bend angle degrees", + "Renal pyramid length", + "Renal pyramid width", + "Inner proportion default", + "Inner proportion ureter" + ]: + if options[key] < 0.1: + options[key] = 0.1 # check again + for key in [ + "Inner proportion default", + "Inner proportion ureter" + ]: + if options[key] < 0.1: + options[key] = 0.1 + elif options[key] > 0.9: + options[key] = 0.9 + for key, angleRange in { + "Ureter bend angle degrees": (0.0, 45.0), + "Major calyx angle degrees": (130.0, 170.0), + "Bottom/top minor calyx bifurcation angle degrees": (60.0, 120.0), + "Lower/upper minor calyx bifurcation angle degrees": (60.0, 120.0), + "Lower/upper minor calyx bend angle degrees": (0.0, 10.0) + }.items(): + if options[key] < angleRange[0]: + options[key] = angleRange[0] + elif options[key] > angleRange[1]: + options[key] = angleRange[1] + + 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"] + ureterLength = options["Ureter length"] + ureterRadius = options["Ureter radius"] + ureterBendAngle = options["Ureter bend angle degrees"] + majorCalyxLength = options["Major calyx length"] + majorCalyxRadius = options["Major calyx radius"] + majorCalyxAngle = options["Major calyx angle degrees"] + majorToBottomMinorCalyxLength = options["Major to bottom/top minor calyx length"] + majorToLowerMinorCalyxLength = options["Major to lower/upper minor calyx length"] + middleMajorLength = options["Middle major calyx length"] + bottomMinorCalyxLength = options["Bottom/top minor calyx length"] + minorCalyxRadius = options["Minor calyx radius"] + lowerMinorCalyxLength = options["Lower/upper minor calyx length"] + bottomMinorCalyxAngle = options["Bottom/top minor calyx bifurcation angle degrees"] + lowerMinorCalyxAngle = options["Lower/upper minor calyx bifurcation angle degrees"] + minorCalyxBendAngle = options["Lower/upper minor calyx bend angle degrees"] + pyramidLength = options["Renal pyramid length"] + pyramidWidth = options["Renal pyramid width"] + innerProportionDefault = options["Inner proportion default"] + innerProportionUreter = options["Inner proportion ureter"] + + networkMesh = NetworkMesh(structure) + networkMesh.create1DLayoutMesh(region) + + fieldmodule = region.getFieldmodule() + mesh = fieldmodule.findMeshByDimension(1) + + # set up element annotations + renalPelvisGroup = AnnotationGroup(region, get_kidney_term("renal pelvis")) + ureterGroup = AnnotationGroup(region, get_ureter_term("ureter")) + majorCalyxGroup = AnnotationGroup(region, get_kidney_term("major calyx")) + minorCalyxGroup = AnnotationGroup(region, get_kidney_term("minor calyx")) + renalPyramidGroup = AnnotationGroup(region, get_kidney_term("renal pyramid")) + annotationGroups = [renalPelvisGroup, ureterGroup, majorCalyxGroup, minorCalyxGroup, renalPyramidGroup] + + renalPelvisMeshGroup = renalPelvisGroup.getMeshGroup(mesh) + elementIdentifier = 1 + ureterElementsCount = 2 + meshGroups = [renalPelvisMeshGroup, ureterGroup.getMeshGroup(mesh)] + for e in range(ureterElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + + majorCalyxElementsCount = 3 + meshGroups = [renalPelvisMeshGroup, majorCalyxGroup.getMeshGroup(mesh)] + bottomMajor, middleMajor, topMajor = 0, 1, 2 + for e in range(majorCalyxElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + if e == middleMajor: + middleMajorCalyxIdentifier = elementIdentifier + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + + meshGroups = [renalPelvisMeshGroup, minorCalyxGroup.getMeshGroup(mesh)] + bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 + for calyx in (bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor): + cElementIdentifier = middleMajorCalyxIdentifier if calyx == middleMajor else elementIdentifier + element = mesh.findElementByIdentifier(cElementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier = elementIdentifier if calyx == middleMajor else elementIdentifier + 1 + + renalPyramidMeshGroup = renalPyramidGroup.getMeshGroup(mesh) + minorCalyxElementsCount = 1 + renalPyramidElementsCount = 2 + anterior, posterior = 0, 1 + bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 + for calyx in (bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor): + for side in (anterior, posterior): + for count, groupCount in [(minorCalyxElementsCount, renalPyramidElementsCount)]: + for e in range(count): + element = mesh.findElementByIdentifier(elementIdentifier) + renalPyramidMeshGroup.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) + + # ureter + nodeIdentifier = 1 + ureterScale = ureterLength / ureterElementsCount + ureterBendAngleRadians = math.radians(ureterBendAngle) + sinUreterAngle = math.sin(ureterBendAngleRadians) + cosUreterAngle = math.cos(ureterBendAngleRadians) + endX = [ureterLength, 0.0, 0.0] + tx = endX[0] - ureterLength * cosUreterAngle + ty = endX[1] - ureterLength * sinUreterAngle + startX = [tx, ty, 0.0] + d1 = [ureterScale, 0.0, 0.0] + d3 = [0.0, 0.0, ureterRadius] + id3 = mult(d3, innerProportionUreter) + td1 = [0.0, ureterScale, 0.0] + sx, sd1 = sampleCubicHermiteCurves([startX, endX], [td1, d1], ureterElementsCount, arcLengthDerivatives=True)[0:2] + sd1 = smoothCubicHermiteDerivativesLine(sx, sd1, fixEndDirection=True) + for e in range(ureterElementsCount + 1): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + sd2 = set_magnitude(cross(d3, sd1[e]), ureterRadius) + sid2 = mult(sd2, innerProportionUreter) + for field, derivatives in ((coordinates, (sd1[e], sd2, d3)), (innerCoordinates, (sd1[e], sid2, id3))): + setNodeFieldParameters(field, fieldcache, sx[e], *derivatives) + nodeIdentifier += 1 + majorCalyxJunctionNodeIdentifier = nodeIdentifier - 1 + majorCalyxStartX = endX + + # major calyx + majorCalyxAngleRadians = math.radians(majorCalyxAngle / 2) + sx = majorCalyxStartX + bottomMajor, middleMajor, topMajor = 0, 1, 2 + majorCalyxNodeIdentifiers, majorCalyxXList = [], [] + for side in (bottomMajor, middleMajor, topMajor): + majorCalyxNodeIdentifiers.append(nodeIdentifier) + calyxLength = middleMajorLength if side == middleMajor else majorCalyxLength + majorCalyxAngle = 0 if side == middleMajor else majorCalyxAngleRadians + cosMajorCalyxAngle = math.cos(majorCalyxAngle) + sinMajorCalyxAngle = math.sin(-majorCalyxAngle if side == bottomMajor else majorCalyxAngle) + majorCalyxEndX = sx[0] + calyxLength * cosMajorCalyxAngle + majorCalyxEndY = sx[1] + calyxLength * sinMajorCalyxAngle + x = [majorCalyxEndX, majorCalyxEndY, 0.0] + majorCalyxXList.append(x) + sd1 = sub(x, sx) + sd2_list, sd3_list, sNodeIdentifiers = [], [], [] + for i in range(2): + isJunctionNode = i == 0 + nodeId = majorCalyxJunctionNodeIdentifier if isJunctionNode else nodeIdentifier + sNodeIdentifiers.append(nodeId) + node = nodes.findNodeByIdentifier(nodeId) + fieldcache.setNode(node) + version = {middleMajor: 3, bottomMajor: 2, topMajor: 4}[side] + sd3 = [0.0, 0.0, majorCalyxRadius] + sid3 = mult(sd3, innerProportionDefault) + sd2 = set_magnitude(cross(sd3, sd1), majorCalyxRadius) + sid2 = mult(sd2, innerProportionDefault) + sd2_list.append(sd2) + sd3_list.append(sd3) + if not isJunctionNode: + for field, derivatives in ( + (coordinates, [x, sd1, sd2, sd3]), + (innerCoordinates, [x, sd1, sid2, sid3]) + ): + for valueLabel, value in zip( + (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D_DS3), + derivatives + ): + field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) + nodeIdentifier += 1 + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + + # top and bottom major calyx to minor calyx + lowerMinor, bottomMinor = 1, 0 + topMinor, upperMinor = 0, 1 + minorCalyxNodeIdentifiers, minorCalyxXList = [], [] + for calyx in [bottomMajor, topMajor]: + cNodeIdentifier = majorCalyxNodeIdentifiers[calyx] + sides = [bottomMinor, lowerMinor] if calyx == bottomMajor else [topMinor, upperMinor] + signValue = -1.0 if calyx == bottomMajor else 1.0 + sx = majorCalyxXList[calyx] + for side in sides: + calyxLength = majorToBottomMinorCalyxLength if side in (bottomMinor, topMinor) else majorToLowerMinorCalyxLength + x = [sx[0], sx[1] + calyxLength * signValue, sx[2]] if side in (bottomMinor, topMinor) else \ + [sx[0] + calyxLength, sx[1], sx[2]] + if side in (lowerMinor, upperMinor): + theta = math.radians(-minorCalyxBendAngle) if calyx == bottomMajor else math.radians(minorCalyxBendAngle) + rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) + ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) + x = [rx, ry, 0.0] + sd1 = sub(x, sx) + minorCalyxNodeIdentifiers.append(nodeIdentifier) + minorCalyxXList.append(x) + minorCalyx_sd2_list, minorCalyx_sd3_list, sNodeIdentifiers = [], [], [] + for i in range(2): + isJunctionNode = i == 0 + nodeId = cNodeIdentifier if isJunctionNode else nodeIdentifier + sNodeIdentifiers.append(nodeId) + node = nodes.findNodeByIdentifier(nodeId) + fieldcache.setNode(node) + + version = 2 if side == 0 else 3 + sd3 = [0.0, 0.0, minorCalyxRadius] + sd2 = [minorCalyxRadius, 0.0, 0.0] if (calyx == bottomMajor and version == 2) \ + else set_magnitude(cross(sd3, sd1), minorCalyxRadius) + sid2 = mult(sd2, innerProportionDefault) + sid3 = mult(sd3, innerProportionDefault) + minorCalyx_sd2_list.append(sd2) + minorCalyx_sd3_list.append(sd3) + if not isJunctionNode: + for field, derivatives in ( + (coordinates, [x, sd1, sd2, sd3]), + (innerCoordinates, [x, sd1, sid2, sid3]) + ): + for valueLabel, value in zip( + (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D_DS3), + derivatives + ): + field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) + nodeIdentifier += 1 + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + + # minor calyx to renal pyramid connection + anterior, posterior = 0, 1 + bottomMinor, lowerMinor, topMinor, upperMinor, middleMajor = 0, 1, 2, 3, 4 + renalPyramidStartX, renalPyramidNodeIdentifiers = [], [] + connection_sd2_list, connection_sd3_list = [], [] + for calyx in (bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor): + cNodeIdentifier = majorCalyxNodeIdentifiers[1] if calyx == middleMajor else minorCalyxNodeIdentifiers[calyx] + + sx = majorCalyxXList[1] if calyx == middleMajor else minorCalyxXList[calyx] + minorCalyxLength = bottomMinorCalyxLength if calyx in (bottomMinor, topMinor) else lowerMinorCalyxLength + minorCalyxAngle = bottomMinorCalyxAngle if calyx in (bottomMinor, topMinor) else lowerMinorCalyxAngle + minorCalyxHalfAngle = 0.5 * minorCalyxAngle + minorCalyxHalfAngleRadians = math.radians(minorCalyxHalfAngle) + sinMinorCalyxAngle = math.sin(minorCalyxHalfAngleRadians) + cosMinorCalyxAngle = math.cos(minorCalyxHalfAngleRadians) + + renalPyramidNodeIdentifiers.append([]) + renalPyramidStartX.append([]) + connection_sd2_list.append([]) + connection_sd3_list.append([]) + for side in [anterior, posterior]: + if calyx in [bottomMinor, topMinor]: + nx = sx[0] + minorCalyxLength * (-sinMinorCalyxAngle if side == anterior else sinMinorCalyxAngle) + ny = sx[1] + minorCalyxLength * (-cosMinorCalyxAngle if calyx == bottomMinor else cosMinorCalyxAngle) + x = [nx, ny, 0.0] + elif calyx in [lowerMinor, upperMinor]: + signValue = -1 if calyx == lowerMinor else 1 + nx = sx[0] + minorCalyxLength * cosMinorCalyxAngle + ny = sx[1] + minorCalyxLength * math.sin(math.radians(minorCalyxBendAngle)) * signValue + nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) + x = [nx, ny, nz] + else: + nx = sx[0] + minorCalyxLength * cosMinorCalyxAngle + nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) + x = [nx, sx[1], nz] + renalPyramidNodeIdentifiers[-1].append(nodeIdentifier) + renalPyramidStartX[-1].append(x) + sd1 = sub(x, sx) + if calyx in [bottomMinor, topMinor]: + sd3 = [0.0, 0.0, minorCalyxRadius] + sd2 = set_magnitude(cross(sd3, sd1), minorCalyxRadius) + elif calyx in [lowerMinor, upperMinor]: + sd2 = set_magnitude(cross([0.0, 0.0, 1.0], sd1), minorCalyxRadius) + sd3 = set_magnitude(cross(sd1, sd2), minorCalyxRadius) + else: + sd2 = [0.0, minorCalyxRadius, 0.0] + sd3 = set_magnitude(cross(sd1, sd2), minorCalyxRadius) + connection_sd2_list[-1].append(sd2) + connection_sd3_list[-1].append(sd3) + + sid2 = mult(sd2, innerProportionDefault) + sid3 = mult(sd3, innerProportionDefault) + sNodeIdentifiers = [] + version = 2 if side == anterior else 3 + for i in range(2): + isJunctionNode = i == 0 + nodeId = cNodeIdentifier if isJunctionNode else nodeIdentifier + sNodeIdentifiers.append(nodeId) + node = nodes.findNodeByIdentifier(nodeId) + fieldcache.setNode(node) + if not isJunctionNode: + for field, derivatives in ( + (coordinates, [x, sd1, sd2, sd3]), + (innerCoordinates, [x, sd1, sid2, sid3]) + ): + for valueLabel, value in zip( + (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D_DS3), + derivatives + ): + field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) + nodeIdentifier += 1 + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + + # minor calyx to renal pyramids + pyramidElementsCount = 2 + pyramidHalfWidth = 0.5 * pyramidWidth + bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 + for calyx in [bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor]: + minorCalyxAngle = bottomMinorCalyxAngle if calyx in (bottomMinor, topMinor) else lowerMinorCalyxAngle + minorCalyxHalfAngle = 0.5 * minorCalyxAngle + minorCalyxHalfAngleRadians = math.radians(minorCalyxHalfAngle) + sinMinorCalyxAngle = math.sin(minorCalyxHalfAngleRadians) + cosMinorCalyxAngle = math.cos(minorCalyxHalfAngleRadians) + for side in [anterior, posterior]: + cNodeIdentifier = renalPyramidNodeIdentifiers[calyx][side] + sx = renalPyramidStartX[calyx][side] + if calyx in [bottomMinor, topMinor]: + nx = -sinMinorCalyxAngle if side == anterior else sinMinorCalyxAngle + ny = -cosMinorCalyxAngle if calyx == bottomMinor else cosMinorCalyxAngle + pyramidDirn = [nx, ny, 0.0] + elif calyx in [lowerMinor, upperMinor]: + tx = [1.0, 0.0, 0.0] + theta = math.radians(-minorCalyxBendAngle) if calyx == lowerMinor else math.radians(minorCalyxBendAngle) + rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) + ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) + pyramidDirn = [rx, ry, 0.0] + else: + pyramidDirn = [1.0, 0.0, 0.0] + xList = [] + for e in range(pyramidElementsCount): + pyramidLengthScale = 0.7 * pyramidLength if e == 0 else pyramidLength + x = add(sx, mult(pyramidDirn, (pyramidLengthScale))) + xList.append(x) + pyramid_sd2_list = [] + pyramid_sd3_list = [] + sNodeIdentifiers = [] + for e in range(pyramidElementsCount): + sNodeIdentifiers.append(nodeIdentifier) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + + sd1 = sub(xList[1], xList[0]) + pyramidWidthScale = pyramidHalfWidth if e == 0 else 0.9 * pyramidHalfWidth + pyramidThickness = 1.1 * minorCalyxRadius if e == 0 else minorCalyxRadius + if calyx in [bottomMinor, topMinor]: + sd3 = [0.0, 0.0, pyramidThickness] + sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) + elif calyx in [lowerMinor, upperMinor]: + sd3 = [0.0, 0.0, pyramidThickness] + sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) + else: + sd2 = [0.0, pyramidWidthScale, 0.0] + sd3 = set_magnitude(cross(sd1, sd2), pyramidThickness) + pyramid_sd2_list.append(sd2) + pyramid_sd3_list.append(sd3) + sid2 = mult(sd2, innerProportionDefault) + sid3 = mult(sd3, innerProportionDefault) + for field, derivatives in ( + (coordinates, [xList[e], sd1, sd2, sd3]), + (innerCoordinates, [xList[e], sd1, sid2, sid3]) + ): + for valueLabel, value in zip( + (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D_DS3), + derivatives + ): + field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) + nodeIdentifier += 1 + pyramid_sd2_list.append(connection_sd2_list[calyx][side]) + pyramid_sd3_list.append(connection_sd3_list[calyx][side]) + for e in range(pyramidElementsCount): + node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) + fieldcache.setNode(node) + sd12 = sub(pyramid_sd2_list[e + 1], pyramid_sd2_list[e]) + sd13 = sub(pyramid_sd3_list[e + 1], pyramid_sd3_list[e]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sd13) + sid12 = mult(sd12, innerProportionDefault) + sid13 = mult(sd13, 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 + + @classmethod + def getInteractiveFunctions(cls): + """ + Edit base class list to include only valid functions. + """ + interactiveFunctions = super(MeshType_1d_renal_pelvis_network_layout1, cls).getInteractiveFunctions() + for interactiveFunction in interactiveFunctions: + if interactiveFunction[0] == "Edit structure...": + interactiveFunctions.remove(interactiveFunction) + break + return interactiveFunctions + + +class MeshType_3d_renal_pelvis1(Scaffold_base): + """ + Generates a 3-D renal pelvis. + """ + + @classmethod + def getName(cls): + return "3D Renal Pelvis 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["Network layout"] = ScaffoldPackage(MeshType_1d_renal_pelvis_network_layout1) + 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["Use linear through shell"] = False + options["Use outer trim surfaces"] = True + options["Show trim surfaces"] = False + return options + + @classmethod + def getOrderedOptionNames(cls): + return [ + "Network layout", + "Elements count around", + "Elements count through shell", + "Annotation elements counts around", + "Target element density along longest segment", + "Use linear through shell", + "Use outer trim surfaces", + "Show trim surfaces" + ] + + @classmethod + def getOptionValidScaffoldTypes(cls, optionName): + if optionName == "Network layout": + return [MeshType_1d_renal_pelvis_network_layout1] + return [] + + @classmethod + def getOptionScaffoldTypeParameterSetNames(cls, optionName, scaffoldType): + assert scaffoldType in cls.getOptionValidScaffoldTypes(optionName), \ + cls.__name__ + ".getOptionScaffoldTypeParameterSetNames. " + \ + "Invalid option \"" + optionName + "\" scaffold type " + scaffoldType.getName() + return scaffoldType.getParameterSetNames() # use the defaults from the network layout scaffold + + @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 == "Network layout": + if not parameterSetName: + parameterSetName = "Default" + return ScaffoldPackage(MeshType_1d_renal_pelvis_network_layout1, defaultParameterSetName=parameterSetName) + assert False, cls.__name__ + ".getOptionScaffoldPackage: Option " + optionName + " is not a scaffold" + + @classmethod + def checkOptions(cls, options): + dependentChanges = False + if (options["Network layout"].getScaffoldType() not in + cls.getOptionValidScaffoldTypes("Network layout")): + options["Body network layout"] = ScaffoldPackage(MeshType_1d_renal_pelvis_network_layout1) + + 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 + """ + layoutRegion = region.createRegion() + networkLayout = options["Network layout"] + networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters + layoutAnnotationGroups = networkLayout.getAnnotationGroups() + networkMesh = networkLayout.getConstructionObject() + + tubeNetworkMeshBuilder = RenalPelvisTubeNetworkMeshBuilder( + networkMesh, + targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], + defaultElementsCountAround=options["Elements count around"], + elementsCountThroughShell=options["Elements count through shell"], + layoutAnnotationGroups=layoutAnnotationGroups, + annotationElementsCountsAround=options["Annotation elements counts around"]) + + tubeNetworkMeshBuilder.build() + generateData = TubeNetworkMeshGenerateData( + region, 3, + isLinearThroughShell=False, + isShowTrimSurfaces=options["Show trim surfaces"]) + 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..7ac6e0a2 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_pelvis1 import MeshType_3d_renal_pelvis1, \ + MeshType_1d_renal_pelvis_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 +104,7 @@ def __init__(self): MeshType_3d_musclefusiform1, MeshType_3d_ostium1, MeshType_3d_ostium2, + MeshType_3d_renal_pelvis1, MeshType_3d_smallintestine1, MeshType_3d_solidcylinder1, MeshType_3d_solidsphere1, @@ -120,7 +123,8 @@ def __init__(self): MeshType_3d_wholebody2 ] self._allPrivateScaffoldTypes = [ - MeshType_1d_human_body_network_layout1 + MeshType_1d_human_body_network_layout1, + MeshType_1d_renal_pelvis_network_layout1 ] def findScaffoldTypeByName(self, name): diff --git a/src/scaffoldmaker/utils/tracksurface.py b/src/scaffoldmaker/utils/tracksurface.py index 09bcc78f..17029b0b 100644 --- a/src/scaffoldmaker/utils/tracksurface.py +++ b/src/scaffoldmaker/utils/tracksurface.py @@ -1163,6 +1163,7 @@ def findNearestPositionOnCurve(self, cx, cd1, loop=False, startCurveLocation=Non else: # add out-of-plane slope component if it < 10: + mag_ri = MAX_SLOPE_FACTOR if mag_ri == 0.0 else mag_ri slope_factor = mag_r * mag_r / (mag_ri * mag_ri) else: slope_factor = 1.0 + r_dot_n / mag_r # wrong, but more reliable diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index f41e6770..cbe5d3fd 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -3152,6 +3152,18 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg self._layoutInnerCoordinates = None self._useOuterTrimSurfaces = useOuterTrimSurfaces if self._layoutInnerCoordinates else False + def checkSegmentCore(self, networkSegment): + """ + Checks whether a segment should have a core locally, depending on its annotation. Currently, the annotation + requires to include "core" in its name to indicate the segment has a core. + If a segment requires a core locally, it overrides the global value of self._isCore variable. + """ + for layoutAnnotationGroup in self._layoutAnnotationGroups: + if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationGroup.getMeshGroup(self._layoutMesh)): + if "core" in layoutAnnotationGroup.getTerm(): + self._isCore = True + return self._isCore + def createSegment(self, networkSegment): pathParametersList = [get_nodeset_path_ordered_field_parameters( self._layoutNodes, self._layoutCoordinates, pathValueLabels, @@ -3173,6 +3185,8 @@ def createSegment(self, networkSegment): elementsCountAround = self._annotationElementsCountsAround[i] break i += 1 + + self.checkSegmentCore(networkSegment) if self._isCore: annotationElementsCountAcrossMinor = [] i = 0 @@ -3253,6 +3267,32 @@ def generateMesh(self, generateData): segment.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) +class RenalPelvisTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): + """ + Specialization of TubeNetworkMeshBuilder adding annotations for the renal pelvis. + Requires network layout to follow these conventions: + - +y-axis is top, and -y-axis is bottom. + - +d3 direction is anterior, -d3 is posterior. + - naming of major calyxes: top, middle, bottom + - naming of minor calyxes: top, upper, middle, lower, bottom + """ + + def checkSegmentCore(self, networkSegment): + super(RenalPelvisTubeNetworkMeshBuilder, self).checkSegmentCore(networkSegment) + """ + Checks whether a segment should have a core locally, depending on its annotation. + For the renal pelvis scaffold, the annotation term "renal pyramid" indicates the segment has a core. + """ + for layoutAnnotationGroup in self._layoutAnnotationGroups: + if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationGroup.getMeshGroup(self._layoutMesh)): + if "renal pyramid" in layoutAnnotationGroup.getTerm(): + self._isCore = True + return self._isCore + + def generateMesh(self, generateData): + super(RenalPelvisTubeNetworkMeshBuilder, self).generateMesh(generateData) + + class TubeEllipseGenerator: """ Generates tube ellipse curves with even-sized elements with specified radius, phase angle, diff --git a/tests/test_renalpelvis.py b/tests/test_renalpelvis.py new file mode 100644 index 00000000..df7e3b3d --- /dev/null +++ b/tests/test_renalpelvis.py @@ -0,0 +1,131 @@ +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.element import Element +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.annotation.ureter_terms import get_ureter_term +from scaffoldmaker.meshtypes.meshtype_3d_renal_pelvis1 import MeshType_3d_renal_pelvis1 + + +from testutils import assertAlmostEqualList + + +class RenalPelviscaffoldTestCase(unittest.TestCase): + + def test_renalpelvis(self): + """ + Test creation of renal pelvis scaffold. + """ + scaffold = MeshType_3d_renal_pelvis1 + parameterSetNames = scaffold.getParameterSetNames() + self.assertEqual(parameterSetNames, ["Default", "Human 1"]) + options = scaffold.getDefaultOptions("Human 1") + + self.assertEqual(9, 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(4.0, options["Target element density along longest segment"]) + self.assertEqual(False, options["Use linear through shell"]) + self.assertEqual(True, options["Use outer trim surfaces"]) + self.assertEqual(False, options["Show trim surfaces"]) + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + annotationGroups = scaffold.generateMesh(region, options)[0] + self.assertEqual(7, len(annotationGroups)) + + fieldmodule = region.getFieldmodule() + self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual(656, mesh3d.getSize()) + mesh2d = fieldmodule.findMeshByDimension(2) + self.assertEqual(2476, mesh2d.getSize()) + mesh1d = fieldmodule.findMeshByDimension(1) + self.assertEqual(2991, mesh1d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(1172, 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, [0.7789485582121838, -2.128648920925165, -0.25340433034411736], tol) + assertAlmostEqualList(self, maximums, [4.04142135623731, 1.5517905914526038, 0.2534043303441174], 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, 0.4091337158319937, delta=tol) + self.assertAlmostEqual(surfaceArea, 13.670578210256929, delta=tol) + + # check some annotation groups: + + expectedSizes3d = { + "core": (320, 0.2200233669825981), + "major calyx": (48, 0.015284024590047216), + "minor calyx": (80, 0.014873238147161206), + "renal pelvis": (176, 0.07598967760825394), + "renal pyramid": (80, 0.017177432136007482), + "shell": (336, 0.18913226215956122), + "ureter": (64, 0.049524462117630445) + } + for name in expectedSizes3d: + term = get_ureter_term(name) if name == "ureter" else 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 = { + "core": (1302, 12.615540404295311), + "major calyx": (208, 1.837248623082175), + "minor calyx": (358, 1.8841596431915135), + "renal pelvis": (734, 7.5061456904809285), + "renal pyramid": (382, 2.1844517878410996), + "shell": (1454, 18.19860793812061), + "ureter": (264, 4.292647846731792) + } + for name in expectedSizes2d: + term = get_ureter_term(name) if name == "ureter" else 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 cc32301e11e41c45230886d206a672b309d9f7fa Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Thu, 19 Dec 2024 11:30:12 +1300 Subject: [PATCH 02/18] Fix bending of ureter --- src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index a379d8d5..0e0263b1 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -3,7 +3,7 @@ """ import math -from cmlibs.maths.vectorops import mult, cross, add, sub, set_magnitude +from cmlibs.maths.vectorops import mult, cross, add, sub, set_magnitude, rotate_about_z_axis from cmlibs.utils.zinc.field import find_or_create_field_coordinates from cmlibs.zinc.field import Field @@ -244,7 +244,8 @@ def generateBaseMesh(cls, region, options): d1 = [ureterScale, 0.0, 0.0] d3 = [0.0, 0.0, ureterRadius] id3 = mult(d3, innerProportionUreter) - td1 = [0.0, ureterScale, 0.0] + # td1 = [0.0, ureterScale, 0.0] + td1 = rotate_about_z_axis(d1, 2 * ureterBendAngleRadians) sx, sd1 = sampleCubicHermiteCurves([startX, endX], [td1, d1], ureterElementsCount, arcLengthDerivatives=True)[0:2] sd1 = smoothCubicHermiteDerivativesLine(sx, sd1, fixEndDirection=True) for e in range(ureterElementsCount + 1): From 8ac331c1768cc6b11b8102fc88b3f1f65de3c562 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 22 Jul 2025 11:03:10 +1200 Subject: [PATCH 03/18] Add methods for user to change the structure of the renal pelvis layout --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 733 ++++++++++++------ 1 file changed, 477 insertions(+), 256 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index 0e0263b1..03e7ab07 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -3,7 +3,8 @@ """ import math -from cmlibs.maths.vectorops import mult, cross, add, sub, set_magnitude, rotate_about_z_axis +from cmlibs.maths.vectorops import mult, cross, add, sub, set_magnitude, rotate_about_z_axis, \ + rotate_vector_around_vector, normalize from cmlibs.utils.zinc.field import find_or_create_field_coordinates from cmlibs.zinc.field import Field @@ -31,42 +32,71 @@ def getName(cls): @classmethod def getParameterSetNames(cls): - return ["Default"] + return [ + "Default", + "Human 1" + ] @classmethod def getDefaultOptions(cls, parameterSetName="Default"): options = {} options["Base parameter set"] = "Human 1" if (parameterSetName == "Default") else parameterSetName - options["Structure"] = ( - "1-2, 2-3.1,3.2-4,3.3-5,3.4-6," - "4.2-7, 4.3-8, 6.2-9, 6.3-10," - "7.2-11, 7.3-12, 8.2-13, 8.3-14, 5.2-15, 5.3-16, 9.2-17, 9.3-18, 10.2-19, 10.3-20," - "11-21-22, 12-23-24, 13-25-26, 14-27-28, 15-29-30, 16-31-32, 17-33-34, 18-35-36, 19-37-38, 20-39-40") - options["Define inner coordinates"] = True - options["Ureter length"] = 3.0 - options["Ureter radius"] = 0.1 - options["Ureter bend angle degrees"] = 45 - options["Major calyx length"] = 0.6 - options["Major calyx radius"] = 0.1 - options["Major calyx angle degrees"] = 170 - options["Middle major calyx length"] = 0.4 - options["Major to bottom/top minor calyx length"] = 0.3 - options["Major to lower/upper minor calyx length"] = 0.3 - options["Bottom/top minor calyx length"] = 0.2 - options["Lower/upper minor calyx length"] = 0.2 - options["Minor calyx radius"] = 0.1 - options["Bottom/top minor calyx bifurcation angle degrees"] = 90 - options["Lower/upper minor calyx bifurcation angle degrees"] = 90 - options["Lower/upper minor calyx bend angle degrees"] = 10 - options["Renal pyramid length"] = 0.5 - options["Renal pyramid width"] = 0.5 - options["Inner proportion default"] = 0.8 - options["Inner proportion ureter"] = 0.7 + + if parameterSetName in ["Default", "Human 1"]: + options["Structure"] = ( + "1-2, 2-3.1,3.2-4,3.3-5,3.4-6,4.2-7,4.3-8," + "6.2-9,6.3-10,7.2-11,7.3-12,8.2-13,8.3-14,5.2-15,5.3-16,9.2-17,9.3-18,10.2-19,10.3-20," + "11-21-22-23,12-24-25-26,13-27-28-29,14-30-31-32,15-33-34-35,16-36-37-38,17-39-40-41,18-42-43-44,19-45-46-47,20-48-49-50") + + options["Define inner coordinates"] = True + options["Top major calyx"] = True + options["Middle major calyx"] = True + options["Bottom major calyx"] = True + options["Upper minor calyx"] = True + options["Lower minor calyx"] = True + options["Rotate upper, middle and lower minor calyxes"] = True + options["Number of calyxes at top minor calyx"] = 2 + options["Number of calyxes at middle major calyx"] = 2 + options["Number of calyxes at bottom minor calyx"] = 2 + options["Number of calyxes at upper minor calyx"] = 2 + options["Number of calyxes at lower minor calyx"] = 2 + options["Ureter length"] = 3.0 + options["Ureter radius"] = 0.1 + options["Ureter bend angle degrees"] = 45 + options["Major calyx length"] = 1.0 + options["Major calyx radius"] = 0.1 + options["Major calyx angle degrees"] = 170 + options["Middle major calyx length"] = 0.5 + options["Major to bottom/top minor calyx length"] = 0.6 + options["Major to lower/upper minor calyx length"] = 0.3 + options["Bottom/top minor calyx length"] = 0.2 + options["Lower/upper minor calyx length"] = 0.2 + options["Minor calyx radius"] = 0.1 + options["Bottom/top minor calyx bifurcation angle degrees"] = 90 + options["Bottom/top minor calyx rotate angle degrees"] = 0 + options["Lower/upper minor calyx bifurcation angle degrees"] = 90 + options["Lower/upper minor calyx bend angle degrees"] = 10 + options["Renal pyramid length"] = 0.5 + options["Renal pyramid width"] = 0.5 + options["Inner proportion default"] = 0.8 + options["Inner proportion ureter"] = 0.7 + return options @classmethod def getOrderedOptionNames(cls): return [ + "Top major calyx", + "Middle major calyx", + "Bottom major calyx", + "Upper minor calyx", + "Lower minor calyx", + "Rotate upper, middle and lower minor calyxes", + "Number of calyxes at top minor calyx", + "Number of calyxes at upper minor calyx", + "Number of calyxes at middle major calyx", + "Number of calyxes at lower minor calyx", + "Number of calyxes at bottom minor calyx", "Ureter length", "Ureter radius", "Ureter bend angle degrees", @@ -80,6 +110,7 @@ def getOrderedOptionNames(cls): "Lower/upper minor calyx length", "Minor calyx radius", "Bottom/top minor calyx bifurcation angle degrees", + "Bottom/top minor calyx rotate angle degrees", "Lower/upper minor calyx bifurcation angle degrees", "Lower/upper minor calyx bend angle degrees", "Renal pyramid length", @@ -124,7 +155,7 @@ def checkOptions(cls, options): options[key] = 0.9 for key, angleRange in { "Ureter bend angle degrees": (0.0, 45.0), - "Major calyx angle degrees": (130.0, 170.0), + "Major calyx angle degrees": (130.0, 200.0), "Bottom/top minor calyx bifurcation angle degrees": (60.0, 120.0), "Lower/upper minor calyx bifurcation angle degrees": (60.0, 120.0), "Lower/upper minor calyx bend angle degrees": (0.0, 10.0) @@ -134,8 +165,117 @@ def checkOptions(cls, options): elif options[key] > angleRange[1]: options[key] = angleRange[1] + options["Structure"] = cls.getPelvisLayoutStructure(options) + return dependentChanges + @classmethod + def getPelvisLayoutStructure(cls, options): + """ + Returns the 1D layout structure of the renal pelvis depending on user inputs. + """ + isTopMC = options["Top major calyx"] + isMidMC = options["Middle major calyx"] + isBottomMC = options["Bottom major calyx"] + isUpperMC = options["Upper minor calyx"] + isLowerMC = options["Lower minor calyx"] + + nTopMajorCalyxes = 2 if isUpperMC else 1 + nTopMinorCalyxes = options["Number of calyxes at top minor calyx"] + nMidMajorCalyxes = options["Number of calyxes at middle major calyx"] + nBottomMajorCalyxes = 2 if isLowerMC else 1 + nBottomMinorCalyxes = options["Number of calyxes at bottom minor calyx"] + nUpperMinorCalyxes = options["Number of calyxes at upper minor calyx"] + nLowerMinorCalyxes = options["Number of calyxes at lower minor calyx"] + + structure = "1-2, 2-3.1" + startNodeIdentifier = 3 + nodeDerivative = 2 + nodeIdentifier = 4 + + # major calyx + majorCalyxNodeIdentifiers = [] + for i in range(3): + if [isBottomMC, isMidMC, isTopMC][i]: + majorCalyxNodeIdentifiers.append(nodeIdentifier) + layout = str(startNodeIdentifier) + "." + str(nodeDerivative) + "-" + str(nodeIdentifier) + nodeDerivative += 1 + nodeIdentifier += 1 + structure = structure + "," + layout + else: + continue + + bmcNodeIdentifiers = [] + if isBottomMC: + startNodeIdentifier = majorCalyxNodeIdentifiers[0] + for n in range(nBottomMajorCalyxes): + bmcNodeIdentifiers.append(nodeIdentifier) + if isLowerMC: + layout = str(startNodeIdentifier) + "." + str(n + 2) + "-" + str(nodeIdentifier) + else: + layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + nodeIdentifier += 1 + structure = structure + "," + layout + + tmcNodeIdentifiers = [] + if isTopMC: + startNodeIdentifier = majorCalyxNodeIdentifiers[-1] + for n in range(nTopMajorCalyxes): + tmcNodeIdentifiers.append(nodeIdentifier) + if isUpperMC: + layout = str(startNodeIdentifier) + "." + str(n + 2) + "-" + str(nodeIdentifier) + else: + layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + nodeIdentifier += 1 + structure = structure + "," + layout + + minorCalyxNodeIndentifiers = [] + + if isBottomMC: + for i in range(nBottomMajorCalyxes): + nCalyxes = [nBottomMinorCalyxes, nLowerMinorCalyxes][i] + startNodeIdentifier = bmcNodeIdentifiers[i] + for j in range(nCalyxes): + minorCalyxNodeIndentifiers.append(nodeIdentifier) + if nCalyxes > 1: + layout = str(startNodeIdentifier) + "." + str(j + 2) + "-" + str(nodeIdentifier) + else: + layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + nodeIdentifier += 1 + structure = structure + "," + layout + + if isMidMC: + index = 0 if len(majorCalyxNodeIdentifiers) == 2 and isTopMC or len(majorCalyxNodeIdentifiers) == 1 else 1 + startNodeIdentifier = majorCalyxNodeIdentifiers[index] + for n in range(nMidMajorCalyxes): + minorCalyxNodeIndentifiers.append(nodeIdentifier) + if nMidMajorCalyxes > 1: + layout = str(startNodeIdentifier) + "." + str(n + 2) + "-" + str(nodeIdentifier) + else: + layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + nodeIdentifier += 1 + structure = structure + "," + layout + + if tmcNodeIdentifiers: + for i in range(nTopMajorCalyxes): + nCalyxes = [nTopMinorCalyxes, nUpperMinorCalyxes][i] + startNodeIdentifier = tmcNodeIdentifiers[i] + for j in range(nCalyxes): + minorCalyxNodeIndentifiers.append(nodeIdentifier) + if nCalyxes > 1: + layout = str(startNodeIdentifier) + "." + str(j + 2) + "-" + str(nodeIdentifier) + else: + layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + nodeIdentifier += 1 + structure = structure + "," + layout + + for nid in minorCalyxNodeIndentifiers: + layout = str(nid) + "-" + str(nodeIdentifier) + "-" + str(nodeIdentifier + 1) + "-" + str(nodeIdentifier + 2) + structure = structure + "," + layout + nodeIdentifier += 3 + + return structure + @classmethod def generateBaseMesh(cls, region, options): """ @@ -146,6 +286,21 @@ def generateBaseMesh(cls, region, options): """ # parameters structure = options["Structure"] + + isTopMC = options["Top major calyx"] + isMidMC = options["Middle major calyx"] + isBottomMC = options["Bottom major calyx"] + isUpperMC = options["Upper minor calyx"] + isLowerMC = options["Lower minor calyx"] + isRotateMinorCalyx = options["Rotate upper, middle and lower minor calyxes"] + + nTopMinorCalyxes = options["Number of calyxes at top minor calyx"] + nUpperMinorCalyxes = options["Number of calyxes at upper minor calyx"] + nMIdMinorCalyxes = options["Number of calyxes at middle major calyx"] + nLowerMinorCalyxes = options["Number of calyxes at lower minor calyx"] + nBottomMinorCalyxes = options["Number of calyxes at bottom minor calyx"] + nMinorCalyxesList = [nBottomMinorCalyxes, nLowerMinorCalyxes, nMIdMinorCalyxes, nTopMinorCalyxes, nUpperMinorCalyxes] + ureterLength = options["Ureter length"] ureterRadius = options["Ureter radius"] ureterBendAngle = options["Ureter bend angle degrees"] @@ -159,6 +314,7 @@ def generateBaseMesh(cls, region, options): minorCalyxRadius = options["Minor calyx radius"] lowerMinorCalyxLength = options["Lower/upper minor calyx length"] bottomMinorCalyxAngle = options["Bottom/top minor calyx bifurcation angle degrees"] + minorCalyxRotateAngle = options["Bottom/top minor calyx rotate angle degrees"] lowerMinorCalyxAngle = options["Lower/upper minor calyx bifurcation angle degrees"] minorCalyxBendAngle = options["Lower/upper minor calyx bend angle degrees"] pyramidLength = options["Renal pyramid length"] @@ -190,33 +346,37 @@ def generateBaseMesh(cls, region, options): meshGroup.addElement(element) elementIdentifier += 1 - majorCalyxElementsCount = 3 + majorCalyxElementsCount = sum([isBottomMC, isMidMC, isTopMC]) meshGroups = [renalPelvisMeshGroup, majorCalyxGroup.getMeshGroup(mesh)] + startIndex = 1 if sum([isBottomMC, isMidMC, isTopMC]) == 2 and isTopMC or sum([isBottomMC, isMidMC, isTopMC]) == 1 else 0 bottomMajor, middleMajor, topMajor = 0, 1, 2 - for e in range(majorCalyxElementsCount): + for e in range(startIndex, majorCalyxElementsCount + startIndex): element = mesh.findElementByIdentifier(elementIdentifier) - if e == middleMajor: - middleMajorCalyxIdentifier = elementIdentifier + if isMidMC: + if e == middleMajor: + middleMajorCalyxIdentifier = elementIdentifier + else: + middleMajorCalyxIdentifier = None for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 meshGroups = [renalPelvisMeshGroup, minorCalyxGroup.getMeshGroup(mesh)] bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 - for calyx in (bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor): - cElementIdentifier = middleMajorCalyxIdentifier if calyx == middleMajor else elementIdentifier + minorCalyxList = (([bottomMinor, lowerMinor] if isBottomMC else []) + ([middleMajor] if isMidMC else []) + + ([topMinor, upperMinor] if isTopMC else [])) + for calyx in minorCalyxList: + cElementIdentifier = middleMajorCalyxIdentifier if calyx == middleMajor and isMidMC else elementIdentifier element = mesh.findElementByIdentifier(cElementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) - elementIdentifier = elementIdentifier if calyx == middleMajor else elementIdentifier + 1 + elementIdentifier = elementIdentifier if calyx == middleMajor and isMidMC else elementIdentifier + 1 renalPyramidMeshGroup = renalPyramidGroup.getMeshGroup(mesh) minorCalyxElementsCount = 1 renalPyramidElementsCount = 2 - anterior, posterior = 0, 1 - bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 - for calyx in (bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor): - for side in (anterior, posterior): + for calyx in minorCalyxList: + for side in range(nMinorCalyxesList[calyx]): for count, groupCount in [(minorCalyxElementsCount, renalPyramidElementsCount)]: for e in range(count): element = mesh.findElementByIdentifier(elementIdentifier) @@ -263,84 +423,30 @@ def generateBaseMesh(cls, region, options): majorCalyxAngleRadians = math.radians(majorCalyxAngle / 2) sx = majorCalyxStartX bottomMajor, middleMajor, topMajor = 0, 1, 2 - majorCalyxNodeIdentifiers, majorCalyxXList = [], [] - for side in (bottomMajor, middleMajor, topMajor): - majorCalyxNodeIdentifiers.append(nodeIdentifier) - calyxLength = middleMajorLength if side == middleMajor else majorCalyxLength - majorCalyxAngle = 0 if side == middleMajor else majorCalyxAngleRadians - cosMajorCalyxAngle = math.cos(majorCalyxAngle) - sinMajorCalyxAngle = math.sin(-majorCalyxAngle if side == bottomMajor else majorCalyxAngle) - majorCalyxEndX = sx[0] + calyxLength * cosMajorCalyxAngle - majorCalyxEndY = sx[1] + calyxLength * sinMajorCalyxAngle - x = [majorCalyxEndX, majorCalyxEndY, 0.0] - majorCalyxXList.append(x) - sd1 = sub(x, sx) - sd2_list, sd3_list, sNodeIdentifiers = [], [], [] - for i in range(2): - isJunctionNode = i == 0 - nodeId = majorCalyxJunctionNodeIdentifier if isJunctionNode else nodeIdentifier - sNodeIdentifiers.append(nodeId) - node = nodes.findNodeByIdentifier(nodeId) - fieldcache.setNode(node) - version = {middleMajor: 3, bottomMajor: 2, topMajor: 4}[side] - sd3 = [0.0, 0.0, majorCalyxRadius] - sid3 = mult(sd3, innerProportionDefault) - sd2 = set_magnitude(cross(sd3, sd1), majorCalyxRadius) - sid2 = mult(sd2, innerProportionDefault) - sd2_list.append(sd2) - sd3_list.append(sd3) - if not isJunctionNode: - for field, derivatives in ( - (coordinates, [x, sd1, sd2, sd3]), - (innerCoordinates, [x, sd1, sid2, sid3]) - ): - for valueLabel, value in zip( - (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, - Node.VALUE_LABEL_D_DS3), - derivatives - ): - field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) - nodeIdentifier += 1 - setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) - setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) - - # top and bottom major calyx to minor calyx - lowerMinor, bottomMinor = 1, 0 - topMinor, upperMinor = 0, 1 - minorCalyxNodeIdentifiers, minorCalyxXList = [], [] - for calyx in [bottomMajor, topMajor]: - cNodeIdentifier = majorCalyxNodeIdentifiers[calyx] - sides = [bottomMinor, lowerMinor] if calyx == bottomMajor else [topMinor, upperMinor] - signValue = -1.0 if calyx == bottomMajor else 1.0 - sx = majorCalyxXList[calyx] - for side in sides: - calyxLength = majorToBottomMinorCalyxLength if side in (bottomMinor, topMinor) else majorToLowerMinorCalyxLength - x = [sx[0], sx[1] + calyxLength * signValue, sx[2]] if side in (bottomMinor, topMinor) else \ - [sx[0] + calyxLength, sx[1], sx[2]] - if side in (lowerMinor, upperMinor): - theta = math.radians(-minorCalyxBendAngle) if calyx == bottomMajor else math.radians(minorCalyxBendAngle) - rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) - ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) - x = [rx, ry, 0.0] + version = 2 + majorCalyxNodeIdentifiers, majorCalyxXList, majorCalyxD1List = [], [], [] + for calyx in (bottomMajor, middleMajor, topMajor): + if [isBottomMC, isMidMC, isTopMC][calyx]: + majorCalyxNodeIdentifiers.append(nodeIdentifier) + calyxLength = middleMajorLength if calyx == middleMajor else majorCalyxLength + majorCalyxAngle = 0 if calyx == middleMajor else majorCalyxAngleRadians + cosMajorCalyxAngle = math.cos(majorCalyxAngle) + sinMajorCalyxAngle = math.sin(-majorCalyxAngle if calyx == bottomMajor else majorCalyxAngle) + majorCalyxEndX = sx[0] + calyxLength * cosMajorCalyxAngle + majorCalyxEndY = sx[1] + calyxLength * sinMajorCalyxAngle + x = [majorCalyxEndX, majorCalyxEndY, 0.0] sd1 = sub(x, sx) - minorCalyxNodeIdentifiers.append(nodeIdentifier) - minorCalyxXList.append(x) - minorCalyx_sd2_list, minorCalyx_sd3_list, sNodeIdentifiers = [], [], [] + majorCalyxXList.append(x) + majorCalyxD1List.append(sd1) for i in range(2): isJunctionNode = i == 0 - nodeId = cNodeIdentifier if isJunctionNode else nodeIdentifier - sNodeIdentifiers.append(nodeId) + nodeId = majorCalyxJunctionNodeIdentifier if isJunctionNode else nodeIdentifier node = nodes.findNodeByIdentifier(nodeId) fieldcache.setNode(node) - - version = 2 if side == 0 else 3 - sd3 = [0.0, 0.0, minorCalyxRadius] - sd2 = [minorCalyxRadius, 0.0, 0.0] if (calyx == bottomMajor and version == 2) \ - else set_magnitude(cross(sd3, sd1), minorCalyxRadius) - sid2 = mult(sd2, innerProportionDefault) + sd3 = [0.0, 0.0, majorCalyxRadius] sid3 = mult(sd3, innerProportionDefault) - minorCalyx_sd2_list.append(sd2) - minorCalyx_sd3_list.append(sd3) + sd2 = set_magnitude(cross(sd3, sd1), majorCalyxRadius) + sid2 = mult(sd2, innerProportionDefault) if not isJunctionNode: for field, derivatives in ( (coordinates, [x, sd1, sd2, sd3]), @@ -355,160 +461,277 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + version += 1 + else: + majorCalyxNodeIdentifiers.append(None) + majorCalyxXList.append(None) + majorCalyxD1List.append(None) + + # top and bottom major calyx to minor calyx + lowerMinor, bottomMinor = 1, 0 + topMinor, upperMinor = 0, 1 + minorCalyxNodeIdentifiers, minorCalyxXList, minorCalyxD1List = [], [], [] + for calyx in [bottomMajor, middleMajor, topMajor]: + if calyx == middleMajor: + minorCalyxNodeIdentifiers.append(majorCalyxNodeIdentifiers[1]) + minorCalyxXList.append(majorCalyxXList[1]) + minorCalyxD1List.append(majorCalyxD1List[1]) + continue + else: + sides = [bottomMinor, lowerMinor] if calyx == bottomMajor else [topMinor, upperMinor] + if [isBottomMC, isMidMC, isTopMC][calyx]: + cNodeIdentifier = majorCalyxNodeIdentifiers[calyx] + signValue = -1.0 if calyx == bottomMajor else 1.0 + sx = majorCalyxXList[calyx] + for side in sides: + if side in [lowerMinor, upperMinor]: + if (calyx == bottomMajor and not isLowerMC) or (calyx == topMajor and not isUpperMC): + minorCalyxNodeIdentifiers.append(None) + minorCalyxXList.append(None) + minorCalyxD1List.append(None) + continue + calyxLength = majorToBottomMinorCalyxLength if side in (bottomMinor, topMinor) else majorToLowerMinorCalyxLength + x = [sx[0], sx[1] + calyxLength * signValue, sx[2]] if side in (bottomMinor, topMinor) else \ + [sx[0] + calyxLength, sx[1], sx[2]] + # rotate minor calyxes if rotate angle is not zero + theta = math.radians(-minorCalyxRotateAngle) if calyx == bottomMajor else math.radians(minorCalyxRotateAngle) + rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) + ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) + x = [rx, ry, 0.0] + if side in (lowerMinor, upperMinor): + # bend lower and upper minor calyxes based on specified bend angle + theta = math.radians(-minorCalyxBendAngle) if calyx == bottomMajor else math.radians(minorCalyxBendAngle) + rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) + ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) + x = [rx, ry, 0.0] + sd1 = sub(x, sx) + minorCalyxNodeIdentifiers.append(nodeIdentifier) + minorCalyxXList.append(x) + minorCalyxD1List.append(sd1) + for i in range(2): + isJunctionNode = i == 0 + nodeId = cNodeIdentifier if isJunctionNode else nodeIdentifier + node = nodes.findNodeByIdentifier(nodeId) + fieldcache.setNode(node) + + version = 2 if side == 0 else 3 + sd3 = [0.0, 0.0, minorCalyxRadius] + sd2 = [minorCalyxRadius, 0.0, 0.0] if (calyx == bottomMajor and version == 2) \ + else set_magnitude(cross(sd3, sd1), minorCalyxRadius) + sid2 = mult(sd2, innerProportionDefault) + sid3 = mult(sd3, innerProportionDefault) + if not isJunctionNode: + for field, derivatives in ( + (coordinates, [x, sd1, sd2, sd3]), + (innerCoordinates, [x, sd1, sid2, sid3]) + ): + for valueLabel, value in zip( + (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D_DS3), + derivatives + ): + field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) + nodeIdentifier += 1 + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + else: + for side in sides: + minorCalyxNodeIdentifiers.append(None) + minorCalyxXList.append(None) + minorCalyxD1List.append(None) # minor calyx to renal pyramid connection anterior, posterior = 0, 1 - bottomMinor, lowerMinor, topMinor, upperMinor, middleMajor = 0, 1, 2, 3, 4 + bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 renalPyramidStartX, renalPyramidNodeIdentifiers = [], [] connection_sd2_list, connection_sd3_list = [], [] for calyx in (bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor): - cNodeIdentifier = majorCalyxNodeIdentifiers[1] if calyx == middleMajor else minorCalyxNodeIdentifiers[calyx] - - sx = majorCalyxXList[1] if calyx == middleMajor else minorCalyxXList[calyx] - minorCalyxLength = bottomMinorCalyxLength if calyx in (bottomMinor, topMinor) else lowerMinorCalyxLength - minorCalyxAngle = bottomMinorCalyxAngle if calyx in (bottomMinor, topMinor) else lowerMinorCalyxAngle - minorCalyxHalfAngle = 0.5 * minorCalyxAngle - minorCalyxHalfAngleRadians = math.radians(minorCalyxHalfAngle) - sinMinorCalyxAngle = math.sin(minorCalyxHalfAngleRadians) - cosMinorCalyxAngle = math.cos(minorCalyxHalfAngleRadians) - - renalPyramidNodeIdentifiers.append([]) - renalPyramidStartX.append([]) - connection_sd2_list.append([]) - connection_sd3_list.append([]) - for side in [anterior, posterior]: - if calyx in [bottomMinor, topMinor]: - nx = sx[0] + minorCalyxLength * (-sinMinorCalyxAngle if side == anterior else sinMinorCalyxAngle) - ny = sx[1] + minorCalyxLength * (-cosMinorCalyxAngle if calyx == bottomMinor else cosMinorCalyxAngle) - x = [nx, ny, 0.0] - elif calyx in [lowerMinor, upperMinor]: - signValue = -1 if calyx == lowerMinor else 1 - nx = sx[0] + minorCalyxLength * cosMinorCalyxAngle - ny = sx[1] + minorCalyxLength * math.sin(math.radians(minorCalyxBendAngle)) * signValue - nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) - x = [nx, ny, nz] - else: - nx = sx[0] + minorCalyxLength * cosMinorCalyxAngle - nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) - x = [nx, sx[1], nz] - renalPyramidNodeIdentifiers[-1].append(nodeIdentifier) - renalPyramidStartX[-1].append(x) - sd1 = sub(x, sx) - if calyx in [bottomMinor, topMinor]: - sd3 = [0.0, 0.0, minorCalyxRadius] - sd2 = set_magnitude(cross(sd3, sd1), minorCalyxRadius) - elif calyx in [lowerMinor, upperMinor]: - sd2 = set_magnitude(cross([0.0, 0.0, 1.0], sd1), minorCalyxRadius) - sd3 = set_magnitude(cross(sd1, sd2), minorCalyxRadius) + if minorCalyxNodeIdentifiers[calyx] is None: + renalPyramidNodeIdentifiers.append(None) + renalPyramidStartX.append(None) + connection_sd2_list.append(None) + connection_sd3_list.append(None) + continue + else: + renalPyramidNodeIdentifiers.append([]) + renalPyramidStartX.append([]) + connection_sd2_list.append([]) + connection_sd3_list.append([]) + index = 0 if calyx <= lowerMinor else (1 if calyx == middleMajor else 2) + if [isBottomMC, isMidMC, isTopMC][index]: + cNodeIdentifier = minorCalyxNodeIdentifiers[calyx] + sx = minorCalyxXList[calyx] + minorCalyxLength = bottomMinorCalyxLength if calyx in (bottomMinor, topMinor) else lowerMinorCalyxLength + if nMinorCalyxesList[calyx] > 1: + minorCalyxAngle = bottomMinorCalyxAngle if calyx in (bottomMinor, topMinor) else lowerMinorCalyxAngle else: - sd2 = [0.0, minorCalyxRadius, 0.0] - sd3 = set_magnitude(cross(sd1, sd2), minorCalyxRadius) - connection_sd2_list[-1].append(sd2) - connection_sd3_list[-1].append(sd3) - - sid2 = mult(sd2, innerProportionDefault) - sid3 = mult(sd3, innerProportionDefault) - sNodeIdentifiers = [] - version = 2 if side == anterior else 3 - for i in range(2): - isJunctionNode = i == 0 - nodeId = cNodeIdentifier if isJunctionNode else nodeIdentifier - sNodeIdentifiers.append(nodeId) - node = nodes.findNodeByIdentifier(nodeId) - fieldcache.setNode(node) - if not isJunctionNode: - for field, derivatives in ( - (coordinates, [x, sd1, sd2, sd3]), - (innerCoordinates, [x, sd1, sid2, sid3]) - ): - for valueLabel, value in zip( - (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, - Node.VALUE_LABEL_D_DS3), - derivatives + minorCalyxAngle = 0 + minorCalyxHalfAngle = 0.5 * minorCalyxAngle + minorCalyxHalfAngleRadians = math.radians(minorCalyxHalfAngle) + sinMinorCalyxAngle = math.sin(minorCalyxHalfAngleRadians) + cosMinorCalyxAngle = math.cos(minorCalyxHalfAngleRadians) + for side in range(nMinorCalyxesList[calyx]): + if calyx in [bottomMinor, topMinor]: + nx = sx[0] + minorCalyxLength * (-sinMinorCalyxAngle if side == anterior else sinMinorCalyxAngle) + ny = sx[1] + minorCalyxLength * (-cosMinorCalyxAngle if calyx == bottomMinor else cosMinorCalyxAngle) + x = [nx, ny, 0.0] + elif calyx == lowerMinor and isLowerMC or calyx == upperMinor and isUpperMC: + nx = sx[0] + minorCalyxLength * cosMinorCalyxAngle + nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) + x = [nx, sx[1], nz] + if isRotateMinorCalyx: + rotateAxis = normalize(minorCalyxD1List[calyx]) + tx = rotate_vector_around_vector((sub(x, sx)), rotateAxis, math.radians(90)) + x = add(tx, sx) + else: + nx = sx[0] + minorCalyxLength * cosMinorCalyxAngle + nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) + x = [nx, sx[1], nz] + if isRotateMinorCalyx: + rotateAxis = normalize(minorCalyxD1List[calyx]) + tx = rotate_vector_around_vector((sub(x, sx)), rotateAxis, math.radians(90)) + x = add(tx, sx) + + if calyx != middleMajor: + theta = math.radians(-minorCalyxRotateAngle) if calyx == bottomMajor else math.radians(minorCalyxRotateAngle) + rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) + ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) + x = [rx, ry, x[2]] + + renalPyramidNodeIdentifiers[-1].append(nodeIdentifier) + renalPyramidStartX[-1].append(x) + sd1 = sub(x, sx) + if calyx in [bottomMinor, topMinor]: + sd3 = [0.0, 0.0, minorCalyxRadius] + sd2 = set_magnitude(cross(sd3, sd1), minorCalyxRadius) + else: + sd2 = set_magnitude(cross([0.0, 0.0, 1.0], sd1), minorCalyxRadius) + sd3 = set_magnitude(cross(sd1, sd2), minorCalyxRadius) + connection_sd2_list[-1].append(sd2) + connection_sd3_list[-1].append(sd3) + + sid2 = mult(sd2, innerProportionDefault) + sid3 = mult(sd3, innerProportionDefault) + sNodeIdentifiers = [] + version = 2 if side == anterior else 3 + for i in range(2): + isJunctionNode = i == 0 + nodeId = cNodeIdentifier if isJunctionNode else nodeIdentifier + sNodeIdentifiers.append(nodeId) + node = nodes.findNodeByIdentifier(nodeId) + fieldcache.setNode(node) + if not isJunctionNode: + for field, derivatives in ( + (coordinates, [x, sd1, sd2, sd3]), + (innerCoordinates, [x, sd1, sid2, sid3]) ): - field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) - nodeIdentifier += 1 - setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) - setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + for valueLabel, value in zip( + (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D_DS3), + derivatives + ): + field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) + nodeIdentifier += 1 + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + else: + renalPyramidNodeIdentifiers.append([]) + renalPyramidStartX.append([]) + connection_sd2_list.append([]) + connection_sd3_list.append([]) + for side in range(nMinorCalyxesList[calyx]): + renalPyramidNodeIdentifiers[-1].append(None) + renalPyramidStartX[-1].append(None) + connection_sd2_list[-1].append(None) + connection_sd3_list[-1].append(None) # minor calyx to renal pyramids - pyramidElementsCount = 2 + pyramidElementsCount = 3 pyramidHalfWidth = 0.5 * pyramidWidth bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 for calyx in [bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor]: - minorCalyxAngle = bottomMinorCalyxAngle if calyx in (bottomMinor, topMinor) else lowerMinorCalyxAngle - minorCalyxHalfAngle = 0.5 * minorCalyxAngle - minorCalyxHalfAngleRadians = math.radians(minorCalyxHalfAngle) - sinMinorCalyxAngle = math.sin(minorCalyxHalfAngleRadians) - cosMinorCalyxAngle = math.cos(minorCalyxHalfAngleRadians) - for side in [anterior, posterior]: - cNodeIdentifier = renalPyramidNodeIdentifiers[calyx][side] - sx = renalPyramidStartX[calyx][side] - if calyx in [bottomMinor, topMinor]: - nx = -sinMinorCalyxAngle if side == anterior else sinMinorCalyxAngle - ny = -cosMinorCalyxAngle if calyx == bottomMinor else cosMinorCalyxAngle - pyramidDirn = [nx, ny, 0.0] - elif calyx in [lowerMinor, upperMinor]: - tx = [1.0, 0.0, 0.0] - theta = math.radians(-minorCalyxBendAngle) if calyx == lowerMinor else math.radians(minorCalyxBendAngle) - rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) - ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) - pyramidDirn = [rx, ry, 0.0] - else: - pyramidDirn = [1.0, 0.0, 0.0] - xList = [] - for e in range(pyramidElementsCount): - pyramidLengthScale = 0.7 * pyramidLength if e == 0 else pyramidLength - x = add(sx, mult(pyramidDirn, (pyramidLengthScale))) - xList.append(x) - pyramid_sd2_list = [] - pyramid_sd3_list = [] - sNodeIdentifiers = [] - for e in range(pyramidElementsCount): - sNodeIdentifiers.append(nodeIdentifier) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - - sd1 = sub(xList[1], xList[0]) - pyramidWidthScale = pyramidHalfWidth if e == 0 else 0.9 * pyramidHalfWidth - pyramidThickness = 1.1 * minorCalyxRadius if e == 0 else minorCalyxRadius - if calyx in [bottomMinor, topMinor]: - sd3 = [0.0, 0.0, pyramidThickness] - sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) - elif calyx in [lowerMinor, upperMinor]: - sd3 = [0.0, 0.0, pyramidThickness] - sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) + if renalPyramidNodeIdentifiers[calyx] is None: + continue + else: + index = 0 if calyx <= lowerMinor else (1 if calyx == middleMajor else 2) + if [isBottomMC, isMidMC, isTopMC][index]: + if nMinorCalyxesList[calyx] > 1: + minorCalyxAngle = bottomMinorCalyxAngle if calyx in (bottomMinor, topMinor) else lowerMinorCalyxAngle else: - sd2 = [0.0, pyramidWidthScale, 0.0] - sd3 = set_magnitude(cross(sd1, sd2), pyramidThickness) - pyramid_sd2_list.append(sd2) - pyramid_sd3_list.append(sd3) - sid2 = mult(sd2, innerProportionDefault) - sid3 = mult(sd3, innerProportionDefault) - for field, derivatives in ( - (coordinates, [xList[e], sd1, sd2, sd3]), - (innerCoordinates, [xList[e], sd1, sid2, sid3]) - ): - for valueLabel, value in zip( - (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, - Node.VALUE_LABEL_D_DS3), - derivatives - ): - field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) - nodeIdentifier += 1 - pyramid_sd2_list.append(connection_sd2_list[calyx][side]) - pyramid_sd3_list.append(connection_sd3_list[calyx][side]) - for e in range(pyramidElementsCount): - node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) - fieldcache.setNode(node) - sd12 = sub(pyramid_sd2_list[e + 1], pyramid_sd2_list[e]) - sd13 = sub(pyramid_sd3_list[e + 1], pyramid_sd3_list[e]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sd13) - sid12 = mult(sd12, innerProportionDefault) - sid13 = mult(sd13, innerProportionDefault) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sid12) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sid13) + minorCalyxAngle = 0 + minorCalyxHalfAngle = 0.5 * minorCalyxAngle + minorCalyxHalfAngleRadians = math.radians(minorCalyxHalfAngle) + sinMinorCalyxAngle = math.sin(minorCalyxHalfAngleRadians) + cosMinorCalyxAngle = math.cos(minorCalyxHalfAngleRadians) + for side in range(nMinorCalyxesList[calyx]): + sx = renalPyramidStartX[calyx][side] + if calyx in [bottomMinor, topMinor]: + nx = -sinMinorCalyxAngle if side == anterior else sinMinorCalyxAngle + ny = -cosMinorCalyxAngle if calyx == bottomMinor else cosMinorCalyxAngle + pyramidDirn = [nx, ny, 0.0] + else: + tx = [1.0, 0.0, 0.0] + theta = math.radians(-minorCalyxBendAngle) if calyx == lowerMinor else (0 if calyx == middleMajor else math.radians(minorCalyxBendAngle)) + rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) + ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) + pyramidDirn = [rx, ry, 0.0] + if isRotateMinorCalyx and nMinorCalyxesList[calyx] > 1: + rotateAngle = math.radians(-20 if side == anterior else 20) + pyramidDirn = rotate_about_z_axis(pyramidDirn, rotateAngle) + xList = [] + for e in range(pyramidElementsCount): + pyramidLengthScale = 0.3 * pyramidLength if e == 0 else (0.7 * pyramidLength if e == 1 else pyramidLength) + x = add(sx, mult(pyramidDirn, (pyramidLengthScale))) + xList.append(x) + pyramid_sd2_list = [] + pyramid_sd3_list = [] + sNodeIdentifiers = [] + for e in range(pyramidElementsCount): + sNodeIdentifiers.append(nodeIdentifier) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + + sd1 = sub(xList[1], xList[0]) + pyramidWidthScale = minorCalyxRadius if e == 0 else (pyramidHalfWidth if e == 1 else 0.9 * pyramidHalfWidth) + pyramidThickness = 1.1 * minorCalyxRadius if e == 1 else minorCalyxRadius + if calyx in [bottomMinor, topMinor]: + sd3 = [0.0, 0.0, pyramidThickness] + sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) + else: + sd3 = [0.0, 0.0, pyramidThickness] + sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) + pyramid_sd2_list.append(sd2) + pyramid_sd3_list.append(sd3) + sid2 = mult(sd2, innerProportionDefault) + sid3 = mult(sd3, innerProportionDefault) + + for field, derivatives in ( + (coordinates, [xList[e], sd1, sd2, sd3]), + (innerCoordinates, [xList[e], sd1, sid2, sid3]) + ): + for valueLabel, value in zip( + (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D_DS3), + derivatives + ): + field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) + nodeIdentifier += 1 + + pyramid_sd2_list.append(connection_sd2_list[calyx][side]) + pyramid_sd3_list.append(connection_sd3_list[calyx][side]) + for e in range(pyramidElementsCount): + node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) + fieldcache.setNode(node) + sd12 = sub(pyramid_sd2_list[e + 1], pyramid_sd2_list[e]) + sd13 = sub(pyramid_sd3_list[e + 1], pyramid_sd3_list[e]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sd13) + sid12 = mult(sd12, innerProportionDefault) + sid13 = mult(sd13, innerProportionDefault) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sid12) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sid13) + else: + continue return annotationGroups, networkMesh @@ -536,17 +759,15 @@ def getName(cls): @classmethod def getParameterSetNames(cls): - return [ - "Default", - "Human 1" - ] + return MeshType_1d_renal_pelvis_network_layout1.getParameterSetNames() @classmethod def getDefaultOptions(cls, parameterSetName='Default'): options = {} useParameterSetName = "Human 1" if (parameterSetName == "Default") else parameterSetName options["Base parameter set"] = useParameterSetName - options["Network layout"] = ScaffoldPackage(MeshType_1d_renal_pelvis_network_layout1) + options["Network layout"] = ScaffoldPackage(MeshType_1d_renal_pelvis_network_layout1, + defaultParameterSetName=useParameterSetName) options["Elements count around"] = 8 options["Elements count through shell"] = 1 options["Annotation elements counts around"] = [0] From 6a4e99640f1e2bbb2b8afea351096ade4ffbf922 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 22 Jul 2025 14:59:40 +1200 Subject: [PATCH 04/18] Add checks for number of calyxes --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index 03e7ab07..e69a9f2c 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -139,12 +139,23 @@ def checkOptions(cls, options): "Lower/upper minor calyx bifurcation angle degrees", "Lower/upper minor calyx bend angle degrees", "Renal pyramid length", - "Renal pyramid width", - "Inner proportion default", - "Inner proportion ureter" + "Renal pyramid width" ]: if options[key] < 0.1: - options[key] = 0.1 # check again + options[key] = 0.1 + + for key in [ + "Number of calyxes at top minor calyx", + "Number of calyxes at upper minor calyx", + "Number of calyxes at middle major calyx", + "Number of calyxes at lower minor calyx", + "Number of calyxes at bottom minor calyx" + ]: + if options[key] < 1: + options[key] = 1 + elif options[key] > 2: + options[key] = 2 + for key in [ "Inner proportion default", "Inner proportion ureter" @@ -153,6 +164,7 @@ def checkOptions(cls, options): options[key] = 0.1 elif options[key] > 0.9: options[key] = 0.9 + for key, angleRange in { "Ureter bend angle degrees": (0.0, 45.0), "Major calyx angle degrees": (130.0, 200.0), From 58e65147ef69453bb7070dbbd11cd7bcd491f122 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 22 Jul 2025 16:38:05 +1200 Subject: [PATCH 05/18] Fix annotation for renal pyramid group --- src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index e69a9f2c..e3337d15 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -386,14 +386,18 @@ def generateBaseMesh(cls, region, options): renalPyramidMeshGroup = renalPyramidGroup.getMeshGroup(mesh) minorCalyxElementsCount = 1 - renalPyramidElementsCount = 2 + pyramidElementsCount = 3 for calyx in minorCalyxList: for side in range(nMinorCalyxesList[calyx]): - for count, groupCount in [(minorCalyxElementsCount, renalPyramidElementsCount)]: + for count, groupCount in [(minorCalyxElementsCount, pyramidElementsCount)]: for e in range(count): element = mesh.findElementByIdentifier(elementIdentifier) renalPyramidMeshGroup.addElement(element) elementIdentifier += 1 + for e in range(pyramidElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + renalPyramidMeshGroup.addElement(element) + elementIdentifier += 1 # set coordinates (outer) fieldcache = fieldmodule.createFieldcache() From bb2dddf3af71dd2c0c5d2e502a4ba99b0414483e Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 23 Jul 2025 12:40:46 +1200 Subject: [PATCH 06/18] Fix core and shell annotations --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 23 +++--- src/scaffoldmaker/utils/tubenetworkmesh.py | 74 ++++++++++++++++++- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index e3337d15..7c4d1c88 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -385,19 +385,24 @@ def generateBaseMesh(cls, region, options): elementIdentifier = elementIdentifier if calyx == middleMajor and isMidMC else elementIdentifier + 1 renalPyramidMeshGroup = renalPyramidGroup.getMeshGroup(mesh) + minorCalyxMeshGroup = minorCalyxGroup.getMeshGroup(mesh) + meshGroups = [renalPelvisMeshGroup, minorCalyxMeshGroup, renalPyramidMeshGroup] minorCalyxElementsCount = 1 pyramidElementsCount = 3 for calyx in minorCalyxList: for side in range(nMinorCalyxesList[calyx]): - for count, groupCount in [(minorCalyxElementsCount, pyramidElementsCount)]: - for e in range(count): - element = mesh.findElementByIdentifier(elementIdentifier) - renalPyramidMeshGroup.addElement(element) - elementIdentifier += 1 - for e in range(pyramidElementsCount): - element = mesh.findElementByIdentifier(elementIdentifier) - renalPyramidMeshGroup.addElement(element) - elementIdentifier += 1 + for e in range(minorCalyxElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + + for calyx in minorCalyxList: + for side in range(nMinorCalyxesList[calyx]): + for e in range(pyramidElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + renalPyramidMeshGroup.addElement(element) + elementIdentifier += 1 # set coordinates (outer) fieldcache = fieldmodule.createFieldcache() diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index cbe5d3fd..8f7a3259 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -1383,6 +1383,50 @@ def _addRimElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGro element = mesh.findElementByIdentifier(elementIdentifier) meshGroup.addElement(element) + def _removeBoxElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGroup): + """ + Remove 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 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): + boxSlice = self._boxElementIds[e2] + if boxSlice: + # print(boxSlice[e1Start:e1Limit]) + for elementIdentifiersList in boxSlice[e1Start:e1Limit]: + for elementIdentifier in elementIdentifiersList[e3Start:e3Limit]: + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroup.removeElement(element) + + def _removeRimElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGroup): + """ + Remove 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: Limi element index rim. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + elementsCountAlong = self.getSampledElementsCountAlong() + mesh = meshGroup.getMasterMesh() + for e2 in range(elementsCountAlong): + rimSlice = self._rimElementIds[e2] + if rimSlice: + for elementIdentifiersList in rimSlice[e3Start:e3Limit]: + partElementIdentifiersList = elementIdentifiersList[e1Start:e1Limit] if (e1Start >= 0) else ( + elementIdentifiersList[e1Start:] + elementIdentifiersList[:e1Limit]) + if None in elementIdentifiersList: + break + for elementIdentifier in partElementIdentifiersList: + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroup.removeElement(element) + def addCoreElementsToMeshGroup(self, meshGroup): """ Ensure all core elements in core box or rim arrays are in mesh group. @@ -1413,6 +1457,17 @@ def addAllElementsToMeshGroup(self, meshGroup): self.addCoreElementsToMeshGroup(meshGroup) self.addShellElementsToMeshGroup(meshGroup) + def removeAllElementsFromMeshGroup(self, meshGroup): + """ + + """ + if not self._isCore: + return + self._removeBoxElementsToMeshGroup(0, self._elementsCountCoreBoxMajor, + 0, self._elementsCountCoreBoxMinor, meshGroup) + self._removeRimElementsToMeshGroup(0, self._elementsCountAround, + 0, self._elementsCountTransition, meshGroup) + def addSideD2ElementsToMeshGroup(self, side: bool, meshGroup): """ Add elements to the mesh group on side of +d2 or -d2, often matching left and right. @@ -1445,6 +1500,12 @@ def addSideD3ElementsToMeshGroup(self, side: bool, meshGroup): e1Limit = e1Start + (self._elementsCountAround // 2) self._addRimElementsToMeshGroup(e1Start, e1Limit, 0, self.getElementsCountRim(), meshGroup) + def getCoreStatus(self): + """ + + """ + return self._isCore + def getRimNodeIdsSlice(self, n2): """ Get slice of rim node IDs. @@ -3287,11 +3348,22 @@ def checkSegmentCore(self, networkSegment): if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationGroup.getMeshGroup(self._layoutMesh)): if "renal pyramid" in layoutAnnotationGroup.getTerm(): self._isCore = True + else: + self._isCore = False return self._isCore def generateMesh(self, generateData): super(RenalPelvisTubeNetworkMeshBuilder, self).generateMesh(generateData) - + if self._isCore: + coreMeshGroup = generateData.getCoreMeshGroup() + shellMeshGroup = generateData.getShellMeshGroup() + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + annotationTerms = segment.getAnnotationTerms() + for annotationTerm in annotationTerms: + if "calyx" in annotationTerm[0]: + segment.addAllElementsToMeshGroup(shellMeshGroup) + segment.removeAllElementsFromMeshGroup(coreMeshGroup) class TubeEllipseGenerator: """ From 341a8a446ce7e99bcf6ce5e0d1fbb46862844b7a Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 23 Jul 2025 15:14:48 +1200 Subject: [PATCH 07/18] Fix generation of core elements for renal pyramids --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 11 +++++++---- src/scaffoldmaker/utils/tubenetworkmesh.py | 6 +++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index 7c4d1c88..af74c8d2 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -521,10 +521,13 @@ def generateBaseMesh(cls, region, options): x = [rx, ry, 0.0] if side in (lowerMinor, upperMinor): # bend lower and upper minor calyxes based on specified bend angle - theta = math.radians(-minorCalyxBendAngle) if calyx == bottomMajor else math.radians(minorCalyxBendAngle) - rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) - ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) - x = [rx, ry, 0.0] + ec = nMinorCalyxesList[1] if side == lowerMinor else nMinorCalyxesList[4] + minorCalyxBendAngle = 0 if ec < 2 else minorCalyxBendAngle + if minorCalyxBendAngle > 0: + theta = math.radians(-minorCalyxBendAngle) if calyx == bottomMajor else math.radians(minorCalyxBendAngle) + rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) + ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) + x = [rx, ry, 0.0] sd1 = sub(x, sx) minorCalyxNodeIdentifiers.append(nodeIdentifier) minorCalyxXList.append(x) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 8f7a3259..e6cc8375 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -1193,7 +1193,7 @@ def getBoxNodeIdsSlice(self, n2): :param n2: Node index along segment, including negative indexes from end. :return: Node IDs arrays, or None if not set. """ - return self._boxNodeIds[n2] + return None if self._boxNodeIds is None else self._boxNodeIds[n2] def getBoxBoundaryNodeIds(self, n1, n2): """ @@ -1652,6 +1652,8 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): elementsCountAcrossMinor = self.getCoreBoxMinorNodesCount() - 1 elementsCountAcrossMajor = self.getCoreBoxMajorNodesCount() - 1 for e3 in range(elementsCountAcrossMajor): + if self._boxNodeIds[e2] is None: + continue e3p = e3 + 1 elementIds = [] for e1 in range(elementsCountAcrossMinor): @@ -1671,6 +1673,8 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): triplePointIndexesList = self.getTriplePointIndexes() ringElementIds = [] for e1 in range(self._elementsCountAround): + if self._boxBoundaryNodeIds[e2] is None: + continue nids, nodeParameters, nodeLayouts = [], [], [] n1p = (e1 + 1) % self._elementsCountAround location = self.getTriplePointLocation(e1) From d1b26ca38bf043423a2570463f057ef1ecab5c38 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 28 Jul 2025 09:06:25 +1200 Subject: [PATCH 08/18] Fix unit test --- tests/test_renalpelvis.py | 43 ++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/tests/test_renalpelvis.py b/tests/test_renalpelvis.py index df7e3b3d..d7b55b79 100644 --- a/tests/test_renalpelvis.py +++ b/tests/test_renalpelvis.py @@ -47,13 +47,13 @@ def test_renalpelvis(self): fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(656, mesh3d.getSize()) + self.assertEqual(672, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(2476, mesh2d.getSize()) + self.assertEqual(2532, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(2991, mesh1d.getSize()) + self.assertEqual(3063, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(1172, nodes.getSize()) + self.assertEqual(1204, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -62,8 +62,8 @@ def test_renalpelvis(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [0.7789485582121838, -2.128648920925165, -0.25340433034411736], tol) - assertAlmostEqualList(self, maximums, [4.04142135623731, 1.5517905914526038, 0.2534043303441174], tol) + assertAlmostEqualList(self, minimums, [0.7789485582121838, -2.250268470689302, -0.1318007396451522], tol) + assertAlmostEqualList(self, maximums, [4.188222198878539, 2.250268470689302, 0.1318007396451522], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -81,18 +81,18 @@ def test_renalpelvis(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.4091337158319937, delta=tol) - self.assertAlmostEqual(surfaceArea, 13.670578210256929, delta=tol) + self.assertAlmostEqual(volume, 0.41239028202973105, delta=tol) + self.assertAlmostEqual(surfaceArea, 15.14363706045464, delta=tol) # check some annotation groups: expectedSizes3d = { - "core": (320, 0.2200233669825981), - "major calyx": (48, 0.015284024590047216), - "minor calyx": (80, 0.014873238147161206), - "renal pelvis": (176, 0.07598967760825394), - "renal pyramid": (80, 0.017177432136007482), - "shell": (336, 0.18913226215956122), + "core": (240, 0.19386743947037463), + "major calyx": (64, 0.025523445233479505), + "minor calyx": (160, 0.039595947108606436), + "renal pelvis": (272, 0.10994753925440572), + "renal pyramid": (480, 0.3199459642092786), + "shell": (432, 0.21852600996918725), "ureter": (64, 0.049524462117630445) } for name in expectedSizes3d: @@ -109,13 +109,11 @@ def test_renalpelvis(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "core": (1302, 12.615540404295311), - "major calyx": (208, 1.837248623082175), - "minor calyx": (358, 1.8841596431915135), - "renal pelvis": (734, 7.5061456904809285), - "renal pyramid": (382, 2.1844517878410996), - "shell": (1454, 18.19860793812061), - "ureter": (264, 4.292647846731792) + "major calyx": (272, 3.0306454318904343), + "minor calyx": (692, 4.858039077961195), + "renal pelvis": (1132, 11.555677753247785), + "renal pyramid": (1780, 18.695951082914355), + "ureter": (264, 4.2985089237162715) } for name in expectedSizes2d: term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) @@ -129,3 +127,6 @@ def test_renalpelvis(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 bd333a44fd2d52c755ab69e90dafa058457d388d Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 28 Jul 2025 10:22:50 +1200 Subject: [PATCH 09/18] Add Rat 1 parameter set --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index af74c8d2..e1d9615c 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -34,7 +34,8 @@ def getName(cls): def getParameterSetNames(cls): return [ "Default", - "Human 1" + "Human 1", + "Rat 1" ] @classmethod @@ -46,7 +47,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Structure"] = ( "1-2, 2-3.1,3.2-4,3.3-5,3.4-6,4.2-7,4.3-8," "6.2-9,6.3-10,7.2-11,7.3-12,8.2-13,8.3-14,5.2-15,5.3-16,9.2-17,9.3-18,10.2-19,10.3-20," - "11-21-22-23,12-24-25-26,13-27-28-29,14-30-31-32,15-33-34-35,16-36-37-38,17-39-40-41,18-42-43-44,19-45-46-47,20-48-49-50") + "11-21-22-23,12-24-25-26,13-27-28-29,14-30-31-32,15-33-34-35,16-36-37-38,17-39-40-41,18-42-43-44,19-45-46-47,20-48-49-50" + ) options["Define inner coordinates"] = True options["Top major calyx"] = True @@ -81,6 +83,44 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Inner proportion default"] = 0.8 options["Inner proportion ureter"] = 0.7 + elif "Rat 1" in parameterSetName: + options["Structure"] = ( + "1-2, 2-3.1,3.2-4,4-5,5-6-7-8" + ) + + options["Define inner coordinates"] = True + options["Top major calyx"] = False + options["Middle major calyx"] = True + options["Bottom major calyx"] = False + options["Upper minor calyx"] = False + options["Lower minor calyx"] = False + options["Rotate upper, middle and lower minor calyxes"] = True + options["Number of calyxes at top minor calyx"] = 1 + options["Number of calyxes at middle major calyx"] = 1 + options["Number of calyxes at bottom minor calyx"] = 1 + options["Number of calyxes at upper minor calyx"] = 1 + options["Number of calyxes at lower minor calyx"] = 1 + options["Ureter length"] = 3.0 + options["Ureter radius"] = 0.1 + options["Ureter bend angle degrees"] = 45 + options["Major calyx length"] = 0.6 + options["Major calyx radius"] = 0.1 + options["Major calyx angle degrees"] = 170 + options["Middle major calyx length"] = 0.4 + options["Major to bottom/top minor calyx length"] = 0.3 + options["Major to lower/upper minor calyx length"] = 0.3 + options["Bottom/top minor calyx length"] = 0.2 + options["Lower/upper minor calyx length"] = 0.2 + options["Minor calyx radius"] = 0.1 + options["Bottom/top minor calyx bifurcation angle degrees"] = 90 + options["Bottom/top minor calyx rotate angle degrees"] = 0 + options["Lower/upper minor calyx bifurcation angle degrees"] = 90 + options["Lower/upper minor calyx bend angle degrees"] = 10 + options["Renal pyramid length"] = 0.5 + options["Renal pyramid width"] = 0.5 + options["Inner proportion default"] = 0.8 + options["Inner proportion ureter"] = 0.7 + return options @classmethod From d78022916278bddcc5ddd46548ebe519122b1aae Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 22 Sep 2025 10:06:16 +1200 Subject: [PATCH 10/18] Fix 1D layout for renal pyramids and minor calyxes --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 55 ++++++++++++------- src/scaffoldmaker/scaffolds.py | 3 + tests/test_renalpelvis.py | 32 +++++------ 3 files changed, 55 insertions(+), 35 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index e1d9615c..913b0515 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -426,7 +426,7 @@ def generateBaseMesh(cls, region, options): renalPyramidMeshGroup = renalPyramidGroup.getMeshGroup(mesh) minorCalyxMeshGroup = minorCalyxGroup.getMeshGroup(mesh) - meshGroups = [renalPelvisMeshGroup, minorCalyxMeshGroup, renalPyramidMeshGroup] + meshGroups = [renalPelvisMeshGroup, minorCalyxMeshGroup] minorCalyxElementsCount = 1 pyramidElementsCount = 3 for calyx in minorCalyxList: @@ -644,7 +644,7 @@ def generateBaseMesh(cls, region, options): nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) x = [nx, sx[1], nz] if isRotateMinorCalyx: - rotateAxis = normalize(minorCalyxD1List[calyx]) + rotateAxis = [1,0,0] tx = rotate_vector_around_vector((sub(x, sx)), rotateAxis, math.radians(90)) x = add(tx, sx) else: @@ -652,12 +652,12 @@ def generateBaseMesh(cls, region, options): nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) x = [nx, sx[1], nz] if isRotateMinorCalyx: - rotateAxis = normalize(minorCalyxD1List[calyx]) + rotateAxis = [1, 0, 0] tx = rotate_vector_around_vector((sub(x, sx)), rotateAxis, math.radians(90)) x = add(tx, sx) if calyx != middleMajor: - theta = math.radians(-minorCalyxRotateAngle) if calyx == bottomMajor else math.radians(minorCalyxRotateAngle) + theta = math.radians(-minorCalyxRotateAngle) if calyx in [lowerMinor, bottomMinor] else math.radians(minorCalyxRotateAngle) rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) x = [rx, ry, x[2]] @@ -719,20 +719,16 @@ def generateBaseMesh(cls, region, options): else: index = 0 if calyx <= lowerMinor else (1 if calyx == middleMajor else 2) if [isBottomMC, isMidMC, isTopMC][index]: - if nMinorCalyxesList[calyx] > 1: - minorCalyxAngle = bottomMinorCalyxAngle if calyx in (bottomMinor, topMinor) else lowerMinorCalyxAngle - else: - minorCalyxAngle = 0 - minorCalyxHalfAngle = 0.5 * minorCalyxAngle - minorCalyxHalfAngleRadians = math.radians(minorCalyxHalfAngle) - sinMinorCalyxAngle = math.sin(minorCalyxHalfAngleRadians) - cosMinorCalyxAngle = math.cos(minorCalyxHalfAngleRadians) for side in range(nMinorCalyxesList[calyx]): sx = renalPyramidStartX[calyx][side] if calyx in [bottomMinor, topMinor]: - nx = -sinMinorCalyxAngle if side == anterior else sinMinorCalyxAngle - ny = -cosMinorCalyxAngle if calyx == bottomMinor else cosMinorCalyxAngle - pyramidDirn = [nx, ny, 0.0] + tx = [0.0, -1.0, 0.0] if calyx == bottomMinor else [0.0, 1.0, 0.0] + rotateAngle = -25 if side == anterior else 25 + rotateAngle = -rotateAngle + minorCalyxRotateAngle if calyx == topMinor else rotateAngle - minorCalyxRotateAngle + theta = math.radians(rotateAngle) + rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) if side == anterior else tx[0] * math.cos(theta) - tx[1] * math.sin(theta) + ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) if side == anterior else tx[0] * math.sin(theta) + tx[1] * math.cos(theta) + pyramidDirn = [rx, ry, 0.0] else: tx = [1.0, 0.0, 0.0] theta = math.radians(-minorCalyxBendAngle) if calyx == lowerMinor else (0 if calyx == middleMajor else math.radians(minorCalyxBendAngle)) @@ -740,8 +736,12 @@ def generateBaseMesh(cls, region, options): ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) pyramidDirn = [rx, ry, 0.0] if isRotateMinorCalyx and nMinorCalyxesList[calyx] > 1: - rotateAngle = math.radians(-20 if side == anterior else 20) - pyramidDirn = rotate_about_z_axis(pyramidDirn, rotateAngle) + rotateAngle = -25 if side == anterior else 25 + if calyx in [lowerMinor, upperMinor]: + rotateAngle = rotateAngle + (minorCalyxRotateAngle if calyx == upperMinor else -minorCalyxRotateAngle) + rotateAngleRad = math.radians(rotateAngle) + pyramidDirn = rotate_about_z_axis(pyramidDirn, rotateAngleRad) + xList = [] for e in range(pyramidElementsCount): pyramidLengthScale = 0.3 * pyramidLength if e == 0 else (0.7 * pyramidLength if e == 1 else pyramidLength) @@ -906,13 +906,30 @@ def generateBaseMesh(cls, region, options): layoutAnnotationGroups = networkLayout.getAnnotationGroups() networkMesh = networkLayout.getConstructionObject() + elementsCountAlongPyramid = 4 + + annotationAlongCounts = [] + defaultCoreBoundaryScalingMode = 1 + annotationCoreBoundaryScalingMode = [] + for layoutAnnotationGroup in layoutAnnotationGroups: + alongCount = 0 + coreBoundaryScalingMode = 0 + name = layoutAnnotationGroup.getName() + if "renal pyramid" in name: + alongCount = elementsCountAlongPyramid + annotationAlongCounts.append(alongCount) + annotationCoreBoundaryScalingMode.append(coreBoundaryScalingMode) + tubeNetworkMeshBuilder = RenalPelvisTubeNetworkMeshBuilder( networkMesh, targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], + layoutAnnotationGroups=layoutAnnotationGroups, + annotationElementsCountsAlong=annotationAlongCounts, defaultElementsCountAround=options["Elements count around"], elementsCountThroughShell=options["Elements count through shell"], - layoutAnnotationGroups=layoutAnnotationGroups, - annotationElementsCountsAround=options["Annotation elements counts around"]) + annotationElementsCountsAround=options["Annotation elements counts around"], + defaultCoreBoundaryScalingMode=defaultCoreBoundaryScalingMode, + annotationCoreBoundaryScalingMode=annotationCoreBoundaryScalingMode) tubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index aa991fa3..38ce6c7b 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -44,6 +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_pelvis1 import MeshType_3d_renal_pelvis1, \ + MeshType_1d_renal_pelvis_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 @@ -134,6 +136,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_pelvis_network_layout1, MeshType_1d_uterus_network_layout1 ] diff --git a/tests/test_renalpelvis.py b/tests/test_renalpelvis.py index d7b55b79..e99c398e 100644 --- a/tests/test_renalpelvis.py +++ b/tests/test_renalpelvis.py @@ -26,7 +26,7 @@ def test_renalpelvis(self): """ scaffold = MeshType_3d_renal_pelvis1 parameterSetNames = scaffold.getParameterSetNames() - self.assertEqual(parameterSetNames, ["Default", "Human 1"]) + self.assertEqual(parameterSetNames, ["Default", "Human 1", "Rat 1"]) options = scaffold.getDefaultOptions("Human 1") self.assertEqual(9, len(options)) @@ -47,13 +47,13 @@ def test_renalpelvis(self): fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(672, mesh3d.getSize()) + self.assertEqual(952, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(2532, mesh2d.getSize()) + self.assertEqual(3496, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(3063, mesh1d.getSize()) + self.assertEqual(4157, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(1204, nodes.getSize()) + self.assertEqual(1614, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -62,8 +62,8 @@ def test_renalpelvis(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [0.7789485582121838, -2.250268470689302, -0.1318007396451522], tol) - assertAlmostEqualList(self, maximums, [4.188222198878539, 2.250268470689302, 0.1318007396451522], tol) + assertAlmostEqualList(self, minimums, [0.7789485582121838, -2.285859056739037, -0.10878747768283183], tol) + assertAlmostEqualList(self, maximums, [4.189664358647292, 2.285859056739037, 0.10878747768283183], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -81,18 +81,18 @@ def test_renalpelvis(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.41239028202973105, delta=tol) - self.assertAlmostEqual(surfaceArea, 15.14363706045464, delta=tol) + self.assertAlmostEqual(volume, 0.37585791891429127, delta=tol) + self.assertAlmostEqual(surfaceArea, 15.62363951360235, delta=tol) # check some annotation groups: expectedSizes3d = { - "core": (240, 0.19386743947037463), + "core": (360, 0.16220839845333077), "major calyx": (64, 0.025523445233479505), "minor calyx": (160, 0.039595947108606436), "renal pelvis": (272, 0.10994753925440572), - "renal pyramid": (480, 0.3199459642092786), - "shell": (432, 0.21852600996918725), + "renal pyramid": (680, 0.2658662115503663), + "shell": (592, 0.21364857265602194), "ureter": (64, 0.049524462117630445) } for name in expectedSizes3d: @@ -109,10 +109,10 @@ def test_renalpelvis(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "major calyx": (272, 3.0306454318904343), - "minor calyx": (692, 4.858039077961195), - "renal pelvis": (1132, 11.555677753247785), - "renal pyramid": (1780, 18.695951082914355), + "major calyx": (272, 3.029777277349344), + "minor calyx": (696, 4.87169182999934), + "renal pelvis": (1136, 11.568504559613418), + "renal pyramid": (2440, 16.180214023192985), "ureter": (264, 4.2985089237162715) } for name in expectedSizes2d: From 45483c5dcdc9d6ea91b150cb3496c551a31b0dbb Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 22 Sep 2025 11:25:40 +1200 Subject: [PATCH 11/18] Update parameters for Human 1 --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 32 +++++++-------- tests/test_renalpelvis.py | 41 +++++++++---------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index 913b0515..fa514ae7 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -43,14 +43,12 @@ def getDefaultOptions(cls, parameterSetName="Default"): options = {} options["Base parameter set"] = "Human 1" if (parameterSetName == "Default") else parameterSetName + options["Define inner coordinates"] = True + options["Inner proportion default"] = 0.8 + options["Inner proportion ureter"] = 0.7 + if parameterSetName in ["Default", "Human 1"]: - options["Structure"] = ( - "1-2, 2-3.1,3.2-4,3.3-5,3.4-6,4.2-7,4.3-8," - "6.2-9,6.3-10,7.2-11,7.3-12,8.2-13,8.3-14,5.2-15,5.3-16,9.2-17,9.3-18,10.2-19,10.3-20," - "11-21-22-23,12-24-25-26,13-27-28-29,14-30-31-32,15-33-34-35,16-36-37-38,17-39-40-41,18-42-43-44,19-45-46-47,20-48-49-50" - ) - options["Define inner coordinates"] = True options["Top major calyx"] = True options["Middle major calyx"] = True options["Bottom major calyx"] = True @@ -62,26 +60,26 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of calyxes at bottom minor calyx"] = 2 options["Number of calyxes at upper minor calyx"] = 2 options["Number of calyxes at lower minor calyx"] = 2 - options["Ureter length"] = 3.0 + options["Ureter length"] = 4.0 options["Ureter radius"] = 0.1 options["Ureter bend angle degrees"] = 45 - options["Major calyx length"] = 1.0 + options["Major calyx length"] = 0.7 options["Major calyx radius"] = 0.1 - options["Major calyx angle degrees"] = 170 - options["Middle major calyx length"] = 0.5 - options["Major to bottom/top minor calyx length"] = 0.6 + options["Major calyx angle degrees"] = 200 + options["Middle major calyx length"] = 0.4 + options["Major to bottom/top minor calyx length"] = 0.4 options["Major to lower/upper minor calyx length"] = 0.3 options["Bottom/top minor calyx length"] = 0.2 options["Lower/upper minor calyx length"] = 0.2 options["Minor calyx radius"] = 0.1 options["Bottom/top minor calyx bifurcation angle degrees"] = 90 - options["Bottom/top minor calyx rotate angle degrees"] = 0 + options["Bottom/top minor calyx rotate angle degrees"] = 30 options["Lower/upper minor calyx bifurcation angle degrees"] = 90 options["Lower/upper minor calyx bend angle degrees"] = 10 - options["Renal pyramid length"] = 0.5 - options["Renal pyramid width"] = 0.5 - options["Inner proportion default"] = 0.8 - options["Inner proportion ureter"] = 0.7 + options["Renal pyramid length"] = 0.6 + options["Renal pyramid width"] = 0.6 + + options["Structure"] = cls.getPelvisLayoutStructure(options) elif "Rat 1" in parameterSetName: options["Structure"] = ( @@ -118,8 +116,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Lower/upper minor calyx bend angle degrees"] = 10 options["Renal pyramid length"] = 0.5 options["Renal pyramid width"] = 0.5 - options["Inner proportion default"] = 0.8 - options["Inner proportion ureter"] = 0.7 return options diff --git a/tests/test_renalpelvis.py b/tests/test_renalpelvis.py index e99c398e..52cb7a50 100644 --- a/tests/test_renalpelvis.py +++ b/tests/test_renalpelvis.py @@ -5,7 +5,6 @@ from cmlibs.utils.zinc.general import ChangeManager 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 @@ -47,13 +46,13 @@ def test_renalpelvis(self): fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(952, mesh3d.getSize()) + self.assertEqual(936, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(3496, mesh2d.getSize()) + self.assertEqual(3430, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(4157, mesh1d.getSize()) + self.assertEqual(4075, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(1614, nodes.getSize()) + self.assertEqual(1582, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -62,8 +61,8 @@ def test_renalpelvis(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [0.7789485582121838, -2.285859056739037, -0.10878747768283183], tol) - assertAlmostEqualList(self, maximums, [4.189664358647292, 2.285859056739037, 0.10878747768283183], tol) + assertAlmostEqualList(self, minimums, [1.0718417770256363, -2.8357557021117126, -0.10878747768283183], tol) + assertAlmostEqualList(self, maximums, [5.199312959129288, 1.8502096232770497, 0.10878747768283183], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -81,19 +80,19 @@ def test_renalpelvis(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.37585791891429127, delta=tol) - self.assertAlmostEqual(surfaceArea, 15.62363951360235, delta=tol) + self.assertAlmostEqual(volume, 0.4866338272404299, delta=tol) + self.assertAlmostEqual(surfaceArea, 17.309413531530122, delta=tol) # check some annotation groups: expectedSizes3d = { - "core": (360, 0.16220839845333077), - "major calyx": (64, 0.025523445233479505), - "minor calyx": (160, 0.039595947108606436), - "renal pelvis": (272, 0.10994753925440572), - "renal pyramid": (680, 0.2658662115503663), - "shell": (592, 0.21364857265602194), - "ureter": (64, 0.049524462117630445) + "core": (360, 0.22934693864957886), + "major calyx": (48, 0.01808641549910496), + "minor calyx": (160, 0.03421501056066799), + "renal pelvis": (256, 0.11402458784598982), + "renal pyramid": (680, 0.3726073231262026), + "shell": (576, 0.2572849723226171), + "ureter": (64, 0.06581299176056989) } for name in expectedSizes3d: term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) @@ -109,11 +108,11 @@ def test_renalpelvis(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "major calyx": (272, 3.029777277349344), - "minor calyx": (696, 4.87169182999934), - "renal pelvis": (1136, 11.568504559613418), - "renal pyramid": (2440, 16.180214023192985), - "ureter": (264, 4.2985089237162715) + "major calyx": (208, 2.157380980683151), + "minor calyx": (694, 4.263807908549397), + "renal pelvis": (1070, 11.526296790112461), + "renal pyramid": (2440, 21.048589257773124), + "ureter": (264, 5.65955310775128) } for name in expectedSizes2d: term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) From 35855a71dd571c1c51e2deaa5873f8c9c7703cec Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 22 Sep 2025 12:08:08 +1200 Subject: [PATCH 12/18] Fix issue with number of minor calyxes --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index fa514ae7..e0602811 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -344,10 +344,10 @@ def generateBaseMesh(cls, region, options): nTopMinorCalyxes = options["Number of calyxes at top minor calyx"] nUpperMinorCalyxes = options["Number of calyxes at upper minor calyx"] - nMIdMinorCalyxes = options["Number of calyxes at middle major calyx"] + nMidMinorCalyxes = options["Number of calyxes at middle major calyx"] nLowerMinorCalyxes = options["Number of calyxes at lower minor calyx"] nBottomMinorCalyxes = options["Number of calyxes at bottom minor calyx"] - nMinorCalyxesList = [nBottomMinorCalyxes, nLowerMinorCalyxes, nMIdMinorCalyxes, nTopMinorCalyxes, nUpperMinorCalyxes] + nMinorCalyxesList = [nBottomMinorCalyxes, nLowerMinorCalyxes, nMidMinorCalyxes, nTopMinorCalyxes, nUpperMinorCalyxes] ureterLength = options["Ureter length"] ureterRadius = options["Ureter radius"] @@ -559,11 +559,10 @@ def generateBaseMesh(cls, region, options): # bend lower and upper minor calyxes based on specified bend angle ec = nMinorCalyxesList[1] if side == lowerMinor else nMinorCalyxesList[4] minorCalyxBendAngle = 0 if ec < 2 else minorCalyxBendAngle - if minorCalyxBendAngle > 0: - theta = math.radians(-minorCalyxBendAngle) if calyx == bottomMajor else math.radians(minorCalyxBendAngle) - rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) - ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) - x = [rx, ry, 0.0] + theta = math.radians(-minorCalyxBendAngle) if calyx == bottomMajor else math.radians(minorCalyxBendAngle) + rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) + ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) + x = [rx, ry, 0.0] sd1 = sub(x, sx) minorCalyxNodeIdentifiers.append(nodeIdentifier) minorCalyxXList.append(x) @@ -719,7 +718,10 @@ def generateBaseMesh(cls, region, options): sx = renalPyramidStartX[calyx][side] if calyx in [bottomMinor, topMinor]: tx = [0.0, -1.0, 0.0] if calyx == bottomMinor else [0.0, 1.0, 0.0] - rotateAngle = -25 if side == anterior else 25 + if nMinorCalyxesList[calyx] > 1: + rotateAngle = -25 if side == anterior else 25 + else: + rotateAngle = 0 rotateAngle = -rotateAngle + minorCalyxRotateAngle if calyx == topMinor else rotateAngle - minorCalyxRotateAngle theta = math.radians(rotateAngle) rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) if side == anterior else tx[0] * math.cos(theta) - tx[1] * math.sin(theta) @@ -727,12 +729,18 @@ def generateBaseMesh(cls, region, options): pyramidDirn = [rx, ry, 0.0] else: tx = [1.0, 0.0, 0.0] - theta = math.radians(-minorCalyxBendAngle) if calyx == lowerMinor else (0 if calyx == middleMajor else math.radians(minorCalyxBendAngle)) + if nMinorCalyxesList[calyx] > 1: + theta = math.radians(-minorCalyxBendAngle) if calyx == lowerMinor else (0 if calyx == middleMajor else math.radians(minorCalyxBendAngle)) + else: + theta = 0 rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) pyramidDirn = [rx, ry, 0.0] - if isRotateMinorCalyx and nMinorCalyxesList[calyx] > 1: - rotateAngle = -25 if side == anterior else 25 + if isRotateMinorCalyx: + if nMinorCalyxesList[calyx] > 1: + rotateAngle = -25 if side == anterior else 25 + else: + rotateAngle = 0 if calyx in [lowerMinor, upperMinor]: rotateAngle = rotateAngle + (minorCalyxRotateAngle if calyx == upperMinor else -minorCalyxRotateAngle) rotateAngleRad = math.radians(rotateAngle) From 0e1cb01f3c57c07f018053c1e3ef40393cfcf727 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 22 Sep 2025 14:41:52 +1200 Subject: [PATCH 13/18] Update parameters and add unit test for rat scaffold --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 87 +++++++------ tests/test_renalpelvis.py | 115 +++++++++++++++++- 2 files changed, 164 insertions(+), 38 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index e0602811..7a4405f7 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -25,6 +25,8 @@ class MeshType_1d_renal_pelvis_network_layout1(MeshType_1d_network_layout1): Defines renal pelvis network layout. """ + isHuman = True + isRat = False @classmethod def getName(cls): @@ -85,6 +87,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Structure"] = ( "1-2, 2-3.1,3.2-4,4-5,5-6-7-8" ) + cls.isHuman = False + cls.isRat = True options["Define inner coordinates"] = True options["Top major calyx"] = False @@ -114,46 +118,59 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Bottom/top minor calyx rotate angle degrees"] = 0 options["Lower/upper minor calyx bifurcation angle degrees"] = 90 options["Lower/upper minor calyx bend angle degrees"] = 10 - options["Renal pyramid length"] = 0.5 - options["Renal pyramid width"] = 0.5 + options["Renal pyramid length"] = 0.6 + options["Renal pyramid width"] = 0.6 return options @classmethod def getOrderedOptionNames(cls): - return [ - "Top major calyx", - "Middle major calyx", - "Bottom major calyx", - "Upper minor calyx", - "Lower minor calyx", - "Rotate upper, middle and lower minor calyxes", - "Number of calyxes at top minor calyx", - "Number of calyxes at upper minor calyx", - "Number of calyxes at middle major calyx", - "Number of calyxes at lower minor calyx", - "Number of calyxes at bottom minor calyx", - "Ureter length", - "Ureter radius", - "Ureter bend angle degrees", - "Major calyx length", - "Major calyx radius", - "Major calyx angle degrees", - "Middle major calyx length", - "Major to bottom/top minor calyx length", - "Major to lower/upper minor calyx length", - "Bottom/top minor calyx length", - "Lower/upper minor calyx length", - "Minor calyx radius", - "Bottom/top minor calyx bifurcation angle degrees", - "Bottom/top minor calyx rotate angle degrees", - "Lower/upper minor calyx bifurcation angle degrees", - "Lower/upper minor calyx bend angle degrees", - "Renal pyramid length", - "Renal pyramid width", - "Inner proportion default", - "Inner proportion ureter" - ] + if cls.isHuman: + return [ + "Top major calyx", + "Middle major calyx", + "Bottom major calyx", + "Upper minor calyx", + "Lower minor calyx", + "Rotate upper, middle and lower minor calyxes", + "Number of calyxes at top minor calyx", + "Number of calyxes at upper minor calyx", + "Number of calyxes at middle major calyx", + "Number of calyxes at lower minor calyx", + "Number of calyxes at bottom minor calyx", + "Ureter length", + "Ureter radius", + "Ureter bend angle degrees", + "Major calyx length", + "Major calyx radius", + "Major calyx angle degrees", + "Middle major calyx length", + "Major to bottom/top minor calyx length", + "Major to lower/upper minor calyx length", + "Bottom/top minor calyx length", + "Lower/upper minor calyx length", + "Minor calyx radius", + "Bottom/top minor calyx bifurcation angle degrees", + "Bottom/top minor calyx rotate angle degrees", + "Lower/upper minor calyx bifurcation angle degrees", + "Lower/upper minor calyx bend angle degrees", + "Renal pyramid length", + "Renal pyramid width", + "Inner proportion default", + "Inner proportion ureter" + ] + elif cls.isRat: + return [ + "Ureter length", + "Ureter radius", + "Ureter bend angle degrees", + "Middle major calyx length", + "Minor calyx radius", + "Renal pyramid length", + "Renal pyramid width", + "Inner proportion default", + "Inner proportion ureter" + ] @classmethod def checkOptions(cls, options): diff --git a/tests/test_renalpelvis.py b/tests/test_renalpelvis.py index 52cb7a50..a3683e27 100644 --- a/tests/test_renalpelvis.py +++ b/tests/test_renalpelvis.py @@ -17,11 +17,11 @@ from testutils import assertAlmostEqualList -class RenalPelviscaffoldTestCase(unittest.TestCase): +class RenalPelviScaffoldTestCase(unittest.TestCase): - def test_renalpelvis(self): + def test_renal_pelvis_human(self): """ - Test creation of renal pelvis scaffold. + Test creation of human renal pelvis scaffold. """ scaffold = MeshType_3d_renal_pelvis1 parameterSetNames = scaffold.getParameterSetNames() @@ -127,5 +127,114 @@ def test_renalpelvis(self): self.assertEqual(result, RESULT_OK) self.assertAlmostEqual(surfaceArea, expectedSizes2d[name][1], delta=tol) + + def test_renal_pelvis_rat(self): + """ + Test creation of rat renal pelvis scaffold. + """ + scaffold = MeshType_3d_renal_pelvis1 + parameterSetNames = scaffold.getParameterSetNames() + self.assertEqual(parameterSetNames, ["Default", "Human 1", "Rat 1"]) + options = scaffold.getDefaultOptions("Rat 1") + + self.assertEqual(9, 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(4.0, options["Target element density along longest segment"]) + self.assertEqual(False, options["Use linear through shell"]) + self.assertEqual(True, options["Use outer trim surfaces"]) + self.assertEqual(False, options["Show trim surfaces"]) + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + annotationGroups = scaffold.generateMesh(region, options)[0] + self.assertEqual(7, len(annotationGroups)) + + fieldmodule = region.getFieldmodule() + self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual(148, mesh3d.getSize()) + mesh2d = fieldmodule.findMeshByDimension(2) + self.assertEqual(564, mesh2d.getSize()) + mesh1d = fieldmodule.findMeshByDimension(1) + self.assertEqual(691, mesh1d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(276, 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, [0.7789485582121838, -2.128648920925165, -0.10878747768283183], tol) + assertAlmostEqualList(self, maximums, [4.2, 0.29820338638253907, 0.10878747768283183], 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, 0.09588370625349187, delta=tol) + self.assertAlmostEqual(surfaceArea, 4.930576718031101, delta=tol) + + # check some annotation groups: + + expectedSizes3d = { + "core": (36, 0.022899342100307266), + "major calyx": (8, 0.005543006096630809), + "minor calyx": (16, 0.007802597971432378), + "renal pelvis": (80, 0.05861664077426631), + "renal pyramid": (68, 0.037266476269253765), + "shell": (112, 0.07298377494321281), + "ureter": (64, 0.050814042802833914) + } + for name in expectedSizes3d: + term = get_ureter_term(name) if name == "ureter" else 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 = { + "major calyx": (40, 0.5472137937926038), + "minor calyx": (72, 0.816589508937787), + "renal pelvis": (328, 5.171627911264767), + "renal pyramid": (244, 2.1052097538093686), + "ureter": (264, 4.371043844773501) + } + for name in expectedSizes2d: + term = get_ureter_term(name) if name == "ureter" else 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) + if __name__ == "__main__": unittest.main() From 21d2d5c3adbdee80cff68a50860c9682e1449b6d Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Tue, 23 Sep 2025 10:25:06 +1200 Subject: [PATCH 14/18] Fix minor issues and clean up code --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 399 ++++++++++-------- tests/test_renalpelvis.py | 22 +- 2 files changed, 236 insertions(+), 185 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index 7a4405f7..05dffdd8 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -4,7 +4,7 @@ import math from cmlibs.maths.vectorops import mult, cross, add, sub, set_magnitude, rotate_about_z_axis, \ - rotate_vector_around_vector, normalize + rotate_vector_around_vector from cmlibs.utils.zinc.field import find_or_create_field_coordinates from cmlibs.zinc.field import Field @@ -50,6 +50,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Inner proportion ureter"] = 0.7 if parameterSetName in ["Default", "Human 1"]: + cls.isHuman = True + cls.isRat = False options["Top major calyx"] = True options["Middle major calyx"] = True @@ -245,98 +247,99 @@ def getPelvisLayoutStructure(cls, options): isUpperMC = options["Upper minor calyx"] isLowerMC = options["Lower minor calyx"] - nTopMajorCalyxes = 2 if isUpperMC else 1 nTopMinorCalyxes = options["Number of calyxes at top minor calyx"] nMidMajorCalyxes = options["Number of calyxes at middle major calyx"] - nBottomMajorCalyxes = 2 if isLowerMC else 1 nBottomMinorCalyxes = options["Number of calyxes at bottom minor calyx"] nUpperMinorCalyxes = options["Number of calyxes at upper minor calyx"] nLowerMinorCalyxes = options["Number of calyxes at lower minor calyx"] + nTopMajorCalyxes = 2 if isUpperMC else 1 + nBottomMajorCalyxes = 2 if isLowerMC else 1 + structure = "1-2, 2-3.1" - startNodeIdentifier = 3 - nodeDerivative = 2 nodeIdentifier = 4 + nodeDerivative = 2 - # major calyx + # Build major calyx connections majorCalyxNodeIdentifiers = [] - for i in range(3): - if [isBottomMC, isMidMC, isTopMC][i]: + majorCalyxFlags = [isBottomMC, isMidMC, isTopMC] + + for i, hasMajorCalyx in enumerate(majorCalyxFlags): + if hasMajorCalyx: majorCalyxNodeIdentifiers.append(nodeIdentifier) - layout = str(startNodeIdentifier) + "." + str(nodeDerivative) + "-" + str(nodeIdentifier) + layout = f"3.{nodeDerivative}-{nodeIdentifier}" + structure += f",{layout}" nodeDerivative += 1 nodeIdentifier += 1 - structure = structure + "," + layout - else: - continue + # Build bottom major calyx to minor calyx connections bmcNodeIdentifiers = [] if isBottomMC: - startNodeIdentifier = majorCalyxNodeIdentifiers[0] + startNode = majorCalyxNodeIdentifiers[0] for n in range(nBottomMajorCalyxes): bmcNodeIdentifiers.append(nodeIdentifier) - if isLowerMC: - layout = str(startNodeIdentifier) + "." + str(n + 2) + "-" + str(nodeIdentifier) - else: - layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + layout = f"{startNode}.{n + 2}-{nodeIdentifier}" if isLowerMC else f"{startNode}-{nodeIdentifier}" + structure += f",{layout}" nodeIdentifier += 1 - structure = structure + "," + layout + # Build top major calyx to minor calyx connections tmcNodeIdentifiers = [] if isTopMC: - startNodeIdentifier = majorCalyxNodeIdentifiers[-1] + startNode = majorCalyxNodeIdentifiers[-1] for n in range(nTopMajorCalyxes): tmcNodeIdentifiers.append(nodeIdentifier) - if isUpperMC: - layout = str(startNodeIdentifier) + "." + str(n + 2) + "-" + str(nodeIdentifier) - else: - layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + layout = f"{startNode}.{n + 2}-{nodeIdentifier}" if isUpperMC else f"{startNode}-{nodeIdentifier}" + structure += f",{layout}" nodeIdentifier += 1 - structure = structure + "," + layout - minorCalyxNodeIndentifiers = [] + # Build minor calyx connections + minorCalyxNodeIdentifiers = [] + # Bottom minor calyxes if isBottomMC: + calyxCounts = [nBottomMinorCalyxes, nLowerMinorCalyxes] for i in range(nBottomMajorCalyxes): - nCalyxes = [nBottomMinorCalyxes, nLowerMinorCalyxes][i] - startNodeIdentifier = bmcNodeIdentifiers[i] + nCalyxes = calyxCounts[i] + startNode = bmcNodeIdentifiers[i] for j in range(nCalyxes): - minorCalyxNodeIndentifiers.append(nodeIdentifier) - if nCalyxes > 1: - layout = str(startNodeIdentifier) + "." + str(j + 2) + "-" + str(nodeIdentifier) - else: - layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + minorCalyxNodeIdentifiers.append(nodeIdentifier) + layout = f"{startNode}.{j + 2}-{nodeIdentifier}" if nCalyxes > 1 else f"{startNode}-{nodeIdentifier}" + structure += f",{layout}" nodeIdentifier += 1 - structure = structure + "," + layout + # Middle minor calyxes if isMidMC: - index = 0 if len(majorCalyxNodeIdentifiers) == 2 and isTopMC or len(majorCalyxNodeIdentifiers) == 1 else 1 - startNodeIdentifier = majorCalyxNodeIdentifiers[index] + # Determine correct index for middle major calyx + if len(majorCalyxNodeIdentifiers) == 3: # All three major calyxes present + midIndex = 1 + elif len(majorCalyxNodeIdentifiers) == 2 and isTopMC: # Mid and Top present + midIndex = 0 + else: # Mid and Bottom present, or only Mid present + midIndex = -1 if len(majorCalyxNodeIdentifiers) == 2 else 0 + + startNode = majorCalyxNodeIdentifiers[midIndex] for n in range(nMidMajorCalyxes): - minorCalyxNodeIndentifiers.append(nodeIdentifier) - if nMidMajorCalyxes > 1: - layout = str(startNodeIdentifier) + "." + str(n + 2) + "-" + str(nodeIdentifier) - else: - layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + minorCalyxNodeIdentifiers.append(nodeIdentifier) + layout = f"{startNode}.{n + 2}-{nodeIdentifier}" if nMidMajorCalyxes > 1 else f"{startNode}-{nodeIdentifier}" + structure += f",{layout}" nodeIdentifier += 1 - structure = structure + "," + layout + # Top minor calyxes if tmcNodeIdentifiers: + calyxCounts = [nTopMinorCalyxes, nUpperMinorCalyxes] for i in range(nTopMajorCalyxes): - nCalyxes = [nTopMinorCalyxes, nUpperMinorCalyxes][i] - startNodeIdentifier = tmcNodeIdentifiers[i] + nCalyxes = calyxCounts[i] + startNode = tmcNodeIdentifiers[i] for j in range(nCalyxes): - minorCalyxNodeIndentifiers.append(nodeIdentifier) - if nCalyxes > 1: - layout = str(startNodeIdentifier) + "." + str(j + 2) + "-" + str(nodeIdentifier) - else: - layout = str(startNodeIdentifier) + "-" + str(nodeIdentifier) + minorCalyxNodeIdentifiers.append(nodeIdentifier) + layout = f"{startNode}.{j + 2}-{nodeIdentifier}" if nCalyxes > 1 else f"{startNode}-{nodeIdentifier}" + structure += f",{layout}" nodeIdentifier += 1 - structure = structure + "," + layout - for nid in minorCalyxNodeIndentifiers: - layout = str(nid) + "-" + str(nodeIdentifier) + "-" + str(nodeIdentifier + 1) + "-" + str(nodeIdentifier + 2) - structure = structure + "," + layout + # Add final connections for each minor calyx + for nid in minorCalyxNodeIdentifiers: + layout = f"{nid}-{nodeIdentifier}-{nodeIdentifier + 1}-{nodeIdentifier + 2}" + structure += f",{layout}" nodeIdentifier += 3 return structure @@ -401,6 +404,7 @@ def generateBaseMesh(cls, region, options): renalPyramidGroup = AnnotationGroup(region, get_kidney_term("renal pyramid")) annotationGroups = [renalPelvisGroup, ureterGroup, majorCalyxGroup, minorCalyxGroup, renalPyramidGroup] + # Ureter elements renalPelvisMeshGroup = renalPelvisGroup.getMeshGroup(mesh) elementIdentifier = 1 ureterElementsCount = 2 @@ -411,37 +415,45 @@ def generateBaseMesh(cls, region, options): meshGroup.addElement(element) elementIdentifier += 1 + # Major calyx elements majorCalyxElementsCount = sum([isBottomMC, isMidMC, isTopMC]) meshGroups = [renalPelvisMeshGroup, majorCalyxGroup.getMeshGroup(mesh)] - startIndex = 1 if sum([isBottomMC, isMidMC, isTopMC]) == 2 and isTopMC or sum([isBottomMC, isMidMC, isTopMC]) == 1 else 0 - bottomMajor, middleMajor, topMajor = 0, 1, 2 + majorCalyxFlags = [isBottomMC, isMidMC, isTopMC] + + totalMajorCalyxes = sum(majorCalyxFlags) + startIndex = 1 if (totalMajorCalyxes == 2 and isTopMC) or (totalMajorCalyxes == 1) else 0 + + middleMajorCalyxIdentifier = None for e in range(startIndex, majorCalyxElementsCount + startIndex): element = mesh.findElementByIdentifier(elementIdentifier) - if isMidMC: - if e == middleMajor: - middleMajorCalyxIdentifier = elementIdentifier - else: - middleMajorCalyxIdentifier = None + if isMidMC and e == 1: # middle major calyx index + middleMajorCalyxIdentifier = elementIdentifier for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 + # Minor calyx elements meshGroups = [renalPelvisMeshGroup, minorCalyxGroup.getMeshGroup(mesh)] + bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 - minorCalyxList = (([bottomMinor, lowerMinor] if isBottomMC else []) + ([middleMajor] if isMidMC else []) + + minorCalyxList = (([bottomMinor, lowerMinor] if isBottomMC else []) + + ([middleMajor] if isMidMC else []) + ([topMinor, upperMinor] if isTopMC else [])) + for calyx in minorCalyxList: - cElementIdentifier = middleMajorCalyxIdentifier if calyx == middleMajor and isMidMC else elementIdentifier + cElementIdentifier = middleMajorCalyxIdentifier if (calyx == middleMajor and isMidMC) else elementIdentifier element = mesh.findElementByIdentifier(cElementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) - elementIdentifier = elementIdentifier if calyx == middleMajor and isMidMC else elementIdentifier + 1 + if not (calyx == middleMajor and isMidMC): + elementIdentifier += 1 + # Minor calyx to pyramid connections renalPyramidMeshGroup = renalPyramidGroup.getMeshGroup(mesh) minorCalyxMeshGroup = minorCalyxGroup.getMeshGroup(mesh) meshGroups = [renalPelvisMeshGroup, minorCalyxMeshGroup] minorCalyxElementsCount = 1 - pyramidElementsCount = 3 + for calyx in minorCalyxList: for side in range(nMinorCalyxesList[calyx]): for e in range(minorCalyxElementsCount): @@ -450,6 +462,8 @@ def generateBaseMesh(cls, region, options): meshGroup.addElement(element) elementIdentifier += 1 + # Pyramid elements + pyramidElementsCount = 3 for calyx in minorCalyxList: for side in range(nMinorCalyxesList[calyx]): for e in range(pyramidElementsCount): @@ -460,67 +474,77 @@ def generateBaseMesh(cls, region, options): # 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) - # ureter + # Generate ureter coordinates nodeIdentifier = 1 ureterScale = ureterLength / ureterElementsCount ureterBendAngleRadians = math.radians(ureterBendAngle) sinUreterAngle = math.sin(ureterBendAngleRadians) cosUreterAngle = math.cos(ureterBendAngleRadians) + endX = [ureterLength, 0.0, 0.0] tx = endX[0] - ureterLength * cosUreterAngle ty = endX[1] - ureterLength * sinUreterAngle startX = [tx, ty, 0.0] + d1 = [ureterScale, 0.0, 0.0] d3 = [0.0, 0.0, ureterRadius] id3 = mult(d3, innerProportionUreter) - # td1 = [0.0, ureterScale, 0.0] td1 = rotate_about_z_axis(d1, 2 * ureterBendAngleRadians) + sx, sd1 = sampleCubicHermiteCurves([startX, endX], [td1, d1], ureterElementsCount, arcLengthDerivatives=True)[0:2] sd1 = smoothCubicHermiteDerivativesLine(sx, sd1, fixEndDirection=True) + for e in range(ureterElementsCount + 1): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) sd2 = set_magnitude(cross(d3, sd1[e]), ureterRadius) sid2 = mult(sd2, innerProportionUreter) + for field, derivatives in ((coordinates, (sd1[e], sd2, d3)), (innerCoordinates, (sd1[e], sid2, id3))): setNodeFieldParameters(field, fieldcache, sx[e], *derivatives) nodeIdentifier += 1 + majorCalyxJunctionNodeIdentifier = nodeIdentifier - 1 majorCalyxStartX = endX - # major calyx + # Generate major calyx coordinates majorCalyxAngleRadians = math.radians(majorCalyxAngle / 2) sx = majorCalyxStartX bottomMajor, middleMajor, topMajor = 0, 1, 2 version = 2 majorCalyxNodeIdentifiers, majorCalyxXList, majorCalyxD1List = [], [], [] + for calyx in (bottomMajor, middleMajor, topMajor): - if [isBottomMC, isMidMC, isTopMC][calyx]: + if majorCalyxFlags[calyx]: majorCalyxNodeIdentifiers.append(nodeIdentifier) calyxLength = middleMajorLength if calyx == middleMajor else majorCalyxLength majorCalyxAngle = 0 if calyx == middleMajor else majorCalyxAngleRadians cosMajorCalyxAngle = math.cos(majorCalyxAngle) sinMajorCalyxAngle = math.sin(-majorCalyxAngle if calyx == bottomMajor else majorCalyxAngle) + majorCalyxEndX = sx[0] + calyxLength * cosMajorCalyxAngle majorCalyxEndY = sx[1] + calyxLength * sinMajorCalyxAngle x = [majorCalyxEndX, majorCalyxEndY, 0.0] sd1 = sub(x, sx) + majorCalyxXList.append(x) majorCalyxD1List.append(sd1) + for i in range(2): isJunctionNode = i == 0 nodeId = majorCalyxJunctionNodeIdentifier if isJunctionNode else nodeIdentifier node = nodes.findNodeByIdentifier(nodeId) fieldcache.setNode(node) + sd3 = [0.0, 0.0, majorCalyxRadius] sid3 = mult(sd3, innerProportionDefault) sd2 = set_magnitude(cross(sd3, sd1), majorCalyxRadius) sid2 = mult(sd2, innerProportionDefault) + if not isJunctionNode: for field, derivatives in ( (coordinates, [x, sd1, sd2, sd3]), @@ -533,57 +557,71 @@ def generateBaseMesh(cls, region, options): ): field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) nodeIdentifier += 1 + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) - setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sd3) version += 1 else: majorCalyxNodeIdentifiers.append(None) majorCalyxXList.append(None) majorCalyxD1List.append(None) - # top and bottom major calyx to minor calyx + # Generate top and bottom major calyx to minor calyx connections lowerMinor, bottomMinor = 1, 0 topMinor, upperMinor = 0, 1 minorCalyxNodeIdentifiers, minorCalyxXList, minorCalyxD1List = [], [], [] + for calyx in [bottomMajor, middleMajor, topMajor]: if calyx == middleMajor: minorCalyxNodeIdentifiers.append(majorCalyxNodeIdentifiers[1]) minorCalyxXList.append(majorCalyxXList[1]) minorCalyxD1List.append(majorCalyxD1List[1]) continue - else: - sides = [bottomMinor, lowerMinor] if calyx == bottomMajor else [topMinor, upperMinor] - if [isBottomMC, isMidMC, isTopMC][calyx]: + + sides = [bottomMinor, lowerMinor] if calyx == bottomMajor else [topMinor, upperMinor] + + if majorCalyxFlags[calyx]: cNodeIdentifier = majorCalyxNodeIdentifiers[calyx] signValue = -1.0 if calyx == bottomMajor else 1.0 sx = majorCalyxXList[calyx] + for side in sides: + # Skip if minor calyx is not present if side in [lowerMinor, upperMinor]: if (calyx == bottomMajor and not isLowerMC) or (calyx == topMajor and not isUpperMC): minorCalyxNodeIdentifiers.append(None) minorCalyxXList.append(None) minorCalyxD1List.append(None) continue - calyxLength = majorToBottomMinorCalyxLength if side in (bottomMinor, topMinor) else majorToLowerMinorCalyxLength - x = [sx[0], sx[1] + calyxLength * signValue, sx[2]] if side in (bottomMinor, topMinor) else \ - [sx[0] + calyxLength, sx[1], sx[2]] - # rotate minor calyxes if rotate angle is not zero - theta = math.radians(-minorCalyxRotateAngle) if calyx == bottomMajor else math.radians(minorCalyxRotateAngle) + + calyxLength = majorToBottomMinorCalyxLength if side in ( + bottomMinor, topMinor) else majorToLowerMinorCalyxLength + + if side in (bottomMinor, topMinor): + x = [sx[0], sx[1] + calyxLength * signValue, sx[2]] + else: + x = [sx[0] + calyxLength, sx[1], sx[2]] + + # Apply rotation if needed + theta = math.radians(-minorCalyxRotateAngle if calyx == bottomMajor else minorCalyxRotateAngle) rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) x = [rx, ry, 0.0] + + # Apply bend angle for lower/upper minor calyxes if side in (lowerMinor, upperMinor): - # bend lower and upper minor calyxes based on specified bend angle ec = nMinorCalyxesList[1] if side == lowerMinor else nMinorCalyxesList[4] - minorCalyxBendAngle = 0 if ec < 2 else minorCalyxBendAngle - theta = math.radians(-minorCalyxBendAngle) if calyx == bottomMajor else math.radians(minorCalyxBendAngle) + bendAngle = 0 if ec < 2 else minorCalyxBendAngle + theta = math.radians(-bendAngle if calyx == bottomMajor else bendAngle) rx = sx[0] + (x[0] - sx[0]) * math.cos(theta) - (x[1] - sx[1]) * math.sin(theta) ry = sx[1] + (x[0] - sx[0]) * math.sin(theta) + (x[1] - sx[1]) * math.cos(theta) x = [rx, ry, 0.0] + sd1 = sub(x, sx) minorCalyxNodeIdentifiers.append(nodeIdentifier) minorCalyxXList.append(x) minorCalyxD1List.append(sd1) + for i in range(2): isJunctionNode = i == 0 nodeId = cNodeIdentifier if isJunctionNode else nodeIdentifier @@ -592,10 +630,14 @@ def generateBaseMesh(cls, region, options): version = 2 if side == 0 else 3 sd3 = [0.0, 0.0, minorCalyxRadius] - sd2 = [minorCalyxRadius, 0.0, 0.0] if (calyx == bottomMajor and version == 2) \ - else set_magnitude(cross(sd3, sd1), minorCalyxRadius) + if calyx == bottomMajor and version == 2: + sd2 = [minorCalyxRadius, 0.0, 0.0] + else: + sd2 = set_magnitude(cross(sd3, sd1), minorCalyxRadius) + sid2 = mult(sd2, innerProportionDefault) sid3 = mult(sd3, innerProportionDefault) + if not isJunctionNode: for field, derivatives in ( (coordinates, [x, sd1, sd2, sd3]), @@ -608,6 +650,7 @@ def generateBaseMesh(cls, region, options): ): field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) nodeIdentifier += 1 + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) else: @@ -616,11 +659,12 @@ def generateBaseMesh(cls, region, options): minorCalyxXList.append(None) minorCalyxD1List.append(None) - # minor calyx to renal pyramid connection + # Generate minor calyx to renal pyramid connections anterior, posterior = 0, 1 bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 renalPyramidStartX, renalPyramidNodeIdentifiers = [], [] connection_sd2_list, connection_sd3_list = [], [] + for calyx in (bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor): if minorCalyxNodeIdentifiers[calyx] is None: renalPyramidNodeIdentifiers.append(None) @@ -633,19 +677,23 @@ def generateBaseMesh(cls, region, options): renalPyramidStartX.append([]) connection_sd2_list.append([]) connection_sd3_list.append([]) + index = 0 if calyx <= lowerMinor else (1 if calyx == middleMajor else 2) if [isBottomMC, isMidMC, isTopMC][index]: cNodeIdentifier = minorCalyxNodeIdentifiers[calyx] sx = minorCalyxXList[calyx] minorCalyxLength = bottomMinorCalyxLength if calyx in (bottomMinor, topMinor) else lowerMinorCalyxLength + if nMinorCalyxesList[calyx] > 1: minorCalyxAngle = bottomMinorCalyxAngle if calyx in (bottomMinor, topMinor) else lowerMinorCalyxAngle else: minorCalyxAngle = 0 + minorCalyxHalfAngle = 0.5 * minorCalyxAngle minorCalyxHalfAngleRadians = math.radians(minorCalyxHalfAngle) sinMinorCalyxAngle = math.sin(minorCalyxHalfAngleRadians) cosMinorCalyxAngle = math.cos(minorCalyxHalfAngleRadians) + for side in range(nMinorCalyxesList[calyx]): if calyx in [bottomMinor, topMinor]: nx = sx[0] + minorCalyxLength * (-sinMinorCalyxAngle if side == anterior else sinMinorCalyxAngle) @@ -656,7 +704,7 @@ def generateBaseMesh(cls, region, options): nz = sx[2] + minorCalyxLength * (sinMinorCalyxAngle if side == anterior else -sinMinorCalyxAngle) x = [nx, sx[1], nz] if isRotateMinorCalyx: - rotateAxis = [1,0,0] + rotateAxis = [1, 0, 0] tx = rotate_vector_around_vector((sub(x, sx)), rotateAxis, math.radians(90)) x = add(tx, sx) else: @@ -721,102 +769,105 @@ def generateBaseMesh(cls, region, options): connection_sd2_list[-1].append(None) connection_sd3_list[-1].append(None) - # minor calyx to renal pyramids - pyramidElementsCount = 3 + # Generate minor calyx to renal pyramids pyramidHalfWidth = 0.5 * pyramidWidth - bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 + for calyx in [bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor]: if renalPyramidNodeIdentifiers[calyx] is None: continue - else: - index = 0 if calyx <= lowerMinor else (1 if calyx == middleMajor else 2) - if [isBottomMC, isMidMC, isTopMC][index]: - for side in range(nMinorCalyxesList[calyx]): - sx = renalPyramidStartX[calyx][side] - if calyx in [bottomMinor, topMinor]: - tx = [0.0, -1.0, 0.0] if calyx == bottomMinor else [0.0, 1.0, 0.0] - if nMinorCalyxesList[calyx] > 1: - rotateAngle = -25 if side == anterior else 25 - else: - rotateAngle = 0 - rotateAngle = -rotateAngle + minorCalyxRotateAngle if calyx == topMinor else rotateAngle - minorCalyxRotateAngle - theta = math.radians(rotateAngle) - rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) if side == anterior else tx[0] * math.cos(theta) - tx[1] * math.sin(theta) - ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) if side == anterior else tx[0] * math.sin(theta) + tx[1] * math.cos(theta) - pyramidDirn = [rx, ry, 0.0] - else: - tx = [1.0, 0.0, 0.0] - if nMinorCalyxesList[calyx] > 1: - theta = math.radians(-minorCalyxBendAngle) if calyx == lowerMinor else (0 if calyx == middleMajor else math.radians(minorCalyxBendAngle)) - else: - theta = 0 - rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) - ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) - pyramidDirn = [rx, ry, 0.0] - if isRotateMinorCalyx: - if nMinorCalyxesList[calyx] > 1: - rotateAngle = -25 if side == anterior else 25 - else: - rotateAngle = 0 - if calyx in [lowerMinor, upperMinor]: - rotateAngle = rotateAngle + (minorCalyxRotateAngle if calyx == upperMinor else -minorCalyxRotateAngle) - rotateAngleRad = math.radians(rotateAngle) - pyramidDirn = rotate_about_z_axis(pyramidDirn, rotateAngleRad) - - xList = [] - for e in range(pyramidElementsCount): - pyramidLengthScale = 0.3 * pyramidLength if e == 0 else (0.7 * pyramidLength if e == 1 else pyramidLength) - x = add(sx, mult(pyramidDirn, (pyramidLengthScale))) - xList.append(x) - pyramid_sd2_list = [] - pyramid_sd3_list = [] - sNodeIdentifiers = [] - for e in range(pyramidElementsCount): - sNodeIdentifiers.append(nodeIdentifier) - node = nodes.findNodeByIdentifier(nodeIdentifier) - fieldcache.setNode(node) - - sd1 = sub(xList[1], xList[0]) - pyramidWidthScale = minorCalyxRadius if e == 0 else (pyramidHalfWidth if e == 1 else 0.9 * pyramidHalfWidth) - pyramidThickness = 1.1 * minorCalyxRadius if e == 1 else minorCalyxRadius - if calyx in [bottomMinor, topMinor]: - sd3 = [0.0, 0.0, pyramidThickness] - sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) - else: - sd3 = [0.0, 0.0, pyramidThickness] - sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) - pyramid_sd2_list.append(sd2) - pyramid_sd3_list.append(sd3) - sid2 = mult(sd2, innerProportionDefault) - sid3 = mult(sd3, innerProportionDefault) - for field, derivatives in ( - (coordinates, [xList[e], sd1, sd2, sd3]), - (innerCoordinates, [xList[e], sd1, sid2, sid3]) + index = 0 if calyx <= lowerMinor else (1 if calyx == middleMajor else 2) + if majorCalyxFlags[index]: + for side in range(nMinorCalyxesList[calyx]): + sx = renalPyramidStartX[calyx][side] + + # Calculate pyramid direction + if calyx in [bottomMinor, topMinor]: + tx = [0.0, -1.0, 0.0] if calyx == bottomMinor else [0.0, 1.0, 0.0] + angle = 25 if nMinorCalyxesList[calyx + 1] > 1 else bottomMinorCalyxAngle * 0.5 + rotateAngle = 0 if nMinorCalyxesList[calyx] <= 1 else (-angle if side == anterior else angle) + + rotateAngle = (-rotateAngle + minorCalyxRotateAngle if calyx == topMinor else rotateAngle - minorCalyxRotateAngle) + theta = math.radians(rotateAngle) + rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) + ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) + pyramidDirn = [rx, ry, 0.0] + else: + tx = [1.0, 0.0, 0.0] + theta = 0 + if nMinorCalyxesList[calyx] > 1: + if calyx == lowerMinor: + theta = math.radians(-minorCalyxBendAngle) + elif calyx == upperMinor: + theta = math.radians(minorCalyxBendAngle) + + rx = tx[0] * math.cos(theta) - tx[1] * math.sin(theta) + ry = tx[0] * math.sin(theta) + tx[1] * math.cos(theta) + pyramidDirn = [rx, ry, 0.0] + + if isRotateMinorCalyx: + rotateAngle = 0 if nMinorCalyxesList[calyx] <= 1 else (-25 if side == anterior else 25) + if calyx in [lowerMinor, upperMinor]: + rotateAngle += (minorCalyxRotateAngle if calyx == upperMinor else -minorCalyxRotateAngle) + rotateAngleRad = math.radians(rotateAngle) + pyramidDirn = rotate_about_z_axis(pyramidDirn, rotateAngleRad) + + # Generate pyramid node positions + xList = [] + for e in range(pyramidElementsCount): + pyramidLengthScale = [0.3, 0.7, 1.0][e] * pyramidLength + x = add(sx, mult(pyramidDirn, pyramidLengthScale)) + xList.append(x) + + # Generate pyramid nodes + pyramid_sd2_list = [] + pyramid_sd3_list = [] + sNodeIdentifiers = [] + + for e in range(pyramidElementsCount): + sNodeIdentifiers.append(nodeIdentifier) + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + + sd1 = sub(xList[1], xList[0]) + pyramidWidthScale = [minorCalyxRadius, pyramidHalfWidth, 0.9 * pyramidHalfWidth][e] + pyramidThickness = 1.1 * minorCalyxRadius if e == 1 else minorCalyxRadius + + sd3 = [0.0, 0.0, pyramidThickness] + sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) + + pyramid_sd2_list.append(sd2) + pyramid_sd3_list.append(sd3) + sid2 = mult(sd2, innerProportionDefault) + sid3 = mult(sd3, innerProportionDefault) + + for field, derivatives in ( + (coordinates, [xList[e], sd1, sd2, sd3]), + (innerCoordinates, [xList[e], sd1, sid2, sid3]) + ): + for valueLabel, value in zip( + (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, + Node.VALUE_LABEL_D_DS3), + derivatives ): - for valueLabel, value in zip( - (Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, - Node.VALUE_LABEL_D_DS3), - derivatives - ): - field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) - nodeIdentifier += 1 + field.setNodeParameters(fieldcache, -1, valueLabel, 1, value) + nodeIdentifier += 1 - pyramid_sd2_list.append(connection_sd2_list[calyx][side]) - pyramid_sd3_list.append(connection_sd3_list[calyx][side]) - for e in range(pyramidElementsCount): - node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) - fieldcache.setNode(node) - sd12 = sub(pyramid_sd2_list[e + 1], pyramid_sd2_list[e]) - sd13 = sub(pyramid_sd3_list[e + 1], pyramid_sd3_list[e]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sd13) - sid12 = mult(sd12, innerProportionDefault) - sid13 = mult(sd13, innerProportionDefault) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sid12) - innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sid13) - else: - continue + # Set cross derivatives for pyramid nodes + pyramid_sd2_list.append(connection_sd2_list[calyx][side]) + pyramid_sd3_list.append(connection_sd3_list[calyx][side]) + + for e in range(pyramidElementsCount): + node = nodes.findNodeByIdentifier(sNodeIdentifiers[e]) + fieldcache.setNode(node) + sd12 = sub(pyramid_sd2_list[e + 1], pyramid_sd2_list[e]) + sd13 = sub(pyramid_sd3_list[e + 1], pyramid_sd3_list[e]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, sd12) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, sd13) + sid12 = mult(sd12, innerProportionDefault) + sid13 = mult(sd13, 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 diff --git a/tests/test_renalpelvis.py b/tests/test_renalpelvis.py index a3683e27..52d1854d 100644 --- a/tests/test_renalpelvis.py +++ b/tests/test_renalpelvis.py @@ -80,19 +80,19 @@ def test_renal_pelvis_human(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.4866338272404299, delta=tol) - self.assertAlmostEqual(surfaceArea, 17.309413531530122, delta=tol) + self.assertAlmostEqual(volume, 0.48202318217985807, delta=tol) + self.assertAlmostEqual(surfaceArea, 17.351342367941086, delta=tol) # check some annotation groups: expectedSizes3d = { "core": (360, 0.22934693864957886), - "major calyx": (48, 0.01808641549910496), - "minor calyx": (160, 0.03421501056066799), - "renal pelvis": (256, 0.11402458784598982), + "major calyx": (48, 0.014074549575560903), + "minor calyx": (160, 0.03321136172402478), + "renal pelvis": (256, 0.10941335689890085), "renal pyramid": (680, 0.3726073231262026), - "shell": (576, 0.2572849723226171), - "ureter": (64, 0.06581299176056989) + "shell": (576, 0.25267374137552767), + "ureter": (64, 0.06522783433316985) } for name in expectedSizes3d: term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) @@ -108,11 +108,11 @@ def test_renal_pelvis_human(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "major calyx": (208, 2.157380980683151), - "minor calyx": (694, 4.263807908549397), - "renal pelvis": (1070, 11.526296790112461), + "major calyx": (208, 2.1196123306102757), + "minor calyx": (694, 4.248513168907139), + "renal pelvis": (1070, 11.484684326107075), "renal pyramid": (2440, 21.048589257773124), - "ureter": (264, 5.65955310775128) + "ureter": (264, 5.649317809917837) } for name in expectedSizes2d: term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) From 0e358ab4da617784fe91c824e272925a6b638a5d Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 26 Sep 2025 10:49:50 +1200 Subject: [PATCH 15/18] Add ureteropelvic junction width and renal pyramid thickness --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 29 ++++++++--- tests/test_renalpelvis.py | 52 +++++++++---------- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index 05dffdd8..65372415 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -66,9 +66,10 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of calyxes at lower minor calyx"] = 2 options["Ureter length"] = 4.0 options["Ureter radius"] = 0.1 + options["Ureteropelvic junction width"] = 0.3 options["Ureter bend angle degrees"] = 45 options["Major calyx length"] = 0.7 - options["Major calyx radius"] = 0.1 + options["Major calyx radius"] = 0.15 options["Major calyx angle degrees"] = 200 options["Middle major calyx length"] = 0.4 options["Major to bottom/top minor calyx length"] = 0.4 @@ -82,6 +83,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Lower/upper minor calyx bend angle degrees"] = 10 options["Renal pyramid length"] = 0.6 options["Renal pyramid width"] = 0.6 + options["Renal pyramid thickness"] = 0.2 options["Structure"] = cls.getPelvisLayoutStructure(options) @@ -106,6 +108,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of calyxes at lower minor calyx"] = 1 options["Ureter length"] = 3.0 options["Ureter radius"] = 0.1 + options["Ureteropelvic junction width"] = options["Ureter radius"] options["Ureter bend angle degrees"] = 45 options["Major calyx length"] = 0.6 options["Major calyx radius"] = 0.1 @@ -122,6 +125,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Lower/upper minor calyx bend angle degrees"] = 10 options["Renal pyramid length"] = 0.6 options["Renal pyramid width"] = 0.6 + options["Renal pyramid thickness"] = 0.2 return options @@ -142,6 +146,7 @@ def getOrderedOptionNames(cls): "Number of calyxes at bottom minor calyx", "Ureter length", "Ureter radius", + "Ureteropelvic junction width", "Ureter bend angle degrees", "Major calyx length", "Major calyx radius", @@ -158,6 +163,7 @@ def getOrderedOptionNames(cls): "Lower/upper minor calyx bend angle degrees", "Renal pyramid length", "Renal pyramid width", + "Renal pyramid thickness", "Inner proportion default", "Inner proportion ureter" ] @@ -170,6 +176,7 @@ def getOrderedOptionNames(cls): "Minor calyx radius", "Renal pyramid length", "Renal pyramid width", + "Renal pyramid thickness", "Inner proportion default", "Inner proportion ureter" ] @@ -180,6 +187,7 @@ def checkOptions(cls, options): for key in [ "Ureter length", "Ureter radius", + "Ureteropelvic junction width", "Ureter bend angle degrees", "Major calyx length", "Major calyx radius", @@ -194,7 +202,8 @@ def checkOptions(cls, options): "Lower/upper minor calyx bifurcation angle degrees", "Lower/upper minor calyx bend angle degrees", "Renal pyramid length", - "Renal pyramid width" + "Renal pyramid width", + "Renal pyramid thickness" ]: if options[key] < 0.1: options[key] = 0.1 @@ -371,6 +380,7 @@ def generateBaseMesh(cls, region, options): ureterLength = options["Ureter length"] ureterRadius = options["Ureter radius"] + upjWidth = options["Ureteropelvic junction width"] ureterBendAngle = options["Ureter bend angle degrees"] majorCalyxLength = options["Major calyx length"] majorCalyxRadius = options["Major calyx radius"] @@ -387,6 +397,7 @@ def generateBaseMesh(cls, region, options): minorCalyxBendAngle = options["Lower/upper minor calyx bend angle degrees"] pyramidLength = options["Renal pyramid length"] pyramidWidth = options["Renal pyramid width"] + pyramidThickness = options["Renal pyramid thickness"] innerProportionDefault = options["Inner proportion default"] innerProportionUreter = options["Inner proportion ureter"] @@ -491,8 +502,6 @@ def generateBaseMesh(cls, region, options): startX = [tx, ty, 0.0] d1 = [ureterScale, 0.0, 0.0] - d3 = [0.0, 0.0, ureterRadius] - id3 = mult(d3, innerProportionUreter) td1 = rotate_about_z_axis(d1, 2 * ureterBendAngleRadians) sx, sd1 = sampleCubicHermiteCurves([startX, endX], [td1, d1], ureterElementsCount, arcLengthDerivatives=True)[0:2] @@ -501,7 +510,13 @@ def generateBaseMesh(cls, region, options): for e in range(ureterElementsCount + 1): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - sd2 = set_magnitude(cross(d3, sd1[e]), ureterRadius) + if e < ureterElementsCount: + d3 = [0.0, 0.0, ureterRadius] + sd2 = set_magnitude(cross(d3, sd1[e]), ureterRadius) # last one should be width + else: + d3 = [0.0, 0.0, majorCalyxRadius] + sd2 = set_magnitude(cross(d3, sd1[e]), upjWidth) + id3 = mult(d3, innerProportionUreter) sid2 = mult(sd2, innerProportionUreter) for field, derivatives in ((coordinates, (sd1[e], sd2, d3)), (innerCoordinates, (sd1[e], sid2, id3))): @@ -831,9 +846,9 @@ def generateBaseMesh(cls, region, options): sd1 = sub(xList[1], xList[0]) pyramidWidthScale = [minorCalyxRadius, pyramidHalfWidth, 0.9 * pyramidHalfWidth][e] - pyramidThickness = 1.1 * minorCalyxRadius if e == 1 else minorCalyxRadius + thickness = minorCalyxRadius if e == 0 else (0.9 * pyramidThickness if e == pyramidElementsCount - 1 else pyramidThickness) - sd3 = [0.0, 0.0, pyramidThickness] + sd3 = [0.0, 0.0, thickness] sd2 = set_magnitude(cross(sd3, sd1), pyramidWidthScale) pyramid_sd2_list.append(sd2) diff --git a/tests/test_renalpelvis.py b/tests/test_renalpelvis.py index 52d1854d..8b993db1 100644 --- a/tests/test_renalpelvis.py +++ b/tests/test_renalpelvis.py @@ -48,9 +48,9 @@ def test_renal_pelvis_human(self): mesh3d = fieldmodule.findMeshByDimension(3) self.assertEqual(936, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(3430, mesh2d.getSize()) + self.assertEqual(3428, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(4075, mesh1d.getSize()) + self.assertEqual(4073, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) self.assertEqual(1582, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) @@ -61,8 +61,8 @@ def test_renal_pelvis_human(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [1.0718417770256363, -2.8357557021117126, -0.10878747768283183], tol) - assertAlmostEqualList(self, maximums, [5.199312959129288, 1.8502096232770497, 0.10878747768283183], tol) + assertAlmostEqualList(self, minimums, [1.0718417770256363, -2.8357557021117126, -0.19871303721933536], tol) + assertAlmostEqualList(self, maximums, [5.199312959129288, 1.8502096232770497, 0.19871303721933536], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -80,19 +80,19 @@ def test_renal_pelvis_human(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.48202318217985807, delta=tol) - self.assertAlmostEqual(surfaceArea, 17.351342367941086, delta=tol) + self.assertAlmostEqual(volume, 0.7814475699705258, delta=tol) + self.assertAlmostEqual(surfaceArea, 20.83194524004843, delta=tol) # check some annotation groups: expectedSizes3d = { - "core": (360, 0.22934693864957886), - "major calyx": (48, 0.014074549575560903), - "minor calyx": (160, 0.03321136172402478), - "renal pelvis": (256, 0.10941335689890085), - "renal pyramid": (680, 0.3726073231262026), - "shell": (576, 0.25267374137552767), - "ureter": (64, 0.06522783433316985) + "core": (360, 0.3846094864179342), + "major calyx": (48, 0.029128480517289796), + "minor calyx": (160, 0.036662643737561694), + "renal pelvis": (256, 0.16709749228397455), + "renal pyramid": (680, 0.6143476950036447), + "shell": (576, 0.39683570086969006), + "ureter": (64, 0.10778687283916291) } for name in expectedSizes3d: term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) @@ -108,11 +108,11 @@ def test_renal_pelvis_human(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "major calyx": (208, 2.1196123306102757), - "minor calyx": (694, 4.248513168907139), - "renal pelvis": (1070, 11.484684326107075), - "renal pyramid": (2440, 21.048589257773124), - "ureter": (264, 5.649317809917837) + "major calyx": (208, 2.978894400047078), + "minor calyx": (692, 4.4296179632944614), + "renal pelvis": (1068, 13.807110887897247), + "renal pyramid": (2440, 25.945226755465498), + "ureter": (264, 7.203840960105056) } for name in expectedSizes2d: term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) @@ -170,8 +170,8 @@ def test_renal_pelvis_rat(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [0.7789485582121838, -2.128648920925165, -0.10878747768283183], tol) - assertAlmostEqualList(self, maximums, [4.2, 0.29820338638253907, 0.10878747768283183], tol) + assertAlmostEqualList(self, minimums, [0.7789485582121838, -2.128648920925165, -0.19868582233021914], tol) + assertAlmostEqualList(self, maximums, [4.2, 0.2983665771942383, 0.19868582233021914], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -189,18 +189,18 @@ def test_renal_pelvis_rat(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.09588370625349187, delta=tol) - self.assertAlmostEqual(surfaceArea, 4.930576718031101, delta=tol) + self.assertAlmostEqual(volume, 0.12005376065418445, delta=tol) + self.assertAlmostEqual(surfaceArea, 5.096473140665664, delta=tol) # check some annotation groups: expectedSizes3d = { - "core": (36, 0.022899342100307266), + "core": (36, 0.03842050612475523), "major calyx": (8, 0.005543006096630809), "minor calyx": (16, 0.007802597971432378), "renal pelvis": (80, 0.05861664077426631), - "renal pyramid": (68, 0.037266476269253765), - "shell": (112, 0.07298377494321281), + "renal pyramid": (68, 0.06143679397579967), + "shell": (112, 0.08163292862531038), "ureter": (64, 0.050814042802833914) } for name in expectedSizes3d: @@ -220,7 +220,7 @@ def test_renal_pelvis_rat(self): "major calyx": (40, 0.5472137937926038), "minor calyx": (72, 0.816589508937787), "renal pelvis": (328, 5.171627911264767), - "renal pyramid": (244, 2.1052097538093686), + "renal pyramid": (244, 2.595915075303359), "ureter": (264, 4.371043844773501) } for name in expectedSizes2d: From 88a5d55fdbfa856e442fe13e559f2f650e8c65f0 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Fri, 26 Sep 2025 15:12:24 +1200 Subject: [PATCH 16/18] Add refine functionality --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 20 ++- tests/test_renalpelvis.py | 117 +++++++++++++++++- 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index 65372415..c69ffb3f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -15,6 +15,7 @@ from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base from scaffoldmaker.scaffoldpackage import ScaffoldPackage from scaffoldmaker.utils.interpolation import sampleCubicHermiteCurves, smoothCubicHermiteDerivativesLine +from scaffoldmaker.utils.meshrefinement import MeshRefinement from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshGenerateData, RenalPelvisTubeNetworkMeshBuilder from cmlibs.zinc.node import Node @@ -512,7 +513,7 @@ def generateBaseMesh(cls, region, options): fieldcache.setNode(node) if e < ureterElementsCount: d3 = [0.0, 0.0, ureterRadius] - sd2 = set_magnitude(cross(d3, sd1[e]), ureterRadius) # last one should be width + sd2 = set_magnitude(cross(d3, sd1[e]), ureterRadius) else: d3 = [0.0, 0.0, majorCalyxRadius] sd2 = set_magnitude(cross(d3, sd1[e]), upjWidth) @@ -926,6 +927,8 @@ def getDefaultOptions(cls, parameterSetName='Default'): options["Use linear through shell"] = False options["Use outer trim surfaces"] = True options["Show trim surfaces"] = False + options["Refine"] = False + options["Refine number of elements"] = 4 return options @classmethod @@ -938,7 +941,9 @@ def getOrderedOptionNames(cls): "Target element density along longest segment", "Use linear through shell", "Use outer trim surfaces", - "Show trim surfaces" + "Show trim surfaces", + "Refine", + "Refine number of elements" ] @classmethod @@ -1028,6 +1033,17 @@ 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) + 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_renalpelvis.py b/tests/test_renalpelvis.py index 8b993db1..8d908897 100644 --- a/tests/test_renalpelvis.py +++ b/tests/test_renalpelvis.py @@ -1,3 +1,4 @@ +import copy import math import unittest @@ -12,7 +13,7 @@ from scaffoldmaker.annotation.kidney_terms import get_kidney_term from scaffoldmaker.annotation.ureter_terms import get_ureter_term from scaffoldmaker.meshtypes.meshtype_3d_renal_pelvis1 import MeshType_3d_renal_pelvis1 - +from scaffoldmaker.utils.meshrefinement import MeshRefinement from testutils import assertAlmostEqualList @@ -28,7 +29,7 @@ def test_renal_pelvis_human(self): self.assertEqual(parameterSetNames, ["Default", "Human 1", "Rat 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,7 +41,19 @@ def test_renal_pelvis_human(self): context = Context("Test") region = context.getDefaultRegion() self.assertTrue(region.isValid()) - annotationGroups = scaffold.generateMesh(region, options)[0] + + fieldmodule = region.getFieldmodule() + with ChangeManager(fieldmodule): + annotationGroups = scaffold.generateBaseMesh(region, options)[0] + fieldmodule.defineAllFaces() + originalAnnotationGroups = copy.copy(annotationGroups) + for annotationGroup in annotationGroups: + annotationGroup.addSubelements() + scaffold.defineFaceAnnotations(region, options, annotationGroups) + for annotationGroup in annotationGroups: + if annotationGroup not in originalAnnotationGroups: + annotationGroup.addSubelements() + self.assertEqual(7, len(annotationGroups)) fieldmodule = region.getFieldmodule() @@ -127,6 +140,47 @@ def test_renal_pelvis_human(self): self.assertEqual(result, RESULT_OK) self.assertAlmostEqual(surfaceArea, expectedSizes2d[name][1], delta=tol) + # refine 2x2x2 and check result + annotationGroups = originalAnnotationGroups + + refineRegion = region.createRegion() + refineFieldmodule = refineRegion.getFieldmodule() + options['Refine'] = True + options['Refine number of elements'] = 2 + refineNumberOfElements = options['Refine number of elements'] + meshrefinement = MeshRefinement(region, refineRegion, annotationGroups) + scaffold.refineMesh(meshrefinement, options) + annotationGroups = meshrefinement.getAnnotationGroups() + + refineFieldmodule.defineAllFaces() + oldAnnotationGroups = copy.copy(annotationGroups) + for annotationGroup in annotationGroups: + annotationGroup.addSubelements() + scaffold.defineFaceAnnotations(refineRegion, options, annotationGroups) + for annotation in annotationGroups: + if annotation not in oldAnnotationGroups: + annotationGroup.addSubelements() + + self.assertEqual(7, len(annotationGroups)) + + mesh3d = refineFieldmodule.findMeshByDimension(3) + self.assertEqual(7488, mesh3d.getSize()) + mesh2d = refineFieldmodule.findMeshByDimension(2) + self.assertEqual(24944, mesh2d.getSize()) + mesh1d = refineFieldmodule.findMeshByDimension(1) + self.assertEqual(27474, mesh1d.getSize()) + nodes = refineFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(10019, nodes.getSize()) + datapoints = refineFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) + self.assertEqual(0, datapoints.getSize()) + + # check some refined annotationGroups: + for name in expectedSizes3d: + term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) + group = getAnnotationGroupForTerm(annotationGroups, term) + size = group.getMeshGroup(mesh3d).getSize() + self.assertEqual(expectedSizes3d[name][0] * (refineNumberOfElements ** 3), size, name) + def test_renal_pelvis_rat(self): """ @@ -137,7 +191,7 @@ def test_renal_pelvis_rat(self): self.assertEqual(parameterSetNames, ["Default", "Human 1", "Rat 1"]) options = scaffold.getDefaultOptions("Rat 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"]) @@ -149,7 +203,18 @@ def test_renal_pelvis_rat(self): context = Context("Test") region = context.getDefaultRegion() self.assertTrue(region.isValid()) - annotationGroups = scaffold.generateMesh(region, options)[0] + + fieldmodule = region.getFieldmodule() + with ChangeManager(fieldmodule): + annotationGroups = scaffold.generateBaseMesh(region, options)[0] + fieldmodule.defineAllFaces() + originalAnnotationGroups = copy.copy(annotationGroups) + for annotationGroup in annotationGroups: + annotationGroup.addSubelements() + scaffold.defineFaceAnnotations(region, options, annotationGroups) + for annotationGroup in annotationGroups: + if annotationGroup not in originalAnnotationGroups: + annotationGroup.addSubelements() self.assertEqual(7, len(annotationGroups)) fieldmodule = region.getFieldmodule() @@ -236,5 +301,47 @@ def test_renal_pelvis_rat(self): self.assertEqual(result, RESULT_OK) self.assertAlmostEqual(surfaceArea, expectedSizes2d[name][1], delta=tol) + # refine 2x2x2 and check result + annotationGroups = originalAnnotationGroups + + refineRegion = region.createRegion() + refineFieldmodule = refineRegion.getFieldmodule() + options['Refine'] = True + options['Refine number of elements'] = 2 + refineNumberOfElements = options['Refine number of elements'] + meshrefinement = MeshRefinement(region, refineRegion, annotationGroups) + scaffold.refineMesh(meshrefinement, options) + annotationGroups = meshrefinement.getAnnotationGroups() + + refineFieldmodule.defineAllFaces() + oldAnnotationGroups = copy.copy(annotationGroups) + for annotationGroup in annotationGroups: + annotationGroup.addSubelements() + scaffold.defineFaceAnnotations(refineRegion, options, annotationGroups) + for annotation in annotationGroups: + if annotation not in oldAnnotationGroups: + annotationGroup.addSubelements() + + self.assertEqual(7, len(annotationGroups)) + + mesh3d = refineFieldmodule.findMeshByDimension(3) + self.assertEqual(1184, mesh3d.getSize()) + mesh2d = refineFieldmodule.findMeshByDimension(2) + self.assertEqual(4032, mesh2d.getSize()) + mesh1d = refineFieldmodule.findMeshByDimension(1) + self.assertEqual(4526, mesh1d.getSize()) + nodes = refineFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(1679, nodes.getSize()) + datapoints = refineFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) + self.assertEqual(0, datapoints.getSize()) + + # check some refined annotationGroups: + for name in expectedSizes3d: + term = get_ureter_term(name) if name == "ureter" else get_kidney_term(name) + group = getAnnotationGroupForTerm(annotationGroups, term) + size = group.getMeshGroup(mesh3d).getSize() + self.assertEqual(expectedSizes3d[name][0] * (refineNumberOfElements ** 3), size, name) + + if __name__ == "__main__": unittest.main() From 605c691b8bb27ebe70d50171bfd65191a283a1b1 Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Wed, 1 Oct 2025 10:39:59 +1300 Subject: [PATCH 17/18] Fix renal pyramid and minor calyx annotations --- src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index c69ffb3f..8f71b31e 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -448,9 +448,9 @@ def generateBaseMesh(cls, region, options): meshGroups = [renalPelvisMeshGroup, minorCalyxGroup.getMeshGroup(mesh)] bottomMinor, lowerMinor, middleMajor, topMinor, upperMinor = 0, 1, 2, 3, 4 - minorCalyxList = (([bottomMinor, lowerMinor] if isBottomMC else []) + - ([middleMajor] if isMidMC else []) + - ([topMinor, upperMinor] if isTopMC else [])) + calyxes = [bottomMinor, lowerMinor, middleMajor, upperMinor, topMinor] + flags = [isBottomMC, isLowerMC, isMidMC, isUpperMC, isTopMC] + minorCalyxList = [c for c, f in zip(calyxes, flags) if f] for calyx in minorCalyxList: cElementIdentifier = middleMajorCalyxIdentifier if (calyx == middleMajor and isMidMC) else elementIdentifier From a662266092501df69d6278aa8f4a004ceec369ab Mon Sep 17 00:00:00 2001 From: Chang-Joon Lee Date: Mon, 17 Nov 2025 13:40:34 +1300 Subject: [PATCH 18/18] Add left-right orientation --- .../meshtypes/meshtype_3d_renal_pelvis1.py | 63 ++++++++++++++++++- tests/test_renalpelvis.py | 20 +++--- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py index 8f71b31e..464cd881 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_renal_pelvis1.py @@ -5,10 +5,11 @@ from cmlibs.maths.vectorops import mult, cross, add, sub, set_magnitude, rotate_about_z_axis, \ rotate_vector_around_vector -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.zinc.field import Field -from scaffoldmaker.annotation.annotationgroup import AnnotationGroup +from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, getAnnotationGroupForTerm, \ + findOrCreateAnnotationGroupForTerm from scaffoldmaker.annotation.kidney_terms import get_kidney_term from scaffoldmaker.annotation.ureter_terms import get_ureter_term from scaffoldmaker.meshtypes.meshtype_1d_network_layout1 import MeshType_1d_network_layout1 @@ -497,7 +498,7 @@ def generateBaseMesh(cls, region, options): sinUreterAngle = math.sin(ureterBendAngleRadians) cosUreterAngle = math.cos(ureterBendAngleRadians) - endX = [ureterLength, 0.0, 0.0] + endX = [0.0, 0.0, 0.0] tx = endX[0] - ureterLength * cosUreterAngle ty = endX[1] - ureterLength * sinUreterAngle startX = [tx, ty, 0.0] @@ -920,6 +921,7 @@ def getDefaultOptions(cls, parameterSetName='Default'): options["Base parameter set"] = useParameterSetName options["Network layout"] = ScaffoldPackage(MeshType_1d_renal_pelvis_network_layout1, defaultParameterSetName=useParameterSetName) + options["Left"] = True options["Elements count around"] = 8 options["Elements count through shell"] = 1 options["Annotation elements counts around"] = [0] @@ -935,6 +937,7 @@ def getDefaultOptions(cls, parameterSetName='Default'): def getOrderedOptionNames(cls): return [ "Network layout", + "Left", "Elements count around", "Elements count through shell", "Annotation elements counts around", @@ -1031,6 +1034,24 @@ def generateBaseMesh(cls, region, options): tubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() + 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() + + tempGroup = fm.createFieldAdd(shellGroup, coreGroup) + allGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region,("all", "")) + allGroup.getMeshGroup(mesh).addElementsConditional(tempGroup) + allNodeset = allGroup.getNodesetGroup(nodes) + + isLeft = options["Left"] + rotateMeshAboutAxis(90, fm, coordinates, allNodeset, axis=3) + if not isLeft: + rotateMeshAboutAxis(180, fm, coordinates, allNodeset, axis=1) + return annotationGroups, None @classmethod @@ -1044,6 +1065,42 @@ def refineMesh(cls, meshRefinement, options): refineElementsCount = options['Refine number of elements'] meshRefinement.refineAllElementsCubeStandard3d(refineElementsCount, refineElementsCount, refineElementsCount) + +def rotateMeshAboutAxis(rotateAngle, fm, coordinates, nodeset, axis): + """ + 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. + :param nodeset: Zinc NodesetGroup containing nodes to transform. + :param axis: Axis of rotation. + :return: None + """ + rotateAngle = -math.radians(rotateAngle) # negative value due to right handed rule + + if axis == 1: + # Rotation about x-axis + rotateMatrix = fm.createFieldConstant([1.0, 0.0, 0.0, + 0.0, math.cos(rotateAngle), math.sin(rotateAngle), + 0.0, -math.sin(rotateAngle), math.cos(rotateAngle)]) + elif axis == 2: + # Rotation about y-axis + rotateMatrix = fm.createFieldConstant([math.cos(rotateAngle), 0.0, -math.sin(rotateAngle), + 0.0, 1.0, 0.0, + math.sin(rotateAngle), 0.0, math.cos(rotateAngle)]) + elif axis == 3: + # Rotation about z-axis + 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(nodeset) + 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_renalpelvis.py b/tests/test_renalpelvis.py index 8d908897..545c2009 100644 --- a/tests/test_renalpelvis.py +++ b/tests/test_renalpelvis.py @@ -29,7 +29,7 @@ def test_renal_pelvis_human(self): self.assertEqual(parameterSetNames, ["Default", "Human 1", "Rat 1"]) options = scaffold.getDefaultOptions("Human 1") - self.assertEqual(11, len(options)) + self.assertEqual(12, 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"]) @@ -54,7 +54,7 @@ def test_renal_pelvis_human(self): if annotationGroup not in originalAnnotationGroups: annotationGroup.addSubelements() - self.assertEqual(7, len(annotationGroups)) + self.assertEqual(8, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) @@ -74,8 +74,8 @@ def test_renal_pelvis_human(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [1.0718417770256363, -2.8357557021117126, -0.19871303721933536], tol) - assertAlmostEqualList(self, maximums, [5.199312959129288, 1.8502096232770497, 0.19871303721933536], tol) + assertAlmostEqualList(self, minimums, [-1.8502096232770495, -2.9281582229743637, -0.19871303721933536], tol) + assertAlmostEqualList(self, maximums, [2.8357557021117126, 1.1993129591292884, 0.19871303721933536], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -161,7 +161,7 @@ def test_renal_pelvis_human(self): if annotation not in oldAnnotationGroups: annotationGroup.addSubelements() - self.assertEqual(7, len(annotationGroups)) + self.assertEqual(8, len(annotationGroups)) mesh3d = refineFieldmodule.findMeshByDimension(3) self.assertEqual(7488, mesh3d.getSize()) @@ -191,7 +191,7 @@ def test_renal_pelvis_rat(self): self.assertEqual(parameterSetNames, ["Default", "Human 1", "Rat 1"]) options = scaffold.getDefaultOptions("Rat 1") - self.assertEqual(11, len(options)) + self.assertEqual(12, 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"]) @@ -215,7 +215,7 @@ def test_renal_pelvis_rat(self): for annotationGroup in annotationGroups: if annotationGroup not in originalAnnotationGroups: annotationGroup.addSubelements() - self.assertEqual(7, len(annotationGroups)) + self.assertEqual(8, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) @@ -235,8 +235,8 @@ def test_renal_pelvis_rat(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-4 - assertAlmostEqualList(self, minimums, [0.7789485582121838, -2.128648920925165, -0.19868582233021914], tol) - assertAlmostEqualList(self, maximums, [4.2, 0.2983665771942383, 0.19868582233021914], tol) + assertAlmostEqualList(self, minimums, [-0.29836657719423826, -2.221051441787816, -0.19868582233021914], tol) + assertAlmostEqualList(self, maximums, [2.128648920925165, 1.2000000000000002, 0.19868582233021914], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -322,7 +322,7 @@ def test_renal_pelvis_rat(self): if annotation not in oldAnnotationGroups: annotationGroup.addSubelements() - self.assertEqual(7, len(annotationGroups)) + self.assertEqual(8, len(annotationGroups)) mesh3d = refineFieldmodule.findMeshByDimension(3) self.assertEqual(1184, mesh3d.getSize())