diff --git a/setup.py b/setup.py index 224e90c5..5ebe0e3b 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ def readfile(filename, split=False): setup( name="scaffoldmaker", - version="0.13.0", + version="0.14.0", description="Python client for generating anatomical scaffolds using Zinc", long_description="\n".join(readme) + source_license, long_description_content_type="text/x-rst", diff --git a/src/scaffoldmaker/annotation/uterus_terms.py b/src/scaffoldmaker/annotation/uterus_terms.py index 67eaa966..16a7dc48 100644 --- a/src/scaffoldmaker/annotation/uterus_terms.py +++ b/src/scaffoldmaker/annotation/uterus_terms.py @@ -10,7 +10,7 @@ ("dorsal top left horn", "None"), ("dorsal top right horn", "None"), ("external cervical os", "UBERON:0013760", "FMA:76836", "ILX:0736534"), - ("fundus", "None"), + ("fundus of uterus", "None"), ("internal cervical os", "UBERON:0013759", "FMA:17747", "ILX:0729495"), ("junction of left round ligament with uterus", "None"), ("junction of right round ligament with uterus", "None"), diff --git a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py index eac2d920..7f236896 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py @@ -8,7 +8,7 @@ from cmlibs.zinc.field import Field, FieldGroup from cmlibs.zinc.node import Node from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base -from scaffoldmaker.utils.networkmesh import NetworkMesh +from scaffoldmaker.utils.networkmesh import NetworkMesh, pathValueLabels from scaffoldmaker.utils.interpolation import smoothCurveSideCrossDerivatives from scaffoldmaker.utils.zinc_utils import clearRegion, get_nodeset_field_parameters, \ get_nodeset_path_ordered_field_parameters, make_nodeset_derivatives_orthogonal, \ @@ -27,7 +27,9 @@ class MeshType_1d_network_layout1(Scaffold_base): "Bifurcation": "1-2.1,2.2-3,2.3-4", "Converging bifurcation": "1-3.1,2-3.2,3.3-4", "Loop": "1-2-3-4-5-6-7-8-1", - "Sphere cube": "1.1-2.1,1.2-3.1,1.3-4.1,2.2-5.2,2.3-6.1,3.2-6.2,3.3-7.1,4.2-7.2,4.3-5.1,5.3-8.1,6.3-8.2,7.3-8.3" + "Sphere cube": "1.1-2.1,1.2-3.1,1.3-4.1,2.2-5.2,2.3-6.1,3.2-6.2,3.3-7.1,4.2-7.2,4.3-5.1,5.3-8.1,6.3-8.2,7.3-8.3", + "Trifurcation": "1-2.1,2.2-3,2.3-4,2.4-5", + "Trifurcation cross": "1-3.1,2-3.2,3.2-4,3.1-5" } @classmethod @@ -261,10 +263,6 @@ def assignCoordinates(cls, region, options, networkMesh, functionOptions, editGr if not selectionMeshGroup.isValid(): print("Assign coordinates: Selection contains no elements. Clear it to assign globally.") return False, False - valueLabels = [ - Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, - Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D2_DS1DS2, - Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3] with ChangeManager(fieldmodule): # get all node parameters (from selection if any) @@ -277,10 +275,10 @@ def assignCoordinates(cls, region, options, networkMesh, functionOptions, editGr tmpMeshGroup = tmpGroup.createMeshGroup(mesh1d) tmpMeshGroup.addElementsConditional(selectionGroup) editNodeset = tmpGroup.getNodesetGroup(nodes) - _, originalNodeParameters = get_nodeset_field_parameters(editNodeset, toCoordinates, valueLabels) + _, originalNodeParameters = get_nodeset_field_parameters(editNodeset, toCoordinates, pathValueLabels) del tmpMeshGroup del tmpGroup - _, nodeParameters = get_nodeset_field_parameters(editNodeset, fromCoordinates, valueLabels) + _, nodeParameters = get_nodeset_field_parameters(editNodeset, fromCoordinates, pathValueLabels) modifyVersions = None # default is to modify all versions if selectionGroup: @@ -324,7 +322,7 @@ def assignCoordinates(cls, region, options, networkMesh, functionOptions, editGr for d in range(2, 6): nNodeParameters[d][v] = oNodeParameters[d][v] - set_nodeset_field_parameters(editNodeset, toCoordinates, valueLabels, nodeParameters, editGroupName) + set_nodeset_field_parameters(editNodeset, toCoordinates, pathValueLabels, nodeParameters, editGroupName) del editNodeset return False, True # settings not changed, nodes changed diff --git a/src/scaffoldmaker/meshtypes/meshtype_2d_tubebifurcation1.py b/src/scaffoldmaker/meshtypes/meshtype_2d_tubebifurcation1.py deleted file mode 100644 index 186a2a73..00000000 --- a/src/scaffoldmaker/meshtypes/meshtype_2d_tubebifurcation1.py +++ /dev/null @@ -1,227 +0,0 @@ -""" -Generates a 2-D tube bifurcation mesh. -""" - -from __future__ import division - -import math - -from cmlibs.maths.vectorops import cross, mult, normalize, sub -from cmlibs.utils.zinc.field import findOrCreateFieldCoordinates -from cmlibs.zinc.field import Field -from cmlibs.zinc.node import Node -from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base -from scaffoldmaker.utils.bifurcation import get_tube_bifurcation_connection_elements_counts, \ - make_tube_bifurcation_points, make_tube_bifurcation_elements_2d -from scaffoldmaker.utils.geometry import createCirclePoints - - -class MeshType_2d_tubebifurcation1(Scaffold_base): - ''' - Generates a 2-D tube bifurcation mesh. - ''' - - @staticmethod - def getName(): - return '2D Tube Bifurcation 1' - - @staticmethod - def getParameterSetNames(): - return ['Default'] - - @staticmethod - def getDefaultOptions(parameterSetName='Default'): - options = {} - options['Number of elements around parent'] = 6 - options['Number of elements around child 1'] = 6 - options['Number of elements around child 2'] = 6 - options['Parent diameter'] = 0.4 - options['Parent length'] = 0.5 - options['Parent length scale factor'] = 0.8 - options['Child 1 angle degrees'] = 45.0 - options['Child 1 diameter'] = 0.36 - options['Child 1 length'] = 0.5 - options['Child 1 length scale factor'] = 0.8 - options['Child 2 angle degrees'] = -45.0 - options['Child 2 diameter'] = 0.2 - options['Child 2 length'] = 0.5 - options['Child 2 length scale factor'] = 0.8 - return options - - @staticmethod - def getOrderedOptionNames(): - return [ - 'Number of elements around parent', - 'Number of elements around child 1', - 'Number of elements around child 2', - 'Parent diameter', - 'Parent length', - 'Parent length scale factor', - 'Child 1 angle degrees', - 'Child 1 diameter', - 'Child 1 length', - 'Child 1 length scale factor', - 'Child 2 angle degrees', - 'Child 2 diameter', - 'Child 2 length', - 'Child 2 length scale factor'] - - @staticmethod - def checkOptions(options): - ''' - :return: True if dependent options changed, otherwise False. This - happens where two or more options must change together to be valid. - ''' - dependentChanges = False - for key in [ - 'Number of elements around parent', - 'Number of elements around child 1', - 'Number of elements around child 2']: - if options[key] < 4: - options[key] = 4 - paCount = options['Number of elements around parent'] - c1Count = options['Number of elements around child 1'] - c2Count = options['Number of elements around child 2'] - if (paCount + c1Count + c2Count) % 2: - c2Count += 1 - pac1Count, c1c2Count, pac2Count = get_tube_bifurcation_connection_elements_counts([paCount, c1Count, c2Count]) - if pac1Count < 2: - c2Count -= 2*(2 - pac1Count) - elif pac2Count < 2: - c2Count += 2*(2 - pac2Count) - elif c1c2Count < 2: - c2Count += 2*(2 - c1c2Count) - if c2Count != options['Number of elements around child 2']: - options['Number of elements around child 2'] = c2Count - dependentChanges = True - return dependentChanges - - @classmethod - def generateBaseMesh(cls, region, options): - """ - Generate the base bicubic Hermite mesh. - :param region: Zinc region to define model in. Must be empty. - :param options: Dict containing options. See getDefaultOptions(). - :return: list of AnnotationGroup, None - """ - paCount = options['Number of elements around parent'] - c1Count = options['Number of elements around child 1'] - c2Count = options['Number of elements around child 2'] - parentRadius = 0.5*options['Parent diameter'] - parentLength = options['Parent length'] - parentLengthScaleFactor = options['Parent length scale factor'] - child1AngleRadians = math.radians(options['Child 1 angle degrees']) - child1Radius = 0.5*options['Child 1 diameter'] - child1Length = options['Child 1 length'] - child1LengthScaleFactor = options['Child 1 length scale factor'] - child2AngleRadians = math.radians(options['Child 2 angle degrees']) - child2Radius = 0.5*options['Child 2 diameter'] - child2Length = options['Child 2 length'] - child2LengthScaleFactor = options['Child 2 length scale factor'] - useCrossDerivatives = False - - # centres: - paCentre = [ 0.0, 0.0, -parentLength ] - c1Centre = [ child1Length*math.sin(child1AngleRadians), 0.0, child1Length*math.cos(child1AngleRadians) ] - c2Centre = [ child2Length*math.sin(child2AngleRadians), 0.0, child2Length*math.cos(child2AngleRadians) ] - c12 = sub(c1Centre, c2Centre) - - pac1Count, c1c2Count, pac2Count = get_tube_bifurcation_connection_elements_counts([paCount, c1Count, c2Count]) - - # parent ring - paAxis3 = [ 0.0, 0.0, parentLength ] - paAxis2 = mult(normalize(cross(paAxis3, c12)), parentRadius) - paAxis1 = mult(normalize(cross(paAxis2, paAxis3)), parentRadius) - paStartRadians = -math.pi*(pac1Count/paCount) - pax, pad1 = createCirclePoints(paCentre, paAxis1, paAxis2, paCount, paStartRadians) - pad2 = [ mult(paAxis3, parentLengthScaleFactor) ]*paCount - # child 1 ring - c1Axis3 = c1Centre - c1Axis2 = mult(normalize(cross(c1Axis3, c12)), child1Radius) - c1Axis1 = mult(normalize(cross(c1Axis2, c1Axis3)), child1Radius) - c1StartRadians = -math.pi*(pac1Count/c1Count) - c1x, c1d1 = createCirclePoints(c1Centre, c1Axis1, c1Axis2, c1Count, c1StartRadians) - c1d2 = [ mult(c1Axis3, child1LengthScaleFactor) ]*c1Count - # child 2 ring - c2Axis3 = c2Centre - c2Axis2 = mult(normalize(cross(c2Axis3, c12)), child2Radius) - c2Axis1 = mult(normalize(cross(c2Axis2, c2Axis3)), child2Radius) - c2StartRadians = -math.pi*(c1c2Count/c2Count) - c2x, c2d1 = createCirclePoints(c2Centre, c2Axis1, c2Axis2, c2Count, c2StartRadians) - c2d2 = [ mult(c2Axis3, child2LengthScaleFactor) ]*c2Count - - rox, rod1, rod2, cox, cod1, cod2, paStartIndex, c1StartIndex, c2StartIndex = \ - make_tube_bifurcation_points(paCentre, pax, pad2, c1Centre, c1x, c1d2, c2Centre, c2x, c2d2) - - fm = region.getFieldmodule() - coordinates = findOrCreateFieldCoordinates(fm) - cache = fm.createFieldcache() - - nodes = fm.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - - ############## - # Create nodes - ############## - - nodeIdentifier = 1 - - nodetemplate = nodes.createNodetemplate() - nodetemplate.defineField(coordinates) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_VALUE, 1) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS1, 1) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS2, 1) - paNodeId = [] - for n in range(paCount): - node = nodes.createNode(nodeIdentifier, nodetemplate) - cache.setNode(node) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_VALUE, 1, pax [n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS1, 1, pad1[n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS2, 1, pad2[n]) - paNodeId.append(nodeIdentifier) - nodeIdentifier = nodeIdentifier + 1 - roNodeId = [] - for n in range(len(rox)): - node = nodes.createNode(nodeIdentifier, nodetemplate) - cache.setNode(node) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_VALUE, 1, rox [n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS1, 1, rod1[n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS2, 1, rod2[n]) - roNodeId.append(nodeIdentifier) - nodeIdentifier = nodeIdentifier + 1 - coNodeId = [] - for n in range(len(cox)): - node = nodes.createNode(nodeIdentifier, nodetemplate) - cache.setNode(node) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_VALUE, 1, cox [n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS1, 1, cod1[n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS2, 1, cod2[n]) - coNodeId.append(nodeIdentifier) - nodeIdentifier = nodeIdentifier + 1 - c1NodeId = [] - for n in range(c1Count): - node = nodes.createNode(nodeIdentifier, nodetemplate) - cache.setNode(node) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_VALUE, 1, c1x [n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS1, 1, c1d1[n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS2, 1, c1d2[n]) - c1NodeId.append(nodeIdentifier) - nodeIdentifier = nodeIdentifier + 1 - c2NodeId = [] - for n in range(c2Count): - node = nodes.createNode(nodeIdentifier, nodetemplate) - cache.setNode(node) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_VALUE, 1, c2x [n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS1, 1, c2d1[n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS2, 1, c2d2[n]) - c2NodeId.append(nodeIdentifier) - nodeIdentifier = nodeIdentifier + 1 - - ################# - # Create elements - ################# - - elementIdentifier = 1 - elementIdentifier = make_tube_bifurcation_elements_2d(region, coordinates, elementIdentifier, - paNodeId, paStartIndex, c1NodeId, c1StartIndex, c2NodeId, c2StartIndex, roNodeId, coNodeId, useCrossDerivatives) - - return [], None diff --git a/src/scaffoldmaker/meshtypes/meshtype_2d_tubebifurcationtree1.py b/src/scaffoldmaker/meshtypes/meshtype_2d_tubebifurcationtree1.py deleted file mode 100644 index dbacf66b..00000000 --- a/src/scaffoldmaker/meshtypes/meshtype_2d_tubebifurcationtree1.py +++ /dev/null @@ -1,147 +0,0 @@ -""" -Generates a 2-D tube bifurcation mesh. -""" - -from __future__ import division -import math - -from cmlibs.utils.zinc.field import findOrCreateFieldCoordinates -from cmlibs.zinc.field import Field -from cmlibs.zinc.node import Node -from scaffoldmaker.meshtypes.meshtype_1d_bifurcationtree1 import MeshType_1d_bifurcationtree1 -from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base -from scaffoldmaker.scaffoldpackage import ScaffoldPackage -from scaffoldmaker.utils.bifurcation import get_curve_circle_points -from scaffoldmaker.utils.interpolation import getCubicHermiteArcLength - - -class MeshType_2d_tubebifurcationtree1(Scaffold_base): - ''' - Generates a 2-D tube bifurcation tree mesh. - ''' - - @staticmethod - def getName(): - return '2D Tube Bifurcation Tree 1' - - @staticmethod - def getParameterSetNames(): - return ['Default'] - - @staticmethod - def getDefaultOptions(parameterSetName='Default'): - options = {} - options['Bifurcation tree'] = ScaffoldPackage(MeshType_1d_bifurcationtree1) - options['Number of elements around root'] = 6 - options['Maximum element length'] = 0.5 - return options - - @staticmethod - def getOrderedOptionNames(): - return [ - 'Bifurcation tree', - 'Number of elements around root', - 'Maximum element length'] - - @classmethod - def getOptionValidScaffoldTypes(cls, optionName): - if optionName == 'Bifurcation tree': - return [ MeshType_1d_bifurcationtree1 ] - 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() - - @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 == 'Bifurcation tree': - if not parameterSetName: - parameterSetName = MeshType_1d_bifurcationtree1.getParameterSetNames()[0] - return ScaffoldPackage(MeshType_1d_bifurcationtree1, defaultParameterSetName=parameterSetName) - assert False, cls.__name__ + '.getOptionScaffoldPackage: Option ' + optionName + ' is not a scaffold' - - @staticmethod - def checkOptions(options): - ''' - :return: True if dependent options changed, otherwise False. This - happens where two or more options must change together to be valid. - ''' - dependentChanges = False - for key in [ - 'Number of elements around root']: - if options[key] < 4: - options[key] = 4 - if options['Maximum element length'] <= 0.0: - options['Maximum element length'] = 1.0 - return dependentChanges - - @classmethod - def generateBaseMesh(cls, region, options): - """ - Generate the base bicubic Hermite mesh. - :param region: Zinc region to define model in. Must be empty. - :param options: Dict containing options. See getDefaultOptions(). - :return: [] empty list of AnnotationGroup, None - """ - bifurcationTreeScaffold = options['Bifurcation tree'] - maxElementLength = options['Maximum element length'] - elementsCountAroundRoot = options['Number of elements around root'] - useCrossDerivatives = False - - fm = region.getFieldmodule() - coordinates = findOrCreateFieldCoordinates(fm) - cache = fm.createFieldcache() - - ############## - # Create nodes - ############## - - bifurcationTree = bifurcationTreeScaffold.getScaffoldType().generateBifurcationTree(bifurcationTreeScaffold.getScaffoldSettings()) - nodeIdentifier = bifurcationTree.generateZincModel(region)[0] - - nodes = fm.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - nodetemplate = nodes.createNodetemplate() - nodetemplate.defineField(coordinates) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_VALUE, 1) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS1, 1) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS2, 1) - - rootNode = bifurcationTree.getRootNode() - child1 = rootNode.getChild(0) - - x1, xd1, r1, x2, xd2, r2 = child1.getChildCurve(0) - rd1 = rd2 = (r2 - r1) - - curveLength = getCubicHermiteArcLength(x1, xd1, x2, xd2) - elementsCount = max(2, math.ceil(curveLength/maxElementLength)) - elementLength = curveLength/elementsCount - side = [ 1.0, 0.0, 0.0 ] - for i in range(elementsCount + 1): - xi = i/elementsCount - x, d1, d2 = get_curve_circle_points(x1, xd1, x2, xd2, r1, rd1, r2, rd2, xi, elementLength, side, elementsCountAroundRoot) - for n in range(elementsCountAroundRoot): - #print('node',nodeIdentifier,i,n,x[n],d1[n],d2[n]) - node = nodes.createNode(nodeIdentifier, nodetemplate) - cache.setNode(node) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_VALUE, 1, x [n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS1, 1, d1[n]) - coordinates.setNodeParameters(cache, -1, Node.VALUE_LABEL_D_DS2, 1, d2[n]) - nodeIdentifier = nodeIdentifier + 1 - - ################# - # Create elements - ################# - - elementIdentifier = 1 - - return [], None diff --git a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py index 3d6e2315..f5385220 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py @@ -1,25 +1,19 @@ """ Generates a 2-D Hermite bifurcating tube network. """ - -import copy - -from cmlibs.utils.zinc.field import find_or_create_field_coordinates -from cmlibs.utils.zinc.finiteelement import get_maximum_element_identifier, get_maximum_node_identifier -from cmlibs.zinc.field import Field 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.bifurcation import generateTubeBifurcationTree +from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData class MeshType_2d_tubenetwork1(Scaffold_base): """ - Generates a 2-D hermite bifurcating tube network. + Generates a 2-D hermite tube network. """ - @staticmethod - def getName(): + @classmethod + def getName(cls): return "2D Tube Network 1" @classmethod @@ -33,7 +27,6 @@ def getDefaultOptions(cls, parameterSetName="Default"): "Elements count around": 8, "Annotation elements counts around": [0], "Target element density along longest segment": 4.0, - "Serendipity": True, "Show trim surfaces": False } return options @@ -45,7 +38,6 @@ def getOrderedOptionNames(): "Elements count around", "Annotation elements counts around", "Target element density along longest segment", - "Serendipity", "Show trim surfaces" ] @@ -99,37 +91,32 @@ def checkOptions(cls, options): dependentChanges = False return dependentChanges - @staticmethod - def generateBaseMesh(region, options): + @classmethod + def generateBaseMesh(cls, region, options): """ - Generate the base hermite-bilinear mesh. See also generateMesh(). + Generate the base bicubic hermite mesh. See also generateMesh(). :param region: Zinc region to define model in. Must be empty. :param options: Dict containing options. See getDefaultOptions(). :return: list of AnnotationGroup, None """ - networkLayout = options["Network layout"] - elementsCountAround = options["Elements count around"] - annotationElementsCountsAround = options["Annotation elements counts around"] - targetElementDensityAlongLongestSegment = options["Target element density along longest segment"] - serendipity = options["Serendipity"] - layoutRegion = region.createRegion() + networkLayout = options["Network layout"] networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters - layoutAnnotationGroups = networkLayout.getAnnotationGroups() - - fieldmodule = region.getFieldmodule() - nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - coordinates = find_or_create_field_coordinates(fieldmodule) - nodeIdentifier = max(get_maximum_node_identifier(nodes), 0) + 1 - mesh = fieldmodule.findMeshByDimension(2) - elementIdentifier = max(get_maximum_element_identifier(mesh), 0) + 1 - networkMesh = networkLayout.getConstructionObject() - nodeIdentifier, elementIdentifier, annotationGroups = generateTubeBifurcationTree( - networkMesh, region, coordinates, nodeIdentifier, elementIdentifier, - elementsCountAround, targetElementDensityAlongLongestSegment, 1, - layoutAnnotationGroups, annotationElementsCountsAround, - serendipity=serendipity, showTrimSurfaces=options["Show trim surfaces"]) + tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( + networkMesh, + targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], + defaultElementsCountAround=options["Elements count around"], + elementsCountThroughWall=1, + layoutAnnotationGroups=networkLayout.getAnnotationGroups(), + annotationElementsCountsAround=options["Annotation elements counts around"]) + tubeNetworkMeshBuilder.build() + generateData = TubeNetworkMeshGenerateData( + region, 2, + isLinearThroughWall=True, + isShowTrimSurfaces=options["Show trim surfaces"]) + tubeNetworkMeshBuilder.generateMesh(generateData) + annotationGroups = generateData.getAnnotationGroups() return annotationGroups, None diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_boxnetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_boxnetwork1.py index b097a5c9..c7ba938f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_boxnetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_boxnetwork1.py @@ -1,120 +1,10 @@ """ Generates a hermite x bilinear 3-D box network mesh from a 1-D network layout. """ -from cmlibs.maths.vectorops import add, sub -from cmlibs.utils.zinc.field import findOrCreateFieldCoordinates -from cmlibs.zinc.element import Element, Elementbasis -from cmlibs.zinc.field import Field -from cmlibs.zinc.node import Node -from scaffoldmaker.annotation.annotationgroup import AnnotationGroup 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.eft_utils import remapEftLocalNodes, remapEftNodeValueLabelVersion, setEftScaleFactorIds -from scaffoldmaker.utils.interpolation import getCubicHermiteCurvesLength, interpolateSampleCubicHermite, \ - sampleCubicHermiteCurvesSmooth -from scaffoldmaker.utils.zinc_utils import get_nodeset_path_ordered_field_parameters -import math - - -class BoxSegmentData: - """ - Definition of box mesh for network segment. - """ - - def __init__(self, pathParameters): - """ - :param pathParameters: (sx[], sd1[], sd2[], sd12[], sd3[], sd13[]) - """ - self._pathParameters = pathParameters # original parameters from network layout - self._segmentLength = getCubicHermiteCurvesLength(pathParameters[0], pathParameters[1]) - self._sampledBoxCoordinates = None - self._sampledNodeIdentifiers = [] - self._annotationMeshGroups = [] - - def getPathParameters(self): - return self._pathParameters - - def getSegmentLength(self): - return self._segmentLength - - def getSampledBoxCoordinates(self): - return self._sampledBoxCoordinates - - def setSampledBoxCoordinates(self, sampledBoxCoordinates): - """ - :param sampledBoxCoordinates: (sx[], sd1[], sd2[], sd12[], sd3[], sd13[]) - """ - self._sampledBoxCoordinates = sampledBoxCoordinates - self._sampledNodeIdentifiers = [None] * len(self._sampledBoxCoordinates[0]) - - def getSampledElementsCountAlong(self): - """ - Must have previously called setSampledBoxCoordinates - :return: Number of elements along sampled box. - """ - return len(self._sampledBoxCoordinates[0]) - 1 - - def getSampledNodeIdentifier(self, nodeIndex): - """ - Get start node identifier for supplying to adjacent straight or junction. - :param nodeIndex: Index of sampled node from 0 to sampledElementsCountAlong, or -1 for last. - """ - return self._sampledNodeIdentifiers[nodeIndex] - - def setSampledNodeIdentifier(self, nodeIndex, nodeIdentifier): - """ - :param nodeIndex: Index of sampled node from 0 to sampledElementsCountAlong, or -1 for last. - :param nodeIdentifier: Identifier of node to set at nodeIndex. - """ - self._sampledNodeIdentifiers[nodeIndex] = nodeIdentifier - - def addAnnotationMeshGroup(self, annotationMeshGroup): - """ - Add an annotation mesh group for segment elements to be added to. - :param annotationMeshGroup: Mesh group to add. - """ - self._annotationMeshGroups.append(annotationMeshGroup) - - def getAnnotationMeshGroups(self): - return self._annotationMeshGroups - - -class BoxJunctionData: - """ - Describes junction between multiple segments, some in, some out. - """ - - def __init__(self, networkSegmentsIn: list, networkSegmentsOut: list, boxSegmentData): - """ - :param networkSegmentsIn: List of input segments. - :param networkSegmentsOut: List of output segments. - :param boxSegmentData: dict NetworkSegment -> BoxSegmentData. - """ - self._networkSegmentsIn = networkSegmentsIn - self._networkSegmentsOut = networkSegmentsOut - self._networkSegments = networkSegmentsIn + networkSegmentsOut - segmentsCount = len(self._networkSegments) - assert segmentsCount > 0 - self._boxData = [boxSegmentData[networkSegment] for networkSegment in self._networkSegments] - self._segmentsIn = [self._networkSegments[s] in self._networkSegmentsIn for s in range(segmentsCount)] - self._nodeIdentifier = None - - def getSegmentsIn(self): - return self._segmentsIn - - def getBoxData(self): - return self._boxData - - def getNodeIdentifier(self): - return self._nodeIdentifier - - def setNodeIdentifier(self, nodeIdentifier): - self._nodeIdentifier = nodeIdentifier - segmentsCount = len(self._networkSegments) - for s in range(segmentsCount): - nodeIndex = -1 if self._segmentsIn[s] else 0 - self._boxData[s].setSampledNodeIdentifier(nodeIndex, nodeIdentifier) +from scaffoldmaker.utils.boxnetworkmesh import BoxNetworkMeshBuilder, BoxNetworkMeshGenerateData class MeshType_3d_boxnetwork1(Scaffold_base): @@ -186,7 +76,7 @@ def checkOptions(cls, options): @classmethod def generateBaseMesh(cls, region, options): """ - Generate the base hermite-bilinear mesh. See also generateMesh(). + Generate hermite-bilinear mesh from network layout. :param region: Zinc region to define model in. Must be empty. :param options: Dict containing options. See getDefaultOptions(). :return: list of AnnotationGroup, None @@ -195,183 +85,15 @@ def generateBaseMesh(cls, region, options): targetElementDensityAlongLongestSegment = options["Target element density along longest segment"] layoutRegion = region.createRegion() - layoutFieldmodule = layoutRegion.getFieldmodule() - layoutNodes = layoutFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - layoutMesh = layoutFieldmodule.findMeshByDimension(1) networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters layoutAnnotationGroups = networkLayout.getAnnotationGroups() - layoutCoordinates = findOrCreateFieldCoordinates(layoutFieldmodule) - networkMesh = networkLayout.getConstructionObject() - fieldmodule = region.getFieldmodule() - fieldcache = fieldmodule.createFieldcache() - coordinates = findOrCreateFieldCoordinates(fieldmodule) - - nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - nodeIdentifier = 1 - nodetemplates = {} # map from derivativeVersionsCount to nodetemplate - - mesh = fieldmodule.findMeshByDimension(3) - elementIdentifier = 1 - hermiteBilinearBasis = fieldmodule.createElementbasis(3, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) - hermiteBilinearBasis.setFunctionType(1, Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE) - elementtemplates = {} # map from (startVersion, endVersion) to (elementtemplate, eft) - - # make box annotation groups from network layout annotations - annotationGroups = [] - layoutAnnotationMeshGroupMap = [] # List of tuples of (layout annotation mesh group, box mesh group) - for layoutAnnotationGroup in layoutAnnotationGroups: - if layoutAnnotationGroup.getDimension() == 1: - annotationGroup = AnnotationGroup(region, layoutAnnotationGroup.getTerm()) - annotationGroups.append(annotationGroup) - layoutAnnotationMeshGroupMap.append( - (layoutAnnotationGroup.getMeshGroup(layoutMesh), annotationGroup.getMeshGroup(mesh))) - - valueLabels = [ - Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, - Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D2_DS1DS2, - Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3] - - networkSegments = networkMesh.getNetworkSegments() - boxSegmentData = {} # map from NetworkSegment to BoxSegmentData - longestSegmentLength = 0.0 - for networkSegment in networkSegments: - pathParameters = get_nodeset_path_ordered_field_parameters( - layoutNodes, layoutCoordinates, valueLabels, - networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions()) - boxSegmentData[networkSegment] = boxData = BoxSegmentData(pathParameters) - segmentLength = boxData.getSegmentLength() - if segmentLength > longestSegmentLength: - longestSegmentLength = segmentLength - for layoutAnnotationMeshGroup, annotationMeshGroup in layoutAnnotationMeshGroupMap: - if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationMeshGroup): - boxData.addAnnotationMeshGroup(annotationMeshGroup) - if longestSegmentLength > 0.0: - targetElementLength = longestSegmentLength / targetElementDensityAlongLongestSegment - else: - targetElementLength = 1.0 - - # map from NetworkNodes to BoxJunctionData - boxNodeJunctionDataMap = {} - for networkSegment in networkSegments: - boxData = boxSegmentData[networkSegment] - segmentNodes = networkSegment.getNetworkNodes() - startSegmentNode = segmentNodes[0] - startBoxJunctionData = boxNodeJunctionDataMap.get(startSegmentNode) - if not startBoxJunctionData: - startInSegments = startSegmentNode.getInSegments() - startOutSegments = startSegmentNode.getOutSegments() - startBoxJunctionData = BoxJunctionData(startInSegments, startOutSegments, boxSegmentData) - boxNodeJunctionDataMap[startSegmentNode] = startBoxJunctionData - endSegmentNode = segmentNodes[-1] - endBoxJunctionData = boxNodeJunctionDataMap.get(endSegmentNode) - if not endBoxJunctionData: - endInSegments = endSegmentNode.getInSegments() - endOutSegments = endSegmentNode.getOutSegments() - endBoxJunctionData = BoxJunctionData(endInSegments, endOutSegments, boxSegmentData) - boxNodeJunctionDataMap[endSegmentNode] = endBoxJunctionData - segmentLength = boxData.getSegmentLength() - elementsCountAlong = max(1, math.ceil(segmentLength / targetElementLength)) - loop = (len(startSegmentNode.getInSegments()) == 1) and \ - (startSegmentNode.getInSegments()[0] is networkSegment) and \ - (networkSegment.getNodeVersions()[0] == networkSegment.getNodeVersions()[-1]) - if (elementsCountAlong < 2) and loop: - elementsCountAlong = 2 # at least 2 segments around loop - pathParameters = boxData.getPathParameters() - sx, sd1, pe, pxi, psf = sampleCubicHermiteCurvesSmooth( - pathParameters[0], pathParameters[1], elementsCountAlong) - sd2, sd12 = interpolateSampleCubicHermite(pathParameters[2], pathParameters[3], pe, pxi, psf) - sd3, sd13 = interpolateSampleCubicHermite(pathParameters[4], pathParameters[5], pe, pxi, psf) - boxData.setSampledBoxCoordinates((sx, sd1, sd2, sd12, sd3, sd13)) - - lastNodeIdentifier = None - for networkSegment in networkSegments: - boxData = boxSegmentData[networkSegment] - segmentNodes = networkSegment.getNetworkNodes() - layoutNodeVersions = networkSegment.getNodeVersions() - sx, sd1, sd2, sd12, sd3, sd13 = boxData.getSampledBoxCoordinates() - elementsCountAlong = boxData.getSampledElementsCountAlong() - annotationMeshGroups = boxData.getAnnotationMeshGroups() - for n in range(elementsCountAlong + 1): - thisNodeIdentifier = None - versionsCount = 1 - version = 1 - boxJunctionData = None - if n in [0, elementsCountAlong]: - nLayout = 0 if (n == 0) else -1 - segmentNode = segmentNodes[nLayout] - boxJunctionData = boxNodeJunctionDataMap[segmentNode] - thisNodeIdentifier = boxJunctionData.getNodeIdentifier() - version = layoutNodeVersions[nLayout] - versionsCount = segmentNode.getVersionsCount() - if thisNodeIdentifier is None: - nodetemplate = nodetemplates.get(versionsCount) - if not nodetemplate: - nodetemplate = nodes.createNodetemplate() - nodetemplate.defineField(coordinates) - for valueLabel in valueLabels[1:]: - nodetemplate.setValueNumberOfVersions(coordinates, -1, valueLabel, versionsCount) - nodetemplates[versionsCount] = nodetemplate - node = nodes.createNode(nodeIdentifier, nodetemplate) - if boxJunctionData: - boxJunctionData.setNodeIdentifier(nodeIdentifier) - thisNodeIdentifier = nodeIdentifier - nodeIdentifier += 1 - else: - node = nodes.findNodeByIdentifier(thisNodeIdentifier) - # note following will set shared versions of coordinates or derivatives multiple times - # future: average derivatives from all adjoining segments - fieldcache.setNode(node) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, sx[n]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, sd1[n]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, sd2[n]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sd12[n]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, sd3[n]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sd13[n]) - - if n > 0: - startVersion = layoutNodeVersions[0] if (n == 1) else 1 - templateKey = (startVersion, version) - elementtemplate_and_eft = elementtemplates.get(templateKey) - if elementtemplate_and_eft: - elementtemplate, eft = elementtemplate_and_eft - else: - eft = mesh.createElementfieldtemplate(hermiteBilinearBasis) - setEftScaleFactorIds(eft, [1], []) - ln = 1 - for n3 in range(2): - s3 = [1] if (n3 == 0) else [] - for n2 in range(2): - s2 = [1] if (n2 == 0) else [] - for n1 in range(2): - v = startVersion if (n1 == 0) else version - remapEftNodeValueLabelVersion( - eft, [ln], Node.VALUE_LABEL_VALUE, - [(Node.VALUE_LABEL_VALUE, 1, []), - (Node.VALUE_LABEL_D_DS2, v, s2), - (Node.VALUE_LABEL_D_DS3, v, s3)]) - remapEftNodeValueLabelVersion( - eft, [ln], Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS1, v, []), - (Node.VALUE_LABEL_D2_DS1DS2, v, s2), - (Node.VALUE_LABEL_D2_DS1DS3, v, s3)]) - ln += 1 - ln_map = [1, 2, 1, 2, 1, 2, 1, 2] - remapEftLocalNodes(eft, 2, ln_map) - elementtemplate = mesh.createElementtemplate() - elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) - elementtemplate.defineField(coordinates, -1, eft) - elementtemplates[templateKey] = (elementtemplate, eft) - - element = mesh.createElement(elementIdentifier, elementtemplate) - nids = [lastNodeIdentifier, thisNodeIdentifier] - element.setNodesByIdentifier(eft, nids) - element.setScaleFactors(eft, [-1.0]) - for annotationMeshGroup in annotationMeshGroups: - annotationMeshGroup.addElement(element) - elementIdentifier += 1 - - lastNodeIdentifier = thisNodeIdentifier + boxNetworkMeshBuilder = BoxNetworkMeshBuilder( + networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups) + boxNetworkMeshBuilder.build() + generateData = BoxNetworkMeshGenerateData(region) + boxNetworkMeshBuilder.generateMesh(generateData) + annotationGroups = generateData.getAnnotationGroups() return annotationGroups, None diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py index c2a2dcb6..b87b61df 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py @@ -1,25 +1,19 @@ """ Generates a 3-D Hermite bifurcating tube network. """ - -import copy - -from cmlibs.utils.zinc.field import find_or_create_field_coordinates -from cmlibs.utils.zinc.finiteelement import get_maximum_element_identifier, get_maximum_node_identifier -from cmlibs.zinc.field import Field 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.bifurcation import generateTubeBifurcationTree +from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData class MeshType_3d_tubenetwork1(Scaffold_base): """ - Generates a 3-D hermite bifurcating tube network, with linear basis through wall. + Generates a 3-D hermite tube network, with linear or cubic basis through wall. """ - @staticmethod - def getName(): + @classmethod + def getName(cls): return "3D Tube Network 1" @classmethod @@ -36,7 +30,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): "Elements count through wall": 1, "Annotation elements counts around": [0], "Target element density along longest segment": 4.0, - "Serendipity": True, + "Use linear through wall": False, "Show trim surfaces": False } return options @@ -49,7 +43,7 @@ def getOrderedOptionNames(): "Elements count through wall", "Annotation elements counts around", "Target element density along longest segment", - "Serendipity", + "Use linear through wall", "Show trim surfaces" ] @@ -107,38 +101,32 @@ def checkOptions(cls, options): dependentChanges = False return dependentChanges - @staticmethod - def generateBaseMesh(region, options): + @classmethod + def generateBaseMesh(cls, region, options): """ - Generate the base hermite-bilinear mesh. See also generateMesh(). + Generate the base tricubic hermite or bicubic hermite-linear mesh. See also generateMesh(). :param region: Zinc region to define model in. Must be empty. :param options: Dict containing options. See getDefaultOptions(). :return: list of AnnotationGroup, None """ - networkLayout = options["Network layout"] - elementsCountAround = options["Elements count around"] - elementsCountThroughWall = options["Elements count through wall"] - annotationElementsCountsAround = options["Annotation elements counts around"] - targetElementDensityAlongLongestSegment = options["Target element density along longest segment"] - serendipity = options["Serendipity"] - layoutRegion = region.createRegion() + networkLayout = options["Network layout"] networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters - layoutAnnotationGroups = networkLayout.getAnnotationGroups() - - fieldmodule = region.getFieldmodule() - nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - coordinates = find_or_create_field_coordinates(fieldmodule) - nodeIdentifier = max(get_maximum_node_identifier(nodes), 0) + 1 - mesh = fieldmodule.findMeshByDimension(2) - elementIdentifier = max(get_maximum_element_identifier(mesh), 0) + 1 - networkMesh = networkLayout.getConstructionObject() - nodeIdentifier, elementIdentifier, annotationGroups = generateTubeBifurcationTree( - networkMesh, region, coordinates, nodeIdentifier, elementIdentifier, - elementsCountAround, targetElementDensityAlongLongestSegment, elementsCountThroughWall, - layoutAnnotationGroups, annotationElementsCountsAround, - serendipity=serendipity, showTrimSurfaces=options["Show trim surfaces"]) + tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( + networkMesh, + targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], + defaultElementsCountAround=options["Elements count around"], + elementsCountThroughWall=options["Elements count through wall"], + layoutAnnotationGroups=networkLayout.getAnnotationGroups(), + annotationElementsCountsAround=options["Annotation elements counts around"]) + tubeNetworkMeshBuilder.build() + generateData = TubeNetworkMeshGenerateData( + region, 3, + isLinearThroughWall=options["Use linear through wall"], + isShowTrimSurfaces=options["Show trim surfaces"]) + tubeNetworkMeshBuilder.generateMesh(generateData) + annotationGroups = generateData.getAnnotationGroups() return annotationGroups, None diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus1.py index ceb33d82..03d4b730 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus1.py @@ -6,7 +6,7 @@ import copy import math -from cmlibs.maths.vectorops import add, mult +from cmlibs.maths.vectorops import add, cross, mult, normalize, sub from cmlibs.utils.zinc.field import findOrCreateFieldCoordinates from cmlibs.utils.zinc.general import ChangeManager from cmlibs.zinc.element import Element @@ -22,14 +22,58 @@ from scaffoldmaker.utils import interpolation as interp from scaffoldmaker.utils import tubemesh from scaffoldmaker.utils import vector -from scaffoldmaker.utils.bifurcation import get_tube_bifurcation_connection_elements_counts, \ - get_bifurcation_triple_point from scaffoldmaker.utils.eftfactory_bicubichermitelinear import eftfactory_bicubichermitelinear from scaffoldmaker.utils.geometry import createEllipsePoints +from scaffoldmaker.utils.interpolation import ( + computeCubicHermiteDerivativeScaling, interpolateCubicHermite, interpolateLagrangeHermiteDerivative, + smoothCubicHermiteDerivativesLine) from scaffoldmaker.utils.zinc_utils import exnode_string_from_nodeset_field_parameters from scaffoldmaker.utils.zinc_utils import get_nodeset_path_ordered_field_parameters +def get_bifurcation_triple_point(p1x, p1d, p2x, p2d, p3x, p3d): + """ + Get coordinates and derivatives of triple point between p1, p2 and p3 with derivatives. + :param p1x..p3d: Point coordinates and derivatives, numbered anticlockwise around triple point. + All derivatives point away from triple point. + Returned d1 points from triple point to p2, d2 points from triple point to p3. + :return: x, d1, d2 + """ + scaling12 = computeCubicHermiteDerivativeScaling(p1x, [-d for d in p1d], p2x, p2d) + scaling23 = computeCubicHermiteDerivativeScaling(p2x, [-d for d in p2d], p3x, p3d) + scaling31 = computeCubicHermiteDerivativeScaling(p3x, [-d for d in p3d], p1x, p1d) + trx1 = interpolateCubicHermite(p1x, mult(p1d, -scaling12), p2x, mult(p2d, scaling12), 0.5) + trx2 = interpolateCubicHermite(p2x, mult(p2d, -scaling23), p3x, mult(p3d, scaling23), 0.5) + trx3 = interpolateCubicHermite(p3x, mult(p3d, -scaling31), p1x, mult(p1d, scaling31), 0.5) + trx = [(trx1[c] + trx2[c] + trx3[c]) / 3.0 for c in range(3)] + td1 = interpolateLagrangeHermiteDerivative(trx, p1x, p1d, 0.0) + td2 = interpolateLagrangeHermiteDerivative(trx, p2x, p2d, 0.0) + td3 = interpolateLagrangeHermiteDerivative(trx, p3x, p3d, 0.0) + n12 = cross(td1, td2) + n23 = cross(td2, td3) + n31 = cross(td3, td1) + norm = normalize([(n12[c] + n23[c] + n31[c]) for c in range(3)]) + sd1 = smoothCubicHermiteDerivativesLine([trx, p1x], [normalize(cross(norm, cross(td1, norm))), p1d], + fixStartDirection=True, fixEndDerivative=True)[0] + sd2 = smoothCubicHermiteDerivativesLine([trx, p2x], [normalize(cross(norm, cross(td2, norm))), p2d], + fixStartDirection=True, fixEndDerivative=True)[0] + sd3 = smoothCubicHermiteDerivativesLine([trx, p3x], [normalize(cross(norm, cross(td3, norm))), p3d], + fixStartDirection=True, fixEndDerivative=True)[0] + trd1 = mult(sub(sd2, add(sd3, sd1)), 0.5) + trd2 = mult(sub(sd3, add(sd1, sd2)), 0.5) + return trx, trd1, trd2 + + +def get_tube_bifurcation_connection_elements_counts(tCounts): + """ + Get number of elements directly connecting tubes 1, 2 and 3 from the supplied number around. + :param tCounts: Number of elements around tubes in order. + :return: List of elements connect tube with its next neighbour, looping back to first. + """ + assert len(tCounts) == 3 + return [(tCounts[i] + tCounts[i - 2] - tCounts[i - 1]) // 2 for i in range(3)] + + class MeshType_3d_uterus1(Scaffold_base): """ Generates a 3-D uterus mesh from a 1-D network layout with variable numbers of elements around, along and through diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py index 6ad25f28..1ac0b69f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py @@ -2,26 +2,125 @@ Generates a 3-D uterus mesh from a 1-D network layout, with variable numbers of elements around, along and through wall. """ - -import math - from cmlibs.utils.zinc.field import findOrCreateFieldCoordinates -from cmlibs.utils.zinc.finiteelement import get_element_node_identifiers -from cmlibs.utils.zinc.general import ChangeManager from cmlibs.zinc.element import Element from cmlibs.zinc.field import Field from cmlibs.zinc.node import Node -from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, getAnnotationGroupForTerm, \ - findOrCreateAnnotationGroupForTerm +from scaffoldmaker.annotation.annotationgroup import getAnnotationGroupForTerm, findOrCreateAnnotationGroupForTerm from scaffoldmaker.annotation.uterus_terms import get_uterus_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.bifurcation import SegmentTubeData, \ - TubeBifurcationData, generateTube, generateTubeBifurcation, generateCurveMesh, blendNetworkNodeCoordinates -from scaffoldmaker.utils.networkmesh import getPathRawTubeCoordinates, resampleTubeCoordinates -from scaffoldmaker.utils.zinc_utils import exnode_string_from_nodeset_field_parameters -from scaffoldmaker.utils.zinc_utils import get_nodeset_path_ordered_field_parameters +from scaffoldmaker.utils.networkmesh import NetworkMesh +from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData +from scaffoldmaker.utils.zinc_utils import exnode_string_from_nodeset_field_parameters, group_add_connected_elements + + +class UterusTubeNetworkMeshGenerateData(TubeNetworkMeshGenerateData): + + def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurfaces, + coordinateFieldName="coordinates", startNodeIdentifier=1, startElementIdentifier=1): + """ + :param isLinearThroughWall: Callers should only set if 3-D with no core. + :param isShowTrimSurfaces: Tells junction generateMesh to make 2-D trim surfaces. + """ + super(UterusTubeNetworkMeshGenerateData, self).__init__( + region, meshDimension, isLinearThroughWall, isShowTrimSurfaces, + coordinateFieldName, startNodeIdentifier, startElementIdentifier) + self._fundusGroup = self.getOrCreateAnnotationGroup(get_uterus_term("fundus of uterus")) + self._leftGroup = self.getOrCreateAnnotationGroup(("left uterus", "None")) + self._rightGroup = self.getOrCreateAnnotationGroup(("right uterus", "None")) + self._dorsalGroup = self.getOrCreateAnnotationGroup(("dorsal uterus", "None")) + self._ventralGroup = self.getOrCreateAnnotationGroup(("ventral uterus", "None")) + + def getFundusMeshGroup(self): + return self._fundusGroup.getMeshGroup(self._mesh) + + def getLeftMeshGroup(self): + return self._leftGroup.getMeshGroup(self._mesh) + + def getRightMeshGroup(self): + return self._rightGroup.getMeshGroup(self._mesh) + + def getDorsalMeshGroup(self): + return self._dorsalGroup.getMeshGroup(self._mesh) + + def getVentralMeshGroup(self): + return self._ventralGroup.getMeshGroup(self._mesh) + +class UterusTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): + + def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, + defaultElementsCountAround: int, elementsCountThroughWall: int, + layoutAnnotationGroups: list = [], annotationElementsCountsAround: list = []): + super(UterusTubeNetworkMeshBuilder, self).__init__( + networkMesh, targetElementDensityAlongLongestSegment, defaultElementsCountAround, + elementsCountThroughWall, layoutAnnotationGroups, annotationElementsCountsAround) + + def generateMesh(self, generateData): + super(UterusTubeNetworkMeshBuilder, self).generateMesh(generateData) + # build temporary left/right dorsal/ventral groups + mesh = generateData.getMesh() + fundusMeshGroup = generateData.getFundusMeshGroup() + leftMeshGroup = generateData.getLeftMeshGroup() + rightMeshGroup = generateData.getRightMeshGroup() + dorsalMeshGroup = generateData.getDorsalMeshGroup() + ventralMeshGroup = generateData.getVentralMeshGroup() + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + elementsCountRim = segment.getElementsCountRim() + elementsCountAlong = segment.getSampledElementsCountAlong() + junctions = segment.getJunctions() + elementsCountAround = segment.getElementsCountAround() + annotationTerms = segment.getAnnotationTerms() + preBifurcation = ("pre-bifurcation segments", "None") in annotationTerms + allLeft = False + allRight = False + fundus = preBifurcation and (get_uterus_term("body of uterus") in annotationTerms) + if preBifurcation: + for annotationTerm in annotationTerms: + if "left" in annotationTerm[0]: + allLeft = True + break + elif "right" in annotationTerm[0]: + allRight = True + break + if not (allLeft or allRight): + prevSegment = junctions[0].getSegments()[0] + if prevSegment is not segment: + prevAnnotationTerms = prevSegment.getAnnotationTerms() + for annotationTerm in prevAnnotationTerms: + if "left" in annotationTerm[0]: + allLeft = True + break + elif "right" in annotationTerm[0]: + allRight = True + break + e1FundusLimit = elementsCountAround // 2 + e1RightStart = elementsCountAround // 2 + e1DVStart = e1RightStart // 2 + e1DVEnd = elementsCountAround - e1DVStart - 1 + for e1 in range(elementsCountAround): + meshGroups = [] + if fundus and ((allLeft and (e1 < e1FundusLimit)) or (allRight and (e1 >= e1FundusLimit))): + meshGroups.append(fundusMeshGroup) + if allLeft or ((not allRight) and (e1 < e1RightStart)): + meshGroups.append(leftMeshGroup) + else: + meshGroups.append(rightMeshGroup) + if ((preBifurcation and (e1 >= e1DVStart) and (e1 <= e1DVEnd)) or + ((not preBifurcation) and ((e1 < e1DVStart) or (e1 > e1DVEnd)))): + meshGroups.append(dorsalMeshGroup) + else: + meshGroups.append(ventralMeshGroup) + for e2 in range(elementsCountAlong): + for e3 in range(elementsCountRim): + elementIdentifier = segment.getRimElementId(e1, e2, e3) + if elementIdentifier is not None: + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + def getDefaultNetworkLayoutScaffoldPackage(cls, parameterSetName): assert parameterSetName in cls.getParameterSetNames() # make sure parameter set is in list of parameters of parent scaffold @@ -33,43 +132,53 @@ def getDefaultNetworkLayoutScaffoldPackage(cls, parameterSetName): }, 'meshEdits': exnode_string_from_nodeset_field_parameters( ["coordinates", "inner coordinates"], - [Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D2_DS1DS2, Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3], [[ - (1, [[-9.16, 10.27,-3.09], [ 0.89, 0.73,-1.15], [ 0.04,-0.23,-0.11], [ 0.08, 0.02, 0.01], [-0.20, 0.03,-0.14], [ 0.08,-0.06,-0.09]]), - (2, [[-8.06, 10.64,-4.32], [ 1.27,-0.02,-1.26], [-0.00,-0.25, 0.00], [-0.10, 0.01, 0.14], [-0.17,-0.00,-0.17], [-0.02,-0.00, 0.02]]), - (3, [[-6.71, 10.19,-5.51], [ 1.73,-0.52,-0.85], [-0.06,-0.25, 0.03], [-0.11,-0.25,-0.05], [-0.12,-0.00,-0.24], [ 0.12, 0.01,-0.37]]), - (4, [[ 9.16, 10.27,-3.09], [-0.89, 0.73,-1.15], [-0.04,-0.23,-0.11], [-0.08, 0.02, 0.01], [-0.20,-0.03, 0.14], [ 0.08, 0.06, 0.09]]), - (5, [[ 8.06, 10.64,-4.32], [-1.27,-0.02,-1.26], [ 0.00,-0.25, 0.00], [ 0.10, 0.01, 0.14], [-0.17, 0.00, 0.17], [-0.02, 0.00,-0.02]]), - (6, [[ 6.71, 10.19,-5.51], [-1.73,-0.52,-0.85], [ 0.06,-0.25, 0.03], [ 0.11,-0.25,-0.05], [-0.12, 0.00, 0.24], [ 0.12,-0.01, 0.37]]), - (7, [[-4.71, 9.65,-5.89], [ 2.18,-0.21,-0.35], [-0.09,-0.77,-0.10], [ 0.27,-1.14,-0.57], [-0.11, 0.11,-0.78], [-0.31, 0.46,-0.76]]), - (8, [[-2.42, 9.80,-6.20], [ 2.36, 0.21,-0.17], [ 0.13,-2.36,-1.07], [ 0.04,-0.93,-0.64], [-0.20, 0.80,-1.80], [ 0.22, 0.68,-0.92]]), - (9, [[ 4.71, 9.65,-5.89], [-2.18,-0.21,-0.35], [ 0.09,-0.77,-0.10], [-0.27,-1.14,-0.57], [-0.11,-0.11, 0.78], [-0.31,-0.46, 0.76]]), - (10, [[ 2.42, 9.80,-6.20], [-2.36, 0.21,-0.17], [-0.13,-2.36,-1.07], [-0.04,-0.93,-0.64], [-0.20,-0.80, 1.80], [ 0.22,-0.68, 0.92]]), - (11, [[-0.00, 10.08,-6.23], [[ 2.47, 0.34, 0.11],[-2.47, 0.34, 0.11],[ 0.02,-0.35, 2.81]], [[ 0.42,-2.59,-1.43],[-0.42,-2.59,-1.43],[ 0.03, 2.93, 0.36]], [[ 0.19, 0.45,-0.09],[-0.19, 0.45,-0.09],[ 0.01,-0.22,-0.05]], [[-0.08, 1.42,-2.59],[-0.08,-1.42, 2.59],[-3.52, 0.03, 0.03]], [[-0.16, 0.54,-0.68],[-0.16,-0.54, 0.68],[ 0.47, 0.03,-0.12]]]), - (12, [[-0.00, 9.50,-3.62], [ 0.00,-0.81, 2.39], [ 0.02, 2.37, 0.80], [-0.02,-0.82, 0.29], [-3.08, 0.02, 0.01], [ 0.41,-0.03, 0.02]]), - (13, [[-0.00, 8.50,-1.48], [ 0.00,-1.16, 1.77], [-0.01, 1.33, 0.87], [-0.01,-0.82, 0.10], [-2.70,-0.01,-0.01], [ 0.29,-0.01,-0.01]]), - (14, [[-0.00, 7.27,-0.08], [ 0.00,-1.09, 0.82], [ 0.00, 0.75, 0.99], [ 0.01,-0.49, 0.09], [-2.51, 0.00, 0.00], [ 0.18, 0.01, 0.01]]), - (15, [[-0.00, 6.50, 0.28], [ 0.00,-0.83, 0.18], [ 0.00, 0.22, 1.00], [ 0.00,-0.30,-0.20], [-2.34, 0.00, 0.00], [ 0.22, 0.00, 0.00]]), - (16, [[-0.00, 5.67, 0.28], [ 0.00,-1.58,-0.05], [ 0.00,-0.03, 0.81], [ 0.00, 0.11,-0.26], [-2.07,-0.00, 0.00], [ 0.63, 0.00, 0.00]]), - (17, [[-0.00, 3.35, 0.14], [ 0.00,-2.85,-0.14], [ 0.00,-0.02, 0.46], [ 0.00, 0.02,-0.13], [-1.08,-0.00, 0.00], [ 0.76, 0.00, 0.00]]), - (18, [[ 0.00,-0.03, 0.00], [ 0.00,-3.91,-0.13], [ 0.00,-0.02, 0.55], [-0.00,-0.01, 0.31], [-0.55,-0.00, 0.00], [ 0.30,-0.00, 0.00]])], [ - (1, [[-9.16,10.27,-3.09], [0.89,0.73,-1.15], [0.02,-0.11,-0.06], [0.04,0.01,0.00], [-0.10,0.02,-0.07], [0.04,-0.03,-0.05]]), - (2, [[-8.06,10.64,-4.32], [1.27,-0.02,-1.26], [0.00,-0.12,0.00], [-0.05,0.00,0.07], [-0.08,0.00,-0.09], [-0.01,0.00,0.01]]), - (3, [[-6.71,10.19,-5.51], [1.73,-0.52,-0.85], [-0.03,-0.13,0.01], [-0.05,-0.13,-0.03], [-0.06,0.00,-0.12], [0.06,0.00,-0.19]]), - (4, [[9.16,10.27,-3.09], [-0.89,0.73,-1.15], [-0.02,-0.11,-0.06], [-0.04,0.01,0.00], [-0.10,-0.02,0.07], [0.04,0.03,0.05]]), - (5, [[8.06,10.64,-4.32], [-1.27,-0.02,-1.26], [0.00,-0.12,0.00], [0.05,0.00,0.07], [-0.08,0.00,0.09], [-0.01,0.00,-0.01]]), - (6, [[6.71,10.19,-5.51], [-1.73,-0.52,-0.85], [0.03,-0.13,0.01], [0.05,-0.13,-0.03], [-0.06,0.00,0.12], [0.06,0.00,0.19]]), - (7, [[-4.71,9.65,-5.89], [2.18,-0.21,-0.35], [-0.05,-0.38,-0.05], [0.13,-0.57,-0.28], [-0.06,0.06,-0.39], [-0.15,0.23,-0.38]]), - (8, [[-2.42,9.80,-6.20], [2.36,0.21,-0.17], [0.07,-1.18,-0.54], [0.02,-0.53,-0.26], [-0.10,0.40,-0.90], [0.11,0.27,-0.49]]), - (9, [[4.71,9.65,-5.89], [-2.18,-0.21,-0.35], [0.05,-0.38,-0.05], [-0.13,-0.57,-0.28], [-0.06,-0.06,0.39], [-0.15,-0.23,0.38]]), - (10, [[2.42,9.80,-6.20], [-2.36,0.21,-0.17], [-0.07,-1.18,-0.54], [-0.02,-0.53,-0.26], [-0.10,-0.40,0.90], [0.11,-0.27,0.49]]), - (11, [[-0.00,10.08,-6.23], [[2.47,0.34,0.11],[-2.47,0.34,0.11],[0.02,-0.35,2.81]], [[0.21,-1.29,-0.72],[-0.21,-1.29,-0.72],[0.01,1.46,0.18]], [[0.09,0.23,-0.05],[-0.09,0.23,-0.05],[0.00,-0.11,-0.02]], [[-0.04,0.71,-1.30],[-0.04,-0.71,1.30],[-1.76,0.01,0.01]], [[-0.08,0.27,-0.34],[-0.08,-0.27,0.34],[0.23,0.02,-0.06]]]), (12, [[-0.00,9.50,-3.62], [0.00,-0.81,2.39], [0.01,1.18,0.40], [-0.01,-0.38,0.15], [-1.54,0.01,0.00], [0.20,-0.01,0.01]]), - (13, [[-0.00,8.50,-1.48], [0.00,-1.16,1.77], [-0.00,0.67,0.44], [0.00,-0.39,0.06], [-1.35,0.00,0.00], [0.59,0.00,0.00]]), - (14, [[-0.00,7.27,-0.08], [0.00,-1.09,0.82], [0.02,0.40,0.53], [0.00,-0.26,0.06], [-0.36,0.01,0.01], [0.47,0.00,0.00]]), - (15, [[-0.00,6.50,0.28], [0.00,-0.83,0.18], [-0.00,0.11,0.50], [-0.01,-0.15,-0.12], [-0.41,0.00,0.00], [0.00,0.00,0.00]]), - (16, [[-0.00,5.67,0.28], [0.00,-1.58,-0.05], [0.00,-0.01,0.41], [-0.00,0.06,-0.13], [-0.36,0.00,0.00], [-0.07,0.00,0.00]]), - (17, [[-0.00,3.35,0.14], [0.00,-2.85,-0.14], [0.00,-0.01,0.23], [-0.00,0.01,-0.06], [-0.54,0.00,0.00], [0.04,0.00,0.00]]), - (18, [[0.00,-0.03,0.00], [0.00,-3.91,-0.13], [0.00,-0.01,0.28], [0.00,-0.01,0.16], [-0.28,0.00,0.00], [0.48,0.00,0.00]])]]), - + [Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D2_DS1DS2, Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3], [[ + (1, [[-9.160,10.270,-3.090], [ 0.889, 0.730,-1.149], [ 0.042,-0.228,-0.113], [ 0.083, 0.023, 0.006], [-0.202, 0.031,-0.137], [ 0.077,-0.062,-0.091]]), + (2, [[-8.060,10.640,-4.320], [ 1.271,-0.020,-1.261], [-0.002,-0.250, 0.002], [-0.103, 0.007, 0.139], [-0.169, 0.000,-0.171], [-0.024,-0.002, 0.016]]), + (3, [[-6.710,10.190,-5.510], [ 1.730,-0.520,-0.850], [-0.060,-0.250, 0.030], [-0.106,-0.253,-0.053], [-0.118, 0.000,-0.241], [ 0.118, 0.006,-0.372]]), + (4, [[ 9.160,10.270,-3.090], [-0.889, 0.730,-1.149], [-0.042,-0.228,-0.113], [-0.083, 0.023, 0.006], [-0.202,-0.031, 0.137], [ 0.077, 0.062, 0.091]]), + (5, [[ 8.060,10.640,-4.320], [-1.271,-0.020,-1.261], [ 0.002,-0.250, 0.002], [ 0.103, 0.007, 0.139], [-0.169, 0.000, 0.171], [-0.024, 0.002,-0.016]]), + (6, [[ 6.710,10.190,-5.510], [-1.730,-0.520,-0.850], [ 0.060,-0.250, 0.030], [ 0.106,-0.253,-0.053], [-0.118, 0.000, 0.241], [ 0.118,-0.006, 0.372]]), + (7, [[-4.710, 9.650,-5.890], [ 2.182,-0.210,-0.350], [-0.090,-0.770,-0.100], [ 0.319,-1.159,-0.237], [-0.114, 0.114,-0.779], [-0.339, 0.211,-0.805]]), + (8, [[-2.420, 9.800,-6.200], [ 2.364, 0.210,-0.170], [ 0.188,-2.460,-0.420], [-0.027,-1.024,-0.146], [-0.169, 0.320,-1.950], [ 0.243, 0.218,-1.088]]), + (9, [[ 4.710, 9.650,-5.890], [-2.182,-0.210,-0.350], [ 0.090,-0.770,-0.100], [-0.319,-1.159,-0.237], [-0.114,-0.114, 0.779], [-0.339,-0.211, 0.805]]), + (10, [[ 2.420, 9.800,-6.200], [-2.364, 0.210,-0.170], [-0.188,-2.460,-0.420], [ 0.027,-1.024,-0.146], [-0.169,-0.320, 1.950], [ 0.243,-0.218, 1.088]]), + (11, [[ 0.000,10.080,-6.230], + [[ 2.471, 0.340, 0.110],[-2.471, 0.340, 0.110],[ 0.000,-0.350, 2.812]], + [[ 0.400,-2.761,-0.461],[-0.400,-2.761,-0.461],[ 0.000, 2.929, 0.365]], + [[ 0.196, 0.407, 0.058],[-0.196, 0.407, 0.058],[ 0.000,-0.216,-0.046]], + [[0.062, 0.495, -2.911], [0.062, -0.495, 2.911], [-3.520, 0.000, 0.000]], + [[-0.011, 0.101,-0.853],[-0.011,-0.101, 0.853],[ 0.470, 0.000, 0.000]]]), + (12, [[ 0.000, 9.500,-3.620], [ 0.000,-0.810, 2.390], [ 0.000, 2.369, 0.803], [ 0.000,-0.819, 0.290], [-3.080, 0.000, 0.000], [ 0.410, 0.000, 0.000]]), + (13, [[ 0.000, 8.500,-1.480], [ 0.000,-1.162, 1.773], [ 0.000, 1.329, 0.871], [ 0.000,-0.823, 0.095], [-2.700, 0.000, 0.000], [ 0.285, 0.000, 0.000]]), + (14, [[ 0.000, 7.270,-0.080], [ 0.000,-1.091, 0.821], [ 0.000, 0.747, 0.993], [ 0.000,-0.491, 0.093], [-2.510, 0.000, 0.000], [ 0.180, 0.000, 0.000]]), + (15, [[ 0.000, 6.500, 0.280], [ 0.000,-0.828, 0.179], [ 0.000, 0.216, 1.001], [ 0.000,-0.302,-0.201], [-2.340, 0.000, 0.000], [ 0.220, 0.000, 0.000]]), + (16, [[ 0.000, 5.670, 0.280], [ 0.000,-1.578,-0.050], [ 0.000,-0.026, 0.810], [ 0.000, 0.109,-0.265], [-2.070, 0.000, 0.000], [ 0.630, 0.000, 0.000]]), + (17, [[ 0.000, 3.350, 0.140], [ 0.000,-2.850,-0.140], [ 0.000,-0.023, 0.460], [ 0.000, 0.021,-0.129], [-1.080, 0.000, 0.000], [ 0.760, 0.000, 0.000]]), + (18, [[ 0.000,-0.030, 0.000], [ 0.000,-3.910,-0.130], [ 0.000,-0.018, 0.550], [ 0.000,-0.014, 0.309], [-0.550, 0.000, 0.000], [ 0.300, 0.000, 0.000]])], [ + (1, [[-9.160,10.270,-3.090], [ 0.889, 0.730,-1.149], [ 0.018,-0.112,-0.057], [ 0.043, 0.018, 0.011], [-0.102, 0.018,-0.067], [ 0.040,-0.036,-0.050]]), + (2, [[-8.060,10.640,-4.320], [ 1.271,-0.020,-1.261], [-0.001,-0.120, 0.001], [-0.050, 0.000, 0.067], [-0.085, 0.000,-0.085], [-0.012, 0.000, 0.008]]), + (3, [[-6.710,10.190,-5.510], [ 1.730,-0.520,-0.850], [-0.033,-0.129, 0.012], [-0.050,-0.127,-0.029], [-0.058, 0.004,-0.121], [ 0.059, 0.004,-0.186]]), + (4, [[ 9.160,10.270,-3.090], [-0.889, 0.730,-1.149], [-0.018,-0.112,-0.057], [-0.043, 0.018, 0.011], [-0.102,-0.018, 0.067], [ 0.040, 0.036, 0.050]]), + (5, [[ 8.060,10.640,-4.320], [-1.271,-0.020,-1.261], [ 0.001,-0.120, 0.001], [ 0.050, 0.000, 0.067], [-0.085, 0.000, 0.085], [-0.012, 0.000,-0.008]]), + (6, [[ 6.710,10.190,-5.510], [-1.730,-0.520,-0.850], [ 0.033,-0.129, 0.012], [ 0.050,-0.127,-0.029], [-0.058,-0.004, 0.121], [ 0.059,-0.004, 0.186]]), + (7, [[-4.710, 9.650,-5.890], [ 2.182,-0.210,-0.350], [-0.045,-0.381,-0.051], [ 0.157,-0.594,-0.107], [-0.057, 0.059,-0.391], [-0.172, 0.094,-0.403]]), + (8, [[-2.420, 9.800,-6.200], [ 2.364, 0.210,-0.170], [ 0.098,-1.262,-0.191], [-0.014,-0.567,-0.074], [-0.083, 0.141,-0.976], [ 0.121, 0.102,-0.547]]), + (9, [[ 4.710, 9.650,-5.890], [-2.182,-0.210,-0.350], [ 0.045,-0.381,-0.051], [-0.157,-0.594,-0.107], [-0.057,-0.059, 0.391], [-0.172,-0.094, 0.403]]), + (10, [[ 2.420, 9.800,-6.200], [-2.364, 0.210,-0.170], [-0.098,-1.262,-0.191], [ 0.014,-0.567,-0.074], [-0.083,-0.141, 0.976], [ 0.121,-0.102, 0.547]]), + (11, [[ 0.000,10.080,-6.230], + [[ 2.471, 0.340, 0.110],[-2.471, 0.340, 0.110],[ 0.000,-0.350, 2.812]], + [[ 0.215,-1.485,-0.234],[-0.215,-1.485,-0.234],[ 0.000, 1.460, 0.182]], + [[ 0.122, 0.114,-0.016],[-0.122, 0.114,-0.016],[ 0.000,-0.114,-0.024]], + [[ 0.033, 0.235,-1.463],[ 0.033,-0.235, 1.463],[-1.760, 0.000, 0.000]], + [[-0.005, 0.071,-0.436],[-0.005,-0.071, 0.436],[ 0.235, 0.000, 0.000]]]), + (12, [[ 0.000, 9.500,-3.620], [ 0.000,-0.810, 2.390], [ 0.000, 1.180, 0.400], [ 0.000,-0.404, 0.146], [-1.540, 0.000, 0.000], [ 0.205, 0.000, 0.000]]), + (13, [[ 0.000, 8.500,-1.480], [ 0.000,-1.162, 1.773], [ 0.000, 0.670, 0.439], [ 0.000,-0.393, 0.063], [-1.350, 0.000, 0.000], [ 0.590, 0.000, 0.000]]), + (14, [[ 0.000, 7.270,-0.080], [ 0.000,-1.091, 0.821], [ 0.000, 0.399, 0.531], [ 0.000,-0.261, 0.055], [-0.360, 0.000, 0.000], [ 0.470, 0.000, 0.000]]), + (15, [[ 0.000, 6.500, 0.280], [ 0.000,-0.828, 0.179], [ 0.000, 0.108, 0.500], [ 0.000,-0.151,-0.120], [-0.410, 0.000, 0.000], [ 0.000, 0.000, 0.000]]), + (16, [[ 0.000, 5.670, 0.280], [ 0.000,-1.578,-0.050], [ 0.000,-0.013, 0.410], [ 0.000, 0.055,-0.132], [-0.360, 0.000, 0.000], [-0.065, 0.000, 0.000]]), + (17, [[ 0.000, 3.350, 0.140], [ 0.000,-2.850,-0.140], [ 0.000,-0.011, 0.230], [ 0.000, 0.010,-0.064], [-0.540, 0.000, 0.000], [ 0.040, 0.000, 0.000]]), + (18, [[ 0.000,-0.030, 0.000], [ 0.000,-3.910,-0.130], [ 0.000,-0.009, 0.280], [ 0.000,-0.007, 0.165], [-0.280, 0.000, 0.000], [ 0.480, 0.000, 0.000]])]]), 'userAnnotationGroups': [ { '_AnnotationGroup': True, @@ -239,12 +348,12 @@ class MeshType_3d_uterus2(Scaffold_base): Magnitude of D2 and D3 are the radii of the uterus in the respective directions. """ - @staticmethod - def getName(): + @classmethod + def getName(cls): return '3D Uterus 2' - @staticmethod - def getParameterSetNames(): + @classmethod + def getParameterSetNames(cls): return [ 'Default', 'Human 1', @@ -256,14 +365,12 @@ def getDefaultOptions(cls, parameterSetName='Default'): options = { 'Base parameter set': parameterSetName, 'Network layout': getDefaultNetworkLayoutScaffoldPackage(cls, parameterSetName), - 'Number of elements around': 10, - 'Number of elements around horns': 8, + 'Number of elements around': 12, + 'Number of elements around horns': 12, 'Number of elements through wall': 1, - "Annotation elements counts around": [0], 'Target element density along longest segment': 5.0, - "Serendipity": True, 'Use linear through wall': True, - 'Use cross derivatives': False, + 'Show trim surfaces': False, 'Refine': False, 'Refine number of elements along': 4, 'Refine number of elements around': 4, @@ -271,19 +378,21 @@ def getDefaultOptions(cls, parameterSetName='Default'): } if 'Mouse' in parameterSetName: options['Number of elements around'] = 8 + options['Number of elements around horns'] = 8 options['Target element density along longest segment'] = 10.0 return options - @staticmethod - def getOrderedOptionNames(): + @classmethod + def getOrderedOptionNames(cls): optionNames = [ 'Network layout', 'Number of elements around', 'Number of elements around horns', 'Number of elements through wall', 'Target element density along longest segment', - "Serendipity", + 'Use linear through wall', + 'Show trim surfaces', 'Refine', 'Refine number of elements along', 'Refine number of elements around', @@ -339,8 +448,8 @@ def checkOptions(cls, options): 'Number of elements around horns']: if options[key] < 4: options[key] = 4 - if options[key] % 2 > 0: - options[key] = options[key] // 2 * 2 + elif (options[key] % 4) > 0: + options[key] += options[key] % 4 if options["Number of elements through wall"] < 1: options["Number of elements through wall"] = 1 @@ -352,418 +461,57 @@ def checkOptions(cls, options): @classmethod def generateBaseMesh(cls, region, options): """ - Generate the base tricubic Hermite mesh. See also generateMesh(). + Generate the base tricubic hermite or bicubic hermite-linear mesh. See also generateMesh(). :param region: Zinc region to define model in. Must be empty. :param options: Dict containing options. See getDefaultOptions(). :return: None """ parameterSetName = options['Base parameter set'] isHuman = parameterSetName in ("Default", "Human 1") - networkLayout = options['Network layout'] - elementsCountAroundPostBifurcation = options['Number of elements around'] - elementsCountAroundPreBifurcation = options['Number of elements around horns'] - elementsCountThroughWall = options['Number of elements through wall'] - defaultElementsCountAround = elementsCountAroundPostBifurcation - annotationElementsCountsAround = [0, 0, elementsCountAroundPostBifurcation, elementsCountAroundPreBifurcation] - targetElementDensityAlongLongestSegment = options['Target element density along longest segment'] - useCrossDerivatives = options['Use cross derivatives'] - serendipity = options["Serendipity"] - showIntersectionCurves = False - showTrimSurfaces = False # options["Show trim surfaces"] - - # Geometric coordinates + layoutRegion = region.createRegion() + networkLayout = options["Network layout"] networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters layoutAnnotationGroups = networkLayout.getAnnotationGroups() - - fieldmodule = region.getFieldmodule() - nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - coordinates = findOrCreateFieldCoordinates(fieldmodule) - nodeIdentifier = 1 - elementIdentifier = 1 - networkMesh = networkLayout.getConstructionObject() - layoutRegion = networkMesh.getRegion() - layoutFieldmodule = layoutRegion.getFieldmodule() - layoutNodes = layoutFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - layoutCoordinates = layoutFieldmodule.findFieldByName("coordinates").castFiniteElement() - layoutInnerCoordinates = layoutFieldmodule.findFieldByName("inner coordinates").castFiniteElement() - if not layoutInnerCoordinates.isValid(): - layoutInnerCoordinates = None - dimension = 3 if layoutInnerCoordinates else 2 - layoutMesh = layoutFieldmodule.findMeshByDimension(1) - assert (elementsCountThroughWall == 1) or (layoutInnerCoordinates and (elementsCountThroughWall >= 1)) + annotationElementsCountsAround = [] + for layoutAnnotationGroup in layoutAnnotationGroups: + elementsCountAround = 0 + if layoutAnnotationGroup.getName() == "pre-bifurcation segments": + elementsCountAround = options['Number of elements around horns'] + elif layoutAnnotationGroup.getName() == "post-bifurcation segments": + elementsCountAround = options['Number of elements around'] + annotationElementsCountsAround.append(elementsCountAround) + + uterusTubeNetworkMeshBuilder = UterusTubeNetworkMeshBuilder( + networkMesh, + targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], + defaultElementsCountAround=options['Number of elements around'], + elementsCountThroughWall=options["Number of elements through wall"], + layoutAnnotationGroups=layoutAnnotationGroups, + annotationElementsCountsAround=annotationElementsCountsAround) + uterusTubeNetworkMeshBuilder.build() + generateData = UterusTubeNetworkMeshGenerateData( + region, 3, + isLinearThroughWall=options["Use linear through wall"], + isShowTrimSurfaces=options["Show trim surfaces"]) + uterusTubeNetworkMeshBuilder.generateMesh(generateData) + annotationGroups = generateData.getAnnotationGroups() + nodeIdentifier, _ = generateData.getNodeElementIdentifiers() fieldmodule = region.getFieldmodule() - mesh = fieldmodule.findMeshByDimension(dimension) - fieldcache = fieldmodule.createFieldcache() + coordinates = findOrCreateFieldCoordinates(fieldmodule) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + mesh = fieldmodule.findMeshByDimension(3) - # make 2D annotation groups from 1D network layout annotation groups - annotationGroups = [] - layoutAnnotationMeshGroupMap = [] # List of tuples of layout annotation mesh group to final mesh group - for layoutAnnotationGroup in layoutAnnotationGroups: - if layoutAnnotationGroup.getDimension() == 1: - annotationGroup = AnnotationGroup(region, layoutAnnotationGroup.getTerm()) - annotationGroups.append(annotationGroup) - layoutAnnotationMeshGroupMap.append( - (layoutAnnotationGroup.getMeshGroup(layoutMesh), annotationGroup.getMeshGroup(mesh))) - - valueLabels = [ - Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, - Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D2_DS1DS2, - Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3] - - networkSegments = networkMesh.getNetworkSegments() - - bodyGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_uterus_term("body of uterus")) - bodyMeshGroup = bodyGroup.getMeshGroup(mesh) - cervixGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_uterus_term("uterine cervix")) - cervixMeshGroup = cervixGroup.getMeshGroup(mesh) uterusGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_uterus_term("uterus")) + myometriumGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_uterus_term("myometrium")) + myometriumGroup.getMeshGroup(mesh).addElementsConditional(uterusGroup.getGroup()) - # map from NetworkSegment to SegmentTubeData - outerSegmentTubeData = {} - innerSegmentTubeData = {} if layoutInnerCoordinates else None - longestSegmentLength = 0.0 - for networkSegment in networkSegments: - pathParameters = get_nodeset_path_ordered_field_parameters( - layoutNodes, layoutCoordinates, valueLabels, - networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions()) - elementsCountAround = defaultElementsCountAround - i = 0 - for layoutAnnotationGroup in layoutAnnotationGroups: - if i >= len(annotationElementsCountsAround): - break - annotationElementsCountAround = annotationElementsCountsAround[i] - if annotationElementsCountAround > 0: - if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationGroup.getMeshGroup(layoutMesh)): - elementsCountAround = annotationElementsCountAround - break - i += 1 - outerSegmentTubeData[networkSegment] = tubeData = SegmentTubeData(pathParameters, elementsCountAround) - px, pd1, pd2, pd12 = getPathRawTubeCoordinates(pathParameters, elementsCountAround) - tubeData.setRawTubeCoordinates((px, pd1, pd2, pd12)) - - segmentLength = tubeData.getSegmentLength() - if segmentLength > longestSegmentLength: - longestSegmentLength = segmentLength - - if layoutInnerCoordinates: - innerPathParameters = get_nodeset_path_ordered_field_parameters( - layoutNodes, layoutInnerCoordinates, valueLabels, - networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions()) - px, pd1, pd2, pd12 = getPathRawTubeCoordinates(innerPathParameters, elementsCountAround) - innerSegmentTubeData[networkSegment] = innerTubeData = SegmentTubeData(innerPathParameters, - elementsCountAround) - innerTubeData.setRawTubeCoordinates((px, pd1, pd2, pd12)) - - for layoutAnnotationMeshGroup, annotationMeshGroup in layoutAnnotationMeshGroupMap: - if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationMeshGroup): - tubeData.addAnnotationMeshGroup(annotationMeshGroup) - if layoutInnerCoordinates: - innerTubeData.addAnnotationMeshGroup(annotationMeshGroup) - - if longestSegmentLength > 0.0: - targetElementLength = longestSegmentLength / targetElementDensityAlongLongestSegment - else: - targetElementLength = 1.0 - - # map from NetworkNodes to bifurcation data, resample tube coordinates to fit bifurcation - outerNodeTubeBifurcationData = {} - innerNodeTubeBifurcationData = {} if layoutInnerCoordinates else None - - allSegmentTubeData = [outerSegmentTubeData] - if layoutInnerCoordinates: - allSegmentTubeData.append(innerSegmentTubeData) - - for segmentTubeData in allSegmentTubeData: - nodeTubeBifurcationData = innerNodeTubeBifurcationData if (segmentTubeData is innerSegmentTubeData) else \ - outerNodeTubeBifurcationData - # count = 0 - for networkSegment in networkSegments: - tubeData = segmentTubeData[networkSegment] - rawTubeCoordinates = tubeData.getRawTubeCoordinates() - segmentNodes = networkSegment.getNetworkNodes() - startSegmentNode = segmentNodes[0] - startTubeBifurcationData = nodeTubeBifurcationData.get(startSegmentNode) - startSurface = None - newBifurcationData = [] - if not startTubeBifurcationData: - startInSegments = startSegmentNode.getInSegments() - startOutSegments = startSegmentNode.getOutSegments() - if ((len(startInSegments) + len(startOutSegments)) == 3): - startTubeBifurcationData = TubeBifurcationData(startInSegments, startOutSegments, segmentTubeData) - nodeTubeBifurcationData[startSegmentNode] = startTubeBifurcationData - newBifurcationData.append(startTubeBifurcationData) - - if startTubeBifurcationData: - startSurface = startTubeBifurcationData.getSegmentTrimSurface(networkSegment) - endSegmentNode = segmentNodes[-1] - endTubeBifurcationData = nodeTubeBifurcationData.get(endSegmentNode) - endSurface = None - createEndBifurcationData = not endTubeBifurcationData - if createEndBifurcationData: - endInSegments = endSegmentNode.getInSegments() - endOutSegments = endSegmentNode.getOutSegments() - if ((len(endInSegments) + len(endOutSegments)) == 3): - # print("create end", networkSegment, endSegmentNode.getNodeIdentifier(), - # len(endInSegments), len(endOutSegments)) - endTubeBifurcationData = TubeBifurcationData(endInSegments, endOutSegments, segmentTubeData) - nodeTubeBifurcationData[endSegmentNode] = endTubeBifurcationData - newBifurcationData.append(endTubeBifurcationData) - if endTubeBifurcationData: - endSurface = endTubeBifurcationData.getSegmentTrimSurface(networkSegment) - if segmentTubeData is outerSegmentTubeData: - segmentLength = tubeData.getSegmentLength() - elementsCountAlong = max(1, math.ceil(segmentLength / targetElementLength)) - loop = (len(startSegmentNode.getInSegments()) == 1) and \ - (startSegmentNode.getInSegments()[0] is networkSegment) and \ - (networkSegment.getNodeVersions()[0] == networkSegment.getNodeVersions()[-1]) - if (elementsCountAlong == 1) and (startTubeBifurcationData and endTubeBifurcationData): - # at least 2 segments if bifurcating at either end, or loop - elementsCountAlong = 1 - elif (elementsCountAlong < 3) and loop: - # at least 3 segments around loop; 2 should work, but zinc currently makes incorrect faces - elementsCountAlong = 3 - else: - # must match count from outer surface! - outerTubeData = outerSegmentTubeData[networkSegment] - elementsCountAlong = outerTubeData.getSampledElementsCountAlong() - sx, sd1, sd2, sd12 = resampleTubeCoordinates( - rawTubeCoordinates, elementsCountAlong, startSurface=startSurface, endSurface=endSurface) - tubeData.setSampledTubeCoordinates((sx, sd1, sd2, sd12)) - del segmentTubeData - - # blend coordinates where versions are shared between segments - blendedNetworkNodes = set() - for networkSegment in networkSegments: - segmentNodes = networkSegment.getNetworkNodes() - for segmentNode in [segmentNodes[0], segmentNodes[-1]]: - if segmentNode not in blendedNetworkNodes: - blendNetworkNodeCoordinates(segmentNode, allSegmentTubeData) - blendedNetworkNodes.add(segmentNode) - del blendedNetworkNodes - - completedBifurcations = set() # record so only done once - - with ChangeManager(fieldmodule): - for networkSegment in networkSegments: - segmentNodes = networkSegment.getNetworkNodes() - startSegmentNode = segmentNodes[0] - startInSegments = startSegmentNode.getInSegments() - startOutSegments = startSegmentNode.getOutSegments() - startSkipCount = 1 if ((len(startInSegments) + len(startOutSegments)) > 2) else 0 - endSegmentNode = segmentNodes[-1] - endInSegments = endSegmentNode.getInSegments() - endOutSegments = endSegmentNode.getOutSegments() - endSkipCount = 1 if ((len(endInSegments) + len(endOutSegments)) > 2) else 0 - - for stage in range(3): - if stage == 1: - # tube - outerTubeData = outerSegmentTubeData[networkSegment] - outerTubeCoordinates = outerTubeData.getSampledTubeCoordinates() - if bodyMeshGroup in outerTubeData.getAnnotationMeshGroups(): - elementsAlongBodyDistalSegment = len(outerTubeCoordinates) - elementsAroundBodyDistalSegment = len(outerTubeCoordinates[0][0]) - if cervixMeshGroup in outerTubeData.getAnnotationMeshGroups(): - elementsAlongCervixSegment = len(outerTubeCoordinates) - elementsAroundCervixSegment = len(outerTubeCoordinates[0][0]) - if cervixMeshGroup in outerTubeData.getAnnotationMeshGroups(): - elementStartIdxCervix = elementIdentifier - loop = (len(startInSegments) == 1) and (startInSegments[0] is networkSegment) and \ - (networkSegment.getNodeVersions()[0] == networkSegment.getNodeVersions()[-1]) - innerTubeData = innerSegmentTubeData[networkSegment] if layoutInnerCoordinates else None - innerTubeCoordinates = innerTubeData.getSampledTubeCoordinates() if layoutInnerCoordinates else None - startNodeIds = outerTubeData.getStartNodeIds(startSkipCount) - if (not startNodeIds) and (startSkipCount == 0) and (startInSegments or startOutSegments): - # discover start nodes from single adjacent segment - if startInSegments: - startNodeIds = outerSegmentTubeData[startInSegments[0]].getEndNodeIds(0) - else: - startNodeIds = outerSegmentTubeData[startOutSegments[0]].getStartNodeIds(0) - if startNodeIds: - outerTubeData.setStartNodeIds(startNodeIds, startSkipCount) - endNodeIds = outerTubeData.getEndNodeIds(endSkipCount) - if (not endNodeIds) and (endSkipCount == 0) and (endOutSegments or endInSegments): - # discover end nodes from single adjacent segment - if endOutSegments: - endNodeIds = outerSegmentTubeData[endOutSegments[0]].getStartNodeIds(0) - elif endInSegments: - endNodeIds = outerSegmentTubeData[endInSegments[0]].getEndNodeIds(0) - if endNodeIds: - outerTubeData.setEndNodeIds(endNodeIds, endSkipCount) - nodeIdentifier, elementIdentifier, startNodeIds, endNodeIds = generateTube( - outerTubeCoordinates, innerTubeCoordinates, elementsCountThroughWall, - region, fieldcache, coordinates, nodeIdentifier, elementIdentifier, - startSkipCount=startSkipCount, endSkipCount=endSkipCount, - startNodeIds=startNodeIds, endNodeIds=endNodeIds, - annotationMeshGroups=outerTubeData.getAnnotationMeshGroups(), - loop=loop, serendipity=serendipity) - outerTubeData.setStartNodeIds(startNodeIds, startSkipCount) - outerTubeData.setEndNodeIds(endNodeIds, endSkipCount) - - if (len(startInSegments) == 1) and (startSkipCount == 0): - # copy startNodeIds to end of last segment - inTubeData = outerSegmentTubeData[startInSegments[0]] - inTubeData.setEndNodeIds(startNodeIds, 0) - if (len(endOutSegments) == 1) and (endSkipCount == 0): - # copy endNodesIds to start of next segment - outTubeData = outerSegmentTubeData[endOutSegments[0]] - outTubeData.setStartNodeIds(endNodeIds, 0) - else: - # start, end bifurcation - outerTubeBifurcationData = outerNodeTubeBifurcationData.get( - startSegmentNode if (stage == 0) else endSegmentNode) - if outerTubeBifurcationData and not outerTubeBifurcationData in completedBifurcations: - if showIntersectionCurves: - lineIdentifier = None - for s in range(3): - curve = outerTubeBifurcationData.getIntersectionCurve(s) - cx, cd1, cProportions, loop = curve - if cx: - nodeIdentifier, lineIdentifier = \ - generateCurveMesh(region, cx, cd1, loop, nodeIdentifier, lineIdentifier) - if showTrimSurfaces: - faceIdentifier = elementIdentifier if (dimension == 2) else None - for s in range(3): - trimSurface = outerTubeBifurcationData.getTrimSurface(s) - if trimSurface: - nodeIdentifier, faceIdentifier = \ - trimSurface.generateMesh(region, nodeIdentifier, faceIdentifier) - if dimension == 2: - elementIdentifier = faceIdentifier - innerTubeBifurcationData = None - if innerNodeTubeBifurcationData: - innerTubeBifurcationData = innerNodeTubeBifurcationData.get( - startSegmentNode if (stage == 0) else endSegmentNode) - - crossIndexes = outerTubeBifurcationData.getCrossIndexes() # only get these from outer - if not crossIndexes: - outerTubeBifurcationData.determineCrossIndexes() - outerTubeBifurcationData.determineMidCoordinates() - if innerTubeBifurcationData: - innerTubeBifurcationData.determineCrossIndexes() - innerTubeBifurcationData.determineMidCoordinates() - crossIndexes = outerTubeBifurcationData.getCrossIndexes() - - outerTubeCoordinates = outerTubeBifurcationData.getConnectingTubeCoordinates() - outerMidCoordinates = outerTubeBifurcationData.getMidCoordinates() - inward = outerTubeBifurcationData.getSegmentsIn() - outerTubeData = outerTubeBifurcationData.getTubeData() - tubeNodeIds = [outerTubeData[s].getEndNodeIds(1) if inward[s] else \ - outerTubeData[s].getStartNodeIds(1) for s in range(3)] - innerTubeCoordinates = None - innerMidCoordinates = None - if innerTubeBifurcationData: - innerTubeCoordinates = innerTubeBifurcationData.getConnectingTubeCoordinates() - innerMidCoordinates = innerTubeBifurcationData.getMidCoordinates() - annotationMeshGroups = [outerTubeData[s].getAnnotationMeshGroups() for s in range(3)] - nodeIdentifier, elementIdentifier = generateTubeBifurcation( - outerTubeCoordinates, innerTubeCoordinates, inward, elementsCountThroughWall, - outerMidCoordinates, innerMidCoordinates, crossIndexes, - region, fieldcache, coordinates, nodeIdentifier, elementIdentifier, tubeNodeIds, - annotationMeshGroups, serendipity=serendipity) - - for s in range(3): - if inward[s]: - if not outerTubeData[s].getEndNodeIds(1): - outerTubeData[s].setEndNodeIds(tubeNodeIds[s], 1) - else: - if not outerTubeData[s].getStartNodeIds(1): - outerTubeData[s].setStartNodeIds(tubeNodeIds[s], 1) - - completedBifurcations.add(outerTubeBifurcationData) - - is_uterus = uterusGroup.getGroup() - myometriumGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_uterus_term("myometrium")) - myometriumGroup.getMeshGroup(mesh).addElementsConditional(is_uterus) - - # Add human specific annotations if isHuman: - # find elements on left and right edges - elementsOnRightMarginVentral = [] - elementsOnRightMarginDorsal = [] - elementsOnLeftMarginVentral = [] - elementsOnLeftMarginDorsal = [] - - for i in range(elementsAlongBodyDistalSegment): - elementsOnRightMarginVentral.append(elementStartIdxCervix - 1 - i * elementsAroundBodyDistalSegment - (math.ceil(elementsAroundBodyDistalSegment * 0.25) - 1)) - elementsOnRightMarginDorsal.append(elementStartIdxCervix - 1 - i * elementsAroundBodyDistalSegment - (math.ceil(elementsAroundBodyDistalSegment * 0.25) - 1) - 1) - elementsOnLeftMarginVentral.append(elementStartIdxCervix - (i + 1) * elementsAroundBodyDistalSegment + (math.ceil(elementsAroundBodyDistalSegment * 0.25) - 1)) - elementsOnLeftMarginDorsal.append(elementStartIdxCervix - (i + 1) * elementsAroundBodyDistalSegment + (math.ceil(elementsAroundBodyDistalSegment * 0.25) - 1) + 1) - - nearRightMarginDorsalGroup = AnnotationGroup(region, ("elements adjacent to right margin dorsal", "None")) - nearRightMarginVentralGroup = AnnotationGroup(region, ("elements adjacent to right margin ventral", "None")) - nearLeftMarginDorsalGroup = AnnotationGroup(region, ("elements adjacent to left margin dorsal", "None")) - nearLeftMarginVentralGroup = AnnotationGroup(region, ("elements adjacent to left margin ventral", "None")) - - # track backwards to find elements near the margins - lastElements = [elementsOnLeftMarginVentral[-1], elementsOnLeftMarginDorsal[-1], - elementsOnRightMarginVentral[-1], elementsOnRightMarginDorsal[-1]] - marginGroups = [nearLeftMarginVentralGroup, nearLeftMarginDorsalGroup, - nearRightMarginVentralGroup, nearRightMarginDorsalGroup] - - for i in range(4): - adjacentElement = mesh.findElementByIdentifier(lastElements[i]) - eft = adjacentElement.getElementfieldtemplate(coordinates, -1) - lastNode = get_element_node_identifiers(adjacentElement, eft)[4] - addElementsToMargin(mesh, coordinates, marginGroups[i], lastNode) - - elementIter = mesh.createElementiterator() - element = elementIter.next() - while element.isValid(): - elementIdentifier = element.getIdentifier() - if elementIdentifier in elementsOnRightMarginVentral: - nearRightMarginVentralGroup.getMeshGroup(mesh).addElement(element) - elif elementIdentifier in elementsOnRightMarginDorsal: - nearRightMarginDorsalGroup.getMeshGroup(mesh).addElement(element) - elif elementIdentifier in elementsOnLeftMarginVentral: - nearLeftMarginVentralGroup.getMeshGroup(mesh).addElement(element) - elif elementIdentifier in elementsOnLeftMarginDorsal: - nearLeftMarginDorsalGroup.getMeshGroup(mesh).addElement(element) - element = elementIter.next() - - annotationGroups.append(nearRightMarginDorsalGroup) - annotationGroups.append(nearRightMarginVentralGroup) - annotationGroups.append(nearLeftMarginDorsalGroup) - annotationGroups.append(nearLeftMarginVentralGroup) - - elementsOnRightMarginVentral = [] - elementsOnRightMarginDorsal = [] - elementsOnLeftMarginVentral = [] - elementsOnLeftMarginDorsal = [] - - for i in range(elementsAlongCervixSegment): - elementsOnRightMarginDorsal.append(elementStartIdxCervix - 1 + i * elementsAroundCervixSegment - (math.ceil(elementsAroundBodyDistalSegment * 0.25) - 1) - 1) - elementsOnRightMarginVentral.append(elementStartIdxCervix - 1 + i * elementsAroundCervixSegment - (math.ceil(elementsAroundBodyDistalSegment * 0.25) - 1)) - elementsOnLeftMarginVentral.append(elementStartIdxCervix + (math.ceil(elementsAroundBodyDistalSegment * 0.25) - 1) + i * elementsAroundCervixSegment) - elementsOnLeftMarginDorsal.append(elementStartIdxCervix + (math.ceil(elementsAroundBodyDistalSegment * 0.25) - 1) + 1 + i * elementsAroundCervixSegment) - - nearRightMarginDorsalCervixGroup = AnnotationGroup(region, ("elements adjacent to right margin dorsal of cervix", "None")) - nearRightMarginVentralCervixGroup = AnnotationGroup(region, ("elements adjacent to right margin ventral of cervix", "None")) - nearLeftMarginDorsalCervixGroup = AnnotationGroup(region, ("elements adjacent to left margin dorsal of cervix", "None")) - nearLeftMarginVentralCervixGroup = AnnotationGroup(region, ("elements adjacent to left margin ventral of cervix", "None")) - - elementIter = mesh.createElementiterator() - element = elementIter.next() - while element.isValid(): - elementIdentifier = element.getIdentifier() - if elementIdentifier in elementsOnRightMarginVentral: - nearRightMarginVentralCervixGroup.getMeshGroup(mesh).addElement(element) - elif elementIdentifier in elementsOnRightMarginDorsal: - nearRightMarginDorsalCervixGroup.getMeshGroup(mesh).addElement(element) - elif elementIdentifier in elementsOnLeftMarginVentral: - nearLeftMarginVentralCervixGroup.getMeshGroup(mesh).addElement(element) - elif elementIdentifier in elementsOnLeftMarginDorsal: - nearLeftMarginDorsalCervixGroup.getMeshGroup(mesh).addElement(element) - element = elementIter.next() - - annotationGroups.append(nearRightMarginDorsalCervixGroup) - annotationGroups.append(nearRightMarginVentralCervixGroup) - annotationGroups.append(nearLeftMarginDorsalCervixGroup) - annotationGroups.append(nearLeftMarginVentralCervixGroup) + # add human specific annotations allMarkers = {"junction of left round ligament with uterus": {"x": [-4.1312, 9.96436, -7.11994]}, "junction of right round ligament with uterus": {"x": [4.13116, 9.96438, -7.11997]}} @@ -777,16 +525,13 @@ def generateBaseMesh(cls, region, options): for group in annotationGroups: group.getNodesetGroup(nodes).addNode(markerNode) - preBifurcationGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, - ("pre-bifurcation segments", "None")) - postBifurcationGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, - ("post-bifurcation segments", "None")) - annotationGroups.remove(preBifurcationGroup) - annotationGroups.remove(postBifurcationGroup) + # remove temporary annotation groups + for annotationTerms in [("pre-bifurcation segments", "None"), ("post-bifurcation segments", "None")]: + annotationGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, annotationTerms) + annotationGroups.remove(annotationGroup) return annotationGroups, None - @classmethod def refineMesh(cls, meshrefinement, options): """ @@ -825,6 +570,7 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): mesh1d = fm.findMeshByDimension(1) mesh2d = fm.findMeshByDimension(2) + nodes = fm.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) is_exterior = fm.createFieldIsExterior() is_exterior_face_xi3_1 = fm.createFieldAnd(is_exterior, fm.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) @@ -883,6 +629,11 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): get_uterus_term("lumen of vagina")) lumenOfVagina.getMeshGroup(mesh2d).addElementsConditional(is_vagina_inner) + leftGroup = getAnnotationGroupForTerm(annotationGroups, ("left uterus", "None")) + rightGroup = getAnnotationGroupForTerm(annotationGroups, ("right uterus", "None")) + dorsalGroup = getAnnotationGroupForTerm(annotationGroups, ("dorsal uterus", "None")) + ventralGroup = getAnnotationGroupForTerm(annotationGroups, ("ventral uterus", "None")) + if isHuman: leftUterineTubeGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("left uterine tube")) is_leftUterineTube = leftUterineTubeGroup.getGroup() @@ -910,19 +661,6 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): get_uterus_term("lumen of right uterine tube")) lumenOfRightUterineTube.getMeshGroup(mesh2d).addElementsConditional(is_rightUterineTube_inner) - nearRightMarginDorsalGroup = getAnnotationGroupForTerm(annotationGroups, - ("elements adjacent to right margin dorsal", - "None")) - nearRightMarginVentralGroup = getAnnotationGroupForTerm(annotationGroups, - ("elements adjacent to right margin ventral", - "None")) - nearLeftMarginDorsalGroup = getAnnotationGroupForTerm(annotationGroups, - ("elements adjacent to left margin dorsal", - "None")) - nearLeftMarginVentralGroup = getAnnotationGroupForTerm(annotationGroups, - ("elements adjacent to left margin ventral", - "None")) - is_pubocervical = fm.createFieldAnd(is_body_outer, is_cervix_outer) pubocervical = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_uterus_term("pubocervical ligament (TA98)")) @@ -943,68 +681,54 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): get_uterus_term("vagina orifice")) vaginaOrifice.getMeshGroup(mesh1d).addElementsConditional(is_vagina_orifice) + # ligaments + is_dorsalVentral = fm.createFieldAnd(dorsalGroup.getGroup(), ventralGroup.getGroup()) + is_dorsalVentralSerosa = fm.createFieldAnd(is_dorsalVentral, is_exterior_face_xi3_1) + is_leftDorsalVentralSerosa = fm.createFieldAnd(leftGroup.getGroup(), is_dorsalVentralSerosa) + is_rightDorsalVentralSerosa = fm.createFieldAnd(rightGroup.getGroup(), is_dorsalVentralSerosa) + fundusGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("fundus of uterus")) + is_bodyNotFundus = fm.createFieldAnd(bodyGroup.getGroup(), fm.createFieldNot(fundusGroup.getGroup())) + # Broad ligament of uterus - is_nearLeftMarginDorsal = fm.createFieldAnd(nearLeftMarginDorsalGroup.getGroup(), is_exterior_face_xi3_1) - is_nearLeftMarginVentral = fm.createFieldAnd(nearLeftMarginVentralGroup.getGroup(), is_exterior_face_xi3_1) - is_leftBroadLigament = fm.createFieldAnd(is_nearLeftMarginDorsal, is_nearLeftMarginVentral) - leftBroadLigament = findOrCreateAnnotationGroupForTerm(annotationGroups, region, - get_uterus_term("left broad ligament of uterus")) + is_leftBroadLigament = fm.createFieldAnd(is_bodyNotFundus, is_leftDorsalVentralSerosa) + leftBroadLigament = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_uterus_term("left broad ligament of uterus")) leftBroadLigament.getMeshGroup(mesh1d).addElementsConditional(is_leftBroadLigament) - - is_nearRightMarginDorsal = fm.createFieldAnd(nearRightMarginDorsalGroup.getGroup(), is_exterior_face_xi3_1) - is_nearRightMarginVentral = fm.createFieldAnd(nearRightMarginVentralGroup.getGroup(), is_exterior_face_xi3_1) - is_rightBroadLigament = fm.createFieldAnd(is_nearRightMarginDorsal, is_nearRightMarginVentral) - rightBroadLigament = findOrCreateAnnotationGroupForTerm(annotationGroups, region, - get_uterus_term("right broad ligament of uterus")) + # add connected edges from left uterine tube, avoiding adding dorsal-ventral edges on the superior edge + leftBroadLigament.addSubelements() # need current nodes in ligament for group_add_connected_elements + tmpGroup = fm.createFieldGroup() + tmpMeshGroup = tmpGroup.createMeshGroup(mesh1d) + tmpMeshGroup.addElementsConditional(fm.createFieldAnd(is_leftUterineTube, is_leftDorsalVentralSerosa)) + group_add_connected_elements(leftBroadLigament.getGroup(), tmpMeshGroup) + del tmpMeshGroup + del tmpGroup + + is_rightBroadLigament = fm.createFieldAnd(is_bodyNotFundus, is_rightDorsalVentralSerosa) + rightBroadLigament = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_uterus_term("right broad ligament of uterus")) rightBroadLigament.getMeshGroup(mesh1d).addElementsConditional(is_rightBroadLigament) + # add connected edges from right uterine tube, avoiding adding dorsal-ventral edges on the superior edge + rightBroadLigament.addSubelements() # need current nodes in ligament for group_add_connected_elements + tmpGroup = fm.createFieldGroup() + tmpMeshGroup = tmpGroup.createMeshGroup(mesh1d) + tmpMeshGroup.addElementsConditional(fm.createFieldAnd(is_rightUterineTube, is_rightDorsalVentralSerosa)) + group_add_connected_elements(rightBroadLigament.getGroup(), tmpMeshGroup) + del tmpMeshGroup + del tmpGroup # Transverse cervical ligament - nearRightMarginDorsalCervixGroup = \ - getAnnotationGroupForTerm(annotationGroups, - ("elements adjacent to right margin dorsal of cervix", "None")) - nearRightMarginVentralCervixGroup = \ - getAnnotationGroupForTerm(annotationGroups, - ("elements adjacent to right margin ventral of cervix", "None")) - nearLeftMarginDorsalCervixGroup = \ - getAnnotationGroupForTerm(annotationGroups, - ("elements adjacent to left margin dorsal of cervix", "None")) - nearLeftMarginVentralCervixGroup = \ - getAnnotationGroupForTerm(annotationGroups, - ("elements adjacent to left margin ventral of cervix", "None")) - - is_nearLeftMarginDorsalCervix = fm.createFieldAnd(nearLeftMarginDorsalCervixGroup.getGroup(), - is_cervix_outer) - is_nearLeftMarginVentralCervix = fm.createFieldAnd(nearLeftMarginVentralCervixGroup.getGroup(), - is_cervix_outer) - is_leftTransverseCervicalLigament = fm.createFieldAnd(is_nearLeftMarginDorsalCervix, - is_nearLeftMarginVentralCervix) - leftTransverseCervicalLigament = \ - findOrCreateAnnotationGroupForTerm(annotationGroups, region, - get_uterus_term("left transverse cervical ligament")) - leftTransverseCervicalLigament.getMeshGroup(mesh1d).addElementsConditional(is_leftTransverseCervicalLigament) - - is_nearRightMarginDorsalCervix = fm.createFieldAnd(nearRightMarginDorsalCervixGroup.getGroup(), - is_cervix_outer) - is_nearRightMarginVentralCervix = fm.createFieldAnd(nearRightMarginVentralCervixGroup.getGroup(), - is_cervix_outer) - is_rightTransverseCervicalLigament = fm.createFieldAnd(is_nearRightMarginDorsalCervix, - is_nearRightMarginVentralCervix) - rightTransverseCervicalLigament = \ - findOrCreateAnnotationGroupForTerm(annotationGroups, region, - get_uterus_term("right transverse cervical ligament")) + is_leftTransverseCervicalLigament = fm.createFieldAnd(cervixGroup.getGroup(), is_leftDorsalVentralSerosa) + leftTransverseCervicalLigament = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_uterus_term("left transverse cervical ligament")) + leftTransverseCervicalLigament.getMeshGroup(mesh1d).addElementsConditional( + is_leftTransverseCervicalLigament) + + is_rightTransverseCervicalLigament = fm.createFieldAnd(cervixGroup.getGroup(), is_rightDorsalVentralSerosa) + rightTransverseCervicalLigament = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_uterus_term("right transverse cervical ligament")) rightTransverseCervicalLigament.getMeshGroup(mesh1d).addElementsConditional( is_rightTransverseCervicalLigament) - annotationGroups.remove(nearRightMarginDorsalGroup) - annotationGroups.remove(nearRightMarginVentralGroup) - annotationGroups.remove(nearLeftMarginDorsalGroup) - annotationGroups.remove(nearLeftMarginVentralGroup) - - annotationGroups.remove(nearRightMarginDorsalCervixGroup) - annotationGroups.remove(nearRightMarginVentralCervixGroup) - annotationGroups.remove(nearLeftMarginDorsalCervixGroup) - annotationGroups.remove(nearLeftMarginVentralCervixGroup) - if isMouse: rightHornGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("right uterine horn")) leftHornGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("left uterine horn")) @@ -1033,25 +757,8 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): get_uterus_term("lumen of left horn")) lumenOfLeftHorn.getMeshGroup(mesh2d).addElementsConditional(is_leftHorn_inner) - -def addElementsToMargin(mesh, coordinates, group, nodeId): - """ - Track from the node to find elements Add elements lying on the margin and add them to a group. - :param coordinates: Coordinates field. - :param group: Group for adding elements lying on the margin. - :param nodeId: Node to start tracking from. - """ - elementIter = mesh.createElementiterator() - element = elementIter.next() - while element.isValid(): - eft = element.getElementfieldtemplate(coordinates, -1) - nodeIdentifiers = get_element_node_identifiers(element, eft) - if nodeIdentifiers[6] == nodeId: - group.getMeshGroup(mesh).addElement(element) - adjacentElement = element - eft = adjacentElement.getElementfieldtemplate(coordinates, -1) - nodeId = get_element_node_identifiers(adjacentElement, eft)[4] - elementIter = mesh.createElementiterator() - element = elementIter.next() - - return + # keeping these in for now + # annotationGroups.remove(leftGroup) + # annotationGroups.remove(rightGroup) + # annotationGroups.remove(dorsalGroup) + # annotationGroups.remove(ventralGroup) diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index c2fbf46b..519ec66e 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -11,7 +11,6 @@ from scaffoldmaker.meshtypes.meshtype_2d_platehole1 import MeshType_2d_platehole1 from scaffoldmaker.meshtypes.meshtype_2d_sphere1 import MeshType_2d_sphere1 from scaffoldmaker.meshtypes.meshtype_2d_tube1 import MeshType_2d_tube1 -from scaffoldmaker.meshtypes.meshtype_2d_tubebifurcation1 import MeshType_2d_tubebifurcation1 from scaffoldmaker.meshtypes.meshtype_2d_tubenetwork1 import MeshType_2d_tubenetwork1 from scaffoldmaker.meshtypes.meshtype_3d_bladder1 import MeshType_3d_bladder1 from scaffoldmaker.meshtypes.meshtype_3d_bladderurethra1 import MeshType_3d_bladderurethra1 @@ -71,8 +70,6 @@ def __init__(self): MeshType_2d_platehole1, MeshType_2d_sphere1, MeshType_2d_tube1, - MeshType_2d_tubebifurcation1, - #MeshType_2d_tubebifurcationtree1, MeshType_2d_tubenetwork1, MeshType_3d_bladder1, MeshType_3d_bladderurethra1, diff --git a/src/scaffoldmaker/utils/bifurcation.py b/src/scaffoldmaker/utils/bifurcation.py deleted file mode 100644 index b1b40cd5..00000000 --- a/src/scaffoldmaker/utils/bifurcation.py +++ /dev/null @@ -1,1998 +0,0 @@ -""" -Utilities for building bifurcating network meshes. -""" - -from cmlibs.maths.vectorops import add, cross, dot, magnitude, mult, normalize, sub -from cmlibs.utils.zinc.general import ChangeManager -# from cmlibs.utils.zinc.field import find_or_create_field_coordinates -from cmlibs.zinc.element import Element, Elementbasis -from cmlibs.zinc.field import Field -from cmlibs.zinc.node import Node -from scaffoldmaker.annotation.annotationgroup import AnnotationGroup -from scaffoldmaker.utils.eft_utils import remapEftNodeValueLabel, scaleEftNodeValueLabels, setEftScaleFactorIds -from scaffoldmaker.utils.geometry import createCirclePoints -from scaffoldmaker.utils.interpolation import computeCubicHermiteArcLength, computeCubicHermiteDerivativeScaling, \ - DerivativeScalingMode, getCubicHermiteCurvesLength, getNearestLocationBetweenCurves, getNearestLocationOnCurve, \ - interpolateCubicHermite, interpolateCubicHermiteDerivative, interpolateCubicHermiteSecondDerivative, \ - interpolateLagrangeHermiteDerivative, smoothCubicHermiteDerivativesLine, smoothCubicHermiteDerivativesLoop, \ - smoothCurveSideCrossDerivatives -from scaffoldmaker.utils.networkmesh import NetworkMesh, getPathRawTubeCoordinates, resampleTubeCoordinates -from scaffoldmaker.utils.tracksurface import TrackSurface, TrackSurfacePosition, calculate_surface_delta_xi -from scaffoldmaker.utils.zinc_utils import generateCurveMesh, get_nodeset_path_ordered_field_parameters -import copy -import math - - -def get_curve_circle_points(x1, xd1, x2, xd2, r1, rd1, r2, rd2, xi, dmag, side, elementsCountAround): - """ - :param dmag: Magnitude of derivative on curve. - :param side: Vector in side direction of first node around. - Need not be unit or exactly normal to curve at xi. - :return: x[], d1[] around, d2[] along - """ - cx = interpolateCubicHermite(x1, xd1, x2, xd2, xi) - cxd = interpolateCubicHermiteDerivative(x1, xd1, x2, xd2, xi) - mag_cxd = magnitude(cxd) - cxd2 = interpolateCubicHermiteSecondDerivative(x1, xd1, x2, xd2, xi) - mag_cxd2 = magnitude(cxd2) - r = interpolateCubicHermite([ r1 ], [ rd1 ], [ r2 ], [ rd2 ], xi)[0] - rd = interpolateCubicHermiteDerivative([ r1 ], [ rd1 ], [ r2 ], [ rd2 ], xi)[0] - axis1 = normalize(cxd) - axis3 = normalize(cross(axis1, side)) - axis2 = cross(axis3, axis1) - x, d1 = createCirclePoints(cx, mult(axis2, r), mult(axis3, r), elementsCountAround) - curvatureVector = mult(cxd2, 1.0/(mag_cxd*mag_cxd)) - d2 = [] - radialGrowth = rd/(mag_cxd*r) - for e in range(elementsCountAround): - radialVector = sub(x[e], cx) - dmagFinal = dmag*(1.0 - dot(radialVector, curvatureVector)) - # add curvature and radius change components: - d2.append(add(mult(cxd, dmagFinal/mag_cxd), mult(radialVector, dmagFinal*radialGrowth))) - return x, d1, d2 - -def get_bifurcation_triple_point(p1x, p1d, p2x, p2d, p3x, p3d): - """ - Get coordinates and derivatives of triple point between p1, p2 and p3 with derivatives. - :param p1x..p3d: Point coordinates and derivatives, numbered anticlockwise around triple point. - All derivatives point away from triple point. - Returned d1 points from triple point to p2, d2 points from triple point to p3. - :return: x, d1, d2 - """ - scaling12 = computeCubicHermiteDerivativeScaling(p1x, [-d for d in p1d], p2x, p2d) - scaling23 = computeCubicHermiteDerivativeScaling(p2x, [-d for d in p2d], p3x, p3d) - scaling31 = computeCubicHermiteDerivativeScaling(p3x, [-d for d in p3d], p1x, p1d) - trx1 = interpolateCubicHermite(p1x, mult(p1d, -scaling12), p2x, mult(p2d, scaling12), 0.5) - trx2 = interpolateCubicHermite(p2x, mult(p2d, -scaling23), p3x, mult(p3d, scaling23), 0.5) - trx3 = interpolateCubicHermite(p3x, mult(p3d, -scaling31), p1x, mult(p1d, scaling31), 0.5) - trx = [(trx1[c] + trx2[c] + trx3[c]) / 3.0 for c in range(3)] - td1 = interpolateLagrangeHermiteDerivative(trx, p1x, p1d, 0.0) - td2 = interpolateLagrangeHermiteDerivative(trx, p2x, p2d, 0.0) - td3 = interpolateLagrangeHermiteDerivative(trx, p3x, p3d, 0.0) - n12 = cross(td1, td2) - n23 = cross(td2, td3) - n31 = cross(td3, td1) - norm = normalize([(n12[c] + n23[c] + n31[c]) for c in range(3)]) - sd1 = smoothCubicHermiteDerivativesLine([trx, p1x], [normalize(cross(norm, cross(td1, norm))), p1d], - fixStartDirection=True, fixEndDerivative=True)[0] - sd2 = smoothCubicHermiteDerivativesLine([trx, p2x], [normalize(cross(norm, cross(td2, norm))), p2d], - fixStartDirection=True, fixEndDerivative=True)[0] - sd3 = smoothCubicHermiteDerivativesLine([trx, p3x], [normalize(cross(norm, cross(td3, norm))), p3d], - fixStartDirection=True, fixEndDerivative=True)[0] - trd1 = mult(sub(sd2, add(sd3, sd1)), 0.5) - trd2 = mult(sub(sd3, add(sd1, sd2)), 0.5) - return trx, trd1, trd2 - - -def get_tube_bifurcation_connection_elements_counts(tCounts): - """ - Get number of elements directly connecting tubes 1, 2 and 3 from the supplied number around. - :param tCounts: Number of elements around tubes in order. - :return: List of elements connect tube with its next neighbour, looping back to first. - """ - assert len(tCounts) == 3 - return [(tCounts[i] + tCounts[i - 2] - tCounts[i - 1]) // 2 for i in range(3)] - - -def make_tube_bifurcation_points(paCentre, pax, pad2, c1Centre, c1x, c1d2, c2Centre, c2x, c2d2): - """ - Gets first ring of coordinates and derivatives between parent pa and - children c1, c2, and over the crotch between c1 and c2. - :return rox, rod1, rod2, cox, cod1, cod2 - """ - paCount = len(pax) - c1Count = len(c1x) - c2Count = len(c2x) - pac1Count, c1c2Count, pac2Count = get_tube_bifurcation_connection_elements_counts([paCount, c1Count, c2Count]) - # convert to number of nodes, includes both 6-way points - pac1NodeCount = pac1Count + 1 - pac2NodeCount = pac2Count + 1 - c1c2NodeCount = c1c2Count + 1 - paStartIndex = 0 - c1StartIndex = 0 - c2StartIndex = 0 - pac1x = [ None ]*pac1NodeCount - pac1d1 = [ None ]*pac1NodeCount - pac1d2 = [ None ]*pac1NodeCount - for n in range(pac1NodeCount): - pan = (paStartIndex + n) % paCount - c1n = (c1StartIndex + n) % c1Count - x1, d1, x2, d2 = pax[pan], mult(pad2[pan], 2.0), c1x[c1n], mult(c1d2[c1n], 2.0) - pac1x [n] = interpolateCubicHermite(x1, d1, x2, d2, 0.5) - pac1d1[n] = [ 0.0, 0.0, 0.0 ] - pac1d2[n] = mult(interpolateCubicHermiteDerivative(x1, d1, x2, d2, 0.5), 0.5) - paStartIndex2 = paStartIndex + pac1Count - c1StartIndex2 = c1StartIndex + pac1Count - c2StartIndex2 = c2StartIndex + c1c2Count - pac2x = [ None ]*pac2NodeCount - pac2d1 = [ None ]*pac2NodeCount - pac2d2 = [ None ]*pac2NodeCount - for n in range(pac2NodeCount): - pan = (paStartIndex2 + n) % paCount - c2n = (c2StartIndex2 + n) % c2Count - x1, d1, x2, d2 = pax[pan], mult(pad2[pan], 2.0), c2x[c2n], mult(c2d2[c2n], 2.0) - pac2x [n] = interpolateCubicHermite(x1, d1, x2, d2, 0.5) - pac2d1[n] = [ 0.0, 0.0, 0.0 ] - pac2d2[n] = mult(interpolateCubicHermiteDerivative(x1, d1, x2, d2, 0.5), 0.5) - c1c2x = [ None ]*c1c2NodeCount - c1c2d1 = [ None ]*c1c2NodeCount - c1c2d2 = [ None ]*c1c2NodeCount - for n in range(c1c2NodeCount): - c1n = (c1StartIndex2 + n) % c1Count - c2n = (c2StartIndex2 - n) % c2Count # note: reversed - x1, d1, x2, d2 = c2x[c2n], mult(c2d2[c2n], -2.0), c1x[c1n], mult(c1d2[c1n], 2.0) - c1c2x [n] = interpolateCubicHermite(x1, d1, x2, d2, 0.5) - c1c2d1[n] = [ 0.0, 0.0, 0.0 ] - c1c2d2[n] = mult(interpolateCubicHermiteDerivative(x1, d1, x2, d2, 0.5), 0.5) - # get hex triple points - hex1, hex1d1, hex1d2 = get_bifurcation_triple_point( - pax[paStartIndex], mult(pad2[paStartIndex], -1.0), - c1x[c1StartIndex], c1d2[c1StartIndex], - c2x[c1StartIndex], c2d2[c2StartIndex]) - hex2, hex2d1, hex2d2 = get_bifurcation_triple_point( - pax[paStartIndex2], mult(pad2[paStartIndex2], -1.0), - c2x[c2StartIndex2], c2d2[c2StartIndex2], - c1x[c1StartIndex2], c1d2[c1StartIndex2]) - # smooth around loops through hex points to get d1 - loop1x = [ hex2 ] + pac2x[1:-1] + [ hex1 ] - loop1d1 = [ [ -d for d in hex2d2 ] ] + pac2d1[1:-1] + [ hex1d1 ] - loop2x = [ hex1 ] + pac1x[1:-1] + [ hex2 ] - loop2d1 = [ [ -d for d in hex1d2 ] ] + pac1d1[1:-1] + [ hex2d1 ] - loop1d1 = smoothCubicHermiteDerivativesLine(loop1x, loop1d1, fixStartDirection=True, fixEndDirection=True, magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - loop2d1 = smoothCubicHermiteDerivativesLine(loop2x, loop2d1, fixStartDirection=True, fixEndDirection=True, magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - # smooth over "crotch" between c1 and c2 - crotchx = [ hex2 ] + c1c2x[1:-1] + [ hex1 ] - crotchd1 = [ add(hex2d1, hex2d2) ] + c1c2d1[1:-1] + [ [ (-hex1d1[c] - hex1d2[c]) for c in range(3) ] ] - crotchd1 = smoothCubicHermiteDerivativesLine(crotchx, crotchd1, fixStartDerivative=True, fixEndDerivative=True, magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - rox = [ hex1 ] + pac1x[1:-1] + [ hex2 ] + pac2x[1:-1] - rod1 = [ loop1d1[-1] ] + loop2d1[1:] + loop1d1[1:-1] - rod2 = [ [ -d for d in loop2d1[ 0] ] ] + pac1d2[1:-1] + [ [ -d for d in loop1d1[0] ] ] + pac2d2[1:-1] - cox = crotchx [1:-1] - cod1 = crotchd1[1:-1] - cod2 = c1c2d2[1:-1] - return rox, rod1, rod2, cox, cod1, cod2, paStartIndex, c1StartIndex, c2StartIndex - - -def make_tube_bifurcation_elements_2d(region, coordinates, elementIdentifier, - paNodeId, paStartIndex, c1NodeId, c1StartIndex, c2NodeId, c2StartIndex, roNodeId, coNodeId, - useCrossDerivatives=False): - """ - Creates elements from parent, ring/row, crotch/column, child1 and child2 nodes. - Assumes client has active ChangeManager(fieldmodule). - :param region: Zinc region to create model in. - :param coordinates: Finite element coordinate field to define. - :param elementIdentifier: First 2D element identifier to use. - :param paNodeId, paStartIndex, c1NodeId, c1StartIndex, c2NodeId, c2StartIndex: - Lists of parent, child1, child2 nodes and their starting index to be at hex2 - :param roNodeId, coNodeId: Lists of ring/row and crotch/column nodes, - starting at hex2 and between hex1 and hex2, respectively. - :return next element identifier. - """ - paCount = len(paNodeId) - c1Count = len(c1NodeId) - c2Count = len(c2NodeId) - pac1Count, c1c2Count, pac2Count = get_tube_bifurcation_connection_elements_counts([paCount, c1Count, c2Count]) - - fieldmodule = region.getFieldmodule() - mesh = fieldmodule.findMeshByDimension(2) - - elementtemplateStd = mesh.createElementtemplate() - elementtemplateStd.setElementShapeType(Element.SHAPE_TYPE_SQUARE) - bicubicHermiteBasis = fieldmodule.createElementbasis(2, Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE) - eftStd = mesh.createElementfieldtemplate(bicubicHermiteBasis) - if not useCrossDerivatives: - for n in range(4): - eftStd.setFunctionNumberOfTerms(n*4 + 4, 0) - elementtemplateStd.defineField(coordinates, -1, eftStd) - elementtemplateMod = mesh.createElementtemplate() - elementtemplateMod.setElementShapeType(Element.SHAPE_TYPE_SQUARE) - - for e1 in range(paCount): - eft = eftStd - elementtemplate = elementtemplateStd - np = e1 + paStartIndex - nids = [ paNodeId[np % paCount], paNodeId[(np + 1) % paCount], roNodeId[e1], roNodeId[(e1 + 1) % paCount] ] - scalefactors = None - meshGroups = [ ] - - if e1 in (0, pac1Count - 1, pac1Count, paCount - 1): - eft = mesh.createElementfieldtemplate(bicubicHermiteBasis) - if not useCrossDerivatives: - for n in range(4): - eft.setFunctionNumberOfTerms(n*4 + 4, 0) - if e1 in (0, pac1Count): - scalefactors = [ -1.0 ] - setEftScaleFactorIds(eft, [1], []) - remapEftNodeValueLabel(eft, [ 3 ], Node.VALUE_LABEL_D_DS1, [ ( Node.VALUE_LABEL_D_DS2, [ 1 ] ) ]) - remapEftNodeValueLabel(eft, [ 3 ], Node.VALUE_LABEL_D_DS2, [ ( Node.VALUE_LABEL_D_DS1, [] ), ( Node.VALUE_LABEL_D_DS2, [] ) ]) - elif e1 in (pac1Count - 1, paCount - 1): - remapEftNodeValueLabel(eft, [ 4 ], Node.VALUE_LABEL_D_DS2, [ ( Node.VALUE_LABEL_D_DS1, [] ), ( Node.VALUE_LABEL_D_DS2, [] ) ]) - elementtemplateMod.defineField(coordinates, -1, eft) - elementtemplate = elementtemplateMod - - element = mesh.createElement(elementIdentifier, elementtemplate) - result2 = element.setNodesByIdentifier(eft, nids) - if scalefactors: - result3 = element.setScaleFactors(eft, scalefactors) - else: - result3 = '-' - #print('create element tube bifurcation pa', element.isValid(), elementIdentifier, result2, result3, nids) - elementIdentifier += 1 - for meshGroup in meshGroups: - meshGroup.addElement(element) - - for e1 in range(c1Count): - eft = eftStd - elementtemplate = elementtemplateStd - nr = e1 - nc = e1 + c1StartIndex - nids = [ roNodeId[nr % paCount], roNodeId[(nr + 1) % paCount], c1NodeId[nc % c1Count], c1NodeId[(nc + 1) % c1Count] ] - if e1 >= pac1Count: - nids[1] = roNodeId[0] if (e1 == (c1Count - 1)) else coNodeId[e1 - pac1Count] - if e1 > pac1Count: - #nids[0] = coNodeId[(e1 - pac1Count - 1)] - nids[0] = coNodeId[(e1 - pac1Count - 1) % c1Count] - scalefactors = None - meshGroups = [ ] - - if e1 in (0, pac1Count, c1Count - 1): - eft = mesh.createElementfieldtemplate(bicubicHermiteBasis) - if not useCrossDerivatives: - for n in range(4): - eft.setFunctionNumberOfTerms(n*4 + 4, 0) - if e1 == 0: - scalefactors = [ -1.0 ] - setEftScaleFactorIds(eft, [1], []) - remapEftNodeValueLabel(eft, [ 1 ], Node.VALUE_LABEL_D_DS1, [ ( Node.VALUE_LABEL_D_DS2, [ 1 ] ) ]) - remapEftNodeValueLabel(eft, [ 1 ], Node.VALUE_LABEL_D_DS2, [ ( Node.VALUE_LABEL_D_DS1, [] ) ]) - elif e1 == pac1Count: - remapEftNodeValueLabel(eft, [ 1 ], Node.VALUE_LABEL_D_DS1, [ ( Node.VALUE_LABEL_D_DS1, [ ] ), ( Node.VALUE_LABEL_D_DS2, [ ] ) ]) - elif e1 == (c1Count - 1): - scalefactors = [ -1.0 ] - setEftScaleFactorIds(eft, [1], []) - remapEftNodeValueLabel(eft, [ 2 ], Node.VALUE_LABEL_D_DS1, [ ( Node.VALUE_LABEL_D_DS1, [ 1 ] ), ( Node.VALUE_LABEL_D_DS2, [ 1 ] ) ]) - remapEftNodeValueLabel(eft, [ 2 ], Node.VALUE_LABEL_D_DS2, [ ( Node.VALUE_LABEL_D_DS1, [] ) ]) - elementtemplateMod.defineField(coordinates, -1, eft) - elementtemplate = elementtemplateMod - - element = mesh.createElement(elementIdentifier, elementtemplate) - result2 = element.setNodesByIdentifier(eft, nids) - if scalefactors: - result3 = element.setScaleFactors(eft, scalefactors) - else: - result3 = '-' - #print('create element tube bifurcation c1', element.isValid(), elementIdentifier, result2, result3, nids) - elementIdentifier += 1 - for meshGroup in meshGroups: - meshGroup.addElement(element) - - for e1 in range(c2Count): - eft = eftStd - elementtemplate = elementtemplateStd - nr = 0 if (e1 == 0) else (paCount - c2Count + e1) - nc = e1 + c2StartIndex - nids = [ roNodeId[nr % paCount], roNodeId[(nr + 1) % paCount], c2NodeId[nc % c2Count], c2NodeId[(nc + 1) % c2Count] ] - if 0 <= e1 < (c1c2Count - 1): - nids[1] = coNodeId[c1c2Count - e1 - 2] - if 0 < e1 <= (c1c2Count - 1): - nids[0] = coNodeId[c1c2Count - e1 - 1] - scalefactors = None - meshGroups = [ ] - - if e1 <= c1c2Count: - eft = mesh.createElementfieldtemplate(bicubicHermiteBasis) - if not useCrossDerivatives: - for n in range(4): - eft.setFunctionNumberOfTerms(n*4 + 4, 0) - scalefactors = [ -1.0 ] - setEftScaleFactorIds(eft, [1], []) - if e1 == 0: - remapEftNodeValueLabel(eft, [ 1 ], Node.VALUE_LABEL_D_DS1, [ ( Node.VALUE_LABEL_D_DS1, [ ] ), ( Node.VALUE_LABEL_D_DS2, [ ] ) ]) - scaleEftNodeValueLabels(eft, [ 2 ], [ Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2 ], [ 1 ]) - elif e1 < (c1c2Count - 1): - scaleEftNodeValueLabels(eft, [ 1, 2 ], [ Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2 ], [ 1 ]) - elif e1 == (c1c2Count - 1): - scaleEftNodeValueLabels(eft, [ 1 ], [ Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2 ], [ 1 ]) - remapEftNodeValueLabel(eft, [ 2 ], Node.VALUE_LABEL_D_DS1, [ ( Node.VALUE_LABEL_D_DS1, [ 1 ] ), ( Node.VALUE_LABEL_D_DS2, [ 1 ] ) ]) - remapEftNodeValueLabel(eft, [ 2 ], Node.VALUE_LABEL_D_DS2, [ ( Node.VALUE_LABEL_D_DS1, [ ] ) ]) - elif e1 == c1c2Count: - remapEftNodeValueLabel(eft, [ 1 ], Node.VALUE_LABEL_D_DS1, [ ( Node.VALUE_LABEL_D_DS2, [ 1 ] ) ]) - remapEftNodeValueLabel(eft, [ 1 ], Node.VALUE_LABEL_D_DS2, [ ( Node.VALUE_LABEL_D_DS1, [ ] ) ]) - elementtemplateMod.defineField(coordinates, -1, eft) - elementtemplate = elementtemplateMod - - element = mesh.createElement(elementIdentifier, elementtemplate) - result2 = element.setNodesByIdentifier(eft, nids) - if scalefactors: - result3 = element.setScaleFactors(eft, scalefactors) - else: - result3 = '-' - #print('create element tube bifurcation c2', element.isValid(), elementIdentifier, result2, result3, nids) - for meshGroup in meshGroups: - meshGroup.addElement(element) - elementIdentifier += 1 - - return elementIdentifier - - -def getBifurcationCrossPoint(cx, p1x, p1d, p2x, p2d, p3x, p3d): - """ - Get derivatives of cross point between p1, p2 and p3 with derivatives. - :param cx: Cross point coordinates. - :param p1x..p3d: Point coordinates and derivatives, numbered anticlockwise around triple point. - All derivatives point away from triple point. - Returned d1 points from triple point to p2, d2 points from triple point to p3. - :return: d1, d2 - """ - # cx1 = interpolateCubicHermite(p1x, mult(p1d, -2.0), p2x, mult(p2d, 2.0), 0.5) - # cx2 = interpolateCubicHermite(p2x, mult(p2d, -2.0), p3x, mult(p3d, 2.0), 0.5) - # cx3 = interpolateCubicHermite(p3x, mult(p3d, -2.0), p1x, mult(p1d, 2.0), 0.5) - # cx = [ (cx1[c] + cx2[c] + cx3[c])/3.0 for c in range(3) ] - td1 = interpolateLagrangeHermiteDerivative(cx, p1x, p1d, 0.0) - td2 = interpolateLagrangeHermiteDerivative(cx, p2x, p2d, 0.0) - td3 = interpolateLagrangeHermiteDerivative(cx, p3x, p3d, 0.0) - n12 = cross(td1, td2) - n23 = cross(td2, td3) - n31 = cross(td3, td1) - norm = normalize([(n12[c] + n23[c] + n31[c]) for c in range(3)]) - sd1 = smoothCubicHermiteDerivativesLine([cx, p1x], [normalize(cross(norm, cross(td1, norm))), p1d], - fixStartDirection=True, fixEndDerivative=True)[0] - sd2 = smoothCubicHermiteDerivativesLine([cx, p2x], [normalize(cross(norm, cross(td2, norm))), p2d], - fixStartDirection=True, fixEndDerivative=True)[0] - sd3 = smoothCubicHermiteDerivativesLine([cx, p3x], [normalize(cross(norm, cross(td3, norm))), p3d], - fixStartDirection=True, fixEndDerivative=True)[0] - cd1 = mult(sub(sd2, add(sd3, sd1)), 0.5) - cd2 = mult(sub(sd3, add(sd1, sd2)), 0.5) - return cd1, cd2 - - -def getTubeBifurcationCoordinates2D(tCoords, inCount): - """ - Compute half loops of middle coordinates between tube and its next neighbour. - :param tCoords: List over 3 tubes (starting with "in" tubes) of [tx, td1, td2] for last/first tube ring in/out. - :param inCount: Number of tubes which are directed in to the junction. - :return: mCoords (list over tubes of mid coordinates x, d1, d2 to next tube, around half loop from first to - second cross point). - """ - tCount = len(tCoords) - assert tCount == 3 - taCounts = [len(v[0]) for v in tCoords] - # get numbers of elements directly connecting t0-t1, t1-t2, t2-t1 - teCounts = [(taCounts[i] + taCounts[i - tCount + 1] - taCounts[i - 1]) // 2 for i in range(tCount)] - # get midside coordinates for edges directly connecting neighbouring tubes, plus start/end cross point contributions - mCoords = [[] for _ in range(tCount)] - for ia in range(tCount): - ib = (ia + 1) % tCount - teCount = teCounts[ia] - tnCount = teCount + 1 - tax, tad2 = tCoords[ia][0], tCoords[ia][2] - tbx, tbd2 = tCoords[ib][0], tCoords[ib][2] - for n in range(tnCount): - aIn = (ia < inCount) - an = n if aIn else -n - ax = tax[an] - ad2 = mult(tad2[an], 2.0 if aIn else -2.0) - bIn = (ib < inCount) - bn = -n if bIn else n - bx = tbx[bn] - bd2 = mult(tbd2[bn], -2.0 if bIn else 2.0) - if not aIn: - ax, ad2, bx, bd2 = bx, bd2, ax, ad2 - mx = interpolateCubicHermite(ax, ad2, bx, bd2, 0.5) - md2 = mult(interpolateCubicHermiteDerivative(ax, ad2, bx, bd2, 0.5), 0.5) - mCoords[ia].append([mx, [0.0, 0.0, 0.0], md2]) - - # get cross points - cCoords = [] - for ic in (0, -1): - x = [sum(mCoords[it][ic][0][c] for it in range(tCount)) / tCount for c in range(3)] - d1, d2 = getBifurcationCrossPoint( - x, - tCoords[0][ic][0], mult(tCoords[0][ic][2], -1.0), - tCoords[1][ic][0], tCoords[1][ic][2], - tCoords[2][ic][0], tCoords[2][ic][2]) - cCoords.append([x, d1, d2]) - - # smooth around curves between cross points and create interior nodes - for ia in range(tCount): - teCount = teCounts[ia] - tnCount = teCount + 1 - lx = [cCoords[0][0]] + [p[0] for p in mCoords[ia][1:-1]] + [cCoords[1][0]] - c1d1 = [-d for d in cCoords[0][2]] if (ia == 0) else \ - [cCoords[0][1][c] + cCoords[0][2][c] for c in range(3)] if (ia == 1) else \ - [-d for d in cCoords[0][2]] - c2d1 = cCoords[1][1] if (ia == 0) else \ - [-cCoords[1][1][c] - cCoords[1][2][c] for c in range(3)] if (ia == 1) else \ - cCoords[1][2] - ld1 = [c1d1] + [p[1] for p in mCoords[ia][1:-1]] + [c2d1] - ld1 = smoothCubicHermiteDerivativesLine(lx, ld1, fixStartDirection=True, fixEndDirection=True, - magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - for il in range(1, teCount): - mCoords[ia][il][1] = ld1[il] - - return mCoords - - -def generateTube(outerTubeCoordinates, innerTubeCoordinates, elementsCountThroughWall, - region, fieldcache, coordinates: Field, nodeIdentifier, elementIdentifier, - startSkipCount: int=0, endSkipCount:int=0, startNodeIds: list=None, endNodeIds: list=None, - annotationMeshGroups=[], loop=False, serendipity=False): - """ - Generate a 2D or 3D thick walled tube from supplied coordinates. - Assumes client has active ChangeManager(fieldmodule). - :param outerTubeCoordinates: Coordinates of outside of tube, or only coordinates for 2D. - [ox, od1, od2, od12], each indexed by e.g. ox[along][around]. - :param innerTubeCoordinates: Coordinates of inside of tube, if 3D elements; like outer coordinates, or None if 2D - :param elementsCountThroughWall: Number of elements through wall. Must be 1 if 2D. - :param region: Zinc region to create model in. - :param fieldcache: Field evaluation cache. - :param coordinates: Finite element coordinate field to define. - :param nodeIdentifier: First node identifier to use. - :param elementIdentifier: First 2D element identifier to use. - :param startSkipCount: Number of element rows to skip along at start. - :param endSkipCount: Number of element rows to skip along at end. - :param startNodeIds: Optional existing node identifiers [wall outward][around] to use at start, or None. - :param endNodeIds: Optional existing node identifiers [wall outward][around] to use at end, or None. - :param annotationMeshGroups: Mesh groups to add elements to. - :param loop: Set to true to loop back to start coordinates at end. - :param serendipity: True to use Hermite serendipity basis, False for regular Hermite with zero cross derivatives. - :return: next node identifier, next element identifier, startNodeIds, endNodeIds - """ - ox, od1, od2, od12 = outerTubeCoordinates - ix, id1, id2, id12 = innerTubeCoordinates if innerTubeCoordinates else (None, None, None, None) - dimension = 3 if innerTubeCoordinates else 2 - assert ((dimension == 2) and (elementsCountThroughWall == 1)) or \ - ((dimension == 3) and (elementsCountThroughWall >= 1)) - nodesCountThroughWall = (elementsCountThroughWall + 1) if (dimension == 3) else 1 - elementsCountAlong = len(ox) - 1 - elementsCountAround = len(ox[0]) - assert (not loop) or ((startSkipCount == 0) and (endSkipCount == 0)) - - fieldmodule = region.getFieldmodule() - - # create nodes - - nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - nodetemplate = nodes.createNodetemplate() - nodetemplate.defineField(coordinates) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS1, 1) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS2, 1) - if od12 and not serendipity: - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D2_DS1DS2, 1) - - tubeNodeIds = [] - oFactor = iFactor = 0.0 - for n2 in range(elementsCountAlong + 1): - if (n2 < startSkipCount) or (n2 > elementsCountAlong - endSkipCount): - tubeNodeIds.append(None) - continue - if startNodeIds and (n2 == startSkipCount): - tubeNodeIds.append(startNodeIds) - continue - if endNodeIds and (n2 == (elementsCountAlong - endSkipCount)): - tubeNodeIds.append(endNodeIds) - continue - if loop and (n2 == elementsCountAlong): - tubeNodeIds.append(tubeNodeIds[0]) - continue - tubeNodeIds.append([]) - for n3 in range(nodesCountThroughWall): - ringNodeIds = [] - otx, otd1, otd2 = (ox[n2], od1[n2], od2[n2]) - otd12 = od12[n2] if (od12 and not serendipity) else None - itx, itd1, itd2 = (ix[n2], id1[n2], id2[n2]) if innerTubeCoordinates else (None, None, None) - itd12 = id12[n2] if (innerTubeCoordinates and otd12) else None - if innerTubeCoordinates: - oFactor = n3 / elementsCountThroughWall - iFactor = 1.0 - oFactor - for n1 in range(elementsCountAround): - node = nodes.createNode(nodeIdentifier, nodetemplate) - fieldcache.setNode(node) - if (not innerTubeCoordinates) or (n3 == elementsCountThroughWall): - rx, rd1, rd2 = otx[n1], otd1[n1], otd2[n1] - elif n3 == 0: - rx, rd1, rd2 = itx[n1], itd1[n1], itd2[n1] - else: - rx = add(mult(otx[n1], oFactor), mult(itx[n1], iFactor)) - rd1 = add(mult(otd1[n1], oFactor), mult(itd1[n1], iFactor)) - rd2 = add(mult(otd2[n1], oFactor), mult(itd2[n1], iFactor)) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, rx) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, rd1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, rd2) - if otd12: - rd12 = otd12[n1] if ((not innerTubeCoordinates) or (n3 == elementsCountThroughWall)) else \ - itd12[n1] if (n3 == 0) else \ - add(mult(otd12[n1], oFactor), mult(itd12[n1], iFactor)) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, rd12) - ringNodeIds.append(nodeIdentifier) - nodeIdentifier += 1 - tubeNodeIds[-1].append(ringNodeIds) - - # create elements - - mesh = fieldmodule.findMeshByDimension(dimension) - elementtemplate = mesh.createElementtemplate() - elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE if (dimension == 3) else Element.SHAPE_TYPE_SQUARE) - basis = fieldmodule.createElementbasis( - dimension, (Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE_SERENDIPITY if serendipity - else Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE)) - if dimension == 3: - basis.setFunctionType(3, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) - eft = mesh.createElementfieldtemplate(basis) - if (not serendipity) and (not od12): - # remove cross derivative terms for full bicubic Hermite - ln = [1, 2, 3, 4] if (dimension == 2) else [1, 2, 3, 4, 5, 6, 7, 8] - remapEftNodeValueLabel(eft, ln, Node.VALUE_LABEL_D2_DS1DS2, []) - elementtemplate.defineField(coordinates, -1, eft) - - for e2 in range(startSkipCount, elementsCountAlong - endSkipCount): - for e3 in range(elementsCountThroughWall): - for e1 in range(elementsCountAround): - e2p = e2 + 1 - e1p = (e1 + 1) % elementsCountAround - nids = [] - for n3 in [e3, e3 + 1] if (dimension == 3) else [0]: - nids += [tubeNodeIds[e2][n3][e1], tubeNodeIds[e2][n3][e1p], - tubeNodeIds[e2p][n3][e1], tubeNodeIds[e2p][n3][e1p]] - element = mesh.createElement(elementIdentifier, elementtemplate) - element.setNodesByIdentifier(eft, nids) - # print("Tube element", elementIdentifier, "nodes", nids) - for annotationMeshGroup in annotationMeshGroups: - annotationMeshGroup.addElement(element) - elementIdentifier += 1 - - return nodeIdentifier, elementIdentifier, tubeNodeIds[startSkipCount], \ - tubeNodeIds[elementsCountAlong - endSkipCount] - - -def generateTubeBifurcation(outerTubeCoordinates, innerTubeCoordinates, inward, elementsCountThroughWall, - outerMidCoordinates, innerMidCoordinates, crossIndexes, - region, fieldcache, coordinates: Field, - nodeIdentifier, elementIdentifier, tubeNodeIds, - annotationMeshGroups, serendipity=False): - """ - Generate a 2D tube bifurcation as elements connecting 3 rings of coordinates, optionally using existing nodes. - Assumes client has active ChangeManager(fieldmodule). - :param outerTubeCoordinates: List over 3 tubes (starting with "in" tubes) of outer coordinates - [[ox], [od1], [od2], [od12]] for last/first tube rings in/out. - :param innerTubeCoordinates: List over 3 tubes (starting with "in" tubes) of inner coordinates - [[ix], [id1], [id2], [id12]] for last/first tube rings in/out, or None if 2D. - :param inward: List over 3 tubes of True if inward, False if not. All inward tubes precede outward. - :param elementsCountThroughWall: Number of elements through wall. Must be 1 if 2D. - :param outerMidCoordinates: List of 3 middle half rings between tubes 1-2, 2-3 and 3-1 of outer coordinates - [[omx], [omd1], [omd2]], from first to second cross point. First/last coordinates in each are the same with d1 - pointing towards tube 2 and d2 pointing towards tube 3 at first cross point, flipped on second cross point. - :param innerMidCoordinates: List of 3 middle half rings between tubes 1-2, 2-3 and 3-1 of inner coordinates - [[imx], [imd1], [imd2]], from first to second cross point. First/last coordinates in each are the same with d1 - pointing towards tube 2 and d2 pointing towards tube 3 at first cross point, flipped on second cross point. - :param crossIndexes: List of 3 indexes around tube coordinates which link to first cross point. - :param region: Zinc region to create model in. - :param fieldcache: Field evaluation cache. - :param coordinates: Finite element coordinate field to define. - :param nodeIdentifier: First node identifier to use. - :param elementIdentifier: First 2D element identifier to use. - :param tubeNodeIds: List over 3 tubes of existing node identifiers [wall outward][around] to use at that inlet/outlet, - any of which may be None. On return, None indexes are filled with new node identifiers. - :param annotationMeshGroups: List over 3 tubes of lists of meshGroups to add elements to for the part of the - bifurcation that is part of that segment. - :param serendipity: True to use Hermite serendipity basis, False for regular Hermite with zero cross derivatives. - :return: next node identifier, next element identifier - """ - dimension = 3 if innerTubeCoordinates else 2 - assert ((dimension == 2) and (elementsCountThroughWall == 1)) or \ - ((dimension == 3) and (elementsCountThroughWall >= 1)) - nodesCountThroughWall = (elementsCountThroughWall + 1) if (dimension == 3) else 1 - - fieldmodule = region.getFieldmodule() - - # create nodes - - nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - nodetemplate = nodes.createNodetemplate() - nodetemplate.defineField(coordinates) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_VALUE, 1) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS1, 1) - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS2, 1) - if (not serendipity) and (len(outerTubeCoordinates[0]) > 3): - nodetemplate.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D2_DS1DS2, 1) - nodetemplateCross = nodes.createNodetemplate() - nodetemplateCross.defineField(coordinates) - nodetemplateCross.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_VALUE, 1) - nodetemplateCross.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS1, 1) - nodetemplateCross.setValueNumberOfVersions(coordinates, -1, Node.VALUE_LABEL_D_DS2, 1) - - midNodeIds = [] - oFactor = iFactor = 0.0 - for s in range(3): - - # ensure tube nodes are supplied or create here - if not tubeNodeIds[s]: - tubeNodeIds[s] = [] - otx, otd1, otd2 = outerTubeCoordinates[s][:3] - otd12 = None if (serendipity or (len(outerTubeCoordinates[s]) == 3)) \ - else outerTubeCoordinates[s][3] - itx, itd1, itd2 = innerTubeCoordinates[s][:3] if innerTubeCoordinates else (None, None, None) - itd12 = None if ((not innerTubeCoordinates) or serendipity or (len(innerTubeCoordinates[s]) == 3)) \ - else innerTubeCoordinates[s][3] - elementsCountAround = len(otx) - for n3 in range(nodesCountThroughWall): - ringNodeIds = [] - if innerTubeCoordinates: - oFactor = n3 / elementsCountThroughWall - iFactor = 1.0 - oFactor - for n1 in range(elementsCountAround): - node = nodes.createNode(nodeIdentifier, nodetemplate) - fieldcache.setNode(node) - if (not innerTubeCoordinates) or (n3 == elementsCountThroughWall): - rx, rd1, rd2 = otx[n1], otd1[n1], otd2[n1] - elif n3 == 0: - rx, rd1, rd2 = itx[n1], itd1[n1], itd2[n1] - else: - rx = add(mult(otx[n1], oFactor), mult(itx[n1], iFactor)) - rd1 = add(mult(otd1[n1], oFactor), mult(itd1[n1], iFactor)) - rd2 = add(mult(otd2[n1], oFactor), mult(itd2[n1], iFactor)) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, rx) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, rd1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, rd2) - if otd12: - rd12 = otd12[n1] if ((not innerTubeCoordinates) or (n3 == elementsCountThroughWall)) else \ - itd12[n1] if (n3 == 0) else add(mult(otd12[n1], oFactor), mult(itd12[n1], iFactor)) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, rd12) - ringNodeIds.append(nodeIdentifier) - nodeIdentifier += 1 - tubeNodeIds[s].append(ringNodeIds) - - # create nodes around middle half rings - midNodeIds.append([]) - omx, omd1, omd2 = outerMidCoordinates[s][:3] - omd12 = None if (serendipity or (len(outerMidCoordinates[s]) == 3)) else outerMidCoordinates[s][3] - imx, imd1, imd2 = innerMidCoordinates[s][:3] if innerMidCoordinates else (None, None, None) - imd12 = None if ((not innerMidCoordinates) or serendipity or (len(innerMidCoordinates[s]) == 3)) \ - else innerMidCoordinates[s][3] - elementsCountAroundHalf = len(omx) - 1 - for n3 in range(nodesCountThroughWall): - ringNodeIds = [] - if innerTubeCoordinates: - oFactor = n3 / elementsCountThroughWall - iFactor = 1.0 - oFactor - for n1 in range(elementsCountAroundHalf + 1): - cross1 = n1 == 0 - cross2 = n1 == elementsCountAroundHalf - if s > 0: - if cross1: - ringNodeIds.append(midNodeIds[0][n3][0]) - continue - if cross2: - ringNodeIds.append(midNodeIds[0][n3][-1]) - continue - node = nodes.createNode(nodeIdentifier, nodetemplateCross if (cross1 or cross2) else nodetemplate) - fieldcache.setNode(node) - if (not innerMidCoordinates) or (n3 == elementsCountThroughWall): - rx, rd1, rd2 = omx[n1], omd1[n1], omd2[n1] - elif n3 == 0: - rx, rd1, rd2 = imx[n1], imd1[n1], imd2[n1] - else: - rx = add(mult(omx[n1], oFactor), mult(imx[n1], iFactor)) - rd1 = add(mult(omd1[n1], oFactor), mult(imd1[n1], iFactor)) - rd2 = add(mult(omd2[n1], oFactor), mult(imd2[n1], iFactor)) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, rx) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, rd1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, rd2) - if omd12 and not (cross1 or cross2): - rd12 = omd12[n1] if ((not innerMidCoordinates) or (n3 == elementsCountThroughWall)) else \ - imd12[n1] if (n3 == 0) else add(mult(omd12[n1], oFactor), mult(imd12[n1], iFactor)) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, rd12) - ringNodeIds.append(nodeIdentifier) - nodeIdentifier = nodeIdentifier + 1 - - midNodeIds[s].append(ringNodeIds) - - # create elements - - mesh = fieldmodule.findMeshByDimension(dimension) - elementtemplateStd = mesh.createElementtemplate() - elementtemplateMidInward = mesh.createElementtemplate() - elementtemplateMidOutward = mesh.createElementtemplate() - elementtemplateCross = mesh.createElementtemplate() - for et in [elementtemplateStd, elementtemplateMidInward, elementtemplateMidOutward, elementtemplateCross]: - et.setElementShapeType(Element.SHAPE_TYPE_CUBE if (dimension == 3) else Element.SHAPE_TYPE_SQUARE) - basis = fieldmodule.createElementbasis( - dimension, (Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE_SERENDIPITY if serendipity - else Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE)) - if dimension == 3: - basis.setFunctionType(3, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) - eftStd = mesh.createElementfieldtemplate(basis) - eftMidInward = mesh.createElementfieldtemplate(basis) - eftMidOutward = mesh.createElementfieldtemplate(basis) - if (not serendipity) and (len(outerTubeCoordinates[0]) < 4): - # remove cross derivative terms for full bicubic Hermite - ln = [1, 2, 3, 4] if (dimension == 2) else [1, 2, 3, 4, 5, 6, 7, 8] - for eft in [eftStd, eftMidInward, eftMidOutward]: - remapEftNodeValueLabel(eft, ln, Node.VALUE_LABEL_D2_DS1DS2, []) - elementtemplateStd.defineField(coordinates, -1, eftStd) - setEftScaleFactorIds(eftMidInward, [1], []) - scaleEftNodeValueLabels(eftMidInward, [3, 4] if (dimension == 2) else [3, 4, 7, 8], - [Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2], [1]) - elementtemplateMidInward.defineField(coordinates, -1, eftMidInward) - setEftScaleFactorIds(eftMidOutward, [1], []) - scaleEftNodeValueLabels(eftMidOutward, [1, 2] if (dimension == 2) else [1, 2, 5, 6], - [Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2], [1]) - elementtemplateMidOutward.defineField(coordinates, -1, eftMidOutward) - - eftCrossForward = [] - eftCrossForwardScalefactors = [] - for s in range(3): - eftCrossForward.append([]) - eftCrossForwardScalefactors.append([]) - for ce in range(2): # cross end 0 or 1 - eft = mesh.createElementfieldtemplate(basis) - if inward[s]: - lnCross = [3] if (ce == 0) else [4] - lnOther = [4] if (ce == 0) else [3] - lnTube = [1, 2] - lnMid = [3, 4] - else: - lnCross = [1] if (ce == 0) else [2] - lnOther = [2] if (ce == 0) else [1] - lnTube = [3, 4] - lnMid = [1, 2] - if dimension == 3: - lnCross.append(lnCross[0] + 4) - lnOther.append(lnOther[0] + 4) - for i in range(2): - lnTube.append(lnTube[i] + 4) - lnMid.append(lnMid[i] + 4) - if not serendipity: - # remove cross derivative terms for full bicubic Hermite where not supplied - if len(outerTubeCoordinates[0]) < 4: - remapEftNodeValueLabel(eft, lnTube, Node.VALUE_LABEL_D2_DS1DS2, []) - if len(outerMidCoordinates[0]) < 4: - remapEftNodeValueLabel(eft, lnMid, Node.VALUE_LABEL_D2_DS1DS2, []) - else: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D2_DS1DS2, []) - if (not inward[s]) or ((s != 1) and (ce == 0)) or ((s == 1) and (inward[s] or (ce != 0))) or \ - ((s == 2) and inward[s] and (ce != 0)): - setEftScaleFactorIds(eft, [1], []) - scalefactors = [-1.0] - else: - scalefactors = None - eftCrossForwardScalefactors[s].append(scalefactors) - if s == 0: - if ce == 0: - if inward[s]: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS2, [1])]) - else: - scaleEftNodeValueLabels(eft, lnCross, [Node.VALUE_LABEL_D_DS1], [1]) - scaling = [] if inward[s] else [1] - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS2, - [(Node.VALUE_LABEL_D_DS1, scaling), (Node.VALUE_LABEL_D_DS2, scaling)]) - if (ce != 0) and not inward[s]: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS2, [])]) - elif s == 1: - scaling = [] if (ce == 0) else [1] - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS1, scaling), (Node.VALUE_LABEL_D_DS2, scaling)]) - if (ce == 0) and inward[s]: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS2, [(Node.VALUE_LABEL_D_DS1, [1])]) - if ce != 0: - if inward[s]: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS2, - [(Node.VALUE_LABEL_D_DS2, [1])]) - else: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS2, - [(Node.VALUE_LABEL_D_DS1, [])]) - else: # s == 2: - if ce == 0: - if inward[s]: - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS1, [(Node.VALUE_LABEL_D_DS1, [1])]) - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS2, [(Node.VALUE_LABEL_D_DS2, [1])]) - else: - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS1, [(Node.VALUE_LABEL_D_DS2, [1])]) - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS2, [(Node.VALUE_LABEL_D_DS1, [])]) - elif inward[s]: - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS2, [(Node.VALUE_LABEL_D_DS1, [1])]) - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS1, [(Node.VALUE_LABEL_D_DS2, [])]) - if not inward[s]: - scaleEftNodeValueLabels(eft, lnOther, [Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2], [1]) - eftCrossForward[s].append(eft) - - eftCrossReverse = [] - eftCrossReverseScalefactors = [] - for s in range(3): - eftCrossReverse.append([]) - eftCrossReverseScalefactors.append([]) - for ce in range(2): # cross end 0 or 1 - eft = mesh.createElementfieldtemplate(basis) - if inward[s]: - lnCross = [3] if (ce == 0) else [4] - lnOther = [4] if (ce == 0) else [3] - lnTube = [1, 2] - lnMid = [3, 4] - else: - lnCross = [1] if (ce == 0) else [2] - lnOther = [2] if (ce == 0) else [1] - lnTube = [3, 4] - lnMid = [1, 2] - if dimension == 3: - lnCross.append(lnCross[0] + 4) - lnOther.append(lnOther[0] + 4) - for i in range(2): - lnTube.append(lnTube[i] + 4) - lnMid.append(lnMid[i] + 4) - if not serendipity: - # remove cross derivative terms for full bicubic Hermite where not supplied - if len(outerTubeCoordinates[0]) < 4: - remapEftNodeValueLabel(eft, lnTube, Node.VALUE_LABEL_D2_DS1DS2, []) - if len(outerMidCoordinates[0]) < 4: - remapEftNodeValueLabel(eft, lnMid, Node.VALUE_LABEL_D2_DS1DS2, []) - else: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D2_DS1DS2, []) - if inward[s] or (s == 0) or ((s != 2) and (ce == 0)) or ((s == 2) and (ce != 0)): - setEftScaleFactorIds(eft, [1], []) - scalefactors = [-1.0] - else: - scalefactors = None - eftCrossReverseScalefactors[s].append(scalefactors) - if s == 0: - if ce == 0: - if inward[s]: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS2, [1])]) - else: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS1, [1])]) - scaling = [] if inward[s] else [1] - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS2, - [(Node.VALUE_LABEL_D_DS1, scaling), (Node.VALUE_LABEL_D_DS2, scaling)]) - if (ce != 0) and not inward[s]: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, [(Node.VALUE_LABEL_D_DS2, [])]) - elif s == 1: - if ce == 0: - if inward[s]: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS1, [1])]) - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS2, - [(Node.VALUE_LABEL_D_DS2, [1])]) - else: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS2, [1])]) - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS2, [(Node.VALUE_LABEL_D_DS1, [])]) - else: - if inward[s]: - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS2, - [(Node.VALUE_LABEL_D_DS1, [1])]) - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS2, [])]) - else: # s == 2: - scaling = [] if (ce == 0) else [1] - remapEftNodeValueLabel(eft, lnCross, Node.VALUE_LABEL_D_DS1, - [(Node.VALUE_LABEL_D_DS1, scaling), (Node.VALUE_LABEL_D_DS2, scaling)]) - if ce == 0: - if inward[s]: - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS2, [(Node.VALUE_LABEL_D_DS1, [1])]) - else: - if inward[s]: - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS2, [(Node.VALUE_LABEL_D_DS2, [1])]) - else: - remapEftNodeValueLabel( - eft, lnCross, Node.VALUE_LABEL_D_DS2, [(Node.VALUE_LABEL_D_DS1, [])]) - if inward[s]: - scaleEftNodeValueLabels(eft, lnOther, [Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2], [1]) - eftCrossReverse[s].append(eft) - - aroundCounts = [len(outerTubeCoordinates[s][0]) for s in range(3)] - connectionCounts = get_tube_bifurcation_connection_elements_counts(aroundCounts) - - for s in range(3): - - # forward connections - - eftMid = eftStd - elementtemplateMid = elementtemplateStd - scalefactorsMid = None - if not inward[s]: - eftMid = eftMidOutward - elementtemplateMid = elementtemplateMidOutward - scalefactorsMid = [-1.0] - - for e3 in range(elementsCountThroughWall): - for e1 in range(connectionCounts[s]): - eft = eftMid - elementtemplate = elementtemplateMid - scalefactors = scalefactorsMid - - if e1 == 0: - eft = eftCrossForward[s][0] - elementtemplateCross.defineField(coordinates, -1, eft) - elementtemplate = elementtemplateCross - scalefactors = eftCrossForwardScalefactors[s][0] - elif e1 == (connectionCounts[s] - 1): - eft = eftCrossForward[s][1] - elementtemplateCross.defineField(coordinates, -1, eft) - elementtemplate = elementtemplateCross - scalefactors = eftCrossForwardScalefactors[s][1] - - nids = [] - for n3 in ([0] if (dimension == 2) else [e3, e3 + 1]): - if inward[s]: - nStart = crossIndexes[s] - aroundCounts[s] - nids += [tubeNodeIds[s][n3][nStart + e1], tubeNodeIds[s][n3][nStart + e1 + 1], - midNodeIds[s][n3][e1], midNodeIds[s][n3][e1 + 1]] - else: - nStart = crossIndexes[s] - connectionCounts[s] - re1 = connectionCounts[s] - e1 - nids += [midNodeIds[s][n3][re1], midNodeIds[s][n3][re1 - 1], - tubeNodeIds[s][n3][nStart + e1], tubeNodeIds[s][n3][nStart + e1 + 1]] - - element = mesh.createElement(elementIdentifier, elementtemplate) - result1 = element.setNodesByIdentifier(eft, nids) - result2 = element.setScaleFactors(eft, scalefactors) if scalefactors else "-" - # print('create element tube bifurcation forward s', s, elementIdentifier, element.isValid(), result1, result2, nids) - for meshGroup in annotationMeshGroups[s]: - meshGroup.addElement(element) - elementIdentifier += 1 - - # reverse connections - - eftMid = eftStd - elementtemplateMid = elementtemplateStd - scalefactorsMid = None - if inward[s]: - eftMid = eftMidInward - elementtemplateMid = elementtemplateMidInward - scalefactorsMid = [-1.0] - - for e3 in range(elementsCountThroughWall): - for e1 in range(connectionCounts[s - 1]): - eft = eftMid - elementtemplate = elementtemplateMid - scalefactors = scalefactorsMid - - if e1 == 0: - eft = eftCrossReverse[s][0] - elementtemplateCross.defineField(coordinates, -1, eft) - elementtemplate = elementtemplateCross - scalefactors = eftCrossReverseScalefactors[s][0] - elif e1 == (connectionCounts[s - 1] - 1): - eft = eftCrossReverse[s][1] - elementtemplateCross.defineField(coordinates, -1, eft) - elementtemplate = elementtemplateCross - scalefactors = eftCrossReverseScalefactors[s][1] - - nids = [] - for n3 in ([0] if (dimension == 2) else [e3, e3 + 1]): - if inward[s]: - nStart = crossIndexes[s] - connectionCounts[s - 1] - re1 = connectionCounts[s - 1] - e1 - nids += [tubeNodeIds[s][n3][nStart + e1], tubeNodeIds[s][n3][nStart + e1 + 1], - midNodeIds[s - 1][n3][re1], midNodeIds[s - 1][n3][re1 - 1]] - else: - nStart = crossIndexes[s] - aroundCounts[s] - nids += [midNodeIds[s - 1][n3][e1], midNodeIds[s - 1][n3][e1 + 1], - tubeNodeIds[s][n3][nStart + e1], tubeNodeIds[s][n3][nStart + e1 + 1]] - - element = mesh.createElement(elementIdentifier, elementtemplate) - result1 = element.setNodesByIdentifier(eft, nids) - result2 = element.setScaleFactors(eft, scalefactors) if scalefactors else "-" - # print('Tube bifurcation element reverse s', s, elementIdentifier, element.isValid(), result1, result2, nids) - for meshGroup in annotationMeshGroups[s]: - meshGroup.addElement(element) - elementIdentifier += 1 - - return nodeIdentifier, elementIdentifier - - -class SegmentTubeData: - - def __init__(self, pathParameters, elementsCountAround): - self._pathParameters = pathParameters - self._elementsCountAround = elementsCountAround - self._segmentLength = getCubicHermiteCurvesLength(pathParameters[0], pathParameters[1]) - self._rawTubeCoordinates = None - self._rawTrackSurface = None - self._sampledTubeCoordinates = None - self._sampledTrackSurface = None - self._sampledNodeIds = [] # indexed along sample nodes. Only ever set on rows where junctions occur - self._annotationMeshGroups = [] - - def getPathParameters(self): - return self._pathParameters - - def getElementsCountAround(self): - return self._elementsCountAround - - def getSegmentLength(self): - return self._segmentLength - - def getRawTrackSurface(self): - """ - Available after calling setRawTubeCoordinates(). - :return: TrackSurface - """ - return self._rawTrackSurface - - def getRawTubeCoordinates(self): - return self._rawTubeCoordinates - - def setRawTubeCoordinates(self, rawTubeCoordinates): - """ - Set raw tube coordinates at network layout spacing. - Creates TrackSurface internally. - :param rawTubeCoordinates: px, pd1, pd2, pd12 - """ - assert len(rawTubeCoordinates[0][0]) == self._elementsCountAround - self._rawTubeCoordinates = rawTubeCoordinates - px, pd1, pd2, pd12 = rawTubeCoordinates - nx = [] - nd1 = [] - nd2 = [] - nd12 = [] - for i in range(len(px)): - nx += px[i] - nd1 += pd1[i] - nd2 += pd2[i] - nd12 += pd12[i] - self._rawTrackSurface = TrackSurface(len(px[0]), len(px) - 1, nx, nd1, nd2, nd12, loop1=True) - - def getSampledTrackSurface(self): - """ - Available after calling setSampledTubeCoordinates(). - :return: TrackSurface - """ - return self._sampledTrackSurface - - def getSampledTubeCoordinates(self): - return self._sampledTubeCoordinates - - def setSampledTubeCoordinates(self, sampledTubeCoordinates): - """ - :param sampledTubeCoordinates: sx, sd1, sd2, sd12 - """ - assert len(sampledTubeCoordinates[0][0]) == self._elementsCountAround - self._sampledTubeCoordinates = sampledTubeCoordinates - self._sampledNodeIds = [None] * len(self._sampledTubeCoordinates[0]) - px, pd1, pd2, pd12 = sampledTubeCoordinates - nx = [] - nd1 = [] - nd2 = [] - nd12 = [] - for i in range(len(px)): - nx += px[i] - nd1 += pd1[i] - nd2 += pd2[i] - nd12 += pd12[i] - self._sampledTrackSurface = TrackSurface(len(px[0]), len(px) - 1, nx, nd1, nd2, nd12, loop1=True) - - def getSampledElementsCountAlong(self): - """ - Must have previously called setSampledTubeCoordinates - :return: Number of elements along sampled tube. - """ - return len(self._sampledTubeCoordinates[0]) - 1 - - def getStartNodeIds(self, startSkipCount): - """ - Get start nodes for supplying to adjacent tube or bifurcation. - :param startSkipCount: Row in from start that node ids are for. - """ - return self._sampledNodeIds[startSkipCount] - - def setStartNodeIds(self, startNodeIds, startSkipCount): - """ - :param startNodeIds: list of node ids around tube start row. - :param startSkipCount: Row in from start that ids are for. - """ - self._sampledNodeIds[startSkipCount] = startNodeIds - - def getEndNodeIds(self, endSkipCount): - """ - Get end nodes for supplying to adjacent tube or bifurcation. - :param endSkipCount: Row in from end that node ids are for. - """ - return self._sampledNodeIds[self.getSampledElementsCountAlong() - endSkipCount] - - def setEndNodeIds(self, endNodeIds, endSkipCount): - """ - :param endNodeIds: list of node ids around tube end row. - :param endSkipCount: Row in from end that node ids are for. - """ - self._sampledNodeIds[self.getSampledElementsCountAlong() - endSkipCount] = endNodeIds - - def addAnnotationMeshGroup(self, annotationMeshGroup): - """ - Add an annotation mesh group for segment elements to be added to. - :param annotationMeshGroup: Mesh group to add. - """ - self._annotationMeshGroups.append(annotationMeshGroup) - - def getAnnotationMeshGroups(self): - return self._annotationMeshGroups - - -class TubeBifurcationData: - """ - Describes junction between three segments. - Used to get intersection curves and points between them, and trim surfaces for segments. - """ - - def __init__(self, networkSegmentsIn: list, networkSegmentsOut: list, segmentTubeData): - """ - :param networkSegmentsIn: List of input segments. - :param networkSegmentsOut: List of output segments. - :param segmentTubeData: dict NetworkSegment -> SegmentTubeData. - """ - self._networkSegmentsIn = networkSegmentsIn - self._networkSegmentsOut = networkSegmentsOut - self._networkSegments = networkSegmentsIn + networkSegmentsOut - segmentCount = len(self._networkSegments) - assert segmentCount == 3 - self._tubeData = [segmentTubeData[networkSegment] for networkSegment in self._networkSegments] - self._segmentsIn = [self._networkSegments[s] in self._networkSegmentsIn for s in range(3)] - # following are calculated in determineCrossIndexes() - self._connectingCoordinateRings = [[]] * 3 # second row of coordinates from end, made into nodes - self._endCoordinateRings = [[]] * 3 # row of coordinates at end, NOT made into nodes - self._aroundCounts = [0] * 3 - self._connectionCounts = [0] * 3 - self._aCrossIndexes = None - self._bCrossIndexes = None - self._intersectionCurves = [] - self._trimSurfaces = [] - # self._calculateTrimSurfacesFromIntersectionCurves() - self._calculateTrimSurfacesNew() - - def _calculateTrimSurfacesFromIntersectionCurves(self): - assert (not self._intersectionCurves) and (not self._trimSurfaces) - segmentCount = len(self._networkSegments) - assert segmentCount == 3 - - # get intersection curves between pairs of segments - for s in range(segmentCount): - tubeData1 = self._tubeData[s] - tubeTrackSurface1 = tubeData1.getRawTrackSurface() - tubeData2 = self._tubeData[(s + 1) % segmentCount] - tubeTrackSurface2 = tubeData2.getRawTrackSurface() - cx, cd1, cProportions, loop = tubeTrackSurface1.findIntersectionCurve(tubeTrackSurface2) - self._intersectionCurves.append((cx, cd1, cProportions, loop)) - - # get trim surfaces - for s in range(segmentCount): - networkSegment = self._networkSegments[s] - tubeData = self._tubeData[s] - pathParameters = tubeData.getPathParameters() - path_d1 = pathParameters[1] - along = path_d1[-1 if networkSegment in self._networkSegmentsIn else 0] - ax, ad1, _, aloop = self._intersectionCurves[s - 1] - bx, bd1, _, bloop = self._intersectionCurves[s] - trimSurface = None - if ax and bx: - d2a = None - cax = None - ha = len(ax) // 2 # halfway index in a - assert ha > 0 - if not aloop: - # get coordinates and 2nd derivative halfway along a - assert ha * 2 == len(ax) - 1 # must be an even number of elements - d2am = interpolateCubicHermiteSecondDerivative(ax[ha - 1], ad1[ha - 1], ax[ha], ad1[ha], 1.0) - d2ap = interpolateCubicHermiteSecondDerivative(ax[ha], ad1[ha], ax[ha + 1], ad1[ha + 1], 0.0) - d2a = add(d2am, d2ap) - cax = ax[4] - d2b = None - cbx = None - hb = len(bx) // 2 # halfway index in b - assert hb > 0 - if not bloop: - # get coordinates and 2nd derivative halfway along b - assert hb * 2 == len(bx) - 1 # must be an even number of elements - d2bm = interpolateCubicHermiteSecondDerivative(bx[hb - 1], bd1[hb - 1], bx[hb], bd1[hb], 1.0) - d2bp = interpolateCubicHermiteSecondDerivative(bx[hb], bd1[hb], bx[hb + 1], bd1[hb + 1], 0.0) - d2b = add(d2bm, d2bp) - cbx = bx[4] - aNearestLocation = None # (element index, xi) - bNearestLocation = None # (element index, xi) - if aloop and bloop: - aNearestLocation, bNearestLocation = \ - getNearestLocationBetweenCurves(ax, ad1, bx, bd1, aloop, bloop)[:2] - else: - if aloop: - aNearestLocation = getNearestLocationOnCurve(ax, ad1, cbx, aloop)[0] - if bloop: - bNearestLocation = getNearestLocationOnCurve(bx, bd1, cax, bloop)[0] - if aloop and aNearestLocation: - # get coordinates and 2nd derivative at opposite to nearest location on a - assert ha * 2 == len(ax) # must be an even number of elements - oa = aNearestLocation[0] - ha # opposite element index in a - xi = aNearestLocation[1] - d2a = interpolateCubicHermiteSecondDerivative(ax[oa], ad1[oa], ax[oa + 1], ad1[oa + 1], xi) - cax = interpolateCubicHermite(ax[oa], ad1[oa], ax[oa + 1], ad1[oa + 1], xi) - if bloop and bNearestLocation: - # get coordinates and 2nd derivative at opposite to nearest location on b - assert hb * 2 == len(bx) # must be an even number of elements - ob = bNearestLocation[0] - hb # opposite element index in b - xi = bNearestLocation[1] - d2b = interpolateCubicHermiteSecondDerivative(bx[ob], bd1[ob], bx[ob + 1], bd1[ob + 1], xi) - cbx = interpolateCubicHermite(bx[ob], bd1[ob], bx[ob + 1], bd1[ob + 1], xi) - if cax and cbx: - d2b = [-s for s in d2b] # reverse derivative on second point - across = sub(cbx, cax) - sizeAcross = magnitude(across) - normAcross = normalize(across) - up = cross(across, along) - cad2 = normalize(ad1[4]) - if dot(up, cad2) < 0.0: - cad2 = [-d for d in cad2] - cbd2 = normalize(bd1[4]) - if dot(up, cbd2) < 0.0: - cbd2 = [-d for d in cbd2] - # move coordinates out slightly to guarantee intersection - arcLength = computeCubicHermiteArcLength(cax, d2a, cbx, d2b, rescaleDerivatives=True) - d2a = mult(d2a, arcLength / magnitude(d2a)) - # cax = sub(cax, mult(d2a, 0.05)) - d2b = mult(d2b, arcLength / magnitude(d2b)) - # cbx = add(cbx, mult(d2b, 0.05)) - nx = [] - nd1 = [] - nd2 = [] - elementsCount2 = 2 - for i in range(elementsCount2 + 1): - delta = (i - elementsCount2 // 2) * 1.2 * sizeAcross - vax = add(cax, mult(cad2, delta)) - vbx = add(cbx, mult(cbd2, delta)) - deltaAcross = sub(vbx, vax) - normDeltaAcross = normalize(deltaAcross) - use_d2a = d2a - use_d2b = d2b - if dot(normAcross, normDeltaAcross) <= 0.5: - use_d2a = deltaAcross - use_d2b = deltaAcross - arcLength = 1.5 * computeCubicHermiteArcLength(vax, use_d2a, vbx, use_d2b, rescaleDerivatives=True) - vad1 = mult(use_d2a, arcLength / magnitude(use_d2a)) - vbd1 = mult(use_d2b, arcLength / magnitude(use_d2b)) - vad2 = mult(cad2, sizeAcross) - vbd2 = mult(cbd2, sizeAcross) - # add extensions before / after - nx.append(sub(vax, vad1)) - nx.append(vax) - nd1.append(vad1) - nd1.append(vad1) - nd2.append(vad2) - nd2.append(vad2) - nx.append(vbx) - nx.append(add(vbx, vbd1)) - nd1.append(vbd1) - nd1.append(vbd1) - nd2.append(vbd2) - nd2.append(vbd2) - # smooth extension d2 - for j in [0, 3]: - tx = [nx[i * 4 + j] for i in range(3)] - td2 = smoothCubicHermiteDerivativesLine(tx, [nd2[i * 4 + j] for i in range(3)]) - for i in range(3): - nd2[i * 4 + j] = td2[i] - trimSurface = TrackSurface(3, elementsCount2, nx, nd1, nd2) - elif ax: - # use previous tube surface as trim surface - trimSurface = self._tubeData[s - 1].getRawTrackSurface() - elif bx: - # use next tube surface as trim surface - trimSurface = self._tubeData[s - 2].getRawTrackSurface() - self._trimSurfaces.append(trimSurface) - - def _calculateTrimSurfacesNew(self): - assert not self._trimSurfaces - segmentCount = len(self._networkSegments) - assert segmentCount == 3 - - for s in range(segmentCount): - self._intersectionCurves.append(None) - - dirEnd = [] - for s in range(segmentCount): - tubeData = self._tubeData[s] - pathParameters = tubeData.getPathParameters() - endIndex = -1 if self._segmentsIn[s] else 0 - dirEnd.append(normalize(pathParameters[1][endIndex])) - - for s in range(segmentCount): - networkSegment = self._networkSegments[s] - tubeData = self._tubeData[s] - pathParameters = tubeData.getPathParameters() - endIndex = -1 if self._segmentsIn[s] else 0 - # d1End = pathParameters[1][endIndex] - d2End = pathParameters[2][endIndex] - d3End = pathParameters[4][endIndex] - # get index of other direction most normal to this segment direction - sos = ((s - 1), (s - 2)) if (abs(dot(dirEnd[s], dirEnd[s - 1])) < abs(dot(dirEnd[s], dirEnd[s - 2]))) \ - else ((s - 2), (s - 1)) - xEnd = pathParameters[0][endIndex] - endEllipseNormal = normalize(cross(d2End, d3End)) - # get components of so1 direction aligned with d2, d3 and compute initial phase - oDirEnd = dirEnd[sos[0]] - if self._segmentsIn[sos[0]]: - oDirEnd = [-d for d in oDirEnd] - dx = dot(oDirEnd, d2End) - dy = dot(oDirEnd, d3End) - phaseAngle = math.atan2(dy, dx) - # if s == 0: - # print("oDirEnd", oDirEnd, "d2", dx, "d3", dy, "phaseAngle", math.degrees(phaseAngle)) - # GRC Future: get exact phase angle with non-linear optimisation! - elementsCountAround = 6 - tubeCoordinates = getPathRawTubeCoordinates( - pathParameters, elementsCountAround, radius=1.0, phaseAngle=phaseAngle) - px, pd1, pd2, pd12 = tubeCoordinates - nx = [] - nd1 = [] - nd2 = [] - nd12 = [] - for i in range(len(px)): - nx += px[i] - nd1 += pd1[i] - nd2 += pd2[i] - nd12 += pd12[i] - tmpTrackSurface = TrackSurface(len(px[0]), len(px) - 1, nx, nd1, nd2, nd12, loop1=True) - pointsCountAlong = len(pathParameters[0]) - # get coordinates and directions of intersection points of longitudinal lines and other track surfaces - rx = [] - rd1 = [] - trim = False - # if s == 0: - # print("sos", (sos[0] % 3) + 1, (sos[1] % 3) + 1) - trimIndex = 0 - for n1 in range(elementsCountAround): - proportion1 = n1 / elementsCountAround - cx = [tubeCoordinates[0][n2][n1] for n2 in range(pointsCountAlong)] - cd2 = [tubeCoordinates[2][n2][n1] for n2 in range(pointsCountAlong)] - ox = x = tubeCoordinates[0][endIndex][n1] - d1 = tubeCoordinates[1][endIndex][n1] - maxProportionFromEnd = 0.0 - for so in sos: - otherTrackSurface = self._tubeData[so].getRawTrackSurface() - otherSurfacePosition, curveLocation, isIntersection = \ - otherTrackSurface.findNearestPositionOnCurve( - cx, cd2, loop=False, sampleEnds=False, sampleHalf=2 if self._segmentsIn[s] else 1) - if isIntersection: - proportion2 = (curveLocation[0] + curveLocation[1]) / (pointsCountAlong - 1) - proportionFromEnd = abs(proportion2 - (1.0 if self._segmentsIn[s] else 0.0)) - if proportionFromEnd > maxProportionFromEnd: - trim = True - trimIndex = (so % 3) + 1 - surfacePosition = tmpTrackSurface.createPositionProportion(proportion1, proportion2) - x, d1, d2 = tmpTrackSurface.evaluateCoordinates(surfacePosition, derivatives=True) - n = cross(d1, d2) - ox, od1, od2 = otherTrackSurface.evaluateCoordinates( - otherSurfacePosition, derivatives=True) - on = cross(od1, od2) - d1 = cross(n, on) - maxProportionFromEnd = proportionFromEnd - # if s == 0: - # print(" ", n1, "trim", trimIndex, "max", maxProportionFromEnd, "x", x, "d1", d1, sub(ox, x)) - # ensure d1 directions go around in same direction as loop - if dot(endEllipseNormal, cross(sub(x, xEnd), d1)) < 0.0: - d1 = [-d for d in d1] - rx.append(x) - rd1.append(d1) - if trim: - rd1 = smoothCubicHermiteDerivativesLoop(rx, rd1, fixAllDirections=True, - magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - rd2 = [sub(rx[n1], xEnd) for n1 in range(elementsCountAround)] - rd12 = smoothCurveSideCrossDerivatives(rx, rd1, [rd2], loop=True)[0] - nx = [] - nd1 = [] - nd2 = [] - nd12 = [] - for factor in (0.5, 1.5): - for n1 in range(elementsCountAround): - d2 = sub(rx[n1], xEnd) - x = add(xEnd, mult(d2, factor)) - d1 = mult(rd1[n1], factor) - d12 = mult(rd12[n1], factor) - nx.append(x) - nd1.append(d1) - nd2.append(d2) - nd12.append(d12) - trimSurface = TrackSurface(elementsCountAround, 1, nx, nd1, nd2, nd12, loop1=True) - self._trimSurfaces.append(trimSurface) - else: - self._trimSurfaces.append(None) - - def getIntersectionCurve(self, s): - """ - :param s: Index from 0 to 2 - :return: (cx, cd1, cProportions on TrackSurface s, loop) - """ - return self._intersectionCurves[s] - - def getCrossIndexes(self): - return self._aCrossIndexes - - def getMidCoordinates(self): - return self._midCoordinates - - def getSegmentsIn(self): - return self._segmentsIn - - def getSegmentTrimSurface(self, networkSegment): - """ - :return: Trim TrackSurface - """ - return self.getTrimSurface(self._networkSegments.index(networkSegment)) - - def getTrimSurface(self, s): - """ - :param s: Index from 0 to 2 - :return: Trim TrackSurface - """ - return self._trimSurfaces[s] - - def getConnectingTubeCoordinates(self): - """ - Get ring of coordinates of attached tubes -- 1 row in from end sampled coordinate rings. - :return: List[4] of coordinates around ring: 4 values are x, d1, d2, d12. - """ - return self._connectingCoordinateRings - - def getTubeData(self): - return self._tubeData - - def _getConnectionCounts(self): - for s in range(3): - tubeData = self._tubeData[s] - row = -2 if self._segmentsIn[s] else 1 - sampledTubeCoordinates = tubeData.getSampledTubeCoordinates() - self._connectingCoordinateRings[s] = [value[row] for value in sampledTubeCoordinates] - endRow = -1 if self._segmentsIn[s] else 0 - self._endCoordinateRings[s] = [value[endRow] for value in sampledTubeCoordinates] - self._aroundCounts[s] = len(self._connectingCoordinateRings[s][0]) - self._connectionCounts = get_tube_bifurcation_connection_elements_counts(self._aroundCounts) - - def copyCrossIndexes(self, sourceTubeBifurcationData): - assert self._aCrossIndexes is None # should only call once - self._getConnectionCounts() - self._aCrossIndexes = sourceTubeBifurcationData._aCrossIndexes - self._bCrossIndexes = sourceTubeBifurcationData._bCrossIndexes - - def determineCrossIndexes(self): - """ - Determine the node indexes around the tubes which give the least distorted 3-way crossing point. - Only call for one of inner or outer bifurcation data (outer is recommendd). For the other (inner) - use copyCrossIndexes(outerTubeBifurcationData) - """ - assert self._aCrossIndexes is None # should only call once - self._getConnectionCounts() - - # get cross points where there is the shortest sum of distance directly connected points on adjacent tubes - minDist = None - aCrossIndexes = [None, None, None] - for i in range(self._aroundCounts[0]): - aCrossIndexes[0] = i - for j in range(self._aroundCounts[1]): - aCrossIndexes[1] = j - for k in range(self._aroundCounts[2]): - aCrossIndexes[2] = k - dist = 0.0 - for s in range(3): - ring1 = self._endCoordinateRings[s] - ring2 = self._endCoordinateRings[s - 2] - ic1 = aCrossIndexes[s] - ic2 = aCrossIndexes[s - 2] - for n in range(1, self._connectionCounts[s]): - i1 = (ic1 + n) % self._aroundCounts[s] if self._segmentsIn[s] else ic1 - n - i2 = (ic2 - n) if self._segmentsIn[s - 2] else (ic2 + n) % self._aroundCounts[s - 2] - delta = sub(ring1[0][i1], ring2[0][i2]) - dist += magnitude(delta) - if (minDist is None) or (dist < minDist): - self._aCrossIndexes = copy.copy(aCrossIndexes) - minDist = dist - self._bCrossIndexes = [ - (self._aCrossIndexes[s] + (self._connectionCounts[s] if self._segmentsIn[s] else -self._connectionCounts[s])) % self._aroundCounts[s] for s in range(3)] - # print("aCrossIndexes", self._aCrossIndexes, "bCrossIndexes", self._bCrossIndexes) - - def _interpolateMidPoint(self, s1, i1, s2, i2): - """ - Calculate position and derivatives of mid-point between two tubes. - Algorithm goes halfway from connecting node to end of sampled track surface, - calculates in-plane direction to the same point on other tube, then blends that direction - with the outward d2 to get a curve on which to sample the coordinates and direction of the mid-point. - The mid-point derivative is smoothed from the connecting node coordinates and derivatives. - :param s1: First tube index from 0 to 2. - :param i1: Node index around first tube. - :param s2: Second tube index from 0 to 2. - :param i2: Node index around second tube. - :return: Mid-point x, d2. - """ - trackSurface1 = self._tubeData[s1].getSampledTrackSurface() - position1 = TrackSurfacePosition( - i1, (trackSurface1.getElementsCount2() - 1) if self._segmentsIn[s1] else 0,0.0, 0.5) - p1x, p1d1, p1d2 = trackSurface1.evaluateCoordinates(position1, derivatives=True) - trackSurface2 = self._tubeData[s2].getSampledTrackSurface() - position2 = TrackSurfacePosition( - i2, (trackSurface2.getElementsCount2() - 1) if self._segmentsIn[s2] else 0,0.0, 0.5) - p2x, p2d1, p2d2 = trackSurface2.evaluateCoordinates(position2, derivatives=True) - sideFactor = 1.0 - outFactor = 0.5 - direction = normalize(sub(p2x, p1x)) - p1dxi1, p1dxi2 = mult(normalize(calculate_surface_delta_xi(p1d1, p1d2, direction)), sideFactor) - p1dxi2 += outFactor if self._segmentsIn[s1] else -outFactor - s1d2 = add(mult(p1d1, p1dxi1), mult(p1d2, p1dxi2)) - p2dxi1, p2dxi2 = mult(normalize(calculate_surface_delta_xi(p2d1, p2d2, direction)), sideFactor) - p2dxi2 += -outFactor if self._segmentsIn[s2] else outFactor - s2d2 = add(mult(p2d1, p2dxi1), mult(p2d2, p2dxi2)) - scaling = computeCubicHermiteDerivativeScaling(p1x, s1d2, p2x, s2d2) - f1d2 = mult(s1d2, scaling) - f2d2 = mult(s2d2, scaling) - # print("mag", magnitude(f1d2), "-", magnitude(f2d2)) - mx = interpolateCubicHermite(p1x, f1d2, p2x, f2d2, 0.5) - md2 = interpolateCubicHermiteDerivative(p1x, f1d2, p2x, f2d2, 0.5) - return mx, md2 - - def _interpolateCrossPoint(self, crossIndexes, swap23): - """ - Calculate position and derivatives of mid-point between three tubes. - Algorithm gets the mean of the mid-points between each pair of tubes, - computes and averages mid-point derivatives from Hermite-Lagrange interpolation, - the re-smooths to fit the tube connecting coordinates and derivatives. - :param crossIndexes: Index of nodes for each ring which connect to cross point. - :param swap23: True if on the first cross point, False if on the other to swap the second and third tubes. - :return: Cross point x, d1, d2. - """ - s1i = [0, 2, 1] if swap23 else [0, 1, 2] - s2i = [2, 1, 0] if swap23 else [1, 2, 0] - px = [] - pd = [] - hx = [] - hd2 = [] - for s in range(3): - s1 = s1i[s] - px.append(self._connectingCoordinateRings[s1][0][crossIndexes[s1]]) - d2 = self._connectingCoordinateRings[s1][2][crossIndexes[s1]] - pd.append([-d for d in d2] if self._segmentsIn[s1] else d2) - s2 = s2i[s] - x, d2 = self._interpolateMidPoint(s1, crossIndexes[s1], s2, crossIndexes[s2]) - hx.append(x) - hd2.append(d2) - cx = [(hx[0][c] + hx[1][c] + hx[2][c]) / 3.0 for c in range(3)] - hd = [interpolateLagrangeHermiteDerivative(cx, px[s], pd[s], 0.0) for s in range(3)] - ns = [cross(hd[s1i[s]], hd[s2i[s]]) for s in range(3)] - # ns = [cross(hd2[s1i[s]], hd2[s2i[s]]) for s in range(3)] - normal = normalize([(ns[0][c] + ns[1][c] + ns[2][c]) for c in range(3)]) - sd = [smoothCubicHermiteDerivativesLine([cx, px[s]], [normalize(cross(normal, cross(hd[s], normal))), pd[s]], - fixStartDirection=True, fixEndDerivative=True)[0] for s in range(3)] - cd1 = mult(sub(sd[1], add(sd[2], sd[0])), 0.5) - cd2 = mult(sub(sd[2], add(sd[0], sd[1])), 0.5) - return cx, cd1, cd2 - - def determineMidCoordinates(self): - """ - Get 3 half-rings of coordinates between the tube coordinate rings. - The order is between the 1-2, 2-3 and 3-1 tubes. - Each half ring goes from the first cross point to the second. - Coordinates at the cross points are averaged. - """ - assert self._aCrossIndexes is not None # must call copyCrossIndexes() or determineCrossIndexes() first - # get cross points a and b - acx, acd1, acd2 = self._interpolateCrossPoint(self._aCrossIndexes, swap23=False) - bcx, bcd1, bcd2 = self._interpolateCrossPoint(self._bCrossIndexes, swap23=True) - - self._midCoordinates = [] - for s in range(3): - self._midCoordinates.append([[acx], [acd1], [acd2]]) - s1 = s - s2 = (s + 1) % 3 - ring1 = self._connectingCoordinateRings[s1] - ring2 = self._connectingCoordinateRings[s2] - for n in range(1, self._connectionCounts[s]): - i1 = (self._aCrossIndexes[s1] + n) % self._aroundCounts[s1] if self._segmentsIn[s1] \ - else self._aCrossIndexes[s] - n - i2 = self._aCrossIndexes[s2] - n if self._segmentsIn[s2] \ - else (self._aCrossIndexes[s2] + n) % self._aroundCounts[s2] - hx, hd2 = self._interpolateMidPoint(s1, i1, s2, i2) - # # print("s", s, "n", n, "/", self._connectionCounts[s], "i1", i1, "i2", i2) - t1x = ring1[0][i1] - r1d2 = ring1[2][i1] if self._segmentsIn[s] else [-d for d in ring1[2][i1]] - t2x = ring2[0][i2] - r2d2 = [-d for d in ring2[2][i2]] if self._segmentsIn[s - 2] else ring2[2][i2] - hd1 = [0.0, 0.0, 0.0] - hd2 = smoothCubicHermiteDerivativesLine([t1x, hx, t2x], [r1d2, hd2, r2d2], - fixStartDerivative=True, fixEndDerivative=True)[1] - self._midCoordinates[s][0].append(hx) - self._midCoordinates[s][1].append(hd1) - self._midCoordinates[s][2].append(hd2) - self._midCoordinates[s][0].append(bcx) - self._midCoordinates[s][1].append(bcd1) - self._midCoordinates[s][2].append(bcd2) - - # smooth around loops through hex points to get d1 - loopx = self._midCoordinates[s][0] - startd1 = [-d for d in acd2] if (s == 0) else \ - add(acd1, acd2) if (s == 1) else \ - [-d for d in acd1] - endd1 = acd1 if (s == 0) else \ - [-d for d in add(acd1, acd2)] if (s == 1) else \ - acd2 - loopd1 = [startd1] + self._midCoordinates[s][1][1:-1] + [endd1] - loopd1 = smoothCubicHermiteDerivativesLine(loopx, loopd1, fixStartDerivative=True, fixEndDerivative=True, - magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - self._midCoordinates[s][1][1:-1] = loopd1[1:-1] - - -def blendNetworkNodeCoordinates(networkNode, segmentTubeDataList): - """ - Blend coordinate d2 between segments connecting at networkNode if sharing the same version. - Must only call after all tube data has been resampled. - :param networkNode: The node to blend coordinates at. - :param segmentTubeDataList: List of dict mapping NetworkSegment to TubeData. Assumes all same structure. - :return: None - """ - inSegments = networkNode.getInSegments() - outSegments = networkNode.getOutSegments() - nodeVersionSegments = {} # map from version number to list of segments using it - for segment in (inSegments + outSegments): - nodeVersions = segment.getNodeVersions() - nodeIndex = -1 if (segment in inSegments) else 0 - nodeVersion = nodeVersions[nodeIndex] - nodeVersionSegment = nodeVersionSegments.get(nodeVersion) - if not nodeVersionSegment: - nodeVersionSegments[nodeVersion] = nodeVersionSegment = [] - nodeVersionSegment.append(segment) - for nodeVersion, segments in nodeVersionSegments.items(): - if len(segments) < 2: - continue # no blending required - for segmentTubeData in segmentTubeDataList: - d2Rings = [] - nodesCountAround = None - for segment in segments: - tubeData = segmentTubeData[segment] - nodeIndex = -1 if segment in inSegments else 0 - d2Ring = tubeData.getSampledTubeCoordinates()[2][nodeIndex] - if (not d2Rings) or (len(d2Ring) == nodesCountAround): - d2Rings.append(d2Ring) - nodesCountAround = len(d2Ring) - else: - print("Cannot blend d1 version " + str(nodeVersion) + " at layout node " + - str(networkNode.getNodeIdentifier()) + " due to mismatch in number around") - break - ringCount = len(d2Rings) - if ringCount < 2: - break - for n in range(nodesCountAround): - # harmonic mean magnitude; directions are the same as same version - sum = 0.0 - ringMags = [] - for d2Ring in d2Rings: - ringMag = magnitude(d2Ring[n]) - ringMags.append(ringMag) - if ringMag == 0.0: - sum = 0.0 - break - sum += 1.0 / ringMag - mag = (ringCount / sum) if (sum != 0.0) else 0.0 - for r in range(ringCount): - d2Ring = d2Rings[r] - if ringMags[r] > 0.0: - d2 = mult(d2Ring[n], mag / ringMags[r]) - for c in range(3): - d2Ring[n][c] = d2[c] - - -def generateTubeBifurcationTree(networkMesh: NetworkMesh, region, coordinates, nodeIdentifier, elementIdentifier, - defaultElementsCountAround: int, targetElementDensityAlongLongestSegment: float, - elementsCountThroughWall: int, layoutAnnotationGroups: list=[], - annotationElementsCountsAround: list=[], - serendipity=False, showTrimSurfaces=False): - """ - Generate a 2D, or 3D (thick walled) tube bifurcation tree mesh. - :param networkMesh: Specification of network path and lateral sizes. - :param region: Zinc region to generate mesh in. - :param coordinates: Finite element coordinate field to define. - :param nodeIdentifier: First node identifier to use. - :param elementIdentifier: First 2D element identifier to use. - :param defaultElementsCountAround: Number of elements around tube, optionally modified per segment. - :param targetElementDensityAlongLongestSegment: Target number of elements along the longest segment, used to - calculate target element length along all segments of network. - :param elementsCountThroughWall: Number of elements through wall if inner coordinates provided to make 3D elements, - otherwise 1. - :param layoutAnnotationGroups: Optional list of annotations defined on layout mesh for networkMesh. - :param annotationElementsCountsAround: Optional list of elements around annotation groups in the same order as - layoutAnnotationGroups. A value 0 means ignore; a shorter list means assume 0 for all remaining annotation groups. - :param serendipity: True to use Hermite serendipity basis, False for regular Hermite with zero cross derivatives. - :param showTrimSurfaces: Set to True to make surfaces from inter-tube trim surfaces. For diagnostic use. - :return: next node identifier, next element identifier, annotationGroups. - """ - layoutRegion = networkMesh.getRegion() - layoutFieldmodule = layoutRegion.getFieldmodule() - layoutNodes = layoutFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - layoutCoordinates = layoutFieldmodule.findFieldByName("coordinates").castFiniteElement() - layoutInnerCoordinates = layoutFieldmodule.findFieldByName("inner coordinates").castFiniteElement() - if not layoutInnerCoordinates.isValid(): - layoutInnerCoordinates = None - dimension = 3 if layoutInnerCoordinates else 2 - layoutMesh = layoutFieldmodule.findMeshByDimension(1) - assert (elementsCountThroughWall == 1) or (layoutInnerCoordinates and (elementsCountThroughWall >= 1)) - - fieldmodule = region.getFieldmodule() - mesh = fieldmodule.findMeshByDimension(dimension) - fieldcache = fieldmodule.createFieldcache() - - # make tube mesh annotation groups from 1D network layout annotation groups - annotationGroups = [] - layoutAnnotationMeshGroupMap = [] # List of tuples of layout annotation mesh group to final mesh group - for layoutAnnotationGroup in layoutAnnotationGroups: - if layoutAnnotationGroup.getDimension() == 1: - annotationGroup = AnnotationGroup(region, layoutAnnotationGroup.getTerm()) - annotationGroups.append(annotationGroup) - layoutAnnotationMeshGroupMap.append( - (layoutAnnotationGroup.getMeshGroup(layoutMesh), annotationGroup.getMeshGroup(mesh))) - - valueLabels = [ - Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, - Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D2_DS1DS2, - Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3] - - networkSegments = networkMesh.getNetworkSegments() - - # map from NetworkSegment to SegmentTubeData - outerSegmentTubeData = {} - innerSegmentTubeData = {} if layoutInnerCoordinates else None - longestSegmentLength = 0.0 - for networkSegment in networkSegments: - pathParameters = get_nodeset_path_ordered_field_parameters( - layoutNodes, layoutCoordinates, valueLabels, - networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions()) - elementsCountAround = defaultElementsCountAround - i = 0 - for layoutAnnotationGroup in layoutAnnotationGroups: - if i >= len(annotationElementsCountsAround): - break - annotationElementsCountAround = annotationElementsCountsAround[i] - if annotationElementsCountAround > 0: - if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationGroup.getMeshGroup(layoutMesh)): - elementsCountAround = annotationElementsCountAround - # print("Segment in group", i, "using", elementsCountAround, "elements around") - break - i += 1 - outerSegmentTubeData[networkSegment] = tubeData = SegmentTubeData(pathParameters, elementsCountAround) - px, pd1, pd2, pd12 = getPathRawTubeCoordinates(pathParameters, elementsCountAround) - tubeData.setRawTubeCoordinates((px, pd1, pd2, pd12)) - segmentLength = tubeData.getSegmentLength() - if segmentLength > longestSegmentLength: - longestSegmentLength = segmentLength - if layoutInnerCoordinates: - innerPathParameters = get_nodeset_path_ordered_field_parameters( - layoutNodes, layoutInnerCoordinates, valueLabels, - networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions()) - innerSegmentTubeData[networkSegment] = innerTubeData = SegmentTubeData( - innerPathParameters, elementsCountAround) - px, pd1, pd2, pd12 = getPathRawTubeCoordinates(innerPathParameters, elementsCountAround) - innerTubeData.setRawTubeCoordinates((px, pd1, pd2, pd12)) - for layoutAnnotationMeshGroup, annotationMeshGroup in layoutAnnotationMeshGroupMap: - if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationMeshGroup): - tubeData.addAnnotationMeshGroup(annotationMeshGroup) - if layoutInnerCoordinates: - innerTubeData.addAnnotationMeshGroup(annotationMeshGroup) - if longestSegmentLength > 0.0: - targetElementLength = longestSegmentLength / targetElementDensityAlongLongestSegment - else: - targetElementLength = 1.0 - - # map from NetworkNodes to bifurcation data, resample tube coordinates to fit bifurcation - outerNodeTubeBifurcationData = {} - innerNodeTubeBifurcationData = {} if layoutInnerCoordinates else None - allSegmentTubeData = [outerSegmentTubeData] - if layoutInnerCoordinates: - allSegmentTubeData.append(innerSegmentTubeData) - for segmentTubeData in allSegmentTubeData: - nodeTubeBifurcationData = innerNodeTubeBifurcationData if (segmentTubeData is innerSegmentTubeData) else \ - outerNodeTubeBifurcationData - for networkSegment in networkSegments: - tubeData = segmentTubeData[networkSegment] - rawTubeCoordinates = tubeData.getRawTubeCoordinates() - segmentNodes = networkSegment.getNetworkNodes() - startSegmentNode = segmentNodes[0] - startTubeBifurcationData = nodeTubeBifurcationData.get(startSegmentNode) - startSurface = None - newBifurcationData = [] - if not startTubeBifurcationData: - startInSegments = startSegmentNode.getInSegments() - startOutSegments = startSegmentNode.getOutSegments() - if (len(startInSegments) + len(startOutSegments)) == 3: - startTubeBifurcationData = TubeBifurcationData(startInSegments, startOutSegments, segmentTubeData) - nodeTubeBifurcationData[startSegmentNode] = startTubeBifurcationData - newBifurcationData.append(startTubeBifurcationData) - if startTubeBifurcationData: - startSurface = startTubeBifurcationData.getSegmentTrimSurface(networkSegment) - endSegmentNode = segmentNodes[-1] - endTubeBifurcationData = nodeTubeBifurcationData.get(endSegmentNode) - endSurface = None - if not endTubeBifurcationData: - endInSegments = endSegmentNode.getInSegments() - endOutSegments = endSegmentNode.getOutSegments() - if (len(endInSegments) + len(endOutSegments)) == 3: - # print("create end", networkSegment, endSegmentNode) - endTubeBifurcationData = TubeBifurcationData(endInSegments, endOutSegments, segmentTubeData) - nodeTubeBifurcationData[endSegmentNode] = endTubeBifurcationData - newBifurcationData.append(endTubeBifurcationData) - if endTubeBifurcationData: - endSurface = endTubeBifurcationData.getSegmentTrimSurface(networkSegment) - if segmentTubeData is outerSegmentTubeData: - segmentLength = tubeData.getSegmentLength() - elementsCountAlong = max(1, math.ceil(segmentLength / targetElementLength)) - loop = (len(startSegmentNode.getInSegments()) == 1) and \ - (startSegmentNode.getInSegments()[0] is networkSegment) and \ - (networkSegment.getNodeVersions()[0] == networkSegment.getNodeVersions()[-1]) - if (elementsCountAlong == 1) and startTubeBifurcationData and endTubeBifurcationData: - # at least 2 segments if bifurcating at both ends - elementsCountAlong = 2 - elif (elementsCountAlong < 2) and loop: - # at least 2 segments around loop - elementsCountAlong = 2 - else: - # must match count from outer surface! - outerTubeData = outerSegmentTubeData[networkSegment] - elementsCountAlong = outerTubeData.getSampledElementsCountAlong() - sx, sd1, sd2, sd12 = resampleTubeCoordinates( - rawTubeCoordinates, elementsCountAlong, startSurface=startSurface, endSurface=endSurface) - tubeData.setSampledTubeCoordinates((sx, sd1, sd2, sd12)) - del segmentTubeData - - # blend coordinates where versions are shared between segments - blendedNetworkNodes = set() - for networkSegment in networkSegments: - segmentNodes = networkSegment.getNetworkNodes() - for segmentNode in [segmentNodes[0], segmentNodes[-1]]: - if segmentNode not in blendedNetworkNodes: - blendNetworkNodeCoordinates(segmentNode, allSegmentTubeData) - blendedNetworkNodes.add(segmentNode) - del blendedNetworkNodes - - completedBifurcations = set() # record so only done once - with ChangeManager(fieldmodule): - for networkSegment in networkSegments: - segmentNodes = networkSegment.getNetworkNodes() - startSegmentNode = segmentNodes[0] - startInSegments = startSegmentNode.getInSegments() - startOutSegments = startSegmentNode.getOutSegments() - startSkipCount = 1 if ((len(startInSegments) + len(startOutSegments)) > 2) else 0 - endSegmentNode = segmentNodes[-1] - endInSegments = endSegmentNode.getInSegments() - endOutSegments = endSegmentNode.getOutSegments() - endSkipCount = 1 if ((len(endInSegments) + len(endOutSegments)) > 2) else 0 - - for stage in range(3): - if stage == 1: - # tube - outerTubeData = outerSegmentTubeData[networkSegment] - outerTubeCoordinates = outerTubeData.getSampledTubeCoordinates() - innerTubeData = innerSegmentTubeData[networkSegment] if layoutInnerCoordinates else None - innerTubeCoordinates = innerTubeData.getSampledTubeCoordinates() if layoutInnerCoordinates else None - startNodeIds = outerTubeData.getStartNodeIds(startSkipCount) - if (not startNodeIds) and (startSkipCount == 0) and (startInSegments or startOutSegments): - # discover start nodes from single adjacent segment - if startInSegments: - startNodeIds = outerSegmentTubeData[startInSegments[0]].getEndNodeIds(0) - else: - startNodeIds = outerSegmentTubeData[startOutSegments[0]].getStartNodeIds(0) - if startNodeIds: - outerTubeData.setStartNodeIds(startNodeIds, startSkipCount) - endNodeIds = outerTubeData.getEndNodeIds(endSkipCount) - if (not endNodeIds) and (endSkipCount == 0) and (endOutSegments or endInSegments): - # discover end nodes from single adjacent segment - if endOutSegments: - endNodeIds = outerSegmentTubeData[endOutSegments[0]].getStartNodeIds(0) - elif endInSegments: - endNodeIds = outerSegmentTubeData[endInSegments[0]].getEndNodeIds(0) - if endNodeIds: - outerTubeData.setEndNodeIds(endNodeIds, endSkipCount) - loop = (len(startInSegments) == 1) and (startInSegments[0] is networkSegment) and \ - (networkSegment.getNodeVersions()[0] == networkSegment.getNodeVersions()[-1]) - nodeIdentifier, elementIdentifier, startNodeIds, endNodeIds = generateTube( - outerTubeCoordinates, innerTubeCoordinates, elementsCountThroughWall, - region, fieldcache, coordinates, nodeIdentifier, elementIdentifier, - startSkipCount=startSkipCount, endSkipCount=endSkipCount, - startNodeIds=startNodeIds, endNodeIds=endNodeIds, - annotationMeshGroups=outerTubeData.getAnnotationMeshGroups(), - loop=loop, serendipity=serendipity) - outerTubeData.setStartNodeIds(startNodeIds, startSkipCount) - outerTubeData.setEndNodeIds(endNodeIds, endSkipCount) - - if (len(startInSegments) == 1) and (startSkipCount == 0): - # copy startNodeIds to end of last segment - inTubeData = outerSegmentTubeData[startInSegments[0]] - inTubeData.setEndNodeIds(startNodeIds, 0) - if (len(endOutSegments) == 1) and (endSkipCount == 0): - # copy endNodesIds to start of next segment - outTubeData = outerSegmentTubeData[endOutSegments[0]] - outTubeData.setStartNodeIds(endNodeIds, 0) - else: - # start, end bifurcation - outerTubeBifurcationData = outerNodeTubeBifurcationData.get( - startSegmentNode if (stage == 0) else endSegmentNode) - if outerTubeBifurcationData and not outerTubeBifurcationData in completedBifurcations: - # if showIntersectionCurves: - # lineIdentifier = None - # for s in range(3): - # curve = outerTubeBifurcationData.getIntersectionCurve(s) - # cx, cd1, cProportions, loop = curve - # if cx: - # nodeIdentifier, lineIdentifier = \ - # generateCurveMesh(region, cx, cd1, loop, nodeIdentifier, lineIdentifier) - if showTrimSurfaces: - faceIdentifier = elementIdentifier if (dimension == 2) else None - for s in range(3): - trimSurface = outerTubeBifurcationData.getTrimSurface(s) - if trimSurface: - nodeIdentifier, faceIdentifier = \ - trimSurface.generateMesh(region, nodeIdentifier, faceIdentifier) - if dimension == 2: - elementIdentifier = faceIdentifier - innerTubeBifurcationData = None - if innerNodeTubeBifurcationData: - innerTubeBifurcationData = innerNodeTubeBifurcationData.get( - startSegmentNode if (stage == 0) else endSegmentNode) - - crossIndexes = outerTubeBifurcationData.getCrossIndexes() # only get these from outer - if not crossIndexes: - outerTubeBifurcationData.determineCrossIndexes() - outerTubeBifurcationData.determineMidCoordinates() - if innerTubeBifurcationData: - innerTubeBifurcationData.copyCrossIndexes(outerTubeBifurcationData) - innerTubeBifurcationData.determineMidCoordinates() - crossIndexes = outerTubeBifurcationData.getCrossIndexes() - - outerTubeCoordinates = outerTubeBifurcationData.getConnectingTubeCoordinates() - outerMidCoordinates = outerTubeBifurcationData.getMidCoordinates() - inward = outerTubeBifurcationData.getSegmentsIn() - outerTubeData = outerTubeBifurcationData.getTubeData() - tubeNodeIds = [outerTubeData[s].getEndNodeIds(1) if inward[s] else \ - outerTubeData[s].getStartNodeIds(1) for s in range(3)] - innerTubeCoordinates = None - innerMidCoordinates = None - if innerTubeBifurcationData: - innerTubeCoordinates = innerTubeBifurcationData.getConnectingTubeCoordinates() - innerMidCoordinates = innerTubeBifurcationData.getMidCoordinates() - annotationMeshGroups = [outerTubeData[s].getAnnotationMeshGroups() for s in range(3)] - nodeIdentifier, elementIdentifier = generateTubeBifurcation( - outerTubeCoordinates, innerTubeCoordinates, inward, elementsCountThroughWall, - outerMidCoordinates, innerMidCoordinates, crossIndexes, - region, fieldcache, coordinates, nodeIdentifier, elementIdentifier, tubeNodeIds, - annotationMeshGroups, serendipity=serendipity) - - for s in range(3): - if inward[s]: - if not outerTubeData[s].getEndNodeIds(1): - outerTubeData[s].setEndNodeIds(tubeNodeIds[s], 1) - else: - if not outerTubeData[s].getStartNodeIds(1): - outerTubeData[s].setStartNodeIds(tubeNodeIds[s], 1) - - completedBifurcations.add(outerTubeBifurcationData) - - return nodeIdentifier, elementIdentifier, annotationGroups diff --git a/src/scaffoldmaker/utils/boxnetworkmesh.py b/src/scaffoldmaker/utils/boxnetworkmesh.py new file mode 100644 index 00000000..b1a6687a --- /dev/null +++ b/src/scaffoldmaker/utils/boxnetworkmesh.py @@ -0,0 +1,253 @@ +""" +Specialisation of Network Mesh for building 3-D box meshes. +""" +from cmlibs.maths.vectorops import magnitude, mult +from cmlibs.zinc.element import Element, Elementbasis +from cmlibs.zinc.node import Node +from scaffoldmaker.utils.eft_utils import remapEftLocalNodes, remapEftNodeValueLabelVersion, setEftScaleFactorIds +from scaffoldmaker.utils.interpolation import interpolateSampleCubicHermite, sampleCubicHermiteCurvesSmooth +from scaffoldmaker.utils.networkmesh import NetworkMesh, NetworkMeshBuilder, NetworkMeshGenerateData, \ + NetworkMeshJunction, NetworkMeshSegment, pathValueLabels +from scaffoldmaker.utils.zinc_utils import get_nodeset_path_ordered_field_parameters +import math + + +class BoxNetworkMeshGenerateData(NetworkMeshGenerateData): + """ + Data for passing to BoxNetworkMesh generateMesh functions. + """ + + def __init__(self, region, coordinateFieldName="coordinates", startNodeIdentifier=1, startElementIdentifier=1): + super(BoxNetworkMeshGenerateData, self).__init__( + region, 3, coordinateFieldName, startNodeIdentifier, startElementIdentifier) + self._nodetemplates = {} # map from #versions to nodetemplate + self._hermiteBilinearBasis = self._fieldmodule.createElementbasis(3, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) + self._hermiteBilinearBasis.setFunctionType(1, Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE) + self._elementtemplatesEfts = {} # map from (startVersion, endVersion) to (elementtemplate, eft) + + def getNodetemplate(self, versionsCount): + """ + :param versionsCount: Number of derivative versions for node template. + :return: Zinc Nodetemplate + """ + nodetemplate = self._nodetemplates.get(versionsCount) + if not nodetemplate: + nodetemplate = self._nodes.createNodetemplate() + nodetemplate.defineField(self._coordinates) + for valueLabel in pathValueLabels[1:]: + nodetemplate.setValueNumberOfVersions(self._coordinates, -1, valueLabel, versionsCount) + self._nodetemplates[versionsCount] = nodetemplate + return nodetemplate + + def getElementtemplateAndEft(self, startEndVersions): + """ + Get element template and element field template for start/end versions. + :param startEndVersions: (startVersion, endVersion) + :return: (Zinc Elementtemplate, Zinc Elementfieldtemplate) + """ + elementtemplateEft = self._elementtemplatesEfts.get(startEndVersions) + if elementtemplateEft: + return elementtemplateEft + eft = self._mesh.createElementfieldtemplate(self._hermiteBilinearBasis) + setEftScaleFactorIds(eft, [1], []) + ln = 1 + for n3 in range(2): + s3 = [1] if (n3 == 0) else [] + for n2 in range(2): + s2 = [1] if (n2 == 0) else [] + for n1 in range(2): + v = startEndVersions[n1] + remapEftNodeValueLabelVersion( + eft, [ln], Node.VALUE_LABEL_VALUE, + [(Node.VALUE_LABEL_VALUE, 1, []), + (Node.VALUE_LABEL_D_DS2, v, s2), + (Node.VALUE_LABEL_D_DS3, v, s3)]) + remapEftNodeValueLabelVersion( + eft, [ln], Node.VALUE_LABEL_D_DS1, + [(Node.VALUE_LABEL_D_DS1, v, []), + (Node.VALUE_LABEL_D2_DS1DS2, v, s2), + (Node.VALUE_LABEL_D2_DS1DS3, v, s3)]) + ln += 1 + ln_map = [1, 2, 1, 2, 1, 2, 1, 2] + remapEftLocalNodes(eft, 2, ln_map) + elementtemplate = self._mesh.createElementtemplate() + elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) + elementtemplate.defineField(self._coordinates, -1, eft) + elementtemplateEft = (elementtemplate, eft) + self._elementtemplatesEfts[startEndVersions] = elementtemplateEft + return elementtemplateEft + + +class BoxNetworkMeshSegment(NetworkMeshSegment): + """ + Builds a box mesh for network segment. + """ + + def __init__(self, networkSegment, pathParametersList): + """ + :param networkSegment: NetworkSegment this is built from. + :param pathParametersList: [pathParameters] or [outerPathParameters, innerPathParameters] + """ + super(BoxNetworkMeshSegment, self).__init__(networkSegment, pathParametersList) + self._sampledBoxCoordinates = None + + def sample(self, targetElementLength): + elementsCountAlong = max(1, math.ceil(self._length / targetElementLength)) + if self._isLoop and (elementsCountAlong < 2): + elementsCountAlong = 2 + pathParameters = self._pathParametersList[0] + sx, sd1, pe, pxi, psf = sampleCubicHermiteCurvesSmooth(pathParameters[0], pathParameters[1], elementsCountAlong) + sd2, sd12 = interpolateSampleCubicHermite(pathParameters[2], pathParameters[3], pe, pxi, psf) + sd3, sd13 = interpolateSampleCubicHermite(pathParameters[4], pathParameters[5], pe, pxi, psf) + self._sampledBoxCoordinates = (sx, sd1, sd2, sd12, sd3, sd13) + + def getSampledD1(self, nodeIndex): + return self._sampledBoxCoordinates[1][nodeIndex] + + def setSampledD1(self, nodeIndex, d1): + self._sampledBoxCoordinates[1][nodeIndex] = d1 + + def generateMesh(self, generateData: BoxNetworkMeshGenerateData): + segmentNodes = self._networkSegment.getNetworkNodes() + layoutNodeVersions = self._networkSegment.getNodeVersions() + sx, sd1, sd2, sd12, sd3, sd13 = self._sampledBoxCoordinates + elementsCountAlong = len(self._sampledBoxCoordinates[0]) - 1 + annotationMeshGroups = generateData.getAnnotationMeshGroups(self._annotationTerms) + coordinates = generateData.getCoordinates() + fieldcache = generateData.getFieldcache() + mesh = generateData.getMesh() + nodes = generateData.getNodes() + lastVersion = None + lastNodeIdentifier = None + for n in range(elementsCountAlong + 1): + nodeIdentifier = None + versionsCount = 1 + version = 1 + junction = None + if n in [0, elementsCountAlong]: + junction = self._junctions[0 if (n == 0) else 1] + nLayout = 0 if (n == 0) else -1 + segmentNode = segmentNodes[nLayout] + versionsCount = segmentNode.getVersionsCount() + version = layoutNodeVersions[nLayout] + nodeIdentifier = junction.getNodeIdentifier() + if nodeIdentifier is None: + nodeIdentifier = generateData.nextNodeIdentifier() + nodetemplate = generateData.getNodetemplate(versionsCount) + node = nodes.createNode(nodeIdentifier, nodetemplate) + if junction: + junction.setNodeIdentifier(nodeIdentifier) + else: + node = nodes.findNodeByIdentifier(nodeIdentifier) + # note following will set shared versions of coordinates or derivatives multiple times + # junction.sample should have averaged derivatives from all adjoining segments so this is harmless + fieldcache.setNode(node) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, sx[n]) # only one value version + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, sd1[n]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, sd2[n]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sd12[n]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, sd3[n]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sd13[n]) + + if n > 0: + startEndVersions = (lastVersion, version) + elementtemplate, eft = generateData.getElementtemplateAndEft(startEndVersions) + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplate) + nids = [lastNodeIdentifier, nodeIdentifier] + element.setNodesByIdentifier(eft, nids) + element.setScaleFactors(eft, [-1.0]) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + + lastVersion = version + lastNodeIdentifier = nodeIdentifier + + +class BoxNetworkMeshJunction(NetworkMeshJunction): + """ + Describes junction between multiple segments, some in, some out. + """ + + def __init__(self, inSegments: list, outSegments: list): + """ + :param inSegments: List of inward BoxNetworkMeshSegment. + :param outSegments: List of outward BoxNetworkMeshSegment. + """ + super(BoxNetworkMeshJunction, self).__init__(inSegments, outSegments) + self._nodeIdentifier = None # set by adjacent segment + + def getNodeIdentifier(self): + """ + :return: Identifier of generated node at junction, or None if not set. + """ + return self._nodeIdentifier + + def setNodeIdentifier(self, nodeIdentifier): + """ + Store the node identifier so it can be reused by adjacent segments. + :param nodeIdentifier: Identifier of generated node at junction. + """ + self._nodeIdentifier = nodeIdentifier + + def sample(self, targetElementLength): + """ + Blend common derivatives across junctions prior to generateMesh. + :param targetElementLength: Ignored here. + """ + s = 0 + nodeVersionSegmentIndexes = {} # map from version number to list of (segment, nodeIndex) using it + for segment in self._segments: + inward = self._segmentsIn[s] + nodeIndex = -1 if inward else 0 + nodeVersion = segment.getNetworkSegment().getNodeVersions()[nodeIndex] + nodeVersionSegmentIndex = nodeVersionSegmentIndexes.get(nodeVersion) + if not nodeVersionSegmentIndex: + nodeVersionSegmentIndexes[nodeVersion] = nodeVersionSegmentIndex = [] + nodeVersionSegmentIndex.append((segment, nodeIndex)) + s += 1 + for nodeVersion, segmentIndexes in nodeVersionSegmentIndexes.items(): + count = len(segmentIndexes) + if count < 2: + continue # no blending required + # harmonic mean magnitude + d1MagRecSum = 0.0 + for segment, nodeIndex in segmentIndexes: + d1 = segment.getSampledD1(nodeIndex) + d1Mag = magnitude(d1) + d1MagRecSum += 1.0 / d1Mag + d1MagMean = count / d1MagRecSum + d1Mean = mult(d1, d1MagMean / d1Mag) + for segment, nodeIndex in segmentIndexes: + segment.setSampledD1(nodeIndex, d1Mean) + + + def generateMesh(self, generateData: BoxNetworkMeshGenerateData): + pass # nothing to do for box network + + +class BoxNetworkMeshBuilder(NetworkMeshBuilder): + + def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, + layoutAnnotationGroups: list = []): + super(BoxNetworkMeshBuilder, self).__init__( + networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups) + + def createSegment(self, networkSegment): + """ + Create box segment object for building box mesh from networkSegment. + :param networkSegment: A network segment from the underlying NetworkMesh + :return: BoxNetworkMeshSegment. + """ + pathParametersList = [get_nodeset_path_ordered_field_parameters( + self._layoutNodes, self._layoutCoordinates, pathValueLabels, + networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions())] + return BoxNetworkMeshSegment(networkSegment, pathParametersList) + + def createJunction(self, inSegments, outSegments): + """ + :param inSegments: List of inward BoxNetworkMeshSegment. + :param outSegments: List of outward BoxNetworkMeshSegment. + :return: A BoxNetworkMeshJunction. + """ + return BoxNetworkMeshJunction(inSegments, outSegments) diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index 93f5c45a..c3ec1f19 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -1,10 +1,12 @@ ''' Utility functions for element field templates shared by mesh generators. ''' -from cmlibs.maths.vectorops import add, dot, magnitude, mult, sub +from cmlibs.maths.vectorops import add, cross, dot, magnitude, mult, normalize, sub from cmlibs.zinc.element import Elementbasis, Elementfieldtemplate from cmlibs.zinc.node import Node from cmlibs.zinc.result import RESULT_OK +from scaffoldmaker.utils.interpolation import interpolateHermiteLagrangeDerivative, interpolateLagrangeHermiteDerivative +import copy import math @@ -455,4 +457,337 @@ def determineTricubicHermiteEft(mesh, nodeParameters, nodeDerivativeFixedWeights eft.setTermNodeParameter(functionNumber, 1, ln, crossDerivativeLabel, 1) if sign < 0.0: eft.setTermScaling(functionNumber, 1, [1]) - return eft, scalefactors \ No newline at end of file + return eft, scalefactors + + +class HermiteNodeLayout: + """ + From the allowable derivative directions expressed as weighted sums, determines all + possible permutations of them giving a right-handed corner without other directions inside them. + """ + + def __init__(self, directions, baseNodeLayout=None, limitDirections=None): + """ + Construct a new node layout from either directions, or baseNodeLayout with limitDirections. + :param directions: List of allowed directions as weighted sums of d1, d2, or d1, d2, d3. + Lists with 2 weights are for bicubic; with each weight paired with the following + in the list to make valid permutations. Do not supply if baseNodeLayout is supplied. + :param baseNodeLayout: Optional base node layout to copy direections and permutations from + when supplying limitDirections. + :param limitDirections: Optional list over element directions of lists of allowable weights for that + direction, or None to not filter. Default None for whole list does not filter any directions. + For example, with no d3, [None, [[0.0, 1.0], [0.0, -1.0]]] places no limit on the first + derivative, but derivative 2 must be [0, +/-1]. + """ + assert (directions and not baseNodeLayout) or (baseNodeLayout and limitDirections and not directions) + self._directions = directions if directions else baseNodeLayout.getDirections() + self._limitDirections = limitDirections if limitDirections else [] + self._cubicDimensions = len(self._directions[0]) + assert self._cubicDimensions in (2, 3) + self._permutations = baseNodeLayout.getPermutations() if baseNodeLayout else self._determinePermutations() + + def _determinePermutations(self): + """ + Determine permutations of weights from directions. + :return: Permutations list. + """ + permutations = [] + directionsCount = len(self._directions) + if self._cubicDimensions == 2: + for i in range(directionsCount): + j = (i + 1) % directionsCount + permutations.append((self._directions[i], self._directions[j])) + # disallowing reverse as messes up bifurcations with 6, 10, 14 etc. around + # would be needed for moebius strip + # permutations.append((directions[j], directions[i])) + else: + normDir = [normalize(dir) for dir in self._directions] + for i in range(directionsCount - 2): + for j in range(i + 1, directionsCount - 1): + if dot(normDir[i], normDir[j]) < -0.9: + continue + ijCross = cross(normDir[i], normDir[j]) + for k in range(j + 1, directionsCount): + if (dot(normDir[i], normDir[k]) < -0.9) or (dot(normDir[j], normDir[k]) < -0.9): + continue + dotCross = dot(ijCross, normDir[k]) + if dotCross == 0.0: + continue + swizzle = dotCross < 0.0 + midSide1 = normalize(add(normDir[i], normDir[j])) + midSide2 = normalize(add(normDir[j], normDir[k])) + midSide3 = normalize(add(normDir[k], normDir[i])) + middle = normalize([midSide1[c] + midSide2[c] + midSide3[c] for c in range(3)]) + maxDot = 0.9 * max(dot(normDir[i], middle), dot(normDir[j], middle), dot(normDir[k], middle)) + for m in range(directionsCount): + if m in [i, j, k]: + continue + if dot(normDir[m], middle) > maxDot: + break # another direction is within i j k region + else: + ii, jj, kk = (i, k, j) if swizzle else (i, j, k) + permutations.append((self._directions[ii], self._directions[jj], self._directions[kk])) + permutations.append((self._directions[jj], self._directions[kk], self._directions[ii])) + permutations.append((self._directions[kk], self._directions[ii], self._directions[jj])) + return permutations + + def getComplexity(self): + """ + :return: Integer complexity equal to number of axis permutations in node layout. + """ + return len(self._permutations) + + def getDirections(self): + return self._directions + + def getPermutations(self): + return self._permutations + + def getDerivativeWeightsList(self, nodeDeltas, nodeDerivatives, localNodeIndex): + """ + Get derivative weights for permutation making nodeDerivatives closest to nodeDeltas. + :param nodeDeltas: List of 3 delta side coordinates to match. + :param nodeDerivatives: List of [d1, d2, d3] parameters from node. d3 is None if linear through wall or 2-D. + :param localNodeIndex: Local node index from 0 to 7 in Zinc order, which flips allowable directions. + :return: List of weights for d1, d2, d3 to give d/dxi1, d/dxi2, d/dxi3. + """ + derivativesPerNode = self._cubicDimensions + if self._cubicDimensions == 2: + flips = [localNodeIndex in [1, 3, 5, 7], localNodeIndex in [2, 3, 6, 7]] + # need to swizzle indexes if odd number of flips, to keep right-handed layout + flipCount = sum(1 for flip in flips if flip) + swizzle = (flipCount % 2) == 1 + swizzleIndexes = [1, 0] if swizzle else [0, 1] + else: + flips = [localNodeIndex in [1, 3, 5, 7], localNodeIndex in [2, 3, 6, 7], localNodeIndex in [4, 5, 6, 7]] + # need to swizzle indexes if odd number of flips, to keep right-handed layout + flipCount = sum(1 for flip in flips if flip) + swizzle = (flipCount % 2) == 1 + swizzleIndexes = [0, 2, 1] if swizzle else [0, 1, 2] + + # modify deltas to point inward towards opposite node + inwardNodeDeltas = [[-d for d in nodeDeltas[i]] if flips[i] else nodeDeltas[i] for i in swizzleIndexes] + derivativeWeightsList = None + greatestSimilarity = -1.0 + for permutation in self._permutations: + # skip permutations using directions not in any supplied limitDirections + skipPermutation = False + limitIndex = 0 + for limitDirections in self._limitDirections: + if limitDirections: + weights = permutation[swizzleIndexes[limitIndex]] + if flips[limitIndex]: + weights = [-wt for wt in weights] + for limitDirection in limitDirections: + if magnitude(sub(weights, limitDirection)) < 1.0E-6: + break + else: + skipPermutation = True + break + limitIndex += 1 + if skipPermutation: + continue + similarity = 0.0 + for d in range(derivativesPerNode): + weights = permutation[d] + derivative = [0.0, 0.0, 0.0] + for i in range(derivativesPerNode): + if weights[i]: + weight = weights[i] + nodeDerivative = nodeDerivatives[i] + for c in range(3): + derivative[c] += weight * nodeDerivative[c] + delta = inwardNodeDeltas[d] + magDelta = magnitude(delta) + magDerivative = magnitude(derivative) + if magDerivative > 0.0: + cosineSimilarity = dot(derivative, delta) / (magDerivative * magDelta) + # magnitudeSimilarity = math.exp(-math.fabs((magDerivative - magDelta) / magDelta)) + similarity += cosineSimilarity # * magnitudeSimilarity + if similarity > greatestSimilarity: + greatestSimilarity = similarity + derivativeWeightsList = permutation + finalWeightsList = [ + [-w for w in derivativeWeightsList[swizzleIndexes[i]]] if flips[i] + else derivativeWeightsList[swizzleIndexes[i]] for i in range(derivativesPerNode) + ] + return finalWeightsList + + +class HermiteNodeLayoutManager: + """ + Managers permutations of hermite node layouts for easy recall. + """ + + def __init__(self): + self._nodeLayoutRegularPermuted = HermiteNodeLayout( + [[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]]) + self._nodeLayoutRegularPermuted_d3Defined = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]]) + self._nodeLayout6Way12 = HermiteNodeLayout( + [[1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [-1.0, 0.0], [-1.0, -1.0], [0.0, -1.0]]) + self._nodeLayout6Way12_d3Defined = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [-1.0, -1.0, 0.0], [0.0, -1.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]]) + self._nodeLayout8Way12 = HermiteNodeLayout( + [[1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [-1.0, 1.0], [-1.0, 0.0], [-1.0, -1.0], [0.0, -1.0], [1.0, -1.0]]) + self._nodeLayout8Way12_d3Defined = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [0.0, 1.0, 0.0], [-1.0, 1.0, 0.0], + [-1.0, 0.0, 0.0], [-1.0, -1.0, 0.0], [0.0, -1.0, 0.0], [1.0, -1.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]]) + + def getNodeLayoutRegularPermuted(self, d3Defined, limitDirections=None): + """ + Get node layout for permutations of +/- d1, d2, d3, optionally limiting some directions. + Note that variants using limitDirections are not currently stored so client should hold them if reusing. + :param d3Defined: Set to True to use tricubic variant with d3 defined, otherwise bicubic is used. + :param limitDirections: Optional list over element directions of lists of allowable weights for that + direction, or None to not filter. Default None for whole list does not filter any directions. + For example, with d3, [None, [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]], [[0.0, 0.0, 1.0]]] places no + limits on the first derivative, but derivative 2 must be [0, +/-1, 0] and d3 must be [0, 0, 1]. + :return: HermiteNodeLayout. + """ + nodeLayout = self._nodeLayoutRegularPermuted_d3Defined if d3Defined else self._nodeLayoutRegularPermuted + if limitDirections: + nodeLayout = HermiteNodeLayout(None, nodeLayout, limitDirections) + return nodeLayout + + def getNodeLayout6Way12(self, d3Defined): + """ + Get node layout for 6-way junction in 1-2 plane, including d1 + d2, -d1 - d2. + :param d3Defined: Set to True to use tricubic variant with d3 defined, otherwise bicubic is used. + :return: HermiteNodeLayout. + """ + return self._nodeLayout6Way12_d3Defined if d3Defined else self._nodeLayout6Way12 + + def getNodeLayout8Way12(self, d3Defined): + """ + Get node layout for 8-way junction in 1-2 plane, including d1 + d2, -d1 + d2, -d1 - d2, d1 - d2. + :param d3Defined: Set to True to use tricubic variant with d3 defined, otherwise bicubic is used. + :return: HermiteNodeLayout. + """ + return self._nodeLayout8Way12_d3Defined if d3Defined else self._nodeLayout8Way12 + + +def determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts): + """ + Determine the bicubic or tricubic Hermite serendipity element field template for + interpolating node parameters at corners of a square or cube, by matching deltas + between corners with node derivatives. + Node lists use zinc ordering which varies nodes fastest over lower element coordinate. + :param mesh: A Zinc mesh of dimension 2 or 3. + :param nodeParameters: List over 4 (2-D) or 8 (3-D) local nodes in Zinc ordering of + 4 parameter vectors x, d1, d2, d3 each with 3 components. d3 is not used in 2-D, and in + 3-d if d3 is omitted the basis is linear in that direction. + :param nodeLayouts: List over 4 or 8 local nodes of HermiteNodeLayout objects describing the + list of allowable derivative combinations for each corner node. None value for a node + keeps the standard, regular layout. + :return: eft, scale factors list [-1.0] or None. Returned eft can be further modified. + """ + meshDimension = mesh.getDimension() + nodesCount = len(nodeParameters) + assert ((meshDimension == 2) and (nodesCount == 4)) or ((meshDimension == 3) and (nodesCount == 8)) + assert len(nodeParameters[0]) == 4 + d3Defined = (meshDimension == 3) and (nodeParameters[0][3] is not None) + assert len(nodeLayouts) == nodesCount + delta12 = sub(nodeParameters[1][0], nodeParameters[0][0]) + delta34 = sub(nodeParameters[3][0], nodeParameters[2][0]) + delta13 = sub(nodeParameters[2][0], nodeParameters[0][0]) + delta24 = sub(nodeParameters[3][0], nodeParameters[1][0]) + if meshDimension == 3: + delta15 = sub(nodeParameters[4][0], nodeParameters[0][0]) + delta26 = sub(nodeParameters[5][0], nodeParameters[1][0]) + delta37 = sub(nodeParameters[6][0], nodeParameters[2][0]) + delta48 = sub(nodeParameters[7][0], nodeParameters[3][0]) + else: + delta15 = delta26 = delta37 = delta48 = [0.0, 0.0, 0.0] + deltas = [ + [delta12, delta13, delta15], + [delta12, delta24, delta26], + [delta34, delta13, delta37], + [delta34, delta24, delta48] + ] + if meshDimension == 3: + delta56 = sub(nodeParameters[5][0], nodeParameters[4][0]) + delta78 = sub(nodeParameters[7][0], nodeParameters[6][0]) + delta57 = sub(nodeParameters[6][0], nodeParameters[4][0]) + delta68 = sub(nodeParameters[7][0], nodeParameters[5][0]) + deltas += [ + [delta56, delta57, delta15], + [delta56, delta68, delta26], + [delta78, delta57, delta37], + [delta78, delta68, delta48] + ] + fieldmodule = mesh.getFieldmodule() + elementbasis = fieldmodule.createElementbasis(meshDimension, Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE_SERENDIPITY) + if (meshDimension == 3) and not d3Defined: + elementbasis.setFunctionType(3, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) + eft = mesh.createElementfieldtemplate(elementbasis) + scalefactors = None + derivativeLabels = [Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3] + derivativesPerNode = 3 if d3Defined else 2 + functionsPerNode = 1 + derivativesPerNode + # order local nodes from default then simplest to most complex node layout + nodeOrder = [] + for n in range(nodesCount): + if not nodeLayouts[n]: + nodeOrder.append(n) + while len(nodeOrder) < nodesCount: + lowestComplexity = 0 + next_n = None + for n in range(nodesCount): + if n in nodeOrder: + continue + complexity = nodeLayouts[n].getComplexity() + if (next_n is None) or (complexity < lowestComplexity): + lowestComplexity = complexity + next_n = n + nodeOrder.append(next_n) + for n in nodeOrder: + ln = n + 1 + nodeLayout = nodeLayouts[n] + nodeDerivatives = [ + nodeParameters[n][1], + nodeParameters[n][2], + nodeParameters[n][3] if d3Defined else None] + derivativeWeightsList =\ + nodeLayout.getDerivativeWeightsList(deltas[n], nodeDerivatives, n) if nodeLayout else None + for ed in range(derivativesPerNode): + if nodeLayout: + derivativeWeights = derivativeWeightsList[ed] + functionNumber = n * functionsPerNode + ed + 2 + termsCount = sum(1 for wt in derivativeWeights if wt != 0.0) + eft.setFunctionNumberOfTerms(functionNumber, termsCount) + term = 0 + elementDerivative = [0.0, 0.0, 0.0] + for i in range(derivativesPerNode): + weight = derivativeWeights[i] + if weight: + term += 1 + eft.setTermNodeParameter(functionNumber, term, ln, derivativeLabels[i], 1) + if weight < 0.0: + if not scalefactors: + setEftScaleFactorIds(eft, [1], []) + scalefactors = [-1.0] + eft.setTermScaling(functionNumber, term, [1]) + for c in range(3): + elementDerivative[c] += weight * nodeDerivatives[i][c] + else: + elementDerivative = nodeDerivatives[ed] + # update delta to equal the exact derivative + oldDeltaMagnitude = magnitude(deltas[n][ed]) + deltas[n][ed] = elementDerivative + # get other node index interpolated with this element derivative + on = n ^ (1 << ed) + if nodeOrder.index(on) > nodeOrder.index(n): + derivativeMagnitude = magnitude(elementDerivative) + newMagnitude = 2.0 / ((1.0 / oldDeltaMagnitude) + (1.0 / derivativeMagnitude)) + elementDerivative = mult(elementDerivative, newMagnitude / derivativeMagnitude) + # update other node delta to work in with this derivative + otherElementDerivative = ( + interpolateHermiteLagrangeDerivative(nodeParameters[n][0], elementDerivative, nodeParameters[on][0], 1.0) + if (on > n) else + interpolateLagrangeHermiteDerivative(nodeParameters[on][0], nodeParameters[n][0], elementDerivative, 0.0)) + deltas[on][ed] = otherElementDerivative + + return eft, scalefactors diff --git a/src/scaffoldmaker/utils/interpolation.py b/src/scaffoldmaker/utils/interpolation.py index 42a68b86..54d05de5 100644 --- a/src/scaffoldmaker/utils/interpolation.py +++ b/src/scaffoldmaker/utils/interpolation.py @@ -196,6 +196,37 @@ def getCubicHermiteCurvesLengthLoop(cx, cd1): totalLength += arcLength return totalLength + +def getCubicHermiteTrimmedCurvesLengths(cx, cd1, startLocation=None, endLocation=None): + """ + Get trimmed lengths of curve: before start, between start and end, and after end, lengths to nodes + :param cx: coordinates along the curve. + :param cd1: d1 derivatives. + :param startLocation: Optional tuple of 'in' (element, xi) to start curve at. + :param endLocation: Optional tuple of 'out' (element, xi) to end curve at. + :return: Length before start, length between start and end, length after end, list of lengths to nodes. + """ + elementsCount = len(cx) - 1 + lengthToNode = [0.0] + length = 0.0 + for e in range(elementsCount): + length += getCubicHermiteArcLength(cx[e], cd1[e], cx[e + 1], cd1[e + 1]) + lengthToNode.append(length) + startLength = 0.0 + if startLocation: + e = startLocation[0] + startLength = (lengthToNode[e] + + getCubicHermiteArcLengthToXi(cx[e], cd1[e], cx[e + 1], cd1[e + 1], startLocation[1])) + length -= startLength + endLength = 0.0 + if endLocation: + e = endLocation[0] + endLength = (lengthToNode[-1] - lengthToNode[e] - + getCubicHermiteArcLengthToXi(cx[e], cd1[e], cx[e + 1], cd1[e + 1], endLocation[1])) + length -= endLength + return startLength, length, endLength, lengthToNode + + def getCubicHermiteCurvature(v1, d1, v2, d2, radialVector, xi): """ :param v1, v2: Values at xi = 0.0 and xi = 1.0, respectively. @@ -453,22 +484,8 @@ def sampleCubicHermiteCurvesSmooth(nx, nd1, elementsCountOut, elementsCountIn = len(nx) - 1 assert (elementsCountIn > 0) and (len(nd1) == (elementsCountIn + 1)) and (elementsCountOut > 0), \ "sampleCubicHermiteCurvesSmooth. Invalid arguments" - lengthToNodeIn = [0.0] - length = 0.0 - for e in range(elementsCountIn): - length += getCubicHermiteArcLength(nx[e], nd1[e], nx[e + 1], nd1[e + 1]) - lengthToNodeIn.append(length) - startDistance = 0.0 - if startLocation: - e = startLocation[0] - startDistance = lengthToNodeIn[e] + \ - getCubicHermiteArcLengthToXi(nx[e], nd1[e], nx[e + 1], nd1[e + 1], startLocation[1]) - length -= startDistance - if endLocation: - e = endLocation[0] - endDistance = lengthToNodeIn[-1] - lengthToNodeIn[e] - \ - getCubicHermiteArcLengthToXi(nx[e], nd1[e], nx[e + 1], nd1[e + 1], endLocation[1]) - length -= endDistance + startLength, length, endLength, lengthToNodeIn =\ + getCubicHermiteTrimmedCurvesLengths(nx, nd1, startLocation, endLocation) hasStartDerivative = derivativeMagnitudeStart is not None hasEndDerivative = derivativeMagnitudeEnd is not None if hasStartDerivative and hasEndDerivative: @@ -480,9 +497,9 @@ def sampleCubicHermiteCurvesSmooth(nx, nd1, elementsCountOut, else: derivativeMagnitudeStart = derivativeMagnitudeEnd = length / elementsCountOut # sample over length to get distances to elements boundaries - x1 = startDistance + x1 = startLength d1 = derivativeMagnitudeStart * elementsCountOut - x2 = startDistance + length + x2 = startLength + length d2 = derivativeMagnitudeEnd * elementsCountOut nodeDistances = [] nodeDerivativeMagnitudes = [] @@ -717,6 +734,8 @@ def smoothCubicHermiteDerivativesLine(nx, nd1, # fixed directions, equal magnitude arcLength = computeCubicHermiteArcLength(nx[0], nd1[0], nx[1], nd1[1], rescaleDerivatives=True) return [ vector.setMagnitude(nd1[0], arcLength), vector.setMagnitude(nd1[1], arcLength) ] + # fixStartMag = magnitude(md1[0]) if fixStartDerivative else 0.0 + # fixEndMag = magnitude(md1[-1]) if fixEndDerivative else 0.0 tol = 1.0E-6 if instrument: print('iter 0', md1) @@ -743,10 +762,17 @@ def smoothCubicHermiteDerivativesLine(nx, nd1, wm = arcLengths[n ]/arcLengthmp wp = arcLengths[nm]/arcLengthmp md1[n] = [ (wm*dirm[c] + wp*dirp[c]) for c in componentRange ] + dnm = arcLengths[nm] + dn = arcLengths[n] + # future: 2nd nodes from end need to work in with fixed end derivatives + # if fixStartDerivative and (n == 1): + # dnm = 2.0 * dnm - fixStartMag + # if fixEndDerivative and (n == (nodesCount - 2)): + # dn = 2.0 * dn - fixEndMag if arithmeticMeanMagnitude: - mag = 0.5*(arcLengths[nm] + arcLengths[n]) + mag = 0.5 * (dnm + dn) else: # harmonicMeanMagnitude - mag = 2.0/(1.0/arcLengths[nm] + 1.0/arcLengths[n]) + mag = 2.0 / (1.0 / dnm + 1.0 / dn) md1[n] = vector.setMagnitude(md1[n], mag) # end if not fixEndDerivative: diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index b2a5e8fe..dd738bae 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -2,20 +2,25 @@ Utility class for defining network meshes from 1-D connectivity and lateral axes, with continuity control. """ -from cmlibs.utils.zinc.field import findOrCreateFieldCoordinates +from cmlibs.utils.zinc.field import find_or_create_field_coordinates from cmlibs.zinc.element import Element, Elementbasis from cmlibs.zinc.field import Field from cmlibs.zinc.node import Node from cmlibs.maths.vectorops import cross, magnitude, normalize, sub -from scaffoldmaker.utils.interpolation import interpolateSampleCubicHermite, sampleCubicHermiteCurvesSmooth,\ - smoothCubicHermiteDerivativesLoop +from scaffoldmaker.annotation.annotationgroup import AnnotationGroup +from scaffoldmaker.utils.interpolation import getCubicHermiteCurvesLength from scaffoldmaker.utils.tracksurface import TrackSurface -from scaffoldmaker.utils.vector import vectorRejection - +from abc import ABC, abstractmethod import math import sys +pathValueLabels = [ + Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, + Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D2_DS1DS2, + Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3] + + class NetworkNode: """ Describes a single node in a network, storing number of versions etc. @@ -313,7 +318,7 @@ def create1DLayoutMesh(self, region): """ self._region = region fieldmodule = region.getFieldmodule() - coordinates = findOrCreateFieldCoordinates(fieldmodule) + coordinates = find_or_create_field_coordinates(fieldmodule) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) nodetemplates = [None] @@ -406,155 +411,389 @@ def create1DLayoutMesh(self, region): elementIdentifier += 1 -def getPathRawTubeCoordinates(pathParameters, elementsCountAround, radius=1.0, phaseAngle=0.0): +class NetworkMeshGenerateData: + """ + Data for passing to NetworkMesh generateMesh functions. + Maintains Zinc region, field, node and element information, and output annotation groups. + Derive from this class to pass additional data. + """ + + def __init__(self, region, meshDimension, coordinateFieldName, startNodeIdentifier=1, startElementIdentifier=1): + self._region = region + self._fieldmodule = region.getFieldmodule() + self._fieldcache = self._fieldmodule.createFieldcache() + self._meshDimension = meshDimension + self._mesh = self._fieldmodule.findMeshByDimension(meshDimension) + self._nodes = self._fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self._coordinates = find_or_create_field_coordinates(self._fieldmodule, coordinateFieldName) + self._nodeIdentifier = startNodeIdentifier + self._elementIdentifier = startElementIdentifier + self._annotationGroups = [] # list of AnnotationGroup to return for mesh's scaffold + self._annotationGroupMap = {} # map from annotation term (name, ontId) to AnnotationGroup in output region + + def getCoordinates(self): + """ + :return: Zinc Finite Element coordinate field being defined. + """ + return self._coordinates + + def getFieldcache(self): + """ + :return: Zinc Fieldcache for assigning field parameters with. + """ + return self._fieldcache + + def getMesh(self): + """ + :return: Zinc Mesh for elements being built. + """ + return self._mesh + + def getMeshDimension(self): + """ + :return: Dimension of elements being built. + """ + return self._meshDimension + + def getNodes(self): + """ + :return: Zinc Nodeset for nodes being built. + """ + return self._nodes + + def getRegion(self): + return self._region + + def getNodeElementIdentifiers(self): + """ + Get next node and element identifiers without incrementing, to call at end of generation. + :return: Next node identifier, next element identifier. + """ + return self._nodeIdentifier, self._elementIdentifier + + def setNodeElementIdentifiers(self, nodeIdentifier, elementIdentifier): + """ + Set next node and element identifiers after generating objects with external code. + """ + self._nodeIdentifier = nodeIdentifier + self._elementIdentifier = elementIdentifier + + def nextNodeIdentifier(self): + nodeIdentifier = self._nodeIdentifier + self._nodeIdentifier += 1 + return nodeIdentifier + + def nextElementIdentifier(self): + elementIdentifier = self._elementIdentifier + self._elementIdentifier += 1 + return elementIdentifier + + def getRegion(self): + return self._region + + def getAnnotationGroups(self): + return self._annotationGroups + + def getOrCreateAnnotationGroup(self, annotationTerm): + annotationGroup = self._annotationGroupMap.get(annotationTerm) + if not annotationGroup: + annotationGroup = AnnotationGroup(self._region, annotationTerm) + self._annotationGroups.append(annotationGroup) + self._annotationGroupMap[annotationTerm] = annotationGroup + return annotationGroup + + def _getAnnotationMeshGroup(self, annotationTerm): + """ + Get mesh group to add elements to for term. + :param annotationTerm: Annotation term (name, ontId). + :return: Zinc MeshGroup. + """ + annotationGroup = self.getOrCreateAnnotationGroup(annotationTerm) + return annotationGroup.getMeshGroup(self._mesh) + + def getAnnotationMeshGroups(self, annotationTerms): + """ + Get mesh groups for all annotation terms to add segment elements to, creating as needed. + :param annotationTerms: List of annotation terms (name, ontId). + :return: List of Zinc MeshGroup. + """ + return [self._getAnnotationMeshGroup(annotationTerm) for annotationTerm in annotationTerms] + + +class NetworkMeshSegment(ABC): + """ + Base class for building a mesh from a NetworkSegment. + """ + + def __init__(self, networkSegment, pathParametersList): + """ + :param networkSegment: NetworkSegment this is built from. + :param pathParametersList: [pathParameters] or [outerPathParameters, innerPathParameters] + Segment length is determined from the first/primary path only. + """ + self._networkSegment = networkSegment + self._pathParametersList = pathParametersList + self._pathsCount = len(pathParametersList) + self._dimension = 3 if (self._pathsCount > 1) else 2 + self._length = getCubicHermiteCurvesLength(pathParametersList[0][0], pathParametersList[0][1]) + self._annotationTerms = [] + self._junctions = [] # start, end junctions. Set when junctions are created. + self._isLoop = False + + def addAnnotationTerm(self, annotationTerm): + """ + Add annotation term to this segment. Should not already be present; this is not checked. + :param annotationTerm: Annotation term (name: str, ontId: str) + """ + self._annotationTerms.append(annotationTerm) + + def getAnnotationTerms(self): + return self._annotationTerms + + def getLength(self): + """ + Get length of the segment's primary path. + :return: Real length. + """ + return self._length + + def getNetworkSegment(self): + """ + :return: Original NetworkSegment this is building from. + """ + return self._networkSegment + + def getPathParameters(self, pathIndex=0): + """ + :return: Path parameters (x, d1, d2, d12, d3, d13) for path index. + """ + if pathIndex > len(self._pathParametersList): + return None + return self._pathParametersList[pathIndex] + + def getPathsCount(self): + return self._pathsCount + + def getJunctions(self): + """ + Get the junctions at start and end of segment. + :return: List of NetworkMeshJunction-derived objects. + """ + return self._junctions + + def setJunctions(self, junctions): + """ + Called by NetworkMeshBuilder so segment knows how it starts and ends. + :param junctions: list of 2 NetworkMeshJunctions-derived objects at start and end of segment. + """ + self._junctions = junctions + self._isLoop = junctions[0] == junctions[1] + + def isLoop(self): + """ + Only valid after call to self.setJunctions() + :return: True if segment is a loop, otherwise False. + """ + return self._isLoop + + @abstractmethod + def sample(self, targetElementLength): + """ + Override to resample curve/raw data to final coordinates. + :param targetElementLength: Target element size along length of segment/junction. + """ + pass + + @abstractmethod + def generateMesh(self, generateData: NetworkMeshGenerateData): + """ + Override to generate mesh for segment. + :param generateData: NetworkMeshGenerateData-derived object. + :return: + """ + pass + + +class NetworkMeshJunction(ABC): """ - Generate coordinates around and along a tube in parametric space around the path parameters, - at xi2^2 + xi3^2 = radius at the same density as path parameters. - :param pathParameters: List over nodes of 6 parameters vectors [cx, cd1, cd2, cd12, cd3, cd13] giving - coordinates cx along path centre, derivatives cd1 along path, cd2 and cd3 giving side vectors, - and cd12, cd13 giving rate of change of side vectors. Parameters have 3 components. - Same format as output of zinc_utils get_nodeset_path_ordered_field_parameters(). - :param elementsCountAround: Number of elements & nodes to create around tube. First location is at +d2. - :param radius: Radius of tube in xi space. - :param phaseAngle: Starting angle around ellipse, where 0.0 is at d2, pi/2 is at d3. - :return: px[][], pd1[][], pd2[][], pd12[][] with first index in range(pointsCountAlong), - second inner index in range(elementsCountAround) + Base class for building a mesh at a junction between segments, some in, some out. """ - assert len(pathParameters) == 6 - pointsCountAlong = len(pathParameters[0]) - assert pointsCountAlong > 1 - assert len(pathParameters[0][0]) == 3 - - # sample around circle in xi space, later smooth and re-sample to get even spacing in geometric space - ellipsePointCount = 16 - aroundScale = 2.0 * math.pi / ellipsePointCount - sxi = [] - sdxi = [] - angleBetweenPoints = 2.0 * math.pi / ellipsePointCount - for q in range(ellipsePointCount): - theta = phaseAngle + q * angleBetweenPoints - xi2 = radius * math.cos(theta) - xi3 = radius * math.sin(theta) - sxi.append([xi2, xi3]) - dxi2 = -xi3 * aroundScale - dxi3 = xi2 * aroundScale - sdxi.append([dxi2, dxi3]) - - px = [] - pd1 = [] - pd2 = [] - pd12 = [] - for p in range(pointsCountAlong): - cx, cd1, cd2, cd12, cd3, cd13 = [cp[p] for cp in pathParameters] - tx = [] - td1 = [] - for q in range(ellipsePointCount): - xi2 = sxi[q][0] - xi3 = sxi[q][1] - x = [(cx[c] + xi2 * cd2[c] + xi3 * cd3[c]) for c in range(3)] - tx.append(x) - dxi2 = sdxi[q][0] - dxi3 = sdxi[q][1] - d1 = [(dxi2 * cd2[c] + dxi3 * cd3[c]) for c in range(3)] - td1.append(d1) - # smooth to get reasonable derivative magnitudes - td1 = smoothCubicHermiteDerivativesLoop(tx, td1, fixAllDirections=True) - # resample to get evenly spaced points around loop, temporarily adding start point to end - ex, ed1, pe, pxi, psf = sampleCubicHermiteCurvesSmooth(tx + tx[:1], td1 + td1[:1], elementsCountAround) - exi, edxi = interpolateSampleCubicHermite(sxi + sxi[:1], sdxi + sdxi[:1], pe, pxi, psf) - ex.pop() - ed1.pop() - exi.pop() - edxi.pop() - - # check closeness of x(exi[i]) to ex[i] - # find nearest xi2, xi3 if above is finite error - # a small but non-negligible error, but results look fine so not worrying - # dxi = [] - # for i in range(len(ex)): - # xi2 = exi[i][0] - # xi3 = exi[i][1] - # xi23 = xi2 * xi3 - # x = [(cx[c] + xi2 * cd2[c] + xi3 * cd3[c]) for c in range(3)] - # dxi.append(sub(x, ex[i])) - # print("error", p, "=", [magnitude(v) for v in dxi]) - - # calculate d2, d12 at exi - ed2 = [] - ed12 = [] - for i in range(len(ex)): - xi2 = exi[i][0] - xi3 = exi[i][1] - d2 = [(cd1[c] + xi2 * cd12[c] + xi3 * cd13[c]) for c in range(3)] - ed2.append(d2) - dxi2 = edxi[i][0] - dxi3 = edxi[i][1] - d12 = [(dxi2 * cd12[c] + dxi3 * cd13[c]) for c in range(3)] - ed12.append(d12) - - px.append(ex) - pd1.append(ed1) - pd2.append(ed2) - pd12.append(ed12) - - return px, pd1, pd2, pd12 - - -def resampleTubeCoordinates(rawTubeCoordinates, elementsCountAlong, - startSurface: TrackSurface=None, endSurface: TrackSurface=None): + + def __init__(self, inSegments: list, outSegments: list): + """ + :param inSegments: List of inward NetworkMeshSegment-derived objects. + :param outSegments: List of outward NetworkMeshSegment-derived objects. + """ + self._segments = inSegments + outSegments + self._segmentsCount = len(self._segments) + self._segmentsIn = [segment in inSegments for segment in self._segments] + + def getNetworkSegments(self): + """ + :return: List of network segments at junction in order with "in" segments first. + """ + return self._networkSegments + + def getSegmentsCount(self): + return self._segmentsCount + + def getSegmentsIn(self): + """ + :return: List of boolean true if segment is inward, false if not, in supplied order. + """ + return self._segmentsIn + + def getSegments(self): + """ + :return: List of NetworkMeshSegment-derived objects joined at junction, in supplied order. + """ + return self._segments + + @abstractmethod + def sample(self, targetElementLength): + """ + Override to resample curve/raw data to final coordinates. + :param targetElementLength: Target element size along length of segment/junction. + """ + pass + + @abstractmethod + def generateMesh(self, generateData: NetworkMeshGenerateData): + """ + Override to generate mesh for junction. + :param generateData: NetworkMeshGenerateData-derived object. + :return: + """ + pass + + +class NetworkMeshBuilder(ABC): """ - Generate new tube coordinates evenly spaced along raw tube coordinates, optionally - starting or ending at intersections at start/end track surfaces. - :param rawTubeCoordinates: (px, pd1, pd2, pd12) returned by getPathRawTubeCoordinates(). - :param elementsCountAlong: Number of elements in resampled coordinates. - :param startSurface: Optional TrackSurface specifying start of tube at intersection with it. - :param endSurface: Optional TrackSurface specifying end of tube at intersection with it. - :return: sx[][], sd1[][], sd2[][], sd12[][] with first index in range(elementsCountAlong + 1), - second inner index in range(elementsCountAround) + Abstract base class for building meshes from a NetworkMesh network layout. """ - px, pd1, pd2, pd12 = rawTubeCoordinates - pointsCountAlong = len(px) - elementsCountAround = len(px[0]) - # resample at even spacing along - sx = [[None] * elementsCountAround for _ in range(elementsCountAlong + 1)] - sd1 = [[None] * elementsCountAround for _ in range(elementsCountAlong + 1)] - sd2 = [[None] * elementsCountAround for _ in range(elementsCountAlong + 1)] - sd12 = [[None] * elementsCountAround for _ in range(elementsCountAlong + 1)] - for q in range(elementsCountAround): - cx = [px[p][q] for p in range(pointsCountAlong)] - cd1 = [pd1[p][q] for p in range(pointsCountAlong)] - cd2 = [pd2[p][q] for p in range(pointsCountAlong)] - cd12 = [pd12[p][q] for p in range(pointsCountAlong)] - startCurveLocation = None - if startSurface: - startSurfacePosition, startCurveLocation, startIntersects = startSurface.findNearestPositionOnCurve(cx, cd2) - if not startIntersects: - startCurveLocation = None - endCurveLocation = None - if endSurface: - endSurfacePosition, endCurveLocation, endIntersects = endSurface.findNearestPositionOnCurve(cx, cd2) - if not endIntersects: - endCurveLocation = None - qx, qd2, pe, pxi, psf = sampleCubicHermiteCurvesSmooth( - cx, cd2, elementsCountAlong, startLocation=startCurveLocation, endLocation=endCurveLocation) - qd1, qd12 = interpolateSampleCubicHermite(cd1, cd12, pe, pxi, psf) - # swizzle - for p in range(elementsCountAlong + 1): - sx[p][q] = qx[p] - sd1[p][q] = qd1[p] - sd2[p][q] = qd2[p] - sd12[p][q] = qd12[p] - - # recalculate d1 around intermediate rings, but still in plane - # normally looks fine, but d1 derivatives are wavy when very distorted - pStart = 0 if startSurface else 1 - pLimit = elementsCountAlong + 1 if endSurface else elementsCountAlong - for p in range(pStart, pLimit): - # first smooth to get d1 with new directions not tangential to surface - td1 = smoothCubicHermiteDerivativesLoop(sx[p], sd1[p]) - # constraint to be tangential to surface - td1 = [vectorRejection(td1[q], normalize(cross(sd1[p][q], sd2[p][q]))) for q in range(elementsCountAround)] - # smooth magnitudes only - sd1[p] = smoothCubicHermiteDerivativesLoop(sx[p], td1, fixAllDirections=True) - - return sx, sd1, sd2, sd12 + + def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, + layoutAnnotationGroups): + self._networkMesh = networkMesh + self._targetElementDensityAlongLongestSegment = targetElementDensityAlongLongestSegment + self._layoutAnnotationGroups = layoutAnnotationGroups + self._layoutRegion = networkMesh.getRegion() + layoutFieldmodule = self._layoutRegion.getFieldmodule() + self._layoutNodes = layoutFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self._layoutCoordinates = layoutFieldmodule.findFieldByName("coordinates").castFiniteElement() + self._layoutMesh = layoutFieldmodule.findMeshByDimension(1) + self._segments = {} # map from NetworkSegment to NetworkMeshSegment-derived object + self._longestSegmentLength = 0.0 + self._targetElementLength = 1.0 + self._junctions = {} # map from NetworkNode to NetworkMeshJunction-derived object + + @abstractmethod + def createSegment(self, networkSegment): + """ + Factory method for creating a NetworkMeshSegment. + Override in concrete class to create a derived Segment for building the mesh of required type. + :param networkSegment: A network segment from the underlying NetworkMesh + :return: An object derived from NetworkMeshSegment. + """ + return None + + def _createSegments(self): + """ + Create objects for building segment meshes. + """ + self._segments = {} + self._longestSegmentLength = 0.0 + for networkSegment in self._networkMesh.getNetworkSegments(): + # derived class makes the segment of its required type + self._segments[networkSegment] = segment = self.createSegment(networkSegment) + segmentLength = segment.getLength() + if segmentLength > self._longestSegmentLength: + self._longestSegmentLength = segmentLength + for layoutAnnotationGroup in self._layoutAnnotationGroups: + if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationGroup.getMeshGroup(self._layoutMesh)): + segment.addAnnotationTerm(layoutAnnotationGroup.getTerm()) + if self._longestSegmentLength > 0.0: + self._targetElementLength = self._longestSegmentLength / self._targetElementDensityAlongLongestSegment + + @abstractmethod + def createJunction(self, inSegments, outSegments): + """ + Factory method for creating a NetworkMeshJunction. + Override in concrete class to create a derived Junction for building the mesh of required type. + :param inSegments: List of inward NetworkMeshSegment-derived objects. + :param outSegments: List of outward NetworkMeshSegment-derived objects. + :return: An object derived from NetworkMeshJunction. + """ + return None + + def _createJunctions(self): + """ + Create objects for building junction meshes between segments. + Must have called self.createSegments() first. + """ + self._junctions = {} + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + segmentNodes = networkSegment.getNetworkNodes() + segmentJunctions = [] + for nodeIndex in (0, -1): + segmentNode = segmentNodes[nodeIndex] + junction = self._junctions.get(segmentNode) + if not junction: + inSegments = [self._segments[networkSegment] for networkSegment in segmentNode.getInSegments()] + outSegments = [self._segments[networkSegment] for networkSegment in segmentNode.getOutSegments()] + junction = self.createJunction(inSegments, outSegments) + self._junctions[segmentNode] = junction + segmentJunctions.append(junction) + segment.setJunctions(segmentJunctions) + + def _sampleSegments(self): + """ + Sample coordinates in segments to fit surrounding junctions. + Must have called self.createJunctions() first. + """ + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + segment.sample(self._targetElementLength) + + def _sampleJunctions(self): + """ + Sample coordinates in junctions to fit surrounding junctions. + Optionally blend common derivatives across simple junctions. + Must have called self.sampleSegments() first. + """ + sampledJunctions = set() + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + for junction in segment.getJunctions(): + if junction not in sampledJunctions: + junction.sample(self._targetElementLength) + sampledJunctions.add(junction) + + def build(self): + """ + Build coordinates for network mesh. + """ + self._createSegments() + self._createJunctions() + self._sampleSegments() + self._sampleJunctions() + + def generateMesh(self, generateData: NetworkMeshGenerateData): + """ + Generate mesh from segments and junctions, in order of segments. + Must have called self.build() first. + Assumes ChangeManager active for region/fieldmodule. + :param generateData: NetworkMeshGenerateData-derived object. + """ + generatedJunctions = set() + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + junctions = segment.getJunctions() + if junctions[0] not in generatedJunctions: + junctions[0].generateMesh(generateData) + generatedJunctions.add(junctions[0]) + segment.generateMesh(generateData) + if junctions[1] not in generatedJunctions: + junctions[1].generateMesh(generateData) + generatedJunctions.add(junctions[1]) diff --git a/src/scaffoldmaker/utils/tracksurface.py b/src/scaffoldmaker/utils/tracksurface.py index 0b8d2d9d..936b7c2b 100644 --- a/src/scaffoldmaker/utils/tracksurface.py +++ b/src/scaffoldmaker/utils/tracksurface.py @@ -1144,13 +1144,17 @@ def findNearestPositionOnCurve(self, cx, cd1, loop=False, startCurveLocation=Non if instrument: print("TrackSurface.findNearestPositionOnCurve: Found intersection: ", curveLocation, "on iter", it + 1) return surfacePosition, curveLocation, True - n = normalize(cross(cross(d, r), d)) - r_dot_n = dot(r, n) - if r_dot_n < 0: - # flip normal to be towards other x - n = [-s for s in n] - r_dot_n = -r_dot_n - rNormal = mult(n, r_dot_n) + cross_dr = cross(d, r) + if magnitude(cross_dr) < (1.0E-6 * magnitude(d)): + rNormal = [0.0, 0.0, 0.0] + else: + n = normalize(cross(cross_dr, d)) + r_dot_n = dot(r, n) + if r_dot_n < 0.0: + # flip normal to be towards other x + n = [-s for s in n] + r_dot_n = -r_dot_n + rNormal = mult(n, r_dot_n) rTangent = sub(r, rNormal) mag_ri = magnitude(rTangent) mag_ro = magnitude(rNormal) @@ -1182,7 +1186,7 @@ def findNearestPositionOnCurve(self, cx, cd1, loop=False, startCurveLocation=Non mag_u = magnitude(u) * curvatureFactor # never go further than parallel, based on curvature from initial angle parallelFactor = 1.0 - if curvature > 0.0: + if (mag_ro > 0.0) and (curvature > 0.0): max_u = math.atan(mag_ri / mag_ro) / curvature if mag_u > max_u: parallelFactor = max_u / mag_u @@ -1231,7 +1235,7 @@ def findNearestPositionOnCurve(self, cx, cd1, loop=False, startCurveLocation=Non break else: print('TrackSurface.findNearestPositionOnCurve did not converge: Reached max iterations', it + 1, - 'closeness in xi', mag_dxi) + 'closeness in xi', mag_dxi, 'distance tangent', mag_ri, 'normal', mag_ro) return surfacePosition, curveLocation, False def trackVector(self, startPosition, direction, trackDistance): diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py new file mode 100644 index 00000000..464fbb1b --- /dev/null +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -0,0 +1,1382 @@ +""" +Specialisation of Network Mesh for building 2-D and 3-D tube mesh networks. +""" +from cmlibs.maths.vectorops import add, cross, dot, magnitude, mult, normalize, sub +from cmlibs.zinc.element import Element, Elementbasis +from cmlibs.zinc.node import Node +from scaffoldmaker.utils.eft_utils import determineCubicHermiteSerendipityEft, HermiteNodeLayoutManager +from scaffoldmaker.utils.interpolation import ( + computeCubicHermiteDerivativeScaling, DerivativeScalingMode, evaluateCoordinatesOnCurve, + getCubicHermiteTrimmedCurvesLengths, interpolateCubicHermite, interpolateCubicHermiteDerivative, + interpolateLagrangeHermiteDerivative, interpolateSampleCubicHermite, sampleCubicHermiteCurvesSmooth, + smoothCubicHermiteDerivativesLine, smoothCubicHermiteDerivativesLoop, smoothCurveSideCrossDerivatives) +from scaffoldmaker.utils.networkmesh import NetworkMesh, NetworkMeshBuilder, NetworkMeshGenerateData, \ + NetworkMeshJunction, NetworkMeshSegment, pathValueLabels +from scaffoldmaker.utils.tracksurface import TrackSurface +from scaffoldmaker.utils.vector import vectorRejection +from scaffoldmaker.utils.zinc_utils import get_nodeset_path_ordered_field_parameters +import copy +import math + + +class TubeNetworkMeshGenerateData(NetworkMeshGenerateData): + """ + Data for passing to TubeNetworkMesh generateMesh functions. + """ + + def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurfaces, + coordinateFieldName="coordinates", startNodeIdentifier=1, startElementIdentifier=1): + """ + :param isLinearThroughWall: Callers should only set if 3-D with no core. + :param isShowTrimSurfaces: Tells junction generateMesh to make 2-D trim surfaces. + """ + super(TubeNetworkMeshGenerateData, self).__init__( + region, meshDimension, coordinateFieldName, startNodeIdentifier, startElementIdentifier) + self._isLinearThroughWall = isLinearThroughWall + self._isShowTrimSurfaces = isShowTrimSurfaces + + # get node template for standard and cross nodes + self._nodetemplate = self._nodes.createNodetemplate() + self._nodetemplate.defineField(self._coordinates) + self._nodetemplate.setValueNumberOfVersions(self._coordinates, -1, Node.VALUE_LABEL_D_DS1, 1) + self._nodetemplate.setValueNumberOfVersions(self._coordinates, -1, Node.VALUE_LABEL_D_DS2, 1) + if (meshDimension == 3) and not isLinearThroughWall: + self._nodetemplate.setValueNumberOfVersions(self._coordinates, -1, Node.VALUE_LABEL_D_DS3, 1) + + # get element template and eft for standard case + self._standardElementtemplate = self._mesh.createElementtemplate() + self._standardElementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE if (meshDimension == 3) + else Element.SHAPE_TYPE_SQUARE) + elementbasis = self._fieldmodule.createElementbasis( + meshDimension, Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE_SERENDIPITY) + if (meshDimension == 3) and isLinearThroughWall: + elementbasis.setFunctionType(3, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) + self._standardEft = self._mesh.createElementfieldtemplate(elementbasis) + self._standardElementtemplate.defineField(self._coordinates, -1, self._standardEft) + + d3Defined = (meshDimension == 3) and not isLinearThroughWall + self._nodeLayoutManager = HermiteNodeLayoutManager() + self._nodeLayout6Way = self._nodeLayoutManager.getNodeLayout6Way12(d3Defined) + self._nodeLayout8Way = self._nodeLayoutManager.getNodeLayout8Way12(d3Defined) + self._nodeLayoutFlipD2 = self._nodeLayoutManager.getNodeLayoutRegularPermuted( + d3Defined, limitDirections=[None, [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]], [[0.0, 0.0, 1.0]]] if d3Defined + else [None, [[0.0, 1.0], [0.0, -1.0]]]) + + def getStandardEft(self): + return self._standardEft + + def getStandardElementtemplate(self): + return self._standardElementtemplate + + def getNodeLayout6Way(self): + return self._nodeLayout6Way + + def getNodeLayout8Way(self): + return self._nodeLayout8Way + + def getNodeLayoutFlipD2(self): + return self._nodeLayoutFlipD2 + + def getNodetemplate(self): + return self._nodetemplate + + def isLinearThroughWall(self): + return self._isLinearThroughWall + + def isShowTrimSurfaces(self): + return self._isShowTrimSurfaces + +class TubeNetworkMeshSegment(NetworkMeshSegment): + + def __init__(self, networkSegment, pathParametersList, elementsCountAround, elementsCountThroughWall): + """ + :param networkSegment: NetworkSegment this is built from. + :param pathParametersList: [pathParameters] if 2-D or [outerPathParameters, innerPathParameters] if 3-D + :param elementsCountAround: Number of elements around this segment. + :param elementsCountThroughWall: Number of elements between inner and outer tube if 3-D, 1 if 2-D. + """ + super(TubeNetworkMeshSegment, self).__init__(networkSegment, pathParametersList) + self._elementsCountAround = elementsCountAround + assert elementsCountThroughWall > 0 + self._elementsCountThroughWall = elementsCountThroughWall + self._rawTubeCoordinatesList = [] + self._rawTrackSurfaceList = [] + for pathParameters in pathParametersList: + px, pd1, pd2, pd12 = getPathRawTubeCoordinates(pathParameters, elementsCountAround) + self._rawTubeCoordinatesList.append((px, pd1, pd2, pd12)) + nx, nd1, nd2, nd12 = [], [], [], [] + for i in range(len(px)): + nx += px[i] + nd1 += pd1[i] + nd2 += pd2[i] + nd12 += pd12[i] + self._rawTrackSurfaceList.append(TrackSurface(len(px[0]), len(px) - 1, nx, nd1, nd2, nd12, loop1=True)) + # list[pathsCount][4] of sx, sd1, sd2, sd12; all [nAlong][nAround]: + self._sampledTubeCoordinates = [None for p in range(self._pathsCount)] + self._rimCoordinates = None + self._rimNodeIds = None + self._rimElementIds = None # [e2][e3][e1] + + def getElementsCountAround(self): + return self._elementsCountAround + + def getRawTubeCoordinates(self, pathIndex=0): + return self._rawTubeCoordinatesList[pathIndex] + + def getRawTrackSurface(self, pathIndex=0): + return self._rawTrackSurfaceList[pathIndex] + + def sample(self, targetElementLength): + trimSurfaces = [self._junctions[j].getTrimSurfaces(self) for j in range(2)] + minimumElementsCountAlong = 2 if (self._isLoop or ((self._junctions[0].getSegmentsCount() > 2) and + (self._junctions[1].getSegmentsCount() > 2))) else 1 + elementsCountAlong = None + for p in range(self._pathsCount): + # determine elementsCountAlong for first/outer tube then fix for inner tubes + self._sampledTubeCoordinates[p] = resampleTubeCoordinates( + self._rawTubeCoordinatesList[p], fixedElementsCountAlong=elementsCountAlong, + targetElementLength=targetElementLength, minimumElementsCountAlong=minimumElementsCountAlong, + startSurface=trimSurfaces[0][p], endSurface=trimSurfaces[1][p]) + if not elementsCountAlong: + elementsCountAlong = len(self._sampledTubeCoordinates[0][0]) - 1 + + if self._dimension == 2: + # copy first sampled tube coordinates, but insert single-entry 'n3' index after n2 + self._rimCoordinates = ( + [[ring] for ring in self._sampledTubeCoordinates[0][0]], + [[ring] for ring in self._sampledTubeCoordinates[0][1]], + [[ring] for ring in self._sampledTubeCoordinates[0][2]], + None) + else: + wallFactor = 1.0 / self._elementsCountThroughWall + ox, od1, od2 = self._sampledTubeCoordinates[0][0:3] + ix, id1, id2 = self._sampledTubeCoordinates[1][0:3] + rx, rd1, rd2, rd3 = [], [], [], [] + for n2 in range(elementsCountAlong + 1): + for r in (rx, rd1, rd2, rd3): + r.append([]) + otx, otd1, otd2 = ox[n2], od1[n2], od2[n2] + itx, itd1, itd2 = ix[n2], id1[n2], id2[n2] + wd3 = [mult(sub(otx[n1], itx[n1]), wallFactor) for n1 in range(self._elementsCountAround)] + for n3 in range(self._elementsCountThroughWall + 1): + oFactor = n3 / self._elementsCountThroughWall + iFactor = 1.0 - oFactor + for r in (rx, rd1, rd2, rd3): + r[n2].append([]) + for n1 in range(self._elementsCountAround): + if n3 == 0: + x, d1, d2 = itx[n1], itd1[n1], itd2[n1] + elif n3 == self._elementsCountThroughWall: + x, d1, d2 = otx[n1], otd1[n1], otd2[n1] + else: + x = add(mult(itx[n1], iFactor), mult(otx[n1], oFactor)) + d1 = add(mult(itd1[n1], iFactor), mult(otd1[n1], oFactor)) + d2 = add(mult(itd2[n1], iFactor), mult(otd2[n1], oFactor)) + d3 = wd3[n1] + for r, value in zip((rx, rd1, rd2, rd3), (x, d1, d2, d3)): + r[n2][n3].append(value) + self._rimCoordinates = rx, rd1, rd2, rd3 + self._rimNodeIds = [None] * (elementsCountAlong + 1) + self._rimElementIds = [None] * elementsCountAlong + + @classmethod + def blendSampledCoordinates(cls, segment1 , nodeIndexAlong1, segment2, nodeIndexAlong2): + nodesCountAround = segment1._elementsCountAround + nodesCountRim = len(segment1._rimCoordinates[0][0]) + if ((nodesCountAround != segment2._elementsCountAround) or + (nodesCountRim != len(segment2._rimCoordinates[0][0]))): + return # can't blend unless these match + + # blend rim coordinates + for n3 in range(nodesCountRim): + s1d2 = segment1._rimCoordinates[2][nodeIndexAlong1][n3] + s2d2 = segment2._rimCoordinates[2][nodeIndexAlong2][n3] + for n1 in range(nodesCountAround): + # harmonic mean magnitude + s1d2Mag = magnitude(s1d2[n1]) + s2d2Mag = magnitude(s2d2[n1]) + d2Mag = 2.0 / ((1.0 / s1d2Mag) + (1.0 / s2d2Mag)) + d2 = mult(s1d2[n1], d2Mag / s1d2Mag) + s1d2[n1] = d2 + s2d2[n1] = d2 + + def getSampledElementsCountAlong(self): + return len(self._sampledTubeCoordinates[0][0]) - 1 + + def getSampledTubeCoordinatesRing(self, pathIndex, nodeIndexAlong): + """ + Get a ring of sampled coordinates at the supplied node index. + :param pathIndex: 0 for outer/primary, 1 or -1 for inner/secondary. + :param nodeIndexAlong: Node index from 0 to self._elementsCountAlong, or negative to count from end. + :return: sx[nAround] + """ + return self._sampledTubeCoordinates[pathIndex][0][nodeIndexAlong] + + def getElementsCountRim(self): + return max(1, len(self._rimCoordinates[0][0]) - 1) + + def getNodesCountRim(self): + return len(self._rimCoordinates[0][0]) + + def getRimCoordinatesListAlong(self, n1, n2List, n3): + """ + Get list of parameters for n2 indexes along segment at given n1, n3. + :param n1: Node index around segment. + :param n2List: List of node indexes along segment. + :param n3: Node index from inner to outer rim. + :return: [x[], d1[], d2[], d3[]]. d3[] may be None + """ + paramsList = [] + for i in range(4): + params = [] + for n2 in n2List: + params.append(self._rimCoordinates[i][n2][n3][n1] if self._rimCoordinates[i] else None) + paramsList.append(params) + return paramsList + + def getRimCoordinates(self, n1, n2, n3): + """ + Get rim parameters at a point. + :param n1: Node index around. + :param n2: Node index along segment. + :param n3: Node index from inner to outer rim. + :return: x, d1, d2, d3 + """ + return (self._rimCoordinates[0][n2][n3][n1], + self._rimCoordinates[1][n2][n3][n1], + self._rimCoordinates[2][n2][n3][n1], + self._rimCoordinates[3][n2][n3][n1] if self._rimCoordinates[3] else None) + + def getRimNodeId(self, n1, n2, n3): + """ + Get a rim node ID for a point. + :param n1: Node index around. + :param n2: Node index along segment. + :param n3: Node index from inner to outer rim. + :return: Node identifier. + """ + return self._rimNodeIds[n2][n3][n1] + + def getRimElementId(self, e1, e2, e3): + """ + Get a rim element ID. + :param e1: Element index around. + :param e2: Element index along segment. + :param e3: Element index from inner to outer rim. + :return: Element identifier. + """ + return self._rimElementIds[e2][e3][e1] + + def setRimElementId(self, e1, e2, e3, elementIdentifier): + """ + Set a rim element ID. Only called by adjacent junctions. + :param e1: Element index around. + :param e2: Element index along segment. + :param e3: Element index from inner to outer rim. + :param elementIdentifier: Element identifier. + """ + if not self._rimElementIds[e2]: + elementsCountRim = self.getElementsCountRim() + self._rimElementIds[e2] = [[None] * self._elementsCountAround for _ in range(elementsCountRim)] + self._rimElementIds[e2][e3][e1] = elementIdentifier + + def getRimNodeIdsSlice(self, n2): + """ + Get slice of rim node IDs. + :param n2: Node index along segment, including negative indexes from end. + :return: Node IDs arrays through wall and around, or None if not set. + """ + return self._rimNodeIds[n2] + + def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): + """ + :param n2Only: If set, create nodes only for that single n2 index along. Must be >= 0! + """ + elementsCountAlong = len(self._rimCoordinates[0]) - 1 + elementsCountRim = self.getElementsCountRim() + coordinates = generateData.getCoordinates() + fieldcache = generateData.getFieldcache() + startSkipCount = 1 if (self._junctions[0].getSegmentsCount() > 2) else 0 + endSkipCount = 1 if (self._junctions[1].getSegmentsCount() > 2) else 0 + + # create nodes + nodes = generateData.getNodes() + isLinearThroughWall = generateData.isLinearThroughWall() + nodetemplate = generateData.getNodetemplate() + for n2 in range(elementsCountAlong + 1) if (n2Only is None) else [n2Only]: + if (n2 < startSkipCount) or (n2 > elementsCountAlong - endSkipCount): + self._rimNodeIds[n2] = None + continue + if self._rimNodeIds[n2]: + continue + # get shared nodes from single adjacent segment, including loop on itself + # only handles one in, one out + if n2 == 0: + if self._junctions[0].getSegmentsCount() == 2: + segments = self._junctions[0].getSegments() + rimNodeIds = segments[0].getRimNodeIdsSlice(-1) + if rimNodeIds: + self._rimNodeIds[n2] = rimNodeIds + continue + if n2 == elementsCountAlong: + if self._junctions[1].getSegmentsCount() == 2: + segments = self._junctions[1].getSegments() + rimNodeIds = segments[1].getRimNodeIdsSlice(0) + if rimNodeIds: + self._rimNodeIds[n2] = rimNodeIds + continue + + # create rim nodes + nodesCountRim = len(self._rimCoordinates[0][0]) + self._rimNodeIds[n2] = [] + for n3 in range(nodesCountRim): + rx = self._rimCoordinates[0][n2][n3] + rd1 = self._rimCoordinates[1][n2][n3] + rd2 = self._rimCoordinates[2][n2][n3] + rd3 = None if isLinearThroughWall else self._rimCoordinates[3][n2][n3] + ringNodeIds = [] + for n1 in range(self._elementsCountAround): + nodeIdentifier = generateData.nextNodeIdentifier() + node = nodes.createNode(nodeIdentifier, nodetemplate) + fieldcache.setNode(node) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, rx[n1]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, rd1[n1]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, rd2[n1]) + if rd3: + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, rd3[n1]) + ringNodeIds.append(nodeIdentifier) + self._rimNodeIds[n2].append(ringNodeIds) + + if n2Only is not None: + return + + # create elements + annotationMeshGroups = generateData.getAnnotationMeshGroups(self._annotationTerms) + mesh = generateData.getMesh() + elementtemplateStd = generateData.getStandardElementtemplate() + eftStd = generateData.getStandardEft() + for e2 in range(startSkipCount, elementsCountAlong - endSkipCount): + self._rimElementIds[e2] = [] + e2p = e2 + 1 + + # create rim elements + for e3 in range(elementsCountRim): + ringElementIds = [] + for e1 in range(self._elementsCountAround): + e1p = (e1 + 1) % self._elementsCountAround + nids = [] + for n3 in [e3, e3 + 1] if (self._dimension == 3) else [0]: + nids += [self._rimNodeIds[e2][n3][e1], self._rimNodeIds[e2][n3][e1p], + self._rimNodeIds[e2p][n3][e1], self._rimNodeIds[e2p][n3][e1p]] + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplateStd) + element.setNodesByIdentifier(eftStd, nids) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + ringElementIds.append(elementIdentifier) + self._rimElementIds[e2].append(ringElementIds) + + +class TubeNetworkMeshJunction(NetworkMeshJunction): + """ + Describes junction between multiple tube segments, some in, some out. + """ + + def __init__(self, inSegments: list, outSegments: list): + """ + :param inSegments: List of inward TubeNetworkMeshSegment. + :param outSegments: List of outward TubeNetworkMeshSegment. + """ + super(TubeNetworkMeshJunction, self).__init__(inSegments, outSegments) + pathsCount = self._segments[0].getPathsCount() + self._trimSurfaces = [[None for p in range(pathsCount)] for s in range(self._segmentsCount)] + self._calculateTrimSurfaces() + # rim indexes are issued for interior points connected to 2 or more segment node indexes + # based on the outer surface, and reused through the wall + self._rimIndexToSegmentNodeList = [] # list[rim index] giving list[(segment number, node index around)] + self._segmentNodeToRimIndex = [] # list[segment number][node index around] to rimIndex + # rim coordinates sampled in the junction are indexed by n3 (through the wall) and 'rim index' + self._rimCoordinates = None # if set, (rx[], rd1[], rd2[], rd3[]) each over [n3][rim index] + self._rimNodeIds = None # if set, nodeIdentifier[n3][rim index] + + def _calculateTrimSurfaces(self): + """ + Calculate surfaces for trimming adjacent segments so they can smoothly transition at junction. + Algorithm gets 6 intersection points of longitudinal lines around each segment with other tubes or the end. + These are joined to make a partial cone radiating back to the centre of the junction. + Longitudinal lines start on the edge adjacent to the other segment most normal to it. + """ + if self._segmentsCount < 3: + return + pathsCount = self._segments[0].getPathsCount() + # get directions at end of segments' paths: + outDirs = [[] for s in range(self._segmentsCount)] + for s in range(self._segmentsCount): + endIndex = -1 if self._segmentsIn[s] else 0 + for p in range(pathsCount): + pathParameters = self._segments[s].getPathParameters(p) + outDir = normalize(pathParameters[1][endIndex]) + if self._segmentsIn[s]: + outDir = [-d for d in outDir] + outDirs[s].append(outDir) + + trimPointsCountAround = 6 + trimAngle = 2.0 * math.pi / trimPointsCountAround + for s in range(self._segmentsCount): + endIndex = -1 if self._segmentsIn[s] else 0 + for p in range(pathsCount): + pathParameters = self._segments[s].getPathParameters(p) + d2End = pathParameters[2][endIndex] + d3End = pathParameters[4][endIndex] + endEllipseNormal = normalize(cross(d2End, d3End)) + sOutDir = outDirs[s][p] + # get phase angles and weights of other segments + angles = [] + weights = [] + sumWeights = 0.0 + maxWeight = 0.0 + phaseAngle = None + for os in range(self._segmentsCount): + if os == s: + continue + osOutDir = outDirs[os][p] + dx = dot(osOutDir, d2End) + dy = dot(osOutDir, d3End) + if (dx == 0.0) and (dy == 0.0): + angle = 0.0 + weight = 0.0 + else: + angle = math.atan2(dy, dx) + if angle < 0.0: + angle += 2.0 * math.pi + weight = math.pi - math.acos(dot(sOutDir, osOutDir)) + if weight > maxWeight: + maxWeight = weight + phaseAngle = angle + sumWeights += weight + weights.append(weight) + angles.append(angle) + # get correction to phase angle + weightedSumDeltaAngles = 0.0 + for os in range(len(angles)): + angle = angles[os] - phaseAngle + if angle < 0.0: + angle += 2.0 * math.pi + nearestAngle = math.floor(angle / trimAngle + 0.5) * trimAngle + deltaAngle = nearestAngle - angle + weightedSumDeltaAngles += weights[os] * deltaAngle + phaseAngle -= weightedSumDeltaAngles / sumWeights + lx, ld1, ld2, ld12 = getPathRawTubeCoordinates( + pathParameters, trimPointsCountAround, radius=1.0, phaseAngle=phaseAngle) + pointsCountAlong = len(pathParameters[0]) + + # get coordinates and directions of intersection points of longitudinal lines and other track surfaces + rx = [] + rd1 = [] + trim = False + lowestMaxProportionFromEnd = 1.0 + for n1 in range(trimPointsCountAround): + cx = [lx[n2][n1] for n2 in range(pointsCountAlong)] + cd1 = [ld1[n2][n1] for n2 in range(pointsCountAlong)] + cd2 = [ld2[n2][n1] for n2 in range(pointsCountAlong)] + cd12 = [ld12[n2][n1] for n2 in range(pointsCountAlong)] + x = lx[endIndex][n1] + d1 = ld1[endIndex][n1] + maxProportionFromEnd = 0.0 + # find intersection point with other segments which is furthest from end + for os in range(self._segmentsCount): + if os == s: + continue + otherSegment = self._segments[os] + otherTrackSurface = otherSegment.getRawTrackSurface(p) + otherSurfacePosition, curveLocation, isIntersection = \ + otherTrackSurface.findNearestPositionOnCurve( + cx, cd2, loop=False, sampleEnds=False, sampleHalf=2 if self._segmentsIn[s] else 1) + if isIntersection: + proportion2 = (curveLocation[0] + curveLocation[1]) / (pointsCountAlong - 1) + proportionFromEnd = math.fabs(proportion2 - (1.0 if self._segmentsIn[s] else 0.0)) + if proportionFromEnd > maxProportionFromEnd: + trim = True + x, d2 = evaluateCoordinatesOnCurve(cx, cd2, curveLocation, loop=False, derivative=True) + d1 = evaluateCoordinatesOnCurve(cd1, cd12, curveLocation, loop=False) + n = cross(d1, d2) # normal to this surface + ox, od1, od2 = otherTrackSurface.evaluateCoordinates( + otherSurfacePosition, derivatives=True) + on = cross(od1, od2) # normal to other surface + d1 = cross(n, on) + maxProportionFromEnd = proportionFromEnd + if maxProportionFromEnd < lowestMaxProportionFromEnd: + lowestMaxProportionFromEnd = maxProportionFromEnd + rx.append(x) + rd1.append(d1) + + if trim: + # centre of trim surfaces is at lowestMaxProportionFromEnd + if lowestMaxProportionFromEnd == 0.0: + xCentre = pathParameters[0][endIndex] + else: + proportion = (1.0 - lowestMaxProportionFromEnd) if self._segmentsIn[s]\ + else lowestMaxProportionFromEnd + e = int(proportion) + curveLocation = (e, proportion - e) + xCentre = evaluateCoordinatesOnCurve(pathParameters[0], pathParameters[1], curveLocation) + # ensure d1 directions go around in same direction as loop + for n1 in range(trimPointsCountAround): + d1 = rd1[n1] + if dot(endEllipseNormal, cross(sub(rx[n1], xCentre), d1)) < 0.0: + for c in range(3): + d1[c] = -d1[c] + rd1 = smoothCubicHermiteDerivativesLoop(rx, rd1, fixAllDirections=True, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) + rd2 = [sub(rx[n1], xCentre) for n1 in range(trimPointsCountAround)] + rd12 = smoothCurveSideCrossDerivatives(rx, rd1, [rd2], loop=True)[0] + nx = [] + nd1 = [] + nd2 = [] + nd12 = [] + for factor in (0.75, 1.25): + for n1 in range(trimPointsCountAround): + d2 = sub(rx[n1], xCentre) + x = add(xCentre, mult(d2, factor)) + d1 = mult(rd1[n1], factor) + d12 = mult(rd12[n1], factor) + nx.append(x) + nd1.append(d1) + nd2.append(d2) + nd12.append(d12) + trimSurface = TrackSurface(trimPointsCountAround, 1, nx, nd1, nd2, nd12, loop1=True) + self._trimSurfaces[s][p] = trimSurface + + def getTrimSurfaces(self, segment): + """ + :param segment: TubeNetworkMeshSegment which must join at junction. + :return: List of trim surfaces for paths of segment at junction. + """ + return self._trimSurfaces[self._segments.index(segment)] + + def _sampleMidPoint(self, segmentsParameterLists): + """ + Get mid-point coordinates and derivatives within junction from 2 or more segments' parameters. + :param segmentsParameterLists: List over segment indexes s of [x, d1, d2, d3], each with 2 last parameters. + d3 will be None for 2-D of bicubic-linear. + :return: Mid-point x, d1, d2, d3. Derivative magnitudes will need smoothing.` + """ + segmentsIn = [dot(sub(params[0][1], params[0][0]), params[2][1]) > 0.0 for params in segmentsParameterLists] + segmentsCount = len(segmentsIn) + assert segmentsCount > 1 + d3Defined = None not in segmentsParameterLists[0][3] + # for each segment get inward parameters halfway between last 2 parameters + xi = 0.5 + hx = [] + hd1 = [] + hd2 = [] + hn = [] # normal hd1 x hd2 + hd3 = [] if d3Defined else None + for s in range(segmentsCount): + params = segmentsParameterLists[s] + hd2m = [params[2][i] if segmentsIn[s] else [-d for d in params[2][i]] for i in range(2)] + hx.append(interpolateCubicHermite(params[0][0], hd2m[0], params[0][1], hd2m[1], xi)) + hd1.append(mult(add(params[1][0], params[1][1]), 0.5 if segmentsIn[s] else -0.5)) + hd2.append(interpolateCubicHermiteDerivative(params[0][0], hd2m[0], params[0][1], hd2m[1], xi)) + hn.append(normalize(cross(hd1[-1], hd2[-1]))) + if d3Defined: + hd3.append(mult(add(params[3][0], params[3][1]), 0.5)) + # get lists of mid-point parameters for all segment permutations + mx = [] + md1 = [] + md2 = [] + md3 = [] if d3Defined else None + xi = 0.5 + sideFactor = 1.0 + outFactor = 0.5 # only used if sideFactor is non-zero + for s1 in range(segmentsCount - 1): + # fxs1 = segmentsParameterLists[s1][0][0] + # fd2s1 = segmentsParameterLists[s1][2][0] + # if segmentsIn[s1]: + # fd2s1 = [-d for d in fd2s1] + for s2 in range(s1 + 1, segmentsCount): + hd2s1 = hd2[s1] + hd2s2 = [-d for d in hd2[s2]] + if sideFactor > 0.0: + # compromise with direct connection respecting surface tangents + sideDirection = normalize(sub(hx[s2], hx[s1])) + side1d1 = dot(normalize(hd1[s1]), sideDirection) + side1d2 = dot(normalize(hd2[s1]), sideDirection) + side1 = add(mult(hd1[s1], side1d1), mult(hd2[s1], side1d2)) + side2d1 = dot(normalize(hd1[s2]), sideDirection) + side2d2 = dot(normalize(hd2[s2]), sideDirection) + side2 = add(mult(hd1[s2], side2d1), mult(hd2[s2], side2d2)) + sideScaling = computeCubicHermiteDerivativeScaling(hx[s1], side1, hx[s2], side2) + hd2s1 = add(mult(hd2s1, outFactor), mult(side1, sideScaling * sideFactor)) + hd2s2 = add(mult(hd2s2, outFactor), mult(side2, sideScaling * sideFactor)) + scaling = computeCubicHermiteDerivativeScaling(hx[s1], hd2s1, hx[s2], hd2s2) + hd2s1 = mult(hd2s1, scaling) + hd2s2 = mult(hd2s2, scaling) + cx = interpolateCubicHermite(hx[s1], hd2s1, hx[s2], hd2s2, xi) + cd2 = interpolateCubicHermiteDerivative(hx[s1], hd2s1, hx[s2], hd2s2, xi) + mx.append(cx) + md1.append(mult(add(hd1[s1], [-d for d in hd1[s2]]), 0.5)) + md2.append(cd2) + # smooth smx, smd2 with 2nd row from end coordinates and derivatives + # fxs2 = segmentsParameterLists[s2][0][0] + # fd2s2 = segmentsParameterLists[s2][2][0] + # if not segmentsIn[s1]: + # fd2s2 = [-d for d in fd2s1] + # tmd2 = smoothCubicHermiteDerivativesLine( + # [fxs1, cx, fxs2], [fd2s1, cd2, fd2s2], fixStartDerivative=True, fixEndDerivative=True) + # md2.append(tmd2[1]) + if d3Defined: + md3.append(mult(add(hd3[s1], hd3[s2]), 0.5)) + if segmentsCount == 2: + if not segmentsIn[0]: + md1[0] = [-d for d in md1[0]] + md2[0] = [-d for d in md2[0]] + return mx[0], md1[0], md2[0], md3[0] if d3Defined else None + mCount = len(mx) + cx = [sum(x[c] for x in mx) / mCount for c in range(3)] + cd3 = [sum(d3[c] for d3 in md3) / mCount for c in range(3)] if d3Defined else None + ns12 = [0.0, 0.0, 0.0] + for m in range(len(md1)): + cp12 = normalize(cross(md1[m], md2[m])) + for c in range(3): + ns12[c] += cp12[c] + ns12 = normalize(ns12) + + # get best fit directions with these 360/segmentsCount degrees apart + + # get preferred derivative at centre out to each segment + rd = [interpolateLagrangeHermiteDerivative( + cx, segmentsParameterLists[s][0][0], [-d for d in segmentsParameterLists[s][2][0]] if segmentsIn[s] + else segmentsParameterLists[s][2][0], 0.0) for s in range(segmentsCount)] + # get orthonormal axes with ns12, axis1 in direction of first preferred derivative + axis1 = normalize(cross(cross(ns12, rd[0]), ns12)) + axis2 = cross(ns12, axis1) + # get angles and sequence around normal, starting at axis1 + angles = [0.0] + for s in range(1, segmentsCount): + angle = math.atan2(dot(rd[s], axis2), dot(rd[s], axis1)) + angles.append((2.0 * math.pi + angle) if (angle < 0.0) else angle) + angle = 0.0 + sequence = [0] + for s in range(1, segmentsCount): + nextAngle = math.pi * 4.0 + nexts = 0 + for t in range(1, segmentsCount): + if angle < angles[t] < nextAngle: + nextAngle = angles[t] + nexts = t + angle = nextAngle + sequence.append(nexts) + + angleIncrement = 2.0 * math.pi / segmentsCount + deltaAngle = 0.0 + angle = 0.0 + magSum = 0.0 + for s in range(1, segmentsCount): + angle += angleIncrement + deltaAngle += angles[sequence[s]] - angle + magSum += 1.0 / magnitude(rd[sequence[s]]) + deltaAngle = deltaAngle / segmentsCount + d2Mean = segmentsCount / magSum + + angle = deltaAngle + sd = [None] * segmentsCount + for s in range(segmentsCount): + x1 = d2Mean * math.cos(angle) + x2 = d2Mean * math.sin(angle) + sd[sequence[s]] = add(mult(axis1, x1), mult(axis2, x2)) + angle += angleIncrement + + if segmentsCount == 3: + # get through score for pairs of directions + maxThroughScore = 0.0 + si = None + for s1 in range(segmentsCount - 1): + dir1 = normalize(segmentsParameterLists[s1][2][1]) + for s2 in range(s1 + 1, segmentsCount): + dir2 = normalize(segmentsParameterLists[s2][2][1]) + throughScore = abs(dot(dir1, dir2)) + if throughScore > maxThroughScore: + maxThroughScore = throughScore + si = (s1, s2) + if maxThroughScore < 0.9: + # maintain symmetry of bifurcations + if (segmentsIn == [True, True, False]) or (segmentsIn == [False, False, True]): + si = (0, 1) + elif (segmentsIn == [True, False, True]) or (segmentsIn == [False, True, False]): + si = (2, 0) + else: + si = (1, 2) + + elif segmentsCount == 4: + si = sequence[1:3] + # print("sequence", sequence, "si", si) + else: + print("TubeNetworkMeshJunction._sampleMidPoint not fully implemented for segmentsCount =", segmentsCount) + si = (1, 2) + + # get harmonic mean of d1 magnitude around midpoints + magSum = 0.0 + for s in range(segmentsCount): + magSum += 1.0 / magnitude(md1[s]) + d1Mean = segmentsCount / magSum + # overall harmonic mean of derivatives to err on the low side for the diagonal derivatives + dMean = 2.0 / (1.0 / d1Mean + 1.0 / d2Mean) + + if segmentsCount == 4: + # use the equal spaced directions + td = [mult(normalize(sd[j]), dMean) for j in si] + else: + # td = [sd[j] for j in si] + # compromise between preferred directions rd and equal spaced directions sd + td = [mult(normalize(add(rd[j], sd[j])), dMean) for j in si] + if segmentsIn.count(True) == 2: + # reverse so matches inward directions + td = [[-c for c in d] for d in td] + # td = [sub(d, mult(ns12, dot(d, ns12))) for d in td] + + cd1, cd2 = td + if dot(cross(cd1, cd2), ns12) < 0.0: + cd1, cd2 = cd2, cd1 + return cx, cd1, cd2, cd3 + + def sample(self, targetElementLength): + """ + Blend sampled d2 derivatives across 2-segment junctions with the same version. + Sample junction coordinates between second-from-end segment coordinates. + :param targetElementLength: Ignored here as always 2 elements across junction. + """ + if self._segmentsCount == 1: + return + if self._segmentsCount == 2: + TubeNetworkMeshSegment.blendSampledCoordinates( + self._segments[0], -1 if self._segmentsIn[0] else 0, + self._segments[1], -1 if self._segmentsIn[1] else 0) + return + + aroundCounts = [segment.getElementsCountAround() for segment in self._segments] + rimIndexesCount = 0 + + if self._segmentsCount == 3: + # numbers of elements directly connecting pairs of segments + # sequence = [0, 1, 2] + connectionCounts = [(aroundCounts[s] + aroundCounts[s - 2] - aroundCounts[s - 1]) // 2 for s in range(3)] + for s in range(3): + if (connectionCounts[s] < 1) or (aroundCounts[s] != (connectionCounts[s - 1] + connectionCounts[s])): + print("Can't make tube bifurcation between elements counts around", aroundCounts) + return + + self._rimIndexToSegmentNodeList = [] # list[rim index] giving list[(segment index, node index around)] + self._segmentNodeToRimIndex = [[None] * aroundCounts[s] for s in range(self._segmentsCount)] + for s1 in range(3): + s2 = (s1 + 1) % 3 + startNodeIndex1 = (aroundCounts[s1] - connectionCounts[s1]) // 2 + startNodeIndex2 = connectionCounts[s1] // -2 + for n in range(connectionCounts[s1] + 1): + nodeIndex1 = startNodeIndex1 + (n if self._segmentsIn[s1] else (connectionCounts[s1] - n)) + if self._segmentNodeToRimIndex[s1][nodeIndex1] is None: + nodeIndex2 = startNodeIndex2 + ((connectionCounts[s1] - n) if self._segmentsIn[s2] else n) + if self._segmentNodeToRimIndex[s2][nodeIndex2] is None: + rimIndex = rimIndexesCount + # keep list in order from lowest s + self._rimIndexToSegmentNodeList.append( + [[s1, nodeIndex1], [s2, nodeIndex2]] if (s1 < s2) else + [[s2, nodeIndex2], [s1, nodeIndex1]]) + self._segmentNodeToRimIndex[s2][nodeIndex2] = rimIndex + rimIndexesCount += 1 + else: + rimIndex = self._segmentNodeToRimIndex[s2][nodeIndex2] + # keep list in order from lowest s + segmentNodeList = self._rimIndexToSegmentNodeList[rimIndex] + index = 0 + for i in range(len(segmentNodeList)): + if s1 < segmentNodeList[i][0]: + break + index += 1 + segmentNodeList.insert(index, [s1, nodeIndex1]) + self._segmentNodeToRimIndex[s1][nodeIndex1] = rimIndex + + elif self._segmentsCount == 4: + # only support plus + shape for now, not tetrahedral + # determine plus + sequence [0, 1, 2, 3], [0, 1, 3, 2], [0, 2, 1, 3] or [0, 2, 3, 1] + outDirections = [] + for s in range(self._segmentsCount): + d1 = self._segments[s].getPathParameters()[1][-1 if self._segmentsIn[s] else 0] + outDirections.append(normalize([-d for d in d1] if self._segmentsIn[s] else d1)) + ms = None + for s in range(1, self._segmentsCount): + if dot(outDirections[0], outDirections[s]) > -0.9: + ns = cross(outDirections[0], outDirections[s]) + break + # get orthonormal axes with ns12, axis1 in direction of first preferred derivative + axis1 = normalize(cross(cross(ns, outDirections[0]), ns)) + axis2 = normalize(cross(ns, axis1)) + # get angles and sequence around normal, starting at axis1 + angles = [0.0] + for s in range(1, self._segmentsCount): + angle = math.atan2(dot(outDirections[s], axis2), dot(outDirections[s], axis1)) + angles.append((2.0 * math.pi + angle) if (angle < 0.0) else angle) + angle = 0.0 + sequence = [0] + for s in range(1, self._segmentsCount): + nextAngle = math.pi * 4.0 + nexts = 0 + for t in range(1, self._segmentsCount): + if angle < angles[t] < nextAngle: + nextAngle = angles[t] + nexts = t + angle = nextAngle + sequence.append(nexts) + pairCount02 = aroundCounts[sequence[0]] + aroundCounts[sequence[2]] + pairCount13 = aroundCounts[sequence[1]] + aroundCounts[sequence[3]] + throughCount02 = ((pairCount02 - pairCount13) // 2) if (pairCount02 > pairCount13) else 0 + throughCount13 = ((pairCount13 - pairCount02) // 2) if (pairCount13 > pairCount02) else 0 + throughCounts = [throughCount02, throughCount13, throughCount02, throughCount13] + # numbers of elements directly connecting pairs of segments + # print("sample sequence", sequence) + # for s in range(self._segmentsCount): + # print(s, ":", sequence[s], "=", aroundCounts[sequence[s]], sequence[(s - 1) % self._segmentsCount], '=', + # aroundCounts[sequence[(s - 1) % self._segmentsCount]], sequence[s - 1], "=", aroundCounts[sequence[s - 1]], throughCounts[s], (s % 2)) + freeAroundCounts = [aroundCounts[sequence[s]] - throughCounts[s] for s in range(self._segmentsCount)] + if freeAroundCounts[0] == freeAroundCounts[2]: + count03 = freeAroundCounts[3] // 2 + count12 = freeAroundCounts[1] // 2 + connectionCounts = [count03, count12, count12, count03] + elif freeAroundCounts[1] == freeAroundCounts[3]: + count03 = freeAroundCounts[0] // 2 + count12 = freeAroundCounts[2] // 2 + connectionCounts = [count03, count12, count12, count03] + else: + connectionCounts = [((freeAroundCounts[s] + freeAroundCounts[(s + 1) % self._segmentsCount] + - freeAroundCounts[s - 1] + (s % 2)) // 2) for s in range(self._segmentsCount)] + # print("aroundCounts", aroundCounts) + # print("sequence", sequence) + # print("throughCounts", throughCounts) + # print("freeAroundCounts", freeAroundCounts) + # print("connectionCounts", connectionCounts) + for s in range(self._segmentsCount): + # print("s", s, "around", aroundCounts[sequence[s]], "connL", connectionCounts[s - 1], "through", throughCounts[s], "connR", connectionCounts[s]) + if (aroundCounts[sequence[s]] != (connectionCounts[s - 1] + throughCounts[s] + connectionCounts[s])): + print("Can't make tube junction between elements counts around", aroundCounts) + return + + self._rimIndexToSegmentNodeList = [] # list[rim index] giving list[(segment index, node index around)] + self._segmentNodeToRimIndex = [[None] * aroundCounts[s] for s in range(self._segmentsCount)] + for os1 in range(self._segmentsCount): + os2 = (os1 + 1) % self._segmentsCount + s1 = sequence[os1] + s2 = sequence[os2] + s3 = sequence[(os1 + 2) % self._segmentsCount] + halfThroughCount = throughCounts[os1] // 2 + os1ConnectionCount = connectionCounts[os1] + os2ConnectionCount = connectionCounts[os2] + if self._segmentsIn[s1]: + startNodeIndex1 = (aroundCounts[s1] - os1ConnectionCount) // 2 + else: + startNodeIndex1 = os1ConnectionCount // -2 + if self._segmentsIn[s2]: + startNodeIndex2 = os1ConnectionCount // -2 + else: + startNodeIndex2 = (aroundCounts[s2] - os1ConnectionCount) // 2 + if self._segmentsIn[s3]: + startNodeIndex3h = os2ConnectionCount // -2 + startNodeIndex3l = startNodeIndex3h - (os2ConnectionCount - os1ConnectionCount) + else: + startNodeIndex3l = (aroundCounts[s3] - os2ConnectionCount) // 2 + startNodeIndex3h = startNodeIndex3l + (os2ConnectionCount - os1ConnectionCount) + + # print(os1, "half", halfThroughCount, "start", startNodeIndex1, + # "in1", self._segmentsIn[s1], "in2", self._segmentsIn[s2], "in3", self._segmentsIn[s3]) + for n in range(-halfThroughCount, os1ConnectionCount + 1 + halfThroughCount): + n1 = startNodeIndex1 + (n if self._segmentsIn[s1] else (os1ConnectionCount - n)) + segmentIndexes = [s1] + nodeIndexes = [n1 % aroundCounts[s1]] + if 0 <= n <= os1ConnectionCount: + n2 = startNodeIndex2 + ((os1ConnectionCount - n) if self._segmentsIn[s2] else n) + segmentIndexes.append(s2) + nodeIndexes.append(n2 % aroundCounts[s2]) + if halfThroughCount and ((n <= 0) or (n >= os1ConnectionCount)): + n3 = ((startNodeIndex3l if n <= 0 else startNodeIndex3h) + + ((os2ConnectionCount - n) if self._segmentsIn[s3] else n)) + segmentIndexes.append(s3) + nodeIndexes.append(n3 % aroundCounts[s3]) + # print("seg node", [[segmentIndexes[i], nodeIndexes[i]] for i in range(len(segmentIndexes))]) + rimIndex = None + for i in range(len(segmentIndexes)): + ri = self._segmentNodeToRimIndex[segmentIndexes[i]][nodeIndexes[i]] + if ri is not None: + rimIndex = ri + # for j in range(len(segmentIndexes)): + # sn = [segmentIndexes[j], nodeIndexes[j]] + # if sn not in self._rimIndexToSegmentNodeList[rimIndex]: + # print(" add", sn, "to rim index", ri, "segment nodes", self._rimIndexToSegmentNodeList[rimIndex]) + break + if rimIndex is None: + # new rim index + rimIndex = rimIndexesCount + rimIndexesCount += 1 + segmentNodeList = [] + self._rimIndexToSegmentNodeList.append(segmentNodeList) + else: + segmentNodeList = self._rimIndexToSegmentNodeList[rimIndex] + # build maps: rim index <--> segment index, node index + for i in range(len(segmentIndexes)): + segmentIndex = segmentIndexes[i] + nodeIndex = nodeIndexes[i] + if self._segmentNodeToRimIndex[segmentIndex][nodeIndex] is None: + # keep segment node list in order from lowest segment index + index = 0 + for j in range(len(segmentNodeList)): + if segmentIndex < segmentNodeList[j][0]: + break + index += 1 + segmentNodeList.insert(index, [segmentIndex, nodeIndex]) + self._segmentNodeToRimIndex[segmentIndex][nodeIndex] = rimIndex + else: + print("Tube network mesh not implemented for", self._segmentsCount, "segments at junction") + return + + if not rimIndexesCount: + return + + # for rimIndex in range(rimIndexesCount): + # print("rim index", rimIndex, self._rimIndexToSegmentNodeList[rimIndex]) + + # get node indexes giving the lowest sum of distances between adjoining points on outer sampled tubes + permutationCount = 1 + for count in aroundCounts: + permutationCount *= count + minIndexes = None + minSum = None + indexes = [0] * self._segmentsCount + rings = [self._segments[s].getSampledTubeCoordinatesRing(0, -1 if self._segmentsIn[s] else 0) + for s in range(self._segmentsCount)] + for p in range(permutationCount): + sum = 0.0 + for rimIndex in range(rimIndexesCount): + segmentNodeList = self._rimIndexToSegmentNodeList[rimIndex] + sCount = len(segmentNodeList) + for i in range(sCount - 1): + s1, n1 = segmentNodeList[i] + nodeIndex1 = (n1 + indexes[s1]) % aroundCounts[s1] + x1 = rings[s1][nodeIndex1] + for j in range(i + 1, sCount): + s2, n2 = segmentNodeList[j] + nodeIndex2 = (n2 + indexes[s2]) % aroundCounts[s2] + x2 = rings[s2][nodeIndex2] + sum += magnitude([x2[0] - x1[0], x2[1] - x1[1], x2[2] - x1[2]]) + if (minSum is None) or (sum < minSum): + minIndexes = copy.copy(indexes) + minSum = sum + # permute through indexes: + for s in range(self._segmentsCount): + indexes[s] += 1 + if indexes[s] < aroundCounts[s]: + break + indexes[s] = 0 + + # print("minIndexes", minIndexes) + + # offset node indexes by minIndexes + for rimIndex in range(rimIndexesCount): + segmentNodeList = self._rimIndexToSegmentNodeList[rimIndex] + for segmentNode in segmentNodeList: + s, n = segmentNode + nodeIndex = (n + minIndexes[s]) % aroundCounts[s] + self._segmentNodeToRimIndex[s][nodeIndex] = rimIndex + segmentNode[1] = nodeIndex + + # sample rim coordinates + nodesCountRim = self._segments[0].getNodesCountRim() + rx, rd1, rd2, rd3 = [ + [[None] * rimIndexesCount for _ in range(nodesCountRim)] for i in range(4)] + self._rimCoordinates = (rx, rd1, rd2, rd3) + for n3 in range(nodesCountRim): + for rimIndex in range(rimIndexesCount): + segmentNodeList = self._rimIndexToSegmentNodeList[rimIndex] + # segments have been ordered from lowest to highest s index + segmentsParameterLists = [] + for s, n1 in segmentNodeList: + segmentsParameterLists.append( + self._segments[s].getRimCoordinatesListAlong( + n1, [-2, -1] if self._segmentsIn[s] else [1, 0], n3)) + rx[n3][rimIndex], rd1[n3][rimIndex], rd2[n3][rimIndex], rd3[n3][rimIndex] = \ + self._sampleMidPoint(segmentsParameterLists) + + def generateMesh(self, generateData: TubeNetworkMeshGenerateData): + if generateData.isShowTrimSurfaces(): + dimension = generateData.getMeshDimension() + nodeIdentifier, elementIdentifier = generateData.getNodeElementIdentifiers() + faceIdentifier = elementIdentifier if (dimension == 2) else None + for s in range(self._segmentsCount): + for trimSurface in self._trimSurfaces[s]: + if trimSurface: + nodeIdentifier, faceIdentifier = \ + trimSurface.generateMesh(generateData.getRegion(), nodeIdentifier, faceIdentifier) + if dimension == 2: + elementIdentifier = faceIdentifier + generateData.setNodeElementIdentifiers(nodeIdentifier, elementIdentifier) + + if self._segmentsCount < 3: + return + + rimIndexesCount = len(self._rimIndexToSegmentNodeList) + nodesCountRim = self._segments[0].getNodesCountRim() + elementsCountRim = max(1, nodesCountRim - 1) + if self._rimCoordinates: + self._rimNodeIds = [[None] * rimIndexesCount for _ in range(nodesCountRim)] + + coordinates = generateData.getCoordinates() + fieldcache = generateData.getFieldcache() + nodes = generateData.getNodes() + nodetemplate = generateData.getNodetemplate() + isLinearThroughWall = generateData.isLinearThroughWall() + mesh = generateData.getMesh() + meshDimension = generateData.getMeshDimension() + elementtemplate = mesh.createElementtemplate() + elementtemplate.setElementShapeType( + Element.SHAPE_TYPE_CUBE if (meshDimension == 3) else Element.SHAPE_TYPE_SQUARE) + d3Defined = (meshDimension == 3) and not isLinearThroughWall + + nodeLayout6Way = generateData.getNodeLayout6Way() + nodeLayout8Way = generateData.getNodeLayout8Way() + nodeLayoutFlipD2 = generateData.getNodeLayoutFlipD2() + + # nodes and elements are generated in order of segments + for s in range(self._segmentsCount): + segment = self._segments[s] + elementsCountAlong = segment.getSampledElementsCountAlong() + e2 = (elementsCountAlong - 1) if self._segmentsIn[s] else 0 + n2 = (elementsCountAlong - 1) if self._segmentsIn[s] else 1 + segment.generateMesh(generateData, n2Only=n2) + + elementsCountAround = segment.getElementsCountAround() + + if self._rimCoordinates: + # create rim nodes + for n3 in range(nodesCountRim): + rx = self._rimCoordinates[0][n3] + rd1 = self._rimCoordinates[1][n3] + rd2 = self._rimCoordinates[2][n3] + rd3 = self._rimCoordinates[3][n3] if d3Defined else None + layerNodeIds = self._rimNodeIds[n3] + for n1 in range(elementsCountAround): + rimIndex = self._segmentNodeToRimIndex[s][n1] + nodeIdentifier = self._rimNodeIds[n3][rimIndex] + if nodeIdentifier is not None: + continue + nodeIdentifier = generateData.nextNodeIdentifier() + node = nodes.createNode(nodeIdentifier, nodetemplate) + fieldcache.setNode(node) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, rx[rimIndex]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, rd1[rimIndex]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, rd2[rimIndex]) + if rd3: + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, rd3[rimIndex]) + layerNodeIds[rimIndex] = nodeIdentifier + + if self._rimCoordinates: + # create rim elements + annotationMeshGroups = generateData.getAnnotationMeshGroups(segment.getAnnotationTerms()) + eftList = [None] * elementsCountAround + scalefactorsList = [None] * elementsCountAround + for e3 in range(elementsCountRim): + for e1 in range(elementsCountAround): + n1p = (e1 + 1) % elementsCountAround + nids = [] + nodeParameters = [] + nodeLayouts = [] + for n3 in [e3, e3 + 1] if (meshDimension == 3) else [e3]: + for n1 in [e1, n1p]: + nids.append(self._segments[s].getRimNodeId(n1, n2, n3)) + if e3 == 0: + rimCoordinates = self._segments[s].getRimCoordinates(n1, n2, n3) + nodeParameters.append(rimCoordinates if d3Defined else + (rimCoordinates[0], rimCoordinates[1], rimCoordinates[2], None)) + nodeLayouts.append(None) + for n1 in [e1, n1p]: + rimIndex = self._segmentNodeToRimIndex[s][n1] + nids.append(self._rimNodeIds[n3][rimIndex]) + if e3 == 0: + nodeParameters.append( + (self._rimCoordinates[0][n3][rimIndex], + self._rimCoordinates[1][n3][rimIndex], + self._rimCoordinates[2][n3][rimIndex], + self._rimCoordinates[3][n3][rimIndex] if d3Defined else None)) + segmentNodesCount = len(self._rimIndexToSegmentNodeList[rimIndex]) + nodeLayouts.append(nodeLayoutFlipD2 if (segmentNodesCount == 2) else + nodeLayout6Way if (segmentNodesCount == 3) else + nodeLayout8Way) + if not self._segmentsIn[s]: + for a in [nids, nodeParameters, nodeLayouts] if (e3 == 0) else [nids]: + a[-4], a[-2] = a[-2], a[-4] + a[-3], a[-1] = a[-1], a[-3] + # exploit efts being same through the wall + eft = eftList[e1] + scalefactors = scalefactorsList[e1] + if not eft: + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + eftList[e1] = eft + scalefactorsList[e1] = scalefactors + elementtemplate.defineField(coordinates, -1, eft) + elementIdentifier = generateData.nextElementIdentifier() + element = mesh.createElement(elementIdentifier, elementtemplate) + segment.setRimElementId(e1, e2, e3, elementIdentifier) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) + for annotationMeshGroup in annotationMeshGroups: + annotationMeshGroup.addElement(element) + + +class TubeNetworkMeshBuilder(NetworkMeshBuilder): + + def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, + defaultElementsCountAround: int, elementsCountThroughWall: int, + layoutAnnotationGroups: list = [], annotationElementsCountsAround: list = [], + annotationElementsCountsAcrossMajor: list = []): + super(TubeNetworkMeshBuilder, self).__init__( + networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups) + self._defaultElementsCountAround = defaultElementsCountAround + self._elementsCountThroughWall = elementsCountThroughWall + self._annotationElementsCountsAround = annotationElementsCountsAround + self._annotationElementsCountsAcrossMajor = annotationElementsCountsAcrossMajor + layoutFieldmodule = self._layoutRegion.getFieldmodule() + self._layoutInnerCoordinates = layoutFieldmodule.findFieldByName("inner coordinates").castFiniteElement() + if not self._layoutInnerCoordinates.isValid(): + self._layoutInnerCoordinates = None + + def createSegment(self, networkSegment): + pathParametersList = [get_nodeset_path_ordered_field_parameters( + self._layoutNodes, self._layoutCoordinates, pathValueLabels, + networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions())] + if self._layoutInnerCoordinates: + pathParametersList.append(get_nodeset_path_ordered_field_parameters( + self._layoutNodes, self._layoutInnerCoordinates, pathValueLabels, + networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions())) + elementsCountAround = self._defaultElementsCountAround + i = 0 + for layoutAnnotationGroup in self._layoutAnnotationGroups: + if i >= len(self._annotationElementsCountsAround): + break + if self._annotationElementsCountsAround[i] > 0: + if networkSegment.hasLayoutElementsInMeshGroup(layoutAnnotationGroup.getMeshGroup(self._layoutMesh)): + elementsCountAround = self._annotationElementsCountsAround[i] + break + i += 1 + return TubeNetworkMeshSegment(networkSegment, pathParametersList, elementsCountAround, + self._elementsCountThroughWall) + + def createJunction(self, inSegments, outSegments): + """ + :param inSegments: List of inward TubeNetworkMeshSegment. + :param outSegments: List of outward TubeNetworkMeshSegment. + :return: A TubeNetworkMeshJunction. + """ + return TubeNetworkMeshJunction(inSegments, outSegments) + + +def getPathRawTubeCoordinates(pathParameters, elementsCountAround, radius=1.0, phaseAngle=0.0): + """ + Generate coordinates around and along a tube in parametric space around the path parameters, + at xi2^2 + xi3^2 = radius at the same density as path parameters. + :param pathParameters: List over nodes of 6 parameters vectors [cx, cd1, cd2, cd12, cd3, cd13] giving + coordinates cx along path centre, derivatives cd1 along path, cd2 and cd3 giving side vectors, + and cd12, cd13 giving rate of change of side vectors. Parameters have 3 components. + Same format as output of zinc_utils get_nodeset_path_ordered_field_parameters(). + :param elementsCountAround: Number of elements & nodes to create around tube. First location is at +d2. + :param radius: Radius of tube in xi space. + :param phaseAngle: Starting angle around ellipse, where 0.0 is at d2, pi/2 is at d3. + :return: px[][], pd1[][], pd2[][], pd12[][] with first index in range(pointsCountAlong), + second inner index in range(elementsCountAround) + """ + assert len(pathParameters) == 6 + pointsCountAlong = len(pathParameters[0]) + assert pointsCountAlong > 1 + assert len(pathParameters[0][0]) == 3 + + # sample around circle in xi space, later smooth and re-sample to get even spacing in geometric space + ellipsePointCount = 16 + aroundScale = 2.0 * math.pi / ellipsePointCount + sxi = [] + sdxi = [] + angleBetweenPoints = 2.0 * math.pi / ellipsePointCount + for q in range(ellipsePointCount): + theta = phaseAngle + q * angleBetweenPoints + xi2 = radius * math.cos(theta) + xi3 = radius * math.sin(theta) + sxi.append([xi2, xi3]) + dxi2 = -xi3 * aroundScale + dxi3 = xi2 * aroundScale + sdxi.append([dxi2, dxi3]) + + px = [] + pd1 = [] + pd2 = [] + pd12 = [] + for p in range(pointsCountAlong): + cx, cd1, cd2, cd12, cd3, cd13 = [cp[p] for cp in pathParameters] + tx = [] + td1 = [] + for q in range(ellipsePointCount): + xi2 = sxi[q][0] + xi3 = sxi[q][1] + x = [(cx[c] + xi2 * cd2[c] + xi3 * cd3[c]) for c in range(3)] + tx.append(x) + dxi2 = sdxi[q][0] + dxi3 = sdxi[q][1] + d1 = [(dxi2 * cd2[c] + dxi3 * cd3[c]) for c in range(3)] + td1.append(d1) + # smooth to get reasonable derivative magnitudes + td1 = smoothCubicHermiteDerivativesLoop(tx, td1, fixAllDirections=True) + # resample to get evenly spaced points around loop, temporarily adding start point to end + ex, ed1, pe, pxi, psf = sampleCubicHermiteCurvesSmooth(tx + tx[:1], td1 + td1[:1], elementsCountAround) + exi, edxi = interpolateSampleCubicHermite(sxi + sxi[:1], sdxi + sdxi[:1], pe, pxi, psf) + ex.pop() + ed1.pop() + exi.pop() + edxi.pop() + + # check closeness of x(exi[i]) to ex[i] + # find nearest xi2, xi3 if above is finite error + # a small but non-negligible error, but results look fine so not worrying + # dxi = [] + # for i in range(len(ex)): + # xi2 = exi[i][0] + # xi3 = exi[i][1] + # xi23 = xi2 * xi3 + # x = [(cx[c] + xi2 * cd2[c] + xi3 * cd3[c]) for c in range(3)] + # dxi.append(sub(x, ex[i])) + # print("error", p, "=", [magnitude(v) for v in dxi]) + + # calculate d2, d12 at exi + ed2 = [] + ed12 = [] + for i in range(len(ex)): + xi2 = exi[i][0] + xi3 = exi[i][1] + d2 = [(cd1[c] + xi2 * cd12[c] + xi3 * cd13[c]) for c in range(3)] + ed2.append(d2) + dxi2 = edxi[i][0] + dxi3 = edxi[i][1] + d12 = [(dxi2 * cd12[c] + dxi3 * cd13[c]) for c in range(3)] + ed12.append(d12) + + px.append(ex) + pd1.append(ed1) + pd2.append(ed2) + pd12.append(ed12) + + return px, pd1, pd2, pd12 + + +def resampleTubeCoordinates(rawTubeCoordinates, fixedElementsCountAlong=None, + targetElementLength=None, minimumElementsCountAlong=1, + startSurface: TrackSurface=None, endSurface: TrackSurface=None): + """ + Generate new tube coordinates along raw tube coordinates, optionally trimmed to start/end surfaces. + Untrimmed tube elements are even sized along each longitudinal curve. + Trimmed tube elements adjust derivatives at trimmed ends to transition from distorted to regular spacing. + Can specify either fixedElementsCountAlong or targetElementLength. + :param rawTubeCoordinates: (px, pd1, pd2, pd12) returned by getPathRawTubeCoordinates(). + :param fixedElementsCountAlong: Number of elements in resampled coordinates, or None to use targetElementLength. + :param targetElementLength: Target element length or None to use fixedElementsCountAlong. + Length is compared with mean trimmed length to determine number along, subject to specified minimum. + :param minimumElementsCountAlong: Minimum number along when targetElementLength is used. + :param startSurface: Optional TrackSurface specifying start of tube at intersection with it. + :param endSurface: Optional TrackSurface specifying end of tube at intersection with it. + :return: sx[][], sd1[][], sd2[][], sd12[][] with first index in range(elementsCountAlong + 1), + second inner index in range(elementsCountAround) + """ + assert fixedElementsCountAlong or targetElementLength + px, pd1, pd2, pd12 = rawTubeCoordinates + pointsCountAlong = len(px) + endPointLocation = float(pointsCountAlong - 1) + elementsCountAround = len(px[0]) + + # work out lengths of longitudinal curves, raw and trimmed + sumLengths = 0.0 + startCurveLocations = [] + startLengths = [] + meanStartLocation = 0.0 + endCurveLocations = [] + endLengths = [] + meanEndLocation = 0.0 + for q in range(elementsCountAround): + cx = [px[p][q] for p in range(pointsCountAlong)] + cd2 = [pd2[p][q] for p in range(pointsCountAlong)] + startCurveLocation = None + if startSurface: + startSurfacePosition, startCurveLocation, startIntersects = startSurface.findNearestPositionOnCurve(cx, cd2) + if startIntersects: + meanStartLocation += startCurveLocation[0] + startCurveLocation[1] + else: + startCurveLocation = None + startCurveLocations.append(startCurveLocation) + endCurveLocation = None + if endSurface: + endSurfacePosition, endCurveLocation, endIntersects = endSurface.findNearestPositionOnCurve(cx, cd2) + if endIntersects: + meanEndLocation += endCurveLocation[0] + endCurveLocation[1] + else: + endCurveLocation = None + if not endCurveLocation: + meanEndLocation += endPointLocation + endCurveLocations.append(endCurveLocation) + startLength, length, endLength =\ + getCubicHermiteTrimmedCurvesLengths(cx, cd2, startCurveLocation, endCurveLocation)[0:3] + sumLengths += length + startLengths.append(startLength) + endLengths.append(endLength) + + meanLength = sumLengths / elementsCountAround + if fixedElementsCountAlong: + elementsCountAlong = fixedElementsCountAlong + else: + # small fudge factor so whole numbers chosen on centroid don't go one higher: + elementsCountAlong = max(minimumElementsCountAlong, math.ceil(meanLength * 0.999 / targetElementLength)) + meanStartLocation /= elementsCountAround + e = min(int(meanStartLocation), pointsCountAlong - 2) + meanStartCurveLocation = (e, meanStartLocation - e) + meanEndLocation /= elementsCountAround + e = min(int(meanEndLocation), pointsCountAlong - 2) + meanEndCurveLocation = (e, meanEndLocation - e) + + # resample along, with variable spacing where ends are trimmed + sx = [[None] * elementsCountAround for _ in range(elementsCountAlong + 1)] + sd1 = [[None] * elementsCountAround for _ in range(elementsCountAlong + 1)] + sd2 = [[None] * elementsCountAround for _ in range(elementsCountAlong + 1)] + sd12 = [[None] * elementsCountAround for _ in range(elementsCountAlong + 1)] + for q in range(elementsCountAround): + cx = [px[p][q] for p in range(pointsCountAlong)] + cd1 = [pd1[p][q] for p in range(pointsCountAlong)] + cd2 = [pd2[p][q] for p in range(pointsCountAlong)] + cd12 = [pd12[p][q] for p in range(pointsCountAlong)] + meanStartLength, meanLength, meanEndLength = \ + getCubicHermiteTrimmedCurvesLengths(cx, cd2, meanStartCurveLocation, meanEndCurveLocation)[0:3] + derivativeMagnitudeStart = (meanLength + 2.0 * (meanStartLength - startLengths[q])) / elementsCountAlong + derivativeMagnitudeEnd = (meanLength + 2.0 * (meanEndLength - endLengths[q])) / elementsCountAlong + qx, qd2, pe, pxi, psf = sampleCubicHermiteCurvesSmooth( + cx, cd2, elementsCountAlong, derivativeMagnitudeStart, derivativeMagnitudeEnd, + startLocation=startCurveLocations[q], endLocation=endCurveLocations[q]) + qd1, qd12 = interpolateSampleCubicHermite(cd1, cd12, pe, pxi, psf) + # swizzle + for p in range(elementsCountAlong + 1): + sx[p][q] = qx[p] + sd1[p][q] = qd1[p] + sd2[p][q] = qd2[p] + sd12[p][q] = qd12[p] + + # recalculate d1 around intermediate rings, but still in plane + # normally looks fine, but d1 derivatives are wavy when very distorted + pStart = 0 if startSurface else 1 + pLimit = elementsCountAlong + 1 if endSurface else elementsCountAlong + for p in range(pStart, pLimit): + # first smooth to get d1 with new directions not tangential to surface + td1 = smoothCubicHermiteDerivativesLoop(sx[p], sd1[p]) + # constraint to be tangential to surface + td1 = [vectorRejection(td1[q], normalize(cross(sd1[p][q], sd2[p][q]))) for q in range(elementsCountAround)] + # smooth magnitudes only + sd1[p] = smoothCubicHermiteDerivativesLoop(sx[p], td1, fixAllDirections=True) + + return sx, sd1, sd2, sd12 diff --git a/src/scaffoldmaker/utils/zinc_utils.py b/src/scaffoldmaker/utils/zinc_utils.py index 62f1979d..66e33f40 100644 --- a/src/scaffoldmaker/utils/zinc_utils.py +++ b/src/scaffoldmaker/utils/zinc_utils.py @@ -1,7 +1,6 @@ """ Utility functions for easing use of Zinc API. """ -from cmlibs.maths.vectorops import mult from cmlibs.utils.zinc.field import find_or_create_field_coordinates, find_or_create_field_group from cmlibs.utils.zinc.finiteelement import get_maximum_element_identifier, get_maximum_node_identifier from cmlibs.utils.zinc.general import ChangeManager, HierarchicalChangeManager @@ -9,7 +8,7 @@ from cmlibs.zinc.element import Element, Elementbasis, MeshGroup from cmlibs.zinc.field import Field, FieldGroup from cmlibs.zinc.fieldmodule import Fieldmodule -from cmlibs.zinc.node import Node, Nodeset +from cmlibs.zinc.node import Node, NodesetGroup from cmlibs.zinc.result import RESULT_OK from scaffoldmaker.utils import interpolation as interp from scaffoldmaker.utils import vector @@ -684,3 +683,75 @@ def generateCurveMesh(region, nx, nd1, loop=False, startNodeIdentifier=None, sta return nodeIdentifier, elementIdentifier + +def mesh_get_element_nodes_map(mesh): + """ + Get the nodes used by each element in mesh, in no particular order. + Supports face and line meshes which inherit field from higher-level elements. + This is quite expensive for large meshes as relies on group sub-element handling. + Zinc issue: nodes list is only based on the first coordinate field. + :param mesh: A Zinc mesh or mesh group containing the elements to query. + :return: dict element identifier -> list(node identifiers) + """ + fieldmodule = mesh.getFieldmodule() + elementid_to_nodeids = {} + with ChangeManager(fieldmodule): + group = fieldmodule.createFieldGroup() + group.setSubelementHandlingMode(FieldGroup.SUBELEMENT_HANDLING_MODE_FULL) + mesh_group = group.createMeshGroup(mesh) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + nodeset_group = group.createNodesetGroup(nodes) + elementiterator = mesh.createElementiterator() + element = elementiterator.next() + while element.isValid(): + mesh_group.addElement(element) + nodeiterator = nodeset_group.createNodeiterator() + node = nodeiterator.next() + nodeIds = [] + while node.isValid(): + nodeIds.append(node.getIdentifier()) + node = nodeiterator.next() + nodeset_group.removeAllNodes() + del nodeiterator + elementid_to_nodeids[element.getIdentifier()] = nodeIds + element = elementiterator.next() + del elementiterator + del mesh_group + del nodeset_group + del group + return elementid_to_nodeids + +def group_add_connected_elements(group: FieldGroup, other_mesh_group: MeshGroup): + """ + Add to group the elements from other_mesh_group which use a node from the group's + node group, or are connected to an element which does. + Note this can be quite expensive for large meshes. + :param group: Zinc FieldGroup to add elements to. The group's NodeGroup must + contain at least one node to connect to, and nodes from connected elements are + added to it + :param other_mesh_group: Other mesh group containing candidate elements to add. + """ + fieldmodule = group.getFieldmodule() + with ChangeManager(fieldmodule): + old_subelement_mode = group.getSubelementHandlingMode() + group.setSubelementHandlingMode(FieldGroup.SUBELEMENT_HANDLING_MODE_FULL) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + mesh_group = group.getOrCreateMeshGroup(other_mesh_group) + assert mesh_group.isValid() + nodeset_group = group.getNodesetGroup(nodes) + assert nodeset_group.isValid() + elementid_to_nodeids = mesh_get_element_nodes_map(other_mesh_group) + connected = True + while connected: + connected = False + for elementid, nodeids in elementid_to_nodeids.items(): + for nodeid in nodeids: + node = nodeset_group.findNodeByIdentifier(nodeid) + if node.isValid(): + connected = True + break + if connected: + mesh_group.addElement(other_mesh_group.findElementByIdentifier(elementid)) + del elementid_to_nodeids[elementid] + break + group.setSubelementHandlingMode(old_subelement_mode) diff --git a/tests/test_general.py b/tests/test_general.py index c0831b2a..3db263d3 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -19,14 +19,14 @@ from scaffoldmaker.meshtypes.meshtype_3d_stomach1 import MeshType_3d_stomach1 from scaffoldmaker.scaffoldpackage import ScaffoldPackage from scaffoldmaker.scaffolds import Scaffolds -from scaffoldmaker.utils.bifurcation import SegmentTubeData from scaffoldmaker.utils.eft_utils import determineTricubicHermiteEft from scaffoldmaker.utils.geometry import getEllipsoidPlaneA, getEllipsoidPolarCoordinatesFromPosition, \ getEllipsoidPolarCoordinatesTangents from scaffoldmaker.utils.interpolation import computeCubicHermiteSideCrossDerivatives, evaluateCoordinatesOnCurve, \ getCubicHermiteCurvesLength, getNearestLocationBetweenCurves, getNearestLocationOnCurve, interpolateCubicHermite -from scaffoldmaker.utils.networkmesh import getPathRawTubeCoordinates, resampleTubeCoordinates from scaffoldmaker.utils.tracksurface import TrackSurface, TrackSurfacePosition +from scaffoldmaker.utils.tubenetworkmesh import ( + TubeNetworkMeshSegment, getPathRawTubeCoordinates, resampleTubeCoordinates) from scaffoldmaker.utils.zinc_utils import generateCurveMesh, get_nodeset_path_ordered_field_parameters from testutils import assertAlmostEqualList @@ -1593,18 +1593,17 @@ def test_2d_tube_intersections_bifurcation(self): Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D2_DS1DS2, Node.VALUE_LABEL_D_DS3, Node.VALUE_LABEL_D2_DS1DS3] - segmentTubeData = [] + tubeSegments = [] trackSurfaces = [] networkSegments = networkMesh.getNetworkSegments() elementsCountAround = 8 for networkSegment in networkSegments: pathParameters = get_nodeset_path_ordered_field_parameters( nodes, coordinates, valueLabels, networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions()) - tubeData = SegmentTubeData(pathParameters, elementsCountAround) - segmentTubeData.append(tubeData) - rawTubeData = getPathRawTubeCoordinates(pathParameters, elementsCountAround=elementsCountAround) - tubeData.setRawTubeCoordinates(rawTubeData) - trackSurfaces.append(tubeData.getRawTrackSurface()) + tubeSegment = TubeNetworkMeshSegment(networkSegment, [pathParameters], + elementsCountAround=elementsCountAround, elementsCountThroughWall=1) + tubeSegments.append(tubeSegment) + trackSurfaces.append(tubeSegment.getRawTrackSurface()) XI_TOL = 1.0E-6 X_TOL = 1.0E-6 @@ -1619,7 +1618,7 @@ def test_2d_tube_intersections_bifurcation(self): self.assertAlmostEqual(nearestPosition.xi1, 0.7937236157191407, delta=XI_TOL) self.assertAlmostEqual(nearestPosition.xi2, 0.5, delta=XI_TOL) - px, pd1, pd2, pd12 = segmentTubeData[0].getRawTubeCoordinates() + px, pd1, pd2, pd12 = tubeSegments[0].getRawTubeCoordinates() cx = [px[0][4], px[1][4]] cd1 = [pd2[0][4], pd2[1][4]] nearestPosition, nearestCurveLocation, isIntersection = \ @@ -1646,7 +1645,7 @@ def test_2d_tube_intersections_bifurcation(self): self.assertAlmostEqual(nearestPosition.xi2, 0.0, delta=XI_TOL) # non-intersecting curve and surface - px, _, pd2, _ = segmentTubeData[0].getRawTubeCoordinates() + px, _, pd2, _ = tubeSegments[0].getRawTubeCoordinates() cx = [px[0][0], px[1][0]] cd1 = [pd2[0][0], pd2[1][0]] nearestPosition, nearestCurveLocation, isIntersection = \ diff --git a/tests/test_network.py b/tests/test_network.py index 976e78de..075a5c43 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,6 +1,7 @@ import math import unittest +from cmlibs.maths.vectorops import magnitude from cmlibs.utils.zinc.finiteelement import evaluateFieldNodesetRange from cmlibs.utils.zinc.general import ChangeManager from cmlibs.utils.zinc.group import identifier_ranges_to_string, mesh_group_add_identifier_ranges, \ @@ -88,14 +89,12 @@ def test_2d_tube_network_bifurcation(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertFalse(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(6, len(settings)) + self.assertEqual(5, len(settings)) self.assertEqual(8, settings["Elements count around"]) self.assertEqual([0], settings["Annotation elements counts around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) - self.assertTrue(settings["Serendipity"]) - settings["Target element density along longest segment"] = 7.5 + settings["Target element density along longest segment"] = 3.4 MeshType_2d_tubenetwork1.checkOptions(settings) - self.assertEqual(7.5, settings["Target element density along longest segment"]) context = Context("Test") region = context.getDefaultRegion() @@ -105,9 +104,9 @@ def test_2d_tube_network_bifurcation(self): fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(8 * (2 * 8 + 7), mesh2d.getSize()) + self.assertEqual(88, mesh2d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(8 * 3 * 8 + 3, nodes.getSize()) + self.assertEqual(99, nodes.getSize()) coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() self.assertTrue(coordinates.isValid()) @@ -124,7 +123,7 @@ def test_2d_tube_network_bifurcation(self): fieldcache = fieldmodule.createFieldcache() result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 1.9292133611202664, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 1.930453257098265, delta=X_TOL) def test_2d_tube_network_sphere_cube(self): """ @@ -135,11 +134,10 @@ def test_2d_tube_network_sphere_cube(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertFalse(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(6, len(settings)) + self.assertEqual(5, len(settings)) self.assertEqual(8, settings["Elements count around"]) self.assertEqual([0], settings["Annotation elements counts around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) - self.assertTrue(settings["Serendipity"]) context = Context("Test") region = context.getDefaultRegion() @@ -185,8 +183,8 @@ def test_2d_tube_network_sphere_cube(self): X_TOL = 1.0E-6 minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - assertAlmostEqualList(self, minimums, [-0.5663822833834603, -0.5965021612010701, -0.598179822484941], X_TOL) - assertAlmostEqualList(self, maximums, [0.5663822151894911, 0.5965021612010702, 0.5981798465355403], X_TOL) + assertAlmostEqualList(self, minimums, [-0.5664610069377636, -0.5965021612010833, -0.5985868755975444], X_TOL) + assertAlmostEqualList(self, maximums, [0.5664609474985409, 0.5965021612010833, 0.5985868966530402], X_TOL) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -195,7 +193,118 @@ def test_2d_tube_network_sphere_cube(self): fieldcache = fieldmodule.createFieldcache() result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 4.057905325323945, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 4.045008760308933, delta=X_TOL) + + def test_2d_tube_network_trifurcation(self): + """ + Test 2D tube triifurcation is generated correctly. + """ + scaffoldPackage = ScaffoldPackage(MeshType_2d_tubenetwork1, defaultParameterSetName="Trifurcation") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + self.assertFalse(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(5, len(settings)) + self.assertEqual(8, settings["Elements count around"]) + self.assertEqual([0], settings["Annotation elements counts around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + MeshType_2d_tubenetwork1.checkOptions(settings) + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + + fieldmodule = region.getFieldmodule() + self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) + mesh2d = fieldmodule.findMeshByDimension(2) + self.assertEqual(112, mesh2d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(126, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [0.0, -1.0707106781186548, -0.10000000000000002], X_TOL) + assertAlmostEqualList(self, maximums, [2.0707106781186546, 1.0707106781186548, 0.1], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + surfaceAreaField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh2d) + surfaceAreaField.setNumbersOfPoints(4) + fieldcache = fieldmodule.createFieldcache() + result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + self.assertAlmostEqual(surfaceArea, 2.792300995131311, delta=X_TOL) + + def test_3d_tube_network_bifurcation(self): + """ + Test bifurcation 3-D tube network is generated correctly. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Bifurcation") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(7, len(settings)) + self.assertEqual(8, settings["Elements count around"]) + self.assertEqual(1, settings["Elements count through wall"]) + self.assertEqual([0], settings["Annotation elements counts around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertFalse(settings["Use linear through wall"]) + self.assertFalse(settings["Show trim surfaces"]) + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + + fieldmodule = region.getFieldmodule() + mesh3d = fieldmodule.findMeshByDimension(3) + + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual(8 * 4 * 3, mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual((8 * 4 * 3 + 3 * 3 + 2) * 2, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [0.0, -0.5894427190999916, -0.10000000000000002], X_TOL) + assertAlmostEqualList(self, maximums, [2.044721359549996, 0.5894427190999916, 0.10000000000000002], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + isExteriorXi3_0 = fieldmodule.createFieldAnd( + isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_0)) + isExteriorXi3_1 = fieldmodule.createFieldAnd( + isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + outerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + outerSurfaceAreaField.setNumbersOfPoints(4) + result, outerSurfaceArea = outerSurfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + innerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_0, coordinates, mesh2d) + innerSurfaceAreaField.setNumbersOfPoints(4) + result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.07381984049942056, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 1.928821019338746, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 0.995733838660512, delta=X_TOL) def test_3d_tube_network_sphere_cube(self): """ @@ -211,8 +320,10 @@ def test_3d_tube_network_sphere_cube(self): self.assertEqual(1, settings["Elements count through wall"]) self.assertEqual([0], settings["Annotation elements counts around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) - self.assertTrue(settings["Serendipity"]) + self.assertFalse(settings["Use linear through wall"]) + self.assertFalse(settings["Show trim surfaces"]) settings["Elements count through wall"] = 2 + settings["Use linear through wall"] = True context = Context("Test") region = context.getDefaultRegion() @@ -244,19 +355,113 @@ def test_3d_tube_network_sphere_cube(self): scaffoldPackage.generate(region) fieldmodule = region.getFieldmodule() - self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) self.assertEqual(32 * 12 * 2, mesh3d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) self.assertEqual(8 * 3 * 12 * 3 + (2 + 3 * 3) * 8 * 3, nodes.getSize()) + mesh2d = fieldmodule.findMeshByDimension(2) + self.assertEqual(32 * 12 * 5 + 24 * 12 * 2 + 12 * 8 * 2, mesh2d.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [-0.5664610069377635, -0.5965021612010833, -0.5985868755975445], X_TOL) + assertAlmostEqualList(self, maximums, [0.5664609474985409, 0.5965021612010833, 0.5985868966530402], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + isExteriorXi3_0 = fieldmodule.createFieldAnd( + isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_0)) + isExteriorXi3_1 = fieldmodule.createFieldAnd( + isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + outerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + outerSurfaceAreaField.setNumbersOfPoints(4) + result, outerSurfaceArea = outerSurfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + innerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_0, coordinates, mesh2d) + innerSurfaceAreaField.setNumbersOfPoints(4) + result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.07425485994940124, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 4.045008760308934, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 3.3328595903228115, delta=X_TOL) + + def test_3d_tube_network_trifurcation_cross(self): + """ + Test trifurcation cross 3-D tube network is generated correctly with variable elements count around. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Trifurcation cross") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(7, len(settings)) + self.assertEqual(8, settings["Elements count around"]) + self.assertEqual(1, settings["Elements count through wall"]) + self.assertEqual([0], settings["Annotation elements counts around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertFalse(settings["Use linear through wall"]) + self.assertFalse(settings["Show trim surfaces"]) + settings["Annotation elements counts around"] = [10] # requires annotation group below + + context = Context("Test") + region = context.getDefaultRegion() + + # add a user-defined annotation group to network layout to vary elements count around. Must generate first + tmpRegion = region.createRegion() + tmpFieldmodule = tmpRegion.getFieldmodule() + networkLayoutScaffoldPackage.generate(tmpRegion) + + annotationGroup1 = networkLayoutScaffoldPackage.createUserAnnotationGroup(("straight", "STRAIGHT:1")) + group = annotationGroup1.getGroup() + mesh1d = tmpFieldmodule.findMeshByDimension(1) + meshGroup = group.createMeshGroup(mesh1d) + mesh_group_add_identifier_ranges(meshGroup, [[1, 1], [4, 4]]) + self.assertEqual(2, meshGroup.getSize()) + self.assertEqual(1, annotationGroup1.getDimension()) + identifier_ranges_string = identifier_ranges_to_string(mesh_group_to_identifier_ranges(meshGroup)) + self.assertEqual("1,4", identifier_ranges_string) + networkLayoutScaffoldPackage.updateUserAnnotationGroups() + + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + annotationGroups = scaffoldPackage.getAnnotationGroups() + self.assertEqual(1, len(annotationGroups)) + + fieldmodule = region.getFieldmodule() + mesh3d = fieldmodule.findMeshByDimension(3) + + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual(144, mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(320, nodes.getSize()) coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() self.assertTrue(coordinates.isValid()) + # check annotation group transferred to 3D tube + annotationGroup = annotationGroups[0] + self.assertEqual("straight", annotationGroup.getName()) + self.assertEqual("STRAIGHT:1", annotationGroup.getId()) + self.assertEqual(80, annotationGroup.getMeshGroup(fieldmodule.findMeshByDimension(3)).getSize()) + X_TOL = 1.0E-6 minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - assertAlmostEqualList(self, minimums, [-0.5663822833834603, -0.5965021612010702, -0.598179822484941], X_TOL) - assertAlmostEqualList(self, maximums, [0.5663822151894911, 0.5965021612010702, 0.5981798465355403], X_TOL) + assertAlmostEqualList(self, minimums, [-0.0447213595499958, -0.5894427190999916, -0.1], X_TOL) + assertAlmostEqualList(self, maximums, [2.044721359549996, 0.5894427190999916, 0.10000000000000002], X_TOL) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -272,19 +477,20 @@ def test_3d_tube_network_sphere_cube(self): volumeField.setNumbersOfPoints(4) result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.074451650669961, delta=X_TOL) outerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) outerSurfaceAreaField.setNumbersOfPoints(4) result, outerSurfaceArea = outerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(outerSurfaceArea, 4.057905325323947, delta=X_TOL) innerSurfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_0, coordinates, mesh2d) innerSurfaceAreaField.setNumbersOfPoints(4) result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(innerSurfaceArea, 3.347440907189292, delta=X_TOL) + + self.assertAlmostEqual(volume, 0.10038746104304462, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.59759659324524, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 1.3635516941376224, delta=X_TOL) def test_3d_box_network_bifurcation(self): """ @@ -340,6 +546,38 @@ def test_3d_box_network_bifurcation(self): expectedSurfaceArea = 6 * 0.2 * 0.2 + 4 * 0.2 * (1.0 + 2 * L2) self.assertAlmostEqual(surfaceArea, expectedSurfaceArea, delta=X_TOL) + def test_3d_box_network_smooth(self): + """ + Test 3-D box network derivative smoothing is working between segments sharing a version. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_boxnetwork1) + settings = scaffoldPackage.getScaffoldSettings() + settings["Target element density along longest segment"] = 1.0 + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + networkLayoutSettings["Structure"] = "1-2-3,3-4" # 2 unequal-sized segments + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + + fieldmodule = region.getFieldmodule() + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(3, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + node2 = nodes.findNodeByIdentifier(2) + self.assertTrue(node2.isValid()) + + # test magnitude of d1 between segments is harmonic mean of element sizes + fieldcache = fieldmodule.createFieldcache() + fieldcache.setNode(node2) + result, d1 = coordinates.getNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, 3) + self.assertEqual(result, RESULT_OK) + d1Mag = magnitude(d1) + self.assertAlmostEqual(4.0 / 3.0, d1Mag, delta=1.0E-12) + def test_3d_tube_network_loop(self): """ Test loop 3-D tube network is generated correctly. diff --git a/tests/test_uterus.py b/tests/test_uterus.py index c619e957..45d6ad37 100644 --- a/tests/test_uterus.py +++ b/tests/test_uterus.py @@ -16,7 +16,7 @@ from testutils import assertAlmostEqualList -class StomachScaffoldTestCase(unittest.TestCase): +class UterusScaffoldTestCase(unittest.TestCase): def test_uterus1(self): """ @@ -32,37 +32,41 @@ def test_uterus1(self): self.assertEqual("1-2-3, 4-5-6, 3-7-8-11.1, 6-9-10-11.2, 11.3-12-13-14,14-15-16,16-17-18", networkLayoutSettings["Structure"]) - self.assertEqual(14, len(options)) - self.assertEqual(10, options.get("Number of elements around")) - self.assertEqual(8, options.get("Number of elements around horns")) + self.assertEqual(12, len(options)) + self.assertEqual(12, options.get("Number of elements around")) + self.assertEqual(12, options.get("Number of elements around horns")) self.assertEqual(1, options.get("Number of elements through wall")) self.assertEqual(5.0, options.get("Target element density along longest segment")) - self.assertEqual(True, options.get("Serendipity")) + self.assertEqual(True, options.get("Use linear through wall")) + # test with fewer elements + options["Number of elements around"] = 8 + options["Number of elements around horns"] = 8 + options["Target element density along longest segment"] = 3.0 context = Context("Test") region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateBaseMesh(region, options)[0] - self.assertEqual(17, len(annotationGroups)) + self.assertEqual(14, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(248, mesh3d.getSize()) + self.assertEqual(136, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(1005, mesh2d.getSize()) + self.assertEqual(556, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(1278, mesh1d.getSize()) + self.assertEqual(715, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(522, nodes.getSize()) + self.assertEqual(296, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - assertAlmostEqualList(self, minimums, [-9.360152113397383, -0.05, -8.943209696521798], 1.0E-6) - assertAlmostEqualList(self, maximums, [9.360152113397215, 12.920227960030479, 1.278732071803069], 1.0E-6) + assertAlmostEqualList(self, minimums, [-9.361977045958657, -0.048, -8.90345243427233], 1.0E-6) + assertAlmostEqualList(self, maximums, [9.36197704595863, 12.809844943106265, 1.09], 1.0E-6) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -74,43 +78,53 @@ def test_uterus1(self): fieldcache = fieldmodule.createFieldcache() result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 264.7433300356289, delta=1.0E-6) result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 188.23971703799126, delta=1.0E-6) + self.assertAlmostEqual(surfaceArea, 261.42077575225807, delta=1.0E-6) + self.assertAlmostEqual(volume, 182.65005670492496, delta=1.0E-6) - # check some annotationGroups: + fieldmodule.defineAllFaces() + for annotationGroup in annotationGroups: + annotationGroup.addSubelements() + scaffold.defineFaceAnnotations(region, options, annotationGroups) + self.assertEqual(34, len(annotationGroups)) + + # check some annotation groups expectedSizes3d = { - "body of uterus": 130, - "left uterine tube": 24, - "uterine cervix": 20, - "vagina": 50, - "uterus": 248 + "body of uterus": 72, + "left uterine tube": 16, + "uterine cervix": 8, + "vagina": 24, + 'left broad ligament of uterus': 8, + 'right broad ligament of uterus': 8, + "uterus": 136 } + meshes = [mesh1d, mesh2d, mesh3d] for name in expectedSizes3d: term = get_uterus_term(name) - group = getAnnotationGroupForTerm(annotationGroups, term) - size = group.getMeshGroup(mesh3d).getSize() + annotationGroup = getAnnotationGroupForTerm(annotationGroups, term) + size = annotationGroup.getMeshGroup(meshes[annotationGroup.getDimension() - 1]).getSize() self.assertEqual(expectedSizes3d[name], size, name) - # refine 4x4x4 and check result - # first remove any surface annotation groups as they are re-added by defineFaceAnnotations + # refine 2x2x2 and check result + # first remove faces/lines and any surface annotation groups as they are re-added by defineFaceAnnotations removeAnnotationGroups = [] for annotationGroup in annotationGroups: - if (not annotationGroup.hasMeshGroup(mesh3d)) and \ - (annotationGroup.hasMeshGroup(mesh2d) or annotationGroup.hasMeshGroup(mesh1d)): + if annotationGroup.getDimension() in [1, 2]: removeAnnotationGroups.append(annotationGroup) - for annotationGroup in removeAnnotationGroups: annotationGroups.remove(annotationGroup) - self.assertEqual(17, len(annotationGroups)) + self.assertEqual(14, len(annotationGroups)) + # also remove all faces and lines as not needed for refinement + mesh2d.destroyAllElements() + mesh1d.destroyAllElements() refineRegion = region.createRegion() refineFieldmodule = refineRegion.getFieldmodule() - options['Refine number of elements along'] = 4 - options['Refine number of elements around'] = 4 - options['Refine number of elements through wall'] = 4 + options['Refine number of elements along'] = 2 + options['Refine number of elements around'] = 2 + options['Refine number of elements through wall'] = 2 meshrefinement = MeshRefinement(region, refineRegion, annotationGroups) scaffold.refineMesh(meshrefinement, options) annotationGroups = meshrefinement.getAnnotationGroups() @@ -123,25 +137,27 @@ def test_uterus1(self): for annotation in annotationGroups: if annotation not in oldAnnotationGroups: annotationGroup.addSubelements() - self.assertEqual(29, len(annotationGroups)) + self.assertEqual(34, len(annotationGroups)) # mesh3d = refineFieldmodule.findMeshByDimension(3) - self.assertEqual(15872, mesh3d.getSize()) + self.assertEqual(1088, mesh3d.getSize()) mesh2d = refineFieldmodule.findMeshByDimension(2) - self.assertEqual(51792, mesh2d.getSize()) + self.assertEqual(3856, mesh2d.getSize()) mesh1d = refineFieldmodule.findMeshByDimension(1) - self.assertEqual(56016, mesh1d.getSize()) + self.assertEqual(4470, mesh1d.getSize()) nodes = refineFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(20097, nodes.getSize()) + self.assertEqual(1703, nodes.getSize()) datapoints = refineFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) # check some refined annotationGroups: + meshes = [mesh1d, mesh2d, mesh3d] + sizeScales = [2, 4, 8] for name in expectedSizes3d: term = get_uterus_term(name) - group = getAnnotationGroupForTerm(annotationGroups, term) - size = group.getMeshGroup(mesh3d).getSize() - self.assertEqual(expectedSizes3d[name]*64, size, name) + annotationGroup = getAnnotationGroupForTerm(annotationGroups, term) + size = annotationGroup.getMeshGroup(meshes[annotationGroup.getDimension() - 1]).getSize() + self.assertEqual(expectedSizes3d[name] * sizeScales[annotationGroup.getDimension() - 1], size, name) # test finding a marker in refined scaffold markerGroup = refineFieldmodule.findFieldByName("marker").castGroup() @@ -157,8 +173,7 @@ def test_uterus1(self): self.assertTrue(node.isValid()) cache.setNode(node) element, xi = markerLocation.evaluateMeshLocation(cache, 3) - self.assertEqual(3712, element.getIdentifier()) - assertAlmostEqualList(self, xi, [1.0, 1.0, 1.0], 1.0E-04) + self.assertTrue(element.isValid()) if __name__ == "__main__":