From bd8d63d826f6d4f66af864c1073aa7dee7be555d Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Fri, 13 Sep 2024 10:59:03 +1200 Subject: [PATCH 01/20] Add human body network layout generator --- src/scaffoldmaker/annotation/body_terms.py | 37 +- .../meshtypes/meshtype_1d_network_layout1.py | 6 +- .../meshtypes/meshtype_3d_wholebody2.py | 610 +++++++++++------- src/scaffoldmaker/scaffolds.py | 10 +- 4 files changed, 418 insertions(+), 245 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index cda4a8a8..a6d22009 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -4,21 +4,28 @@ # convention: preferred name, preferred id, followed by any other ids and alternative names body_terms = [ - ( "abdomen", "UBERON:0000916", "ILX:0725977" ), - ( "thorax", "ILX:0742178" ), - ( "neck", "UBERON:0000974", "ILX:0733967" ), - ( "head", "UBERON:0000033", "ILX:0104909" ), - ( "neck core", "" ), - ( "head core", "" ), - ( "skin epidermis", "UBERON:0001003", "ILX:0728574" ), - ( "diaphragm", "UBERON:0001103", "ILX:0103194" ), - ( "spinal cord", "UBERON:0002240", "ILX:0110909" ), - ( "body", "UBERON:0000468", "ILX:0101370" ), - ( "core", "" ), - ( "non core", "" ), - ( "core boundary", "" ), - ( "arm", "UBERON:0001460"), - ( "leg", "UBERON:0000978") + ("abdomen", "UBERON:0000916", "ILX:0725977"), + ("abdominal cavity", "UBERON:0003684"), + ("arm", "UBERON:0001460"), + ("left arm", ""), + ("right arm", ""), + ("body", "UBERON:0000468", "ILX:0101370"), + ("core", ""), + ("core boundary", ""), + ("head", "UBERON:0000033", "ILX:0104909"), + ("head core", ""), + ("diaphragm", "UBERON:0001103", "ILX:0103194"), + ("leg", "UBERON:0000978"), + ("left leg", ""), + ("right leg", ""), + ("neck", "UBERON:0000974", "ILX:0733967"), + ("neck core", ""), + ("non core", ""), + ("shell", ""), + ("skin epidermis", "UBERON:0001003", "ILX:0728574"), + ("spinal cord", "UBERON:0002240", "ILX:0110909"), + ("thoracic cavity", "UBERON:0002224"), + ("thorax", "ILX:0742178"), ] def get_body_term(name : str): diff --git a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py index c1f4c5f7..afb755f1 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py @@ -158,12 +158,12 @@ def generateBaseMesh(cls, region, options): coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, v + 1, cd13[n][v]) if defineInnerCoordinates: - cls._defineInnerCoordinates(region, coordinates, options, networkMesh) + cls.defineInnerCoordinates(region, coordinates, options, networkMesh) return [], networkMesh @classmethod - def _defineInnerCoordinates(cls, region, coordinates, options, networkMesh): + def defineInnerCoordinates(cls, region, coordinates, options, networkMesh): """ Copy coordinates to inner coordinates via in-memory model file. Assign using the interactive function. @@ -213,7 +213,7 @@ def editStructure(cls, region, options, networkMesh, functionOptions, editGroupN coordinates.setManaged(True) # since cleared by clearRegion defineInnerCoordinates = options["Define inner coordinates"] if defineInnerCoordinates: - cls._defineInnerCoordinates(region, coordinates, options, networkMesh) + cls.defineInnerCoordinates(region, coordinates, options, networkMesh) return True, False # settings changed, nodes not changed (since reset to original coordinates) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 1e165d44..83d2ff47 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -1,200 +1,368 @@ """ Generates a 3D body coordinates using tube network mesh. """ - +from cmlibs.maths.vectorops import add, mult +from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates +from cmlibs.utils.zinc.general import ChangeManager from cmlibs.zinc.element import Element +from cmlibs.zinc.node import Node 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.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm, \ - getAnnotationGroupForTerm +from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm from scaffoldmaker.annotation.body_terms import get_body_term +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 -from cmlibs.zinc.node import Node +import math + +class MeshType_1d_human_body_network_layout1(MeshType_1d_network_layout1): + """ + Defines body network layout. + """ + + @classmethod + def getName(cls): + return "1D Human Body Network Layout 1" + + @classmethod + def getParameterSetNames(cls): + return ["Default"] + + @classmethod + def getDefaultOptions(cls, parameterSetName="Default"): + options = {} + options["Base parameter set"] = parameterSetName + options["Structure"] = ( + "1-2-3-4," + "4-5-6.1," + "6.2-14-15-16-17-18-19," + "6.3-20-21-22-23-24-25," + "6.1-7-8-9," + "9-10-11-12-13.1," + "13.2-26-27-28,28-29-30-31-32," + "13.3-33-34-35,35-36-37-38-39") + options["Define inner coordinates"] = True + options["Head depth"] = 2.0 + options["Head length"] = 2.25 + options["Head width"] = 2.0 + options["Neck length"] = 1.5 + options["Shoulder drop"] = 0.5 + options["Shoulder width"] = 5.0 + options["Arm lateral angle degrees"] = 15.0 + options["Arm length"] = 6.0 + options["Arm width"] = 0.8 + options["Hand length"] = 2.0 + options["Hand thickness"] = 0.5 + options["Hand width"] = 1.2 + options["Torso depth"] = 2.5 + options["Torso length"] = 6.0 + options["Torso width"] = 3.5 + options["Pelvis drop"] = 0.5 + options["Pelvis width"] = 2.5 + options["Leg lateral angle degrees"] = 15.0 + options["Leg length"] = 8.0 + return options + + @classmethod + def getOrderedOptionNames(cls): + return [ + "Head depth", + "Head length", + "Head width", + "Neck length", + "Shoulder drop", + "Shoulder width", + "Arm lateral angle degrees", + "Arm length", + "Arm width", + "Hand length", + "Hand thickness", + "Hand width", + "Torso depth", + "Torso length", + "Torso width", + "Pelvis drop", + "Pelvis width", + "Leg lateral angle degrees", + "Leg length" + ] + + @classmethod + def checkOptions(cls, options): + dependentChanges = False + for key in [ + "Head depth", + "Head length", + "Head width", + "Neck length", + # "Shoulder drop", + "Shoulder width", + "Arm length", + "Arm width", + "Hand length", + "Hand thickness", + "Hand width", + # "Pelvis drop", + "Pelvis width", + "Torso depth", + "Torso width", + "Leg length" + ]: + if options[key] < 0.1: + options[key] = 0.1 + for key in [ + "Arm lateral angle degrees" + ]: + if options[key] < -60.0: + options[key] = -60.0 + elif options[key] > 200.0: + options[key] = 200.0 + for key in [ + "Leg lateral angle degrees" + ]: + if options[key] < -20.0: + options[key] = -20.0 + elif options[key] > 60.0: + options[key] = 60.0 + return dependentChanges -def getDefaultNetworkLayoutScaffoldPackage(cls, parameterSetName): - assert parameterSetName in cls.getParameterSetNames() - if parameterSetName in ("Default", "Human 1"): - return ScaffoldPackage(MeshType_1d_network_layout1, { - "scaffoldSettings": { - "Structure": "1-2-3, 3-4,4.2-5,4.3-6,4.1-7,7-8,8-9.1,9.2-10,9.3-11, \ - 5-12-13-14,6-15-16-17,10-18-19-20, 11-21-22-23", - "Define inner coordinates": True - }, - "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, [[0.000, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.400, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.400], [0.000, 0.000, -0.000]]), - (2, [[0.500, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.400, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.400], [0.000, 0.000, -0.000]]), - (3, [[1.000, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.400, 0.000], [0.000, 0.100, 0.000], [0.000, 0.000, 0.400], [0.000, 0.000, 0.100]]), - (4, [[1.500, 0.000, 0.000], [[0.643, 0.000, 0.000], [0.028, -1.246, 0.000], [0.028, 1.246, 0.000]], [[0.000, 0.600, 0.000], [0.396, 0.009, 0.000], [-0.395, 0.009, 0.000]], [[0.000, 0.100, 0.000], [-0.374, 0.001, 0.000], [0.376, 0.005, 0.000]], [[0.000, 0.000, 0.600], [-0.000, 0.000, 0.400], [0.000, -0.000, 0.400]], [[0.000, 0.000, 0.100], [0.000, 0.000, -0.270], [0.000, 0.000, -0.268]]]), - (5, [[1.788, -1.014, 0.000], [0.546, -0.709, 0.000], [0.121, 0.093, 0.000], [-0.189, 0.048, 0.000], [-0.000, 0.000, 0.200], [0.000, 0.000, -0.130]]), - (6, [[1.788, 1.014, 0.000], [0.547, 0.709, 0.000], [-0.120, 0.093, 0.000], [0.186, 0.045, 0.000], [0.000, -0.000, 0.201], [0.000, 0.000, -0.130]]), - (7, [[2.400, -0.000, 0.000], [0.788, 0.000, 0.000], [0.000, 0.600, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.600], [0.000, 0.000, -0.000]]), - (8, [[3.100, -0.000, 0.000], [0.747, 0.000, 0.000], [0.000, 0.600, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.600], [0.000, 0.000, -0.000]]), - (9, [[3.900, -0.000, 0.000], [[0.853, 0.000, 0.000], [0.731, -0.720, 0.000], [0.731, 0.720, 0.000]], [[0.000, 0.600, 0.000], [0.496, 0.504, 0.000], [-0.470, 0.478, 0.000]], [[-0.000, 0.000, -0.000], [-0.839, -0.173, -0.006], [0.775, -0.153, 0.006]], [[0.000, 0.000, 0.600], [-0.000, 0.000, 0.350], [0.000, -0.000, 0.350]], [[-0.000, -0.000, 0.000], [0.006, 0.004, -0.022], [0.006, -0.004, -0.022]]]), - (10, [[5.250, -0.600, 0.000], [1.915, -0.427, 0.000], [0.056, 0.249, 0.000], [-0.211, -0.182, 0.006], [-0.000, 0.000, 0.300], [-0.003, -0.005, -0.078]]), - (11, [[5.250, 0.600, 0.000], [1.915, 0.427, 0.000], [-0.056, 0.249, 0.000], [0.206, -0.165, -0.006], [0.000, -0.000, 0.300], [-0.003, 0.005, -0.078]]), - (12, [[2.439, -1.399, 0.000], [0.920, -0.382, 0.000], [0.057, 0.137, 0.000], [-0.009, -0.011, 0.001], [-0.000, 0.000, 0.140], [0.006, -0.002, -0.030]]), - (13, [[3.807, -1.755, 0.000], [1.181, -0.272, 0.078], [0.028, 0.121, -0.000], [-0.018, -0.063, 0.000], [-0.009, 0.002, 0.140], [-0.023, 0.005, -0.056]]), - (14, [[4.841, -1.961, 0.137], [0.882, -0.141, 0.194], [0.002, 0.015, -0.000], [-0.023, -0.149, 0.002], [-0.006, 0.001, 0.029], [0.054, -0.011, -0.158]]), - (15, [[2.439, 1.399, 0.000], [0.946, 0.385, 0.000], [-0.056, 0.138, 0.000], [0.012, -0.011, -0.001], [0.000, -0.000, 0.140], [0.006, 0.002, -0.031]]), - (16, [[3.911, 1.761, 0.000], [1.211, 0.255, 0.091], [-0.025, 0.121, 0.000], [0.018, -0.056, -0.000], [-0.010, -0.002, 0.140], [-0.028, -0.005, -0.042]]), - (17, [[4.932, 1.939, 0.153], [0.825, 0.101, 0.214], [-0.004, 0.029, 0.000], [0.014, -0.129, -0.002], [-0.015, -0.002, 0.058], [0.044, 0.008, -0.114]]), - (18, [[8.100, -0.665, 0.000], [0.775, -0.027, 0.275], [0.007, 0.250, 0.003], [0.032, -0.000, 0.006], [-0.069, -0.000, 0.194], [-0.224, -0.006, -0.056]]), - (19, [[8.459, -0.683, 0.298], [0.258, -0.010, 0.370], [0.004, 0.250, 0.004], [-0.008, 0.000, -0.013], [-0.181, 0.001, 0.126], [0.051, -0.005, -0.001]]), - (20, [[8.601, -0.685, 0.694], [0.024, 0.006, 0.399], [-0.001, 0.250, -0.004], [-0.002, -0.000, 0.005], [-0.141, -0.000, 0.009], [0.109, 0.005, -0.120]]), - (21, [[8.100, 0.665, 0.000], [0.775, 0.027, 0.275], [-0.007, 0.250, -0.003], [-0.032, -0.000, -0.009], [-0.069, 0.000, 0.194], [-0.224, 0.008, -0.055]]), - (22, [[8.459, 0.683, 0.298], [0.258, 0.010, 0.370], [0.000, 0.250, -0.007], [0.008, 0.000, 0.013], [-0.182, 0.004, 0.127], [0.049, 0.005, 0.002]]), - (23, [[8.601, 0.685, 0.694], [0.024, -0.006, 0.399], [0.001, 0.250, 0.004], [-0.007, 0.000, -0.005], [-0.148, 0.000, 0.009], [0.101, -0.015, -0.124]])], [ - (1, [[0.000, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.280, 0.000], [0.000, 0.000, 0.000], [0.000, 0.000, 0.280], [0.000, 0.000, 0.000]]), - (2, [[0.500, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.280, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.280], [0.000, 0.000, -0.000]]), - (3, [[1.000, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.280, 0.000], [0.000, 0.070, 0.000], [0.000, 0.000, 0.280], [0.000, 0.000, 0.070]]), - (4, [[1.500, 0.000, 0.000], [[0.643, 0.000, 0.000], [0.028, -1.246, 0.000], [0.028, 1.246, 0.000]], [[0.000, 0.420, 0.000], [0.277, 0.006, 0.000], [-0.277, 0.006, 0.000]], [[0.000, 0.070, 0.000], [-0.262, 0.001, 0.000], [0.263, 0.003, 0.000]], [[0.000, 0.000, 0.420], [-0.000, 0.000, 0.280], [0.000, -0.000, 0.280]], [[0.000, 0.000, 0.070], [0.000, 0.000, -0.189], [0.000, 0.000, -0.188]]]), - (5, [[1.788, -1.014, 0.000], [0.546, -0.709, 0.000], [0.085, 0.065, 0.000], [-0.133, 0.033, 0.000], [-0.000, 0.000, 0.140], [0.000, 0.000, -0.091]]), - (6, [[1.788, 1.014, 0.000], [0.547, 0.709, 0.000], [-0.084, 0.065, 0.000], [0.130, 0.032, 0.000], [0.000, -0.000, 0.141], [0.000, 0.000, -0.091]]), - (7, [[2.400, -0.000, 0.000], [0.788, 0.000, 0.000], [0.000, 0.420, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.420], [0.000, 0.000, 0.000]]), - (8, [[3.100, -0.000, 0.000], [0.747, 0.000, 0.000], [0.000, 0.420, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.420], [0.000, 0.000, -0.000]]), - (9, [[3.900, -0.000, 0.000], [[0.853, 0.000, 0.000], [0.731, -0.720, 0.000], [0.731, 0.720, 0.000]], [[0.000, 0.420, 0.000], [0.347, 0.353, 0.000], [-0.329, 0.334, 0.000]], [[-0.000, 0.000, -0.000], [-0.587, -0.121, -0.004], [0.543, -0.107, 0.004]], [[0.000, 0.000, 0.420], [-0.000, 0.000, 0.245], [0.000, -0.000, 0.245]], [[-0.000, -0.000, 0.000], [0.004, 0.003, -0.015], [0.004, -0.003, -0.015]]]), - (10, [[5.250, -0.600, 0.000], [1.915, -0.427, 0.000], [0.039, 0.174, 0.000], [-0.148, -0.127, 0.004], [-0.000, 0.000, 0.210], [-0.002, -0.004, -0.055]]), - (11, [[5.250, 0.600, 0.000], [1.915, 0.427, 0.000], [-0.039, 0.174, 0.000], [0.144, -0.115, -0.004], [0.000, -0.000, 0.210], [-0.002, 0.004, -0.055]]), - (12, [[2.439, -1.399, 0.000], [0.920, -0.382, 0.000], [0.040, 0.096, 0.000], [-0.006, -0.008, 0.001], [-0.000, 0.000, 0.098], [0.004, -0.002, -0.021]]), - (13, [[3.807, -1.755, 0.000], [1.181, -0.272, 0.078], [0.019, 0.084, -0.000], [-0.013, -0.044, 0.000], [-0.006, 0.001, 0.098], [-0.016, 0.003, -0.039]]), - (14, [[4.841, -1.961, 0.137], [0.882, -0.141, 0.194], [0.002, 0.011, -0.000], [-0.016, -0.104, 0.001], [-0.004, 0.001, 0.021], [0.038, -0.008, -0.110]]), - (15, [[2.439, 1.399, 0.000], [0.946, 0.385, 0.000], [-0.039, 0.096, 0.000], [0.008, -0.007, -0.001], [0.000, -0.000, 0.098], [0.004, 0.002, -0.022]]), - (16, [[3.911, 1.761, 0.000], [1.211, 0.255, 0.091], [-0.018, 0.085, 0.000], [0.013, -0.039, -0.000], [-0.007, -0.002, 0.098], [-0.019, -0.004, -0.029]]), - (17, [[4.932, 1.939, 0.153], [0.825, 0.101, 0.214], [-0.003, 0.020, 0.000], [0.010, -0.090, -0.002], [-0.010, -0.002, 0.041], [0.031, 0.006, -0.080]]), - (18, [[8.100, -0.665, 0.000], [0.775, -0.027, 0.275], [0.005, 0.175, 0.002], [0.022, -0.000, 0.004], [-0.048, -0.000, 0.136], [-0.157, -0.004, -0.039]]), - (19, [[8.459, -0.683, 0.298], [0.258, -0.010, 0.370], [0.003, 0.175, 0.003], [-0.005, 0.000, -0.009], [-0.127, 0.001, 0.088], [0.036, -0.004, -0.000]]), - (20, [[8.601, -0.685, 0.694], [0.024, 0.006, 0.399], [-0.001, 0.175, -0.002], [-0.001, -0.000, 0.003], [-0.099, -0.000, 0.006], [0.077, 0.004, -0.084]]), - (21, [[8.100, 0.665, 0.000], [0.775, 0.027, 0.275], [-0.005, 0.175, -0.002], [-0.022, -0.000, -0.006], [-0.048, 0.000, 0.136], [-0.157, 0.006, -0.039]]), - (22, [[8.459, 0.683, 0.298], [0.258, 0.010, 0.370], [0.000, 0.175, -0.005], [0.005, 0.000, 0.009], [-0.127, 0.002, 0.089], [0.034, 0.004, 0.001]]), - (23, [[8.601, 0.685, 0.694], [0.024, -0.006, 0.399], [0.001, 0.175, 0.002], [-0.005, 0.000, -0.003], [-0.104, 0.000, 0.006], [0.071, -0.010, -0.087]]) - ]]), - "userAnnotationGroups": [ - { - '_AnnotationGroup': True, - 'dimension': 1, - 'identifierRanges': '1-3', - 'name': get_body_term('head')[0], - 'ontId': get_body_term('head')[1] - }, - { - '_AnnotationGroup': True, - 'dimension': 1, - 'identifierRanges': '6-8', - 'name': 'torso', - 'ontId': 'None' - }, - { - '_AnnotationGroup': True, - 'dimension': 1, - 'identifierRanges': '4,11-13, 5,14-16', - 'name': get_body_term('arm')[0], - 'ontId': get_body_term('arm')[1] - }, - { - '_AnnotationGroup': True, - 'dimension': 1, - 'identifierRanges': '9,17-18-19, 10,20-21-22', - 'name': get_body_term('leg')[0], - 'ontId': get_body_term('leg')[1] - } - ] - }) - elif parameterSetName in ("Human 2 Coarse", "Human 2 Medium", "Human 2 Fine"): - return ScaffoldPackage(MeshType_1d_network_layout1, { - "scaffoldSettings": { - "Structure": "1-2-3, 3-4,4.2-5,4.3-6,4.1-7,7-8,8-9.1,9.2-10,9.3-11, \ - 5-12-13-14,6-15-16-17,10-18-19-20, 11-21-22-23", - "Define inner coordinates": True - }, - "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, [[0.000, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.400, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.400], [0.000, 0.000, -0.000]]), - (2, [[0.500, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.400, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.400], [0.000, 0.000, -0.000]]), - (3, [[1.000, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.400, 0.000], [0.000, 0.100, 0.000], [0.000, 0.000, 0.400], [0.000, 0.000, 0.100]]), - (4, [[1.500, 0.000, 0.000], [[0.643, 0.000, 0.000], [0.012, -1.338, 0.000], [0.054, 1.358, 0.000]], [[0.000, 0.600, 0.000], [0.277, 0.003, 0.000], [-0.277, 0.011, 0.000]], [[0.000, 0.100, 0.000], [-0.083, -0.206, 0.000], [0.082, -0.198, 0.000]], [[0.000, 0.000, 0.600], [-0.000, 0.000, 0.280], [0.000, -0.000, 0.280]], [[0.000, 0.000, 0.100], [0.000, 0.000, -0.195], [0.000, 0.000, -0.195]]]), - (5, [[1.806, -1.023, 0.000], [0.599, -0.599, 0.000], [0.158, 0.158, 0.000], [-0.253, 0.182, 0.000], [-0.000, 0.000, 0.150], [0.000, 0.000, -0.065]]), - (6, [[1.825, 1.023, 0.000], [0.592, 0.587, 0.000], [-0.156, 0.158, 0.000], [0.242, 0.173, 0.000], [0.000, -0.000, 0.150], [0.000, 0.000, -0.065]]), - (7, [[2.400, -0.000, 0.000], [0.788, 0.000, 0.000], [0.000, 0.600, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.600], [0.000, 0.000, -0.000]]), - (8, [[3.100, -0.000, 0.000], [0.747, 0.000, 0.000], [0.000, 0.600, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.600], [0.000, 0.000, -0.000]]), - (9, [[3.900, -0.000, 0.000], [[0.853, 0.000, 0.000], [0.731, -0.720, 0.000], [0.731, 0.720, 0.000]], [[0.000, 0.600, 0.000], [0.496, 0.504, 0.000], [-0.470, 0.478, 0.000]], [[-0.000, 0.000, -0.000], [-0.839, -0.173, -0.006], [0.776, -0.153, 0.006]], [[0.000, 0.000, 0.600], [-0.000, 0.000, 0.350], [0.000, -0.000, 0.350]], [[-0.000, -0.000, 0.000], [0.006, 0.004, -0.022], [0.006, -0.004, -0.022]]]), - (10, [[5.250, -0.600, 0.000], [1.915, -0.427, 0.000], [0.056, 0.249, 0.000], [-0.211, -0.182, 0.006], [-0.000, 0.000, 0.300], [-0.003, -0.005, -0.078]]), - (11, [[5.250, 0.600, 0.000], [1.915, 0.427, 0.000], [-0.056, 0.249, 0.000], [0.206, -0.165, -0.006], [0.000, -0.000, 0.300], [-0.003, 0.005, -0.078]]), - (12, [[2.450, -1.213, 0.000], [1.000, -0.145, 0.000], [0.022, 0.151, 0.000], [0.031, -0.007, 0.000], [-0.000, 0.000, 0.150], [0.000, 0.000, -0.015]]), - (13, [[4.327, -1.224, 0.000], [0.756, 0.003, 0.000], [-0.001, 0.200, 0.000], [-0.001, 0.075, -0.001], [0.000, -0.000, 0.120], [-0.000, 0.000, -0.025]]), - (14, [[4.800, -1.218, 0.000], [0.190, 0.009, 0.000], [-0.015, 0.300, -0.001], [-0.045, 0.124, -0.001], [-0.000, 0.000, 0.100], [-0.000, 0.000, -0.015]]), - (15, [[2.450, 1.213, 0.000], [0.979, 0.146, 0.000], [-0.023, 0.151, 0.000], [-0.029, -0.008, 0.000], [0.000, -0.000, 0.150], [0.000, 0.000, -0.015]]), - (16, [[4.327, 1.224, 0.000], [0.756, -0.003, 0.000], [0.001, 0.200, 0.000], [0.001, 0.075, 0.001], [-0.000, 0.000, 0.120], [-0.000, -0.000, -0.025]]), - (17, [[4.800, 1.218, 0.000], [0.190, -0.009, 0.000], [0.015, 0.300, 0.001], [0.045, 0.124, 0.001], [-0.000, -0.000, 0.100], [-0.000, -0.000, -0.015]]), - (18, [[8.100, -0.665, 0.000], [0.775, -0.027, 0.275], [0.007, 0.250, 0.003], [0.032, -0.000, 0.006], [-0.069, -0.000, 0.194], [-0.224, -0.006, -0.056]]), - (19, [[8.459, -0.683, 0.298], [0.258, -0.010, 0.370], [0.004, 0.250, 0.004], [-0.008, 0.000, -0.013], [-0.181, 0.001, 0.126], [0.051, -0.006, -0.001]]), - (20, [[8.601, -0.685, 0.694], [0.024, 0.006, 0.399], [-0.001, 0.250, -0.004], [-0.003, -0.000, 0.005], [-0.141, -0.000, 0.009], [0.111, 0.004, -0.120]]), - (21, [[8.100, 0.665, 0.000], [0.775, 0.027, 0.275], [-0.007, 0.250, -0.003], [-0.032, -0.000, -0.008], [-0.069, 0.000, 0.194], [-0.224, 0.008, -0.055]]), - (22, [[8.459, 0.683, 0.298], [0.258, 0.010, 0.370], [-0.000, 0.250, -0.007], [0.008, 0.000, 0.013], [-0.182, 0.003, 0.127], [0.049, 0.005, 0.002]]), - (23, [[8.601, 0.685, 0.694], [0.024, -0.006, 0.399], [0.001, 0.250, 0.004], [-0.006, 0.000, -0.005], [-0.148, 0.000, 0.009], [0.101, -0.013, -0.124]])], [ - (1, [[0.000, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.280, 0.000], [0.000, 0.000, 0.000], [0.000, 0.000, 0.280], [0.000, 0.000, 0.000]]), - (2, [[0.500, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.280, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.280], [0.000, 0.000, -0.000]]), - (3, [[1.000, 0.000, 0.000], [0.500, 0.000, 0.000], [0.000, 0.280, 0.000], [0.000, 0.070, 0.000], [0.000, 0.000, 0.280], [0.000, 0.000, 0.070]]), - (4, [[1.500, 0.000, 0.000], [[0.643, 0.000, 0.000], [0.012, -1.338, 0.000], [0.054, 1.358, 0.000]], [[0.000, 0.420, 0.000], [0.194, 0.002, 0.000], [-0.194, 0.008, 0.000]], [[0.000, 0.070, 0.000], [-0.058, -0.144, 0.000], [0.057, -0.139, 0.000]], [[0.000, 0.000, 0.420], [-0.000, 0.000, 0.196], [0.000, -0.000, 0.196]], [[0.000, 0.000, 0.070], [0.000, 0.000, -0.137], [0.000, 0.000, -0.137]]]), - (5, [[1.806, -1.023, 0.000], [0.599, -0.599, 0.000], [0.111, 0.111, 0.000], [-0.177, 0.128, 0.000], [-0.000, 0.000, 0.105], [0.000, 0.000, -0.045]]), - (6, [[1.825, 1.023, 0.000], [0.592, 0.587, 0.000], [-0.109, 0.110, 0.000], [0.169, 0.121, 0.000], [0.000, -0.000, 0.105], [0.000, 0.000, -0.045]]), - (7, [[2.400, -0.000, 0.000], [0.788, 0.000, 0.000], [0.000, 0.420, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.420], [0.000, 0.000, 0.000]]), - (8, [[3.100, -0.000, 0.000], [0.747, 0.000, 0.000], [0.000, 0.420, 0.000], [0.000, -0.000, 0.000], [0.000, 0.000, 0.420], [0.000, 0.000, -0.000]]), - (9, [[3.900, -0.000, 0.000], [[0.853, 0.000, 0.000], [0.731, -0.720, 0.000], [0.731, 0.720, 0.000]], [[0.000, 0.420, 0.000], [0.347, 0.353, 0.000], [-0.329, 0.335, 0.000]], [[-0.000, 0.000, -0.000], [-0.587, -0.121, -0.004], [0.543, -0.107, 0.004]], [[0.000, 0.000, 0.420], [-0.000, 0.000, 0.245], [0.000, -0.000, 0.245]], [[-0.000, -0.000, 0.000], [0.004, 0.003, -0.015], [0.004, -0.003, -0.015]]]), - (10, [[5.250, -0.600, 0.000], [1.915, -0.427, 0.000], [0.039, 0.174, 0.000], [-0.147, -0.127, 0.004], [-0.000, 0.000, 0.210], [-0.002, -0.004, -0.055]]), - (11, [[5.250, 0.600, 0.000], [1.915, 0.427, 0.000], [-0.039, 0.174, 0.000], [0.144, -0.115, -0.004], [0.000, -0.000, 0.210], [-0.002, 0.004, -0.055]]), - (12, [[2.450, -1.213, 0.000], [1.000, -0.145, 0.000], [0.015, 0.106, 0.000], [0.022, -0.005, 0.000], [-0.000, 0.000, 0.105], [0.000, 0.000, -0.011]]), - (13, [[4.327, -1.224, 0.000], [0.756, 0.003, 0.000], [-0.000, 0.140, 0.000], [-0.000, 0.052, -0.000], [0.000, -0.000, 0.084], [-0.000, 0.000, -0.018]]), - (14, [[4.800, -1.218, 0.000], [0.190, 0.009, 0.000], [-0.010, 0.210, -0.001], [-0.032, 0.087, -0.001], [-0.000, 0.000, 0.070], [-0.000, 0.000, -0.011]]), - (15, [[2.450, 1.213, 0.000], [0.979, 0.146, 0.000], [-0.016, 0.106, 0.000], [-0.020, -0.006, 0.000], [0.000, -0.000, 0.105], [0.000, 0.000, -0.011]]), - (16, [[4.327, 1.224, 0.000], [0.756, -0.003, 0.000], [0.000, 0.140, 0.000], [0.001, 0.052, 0.000], [-0.000, 0.000, 0.084], [-0.000, -0.000, -0.018]]), - (17, [[4.800, 1.218, 0.000], [0.190, -0.009, 0.000], [0.010, 0.210, 0.001], [0.031, 0.087, 0.001], [-0.000, -0.000, 0.070], [-0.000, -0.000, -0.011]]), - (18, [[8.100, -0.665, 0.000], [0.775, -0.027, 0.275], [0.005, 0.175, 0.002], [0.022, -0.000, 0.004], [-0.048, -0.000, 0.136], [-0.157, -0.004, -0.039]]), - (19, [[8.459, -0.683, 0.298], [0.258, -0.010, 0.370], [0.003, 0.175, 0.003], [-0.005, 0.000, -0.009], [-0.127, 0.001, 0.089], [0.036, -0.004, -0.000]]), - (20, [[8.601, -0.685, 0.694], [0.024, 0.006, 0.399], [-0.001, 0.175, -0.002], [-0.002, -0.000, 0.003], [-0.099, -0.000, 0.006], [0.077, 0.003, -0.084]]), - (21, [[8.100, 0.665, 0.000], [0.775, 0.027, 0.275], [-0.005, 0.175, -0.002], [-0.022, -0.000, -0.006], [-0.048, 0.000, 0.136], [-0.157, 0.006, -0.039]]), - (22, [[8.459, 0.683, 0.298], [0.258, 0.010, 0.370], [-0.000, 0.175, -0.005], [0.005, 0.000, 0.009], [-0.127, 0.002, 0.089], [0.034, 0.004, 0.001]]), - (23, [[8.601, 0.685, 0.694], [0.024, -0.006, 0.399], [0.001, 0.175, 0.002], [-0.004, 0.000, -0.003], [-0.104, 0.000, 0.006], [0.071, -0.009, -0.087]]) - ]]), - "userAnnotationGroups": [ - { - '_AnnotationGroup': True, - 'dimension': 1, - 'identifierRanges': '1-3', - 'name': get_body_term('head')[0], - 'ontId': get_body_term('head')[1] - }, - { - '_AnnotationGroup': True, - 'dimension': 1, - 'identifierRanges': '6-8', - 'name': 'torso', - 'ontId': 'None' - }, - { - '_AnnotationGroup': True, - 'dimension': 1, - 'identifierRanges': '4,11-13, 5,14-16', - 'name': get_body_term('arm')[0], - 'ontId': get_body_term('arm')[1] - }, - { - '_AnnotationGroup': True, - 'dimension': 1, - 'identifierRanges': '9,17-18-19, 10,20-21-22', - 'name': get_body_term('leg')[0], - 'ontId': get_body_term('leg')[1] - } - ] - }) + @classmethod + def generateBaseMesh(cls, region, options): + """ + Generate the unrefined mesh. + :param region: Zinc region to define model in. Must be empty. + :param options: Dict containing options. See getDefaultOptions(). + :return: [] empty list of AnnotationGroup, NetworkMesh + """ + # parameterSetName = options['Base parameter set'] + structure = options["Structure"] + headDepth = options["Head depth"] + headLength = options["Head length"] + headWidth = options["Head width"] + neckLength = options["Neck length"] + shoulderDrop = options["Shoulder drop"] + halfShoulderWidth = 0.5 * options["Shoulder width"] + armAngle = options["Arm lateral angle degrees"] + armLength = options["Arm length"] + halfArmWidth = 0.5 * options["Arm width"] + handLength = options["Hand length"] + handThickness = options["Hand thickness"] + handWidth = options["Hand width"] + halfTorsoDepth = 0.5 * options["Torso depth"] + torsoLength = options["Torso length"] + halfTorsoWidth = 0.5 * options["Torso width"] + pelvisDrop = options["Pelvis drop"] + halfPelvisWidth = 0.5 * options["Pelvis width"] + legAngle = options["Leg lateral angle degrees"] + legLength = options["Leg length"] + halfLegWidth = 0.5 * halfTorsoWidth + + networkMesh = NetworkMesh(structure) + networkMesh.create1DLayoutMesh(region) + + fieldmodule = region.getFieldmodule() + mesh = fieldmodule.findMeshByDimension(1) + + # set up element annotations + bodyGroup = AnnotationGroup(region, get_body_term("body")) + headGroup = AnnotationGroup(region, get_body_term("head")) + neckGroup = AnnotationGroup(region, get_body_term("neck")) + armGroup = AnnotationGroup(region, get_body_term("arm")) + leftArmGroup = AnnotationGroup(region, get_body_term("left arm")) + rightArmGroup = AnnotationGroup(region, get_body_term("right arm")) + thoraxGroup = AnnotationGroup(region, get_body_term("thorax")) + abdomenGroup = AnnotationGroup(region, get_body_term("abdomen")) + legGroup = AnnotationGroup(region, get_body_term("leg")) + leftLegGroup = AnnotationGroup(region, get_body_term("left leg")) + rightLegGroup = AnnotationGroup(region, get_body_term("right leg")) + annotationGroups = [bodyGroup, headGroup, neckGroup, armGroup, leftArmGroup, rightArmGroup, + thoraxGroup, abdomenGroup, legGroup, leftLegGroup, rightLegGroup] + bodyMeshGroup = bodyGroup.getMeshGroup(mesh) + elementIdentifier = 1 + headElementsCount = 3 + meshGroups = [bodyMeshGroup, headGroup.getMeshGroup(mesh)] + for e in range(headElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + neckElementsCount = 2 + meshGroups = [bodyMeshGroup, neckGroup.getMeshGroup(mesh)] + for e in range(neckElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + armElementsCount = 6 + left = 0 + right = 1 + for side in (left, right): + sideArmGroup = leftArmGroup if (side == left) else rightArmGroup + meshGroups = [bodyMeshGroup, armGroup.getMeshGroup(mesh), sideArmGroup.getMeshGroup(mesh)] + for e in range(armElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + thoraxElementsCount = 3 + abdomenElementsCount = 4 + torsoElementsCount = thoraxElementsCount + abdomenElementsCount + meshGroups = [bodyMeshGroup, thoraxGroup.getMeshGroup(mesh)] + for e in range(thoraxElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + meshGroups = [bodyMeshGroup, abdomenGroup.getMeshGroup(mesh)] + for e in range(abdomenElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + legElementsCount = 7 + for side in (left, right): + sideLegGroup = leftLegGroup if (side == left) else rightLegGroup + meshGroups = [bodyMeshGroup, legGroup.getMeshGroup(mesh), sideLegGroup.getMeshGroup(mesh)] + for e in range(legElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + + # set coordinates (outer) + fieldcache = fieldmodule.createFieldcache() + coordinates = find_or_create_field_coordinates(fieldmodule).castFiniteElement() + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + + headScale = headLength / headElementsCount + nodeIdentifier = 1 + d1 = [headScale, 0.0, 0.0] + d2 = [0.0, 0.5 * headWidth, 0.0] + d3 = [0.0, 0.0, 0.5 * headDepth] + for i in range(headElementsCount + 1): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, [headScale * i, 0.0, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + nodeIdentifier += 1 + neckScale = neckLength / neckElementsCount + d1 = [neckScale, 0.0, 0.0] + d2 = [0.0, 0.5 * headWidth, 0.0] + d3 = [0.0, 0.0, 0.5 * headWidth] + for i in range(1, neckElementsCount): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = [headLength + neckScale * i, 0.0, 0.0] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + nodeIdentifier += 1 + armJunctionNodeIdentifier = nodeIdentifier + torsoScale = torsoLength / torsoElementsCount + d1 = [torsoScale, 0.0, 0.0] + d2 = [0.0, halfTorsoWidth, 0.0] + d3 = [0.0, 0.0, halfTorsoDepth] + torsoStartX = headLength + neckLength + for i in range(torsoElementsCount + 1): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = [torsoStartX + torsoScale * i, 0.0, 0.0] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + nodeIdentifier += 1 + legJunctionNodeIdentifier = nodeIdentifier - 1 + + # arms + # set arm versions 2 (left) and 3 (right) on arm junction node + node = nodes.findNodeByIdentifier(armJunctionNodeIdentifier) + fieldcache.setNode(node) + d3 = [0.0, 0.0, 0.5 * (1.5 * halfArmWidth + halfTorsoDepth)] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 2, [0.0, halfShoulderWidth, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 2, [-1.5 * halfArmWidth, 0.0, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 2, d3) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 3, [0.0, -halfShoulderWidth, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 3, [1.5 * halfArmWidth, 0.0, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 3, d3) + # now set remainder of arm and hand coordinates + armStartX = torsoStartX + shoulderDrop + armScale = armLength / (armElementsCount - 1) + d3 = [0.0, 0.0, halfArmWidth] + for side in (left, right): + armAngleRadians = math.radians(armAngle if (side == left) else -armAngle) + cosArmAngle = math.cos(armAngleRadians) + sinArmAngle = math.sin(armAngleRadians) + d1 = [armScale * cosArmAngle, armScale * sinArmAngle, 0.0] + d2 = [-halfArmWidth * sinArmAngle, halfArmWidth * cosArmAngle, 0.0] + armStartY = halfShoulderWidth if (side == left) else -halfShoulderWidth + for i in range(armElementsCount): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = [armStartX + d1[0] * i, armStartY + d1[1] * i, d1[2] * i] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + nodeIdentifier += 1 + + # legs + # set leg versions 2 (left) and 3 (right) on leg junction node + node = nodes.findNodeByIdentifier(legJunctionNodeIdentifier) + fieldcache.setNode(node) + d3 = [0.0, 0.0, halfTorsoDepth] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 2, [0.0, halfPelvisWidth, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 2, [-halfLegWidth, 0.0, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 2, d3) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 3, [0.0, -halfPelvisWidth, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 3, [halfLegWidth, 0.0, 0.0]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 3, d3) + # now set remainder of leg and hand coordinates + legStartX = torsoStartX + torsoLength + pelvisDrop + legScale = legLength / (legElementsCount - 1) + d3 = [0.0, 0.0, halfTorsoDepth] + for side in (left, right): + legAngleRadians = math.radians(legAngle if (side == left) else -legAngle) + coslegAngle = math.cos(legAngleRadians) + sinlegAngle = math.sin(legAngleRadians) + d1 = [legScale * coslegAngle, legScale * sinlegAngle, 0.0] + d2 = [-halfLegWidth * sinlegAngle, halfLegWidth * coslegAngle, 0.0] + legStartY = halfPelvisWidth if (side == left) else -halfPelvisWidth + for i in range(legElementsCount): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = [legStartX + d1[0] * i, legStartY + d1[1] * i, d1[2] * i] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + nodeIdentifier += 1 + + smoothOptions = { + "Field": {"coordinates": True, "inner coordinates": False}, + "Smooth D12": True, + "Smooth D13": True} + cls.smoothSideCrossDerivatives(region, options, networkMesh, smoothOptions, None) + + cls.defineInnerCoordinates(region, coordinates, options, networkMesh) + + return annotationGroups, networkMesh + @classmethod + def getInteractiveFunctions(cls): + """ + Edit base class list to include only valid functions. + """ + interactiveFunctions = super(MeshType_1d_human_body_network_layout1, cls).getInteractiveFunctions() + for interactiveFunction in interactiveFunctions: + if interactiveFunction[0] == "Edit structure...": + interactiveFunctions.remove(interactiveFunction) + break + return interactiveFunctions class MeshType_3d_wholebody2(Scaffold_base): """ @@ -209,38 +377,33 @@ def getName(cls): def getParameterSetNames(cls): return [ "Default", - "Human 1", - "Human 2 Coarse", - "Human 2 Medium", - "Human 2 Fine" + "Human 1 Coarse", + "Human 1 Medium", + "Human 1 Fine" ] @classmethod def getDefaultOptions(cls, parameterSetName="Default"): - - options = { - 'Base parameter set': parameterSetName, - "Network layout": getDefaultNetworkLayoutScaffoldPackage(cls, parameterSetName), - "Number of elements around head": 12, - "Number of elements around torso": 12, - "Number of elements around arm": 8, - "Number of elements around leg": 8, - "Number of elements through shell": 1, - "Target element density along longest segment": 5.0, - "Show trim surfaces": False, - "Use Core": True, - "Number of elements across core box minor": 2, - "Number of elements across core transition": 1 - } - - if "Human 2 Medium" in parameterSetName: + options = {} + useParameterSetName = "Human 1 Coarse" if (parameterSetName == "Default") else parameterSetName + options["Base parameter set"] = useParameterSetName + options["Network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) + options["Number of elements around head"] = 12 + options["Number of elements around torso"] = 12 + options["Number of elements around arm"] = 8 + options["Number of elements around leg"] = 8 + options["Number of elements through shell"] = 1 + options["Target element density along longest segment"] = 5.0 + options["Show trim surfaces"] = False + options["Use Core"] = True + options["Number of elements across core box minor"] = 2 + options["Number of elements across core transition"] = 1 + if "Medium" in useParameterSetName: options["Number of elements around head"] = 16 options["Number of elements around torso"] = 16 - options["Number of elements around arm"] = 8 options["Number of elements around leg"] = 12 options["Target element density along longest segment"] = 8.0 - options["Number of elements across core box minor"] = 2 - elif "Human 2 Fine" in parameterSetName: + elif "Fine" in useParameterSetName: options["Number of elements around head"] = 24 options["Number of elements around torso"] = 24 options["Number of elements around arm"] = 12 @@ -270,18 +433,9 @@ def getOrderedOptionNames(cls): @classmethod def getOptionValidScaffoldTypes(cls, optionName): if optionName == "Network layout": - return [MeshType_1d_network_layout1] + return [MeshType_1d_human_body_network_layout1] return [] - @classmethod - def getOptionScaffoldTypeParameterSetNames(cls, optionName, scaffoldType): - if optionName == "Network layout": - return cls.getParameterSetNames() - 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): """ @@ -295,14 +449,14 @@ def getOptionScaffoldPackage(cls, optionName, scaffoldType, parameterSetName=Non if optionName == "Network layout": if not parameterSetName: parameterSetName = "Default" - return getDefaultNetworkLayoutScaffoldPackage(cls, parameterSetName) + return ScaffoldPackage(MeshType_1d_human_body_network_layout1, defaultParameterSetName=parameterSetName) assert False, cls.__name__ + ".getOptionScaffoldPackage: Option " + optionName + " is not a scaffold" @classmethod def checkOptions(cls, options): dependentChanges = False if not options["Network layout"].getScaffoldType() in cls.getOptionValidScaffoldTypes("Network layout"): - options["Network layout"] = cls.getOptionScaffoldPackage('Network layout', MeshType_1d_network_layout1) + options["Network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) minElementsCountAround = None for key in [ "Number of elements around head", @@ -349,8 +503,6 @@ def generateBaseMesh(cls, region, options): :return: list of AnnotationGroup, None """ # parameterSetName = options['Base parameter set'] - # isHuman = parameterSetName in ["Default", "Human 1", "Human 2 Coarse", "Human 2 Medium", "Human 2 Fine"] - layoutRegion = region.createRegion() networkLayout = options["Network layout"] networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters @@ -366,12 +518,18 @@ def generateBaseMesh(cls, region, options): aroundCount = 0 coreMajorCount = 0 name = layoutAnnotationGroup.getName() - if name in ["head", "torso", "arm", "leg"]: - aroundCount = options["Number of elements around " + name] + if ("head" in name) or ("neck" in name): + aroundCount = options["Number of elements around head"] + elif ("abdomen" in name) or ("thorax" in name) or ("torso" in name): + aroundCount = options["Number of elements around torso"] + elif "arm" in name: + aroundCount = options["Number of elements around arm"] + elif "leg" in name: + aroundCount = options["Number of elements around leg"] + if aroundCount: coreMajorCount = aroundCount // 2 - coreBoxMinorCount + 2 * coreTransitionCount annotationAroundCounts.append(aroundCount) annotationCoreMajorCounts.append(coreMajorCount) - isCore = options["Use Core"] tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 68dfd0de..8e938dc6 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -56,7 +56,8 @@ from scaffoldmaker.meshtypes.meshtype_3d_uterus1 import MeshType_3d_uterus1 from scaffoldmaker.meshtypes.meshtype_3d_uterus2 import MeshType_3d_uterus2 from scaffoldmaker.meshtypes.meshtype_3d_wholebody1 import MeshType_3d_wholebody1 -from scaffoldmaker.meshtypes.meshtype_3d_wholebody2 import MeshType_3d_wholebody2 +from scaffoldmaker.meshtypes.meshtype_3d_wholebody2 import ( + MeshType_3d_wholebody2, MeshType_1d_human_body_network_layout1) from scaffoldmaker.scaffoldpackage import ScaffoldPackage @@ -116,13 +117,20 @@ def __init__(self): MeshType_3d_uterus1, MeshType_3d_uterus2, MeshType_3d_wholebody1, + MeshType_1d_human_body_network_layout1, MeshType_3d_wholebody2 ] + self._allPrivateScaffoldTypes = [ + MeshType_1d_human_body_network_layout1 + ] def findScaffoldTypeByName(self, name): for scaffoldType in self._allScaffoldTypes: if scaffoldType.getName() == name: return scaffoldType + for scaffoldType in self._allPrivateScaffoldTypes: + if scaffoldType.getName() == name: + return scaffoldType return None def getDefaultMeshType(self): From 6ade0e7872c0def6cc3eb1a2979bb32c59e62c19 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Tue, 17 Sep 2024 10:49:32 +1200 Subject: [PATCH 02/20] Improve body shape, fix trim surfaces --- src/scaffoldmaker/annotation/body_terms.py | 1 + .../meshtypes/meshtype_3d_wholebody2.py | 217 +++++++++++------- src/scaffoldmaker/scaffolds.py | 2 +- src/scaffoldmaker/utils/tubenetworkmesh.py | 82 +++++-- tests/test_uterus.py | 6 +- tests/test_wholebody2.py | 10 +- 6 files changed, 208 insertions(+), 110 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index a6d22009..b9383fd9 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -15,6 +15,7 @@ ("head", "UBERON:0000033", "ILX:0104909"), ("head core", ""), ("diaphragm", "UBERON:0001103", "ILX:0103194"), + ("hand", "FMA:9712"), ("leg", "UBERON:0000978"), ("left leg", ""), ("right leg", ""), diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 83d2ff47..b196a455 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -1,7 +1,7 @@ """ Generates a 3D body coordinates using tube network mesh. """ -from cmlibs.maths.vectorops import add, mult +from cmlibs.maths.vectorops import cross, mult, set_magnitude from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates from cmlibs.utils.zinc.general import ChangeManager from cmlibs.zinc.element import Element @@ -11,6 +11,8 @@ from scaffoldmaker.scaffoldpackage import ScaffoldPackage from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm from scaffoldmaker.annotation.body_terms import get_body_term +from scaffoldmaker.utils.interpolation import ( + computeCubicHermiteEndDerivative, interpolateLagrangeHermiteDerivative, sampleCubicHermiteCurvesSmooth) from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData import math @@ -35,32 +37,39 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Structure"] = ( "1-2-3-4," "4-5-6.1," - "6.2-14-15-16-17-18-19," - "6.3-20-21-22-23-24-25," + "6.2-14-15-16-17-18-19,19-20," + "6.3-21-22-23-24-25-26,26-27," "6.1-7-8-9," "9-10-11-12-13.1," - "13.2-26-27-28,28-29-30-31-32," - "13.3-33-34-35,35-36-37-38-39") + "13.2-28-29-30,30-31-32-33,33-34," + "13.3-35-36-37,37-38-39-40,40-41") options["Define inner coordinates"] = True options["Head depth"] = 2.0 - options["Head length"] = 2.25 + options["Head length"] = 2.5 options["Head width"] = 2.0 options["Neck length"] = 1.5 - options["Shoulder drop"] = 0.5 + options["Shoulder drop"] = 0.7 options["Shoulder width"] = 5.0 - options["Arm lateral angle degrees"] = 15.0 - options["Arm length"] = 6.0 - options["Arm width"] = 0.8 - options["Hand length"] = 2.0 - options["Hand thickness"] = 0.5 - options["Hand width"] = 1.2 + options["Arm lateral angle degrees"] = 10.0 + options["Arm length"] = 7.0 + options["Arm top diameter"] = 1.0 + options["Wrist thickness"] = 0.5 + options["Wrist width"] = 0.7 + options["Hand length"] = 1.5 + options["Hand thickness"] = 0.3 + options["Hand width"] = 1.0 options["Torso depth"] = 2.5 options["Torso length"] = 6.0 options["Torso width"] = 3.5 - options["Pelvis drop"] = 0.5 - options["Pelvis width"] = 2.5 - options["Leg lateral angle degrees"] = 15.0 - options["Leg length"] = 8.0 + options["Pelvis drop"] = 1.0 + options["Pelvis width"] = 2.0 + options["Leg lateral angle degrees"] = 10.0 + options["Leg length"] = 9.0 + options["Leg top diameter"] = 1.75 + options["Leg bottom diameter"] = 0.75 + options["Foot length"] = 2.0 + options["Foot thickness"] = 0.4 + options["Foot width"] = 1.2 return options @classmethod @@ -74,7 +83,9 @@ def getOrderedOptionNames(cls): "Shoulder width", "Arm lateral angle degrees", "Arm length", - "Arm width", + "Arm top diameter", + "Wrist thickness", + "Wrist width", "Hand length", "Hand thickness", "Hand width", @@ -84,7 +95,12 @@ def getOrderedOptionNames(cls): "Pelvis drop", "Pelvis width", "Leg lateral angle degrees", - "Leg length" + "Leg length", + "Leg top diameter", + "Leg bottom diameter", + "Foot length", + "Foot thickness", + "Foot width" ] @classmethod @@ -95,18 +111,25 @@ def checkOptions(cls, options): "Head length", "Head width", "Neck length", - # "Shoulder drop", + "Shoulder drop", "Shoulder width", "Arm length", - "Arm width", + "Arm top diameter", + "Wrist thickness", + "Wrist width", "Hand length", "Hand thickness", "Hand width", - # "Pelvis drop", + "Pelvis drop", "Pelvis width", "Torso depth", "Torso width", - "Leg length" + "Leg length", + "Leg top diameter", + "Leg bottom diameter", + "Foot length", + "Foot thickness", + "Foot width" ]: if options[key] < 0.1: options[key] = 0.1 @@ -143,20 +166,24 @@ def generateBaseMesh(cls, region, options): neckLength = options["Neck length"] shoulderDrop = options["Shoulder drop"] halfShoulderWidth = 0.5 * options["Shoulder width"] - armAngle = options["Arm lateral angle degrees"] + armAngleRadians = math.radians(options["Arm lateral angle degrees"]) + armAngleDrop = shoulderDrop * math.cos(armAngleRadians) armLength = options["Arm length"] - halfArmWidth = 0.5 * options["Arm width"] + armTopRadius = 0.5 * options["Arm top diameter"] + halfWristThickness = 0.5 * options["Wrist thickness"] + halfWristWidth = 0.5 * options["Wrist width"] handLength = options["Hand length"] - handThickness = options["Hand thickness"] - handWidth = options["Hand width"] + halfHandThickness = 0.5 * options["Hand thickness"] + halfHandWidth = 0.5 * options["Hand width"] halfTorsoDepth = 0.5 * options["Torso depth"] torsoLength = options["Torso length"] halfTorsoWidth = 0.5 * options["Torso width"] pelvisDrop = options["Pelvis drop"] halfPelvisWidth = 0.5 * options["Pelvis width"] - legAngle = options["Leg lateral angle degrees"] + legAngleRadians = math.radians(options["Leg lateral angle degrees"]) legLength = options["Leg length"] - halfLegWidth = 0.5 * halfTorsoWidth + legTopRadius = 0.5 * options["Leg top diameter"] + legBottomRadius = 0.5 * options["Leg bottom diameter"] networkMesh = NetworkMesh(structure) networkMesh.create1DLayoutMesh(region) @@ -171,12 +198,13 @@ def generateBaseMesh(cls, region, options): armGroup = AnnotationGroup(region, get_body_term("arm")) leftArmGroup = AnnotationGroup(region, get_body_term("left arm")) rightArmGroup = AnnotationGroup(region, get_body_term("right arm")) + handGroup = AnnotationGroup(region, get_body_term("hand")) thoraxGroup = AnnotationGroup(region, get_body_term("thorax")) abdomenGroup = AnnotationGroup(region, get_body_term("abdomen")) legGroup = AnnotationGroup(region, get_body_term("leg")) leftLegGroup = AnnotationGroup(region, get_body_term("left leg")) rightLegGroup = AnnotationGroup(region, get_body_term("right leg")) - annotationGroups = [bodyGroup, headGroup, neckGroup, armGroup, leftArmGroup, rightArmGroup, + annotationGroups = [bodyGroup, headGroup, neckGroup, armGroup, leftArmGroup, rightArmGroup, handGroup, thoraxGroup, abdomenGroup, legGroup, leftLegGroup, rightLegGroup] bodyMeshGroup = bodyGroup.getMeshGroup(mesh) elementIdentifier = 1 @@ -194,16 +222,20 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - armElementsCount = 6 left = 0 right = 1 + armElementsCount = 7 + armMeshGroup = armGroup.getMeshGroup(mesh) + handMeshGroup = handGroup.getMeshGroup(mesh) for side in (left, right): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup - meshGroups = [bodyMeshGroup, armGroup.getMeshGroup(mesh), sideArmGroup.getMeshGroup(mesh)] + meshGroups = [bodyMeshGroup, armMeshGroup, sideArmGroup.getMeshGroup(mesh)] for e in range(armElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) + if e == (armElementsCount - 1): + handMeshGroup.addElement(element) elementIdentifier += 1 thoraxElementsCount = 3 abdomenElementsCount = 4 @@ -221,9 +253,10 @@ def generateBaseMesh(cls, region, options): meshGroup.addElement(element) elementIdentifier += 1 legElementsCount = 7 + legMeshGroup = legGroup.getMeshGroup(mesh) for side in (left, right): sideLegGroup = leftLegGroup if (side == left) else rightLegGroup - meshGroups = [bodyMeshGroup, legGroup.getMeshGroup(mesh), sideLegGroup.getMeshGroup(mesh)] + meshGroups = [bodyMeshGroup, legMeshGroup, sideLegGroup.getMeshGroup(mesh)] for e in range(legElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: @@ -267,6 +300,7 @@ def generateBaseMesh(cls, region, options): d2 = [0.0, halfTorsoWidth, 0.0] d3 = [0.0, 0.0, halfTorsoDepth] torsoStartX = headLength + neckLength + sx = [torsoStartX, 0.0, 0.0] for i in range(torsoElementsCount + 1): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -277,65 +311,94 @@ def generateBaseMesh(cls, region, options): coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) nodeIdentifier += 1 legJunctionNodeIdentifier = nodeIdentifier - 1 + px = x # arms - # set arm versions 2 (left) and 3 (right) on arm junction node - node = nodes.findNodeByIdentifier(armJunctionNodeIdentifier) - fieldcache.setNode(node) - d3 = [0.0, 0.0, 0.5 * (1.5 * halfArmWidth + halfTorsoDepth)] - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 2, [0.0, halfShoulderWidth, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 2, [-1.5 * halfArmWidth, 0.0, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 2, d3) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 3, [0.0, -halfShoulderWidth, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 3, [1.5 * halfArmWidth, 0.0, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 3, d3) - # now set remainder of arm and hand coordinates - armStartX = torsoStartX + shoulderDrop - armScale = armLength / (armElementsCount - 1) - d3 = [0.0, 0.0, halfArmWidth] + armStartX = torsoStartX + armAngleDrop + nonHandArmLength = armLength - handLength + armScale = nonHandArmLength / (armElementsCount - 3) + sd3 = [0.0, 0.0, armTopRadius] + hd3 = [0.0, 0.0, halfHandWidth] for side in (left, right): - armAngleRadians = math.radians(armAngle if (side == left) else -armAngle) - cosArmAngle = math.cos(armAngleRadians) - sinArmAngle = math.sin(armAngleRadians) - d1 = [armScale * cosArmAngle, armScale * sinArmAngle, 0.0] - d2 = [-halfArmWidth * sinArmAngle, halfArmWidth * cosArmAngle, 0.0] + armAngle = armAngleRadians if (side == left) else -armAngleRadians + cosArmAngle = math.cos(armAngle) + sinArmAngle = math.sin(armAngle) armStartY = halfShoulderWidth if (side == left) else -halfShoulderWidth - for i in range(armElementsCount): + x = [armStartX, armStartY, 0.0] + d1 = [armScale * cosArmAngle, armScale * sinArmAngle, 0.0] + # set leg versions 2 (left) and 3 (right) on leg junction node, and intermediate shoulder node + sd1 = interpolateLagrangeHermiteDerivative(sx, x, d1, 0.0) + nx, nd1 = sampleCubicHermiteCurvesSmooth([sx, x], [sd1, d1], 2, derivativeMagnitudeEnd=armScale)[0:2] + for n in range(2): + node = nodes.findNodeByIdentifier(nodeIdentifier if (n > 0) else armJunctionNodeIdentifier) + fieldcache.setNode(node) + version = 1 if (n > 0) else 2 if (side == left) else 3 + sd1 = nd1[n] + sd2 = set_magnitude(cross(sd3, sd1), armTopRadius) + if n > 0: + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, version, nx[n]) + nodeIdentifier += 1 + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, sd1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, sd2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, sd3) + # main part of arm to wrist + for i in range(armElementsCount - 2): + xi = i / (armElementsCount - 3) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [armStartX + d1[0] * i, armStartY + d1[1] * i, d1[2] * i] + halfThickness = xi * halfWristThickness + (1.0 - xi) * armTopRadius + halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius + d2 = [-halfThickness * sinArmAngle, halfThickness * cosArmAngle, 0.0] + d3 = [0.0, 0.0, halfWidth] coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) nodeIdentifier += 1 + # hand + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + h = (armElementsCount - 3) + handLength / armScale + hx = [armStartX + armLength * cosArmAngle, armStartY + armLength * sinArmAngle, 0.0] + hd1 = computeCubicHermiteEndDerivative(x, d1, hx, d1) + hd2 = set_magnitude(d2, halfHandThickness) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, hx) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, hd1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, hd2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, hd3) + nodeIdentifier += 1 # legs - # set leg versions 2 (left) and 3 (right) on leg junction node - node = nodes.findNodeByIdentifier(legJunctionNodeIdentifier) - fieldcache.setNode(node) - d3 = [0.0, 0.0, halfTorsoDepth] - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 2, [0.0, halfPelvisWidth, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 2, [-halfLegWidth, 0.0, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 2, d3) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 3, [0.0, -halfPelvisWidth, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 3, [halfLegWidth, 0.0, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 3, d3) - # now set remainder of leg and hand coordinates legStartX = torsoStartX + torsoLength + pelvisDrop legScale = legLength / (legElementsCount - 1) - d3 = [0.0, 0.0, halfTorsoDepth] + pd3 = [0.0, 0.0, halfTorsoDepth] for side in (left, right): - legAngleRadians = math.radians(legAngle if (side == left) else -legAngle) - coslegAngle = math.cos(legAngleRadians) - sinlegAngle = math.sin(legAngleRadians) - d1 = [legScale * coslegAngle, legScale * sinlegAngle, 0.0] - d2 = [-halfLegWidth * sinlegAngle, halfLegWidth * coslegAngle, 0.0] + legAngle = legAngleRadians if (side == left) else -legAngleRadians + coslegAngle = math.cos(legAngle) + sinlegAngle = math.sin(legAngle) legStartY = halfPelvisWidth if (side == left) else -halfPelvisWidth + x = [legStartX, legStartY, 0.0] + d1 = [legScale * coslegAngle, legScale * sinlegAngle, 0.0] + + # set leg versions 2 (left) and 3 (right) on leg junction node + node = nodes.findNodeByIdentifier(legJunctionNodeIdentifier) + fieldcache.setNode(node) + pd1 = interpolateLagrangeHermiteDerivative(px, x, d1, 0.0) + pd2 = set_magnitude(cross(pd3, pd1), legTopRadius) + version = 2 if (side == left) else 3 + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, pd1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, pd2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, pd3) + for i in range(legElementsCount): + xi = i / (legElementsCount - 1) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [legStartX + d1[0] * i, legStartY + d1[1] * i, d1[2] * i] + radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius + d2 = [-radius * sinlegAngle, radius * coslegAngle, 0.0] + d3 = [0.0, 0.0, radius] coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) @@ -387,7 +450,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options = {} useParameterSetName = "Human 1 Coarse" if (parameterSetName == "Default") else parameterSetName options["Base parameter set"] = useParameterSetName - options["Network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) + options["Body network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) options["Number of elements around head"] = 12 options["Number of elements around torso"] = 12 options["Number of elements around arm"] = 8 @@ -417,7 +480,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): @classmethod def getOrderedOptionNames(cls): optionNames = [ - "Network layout", + "Body network layout", "Number of elements around head", "Number of elements around torso", "Number of elements around arm", @@ -432,7 +495,7 @@ def getOrderedOptionNames(cls): @classmethod def getOptionValidScaffoldTypes(cls, optionName): - if optionName == "Network layout": + if optionName == "Body network layout": return [MeshType_1d_human_body_network_layout1] return [] @@ -446,7 +509,7 @@ def getOptionScaffoldPackage(cls, optionName, scaffoldType, parameterSetName=Non assert parameterSetName in cls.getOptionScaffoldTypeParameterSetNames(optionName, scaffoldType), \ "Invalid parameter set " + str(parameterSetName) + " for scaffold " + str(scaffoldType.getName()) + \ " in option " + str(optionName) + " of scaffold " + cls.getName() - if optionName == "Network layout": + if optionName == "Body network layout": if not parameterSetName: parameterSetName = "Default" return ScaffoldPackage(MeshType_1d_human_body_network_layout1, defaultParameterSetName=parameterSetName) @@ -455,8 +518,8 @@ def getOptionScaffoldPackage(cls, optionName, scaffoldType, parameterSetName=Non @classmethod def checkOptions(cls, options): dependentChanges = False - if not options["Network layout"].getScaffoldType() in cls.getOptionValidScaffoldTypes("Network layout"): - options["Network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) + if not options["Body network layout"].getScaffoldType() in cls.getOptionValidScaffoldTypes("Body network layout"): + options["Body network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) minElementsCountAround = None for key in [ "Number of elements around head", @@ -504,7 +567,7 @@ def generateBaseMesh(cls, region, options): """ # parameterSetName = options['Base parameter set'] layoutRegion = region.createRegion() - networkLayout = options["Network layout"] + networkLayout = options["Body network layout"] networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters layoutAnnotationGroups = networkLayout.getAnnotationGroups() networkMesh = networkLayout.getConstructionObject() diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index 8e938dc6..cc15f536 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -117,7 +117,7 @@ def __init__(self): MeshType_3d_uterus1, MeshType_3d_uterus2, MeshType_3d_wholebody1, - MeshType_1d_human_body_network_layout1, + MeshType_1d_human_body_network_layout1, # GRC remove MeshType_3d_wholebody2 ] self._allPrivateScaffoldTypes = [ diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index c32dd862..c5ecaa76 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -36,6 +36,7 @@ def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurface region, meshDimension, coordinateFieldName, startNodeIdentifier, startElementIdentifier) self._isLinearThroughWall = isLinearThroughWall self._isShowTrimSurfaces = isShowTrimSurfaces + self._trimAnnotationGroupCount = 0 # incremented to make unique annotation group names for trim surfaces # get node template for standard and cross nodes self._nodetemplate = self._nodes.createNodetemplate() @@ -176,6 +177,11 @@ def isLinearThroughWall(self): def isShowTrimSurfaces(self): return self._isShowTrimSurfaces + def getNewTrimAnnotationGroup(self): + self._trimAnnotationGroupCount += 1 + return self.getOrCreateAnnotationGroup(("trim surface " + "{:03d}".format(self._trimAnnotationGroupCount), "")) + + class TubeNetworkMeshSegment(NetworkMeshSegment): def __init__(self, networkSegment, pathParametersList, elementsCountAround, elementsCountThroughWall, @@ -1315,6 +1321,24 @@ def _calculateTrimSurfaces(self): outDir = [-d for d in outDir] outDirs[s].append(outDir) + segmentEndPlaneTrackSurfaces = [] + for s in range(self._segmentsCount): + endIndex = -1 if self._segmentsIn[s] else 0 + pathEndPlaneTrackSurfaces = [] + for p in range(pathsCount): + pathParameters = self._segments[s].getPathParameters(p) + centre = pathParameters[0][endIndex] + axis1 = pathParameters[2][endIndex] + axis2 = pathParameters[4][endIndex] + nx = [sub(sub(centre, axis1), axis2), + sub(add(centre, axis1), axis2), + add(sub(centre, axis1), axis2), + add(add(centre, axis1), axis2)] + nd1 = [mult(axis1, 2.0)] * 4 + nd2 = [mult(axis2, 2.0)] * 4 + pathEndPlaneTrackSurfaces.append(TrackSurface(1, 1, nx, nd1, nd2)) + segmentEndPlaneTrackSurfaces.append(pathEndPlaneTrackSurfaces) + trimPointsCountAround = 6 trimAngle = 2.0 * math.pi / trimPointsCountAround for s in range(self._segmentsCount): @@ -1389,23 +1413,32 @@ def _calculateTrimSurfaces(self): 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 + for i in range(2): + otherTrackSurface = \ + otherSegment.getRawTrackSurface(p) if (i == 0) else segmentEndPlaneTrackSurfaces[os][p] + otherSurfacePosition, curveLocation, isIntersection = \ + otherTrackSurface.findNearestPositionOnCurve( + cx, cd2, loop=False, sampleEnds=False, sampleHalf=2 if self._segmentsIn[s] else 1) + if isIntersection: + if i == 1: + # must be within ellipse inside rectangular plane + xi1 = otherSurfacePosition.xi1 - 0.5 + xi2 = otherSurfacePosition.xi2 - 0.5 + if (xi1 * xi1 + xi2 * xi2) > 0.25: + break + proportion2 = (curveLocation[0] + curveLocation[1]) / (pointsCountAlong - 1) + proportionFromEnd = (1.0 - proportion2) if self._segmentsIn[s] else proportion2 + 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 + break if maxProportionFromEnd < lowestMaxProportionFromEnd: lowestMaxProportionFromEnd = maxProportionFromEnd rx.append(x) @@ -1413,13 +1446,14 @@ def _calculateTrimSurfaces(self): if trim: # centre of trim surfaces is at lowestMaxProportionFromEnd - if lowestMaxProportionFromEnd == 0.0: + 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) + proportion = \ + (1.0 - lowestMaxProportionFromEnd) if self._segmentsIn[s] else lowestMaxProportionFromEnd + eProportion = proportion * (pointsCountAlong - 1) + e = min(int(eProportion), (pointsCountAlong - 2)) + curveLocation = (e, eProportion - e) xCentre = evaluateCoordinatesOnCurve(pathParameters[0], pathParameters[1], curveLocation) # ensure d1 directions go around in same direction as loop for n1 in range(trimPointsCountAround): @@ -2442,8 +2476,10 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): for s in range(self._segmentsCount): for trimSurface in self._trimSurfaces[s]: if trimSurface: + annotationGroup = generateData.getNewTrimAnnotationGroup() nodeIdentifier, faceIdentifier = \ - trimSurface.generateMesh(generateData.getRegion(), nodeIdentifier, faceIdentifier) + trimSurface.generateMesh(generateData.getRegion(), nodeIdentifier, faceIdentifier, + group_name=annotationGroup.getName()) if dimension == 2: elementIdentifier = faceIdentifier generateData.setNodeElementIdentifiers(nodeIdentifier, elementIdentifier) diff --git a/tests/test_uterus.py b/tests/test_uterus.py index 45d6ad37..0322bcf8 100644 --- a/tests/test_uterus.py +++ b/tests/test_uterus.py @@ -66,7 +66,7 @@ def test_uterus1(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) assertAlmostEqualList(self, minimums, [-9.361977045958657, -0.048, -8.90345243427233], 1.0E-6) - assertAlmostEqualList(self, maximums, [9.36197704595863, 12.809844943106265, 1.09], 1.0E-6) + assertAlmostEqualList(self, maximums, [9.36197704595863, 12.810434295416874, 1.09], 1.0E-6) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -80,8 +80,8 @@ def test_uterus1(self): self.assertEqual(result, RESULT_OK) result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 261.42077575225807, delta=1.0E-6) - self.assertAlmostEqual(volume, 182.65005670492496, delta=1.0E-6) + self.assertAlmostEqual(surfaceArea, 261.4231659225306, delta=1.0E-6) + self.assertAlmostEqual(volume, 182.6551617962469, delta=1.0E-6) fieldmodule.defineAllFaces() for annotationGroup in annotationGroups: diff --git a/tests/test_wholebody2.py b/tests/test_wholebody2.py index c7610463..62a1428d 100644 --- a/tests/test_wholebody2.py +++ b/tests/test_wholebody2.py @@ -24,8 +24,7 @@ def test_wholebody2_core(self): """ scaffold = MeshType_3d_wholebody2 parameterSetNames = scaffold.getParameterSetNames() - self.assertEqual(parameterSetNames, ["Default", "Human 1", "Human 2 Coarse", "Human 2 Medium", - "Human 2 Fine"]) + self.assertEqual(parameterSetNames, ["Default", "Human 1 Coarse", "Human 1 Medium", "Human 1 Fine"]) options = scaffold.getDefaultOptions("Default") self.assertEqual(12, len(options)) self.assertEqual(12, options["Number of elements around head"]) @@ -43,7 +42,7 @@ def test_wholebody2_core(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(5, len(annotationGroups)) # Needs updating as we add more annotation groups + self.assertEqual(13, len(annotationGroups)) # Needs updating as we add more annotation groups fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) @@ -113,8 +112,7 @@ def test_wholebody2_tube(self): """ scaffold = MeshType_3d_wholebody2 parameterSetNames = scaffold.getParameterSetNames() - self.assertEqual(parameterSetNames, ["Default", "Human 1", "Human 2 Coarse", "Human 2 Medium", - "Human 2 Fine"]) + self.assertEqual(parameterSetNames, ["Default", "Human 1 Coarse", "Human 1 Medium", "Human 1 Fine"]) options = scaffold.getDefaultOptions("Default") self.assertEqual(12, len(options)) self.assertEqual(12, options["Number of elements around head"]) @@ -133,7 +131,7 @@ def test_wholebody2_tube(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(5, len(annotationGroups)) + self.assertEqual(13, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) From 3a8b30b7547327bf71286057bdfee0676c74b008 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Thu, 19 Sep 2024 15:48:24 +1200 Subject: [PATCH 03/20] Improve body shape, fix core holes --- .../meshtypes/meshtype_3d_wholebody2.py | 92 ++++++++++++------- src/scaffoldmaker/utils/eft_utils.py | 19 +--- src/scaffoldmaker/utils/tubenetworkmesh.py | 33 +++---- 3 files changed, 78 insertions(+), 66 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index b196a455..8524c248 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -1,7 +1,7 @@ """ Generates a 3D body coordinates using tube network mesh. """ -from cmlibs.maths.vectorops import cross, mult, set_magnitude +from cmlibs.maths.vectorops import add, cross, mult, set_magnitude from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates from cmlibs.utils.zinc.general import ChangeManager from cmlibs.zinc.element import Element @@ -12,7 +12,8 @@ from scaffoldmaker.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm from scaffoldmaker.annotation.body_terms import get_body_term from scaffoldmaker.utils.interpolation import ( - computeCubicHermiteEndDerivative, interpolateLagrangeHermiteDerivative, sampleCubicHermiteCurvesSmooth) + computeCubicHermiteEndDerivative, interpolateLagrangeHermiteDerivative, sampleCubicHermiteCurvesSmooth, + smoothCubicHermiteDerivativesLine) from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData import math @@ -41,17 +42,17 @@ def getDefaultOptions(cls, parameterSetName="Default"): "6.3-21-22-23-24-25-26,26-27," "6.1-7-8-9," "9-10-11-12-13.1," - "13.2-28-29-30,30-31-32-33,33-34," - "13.3-35-36-37,37-38-39-40,40-41") + "13.2-28-29-30-31-32,32-33,33-34," + "13.3-35-36-37-38-39,39-40,40-41") options["Define inner coordinates"] = True - options["Head depth"] = 2.0 - options["Head length"] = 2.5 - options["Head width"] = 2.0 - options["Neck length"] = 1.5 + options["Head depth"] = 2.1 + options["Head length"] = 2.2 + options["Head width"] = 1.9 + options["Neck length"] = 1.3 options["Shoulder drop"] = 0.7 - options["Shoulder width"] = 5.0 + options["Shoulder width"] = 4.5 options["Arm lateral angle degrees"] = 10.0 - options["Arm length"] = 7.0 + options["Arm length"] = 7.5 options["Arm top diameter"] = 1.0 options["Wrist thickness"] = 0.5 options["Wrist width"] = 0.7 @@ -59,17 +60,17 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Hand thickness"] = 0.3 options["Hand width"] = 1.0 options["Torso depth"] = 2.5 - options["Torso length"] = 6.0 - options["Torso width"] = 3.5 - options["Pelvis drop"] = 1.0 + options["Torso length"] = 5.5 + options["Torso width"] = 3.2 + options["Pelvis drop"] = 1.5 options["Pelvis width"] = 2.0 options["Leg lateral angle degrees"] = 10.0 - options["Leg length"] = 9.0 - options["Leg top diameter"] = 1.75 - options["Leg bottom diameter"] = 0.75 - options["Foot length"] = 2.0 - options["Foot thickness"] = 0.4 - options["Foot width"] = 1.2 + options["Leg length"] = 10.0 + options["Leg top diameter"] = 2.0 + options["Leg bottom diameter"] = 0.7 + options["Foot length"] = 2.5 + options["Foot thickness"] = 0.3 + options["Foot width"] = 1.0 return options @classmethod @@ -184,6 +185,9 @@ def generateBaseMesh(cls, region, options): legLength = options["Leg length"] legTopRadius = 0.5 * options["Leg top diameter"] legBottomRadius = 0.5 * options["Leg bottom diameter"] + footLength = options["Foot length"] + halfFootThickness = 0.5 * options["Foot thickness"] + halfFootWidth = 0.5 * options["Foot width"] networkMesh = NetworkMesh(structure) networkMesh.create1DLayoutMesh(region) @@ -370,40 +374,66 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 # legs + cos45 = math.cos(0.25 * math.pi) legStartX = torsoStartX + torsoLength + pelvisDrop - legScale = legLength / (legElementsCount - 1) - pd3 = [0.0, 0.0, halfTorsoDepth] + legScale = legLength / (legElementsCount - 2) + pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] # GRC for side in (left, right): legAngle = legAngleRadians if (side == left) else -legAngleRadians - coslegAngle = math.cos(legAngle) - sinlegAngle = math.sin(legAngle) + cosLegAngle = math.cos(legAngle) + sinLegAngle = math.sin(legAngle) legStartY = halfPelvisWidth if (side == left) else -halfPelvisWidth - x = [legStartX, legStartY, 0.0] - d1 = [legScale * coslegAngle, legScale * sinlegAngle, 0.0] - + x = legStart = [legStartX, legStartY, 0.0] + legDirn = [cosLegAngle, sinLegAngle, 0.0] + legSide = [-sinLegAngle, cosLegAngle, 0.0] + legFront = cross(legDirn, legSide) + d1 = mult(legDirn, legScale) # set leg versions 2 (left) and 3 (right) on leg junction node node = nodes.findNodeByIdentifier(legJunctionNodeIdentifier) fieldcache.setNode(node) pd1 = interpolateLagrangeHermiteDerivative(px, x, d1, 0.0) - pd2 = set_magnitude(cross(pd3, pd1), legTopRadius) + pd2 = set_magnitude(cross(pd3, pd1), 0.5 * legTopRadius + 0.5 * halfTorsoWidth) # GRC version = 2 if (side == left) else 3 coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, pd1) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, pd2) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, pd3) - - for i in range(legElementsCount): - xi = i / (legElementsCount - 1) + # main part of leg to ankle + for i in range(legElementsCount - 2): + xi = i / (legElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [legStartX + d1[0] * i, legStartY + d1[1] * i, d1[2] * i] radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius - d2 = [-radius * sinlegAngle, radius * coslegAngle, 0.0] + d2 = mult(legSide, radius) d3 = [0.0, 0.0, radius] coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) nodeIdentifier += 1 + # foot + heelOffset = legBottomRadius * (1.0 - cos45) + fx = [add(add(legStart, mult(legDirn, legLength - legBottomRadius)), + [-heelOffset * cosLegAngle, -heelOffset * sinLegAngle, heelOffset]), + add(add(legStart, mult(legDirn, legLength - halfFootThickness)), + [0.0, 0.0, footLength - legBottomRadius])] + fd1 = smoothCubicHermiteDerivativesLine( + [x] + fx, [d1, set_magnitude(add(legDirn, legFront), legScale), + [0.0, 0.0, 2.0 * footLength]], + fixAllDirections=True, fixStartDerivative=True, fixEndDerivative=True)[1:] + halfAnkleThickness = math.sqrt(2.0 * legBottomRadius * legBottomRadius) + ankleRadius = legBottomRadius # GRC check + fd2 = [mult(legSide, ankleRadius), mult(legSide, halfFootWidth)] + fd3 = [set_magnitude(cross(fd1[0], fd2[0]), halfAnkleThickness), + set_magnitude(cross(fd1[1], fd2[1]), halfFootThickness)] + for i in range(2): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, fx[i]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, fd1[i]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, fd2[i]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, fd3[i]) + nodeIdentifier += 1 smoothOptions = { "Field": {"coordinates": True, "inner coordinates": False}, diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index 713913ad..e2b34343 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -629,21 +629,16 @@ def __init__(self): 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._nodeLayout6WayBifurcation = 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], [1.0, -1.0, 0.0], [-1.0, 1.0, 0.0]]) - self._nodeLayout6WayBifurcationTransition = HermiteNodeLayout( - [[0.0, 0.0, -1.0], [0.0, 0.0, 1.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, 1.0, 0.0]]) self._nodeLayout6WayTriplePointTop1 = 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], [-1.0, -1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]) self._nodeLayout6WayTriplePointTop2 = HermiteNodeLayout( - [[-1.0, -1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]) + [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, -1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]) self._nodeLayout6WayTriplePointBottom1 = 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], [-1.0, -1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, -1.0]]) self._nodeLayout6WayTriplePointBottom2 = HermiteNodeLayout( - [[-1.0, -1.0, 0.0], [1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, -1.0]]) + [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, -1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.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( @@ -714,15 +709,7 @@ def getNodeLayout6WayBifurcation(self): 6-way bifurcation transition and a node layout for 6-way triple point. :return: HermiteNodeLayout. """ - return self._nodeLayout6WayBifurcation - - def getNodeLayout6WayBifurcationTransition(self): - """ - Get node layout for a special case of 6-way bifurcation transition, used in conjunction with a node layout for - 6-way bifurcation and a node layout for 6-way triple point. - :return: HermiteNodeLayout. - """ - return self._nodeLayout6WayBifurcationTransition + return self._nodeLayout6Way12_d3Defined def getNodeLayout6WayTriplePoint(self): """ diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index c5ecaa76..1eff5d54 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -65,12 +65,11 @@ def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurface 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]]]) self._nodeLayout6WayTriplePoint = self._nodeLayoutManager.getNodeLayout6WayTriplePoint() - self._nodeLayoutBifrucation = self._nodeLayoutManager.getNodeLayout6WayBifurcation() + self._nodeLayoutBifurcation = self._nodeLayoutManager.getNodeLayout6WayBifurcation() self._nodeLayoutTrifurcation = None self._nodeLayoutTransition = self._nodeLayoutManager.getNodeLayoutRegularPermuted( d3Defined, limitDirections=[None, [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]], None]) self._nodeLayoutTransitionTriplePoint = None - self._nodeLayoutBifurcationTransition = self._nodeLayoutManager.getNodeLayout6WayBifurcationTransition() def getStandardEft(self): @@ -115,7 +114,7 @@ def getNodeLayoutBifurcation(self): """ Special node layout for generating core elements for bifurcation. """ - return self._nodeLayoutBifrucation + return self._nodeLayoutBifurcation def getNodeLayoutTrifurcation(self, location): """ @@ -138,12 +137,6 @@ def getNodeLayoutTransition(self): """ return self._nodeLayoutTransition - def getNodeLayoutBifurcationTransition(self): - """ - Special node layout for generating core transition elements for bifurcation. - """ - return self._nodeLayoutBifurcationTransition - def getNodeLayoutTransitionTriplePoint(self, location): """ Special node layout for generating core transition elements at triple points. @@ -2303,6 +2296,8 @@ def _generateBoxElements(self, s, n2, mesh, elementtemplate, coordinates, segmen for e1 in range(boxElementsCountAcrossMinor): e3p = (e3 + 1) nids, nodeParameters, nodeLayouts = [], [], [] + # get identifier early to aid debugging + elementIdentifier = generateData.nextElementIdentifier() for n1 in [e1, e1 + 1]: for n3 in [e3, e3p]: nids.append(segment.getBoxNodeIds(n1, n2, n3)) @@ -2337,7 +2332,6 @@ def _generateBoxElements(self, s, n2, mesh, elementtemplate, coordinates, segmen eftList[e3][e1] = eft scalefactorsList[e3][e1] = scalefactors elementtemplate.defineField(coordinates, -1, eft) - elementIdentifier = generateData.nextElementIdentifier() element = mesh.createElement(elementIdentifier, elementtemplate) element.setNodesByIdentifier(eft, nids) if scalefactors: @@ -2359,8 +2353,9 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, scalefactorsList = [None] * elementsCountAround triplePointIndexesList = segment.getTriplePointIndexes() - is6WayTriplePoint = True if (((max(acrossMajorCounts) - 2) // 2) == (min(acrossMajorCounts) - 2) - and (self._segmentsCount == 3)) else False + coreTransitionCount = self._segments[0].getElementsCountTransition() + coreBoxMajorCounts = [count - 2 * coreTransitionCount for count in acrossMajorCounts] + is6WayTriplePoint = (self._segmentsCount == 3) and ((max(coreBoxMajorCounts) // 2) == min(coreBoxMajorCounts)) pSegment = acrossMajorCounts.index(max(acrossMajorCounts)) topMidIndex = (nodesCountAcrossMajor[pSegment] // 2) + (nodesCountAcrossMinor // 2) bottomMidIndex = elementsCountAround - topMidIndex @@ -2370,14 +2365,15 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, nodeLayout8Way = generateData.getNodeLayout8Way() nodeLayoutFlipD2 = generateData.getNodeLayoutFlipD2() nodeLayoutTransition = generateData.getNodeLayoutTransition() - nodeLayoutBifurcationTransition = generateData.getNodeLayoutBifurcationTransition() + nodeLayoutBifurcation = generateData.getNodeLayoutBifurcation() e2 = n2 if self._segmentsIn[s] else 0 - for e1 in range(elementsCountAround): nids, nodeParameters, nodeLayouts = [], [], [] n1p = (e1 + 1) % elementsCountAround oLocation = segment.getTriplePointLocation(e1) + # get identifier early to aid debugging + elementIdentifier = generateData.nextElementIdentifier() # outside of core box for n1 in [e1, n1p]: @@ -2388,6 +2384,7 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, generateData.getNodeLayoutTransitionTriplePoint(oLocation)) nodeLayouts.append(nodeLayoutTransitionTriplePoint if n1 in triplePointIndexesList else nodeLayoutTransition) + for n1 in [e1, n1p]: nid = boxBoundaryNodeIds[n1] nids.append(nid) @@ -2406,13 +2403,12 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, location = 4 if abs(location) == 2 else location nodeLayout = generateData.getNodeLayout6WayTriplePoint(location) else: - nodeLayout = nodeLayoutBifurcationTransition + nodeLayout = nodeLayoutBifurcation elif self._segmentsCount == 4 and self._segmentsIn[s]: # Trifurcation case location = \ 1 if (e1 < elementsCountAround // 4) or (e1 >= 3 * elementsCountAround // 4) else 2 - nodeLayoutTrifurcation = generateData.getNodeLayoutTrifurcation(location) - nodeLayout = nodeLayout6Way if self._sequence == [0, 1, 3, 2] else ( - nodeLayoutTrifurcation) + nodeLayout = (nodeLayout6Way if self._sequence == [0, 1, 3, 2] else + generateData.getNodeLayoutTrifurcation(location)) else: nodeLayout = nodeLayout6Way elif segmentNodesCount == 4: # 8-way node @@ -2459,7 +2455,6 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, eftList[e1] = eft scalefactorsList[e1] = scalefactors elementtemplate.defineField(coordinates, -1, eft) - elementIdentifier = generateData.nextElementIdentifier() element = mesh.createElement(elementIdentifier, elementtemplate) element.setNodesByIdentifier(eft, nids) if scalefactors: From 1a1f092bc54f132991f0e490b33e4d6d38b16750 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Fri, 20 Sep 2024 16:01:10 +1200 Subject: [PATCH 04/20] Control numbers of elements along networks --- src/scaffoldmaker/annotation/body_terms.py | 9 +- .../meshtypes/meshtype_2d_tubenetwork1.py | 12 ++ .../meshtypes/meshtype_3d_boxnetwork1.py | 18 +- .../meshtypes/meshtype_3d_tubenetwork1.py | 16 +- .../meshtypes/meshtype_3d_uterus2.py | 8 +- .../meshtypes/meshtype_3d_wholebody2.py | 183 ++++++++++++++---- src/scaffoldmaker/utils/boxnetworkmesh.py | 9 +- src/scaffoldmaker/utils/networkmesh.py | 20 +- src/scaffoldmaker/utils/tubenetworkmesh.py | 40 ++-- tests/test_network.py | 30 ++- 10 files changed, 266 insertions(+), 79 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index b9383fd9..2b2d9b77 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -7,8 +7,8 @@ ("abdomen", "UBERON:0000916", "ILX:0725977"), ("abdominal cavity", "UBERON:0003684"), ("arm", "UBERON:0001460"), - ("left arm", ""), - ("right arm", ""), + ("left arm", "FMA:24896"), + ("right arm", "FMA:24895"), ("body", "UBERON:0000468", "ILX:0101370"), ("core", ""), ("core boundary", ""), @@ -17,8 +17,9 @@ ("diaphragm", "UBERON:0001103", "ILX:0103194"), ("hand", "FMA:9712"), ("leg", "UBERON:0000978"), - ("left leg", ""), - ("right leg", ""), + ("left leg", "FMA:24981"), + ("right leg", "FMA:24980"), + ("foot", "FMA:9664"), ("neck", "UBERON:0000974", "ILX:0733967"), ("neck core", ""), ("non core", ""), diff --git a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py index 3b3db148..354aef2e 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py @@ -27,6 +27,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): "Number of elements around": 8, "Annotation numbers of elements around": [0], "Target element density along longest segment": 4.0, + "Annotation numbers of elements along": [0], "Show trim surfaces": False } return options @@ -38,6 +39,7 @@ def getOrderedOptionNames(): "Number of elements around", "Annotation numbers of elements around", "Target element density along longest segment", + "Annotation numbers of elements along", "Show trim surfaces" ] @@ -88,6 +90,15 @@ def checkOptions(cls, options): annotationElementsCountsAround[i] = 4 if options["Target element density along longest segment"] < 1.0: options["Target element density along longest segment"] = 1.0 + annotationAlongCounts = options["Annotation numbers of elements along"] + if len(annotationAlongCounts) == 0: + options["Annotation numbers of elements along"] = [0] + else: + for i in range(len(annotationAlongCounts)): + if annotationAlongCounts[i] <= 0: + annotationAlongCounts[i] = 0 + elif annotationAlongCounts[i] < 1: + annotationAlongCounts[i] = 1 return dependentChanges @classmethod @@ -109,6 +120,7 @@ def generateBaseMesh(cls, region, options): defaultElementsCountAround=options["Number of elements around"], elementsCountThroughWall=1, layoutAnnotationGroups=networkLayout.getAnnotationGroups(), + annotationElementsCountsAlong=options["Annotation numbers of elements along"], annotationElementsCountsAround=options["Annotation numbers of elements around"]) tubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_boxnetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_boxnetwork1.py index c7ba938f..20817327 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_boxnetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_boxnetwork1.py @@ -24,7 +24,8 @@ def getParameterSetNames(cls): def getDefaultOptions(cls, parameterSetName="Default"): options = { "Network layout": ScaffoldPackage(MeshType_1d_network_layout1, defaultParameterSetName=parameterSetName), - "Target element density along longest segment": 4.0 + "Target element density along longest segment": 4.0, + "Annotation numbers of elements along": [0] } return options @@ -32,7 +33,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): def getOrderedOptionNames(cls): return [ "Network layout", - "Target element density along longest segment" + "Target element density along longest segment", + "Annotation numbers of elements along" ] @classmethod @@ -70,6 +72,15 @@ def checkOptions(cls, options): options["Network layout"] = cls.getOptionScaffoldPackage("Network layout", MeshType_1d_network_layout1) if options["Target element density along longest segment"] < 1.0: options["Target element density along longest segment"] = 1.0 + annotationAlongCounts = options["Annotation numbers of elements along"] + if len(annotationAlongCounts) == 0: + options["Annotation numbers of elements along"] = [0] + else: + for i in range(len(annotationAlongCounts)): + if annotationAlongCounts[i] <= 0: + annotationAlongCounts[i] = 0 + elif annotationAlongCounts[i] < 1: + annotationAlongCounts[i] = 1 dependentChanges = False return dependentChanges @@ -83,6 +94,7 @@ def generateBaseMesh(cls, region, options): """ networkLayout = options["Network layout"] targetElementDensityAlongLongestSegment = options["Target element density along longest segment"] + annotationElementsCountsAlong = options["Annotation numbers of elements along"] layoutRegion = region.createRegion() networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters @@ -90,7 +102,7 @@ def generateBaseMesh(cls, region, options): networkMesh = networkLayout.getConstructionObject() boxNetworkMeshBuilder = BoxNetworkMeshBuilder( - networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups) + networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups, annotationElementsCountsAlong) boxNetworkMeshBuilder.build() generateData = BoxNetworkMeshGenerateData(region) boxNetworkMeshBuilder.generateMesh(generateData) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py index 63d519e5..02a9ee1f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py @@ -37,9 +37,10 @@ def getDefaultOptions(cls, parameterSetName="Default"): {"scaffoldSettings": {"Define inner coordinates": True}}, defaultParameterSetName=parameterSetName), "Number of elements around": 8, - "Number of elements through shell": 1, "Annotation numbers of elements around": [0], "Target element density along longest segment": 4.0, + "Annotation numbers of elements along": [0], + "Number of elements through shell": 1, "Use linear through shell": False, "Show trim surfaces": False, "Core": False, @@ -54,9 +55,10 @@ def getOrderedOptionNames(): return [ "Network layout", "Number of elements around", - "Number of elements through shell", "Annotation numbers of elements around", "Target element density along longest segment", + "Annotation numbers of elements along", + "Number of elements through shell", "Use linear through shell", "Show trim surfaces", "Core", @@ -183,6 +185,15 @@ def checkOptions(cls, options): if options["Target element density along longest segment"] < 1.0: options["Target element density along longest segment"] = 1.0 + annotationAlongCounts = options["Annotation numbers of elements along"] + if len(annotationAlongCounts) == 0: + options["Annotation numbers of elements along"] = [0] + else: + for i in range(len(annotationAlongCounts)): + if annotationAlongCounts[i] <= 0: + annotationAlongCounts[i] = 0 + elif annotationAlongCounts[i] < 1: + annotationAlongCounts[i] = 1 return dependentChanges @@ -221,6 +232,7 @@ def generateBaseMesh(cls, region, options): defaultElementsCountAround=defaultAroundCount, elementsCountThroughWall=options["Number of elements through shell"], layoutAnnotationGroups=layoutAnnotationGroups, + annotationElementsCountsAlong=options["Annotation numbers of elements along"], annotationElementsCountsAround=annotationAroundCounts, defaultElementsCountAcrossMajor=defaultCoreMajorCount, elementsCountTransition=coreTransitionCount, diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py index 1ac0b69f..d4564242 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py @@ -52,10 +52,12 @@ class UterusTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, defaultElementsCountAround: int, elementsCountThroughWall: int, - layoutAnnotationGroups: list = [], annotationElementsCountsAround: list = []): + layoutAnnotationGroups: list = [], annotationElementsCountsAlong: list = [], + annotationElementsCountsAround: list = []): super(UterusTubeNetworkMeshBuilder, self).__init__( networkMesh, targetElementDensityAlongLongestSegment, defaultElementsCountAround, - elementsCountThroughWall, layoutAnnotationGroups, annotationElementsCountsAround) + elementsCountThroughWall, layoutAnnotationGroups, + annotationElementsCountsAlong, annotationElementsCountsAround) def generateMesh(self, generateData): super(UterusTubeNetworkMeshBuilder, self).generateMesh(generateData) @@ -475,6 +477,7 @@ def generateBaseMesh(cls, region, options): layoutAnnotationGroups = networkLayout.getAnnotationGroups() networkMesh = networkLayout.getConstructionObject() + annotationAlongCounts = [] annotationElementsCountsAround = [] for layoutAnnotationGroup in layoutAnnotationGroups: elementsCountAround = 0 @@ -490,6 +493,7 @@ def generateBaseMesh(cls, region, options): defaultElementsCountAround=options['Number of elements around'], elementsCountThroughWall=options["Number of elements through wall"], layoutAnnotationGroups=layoutAnnotationGroups, + annotationElementsCountsAlong=annotationAlongCounts, annotationElementsCountsAround=annotationElementsCountsAround) uterusTubeNetworkMeshBuilder.build() generateData = UterusTubeNetworkMeshGenerateData( diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 8524c248..db1637b0 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -42,8 +42,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): "6.3-21-22-23-24-25-26,26-27," "6.1-7-8-9," "9-10-11-12-13.1," - "13.2-28-29-30-31-32,32-33,33-34," - "13.3-35-36-37-38-39,39-40,40-41") + "13.2-28-29-30-31-32,32-33-34," + "13.3-35-36-37-38-39,39-40-41") options["Define inner coordinates"] = True options["Head depth"] = 2.1 options["Head length"] = 2.2 @@ -59,8 +59,9 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Hand length"] = 1.5 options["Hand thickness"] = 0.3 options["Hand width"] = 1.0 + options["Thorax length"] = 2.5 + options["Abdomen length"] = 3.0 options["Torso depth"] = 2.5 - options["Torso length"] = 5.5 options["Torso width"] = 3.2 options["Pelvis drop"] = 1.5 options["Pelvis width"] = 2.0 @@ -90,8 +91,9 @@ def getOrderedOptionNames(cls): "Hand length", "Hand thickness", "Hand width", + "Thorax length", + "Abdomen length", "Torso depth", - "Torso length", "Torso width", "Pelvis drop", "Pelvis width", @@ -123,6 +125,8 @@ def checkOptions(cls, options): "Hand width", "Pelvis drop", "Pelvis width", + "Thorax length", + "Abdomen length", "Torso depth", "Torso width", "Leg length", @@ -177,7 +181,8 @@ def generateBaseMesh(cls, region, options): halfHandThickness = 0.5 * options["Hand thickness"] halfHandWidth = 0.5 * options["Hand width"] halfTorsoDepth = 0.5 * options["Torso depth"] - torsoLength = options["Torso length"] + thoraxLength = options["Thorax length"] + abdomenLength = options["Abdomen length"] halfTorsoWidth = 0.5 * options["Torso width"] pelvisDrop = options["Pelvis drop"] halfPelvisWidth = 0.5 * options["Pelvis width"] @@ -200,16 +205,21 @@ def generateBaseMesh(cls, region, options): headGroup = AnnotationGroup(region, get_body_term("head")) neckGroup = AnnotationGroup(region, get_body_term("neck")) armGroup = AnnotationGroup(region, get_body_term("arm")) + armToHandGroup = AnnotationGroup(region, ("arm to hand", "")) leftArmGroup = AnnotationGroup(region, get_body_term("left arm")) rightArmGroup = AnnotationGroup(region, get_body_term("right arm")) handGroup = AnnotationGroup(region, get_body_term("hand")) thoraxGroup = AnnotationGroup(region, get_body_term("thorax")) abdomenGroup = AnnotationGroup(region, get_body_term("abdomen")) legGroup = AnnotationGroup(region, get_body_term("leg")) + legToFootGroup = AnnotationGroup(region, ("leg to foot", "")) leftLegGroup = AnnotationGroup(region, get_body_term("left leg")) rightLegGroup = AnnotationGroup(region, get_body_term("right leg")) - annotationGroups = [bodyGroup, headGroup, neckGroup, armGroup, leftArmGroup, rightArmGroup, handGroup, - thoraxGroup, abdomenGroup, legGroup, leftLegGroup, rightLegGroup] + footGroup = AnnotationGroup(region, get_body_term("foot")) + annotationGroups = [bodyGroup, headGroup, neckGroup, + armGroup, armToHandGroup, leftArmGroup, rightArmGroup, handGroup, + thoraxGroup, abdomenGroup, + legGroup, legToFootGroup, leftLegGroup, rightLegGroup, footGroup] bodyMeshGroup = bodyGroup.getMeshGroup(mesh) elementIdentifier = 1 headElementsCount = 3 @@ -230,6 +240,7 @@ def generateBaseMesh(cls, region, options): right = 1 armElementsCount = 7 armMeshGroup = armGroup.getMeshGroup(mesh) + armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) for side in (left, right): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup @@ -238,12 +249,13 @@ def generateBaseMesh(cls, region, options): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) - if e == (armElementsCount - 1): + if e < (armElementsCount - 1): + armToHandMeshGroup.addElement(element) + else: handMeshGroup.addElement(element) elementIdentifier += 1 thoraxElementsCount = 3 abdomenElementsCount = 4 - torsoElementsCount = thoraxElementsCount + abdomenElementsCount meshGroups = [bodyMeshGroup, thoraxGroup.getMeshGroup(mesh)] for e in range(thoraxElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) @@ -258,6 +270,8 @@ def generateBaseMesh(cls, region, options): elementIdentifier += 1 legElementsCount = 7 legMeshGroup = legGroup.getMeshGroup(mesh) + legToFootMeshGroup = legToFootGroup.getMeshGroup(mesh) + footMeshGroup = footGroup.getMeshGroup(mesh) for side in (left, right): sideLegGroup = leftLegGroup if (side == left) else rightLegGroup meshGroups = [bodyMeshGroup, legMeshGroup, sideLegGroup.getMeshGroup(mesh)] @@ -265,6 +279,10 @@ def generateBaseMesh(cls, region, options): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) + if e < (legElementsCount - 2): + legToFootMeshGroup.addElement(element) + else: + footMeshGroup.addElement(element) elementIdentifier += 1 # set coordinates (outer) @@ -277,7 +295,7 @@ def generateBaseMesh(cls, region, options): d1 = [headScale, 0.0, 0.0] d2 = [0.0, 0.5 * headWidth, 0.0] d3 = [0.0, 0.0, 0.5 * headDepth] - for i in range(headElementsCount + 1): + for i in range(headElementsCount): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, [headScale * i, 0.0, 0.0]) @@ -285,30 +303,47 @@ def generateBaseMesh(cls, region, options): coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) nodeIdentifier += 1 + neckScale = neckLength / neckElementsCount - d1 = [neckScale, 0.0, 0.0] d2 = [0.0, 0.5 * headWidth, 0.0] d3 = [0.0, 0.0, 0.5 * headWidth] - for i in range(1, neckElementsCount): + for i in range(neckElementsCount): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [headLength + neckScale * i, 0.0, 0.0] + d1 = [0.5 * (headScale + neckScale) if (i == 0) else neckScale, 0.0, 0.0] coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) nodeIdentifier += 1 armJunctionNodeIdentifier = nodeIdentifier - torsoScale = torsoLength / torsoElementsCount - d1 = [torsoScale, 0.0, 0.0] + + thoraxScale = thoraxLength / thoraxElementsCount d2 = [0.0, halfTorsoWidth, 0.0] d3 = [0.0, 0.0, halfTorsoDepth] - torsoStartX = headLength + neckLength - sx = [torsoStartX, 0.0, 0.0] - for i in range(torsoElementsCount + 1): + thoraxStartX = headLength + neckLength + sx = [thoraxStartX, 0.0, 0.0] + for i in range(thoraxElementsCount): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - x = [torsoStartX + torsoScale * i, 0.0, 0.0] + x = [thoraxStartX + thoraxScale * i, 0.0, 0.0] + d1 = [0.5 * (neckScale + thoraxScale) if (i == 0) else thoraxScale, 0.0, 0.0] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + nodeIdentifier += 1 + + abdomenScale = abdomenLength / abdomenElementsCount + d2 = [0.0, halfTorsoWidth, 0.0] + d3 = [0.0, 0.0, halfTorsoDepth] + abdomenStartX = thoraxStartX + thoraxLength + for i in range(abdomenElementsCount + 1): + node = nodes.findNodeByIdentifier(nodeIdentifier) + fieldcache.setNode(node) + x = [abdomenStartX + abdomenScale * i, 0.0, 0.0] + d1 = [0.5 * (thoraxScale + abdomenScale) if (i == 0) else abdomenScale, 0.0, 0.0] coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) @@ -318,7 +353,7 @@ def generateBaseMesh(cls, region, options): px = x # arms - armStartX = torsoStartX + armAngleDrop + armStartX = thoraxStartX + armAngleDrop nonHandArmLength = armLength - handLength armScale = nonHandArmLength / (armElementsCount - 3) sd3 = [0.0, 0.0, armTopRadius] @@ -375,7 +410,7 @@ def generateBaseMesh(cls, region, options): # legs cos45 = math.cos(0.25 * math.pi) - legStartX = torsoStartX + torsoLength + pelvisDrop + legStartX = abdomenStartX + abdomenLength + pelvisDrop legScale = legLength / (legElementsCount - 2) pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] # GRC for side in (left, right): @@ -481,28 +516,49 @@ def getDefaultOptions(cls, parameterSetName="Default"): useParameterSetName = "Human 1 Coarse" if (parameterSetName == "Default") else parameterSetName options["Base parameter set"] = useParameterSetName options["Body network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) + options["Number of elements along head"] = 2 + options["Number of elements along neck"] = 1 + options["Number of elements along thorax"] = 2 + options["Number of elements along abdomen"] = 2 + options["Number of elements along arm to hand"] = 4 + options["Number of elements along hand"] = 1 + options["Number of elements along leg to foot"] = 4 + options["Number of elements along foot"] = 2 options["Number of elements around head"] = 12 options["Number of elements around torso"] = 12 options["Number of elements around arm"] = 8 options["Number of elements around leg"] = 8 options["Number of elements through shell"] = 1 - options["Target element density along longest segment"] = 5.0 options["Show trim surfaces"] = False options["Use Core"] = True options["Number of elements across core box minor"] = 2 options["Number of elements across core transition"] = 1 if "Medium" in useParameterSetName: + options["Number of elements along head"] = 3 + options["Number of elements along neck"] = 2 + options["Number of elements along thorax"] = 3 + options["Number of elements along abdomen"] = 3 + options["Number of elements along arm to hand"] = 6 + options["Number of elements along hand"] = 1 + options["Number of elements along leg to foot"] = 6 + options["Number of elements along foot"] = 2 options["Number of elements around head"] = 16 options["Number of elements around torso"] = 16 options["Number of elements around leg"] = 12 - options["Target element density along longest segment"] = 8.0 elif "Fine" in useParameterSetName: + options["Number of elements along head"] = 4 + options["Number of elements along neck"] = 2 + options["Number of elements along thorax"] = 4 + options["Number of elements along abdomen"] = 4 + options["Number of elements along arm to hand"] = 8 + options["Number of elements along hand"] = 2 + options["Number of elements along leg to foot"] = 8 + options["Number of elements along foot"] = 3 options["Number of elements around head"] = 24 options["Number of elements around torso"] = 24 options["Number of elements around arm"] = 12 options["Number of elements around leg"] = 16 options["Number of elements through shell"] = 1 - options["Target element density along longest segment"] = 10.0 options["Number of elements across core box minor"] = 4 return options @@ -511,12 +567,19 @@ def getDefaultOptions(cls, parameterSetName="Default"): def getOrderedOptionNames(cls): optionNames = [ "Body network layout", + "Number of elements along head", + "Number of elements along neck", + "Number of elements along thorax", + "Number of elements along abdomen", + "Number of elements along arm to hand", + "Number of elements along hand", + "Number of elements along leg to foot", + "Number of elements along foot", "Number of elements around head", "Number of elements around torso", "Number of elements around arm", "Number of elements around leg", "Number of elements through shell", - "Target element density along longest segment", "Show trim surfaces", "Use Core", "Number of elements across core box minor", @@ -550,6 +613,18 @@ def checkOptions(cls, options): dependentChanges = False if not options["Body network layout"].getScaffoldType() in cls.getOptionValidScaffoldTypes("Body network layout"): options["Body network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) + for key in [ + "Number of elements along head", + "Number of elements along neck", + "Number of elements along thorax", + "Number of elements along abdomen", + "Number of elements along arm to hand", + "Number of elements along hand", + "Number of elements along leg to foot", + "Number of elements along foot" + ]: + if options[key] < 1: + options[key] = 1 minElementsCountAround = None for key in [ "Number of elements around head", @@ -567,9 +642,6 @@ def checkOptions(cls, options): if options["Number of elements through shell"] < 0: options["Number of elements through shell"] = 1 - if options["Target element density along longest segment"] < 1.0: - options["Target element density along longest segment"] = 1.0 - if options["Number of elements across core transition"] < 1: options["Number of elements across core transition"] = 1 @@ -596,41 +668,74 @@ def generateBaseMesh(cls, region, options): :return: list of AnnotationGroup, None """ # parameterSetName = options['Base parameter set'] - layoutRegion = region.createRegion() networkLayout = options["Body network layout"] + elementsCountAlongHead = options["Number of elements along head"] + elementsCountAlongNeck = options["Number of elements along neck"] + elementsCountAlongThorax = options["Number of elements along thorax"] + elementsCountAlongAbdomen = options["Number of elements along abdomen"] + elementsCountAlongArmToHand = options["Number of elements along arm to hand"] + elementsCountAlongHand = options["Number of elements along hand"] + elementsCountAlongLegToFoot = options["Number of elements along leg to foot"] + elementsCountAlongFoot = options["Number of elements along foot"] + elementsCountAroundHead = options["Number of elements around head"] + elementsCountAroundTorso = options["Number of elements around torso"] + elementsCountAroundArm = options["Number of elements around arm"] + elementsCountAroundLeg = options["Number of elements around leg"] + coreBoxMinorCount = options["Number of elements across core box minor"] + coreTransitionCount = options['Number of elements across core transition'] + + layoutRegion = region.createRegion() networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters layoutAnnotationGroups = networkLayout.getAnnotationGroups() networkMesh = networkLayout.getConstructionObject() - coreBoxMinorCount = options["Number of elements across core box minor"] - coreTransitionCount = options['Number of elements across core transition'] + annotationAlongCounts = [] annotationAroundCounts = [] # implementation currently uses major count including transition annotationCoreMajorCounts = [] for layoutAnnotationGroup in layoutAnnotationGroups: + alongCount = 0 aroundCount = 0 coreMajorCount = 0 name = layoutAnnotationGroup.getName() - if ("head" in name) or ("neck" in name): - aroundCount = options["Number of elements around head"] - elif ("abdomen" in name) or ("thorax" in name) or ("torso" in name): - aroundCount = options["Number of elements around torso"] - elif "arm" in name: - aroundCount = options["Number of elements around arm"] - elif "leg" in name: - aroundCount = options["Number of elements around leg"] + if "head" in name: + alongCount = elementsCountAlongHead + aroundCount = elementsCountAroundHead + elif "neck" in name: + alongCount = elementsCountAlongNeck + aroundCount = elementsCountAroundHead + elif "thorax" in name: + alongCount = elementsCountAlongThorax + aroundCount = elementsCountAroundTorso + elif "abdomen" in name: + alongCount = elementsCountAlongAbdomen + aroundCount = elementsCountAroundTorso + elif "arm to hand" in name: + alongCount = elementsCountAlongArmToHand + aroundCount = elementsCountAroundArm + elif "hand" in name: + alongCount = elementsCountAlongHand + aroundCount = elementsCountAroundArm + elif "leg to foot" in name: + alongCount = elementsCountAlongLegToFoot + aroundCount = elementsCountAroundLeg + elif "foot" in name: + alongCount = elementsCountAlongFoot + aroundCount = elementsCountAroundLeg if aroundCount: coreMajorCount = aroundCount // 2 - coreBoxMinorCount + 2 * coreTransitionCount + annotationAlongCounts.append(alongCount) annotationAroundCounts.append(aroundCount) annotationCoreMajorCounts.append(coreMajorCount) isCore = options["Use Core"] tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( networkMesh, - targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], + targetElementDensityAlongLongestSegment=2.0, # not used for body defaultElementsCountAround=options["Number of elements around head"], elementsCountThroughWall=options["Number of elements through shell"], layoutAnnotationGroups=layoutAnnotationGroups, + annotationElementsCountsAlong=annotationAlongCounts, annotationElementsCountsAround=annotationAroundCounts, defaultElementsCountAcrossMajor=annotationCoreMajorCounts[-1], elementsCountTransition=coreTransitionCount, diff --git a/src/scaffoldmaker/utils/boxnetworkmesh.py b/src/scaffoldmaker/utils/boxnetworkmesh.py index b1a6687a..969664b9 100644 --- a/src/scaffoldmaker/utils/boxnetworkmesh.py +++ b/src/scaffoldmaker/utils/boxnetworkmesh.py @@ -91,8 +91,9 @@ def __init__(self, networkSegment, pathParametersList): super(BoxNetworkMeshSegment, self).__init__(networkSegment, pathParametersList) self._sampledBoxCoordinates = None - def sample(self, targetElementLength): - elementsCountAlong = max(1, math.ceil(self._length / targetElementLength)) + def sample(self, fixedElementsCountAlong, targetElementLength): + elementsCountAlong = (fixedElementsCountAlong if fixedElementsCountAlong else + max(1, math.ceil(self._length / targetElementLength))) if self._isLoop and (elementsCountAlong < 2): elementsCountAlong = 2 pathParameters = self._pathParametersList[0] @@ -229,9 +230,9 @@ def generateMesh(self, generateData: BoxNetworkMeshGenerateData): class BoxNetworkMeshBuilder(NetworkMeshBuilder): def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, - layoutAnnotationGroups: list = []): + layoutAnnotationGroups: list = [], annotationElementsCountsAlong: list = []): super(BoxNetworkMeshBuilder, self).__init__( - networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups) + networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups, annotationElementsCountsAlong) def createSegment(self, networkSegment): """ diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index dd738bae..c4e651ee 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -597,9 +597,11 @@ def isLoop(self): return self._isLoop @abstractmethod - def sample(self, targetElementLength): + def sample(self, fixedElementsCountAlong, targetElementLength): """ Override to resample curve/raw data to final coordinates. + :param fixedElementsCountAlong: Fixed number of elements along > 0 or None to use targetElementLength. + Implementations may enforce a higher minimum number. :param targetElementLength: Target element size along length of segment/junction. """ pass @@ -673,10 +675,11 @@ class NetworkMeshBuilder(ABC): """ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, - layoutAnnotationGroups): + layoutAnnotationGroups, annotationElementsCountsAlong=[]): self._networkMesh = networkMesh self._targetElementDensityAlongLongestSegment = targetElementDensityAlongLongestSegment self._layoutAnnotationGroups = layoutAnnotationGroups + self._annotationElementsCountsAlong = annotationElementsCountsAlong self._layoutRegion = networkMesh.getRegion() layoutFieldmodule = self._layoutRegion.getFieldmodule() self._layoutNodes = layoutFieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) @@ -753,8 +756,19 @@ def _sampleSegments(self): Must have called self.createJunctions() first. """ for networkSegment in self._networkMesh.getNetworkSegments(): + fixedElementsCountAlong = None + i = 0 + for layoutAnnotationGroup in self._layoutAnnotationGroups: + if i >= len(self._annotationElementsCountsAlong): + break + if self._annotationElementsCountsAlong[i] > 0: + if networkSegment.hasLayoutElementsInMeshGroup( + layoutAnnotationGroup.getMeshGroup(self._layoutMesh)): + fixedElementsCountAlong = self._annotationElementsCountsAlong[i] + break + i += 1 segment = self._segments[networkSegment] - segment.sample(self._targetElementLength) + segment.sample(fixedElementsCountAlong, self._targetElementLength) def _sampleJunctions(self): """ diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 1eff5d54..15258ab8 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -249,18 +249,18 @@ def getElementsCountAcrossTransition(self): def getRawTrackSurface(self, pathIndex=0): return self._rawTrackSurfaceList[pathIndex] - def sample(self, targetElementLength): + def sample(self, fixedElementsCountAlong, 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 + elementsCountAlong = fixedElementsCountAlong 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: + if p == 0: elementsCountAlong = len(self._sampledTubeCoordinates[0][0]) - 1 if self._dimension == 2: @@ -2644,12 +2644,29 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): class TubeNetworkMeshBuilder(NetworkMeshBuilder): def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, - defaultElementsCountAround: int, elementsCountThroughWall: int, - layoutAnnotationGroups: list = [], annotationElementsCountsAround: list = [], + defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], + annotationElementsCountsAlong: list = [], annotationElementsCountsAround: list = [], defaultElementsCountAcrossMajor: int = 4, elementsCountTransition: int = 1, annotationElementsCountsAcrossMajor: list = [], isCore=False): + """ + :param networkMesh: Description of the topology of the network layout. + :param targetElementDensityAlongLongestSegment: + :param defaultElementsCountAround: + :param elementsCountThroughWall: + :param layoutAnnotationGroups: + :param annotationElementsCountsAlong: List in same order as layoutAnnotationGroups, specifying fixed + number along segment with any elements in the annotation group. Client must ensure exclusive map from segments. + Groups with zero value or past end of this list use the targetElementDensityAlongLongestSegment. + :param annotationElementsCountsAround: List in same order as layoutAnnotationGroups, specifying fixed + number around segment with any elements in the annotation group. Client must ensure exclusive map from segments. + Groups with zero value or past end of this list use the defaultElementsCountAround. + :param defaultElementsCountAcrossMajor: + :param elementsCountTransition: + :param annotationElementsCountsAcrossMajor: + :param isCore: + """ super(TubeNetworkMeshBuilder, self).__init__( - networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups) + networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups, annotationElementsCountsAlong) self._defaultElementsCountAround = defaultElementsCountAround self._elementsCountThroughWall = elementsCountThroughWall self._layoutAnnotationGroups = layoutAnnotationGroups @@ -2815,7 +2832,8 @@ def resampleTubeCoordinates(rawTubeCoordinates, fixedElementsCountAlong=None, :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 minimumElementsCountAlong: Minimum number along to apply regardless of fixedElementsCountAlong or number + calculated from targetElementLength. :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), @@ -2863,11 +2881,9 @@ def resampleTubeCoordinates(rawTubeCoordinates, fixedElementsCountAlong=None, 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)) + # small fudge factor on targetElementLength so whole numbers chosen on centroid don't go one higher: + elementsCountAlong = max(minimumElementsCountAlong, fixedElementsCountAlong if fixedElementsCountAlong else + math.ceil(meanLength * 0.999 / targetElementLength)) meanStartLocation /= elementsCountAround e = min(int(meanStartLocation), pointsCountAlong - 2) meanStartCurveLocation = (e, meanStartLocation - e) diff --git a/tests/test_network.py b/tests/test_network.py index 5281f2cf..1108095f 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -89,10 +89,11 @@ def test_2d_tube_network_bifurcation(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertFalse(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(5, len(settings)) + self.assertEqual(6, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) settings["Target element density along longest segment"] = 3.4 MeshType_2d_tubenetwork1.checkOptions(settings) @@ -134,10 +135,11 @@ def test_2d_tube_network_sphere_cube(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertFalse(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(5, len(settings)) + self.assertEqual(6, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) context = Context("Test") region = context.getDefaultRegion() @@ -204,10 +206,11 @@ def test_2d_tube_network_trifurcation(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertFalse(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(5, len(settings)) + self.assertEqual(6, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) MeshType_2d_tubenetwork1.checkOptions(settings) context = Context("Test") @@ -248,11 +251,12 @@ def test_3d_tube_network_bifurcation(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(11, len(settings)) + self.assertEqual(12, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) self.assertFalse(settings["Use linear through shell"]) self.assertFalse(settings["Show trim surfaces"]) self.assertFalse(settings["Core"]) @@ -319,11 +323,12 @@ def test_3d_tube_network_bifurcation_core(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(11, len(settings)) + self.assertEqual(12, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) self.assertFalse(settings["Use linear through shell"]) self.assertFalse(settings["Show trim surfaces"]) self.assertFalse(settings["Core"]) @@ -382,11 +387,12 @@ def test_3d_tube_network_sphere_cube(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(11, len(settings)) + self.assertEqual(12, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) self.assertFalse(settings["Use linear through shell"]) self.assertFalse(settings["Show trim surfaces"]) settings["Number of elements through shell"] = 2 @@ -475,11 +481,12 @@ def test_3d_tube_network_sphere_cube_core(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(11, len(settings)) + self.assertEqual(12, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) self.assertFalse(settings["Use linear through shell"]) self.assertFalse(settings["Show trim surfaces"]) self.assertFalse(settings["Core"]) @@ -565,11 +572,12 @@ def test_3d_tube_network_trifurcation_cross(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(11, len(settings)) + self.assertEqual(12, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) self.assertFalse(settings["Use linear through shell"]) self.assertFalse(settings["Show trim surfaces"]) settings["Annotation numbers of elements around"] = [10] # requires annotation group below @@ -659,11 +667,12 @@ def test_3d_tube_network_trifurcation_cross_core(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(11, len(settings)) + self.assertEqual(12, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) self.assertFalse(settings["Use linear through shell"]) self.assertFalse(settings["Show trim surfaces"]) self.assertFalse(settings["Core"]) @@ -748,8 +757,9 @@ def test_3d_box_network_bifurcation(self): settings = scaffoldPackage.getScaffoldSettings() networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() - self.assertEqual(2, len(settings)) + self.assertEqual(3, len(settings)) self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) context = Context("Test") region = context.getDefaultRegion() From 1fd4773ca460a8b296be32383ce61e980f371eab Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Mon, 23 Sep 2024 11:53:08 +1200 Subject: [PATCH 05/20] Improve shoulder rotation effect --- .../meshtypes/meshtype_3d_wholebody2.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index db1637b0..ac46a3f7 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -172,7 +172,6 @@ def generateBaseMesh(cls, region, options): shoulderDrop = options["Shoulder drop"] halfShoulderWidth = 0.5 * options["Shoulder width"] armAngleRadians = math.radians(options["Arm lateral angle degrees"]) - armAngleDrop = shoulderDrop * math.cos(armAngleRadians) armLength = options["Arm length"] armTopRadius = 0.5 * options["Arm top diameter"] halfWristThickness = 0.5 * options["Wrist thickness"] @@ -353,7 +352,14 @@ def generateBaseMesh(cls, region, options): px = x # arms - armStartX = thoraxStartX + armAngleDrop + # rotate shoulder with arm, pivoting about shoulder drop below arm junction on network + # this has the realistic effect of shoulders becoming narrower with higher angles + # initial shoulder rotation with arm is negligible, hence: + shoulderRotationFactor = 1.0 - math.cos(0.5 * armAngleRadians) + # assume shoulder drop is half shrug distance to get limiting shoulder angle for 180 degree arm rotation + shoulderLimitAngleRadians = math.asin(2.0 * shoulderDrop / halfShoulderWidth) + shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians + armStartX = thoraxStartX + shoulderDrop - halfShoulderWidth * math.sin(shoulderAngleRadians) nonHandArmLength = armLength - handLength armScale = nonHandArmLength / (armElementsCount - 3) sd3 = [0.0, 0.0, armTopRadius] @@ -362,7 +368,7 @@ def generateBaseMesh(cls, region, options): armAngle = armAngleRadians if (side == left) else -armAngleRadians cosArmAngle = math.cos(armAngle) sinArmAngle = math.sin(armAngle) - armStartY = halfShoulderWidth if (side == left) else -halfShoulderWidth + armStartY = (halfShoulderWidth if (side == left) else -halfShoulderWidth) * math.cos(shoulderAngleRadians) x = [armStartX, armStartY, 0.0] d1 = [armScale * cosArmAngle, armScale * sinArmAngle, 0.0] # set leg versions 2 (left) and 3 (right) on leg junction node, and intermediate shoulder node From 4a3ca8115254c50ffbdd2f110469f40dc5f3d37e Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Wed, 25 Sep 2024 22:25:05 +1200 Subject: [PATCH 06/20] Add body internal annotations --- src/scaffoldmaker/annotation/body_terms.py | 7 + .../meshtypes/meshtype_1d_network_layout1.py | 7 +- .../meshtypes/meshtype_3d_wholebody2.py | 353 ++++++++++++++---- src/scaffoldmaker/utils/tubenetworkmesh.py | 148 +++++++- 4 files changed, 439 insertions(+), 76 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index 2b2d9b77..1c61ec59 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -6,16 +6,20 @@ body_terms = [ ("abdomen", "UBERON:0000916", "ILX:0725977"), ("abdominal cavity", "UBERON:0003684"), + ("abdominal cavity boundary", ""), + ("abdominopelvic cavity", "UBERON:0035819"), ("arm", "UBERON:0001460"), ("left arm", "FMA:24896"), ("right arm", "FMA:24895"), ("body", "UBERON:0000468", "ILX:0101370"), ("core", ""), ("core boundary", ""), + ("dorsal", ""), ("head", "UBERON:0000033", "ILX:0104909"), ("head core", ""), ("diaphragm", "UBERON:0001103", "ILX:0103194"), ("hand", "FMA:9712"), + ("left", ""), ("leg", "UBERON:0000978"), ("left leg", "FMA:24981"), ("right leg", "FMA:24980"), @@ -23,11 +27,14 @@ ("neck", "UBERON:0000974", "ILX:0733967"), ("neck core", ""), ("non core", ""), + ("right", ""), ("shell", ""), ("skin epidermis", "UBERON:0001003", "ILX:0728574"), ("spinal cord", "UBERON:0002240", "ILX:0110909"), ("thoracic cavity", "UBERON:0002224"), + ("thoracic cavity boundary", ""), ("thorax", "ILX:0742178"), + ("ventral", "") ] def get_body_term(name : str): diff --git a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py index afb755f1..7baf8723 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py @@ -163,7 +163,7 @@ def generateBaseMesh(cls, region, options): return [], networkMesh @classmethod - def defineInnerCoordinates(cls, region, coordinates, options, networkMesh): + def defineInnerCoordinates(cls, region, coordinates, options, networkMesh, innerProportion=0.8): """ Copy coordinates to inner coordinates via in-memory model file. Assign using the interactive function. @@ -171,6 +171,7 @@ def defineInnerCoordinates(cls, region, coordinates, options, networkMesh): :param coordinates: Standard/outer coordinate field. :param options: Options used to generate scaffold. :param networkMesh: Network mesh object used to generate scaffold. + :param innerProportion: Proportion of outer coordinates to assign to inner, typically 0.0 < p < 1.0. """ assert options["Define inner coordinates"] coordinates.setName("inner coordinates") # temporarily rename @@ -186,8 +187,8 @@ def defineInnerCoordinates(cls, region, coordinates, options, networkMesh): "To field": {"coordinates": False, "inner coordinates": True}, "From field": {"coordinates": True, "inner coordinates": False}, "Mode": {"Scale": True, "Offset": False}, - "D2 value": 0.8, - "D3 value": 0.8} + "D2 value": innerProportion, + "D3 value": innerProportion} cls.assignCoordinates(region, options, networkMesh, functionOptions, editGroupName=None) @classmethod diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index ac46a3f7..d6e503a5 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -3,14 +3,13 @@ """ from cmlibs.maths.vectorops import add, cross, mult, set_magnitude from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates -from cmlibs.utils.zinc.general import ChangeManager -from cmlibs.zinc.element import Element from cmlibs.zinc.node import Node +from scaffoldmaker.annotation.annotationgroup import ( + AnnotationGroup, findOrCreateAnnotationGroupForTerm, getAnnotationGroupForTerm) +from scaffoldmaker.annotation.body_terms import get_body_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.annotation.annotationgroup import AnnotationGroup, findOrCreateAnnotationGroupForTerm -from scaffoldmaker.annotation.body_terms import get_body_term from scaffoldmaker.utils.interpolation import ( computeCubicHermiteEndDerivative, interpolateLagrangeHermiteDerivative, sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine) @@ -45,9 +44,9 @@ def getDefaultOptions(cls, parameterSetName="Default"): "13.2-28-29-30-31-32,32-33-34," "13.3-35-36-37-38-39,39-40-41") options["Define inner coordinates"] = True - options["Head depth"] = 2.1 + options["Head depth"] = 2.0 options["Head length"] = 2.2 - options["Head width"] = 1.9 + options["Head width"] = 2.0 options["Neck length"] = 1.3 options["Shoulder drop"] = 0.7 options["Shoulder width"] = 4.5 @@ -72,6 +71,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Foot length"] = 2.5 options["Foot thickness"] = 0.3 options["Foot width"] = 1.0 + options["Inner proportion default"] = 0.7 + options["Inner proportion head"] = 0.35 return options @classmethod @@ -103,7 +104,9 @@ def getOrderedOptionNames(cls): "Leg bottom diameter", "Foot length", "Foot thickness", - "Foot width" + "Foot width", + "Inner proportion default", + "Inner proportion head" ] @classmethod @@ -138,6 +141,14 @@ def checkOptions(cls, options): ]: if options[key] < 0.1: options[key] = 0.1 + for key in [ + "Inner proportion default", + "Inner proportion head" + ]: + if options[key] < 0.1: + options[key] = 0.1 + elif options[key] > 0.9: + options[key] = 0.9 for key in [ "Arm lateral angle degrees" ]: @@ -165,9 +176,9 @@ def generateBaseMesh(cls, region, options): """ # parameterSetName = options['Base parameter set'] structure = options["Structure"] - headDepth = options["Head depth"] + halfHeadDepth = 0.5 * options["Head depth"] headLength = options["Head length"] - headWidth = options["Head width"] + halfHeadWidth = 0.5 * options["Head width"] neckLength = options["Neck length"] shoulderDrop = options["Shoulder drop"] halfShoulderWidth = 0.5 * options["Shoulder width"] @@ -192,6 +203,8 @@ def generateBaseMesh(cls, region, options): footLength = options["Foot length"] halfFootThickness = 0.5 * options["Foot thickness"] halfFootWidth = 0.5 * options["Foot width"] + innerProportionDefault = options["Inner proportion default"] + innerProportionHead = options["Inner proportion head"] networkMesh = NetworkMesh(structure) networkMesh.create1DLayoutMesh(region) @@ -286,70 +299,81 @@ def generateBaseMesh(cls, region, options): # set coordinates (outer) fieldcache = fieldmodule.createFieldcache() - coordinates = find_or_create_field_coordinates(fieldmodule).castFiniteElement() + coordinates = find_or_create_field_coordinates(fieldmodule) + # need to ensure inner coordinates are at least defined: + cls.defineInnerCoordinates(region, coordinates, options, networkMesh, innerProportion=0.75) + innerCoordinates = find_or_create_field_coordinates(fieldmodule, "inner coordinates") nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) headScale = headLength / headElementsCount nodeIdentifier = 1 d1 = [headScale, 0.0, 0.0] - d2 = [0.0, 0.5 * headWidth, 0.0] - d3 = [0.0, 0.0, 0.5 * headDepth] + d2 = [0.0, halfHeadWidth, 0.0] + d3 = [0.0, 0.0, halfHeadDepth] + id2 = mult(d2, innerProportionHead) + id3 = mult(d3, innerProportionHead) for i in range(headElementsCount): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, [headScale * i, 0.0, 0.0]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + x = [headScale * i, 0.0, 0.0] + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 neckScale = neckLength / neckElementsCount - d2 = [0.0, 0.5 * headWidth, 0.0] - d3 = [0.0, 0.0, 0.5 * headWidth] + d2 = [0.0, halfHeadWidth, 0.0] + d3 = [0.0, 0.0, halfHeadWidth] + id2 = mult(d2, innerProportionHead) + id3 = mult(d3, innerProportionHead) for i in range(neckElementsCount): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [headLength + neckScale * i, 0.0, 0.0] d1 = [0.5 * (headScale + neckScale) if (i == 0) else neckScale, 0.0, 0.0] - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 armJunctionNodeIdentifier = nodeIdentifier thoraxScale = thoraxLength / thoraxElementsCount d2 = [0.0, halfTorsoWidth, 0.0] - d3 = [0.0, 0.0, halfTorsoDepth] thoraxStartX = headLength + neckLength sx = [thoraxStartX, 0.0, 0.0] for i in range(thoraxElementsCount): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [thoraxStartX + thoraxScale * i, 0.0, 0.0] - d1 = [0.5 * (neckScale + thoraxScale) if (i == 0) else thoraxScale, 0.0, 0.0] - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + if i == 0: + d1 = [0.5 * (neckScale + thoraxScale), 0.0, 0.0] + d3 = [0.0, 0.0, 0.5 * (halfHeadWidth + halfTorsoDepth)] + id2 = mult(d2, 0.5 * (innerProportionHead + innerProportionDefault)) + id3 = mult(d3, 0.5 * (innerProportionHead + innerProportionDefault)) + else: + d1 = [thoraxScale, 0.0, 0.0] + d3 = [0.0, 0.0, halfTorsoDepth] + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 abdomenScale = abdomenLength / abdomenElementsCount d2 = [0.0, halfTorsoWidth, 0.0] d3 = [0.0, 0.0, halfTorsoDepth] + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) abdomenStartX = thoraxStartX + thoraxLength + px = None for i in range(abdomenElementsCount + 1): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [abdomenStartX + abdomenScale * i, 0.0, 0.0] d1 = [0.5 * (thoraxScale + abdomenScale) if (i == 0) else abdomenScale, 0.0, 0.0] - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 legJunctionNodeIdentifier = nodeIdentifier - 1 - px = x + px = [abdomenStartX + abdomenLength, 0.0, 0.0] # arms # rotate shoulder with arm, pivoting about shoulder drop below arm junction on network @@ -363,7 +387,9 @@ def generateBaseMesh(cls, region, options): nonHandArmLength = armLength - handLength armScale = nonHandArmLength / (armElementsCount - 3) sd3 = [0.0, 0.0, armTopRadius] + sid3 = mult(sd3, innerProportionDefault) hd3 = [0.0, 0.0, halfHandWidth] + hid3 = mult(hd3, innerProportionDefault) for side in (left, right): armAngle = armAngleRadians if (side == left) else -armAngleRadians cosArmAngle = math.cos(armAngle) @@ -380,12 +406,13 @@ def generateBaseMesh(cls, region, options): version = 1 if (n > 0) else 2 if (side == left) else 3 sd1 = nd1[n] sd2 = set_magnitude(cross(sd3, sd1), armTopRadius) + sid2 = mult(sd2, innerProportionDefault) if n > 0: - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, version, nx[n]) + for field in (coordinates, innerCoordinates): + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, nx[n]) nodeIdentifier += 1 - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, sd1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, sd2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, sd3) + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) # main part of arm to wrist for i in range(armElementsCount - 2): xi = i / (armElementsCount - 3) @@ -396,29 +423,28 @@ def generateBaseMesh(cls, region, options): halfWidth = xi * halfWristWidth + (1.0 - xi) * armTopRadius d2 = [-halfThickness * sinArmAngle, halfThickness * cosArmAngle, 0.0] d3 = [0.0, 0.0, halfWidth] - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 # hand node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - h = (armElementsCount - 3) + handLength / armScale hx = [armStartX + armLength * cosArmAngle, armStartY + armLength * sinArmAngle, 0.0] hd1 = computeCubicHermiteEndDerivative(x, d1, hx, d1) hd2 = set_magnitude(d2, halfHandThickness) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, hx) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, hd1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, hd2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, hd3) + hid2 = mult(hd2, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, hx, hd1, hd2, hd3) + setNodeFieldParameters(innerCoordinates, fieldcache, hx, hd1, hid2, hid3) nodeIdentifier += 1 # legs cos45 = math.cos(0.25 * math.pi) legStartX = abdomenStartX + abdomenLength + pelvisDrop legScale = legLength / (legElementsCount - 2) - pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] # GRC + pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] + pid3 = mult(pd3, innerProportionDefault) for side in (left, right): legAngle = legAngleRadians if (side == left) else -legAngleRadians cosLegAngle = math.cos(legAngle) @@ -434,10 +460,10 @@ def generateBaseMesh(cls, region, options): fieldcache.setNode(node) pd1 = interpolateLagrangeHermiteDerivative(px, x, d1, 0.0) pd2 = set_magnitude(cross(pd3, pd1), 0.5 * legTopRadius + 0.5 * halfTorsoWidth) # GRC + pid2 = mult(pd2, innerProportionDefault) version = 2 if (side == left) else 3 - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, pd1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, pd2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, pd3) + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, pd1, pd2, pd3) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, pd1, pid2, pid3) # main part of leg to ankle for i in range(legElementsCount - 2): xi = i / (legElementsCount - 2) @@ -447,10 +473,10 @@ def generateBaseMesh(cls, region, options): radius = xi * legBottomRadius + (1.0 - xi) * legTopRadius d2 = mult(legSide, radius) d3 = [0.0, 0.0, radius] - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + id2 = mult(d2, innerProportionDefault) + id3 = mult(d3, innerProportionDefault) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 # foot heelOffset = legBottomRadius * (1.0 - cos45) @@ -470,10 +496,10 @@ def generateBaseMesh(cls, region, options): for i in range(2): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, fx[i]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, fd1[i]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, fd2[i]) - coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, fd3[i]) + setNodeFieldParameters(coordinates, fieldcache, fx[i], fd1[i], fd2[i], fd3[i]) + fid2 = mult(fd2[i], innerProportionDefault) + fid3 = mult(fd3[i], innerProportionDefault) + setNodeFieldParameters(innerCoordinates, fieldcache, fx[i], fd1[i], fid2, fid3) nodeIdentifier += 1 smoothOptions = { @@ -481,8 +507,11 @@ def generateBaseMesh(cls, region, options): "Smooth D12": True, "Smooth D13": True} cls.smoothSideCrossDerivatives(region, options, networkMesh, smoothOptions, None) - - cls.defineInnerCoordinates(region, coordinates, options, networkMesh) + smoothOptions = { + "Field": {"coordinates": False, "inner coordinates": True}, + "Smooth D12": True, + "Smooth D13": True} + cls.smoothSideCrossDerivatives(region, options, networkMesh, smoothOptions, None) return annotationGroups, networkMesh @@ -498,6 +527,101 @@ def getInteractiveFunctions(cls): break return interactiveFunctions + +class WholeBodyTubeNetworkMeshGenerateData(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(WholeBodyTubeNetworkMeshGenerateData, self).__init__( + region, meshDimension, isLinearThroughWall, isShowTrimSurfaces, + coordinateFieldName, startNodeIdentifier, startElementIdentifier) + # annotation groups are created on demand: + self._coreGroup = None + self._shellGroup = None + self._leftGroup = None + self._rightGroup = None + self._dorsalGroup = None + self._ventralGroup = None + + def getCoreMeshGroup(self): + if not self._coreGroup: + self._coreGroup = self.getOrCreateAnnotationGroup(("core", "")) + return self._coreGroup.getMeshGroup(self._mesh) + + def getShellMeshGroup(self): + if not self._shellGroup: + self._shellGroup = self.getOrCreateAnnotationGroup(("shell", "")) + return self._shellGroup.getMeshGroup(self._mesh) + + def getLeftMeshGroup(self): + if not self._leftGroup: + self._leftGroup = self.getOrCreateAnnotationGroup(("left", "")) + return self._leftGroup.getMeshGroup(self._mesh) + + def getRightMeshGroup(self): + if not self._rightGroup: + self._rightGroup = self.getOrCreateAnnotationGroup(("right", "")) + return self._rightGroup.getMeshGroup(self._mesh) + + def getDorsalMeshGroup(self): + if not self._dorsalGroup: + self._dorsalGroup = self.getOrCreateAnnotationGroup(("dorsal", "")) + return self._dorsalGroup.getMeshGroup(self._mesh) + + def getVentralMeshGroup(self): + if not self._ventralGroup: + self._ventralGroup = self.getOrCreateAnnotationGroup(("ventral", "")) + return self._ventralGroup.getMeshGroup(self._mesh) + + +class WholeBodyNetworkMeshBuilder(TubeNetworkMeshBuilder): + + def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, + defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], + annotationElementsCountsAlong: list = [], annotationElementsCountsAround: list = [], + defaultElementsCountAcrossMajor: int = 4, elementsCountTransition: int = 1, + annotationElementsCountsAcrossMajor: list = [], isCore=False): + super(WholeBodyNetworkMeshBuilder, self).__init__( + networkMesh, targetElementDensityAlongLongestSegment, defaultElementsCountAround, + elementsCountThroughWall, layoutAnnotationGroups, + annotationElementsCountsAlong, annotationElementsCountsAround, defaultElementsCountAcrossMajor, + elementsCountTransition, annotationElementsCountsAcrossMajor, isCore) + + def generateMesh(self, generateData): + super(WholeBodyNetworkMeshBuilder, self).generateMesh(generateData) + # build core, shell, left, right annotation groups + coreMeshGroup = generateData.getCoreMeshGroup() if self._isCore else None + shellMeshGroup = generateData.getShellMeshGroup() if self._isCore else None + leftMeshGroup = generateData.getLeftMeshGroup() + rightMeshGroup = generateData.getRightMeshGroup() + dorsalMeshGroup = generateData.getDorsalMeshGroup() + ventralMeshGroup = generateData.getVentralMeshGroup() + for networkSegment in self._networkMesh.getNetworkSegments(): + # print("Segment", networkSegment.getNodeIdentifiers()) + segment = self._segments[networkSegment] + if self._isCore: + segment.addCoreElementsToMeshGroup(coreMeshGroup) + segment.addShellElementsToMeshGroup(shellMeshGroup) + annotationTerms = segment.getAnnotationTerms() + for annotationTerm in annotationTerms: + if "left" in annotationTerm[0]: + segment.addAllElementsToMeshGroup(leftMeshGroup) + break + if "right" in annotationTerm[0]: + segment.addAllElementsToMeshGroup(rightMeshGroup) + break + else: + # segment on main axis + segment.addSideD2ElementsToMeshGroup(False, leftMeshGroup) + segment.addSideD2ElementsToMeshGroup(True, rightMeshGroup) + segment.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) + segment.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) + + class MeshType_3d_wholebody2(Scaffold_base): """ Generates a 3-D hermite bifurcating tube network with core representing the human body. @@ -735,7 +859,7 @@ def generateBaseMesh(cls, region, options): annotationCoreMajorCounts.append(coreMajorCount) isCore = options["Use Core"] - tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( + tubeNetworkMeshBuilder = WholeBodyNetworkMeshBuilder( networkMesh, targetElementDensityAlongLongestSegment=2.0, # not used for body defaultElementsCountAround=options["Number of elements around head"], @@ -748,14 +872,31 @@ def generateBaseMesh(cls, region, options): annotationElementsCountsAcrossMajor=annotationCoreMajorCounts, isCore=isCore) + meshDimension = 3 tubeNetworkMeshBuilder.build() - generateData = TubeNetworkMeshGenerateData( - region, 3, + generateData = WholeBodyTubeNetworkMeshGenerateData( + region, meshDimension, isLinearThroughWall=False, isShowTrimSurfaces=options["Show trim surfaces"]) tubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() + fieldmodule = region.getFieldmodule() + mesh = fieldmodule.findMeshByDimension(meshDimension) + thoraxGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("thorax")) + abdomenGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("abdomen")) + coreGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("core")) + + thoracicCavityGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("thoracic cavity")) + is_thoracic_cavity = fieldmodule.createFieldAnd(thoraxGroup.getGroup(), coreGroup.getGroup()) + thoracicCavityGroup.getMeshGroup(mesh).addElementsConditional(is_thoracic_cavity) + + abdominalCavityGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("abdominal cavity")) + is_abdominal_cavity = fieldmodule.createFieldAnd(abdomenGroup.getGroup(), coreGroup.getGroup()) + abdominalCavityGroup.getMeshGroup(mesh).addElementsConditional(is_abdominal_cavity) + return annotationGroups, None @classmethod @@ -770,14 +911,86 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): """ # create 2d surface mesh groups - fm = region.getFieldmodule() - mesh2d = fm.findMeshByDimension(2) + fieldmodule = region.getFieldmodule() + mesh2d = fieldmodule.findMeshByDimension(2) + mesh1d = fieldmodule.findMeshByDimension(1) + + neckGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("neck")) + thoracicCavityGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("thoracic cavity")) + abdominalCavityGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("abdominal cavity")) + armGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("arm")) + legGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("leg")) + + coreGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("core")) + shellGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("shell")) + leftGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("left")) + rightGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("right")) + dorsalGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("dorsal")) + + is_exterior = fieldmodule.createFieldIsExterior() + is_core_shell = fieldmodule.createFieldAnd(coreGroup.getGroup(), shellGroup.getGroup()) + is_left_right = fieldmodule.createFieldAnd(leftGroup.getGroup(), rightGroup.getGroup()) + is_left_right_dorsal = fieldmodule.createFieldAnd(is_left_right, dorsalGroup.getGroup()) skinGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("skin epidermis")) + is_skin = is_exterior + skinGroup.getMeshGroup(mesh2d).addElementsConditional(is_skin) + + thoracicCavityBoundaryGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("thoracic cavity boundary")) + is_thoracic_cavity_boundary = fieldmodule.createFieldAnd( + thoracicCavityGroup.getGroup(), + fieldmodule.createFieldOr( + fieldmodule.createFieldOr(neckGroup.getGroup(), armGroup.getGroup()), + fieldmodule.createFieldOr(shellGroup.getGroup(), abdominalCavityGroup.getGroup()))) + thoracicCavityBoundaryGroup.getMeshGroup(mesh2d).addElementsConditional(is_thoracic_cavity_boundary) + + abdominalCavityBoundaryGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("abdominal cavity boundary")) + is_abdominal_cavity_boundary = fieldmodule.createFieldAnd( + abdominalCavityGroup.getGroup(), + fieldmodule.createFieldOr( + thoracicCavityGroup.getGroup(), + fieldmodule.createFieldOr(shellGroup.getGroup(), legGroup.getGroup()))) + abdominalCavityBoundaryGroup.getMeshGroup(mesh2d).addElementsConditional(is_abdominal_cavity_boundary) + + diaphragmGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("diaphragm")) + is_diaphragm = fieldmodule.createFieldAnd(thoracicCavityGroup.getGroup(), abdominalCavityGroup.getGroup()) + diaphragmGroup.getMeshGroup(mesh2d).addElementsConditional(is_diaphragm) + + spinalCordGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("spinal cord")) + is_spinal_cord = fieldmodule.createFieldAnd(is_core_shell, is_left_right_dorsal) + spinalCordGroup.getMeshGroup(mesh1d).addElementsConditional(is_spinal_cord) + + +def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3): + """ + Assign node field parameters x, d1, d2, d3 of field. + :param field: Field parameters to assign. + :param fieldcache: Fieldcache with node set. + :param x: Parameters to set for Node.VALUE_LABEL_VALUE. + :param d1: Parameters to set for Node.VALUE_LABEL_D_DS1. + :param d2: Parameters to set for Node.VALUE_LABEL_D_DS2. + :param d3: Parameters to set for Node.VALUE_LABEL_D_DS3. + :return: + """ + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) - is_exterior = fm.createFieldIsExterior() - is_on_face_xi3_1 = fm.createFieldIsOnFace(Element.FACE_TYPE_XI3_1) - is_skin = fm.createFieldAnd(is_exterior, is_on_face_xi3_1) - skinMeshGroup = skinGroup.getMeshGroup(mesh2d) - skinMeshGroup.addElementsConditional(is_skin) +def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3): + """ + Assign node field parameters d1, d2, d3 of field. + :param field: Field to assign parameters of. + :param fieldcache: Fieldcache with node set. + :param version: Version of d1, d2, d3 >= 1. + :param d1: Parameters to set for Node.VALUE_LABEL_D_DS1. + :param d2: Parameters to set for Node.VALUE_LABEL_D_DS2. + :param d3: Parameters to set for Node.VALUE_LABEL_D_DS3. + :return: + """ + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, d1) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, d2) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, d3) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 15258ab8..b1e0b9db 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -195,6 +195,8 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem self._elementsCountAcrossMajor = elementsCountAcrossMajor # includes 2 * elementsCountTransition self._elementsCountAcrossMinor = \ self._elementsCountAround // 2 - elementsCountAcrossMajor + 4 * elementsCountTransition + self._elementsCountCoreBoxMajor = self._elementsCountAcrossMajor - 2 * elementsCountTransition + self._elementsCountCoreBoxMinor = self._elementsCountAcrossMinor - 2 * elementsCountTransition self._elementsCountTransition = elementsCountTransition # if self._isCore and self._elementsCountTransition > 1: @@ -221,12 +223,13 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem self._boxCoordinates = None self._transitionCoordinates = None - self._boxNodeIds = None # [nAlong][nAcrossMajor][nAcrossMinor] + self._boxNodeIds = None # [along][major][minor] self._boxBoundaryNodeIds = None # boxNodeIds that form the boundary of the solid core, rearranged in circular format self._boxBoundaryNodeToBoxId = None # lookup table that translates box boundary node ids in a circular format to box node ids in # [nAlong][nAcrossMajor][nAcrossMinor] format. + self._boxElementIds = None # [along][major][minor] def getElementsCountAround(self): return self._elementsCountAround @@ -305,6 +308,7 @@ def sample(self, fixedElementsCountAlong, targetElementLength): self._rimCoordinates = rx, rd1, rd2, rd3 self._rimNodeIds = [None] * (elementsCountAlong + 1) self._rimElementIds = [None] * elementsCountAlong + self._boxElementIds = [None] * elementsCountAlong if self._isCore: # sample coordinates for the solid core @@ -1049,6 +1053,137 @@ def setRimElementId(self, e1, e2, e3, elementIdentifier): self._rimElementIds[e2] = [[None] * self._elementsCountAround for _ in range(elementsCountRim)] self._rimElementIds[e2][e3][e1] = elementIdentifier + def getBoxElementId(self, e1, e2, e3): + """ + Get a box element ID. + :param e1: Element index across core box major / d2 direction. + :param e2: Element index along segment. + :param e3: Element index across core box minor / d3 direction. + :return: Element identifier. + """ + return self._boxElementIds[e2][e3][e1] + + def setBoxElementId(self, e1, e2, e3, elementIdentifier): + """ + Set a box element ID. Only called by adjacent junctions. + :param e1: Element index across core box major / d2 direction. + :param e2: Element index along segment. + :param e3: Element index across core box minor / d3 direction. + :param elementIdentifier: Element identifier. + """ + self._elementsCountCoreBoxMajor + self._elementsCountCoreBoxMinor + if not self._boxElementIds[e2]: + elementsCountRim = self.getElementsCountRim() + self._boxElementIds[e2] = [ + [None] * self._elementsCountCoreBoxMinor for _ in range(self._elementsCountCoreBoxMajor)] + self._boxElementIds[e2][e3][e1] = elementIdentifier + + def _addBoxElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGroup): + """ + Add ranges of box elements to mesh group. + :param e1Start: Start element index in major / d2 direction. + :param e1Limit: Limit element index in major / d2 direction. + :param e3Start: Start element index in minor / d3 direction. + :param e3Limit: Limit element index in minor / d3 direction. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + # print("Add box elements", e1Start, e1Limit, e3Start, e3Limit, meshGroup.getName()) + elementsCountAlong = self.getSampledElementsCountAlong() + mesh = meshGroup.getMasterMesh() + for e2 in range(elementsCountAlong): + boxSlice = self._boxElementIds[e2] + if boxSlice: + # print(boxSlice[e1Start:e1Limit]) + for elementIdentifiersList in boxSlice[e1Start:e1Limit]: + for elementIdentifier in elementIdentifiersList[e3Start:e3Limit]: + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroup.addElement(element) + + def _addRimElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGroup): + """ + Add ranges of rim elements to mesh group. + :param e1Start: Start element index around. Can be negative which supports wrapping. + :param e1Limit: Limit element index around. + :param e3Start: Start element index rim. + :param e3Limit: Limi element index rim. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + # print("Add rim elements", e1Start, e1Limit, e3Start, e3Limit, meshGroup.getName()) + elementsCountAlong = self.getSampledElementsCountAlong() + elementsCountAround = self.getElementsCountAround() + mesh = meshGroup.getMasterMesh() + for e2 in range(elementsCountAlong): + rimSlice = self._rimElementIds[e2] + if rimSlice: + for elementIdentifiersList in rimSlice[e3Start:e3Limit]: + partElementIdentifiersList = elementIdentifiersList[e1Start:e1Limit] if (e1Start >= 0) else ( + elementIdentifiersList[e1Start:] + elementIdentifiersList[:e1Limit]) + # print(partElementIdentifiersList) + if None in elementIdentifiersList: + break + for elementIdentifier in partElementIdentifiersList: + element = mesh.findElementByIdentifier(elementIdentifier) + meshGroup.addElement(element) + + def addCoreElementsToMeshGroup(self, meshGroup): + if not self._isCore: + return + self._addBoxElementsToMeshGroup(0, self._elementsCountCoreBoxMajor, + 0, self._elementsCountCoreBoxMinor, meshGroup) + self._addRimElementsToMeshGroup(0, self._elementsCountAround, + 0, self._elementsCountTransition, meshGroup) + + def addShellElementsToMeshGroup(self, meshGroup): + """ + Add elements in the shell to mesh group. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + elementsCountRim = self.getElementsCountRim() + elementsCountShell = self._elementsCountThroughWall + e3ShellStart = elementsCountRim - elementsCountShell + self._addRimElementsToMeshGroup(0, self._elementsCountAround, e3ShellStart, elementsCountRim, meshGroup) + + def addAllElementsToMeshGroup(self, meshGroup): + """ + Add all elements in the segment to mesh group. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + self.addCoreElementsToMeshGroup(meshGroup) + self.addShellElementsToMeshGroup(meshGroup) + + def addSideD2ElementsToMeshGroup(self, side: bool, meshGroup): + """ + Add elements to the mesh group on side of +d2 or -d2, often matching left and right. + Only works with even numbers around and phase starting at +d2. + :param side: False for +d2 direction, True for -d2 direction. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + if self._isCore: + e1Start = (self._elementsCountCoreBoxMajor // 2) if side else 0 + e1Limit = self._elementsCountCoreBoxMajor if side else ((self._elementsCountCoreBoxMajor + 1) // 2) + self._addBoxElementsToMeshGroup(e1Start, e1Limit, 0, self._elementsCountCoreBoxMinor, meshGroup) + e1Start = (self._elementsCountAround // 4) if side else -((self._elementsCountAround + 2) // 4) + e1Limit = e1Start + (self._elementsCountAround // 2) + if (self._elementsCountAround % 4) == 2: + eLimit += 1 + self._addRimElementsToMeshGroup(e1Start, e1Limit, 0, self.getElementsCountRim(), meshGroup) + + def addSideD3ElementsToMeshGroup(self, side: bool, meshGroup): + """ + Add elements to the mesh group on side of +d3 or -d3, often matching anterior/ventral and posterior/dorsal. + Only works with even numbers around and phase starting at +d2. + :param side: False for +d3 direction, True for -d3 direction. + :param meshGroup: Zinc MeshGroup to add elements to. + """ + if self._isCore: + e3Start = 0 if side else (self._elementsCountCoreBoxMinor // 2) + e3Limit = ((self._elementsCountCoreBoxMinor + 1) // 2) if side else self._elementsCountCoreBoxMinor + self._addBoxElementsToMeshGroup(0, self._elementsCountCoreBoxMajor, e3Start, e3Limit, meshGroup) + e1Start = (self._elementsCountAround // 2) if side else 0 + e1Limit = e1Start + (self._elementsCountAround // 2) + self._addRimElementsToMeshGroup(e1Start, e1Limit, 0, self.getElementsCountRim(), meshGroup) + def getRimNodeIdsSlice(self, n2): """ Get slice of rim node IDs. @@ -1176,6 +1311,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): elementtemplateStd = generateData.getStandardElementtemplate() eftStd = generateData.getStandardEft() for e2 in range(startSkipCount, elementsCountAlong - endSkipCount): + self._boxElementIds[e2] = [] self._rimElementIds[e2] = [] e2p = e2 + 1 if self._isCore: @@ -1184,6 +1320,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): elementsCountAcrossMajor = self.getCoreNodesCountAcrossMajor() - 1 for e3 in range(elementsCountAcrossMajor): e3p = e3 + 1 + elementIds = [] for e1 in range(elementsCountAcrossMinor): nids = [] for n1 in [e1, e1 + 1]: @@ -1194,6 +1331,8 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): element.setNodesByIdentifier(eftStd, nids) for annotationMeshGroup in annotationMeshGroups: annotationMeshGroup.addElement(element) + elementIds.append(elementIdentifier) + self._boxElementIds[e2].append(elementIds) # create core transition elements triplePointIndexesList = self.getTriplePointIndexes() @@ -1516,7 +1655,7 @@ def _sampleMidPoint(self, segmentsParameterLists): md3 = [] if d3Defined else None xi = 0.5 sideFactor = 1.0 - outFactor = 0.5 # only used if sideFactor is non-zero + outFactor = 1.0 # only used as relative proportion with non-zero sideFactor for s1 in range(segmentsCount - 1): # fxs1 = segmentsParameterLists[s1][0][0] # fd2s1 = segmentsParameterLists[s1][2][0] @@ -2292,6 +2431,7 @@ def _generateBoxElements(self, s, n2, mesh, elementtemplate, coordinates, segmen nodeLayoutFlipD2 = generateData.getNodeLayoutFlipD2() nodeLayoutBifurcation = generateData.getNodeLayoutBifurcation() + e2 = n2 if self._segmentsIn[s] else 0 for e3 in range(boxElementsCountAcrossMajor[s]): for e1 in range(boxElementsCountAcrossMinor): e3p = (e3 + 1) @@ -2336,6 +2476,7 @@ def _generateBoxElements(self, s, n2, mesh, elementtemplate, coordinates, segmen element.setNodesByIdentifier(eft, nids) if scalefactors: element.setScaleFactors(eft, scalefactors) + segment.setBoxElementId(e1, e2, e3, elementIdentifier) for annotationMeshGroup in annotationMeshGroups: annotationMeshGroup.addElement(element) @@ -2592,6 +2733,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): eftList = [None] * elementsCountAround scalefactorsList = [None] * elementsCountAround for e3 in range(elementsCountRimRegular): + rim_e3 = e3 + 1 if self._isCore else e3 for e1 in range(elementsCountAround): n1p = (e1 + 1) % elementsCountAround nids = [] @@ -2633,10 +2775,10 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): 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) + segment.setRimElementId(e1, e2, rim_e3, elementIdentifier) for annotationMeshGroup in annotationMeshGroups: annotationMeshGroup.addElement(element) From a18ffd9d1de6435589f54562e13687cc9e34c35a Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Thu, 26 Sep 2024 12:04:34 +1200 Subject: [PATCH 07/20] Add arm and leg skin annotations --- src/scaffoldmaker/annotation/body_terms.py | 4 + .../meshtypes/meshtype_3d_wholebody2.py | 135 +++++++++++------- 2 files changed, 84 insertions(+), 55 deletions(-) diff --git a/src/scaffoldmaker/annotation/body_terms.py b/src/scaffoldmaker/annotation/body_terms.py index 1c61ec59..d845fe59 100644 --- a/src/scaffoldmaker/annotation/body_terms.py +++ b/src/scaffoldmaker/annotation/body_terms.py @@ -10,7 +10,9 @@ ("abdominopelvic cavity", "UBERON:0035819"), ("arm", "UBERON:0001460"), ("left arm", "FMA:24896"), + ("left arm skin epidermis", ""), ("right arm", "FMA:24895"), + ("right arm skin epidermis", ""), ("body", "UBERON:0000468", "ILX:0101370"), ("core", ""), ("core boundary", ""), @@ -22,7 +24,9 @@ ("left", ""), ("leg", "UBERON:0000978"), ("left leg", "FMA:24981"), + ("left leg skin epidermis", ""), ("right leg", "FMA:24980"), + ("right leg skin epidermis", ""), ("foot", "FMA:9664"), ("neck", "UBERON:0000974", "ILX:0733967"), ("neck core", ""), diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index d6e503a5..93fee400 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -881,21 +881,22 @@ def generateBaseMesh(cls, region, options): tubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() - fieldmodule = region.getFieldmodule() - mesh = fieldmodule.findMeshByDimension(meshDimension) - thoraxGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("thorax")) - abdomenGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("abdomen")) - coreGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("core")) - - thoracicCavityGroup = findOrCreateAnnotationGroupForTerm( - annotationGroups, region, get_body_term("thoracic cavity")) - is_thoracic_cavity = fieldmodule.createFieldAnd(thoraxGroup.getGroup(), coreGroup.getGroup()) - thoracicCavityGroup.getMeshGroup(mesh).addElementsConditional(is_thoracic_cavity) - - abdominalCavityGroup = findOrCreateAnnotationGroupForTerm( - annotationGroups, region, get_body_term("abdominal cavity")) - is_abdominal_cavity = fieldmodule.createFieldAnd(abdomenGroup.getGroup(), coreGroup.getGroup()) - abdominalCavityGroup.getMeshGroup(mesh).addElementsConditional(is_abdominal_cavity) + if isCore: + fieldmodule = region.getFieldmodule() + mesh = fieldmodule.findMeshByDimension(meshDimension) + thoraxGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("thorax")) + abdomenGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("abdomen")) + coreGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("core")) + + thoracicCavityGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("thoracic cavity")) + is_thoracic_cavity = fieldmodule.createFieldAnd(thoraxGroup.getGroup(), coreGroup.getGroup()) + thoracicCavityGroup.getMeshGroup(mesh).addElementsConditional(is_thoracic_cavity) + + abdominalCavityGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("abdominal cavity")) + is_abdominal_cavity = fieldmodule.createFieldAnd(abdomenGroup.getGroup(), coreGroup.getGroup()) + abdominalCavityGroup.getMeshGroup(mesh).addElementsConditional(is_abdominal_cavity) return annotationGroups, None @@ -909,58 +910,82 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): :param annotationGroups: List of annotation groups for top-level elements. New face annotation groups are appended to this list. """ + isCore = options["Use Core"] - # create 2d surface mesh groups + # create 2-D surface mesh groups, 1-D spinal cord fieldmodule = region.getFieldmodule() mesh2d = fieldmodule.findMeshByDimension(2) mesh1d = fieldmodule.findMeshByDimension(1) - neckGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("neck")) - thoracicCavityGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("thoracic cavity")) - abdominalCavityGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("abdominal cavity")) - armGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("arm")) - legGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("leg")) - - coreGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("core")) - shellGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("shell")) - leftGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("left")) - rightGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("right")) - dorsalGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("dorsal")) - is_exterior = fieldmodule.createFieldIsExterior() - is_core_shell = fieldmodule.createFieldAnd(coreGroup.getGroup(), shellGroup.getGroup()) - is_left_right = fieldmodule.createFieldAnd(leftGroup.getGroup(), rightGroup.getGroup()) - is_left_right_dorsal = fieldmodule.createFieldAnd(is_left_right, dorsalGroup.getGroup()) skinGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("skin epidermis")) is_skin = is_exterior skinGroup.getMeshGroup(mesh2d).addElementsConditional(is_skin) - thoracicCavityBoundaryGroup = findOrCreateAnnotationGroupForTerm( - annotationGroups, region, get_body_term("thoracic cavity boundary")) - is_thoracic_cavity_boundary = fieldmodule.createFieldAnd( - thoracicCavityGroup.getGroup(), - fieldmodule.createFieldOr( - fieldmodule.createFieldOr(neckGroup.getGroup(), armGroup.getGroup()), - fieldmodule.createFieldOr(shellGroup.getGroup(), abdominalCavityGroup.getGroup()))) - thoracicCavityBoundaryGroup.getMeshGroup(mesh2d).addElementsConditional(is_thoracic_cavity_boundary) - - abdominalCavityBoundaryGroup = findOrCreateAnnotationGroupForTerm( - annotationGroups, region, get_body_term("abdominal cavity boundary")) - is_abdominal_cavity_boundary = fieldmodule.createFieldAnd( - abdominalCavityGroup.getGroup(), - fieldmodule.createFieldOr( + leftArmGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("left arm")) + leftArmSkinGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("left arm skin epidermis")) + leftArmSkinGroup.getMeshGroup(mesh2d).addElementsConditional( + fieldmodule.createFieldAnd(leftArmGroup.getGroup(), is_exterior)) + rightArmGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("right arm")) + rightArmSkinGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("right arm skin epidermis")) + rightArmSkinGroup.getMeshGroup(mesh2d).addElementsConditional( + fieldmodule.createFieldAnd(rightArmGroup.getGroup(), is_exterior)) + leftLegGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("left leg")) + leftLegSkinGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("left leg skin epidermis")) + leftLegSkinGroup.getMeshGroup(mesh2d).addElementsConditional( + fieldmodule.createFieldAnd(leftLegGroup.getGroup(), is_exterior)) + rightLegGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("right leg")) + rightLegSkinGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("right leg skin epidermis")) + rightLegSkinGroup.getMeshGroup(mesh2d).addElementsConditional( + fieldmodule.createFieldAnd(rightLegGroup.getGroup(), is_exterior)) + + if isCore: + coreGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("core")) + shellGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("shell")) + leftGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("left")) + rightGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("right")) + dorsalGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("dorsal")) + + is_core_shell = fieldmodule.createFieldAnd(coreGroup.getGroup(), shellGroup.getGroup()) + is_left_right = fieldmodule.createFieldAnd(leftGroup.getGroup(), rightGroup.getGroup()) + is_left_right_dorsal = fieldmodule.createFieldAnd(is_left_right, dorsalGroup.getGroup()) + + neckGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("neck")) + thoracicCavityGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("thoracic cavity")) + abdominalCavityGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("abdominal cavity")) + armGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("arm")) + legGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("leg")) + + thoracicCavityBoundaryGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("thoracic cavity boundary")) + is_thoracic_cavity_boundary = fieldmodule.createFieldAnd( thoracicCavityGroup.getGroup(), - fieldmodule.createFieldOr(shellGroup.getGroup(), legGroup.getGroup()))) - abdominalCavityBoundaryGroup.getMeshGroup(mesh2d).addElementsConditional(is_abdominal_cavity_boundary) - - diaphragmGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("diaphragm")) - is_diaphragm = fieldmodule.createFieldAnd(thoracicCavityGroup.getGroup(), abdominalCavityGroup.getGroup()) - diaphragmGroup.getMeshGroup(mesh2d).addElementsConditional(is_diaphragm) - - spinalCordGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("spinal cord")) - is_spinal_cord = fieldmodule.createFieldAnd(is_core_shell, is_left_right_dorsal) - spinalCordGroup.getMeshGroup(mesh1d).addElementsConditional(is_spinal_cord) + fieldmodule.createFieldOr( + fieldmodule.createFieldOr(neckGroup.getGroup(), armGroup.getGroup()), + fieldmodule.createFieldOr(shellGroup.getGroup(), abdominalCavityGroup.getGroup()))) + thoracicCavityBoundaryGroup.getMeshGroup(mesh2d).addElementsConditional(is_thoracic_cavity_boundary) + + abdominalCavityBoundaryGroup = findOrCreateAnnotationGroupForTerm( + annotationGroups, region, get_body_term("abdominal cavity boundary")) + is_abdominal_cavity_boundary = fieldmodule.createFieldAnd( + abdominalCavityGroup.getGroup(), + fieldmodule.createFieldOr( + thoracicCavityGroup.getGroup(), + fieldmodule.createFieldOr(shellGroup.getGroup(), legGroup.getGroup()))) + abdominalCavityBoundaryGroup.getMeshGroup(mesh2d).addElementsConditional(is_abdominal_cavity_boundary) + + diaphragmGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("diaphragm")) + is_diaphragm = fieldmodule.createFieldAnd(thoracicCavityGroup.getGroup(), abdominalCavityGroup.getGroup()) + diaphragmGroup.getMeshGroup(mesh2d).addElementsConditional(is_diaphragm) + + spinalCordGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("spinal cord")) + is_spinal_cord = fieldmodule.createFieldAnd(is_core_shell, is_left_right_dorsal) + spinalCordGroup.getMeshGroup(mesh1d).addElementsConditional(is_spinal_cord) def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3): From d9ab1dd0829a1eed9d4fc158abe9d5db98502a49 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Fri, 27 Sep 2024 16:03:02 +1200 Subject: [PATCH 08/20] Use outer trim; new segment sampling prep --- .../meshtypes/meshtype_1d_network_layout1.py | 57 ++++++++++++++++++- .../meshtypes/meshtype_2d_tubenetwork1.py | 2 + .../meshtypes/meshtype_3d_tubenetwork1.py | 7 ++- .../meshtypes/meshtype_3d_uterus2.py | 4 +- .../meshtypes/meshtype_3d_wholebody2.py | 4 +- src/scaffoldmaker/utils/networkmesh.py | 55 +++++++++++++++--- src/scaffoldmaker/utils/tubenetworkmesh.py | 25 ++++++-- 7 files changed, 135 insertions(+), 19 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py index 7baf8723..58ba3ea3 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py @@ -27,9 +27,11 @@ 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", + "Snake": "1-2-3-4-5-6-7-8-9-10-11-12-13-14-15-16-17-18-19-20-21-22-23-24-25-26-27-28-29-30-31-32-33", "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" + "Trifurcation cross": "1-3.1,2-3.2,3.2-4,3.1-5", + "Vase": "1-2-3-4-5" } @classmethod @@ -101,6 +103,34 @@ def generateBaseMesh(cls, region, options): coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, d13) + elif "Snake" in parameterSetName: + snakeRadius = 0.5 + tubeRadius = 0.1 + nodesCount = nodes.getSize() + elementsCountHalfCircle = 8 + elementAngle = math.pi / elementsCountHalfCircle + d1Mag = snakeRadius * elementAngle + xSign = -1.0 + xOffset = -snakeRadius + for n in range(nodesCount): + if (n % elementsCountHalfCircle) == 0: + xSign = -xSign + xOffset += 2.0 * snakeRadius + angle = elementAngle * n + cosAngle = math.cos(angle) + sinAngle = math.sin(angle) + node = nodes.findNodeByIdentifier(n + 1) + fieldcache.setNode(node) + x = [xOffset - xSign * snakeRadius * cosAngle, snakeRadius * sinAngle, 0.0] + d1 = [xSign * d1Mag * sinAngle, d1Mag * cosAngle, 0.0] + d2 = [-tubeRadius * cosAngle, xSign * tubeRadius * sinAngle, 0.0] + d12 = mult(d1, elementAngle * tubeRadius / d1Mag) + d3 = [0.0, 0.0, tubeRadius] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, d12) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) elif "Sphere cube" in parameterSetName: # edit node parameters sphereRadius = 0.5 @@ -156,6 +186,31 @@ def generateBaseMesh(cls, region, options): coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, v + 1, cd2[n][v]) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, v + 1, cd3[n]) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, v + 1, cd13[n][v]) + elif "Vase" in parameterSetName: + midRadius = 1.0 + magRadius = 0.5 + nodesCount = nodes.getSize() + elementsCountWavelength = 4 + elementAngle = 2.0 * math.pi / elementsCountWavelength + for n in range(nodesCount): + angle = elementAngle * n + cosAngle = math.cos(angle) + sinAngle = math.sin(angle) + node = nodes.findNodeByIdentifier(n + 1) + fieldcache.setNode(node) + x = [0.0, 0.0, n] + d1 = [0.0, 0.0, 1.0] + r = midRadius + magRadius * sinAngle + d2 = [0.0, r, 0.0] + d12 = [0.0, 2.0 * magRadius * cosAngle, 0.0] + d3 = [r, 0.0, 0.0] + d13 = [2.0 * magRadius * cosAngle, 0.0, 0.0] + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, d12) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, d13) if defineInnerCoordinates: cls.defineInnerCoordinates(region, coordinates, options, networkMesh) diff --git a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py index 354aef2e..ca175c2a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py @@ -30,6 +30,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): "Annotation numbers of elements along": [0], "Show trim surfaces": False } + if parameterSetName in ["Loop", "Snake", "Vase"]: + options["Target element density along longest segment"] = 12.0 return options @staticmethod diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py index 02a9ee1f..20533431 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py @@ -42,12 +42,15 @@ def getDefaultOptions(cls, parameterSetName="Default"): "Annotation numbers of elements along": [0], "Number of elements through shell": 1, "Use linear through shell": False, + "Use outer trim surfaces": True, "Show trim surfaces": False, "Core": False, "Number of elements across core box minor": 2, "Number of elements across core transition": 1, "Annotation numbers of elements across core box minor": [0] } + if parameterSetName in ["Loop", "Snake", "Vase"]: + options["Target element density along longest segment"] = 12.0 return options @staticmethod @@ -60,6 +63,7 @@ def getOrderedOptionNames(): "Annotation numbers of elements along", "Number of elements through shell", "Use linear through shell", + "Use outer trim surfaces", "Show trim surfaces", "Core", "Number of elements across core box minor", @@ -237,7 +241,8 @@ def generateBaseMesh(cls, region, options): defaultElementsCountAcrossMajor=defaultCoreMajorCount, elementsCountTransition=coreTransitionCount, annotationElementsCountsAcrossMajor=annotationCoreMajorCounts, - isCore=options["Core"]) + isCore=options["Core"], + useOuterTrimSurfaces=options["Use outer trim surfaces"]) tubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py index d4564242..b1c18601 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py @@ -53,11 +53,11 @@ class UterusTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], annotationElementsCountsAlong: list = [], - annotationElementsCountsAround: list = []): + annotationElementsCountsAround: list = [], useOuterTrimSurfaces=True): super(UterusTubeNetworkMeshBuilder, self).__init__( networkMesh, targetElementDensityAlongLongestSegment, defaultElementsCountAround, elementsCountThroughWall, layoutAnnotationGroups, - annotationElementsCountsAlong, annotationElementsCountsAround) + annotationElementsCountsAlong, annotationElementsCountsAround, useOuterTrimSurfaces=useOuterTrimSurfaces) def generateMesh(self, generateData): super(UterusTubeNetworkMeshBuilder, self).generateMesh(generateData) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 93fee400..a72c4087 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -584,12 +584,12 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], annotationElementsCountsAlong: list = [], annotationElementsCountsAround: list = [], defaultElementsCountAcrossMajor: int = 4, elementsCountTransition: int = 1, - annotationElementsCountsAcrossMajor: list = [], isCore=False): + annotationElementsCountsAcrossMajor: list = [], isCore=False, useOuterTrimSurfaces=True): super(WholeBodyNetworkMeshBuilder, self).__init__( networkMesh, targetElementDensityAlongLongestSegment, defaultElementsCountAround, elementsCountThroughWall, layoutAnnotationGroups, annotationElementsCountsAlong, annotationElementsCountsAround, defaultElementsCountAcrossMajor, - elementsCountTransition, annotationElementsCountsAcrossMajor, isCore) + elementsCountTransition, annotationElementsCountsAcrossMajor, isCore, useOuterTrimSurfaces) def generateMesh(self, generateData): super(WholeBodyNetworkMeshBuilder, self).generateMesh(generateData) diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index c4e651ee..46f5328e 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -6,9 +6,10 @@ 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 cmlibs.maths.vectorops import cross, magnitude, normalize, rejection, sub from scaffoldmaker.annotation.annotationgroup import AnnotationGroup -from scaffoldmaker.utils.interpolation import getCubicHermiteCurvesLength +from scaffoldmaker.utils.interpolation import ( + gaussWt4, gaussXi4, getCubicHermiteCurvesLength, interpolateCubicHermiteDerivative) from scaffoldmaker.utils.tracksurface import TrackSurface from abc import ABC, abstractmethod import math @@ -535,10 +536,10 @@ def __init__(self, networkSegment, pathParametersList): 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 + self._lengthParameters = self._calculateLengthParameters() def addAnnotationTerm(self, annotationTerm): """ @@ -550,12 +551,12 @@ def addAnnotationTerm(self, annotationTerm): def getAnnotationTerms(self): return self._annotationTerms - def getLength(self): + def getSampleLength(self): """ - Get length of the segment's primary path. - :return: Real length. + Get sample length of the segment's primary path. + :return: Length. """ - return self._length + return self._lengthParameters[0][-1] def getNetworkSegment(self): """ @@ -596,6 +597,44 @@ def isLoop(self): """ return self._isLoop + def _calculateLengthParameters(self): + """ + Calculate scalar field values and derivatives giving effective length along segment central path + with allowance for mean change in lateral axes, used for sampling elements along segment. + Uses first (outer) path parameters only. + Calculated in constructor and stored. + :return: Length parameters (lx[], ld1[]) + """ + px, pd1, pd2, pd12, pd3, pd13 = self._pathParametersList[0] + lx = [[0.0]] + totalLength = 0.0 + for e in range(len(px) - 1): + ax, ad1, ad2, ad12, ad3, ad13 = px[e], pd1[e], pd2[e], pd12[e], pd3[e], pd13[e] + bx, bd1, bd2, bd12, bd3, bd13 = px[e + 1], pd1[e + 1], pd2[e + 1], pd12[e + 1], pd3[e + 1], pd13[e + 1] + length = 0.0 + for i in range(4): + d1 = interpolateCubicHermiteDerivative(ax, ad1, bx, bd1, gaussXi4[i]) + gd1 = magnitude(d1) + gd2 = magnitude(rejection(interpolateCubicHermiteDerivative(ad2, ad12, bd2, bd12, gaussXi4[i]), d1)) + gd3 = magnitude(rejection(interpolateCubicHermiteDerivative(ad3, ad13, bd3, bd13, gaussXi4[i]), d1)) + gds = 0.5 * (gd2 + gd3) + length += gaussWt4[i] * math.sqrt(gd1 * gd1 + gds * gds) + totalLength += length + lx.append(totalLength) + ld = [] + for n in range(len(px)): + d1 = magnitude(pd1[n]) + d2 = 0.5 * (magnitude(pd12[n]) + magnitude(pd13[n])) + ld.append([math.sqrt(d1 * d1 + d2 * d2)]) + return (lx, ld) + + def getLengthParameters(self): + """ + Get scalar field parameter (value, derivative) giving effective length along segment. + :return: Length parameters (lx[], ld[]) + """ + return self._lengthParameters + @abstractmethod def sample(self, fixedElementsCountAlong, targetElementLength): """ @@ -709,7 +748,7 @@ def _createSegments(self): 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() + segmentLength = segment.getSampleLength() if segmentLength > self._longestSegmentLength: self._longestSegmentLength = segmentLength for layoutAnnotationGroup in self._layoutAnnotationGroups: diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index b1e0b9db..4eda34ac 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -1400,14 +1400,16 @@ class TubeNetworkMeshJunction(NetworkMeshJunction): Describes junction between multiple tube segments, some in, some out. """ - def __init__(self, inSegments: list, outSegments: list): + def __init__(self, inSegments: list, outSegments: list, useOuterTrimSurfaces): """ :param inSegments: List of inward TubeNetworkMeshSegment. :param outSegments: List of outward TubeNetworkMeshSegment. + :param useOuterTrimSurfaces: Set to True to use common trim surfaces calculated from outer. """ 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._useOuterTrimSurfaces = useOuterTrimSurfaces 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 @@ -1458,6 +1460,9 @@ def _calculateTrimSurfaces(self): endIndex = -1 if self._segmentsIn[s] else 0 pathEndPlaneTrackSurfaces = [] for p in range(pathsCount): + if self._useOuterTrimSurfaces and (p > 0): + pathEndPlaneTrackSurfaces.append(pathEndPlaneTrackSurfaces[-1]) + continue pathParameters = self._segments[s].getPathParameters(p) centre = pathParameters[0][endIndex] axis1 = pathParameters[2][endIndex] @@ -1476,6 +1481,9 @@ def _calculateTrimSurfaces(self): for s in range(self._segmentsCount): endIndex = -1 if self._segmentsIn[s] else 0 for p in range(pathsCount): + if self._useOuterTrimSurfaces and (p > 0): + self._trimSurfaces[s][p] = self._trimSurfaces[s][p - 1] + continue pathParameters = self._segments[s].getPathParameters(p) d2End = pathParameters[2][endIndex] d3End = pathParameters[4][endIndex] @@ -1601,12 +1609,15 @@ def _calculateTrimSurfaces(self): nd1 = [] nd2 = [] nd12 = [] - for factor in (0.75, 1.25): + trimWidthFactors = (0.25, 1.75) if self._useOuterTrimSurfaces else (0.75, 1.25) + d2scale = trimWidthFactors[1] - trimWidthFactors[0] + for factor in trimWidthFactors: 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) + d2 = mult(d2, d2scale) nx.append(x) nd1.append(d1) nd2.append(d2) @@ -2616,6 +2627,8 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): nodeIdentifier, faceIdentifier = \ trimSurface.generateMesh(generateData.getRegion(), nodeIdentifier, faceIdentifier, group_name=annotationGroup.getName()) + if self._useOuterTrimSurfaces: + break; if dimension == 2: elementIdentifier = faceIdentifier generateData.setNodeElementIdentifiers(nodeIdentifier, elementIdentifier) @@ -2789,7 +2802,7 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], annotationElementsCountsAlong: list = [], annotationElementsCountsAround: list = [], defaultElementsCountAcrossMajor: int = 4, elementsCountTransition: int = 1, - annotationElementsCountsAcrossMajor: list = [], isCore=False): + annotationElementsCountsAcrossMajor: list = [], isCore=False, useOuterTrimSurfaces=False): """ :param networkMesh: Description of the topology of the network layout. :param targetElementDensityAlongLongestSegment: @@ -2805,7 +2818,8 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg :param defaultElementsCountAcrossMajor: :param elementsCountTransition: :param annotationElementsCountsAcrossMajor: - :param isCore: + :param isCore: Set to True to define solid core box and transition elements. + :param useOuterTrimSurfaces: Set to True to use common trim surfaces calculated from outer. """ super(TubeNetworkMeshBuilder, self).__init__( networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups, annotationElementsCountsAlong) @@ -2818,6 +2832,7 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg if not self._layoutInnerCoordinates.isValid(): self._layoutInnerCoordinates = None self._isCore = isCore + self._useOuterTrimSurfaces = useOuterTrimSurfaces self._defaultElementsCountAcrossMajor = defaultElementsCountAcrossMajor self._elementsCountTransition = elementsCountTransition self._annotationElementsCountsAcrossMajor = annotationElementsCountsAcrossMajor @@ -2865,7 +2880,7 @@ def createJunction(self, inSegments, outSegments): :param outSegments: List of outward TubeNetworkMeshSegment. :return: A TubeNetworkMeshJunction. """ - return TubeNetworkMeshJunction(inSegments, outSegments) + return TubeNetworkMeshJunction(inSegments, outSegments, self._useOuterTrimSurfaces) def getPathRawTubeCoordinates(pathParameters, elementsCountAround, radius=1.0, phaseAngle=0.0): From 97f47525877867385cc58662bd985b8689563d92 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Wed, 2 Oct 2024 23:24:24 +1300 Subject: [PATCH 09/20] Improve segment sample algorithm --- .../meshtypes/meshtype_1d_network_layout1.py | 6 +- .../meshtypes/meshtype_3d_wholebody2.py | 2 +- src/scaffoldmaker/utils/boxnetworkmesh.py | 2 +- src/scaffoldmaker/utils/interpolation.py | 23 +- src/scaffoldmaker/utils/networkmesh.py | 9 +- src/scaffoldmaker/utils/tubenetworkmesh.py | 383 +++++++++++++----- tests/test_general.py | 38 +- tests/test_network.py | 195 ++++++--- tests/test_uterus.py | 8 +- tests/test_wholebody2.py | 29 +- 10 files changed, 495 insertions(+), 200 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py index 58ba3ea3..af0a5f16 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py @@ -113,7 +113,8 @@ def generateBaseMesh(cls, region, options): xSign = -1.0 xOffset = -snakeRadius for n in range(nodesCount): - if (n % elementsCountHalfCircle) == 0: + halfCircle = (n % elementsCountHalfCircle) == 0 + if halfCircle: xSign = -xSign xOffset += 2.0 * snakeRadius angle = elementAngle * n @@ -124,7 +125,8 @@ def generateBaseMesh(cls, region, options): x = [xOffset - xSign * snakeRadius * cosAngle, snakeRadius * sinAngle, 0.0] d1 = [xSign * d1Mag * sinAngle, d1Mag * cosAngle, 0.0] d2 = [-tubeRadius * cosAngle, xSign * tubeRadius * sinAngle, 0.0] - d12 = mult(d1, elementAngle * tubeRadius / d1Mag) + d12Sign = 0.0 if halfCircle else xSign + d12 = mult(d1, d12Sign * elementAngle * tubeRadius / d1Mag) d3 = [0.0, 0.0, tubeRadius] coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index a72c4087..20838c19 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -650,7 +650,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along neck"] = 1 options["Number of elements along thorax"] = 2 options["Number of elements along abdomen"] = 2 - options["Number of elements along arm to hand"] = 4 + options["Number of elements along arm to hand"] = 5 options["Number of elements along hand"] = 1 options["Number of elements along leg to foot"] = 4 options["Number of elements along foot"] = 2 diff --git a/src/scaffoldmaker/utils/boxnetworkmesh.py b/src/scaffoldmaker/utils/boxnetworkmesh.py index 969664b9..6c477ffd 100644 --- a/src/scaffoldmaker/utils/boxnetworkmesh.py +++ b/src/scaffoldmaker/utils/boxnetworkmesh.py @@ -93,7 +93,7 @@ def __init__(self, networkSegment, pathParametersList): def sample(self, fixedElementsCountAlong, targetElementLength): elementsCountAlong = (fixedElementsCountAlong if fixedElementsCountAlong else - max(1, math.ceil(self._length / targetElementLength))) + max(1, math.ceil(self.getSampleLength() / targetElementLength))) if self._isLoop and (elementsCountAlong < 2): elementsCountAlong = 2 pathParameters = self._pathParametersList[0] diff --git a/src/scaffoldmaker/utils/interpolation.py b/src/scaffoldmaker/utils/interpolation.py index 70e55f90..4bc3de51 100644 --- a/src/scaffoldmaker/utils/interpolation.py +++ b/src/scaffoldmaker/utils/interpolation.py @@ -293,8 +293,16 @@ def getCubicHermiteCurvatureSimple(v1, d1, v2, d2, xi): mag_tangent = magnitude(tangent) if mag_tangent > 0.0: dTangent = interpolateCubicHermiteSecondDerivative(v1, d1, v2, d2, xi) - cp = cross(tangent, dTangent) - curvature = magnitude(cp) / (mag_tangent * mag_tangent * mag_tangent) + componentsCount = len(v1) + if componentsCount > 1: + if componentsCount == 3: + cp = cross(tangent, dTangent) + mag_cp = magnitude(cp) + else: + mag_cp = tangent[0] * dTangent[1] - tangent[1] * dTangent[0] + curvature = mag_cp / (mag_tangent * mag_tangent * mag_tangent) + else: + curvature = 0.0 else: curvature = 0.0 dTangent = [0.0, 0.0, 0.0] @@ -1211,7 +1219,7 @@ def advanceCurveLocation(startLocation, dxi, elementsCount, loop=False, MAX_MAG_ if loop: if proportion < 0.0: proportion += 1.0 - elif proportion > 1.0: + elif proportion >= 1.0: proportion -= 1.0 else: if proportion < 0.0: @@ -1222,7 +1230,7 @@ def advanceCurveLocation(startLocation, dxi, elementsCount, loop=False, MAX_MAG_ onBoundary = 2 adxi = (proportion - startProportion) * elementsCount scaledProportion = proportion * elementsCount - elementIndex = int(scaledProportion) + elementIndex = min(int(scaledProportion), elementsCount - 1) location = (elementIndex, scaledProportion - elementIndex) return location, adxi, onBoundary @@ -1349,6 +1357,7 @@ def getNearestLocationOnCurve(nx, nd1, targetx, loop=False, startLocation=None, x = None mag_adxi = -1 it = MAX_ITERS = 100 + componentsCount = len(targetx) for it in range(MAX_ITERS): x, d = evaluateCoordinatesOnCurve(nx, nd1, location, loop, derivative=True) mag_d = magnitude(d) @@ -1371,7 +1380,10 @@ def getNearestLocationOnCurve(nx, nd1, targetx, loop=False, startLocation=None, jVector = normalize(tangent) if dxi < 0.0: jVector = [-d for d in jVector] - iVector = normalize(cross(tangent, cross(tangent, dTangent))) + if componentsCount == 3: + iVector = normalize(cross(tangent, cross(tangent, dTangent))) + else: + iVector = [-jVector[1], jVector[0]] centre = sub(x, mult(iVector, radius)) delta = sub(targetx, centre) dj = dot(delta, jVector) @@ -1393,6 +1405,7 @@ def getNearestLocationOnCurve(nx, nd1, targetx, loop=False, startLocation=None, print(" iVector", iVector, "di", di) print(" jVector", jVector, "dj", dj) print(" curved dxi", dxi) + location, adxi, onBoundary = advanceCurveLocation(location, dxi, elementsCount, loop, MAX_MAG_DXI) mag_adxi = abs(adxi) if instrument: diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index 46f5328e..58adc7a1 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -6,7 +6,7 @@ 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, rejection, sub +from cmlibs.maths.vectorops import cross, magnitude, mult, normalize, rejection, sub from scaffoldmaker.annotation.annotationgroup import AnnotationGroup from scaffoldmaker.utils.interpolation import ( gaussWt4, gaussXi4, getCubicHermiteCurvesLength, interpolateCubicHermiteDerivative) @@ -365,12 +365,11 @@ def create1DLayoutMesh(self, region): break if prevNetworkNode or nextNetworkNode: if prevNetworkNode and nextNetworkNode: - d1 = sub(nextNetworkNode.getX(), prevNetworkNode.getX()) + d1 = mult(sub(nextNetworkNode.getX(), prevNetworkNode.getX()), 0.5) elif prevNetworkNode: d1 = sub(networkNode.getX(), prevNetworkNode.getX()) else: d1 = sub(nextNetworkNode.getX(), networkNode.getX()) - d1 = normalize(d1) d3 = [0.0, 0.0, 0.1] d2 = cross(d3, normalize(d1)) coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, nodeVersion, d1) @@ -556,7 +555,7 @@ def getSampleLength(self): Get sample length of the segment's primary path. :return: Length. """ - return self._lengthParameters[0][-1] + return self._lengthParameters[0][-1][0] def getNetworkSegment(self): """ @@ -620,7 +619,7 @@ def _calculateLengthParameters(self): gds = 0.5 * (gd2 + gd3) length += gaussWt4[i] * math.sqrt(gd1 * gd1 + gds * gds) totalLength += length - lx.append(totalLength) + lx.append([totalLength]) ld = [] for n in range(len(px)): d1 = magnitude(pd1[n]) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 4eda34ac..cb81b5ab 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -7,8 +7,8 @@ from scaffoldmaker.utils.eft_utils import determineCubicHermiteSerendipityEft, HermiteNodeLayoutManager from scaffoldmaker.utils.interpolation import ( computeCubicHermiteDerivativeScaling, computeCubicHermiteEndDerivative, DerivativeScalingMode, - evaluateCoordinatesOnCurve, - getCubicHermiteTrimmedCurvesLengths, interpolateCubicHermite, interpolateCubicHermiteDerivative, + evaluateCoordinatesOnCurve, getCubicHermiteTrimmedCurvesLengths, getNearestLocationOnCurve, + interpolateCubicHermite, interpolateCubicHermiteDerivative, interpolateHermiteLagrangeDerivative, interpolateLagrangeHermiteDerivative, interpolateSampleCubicHermite, sampleCubicHermiteCurves, sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine, smoothCubicHermiteDerivativesLoop, @@ -216,7 +216,7 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem 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._sampledTubeCoordinates = [[[], [], [], []] for p in range(self._pathsCount)] self._rimCoordinates = None self._rimNodeIds = None self._rimElementIds = None # [e2][e3][e1] @@ -252,19 +252,163 @@ def getElementsCountAcrossTransition(self): def getRawTrackSurface(self, pathIndex=0): return self._rawTrackSurfaceList[pathIndex] - def sample(self, fixedElementsCountAlong, targetElementLength): - trimSurfaces = [self._junctions[j].getTrimSurfaces(self) for j in range(2)] + def _sampleTubeCoordinates(self, fixedElementsCountAlong, targetElementLength, transitionFactor=3.0): + """ + Generate sampled outer, inner tube coordinates optionally trimmed to start/end surfaces. + Element sizes are constant size at the max length, but compressed if trimmed. + Lateral cross sections in untrimmed areas are in d2-d3 plane of the network layout, + hence elements are generally bigger on the outside and smaller on the inside of curves. + Algorithm uses a finite transition region at trimmed ends based on the trim range, so trimming is local. + :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 determined from mean trimmed length, subject to a minimum for the configuration. + :param transitionFactor: Factor > 1.0 multiplying range of trimmed lengths at each end to complete + local element size transition over. + """ + lx, ld = self._lengthParameters + # print("lx", lx, "ld", ld) + rawNodesCountAlong = len(self._rawTubeCoordinatesList[0][0]) + rawElementsCountAlong = rawNodesCountAlong - 1 + + # work out trim lengths of outer, inner tubes + startCurveLocations = [] + startLengths = [] + sumStartLengths = 0.0 + endCurveLocations = [] + endLengths = [] + sumEndLengths = 0.0 + for p in range(self._pathsCount): + px, pd1, pd2, pd12 = self._rawTubeCoordinatesList[p] + startTrimSurface = self._junctions[0].getTrimSurfaces(self)[p] + endTrimSurface = self._junctions[1].getTrimSurfaces(self)[p] + + for q in range(self._elementsCountAround): + cx = [px[p][q] for p in range(rawNodesCountAlong)] + cd2 = [pd2[p][q] for p in range(rawNodesCountAlong)] + startCurveLocation = (0, 0.0) + startLength = 0.0 + if startTrimSurface: + surfacePosition, curveLocation, intersects = startTrimSurface.findNearestPositionOnCurve(cx, cd2) + if intersects: + startCurveLocation = curveLocation + startLength = evaluateCoordinatesOnCurve(lx, ld, startCurveLocation)[0] + startCurveLocations.append(startCurveLocation) + startLengths.append(startLength) + sumStartLengths += startLength + endCurveLocation = (rawElementsCountAlong, 1.0) + endLength = lx[-1][0] + if endTrimSurface: + surfacePosition, curveLocation, intersects = endTrimSurface.findNearestPositionOnCurve(cx, cd2) + if intersects: + endCurveLocation = curveLocation + endLength = evaluateCoordinatesOnCurve(lx, ld, endCurveLocation)[0] + endCurveLocations.append(endCurveLocation) + endLengths.append(endLength) + sumEndLengths += endLength + + minStartLength = min(startLengths) + maxStartLength = max(startLengths) + minEndLength = min(endLengths) + maxEndLength = max(endLengths) + maxLength = maxEndLength - minStartLength + # minimum number applies to fixedElementsCountAlong and targetElementLength minimumElementsCountAlong = 2 if (self._isLoop or ((self._junctions[0].getSegmentsCount() > 2) and - (self._junctions[1].getSegmentsCount() > 2))) else 1 - elementsCountAlong = fixedElementsCountAlong + (self._junctions[1].getSegmentsCount() > 2))) else 1 + # small fudge factor on targetElementLength so whole numbers chosen on centroid don't go one higher: + elementsCountAlong = max(minimumElementsCountAlong, fixedElementsCountAlong if fixedElementsCountAlong else + math.ceil(maxLength * 0.9999 / targetElementLength)) + startTransitionSize = transitionFactor * (maxStartLength - minStartLength) + endTransitionSize = transitionFactor * (maxEndLength - minEndLength) + if (startTransitionSize + endTransitionSize) > maxLength: + # reduce transitions in proportion to fit mean length + startTransitionSize *= maxLength / (startTransitionSize + endTransitionSize) + endTransitionSize = maxLength - startTransitionSize + maxElementLength = maxLength / elementsCountAlong + # print("maxLength", maxLength, "maxElementLength", maxElementLength) + # for parametric coordinate [0,1] over startTransitionSize, these are the start derivatives + # where mean would have value startTransitionSize at both ends + startTransitionXiSpacing = (maxElementLength / startTransitionSize) if (startTransitionSize > 0.0) else 0.0 + endTransitionXiSpacing = (maxElementLength / endTransitionSize) if (endTransitionSize > 0.0) else 0.0 + dStart = [[None] * self._elementsCountAround for _ in range(self._pathsCount)] + dEnd = [[None] * self._elementsCountAround for _ in range(self._pathsCount)] + startTransitionEndLength = minStartLength + startTransitionSize + endTransitionStartLength = maxEndLength - endTransitionSize + # print("Transition start min", minStartLength, "max", maxStartLength, "size", startTransitionSize, "end", startTransitionEndLength) + # print("Transition end min", minEndLength, "max", maxEndLength, "size", endTransitionSize, "end", endTransitionStartLength) 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 p == 0: - elementsCountAlong = len(self._sampledTubeCoordinates[0][0]) - 1 + startTrimSurface = self._junctions[0].getTrimSurfaces(self)[p] + if startTrimSurface: + for q in range(self._elementsCountAround): + ls = startLengths[p * self._elementsCountAround + q] + dStart[p][q] = interpolateLagrangeHermiteDerivative( + [ls], [startTransitionEndLength], [startTransitionSize], 0.0)[0] + # print(" p", p, "q", q, "dStart", dStart[p][q]) + endTrimSurface = self._junctions[1].getTrimSurfaces(self)[p] + if endTrimSurface: + for q in range(self._elementsCountAround): + le = endLengths[p * self._elementsCountAround + q] + dEnd[p][q] = interpolateHermiteLagrangeDerivative( + [endTransitionStartLength], [endTransitionSize], [le], 1.0)[0] + # print(" p", p, "q", q, "dEnd", dEnd[p][q]) + + tubeGenerator = TubeEllipseGenerator() + + for n in range(elementsCountAlong + 1): + + # get point parameters at mean sampling points + lm = minStartLength + n * maxElementLength + curveLocation = getNearestLocationOnCurve(lx, ld, [lm])[0] + + for p in range(self._pathsCount): + cx, cd1, cd2, cd12, cd3, cd13 = self._pathParametersList[p] + px, pd1 = evaluateCoordinatesOnCurve(cx, cd1, curveLocation, derivative=True) + pd2, pd12 = evaluateCoordinatesOnCurve(cd2, cd12, curveLocation, derivative=True) + pd3, pd13 = evaluateCoordinatesOnCurve(cd3, cd13, curveLocation, derivative=True) + ex, ed1, ed2, ed12 = tubeGenerator.generate(px, pd1, pd2, pd12, pd3, pd13, self._elementsCountAround, + maxElementLength) + startTrimSurface = self._junctions[0].getTrimSurfaces(self)[p] + endTrimSurface = self._junctions[1].getTrimSurfaces(self)[p] + startTransition = (startTransitionSize > 0.0) and (lm < startTransitionEndLength) + endTransition = (endTransitionSize > 0.0) and (lm > endTransitionStartLength) + if startTransition or endTransition: + for q in range(self._elementsCountAround): + if startTransition: + ls = startLengths[p * self._elementsCountAround + q] + xi = n * startTransitionXiSpacing + v1, d1, v2, d2 = [ls], [dStart[p][q]], [startTransitionEndLength], [startTransitionSize] + else: # if endTransition: + le = endLengths[p * self._elementsCountAround + q] + xi = 1.0 - (elementsCountAlong - n) * endTransitionXiSpacing + v1, d1, v2, d2 = [endTransitionStartLength], [endTransitionSize], [le], [dEnd[p][q]] + lt = interpolateCubicHermite(v1, d1, v2, d2, xi)[0] + ltd = interpolateCubicHermiteDerivative(v1, d1, v2, d2, xi)[0] + qCurveLocation = getNearestLocationOnCurve(lx, ld, [lt])[0] + px, pd1 = evaluateCoordinatesOnCurve(cx, cd1, qCurveLocation, derivative=True) + pd2, pd12 = evaluateCoordinatesOnCurve(cd2, cd12, qCurveLocation, derivative=True) + pd3, pd13 = evaluateCoordinatesOnCurve(cd3, cd13, qCurveLocation, derivative=True) + qex, qed1, qed2, qed12 = tubeGenerator.generate( + px, pd1, pd2, pd12, pd3, pd13, self._elementsCountAround, + ltd * maxElementLength / (startTransitionSize if startTransition else endTransitionSize)) + for ev, qv in zip((ex, ed1, ed2, ed12), (qex, qed1, qed2, qed12)): + ev[q] = qv[q] + # recalculate d1 around rings + # first smooth to get d1 with new directions not tangential to surface + ted1 = smoothCubicHermiteDerivativesLoop(ex, ed1) + # constraint to be tangential to tube surface + ted1 = [rejection(ted1[q], normalize(cross(ed1[q], ed2[q]))) + for q in range(self._elementsCountAround)] + # smooth magnitudes only + ed1 = smoothCubicHermiteDerivativesLoop(ex, ted1, fixAllDirections=True) + + for lst, ev in zip(self._sampledTubeCoordinates[p], (ex, ed1, ed2, ed12)): + lst.append(ev) + + # smooth d2, d12 + + def sample(self, fixedElementsCountAlong, targetElementLength): + self._sampleTubeCoordinates(fixedElementsCountAlong, targetElementLength) + + elementsCountAlong = len(self._sampledTubeCoordinates[0][0]) - 1 if self._dimension == 2: # copy first sampled tube coordinates, but insert single-entry 'n3' index after n2 @@ -668,15 +812,34 @@ def _determineCoreD2Derivatives(self, boxx, boxd1, boxd3, transx, transd1, trans transd2 = [[[None for _ in range(self._elementsCountAround)] for _ in range(self._elementsCountTransition - 1)] for _ in range(elementsCountAlong)] + # compute core d2 directions by weighting with 1/distance from inner coordinates + + def get_d2(n2, x): + sum_weight = 0.0 + sum_d2 = [0.0, 0.0, 0.0] + ix = self._rimCoordinates[0][n2][0] + id2 = self._rimCoordinates[2][n2][0] + for i in range(len(ix)): + distance_sq = 0.0 + for c in range(3): + delta = x[c] - ix[i][c] + distance_sq += delta * delta + if distance_sq == 0.0: + return id2[i] + weight = 1.0 / math.sqrt(distance_sq) + sum_weight += weight + for c in range(3): + sum_d2[c] += weight * id2[i][c] + return [sum_d2[c] / sum_weight for c in range(3)] + for m in range(nodesCountAcrossMajor): for n in range(nodesCountAcrossMinor): tx, td2 = [], [] for n2 in range(elementsCountAlong): x = boxx[n2][m][n] - d2 = cross(boxd3[n2][m][n], boxd1[n2][m][n]) tx.append(x) - td2.append(d2) - td2 = smoothCubicHermiteDerivativesLine(tx, td2, fixStartDirection=False, fixEndDirection=False) + td2.append(get_d2(n2, x)) + # td2 = smoothCubicHermiteDerivativesLine(tx, td2, fixAllDirections=True) for n2 in range(elementsCountAlong): boxd2[n2][m][n] = td2[n2] @@ -686,10 +849,9 @@ def _determineCoreD2Derivatives(self, boxx, boxd1, boxd3, transx, transd1, trans tx, td2 = [], [] for n2 in range(elementsCountAlong): x = transx[n2][n3][n1] - d2 = cross(transd3[n2][n3][n1], transd1[n2][n3][n1]) tx.append(x) - td2.append(d2) - td2 = smoothCubicHermiteDerivativesLine(tx, td2, fixStartDirection=False, fixEndDirection=True) + td2.append(get_d2(n2, x)) + # td2 = smoothCubicHermiteDerivativesLine(tx, td2, fixAllDirections=True) for n2 in range(elementsCountAlong): transd2[n2][n3][n1] = td2[n2] @@ -1196,6 +1358,18 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): """ :param n2Only: If set, create nodes only for that single n2 index along. Must be >= 0! """ + if (not n2Only) and generateData.isShowTrimSurfaces(): + dimension = generateData.getMeshDimension() + nodeIdentifier, elementIdentifier = generateData.getNodeElementIdentifiers() + faceIdentifier = elementIdentifier if (dimension == 2) else None + annotationGroup = generateData.getNewTrimAnnotationGroup() + nodeIdentifier, faceIdentifier = \ + self._rawTrackSurfaceList[0].generateMesh(generateData.getRegion(), nodeIdentifier, faceIdentifier, + group_name=annotationGroup.getName()) + if dimension == 2: + elementIdentifier = faceIdentifier + generateData.setNodeElementIdentifiers(nodeIdentifier, elementIdentifier) + elementsCountAlong = len(self._rimCoordinates[0]) - 1 elementsCountRim = self.getElementsCountRim() elementsCountTransition = self.getElementsCountTransition() @@ -1669,9 +1843,10 @@ def _sampleMidPoint(self, segmentsParameterLists): outFactor = 1.0 # only used as relative proportion with non-zero sideFactor 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] + fd2s1 = segmentsParameterLists[s1][2][0] + if segmentsIn[s1]: + fd2s1 = [-d for d in fd2s1] + norm_fd2s1 = normalize(fd2s1) for s2 in range(s1 + 1, segmentsCount): hd2s1 = hd2[s1] hd2s2 = [-d for d in hd2[s2]] @@ -1694,14 +1869,18 @@ def _sampleMidPoint(self, segmentsParameterLists): 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] + fd2s2 = segmentsParameterLists[s2][2][0] + if not segmentsIn[s1]: + fd2s2 = [-d for d in fd2s1] + norm_fd2s2 = normalize(fd2s2) + # reduce md2 up to 50% depending on how out-of line they are + md2Factor = 1.0 + 0.5 * dot(norm_fd2s1, norm_fd2s2) + md2.append(mult(cd2, md2Factor)) + # smooth smx, smd2 with 2nd row from end coordinates and derivatives # tmd2 = smoothCubicHermiteDerivativesLine( - # [fxs1, cx, fxs2], [fd2s1, cd2, fd2s2], fixStartDerivative=True, fixEndDerivative=True) + # [fxs1, cx, fxs2], [fd2s1, cd2, fd2s2], fixStartDerivative=True, fixEndDerivative=True, + # magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) # md2.append(tmd2[1]) if d3Defined: md3.append(mult(add(hd3[s1], hd3[s2]), 0.5)) @@ -2628,7 +2807,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): trimSurface.generateMesh(generateData.getRegion(), nodeIdentifier, faceIdentifier, group_name=annotationGroup.getName()) if self._useOuterTrimSurfaces: - break; + break if dimension == 2: elementIdentifier = faceIdentifier generateData.setNodeElementIdentifiers(nodeIdentifier, elementIdentifier) @@ -2883,62 +3062,52 @@ def createJunction(self, inSegments, outSegments): return TubeNetworkMeshJunction(inSegments, outSegments, self._useOuterTrimSurfaces) -def getPathRawTubeCoordinates(pathParameters, elementsCountAround, radius=1.0, phaseAngle=0.0): +class TubeEllipseGenerator: """ - 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) + Generates tube ellipse curves with even-sized elements with specified radius, phase angle, + for any number of user-supplied parameters and number around. """ - 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] + def __init__(self, radius=1.0, phaseAngle=0.0): + self._radius = radius + self._phaseAngle = phaseAngle + + # sample around circle, later scaled by ellipse parameters and resampled to get even spacing in geometric space + self._circlePointCount = 16 + self._cx = [] + self._cd = [] + aroundScale = 2.0 * math.pi / self._circlePointCount + for q in range(self._circlePointCount): + theta = phaseAngle + 2.0 * math.pi * q / self._circlePointCount + x = [radius * math.cos(theta), radius * math.sin(theta)] + self._cx.append(x) + d = [-x[1] * aroundScale, x[0] * aroundScale] + self._cd.append(d) + + def generate(self, px, pd1, pd2, pd12, pd3, pd13, elementsCountAround, d2Scale=1.0): + """ + Generate a single row of 2-D ellipse parameters for the tube. + :param px: Centre of ellipse. + :param pd1: Derivative along centre of tube. + :param pd2: Major axis of ellipse. + :param pd12: Rate of change of major axis along tube. + :param pd3: Minor axis of ellipse. + :param pd13: Rate of change of minor axis along tube. + :param d2Scale: Scale to apply to derivative along the tube. + :return: 2-D tube ellipse row parameters ex, ed1, ed2, ed12 + """ 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) + for q in range(self._circlePointCount): + xi2, xi3 = self._cx[q] + tx.append([(px[c] + xi2 * pd2[c] + xi3 * pd3[c]) for c in range(3)]) + dxi2, dxi3 = self._cd[q] + td1.append([(dxi2 * pd2[c] + dxi3 * pd3[c]) for c in range(3)]) # 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) + exi, edxi = interpolateSampleCubicHermite(self._cx + self._cx[:1], self._cd + self._cd[:1], pe, pxi, psf) ex.pop() ed1.pop() exi.pop() @@ -2957,24 +3126,54 @@ def getPathRawTubeCoordinates(pathParameters, elementsCountAround, radius=1.0, p # print("error", p, "=", [magnitude(v) for v in dxi]) # calculate d2, d12 at exi + mag1 = magnitude(pd1) + mag2 = 0.5 * (magnitude(pd12) + magnitude(pd13)) + d2ScaleFinal = d2Scale / (math.sqrt(mag1 * mag1 + mag2 * mag2)) 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 + xi2, xi3 = exi[i] + ed2.append([d2ScaleFinal * (pd1[c] + xi2 * pd12[c] + xi3 * pd13[c]) for c in range(3)]) + dxi2, dxi3 = edxi[i] + ed12.append([d2ScaleFinal * (dxi2 * pd12[c] + dxi3 * pd13[c]) for c in range(3)]) + + return ex, ed1, ed2, ed12 + + +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 + + tubeGenerator = TubeEllipseGenerator(radius, phaseAngle) + tx = [] + td1 = [] + td2 = [] + td12 = [] + for p in range(pointsCountAlong): + px, pd1, pd2, pd12, pd3, pd13 = [cp[p] for cp in pathParameters] + ex, ed1, ed2, ed12 = tubeGenerator.generate( + px, pd1, pd2, pd12, pd3, pd13, elementsCountAround, d2Scale=magnitude(pd1)) + tx.append(ex) + td1.append(ed1) + td2.append(ed2) + td12.append(ed12) + + return tx, td1, td2, td12 def resampleTubeCoordinates(rawTubeCoordinates, fixedElementsCountAlong=None, diff --git a/tests/test_general.py b/tests/test_general.py index 89a4f8ee..a68708a5 100644 --- a/tests/test_general.py +++ b/tests/test_general.py @@ -985,8 +985,8 @@ def test_tube_intersections1(self): nearestPosition = tube3Surface.findNearestPosition(targetx, startPosition) self.assertEqual(nearestPosition.e1, 2) self.assertEqual(nearestPosition.e2, 1) - self.assertAlmostEqual(nearestPosition.xi1, 0.44002661465024806, delta=XI_TOL) - self.assertAlmostEqual(nearestPosition.xi2, 0.7782327241770322, delta=XI_TOL) + self.assertAlmostEqual(nearestPosition.xi1, 0.4402234141866752, delta=XI_TOL) + self.assertAlmostEqual(nearestPosition.xi2, 0.7779349419901669, delta=XI_TOL) targetx = [0.9745695128243425, -0.28544615442781057, -0.23619538278312255] startPosition = TrackSurfacePosition(4, 0, 0.158, 0.0) @@ -1017,12 +1017,12 @@ def test_tube_intersections1(self): self.assertTrue(aloop) aCircumference = getCubicHermiteCurvesLength(ax, ad1, loop=True) self.assertAlmostEqual(aCircumference, 2.3973686453086143, delta=X_TOL) - assertAlmostEqualList(self, [0.9708619388739947, -0.3270981496668778, 0.14086800149667], ax[0], delta=X_TOL) - assertAlmostEqualList(self, [1.0050064347902659, 0.056201268034582176, -0.4072936264713945], ax[4], delta=X_TOL) - assertAlmostEqualList(self, [1.024996071040654, 0.28060105555314807, 0.24424001783212906], ax[8], delta=X_TOL) - assertAlmostEqualList(self, [0.4405054359950804, 1.0], aprops[0], delta=XI_TOL) - assertAlmostEqualList(self, [0.7737970104355056, 1.0], aprops[4], delta=XI_TOL) - assertAlmostEqualList(self, [1.10696010571695916, 1.0], aprops[8], delta=XI_TOL) + assertAlmostEqualList(self, [0.9708658007728959, -0.32705488164620056, 0.14100946581662172], ax[0], delta=X_TOL) + assertAlmostEqualList(self, [1.0049935720991983, 0.05605687388777593, -0.40732283704015587], ax[4], delta=X_TOL) + assertAlmostEqualList(self, [1.0250029888842724, 0.28067871392667065, 0.24411427503697689], ax[8], delta=X_TOL) + assertAlmostEqualList(self, [0.4404437939919339, 1.0], aprops[0], delta=XI_TOL) + assertAlmostEqualList(self, [0.7737349073601545, 1.0], aprops[4], delta=XI_TOL) + assertAlmostEqualList(self, [1.1068983034552708, 1.0], aprops[8], delta=XI_TOL) # get loop intersection of unconnected tube2 and tube3 bx, bd1, bprops, bloop = tube2Surface.findIntersectionCurve(tube3Surface, curveElementsCount=12) @@ -1037,11 +1037,11 @@ def test_tube_intersections1(self): self.assertEqual(len(cx), 8) self.assertTrue(cloop) cCircumference = getCubicHermiteCurvesLength(cx, cd1, loop=True) - self.assertAlmostEqual(cCircumference, 0.587857727905694, delta=X_TOL) - assertAlmostEqualList(self, [0.7318758089726128, 0.28115396634786666, 0.13238674558014327], cx[0], delta=X_TOL) - assertAlmostEqualList(self, [0.8408765202354725, 0.3182280500580874, -0.04549438718340043], cx[4], delta=X_TOL) - assertAlmostEqualList(self, [1.0762363614295223, 0.7158314515175085], cprops[0], delta=XI_TOL) - assertAlmostEqualList(self, [0.9766132119015503, 0.8198673158401282], cprops[4], delta=XI_TOL) + self.assertAlmostEqual(cCircumference, 0.5887574920030573, delta=X_TOL) + assertAlmostEqualList(self, [0.7317317911306801, 0.2826742296218589, 0.13294871691322063], cx[0], delta=X_TOL) + assertAlmostEqualList(self, [0.8395922582810261, 0.31875624576664224, -0.045585853595370915], cx[4], delta=X_TOL) + assertAlmostEqualList(self, [1.0752584167399162, 0.7160306656756313], cprops[0], delta=XI_TOL) + assertAlmostEqualList(self, [0.976739018142142, 0.818749339695521], cprops[4], delta=XI_TOL) # make trimmed tube3 starting at intersection with tube1 tx, td1, td2, td12 = resampleTubeCoordinates((px, pd1, pd2, pd12), elementsCountAlong, @@ -1057,15 +1057,15 @@ def test_tube_intersections1(self): nd12 += td12[i] tube3TrimmedSurface = TrackSurface(elementsCountAround, elementsCountAlong, nx, nd1, nd2, nd12, loop1=True) tCircumference = getCubicHermiteCurvesLength(tx[0], td1[0], loop=True) - self.assertAlmostEqual(tCircumference, 0.5891599271757954, delta=X_TOL) + self.assertAlmostEqual(tCircumference, 0.5901062022041457, delta=X_TOL) tLength = getCubicHermiteCurvesLength([tx[n][0] for n in range(elementsCountAlong + 1)], [td2[n][0] for n in range(elementsCountAlong + 1)]) - self.assertAlmostEqual(tLength, 0.5004144140988955, delta=X_TOL) + self.assertAlmostEqual(tLength, 0.49914658490095454, delta=X_TOL) curveLocation1, curveX1 = getNearestLocationOnCurve( cx, cd1, targetx=[1.0307591456989758, 0.3452962162336672, -0.05130331144410176], loop=True) self.assertEqual(curveLocation1[0], 3) - self.assertAlmostEqual(curveLocation1[1], 0.2627396466353775, delta=XI_TOL) + self.assertAlmostEqual(curveLocation1[1], 0.2487406936675347, delta=XI_TOL) aCurveLocation, cCurveLocation, acIntersection = getNearestLocationBetweenCurves( ax, ad1, cx, cd1, aloop, cloop) @@ -1073,9 +1073,9 @@ def test_tube_intersections1(self): p3x = evaluateCoordinatesOnCurve(ax, ad1, aCurveLocation, aloop) p4x = evaluateCoordinatesOnCurve(cx, cd1, cCurveLocation, cloop) self.assertEqual(aCurveLocation[0], 6) - self.assertAlmostEqual(aCurveLocation[1], 0.7537941135756656, delta=XI_TOL) + self.assertAlmostEqual(aCurveLocation[1], 0.7537596353150168, delta=XI_TOL) self.assertEqual(cCurveLocation[0], 3) - self.assertAlmostEqual(cCurveLocation[1], 0.05064926617363552, delta=XI_TOL) + self.assertAlmostEqual(cCurveLocation[1], 0.04146736244924165, delta=XI_TOL) # context = Context("TrackSurface") # region = context.getDefaultRegion() @@ -1630,7 +1630,7 @@ def test_2d_tube_intersections_bifurcation(self): self.assertEqual(nearestPosition.e1, 4) self.assertEqual(nearestPosition.e2, 0) self.assertAlmostEqual(nearestPosition.xi1, 0.0, delta=XI_TOL) - self.assertAlmostEqual(nearestPosition.xi2, 0.023415557045696735, delta=XI_TOL) + self.assertAlmostEqual(nearestPosition.xi2, 0.021114449677837155, delta=XI_TOL) # distant point p6x = targetx = [0.8187820665733468, -0.1, 0.0] diff --git a/tests/test_network.py b/tests/test_network.py index 1108095f..9c5d45cd 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -94,7 +94,7 @@ def test_2d_tube_network_bifurcation(self): self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) self.assertEqual([0], settings["Annotation numbers of elements along"]) - settings["Target element density along longest segment"] = 3.4 + settings["Target element density along longest segment"] = 3.3 MeshType_2d_tubenetwork1.checkOptions(settings) context = Context("Test") @@ -124,7 +124,55 @@ 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.930453257098265, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 1.9345569273205805, delta=X_TOL) + + def test_2d_tube_network_snake(self): + """ + Test 2D tube snake has radial elements. + """ + scaffoldPackage = ScaffoldPackage(MeshType_2d_tubenetwork1, defaultParameterSetName="Snake") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + self.assertEqual(12.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(96, mesh2d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(104, 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.1, -0.5196340284402325, -0.1], X_TOL) + assertAlmostEqualList(self, maximums, [4.1, 0.5196340284402319, 0.1], X_TOL) + + with ChangeManager(fieldmodule): + # check range of d2 shows element sizes vary from inside to outside of curves + d2 = fieldmodule.createFieldNodeValue(coordinates, Node.VALUE_LABEL_D_DS2, 1) + mag_d2 = fieldmodule.createFieldMagnitude(d2) + min_mag_d2, max_mag_d2 = evaluateFieldNodesetRange(mag_d2, nodes) + + 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(min_mag_d2, 0.41678801141467386, delta=X_TOL) + self.assertAlmostEqual(max_mag_d2, 0.6251820171220115, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 3.883499820061501, delta=X_TOL) def test_2d_tube_network_sphere_cube(self): """ @@ -185,8 +233,8 @@ def test_2d_tube_network_sphere_cube(self): X_TOL = 1.0E-6 minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - assertAlmostEqualList(self, minimums, [-0.5664610069377636, -0.5965021612010833, -0.5985868755975444], X_TOL) - assertAlmostEqualList(self, maximums, [0.5664609474985409, 0.5965021612010833, 0.5985868966530402], X_TOL) + assertAlmostEqualList(self, minimums, [-0.5665335420558559, -0.5965021612011158, -0.5986833971069179], X_TOL) + assertAlmostEqualList(self, maximums, [0.5665335420558559, 0.5965021612011159, 0.5986833971069178], X_TOL) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -195,7 +243,7 @@ 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.045008760308933, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 4.045738080224775, delta=X_TOL) def test_2d_tube_network_trifurcation(self): """ @@ -240,7 +288,55 @@ def test_2d_tube_network_trifurcation(self): fieldcache = fieldmodule.createFieldcache() result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 2.792300995131311, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.799227811309674, delta=X_TOL) + + def test_2d_tube_network_vase(self): + """ + Test 2D tube vase has near constant length elements despite radius changes. + """ + scaffoldPackage = ScaffoldPackage(MeshType_2d_tubenetwork1, defaultParameterSetName="Vase") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + self.assertEqual(12.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(96, mesh2d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(104, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [-1.5, -1.5, 0.0], X_TOL) + assertAlmostEqualList(self, maximums, [1.5, 1.5, 4.0], X_TOL) + + with ChangeManager(fieldmodule): + # check range of d2 shows near constant element sizes + d2 = fieldmodule.createFieldNodeValue(coordinates, Node.VALUE_LABEL_D_DS2, 1) + mag_d2 = fieldmodule.createFieldMagnitude(d2) + min_mag_d2, max_mag_d2 = evaluateFieldNodesetRange(mag_d2, nodes) + + 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(min_mag_d2, 0.38259775266954776, delta=X_TOL) + self.assertAlmostEqual(max_mag_d2, 0.3825977526695479, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 28.820994366312384, delta=X_TOL) def test_3d_tube_network_bifurcation(self): """ @@ -251,13 +347,14 @@ def test_3d_tube_network_bifurcation(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(12, len(settings)) + self.assertEqual(13, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) self.assertEqual(4.0, settings["Target element density along longest segment"]) self.assertEqual([0], settings["Annotation numbers of elements along"]) self.assertFalse(settings["Use linear through shell"]) + self.assertTrue(settings["Use outer trim surfaces"]) self.assertFalse(settings["Show trim surfaces"]) self.assertFalse(settings["Core"]) self.assertEqual(2, settings["Number of elements across core box minor"]) @@ -310,9 +407,9 @@ def test_3d_tube_network_bifurcation(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.0351511378107642, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 1.928821019338746, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 1.561184510316338, delta=X_TOL) + self.assertAlmostEqual(volume, 0.0350166554737855, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 1.9320679669979417, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 1.5635839608316708, delta=X_TOL) def test_3d_tube_network_bifurcation_core(self): """ @@ -323,7 +420,7 @@ def test_3d_tube_network_bifurcation_core(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(12, len(settings)) + self.assertEqual(13, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) @@ -360,8 +457,6 @@ def test_3d_tube_network_bifurcation_core(self): with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) isExterior = fieldmodule.createFieldIsExterior() - isExteriorXi3_1 = fieldmodule.createFieldAnd( - isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) mesh2d = fieldmodule.findMeshByDimension(2) fieldcache = fieldmodule.createFieldcache() @@ -370,13 +465,13 @@ def test_3d_tube_network_bifurcation_core(self): result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) surfaceAreaField.setNumbersOfPoints(4) result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.09946683712947964, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 1.928821019338746, delta=X_TOL) + self.assertAlmostEqual(volume, 0.09934789293818859, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.0262176284480025, delta=X_TOL) def test_3d_tube_network_sphere_cube(self): """ @@ -387,7 +482,7 @@ def test_3d_tube_network_sphere_cube(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(12, len(settings)) + self.assertEqual(13, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) @@ -440,8 +535,8 @@ def test_3d_tube_network_sphere_cube(self): 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) + assertAlmostEqualList(self, minimums, [-0.5665130262270113, -0.5965021612011158, -0.5986773876363235], X_TOL) + assertAlmostEqualList(self, maximums, [0.5665130262270113, 0.5965021612011159, 0.5986773876363234], X_TOL) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -468,9 +563,9 @@ def test_3d_tube_network_sphere_cube(self): 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) + self.assertAlmostEqual(volume, 0.07364074411579775, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 4.045725519817575, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 3.3315247370347674, delta=X_TOL) def test_3d_tube_network_sphere_cube_core(self): """ @@ -481,7 +576,7 @@ def test_3d_tube_network_sphere_cube_core(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(12, len(settings)) + self.assertEqual(13, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) @@ -538,14 +633,12 @@ def test_3d_tube_network_sphere_cube_core(self): 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) + assertAlmostEqualList(self, minimums, [-0.5665130262270113, -0.5965021612011158, -0.5986773876363235], X_TOL) + assertAlmostEqualList(self, maximums, [0.5665130262270113, 0.5965021612011159, 0.5986773876363234], X_TOL) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) isExterior = fieldmodule.createFieldIsExterior() - isExteriorXi3_1 = fieldmodule.createFieldAnd( - isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) mesh2d = fieldmodule.findMeshByDimension(2) fieldcache = fieldmodule.createFieldcache() @@ -554,13 +647,13 @@ def test_3d_tube_network_sphere_cube_core(self): result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) surfaceAreaField.setNumbersOfPoints(4) result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.21482044353689586, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 4.045008760308934, delta=X_TOL) + self.assertAlmostEqual(volume, 0.21361144824524667, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 4.045725519817576, delta=X_TOL) def test_3d_tube_network_trifurcation_cross(self): @@ -572,7 +665,7 @@ def test_3d_tube_network_trifurcation_cross(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(12, len(settings)) + self.assertEqual(13, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) @@ -653,9 +746,9 @@ def test_3d_tube_network_trifurcation_cross(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.047609658608033, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 2.59759659324524, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 2.1152592960466077, delta=X_TOL) + self.assertAlmostEqual(volume, 0.047265446041689, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.6025349554689585, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 2.1185123279913802, delta=X_TOL) def test_3d_tube_network_trifurcation_cross_core(self): """ @@ -667,7 +760,7 @@ def test_3d_tube_network_trifurcation_cross_core(self): networkLayoutScaffoldPackage = settings["Network layout"] networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertTrue(networkLayoutSettings["Define inner coordinates"]) - self.assertEqual(12, len(settings)) + self.assertEqual(13, len(settings)) self.assertEqual(8, settings["Number of elements around"]) self.assertEqual(1, settings["Number of elements through shell"]) self.assertEqual([0], settings["Annotation numbers of elements around"]) @@ -731,8 +824,6 @@ def test_3d_tube_network_trifurcation_cross_core(self): with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) isExterior = fieldmodule.createFieldIsExterior() - isExteriorXi3_1 = fieldmodule.createFieldAnd( - isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) mesh2d = fieldmodule.findMeshByDimension(2) fieldcache = fieldmodule.createFieldcache() @@ -741,13 +832,13 @@ def test_3d_tube_network_trifurcation_cross_core(self): result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) surfaceAreaField.setNumbersOfPoints(4) result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.1355916886131598, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.596646206538057, delta=X_TOL) + self.assertAlmostEqual(volume, 0.13518934925801437, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.7269545818490495, delta=X_TOL) def test_3d_box_network_bifurcation(self): """ @@ -887,9 +978,9 @@ def test_3d_tube_network_loop(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.03536584166731818, delta=1.0E-6) - self.assertAlmostEqual(outerSurfaceArea, 1.9689027258731782, delta=1.0E-6) - self.assertAlmostEqual(innerSurfaceArea, 1.5751215539100383, delta=1.0E-6) + self.assertAlmostEqual(volume, 0.03534439013604324, delta=1.0E-6) + self.assertAlmostEqual(outerSurfaceArea, 1.9683574196198823, delta=1.0E-6) + self.assertAlmostEqual(innerSurfaceArea, 1.5748510621127434, delta=1.0E-6) def test_3d_tube_network_loop_core(self): """ @@ -923,8 +1014,6 @@ def test_3d_tube_network_loop_core(self): with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) isExterior = fieldmodule.createFieldIsExterior() - isExteriorXi3_1 = fieldmodule.createFieldAnd( - isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) mesh2d = fieldmodule.findMeshByDimension(2) fieldcache = fieldmodule.createFieldcache() @@ -932,13 +1021,13 @@ def test_3d_tube_network_loop_core(self): volumeField.setNumbersOfPoints(4) result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) surfaceAreaField.setNumbersOfPoints(4) result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.09823844907582693, delta=1.0E-6) - self.assertAlmostEqual(surfaceArea, 1.9689027258731782, delta=1.0E-6) + self.assertAlmostEqual(volume, 0.0982033864405135, delta=1.0E-6) + self.assertAlmostEqual(surfaceArea, 1.9683574196198823, delta=1.0E-6) def test_3d_tube_network_loop_two_segments(self): @@ -989,8 +1078,8 @@ def test_3d_tube_network_loop_two_segments(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - assertAlmostEqualList(self, minimums, [-0.5845857744155142, -0.6, -0.10000000000000003], 1.0E-8) - assertAlmostEqualList(self, maximums, [0.6, 0.5845857768617423, 0.1], 1.0E-8) + assertAlmostEqualList(self, minimums, [-0.5846409928643533, -0.6, -0.1], 1.0E-8) + assertAlmostEqualList(self, maximums, [0.6, 0.5846409928643533, 0.1], 1.0E-8) bob = fieldmodule.findFieldByName("bob").castGroup() self.assertTrue(bob.isValid()) @@ -1024,9 +1113,9 @@ def test_3d_tube_network_loop_two_segments(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.03536545586642272, delta=1.0E-6) - self.assertAlmostEqual(outerSurfaceArea, 1.9684589894847588, delta=1.0E-6) - self.assertAlmostEqual(innerSurfaceArea, 1.5747667641754262, delta=1.0E-6) + self.assertAlmostEqual(volume, 0.0353515741325893, delta=1.0E-6) + self.assertAlmostEqual(outerSurfaceArea, 1.9681077595642782, delta=1.0E-6) + self.assertAlmostEqual(innerSurfaceArea, 1.5745958498454014, delta=1.0E-6) if __name__ == "__main__": diff --git a/tests/test_uterus.py b/tests/test_uterus.py index 0322bcf8..8186b792 100644 --- a/tests/test_uterus.py +++ b/tests/test_uterus.py @@ -65,8 +65,8 @@ def test_uterus1(self): coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - assertAlmostEqualList(self, minimums, [-9.361977045958657, -0.048, -8.90345243427233], 1.0E-6) - assertAlmostEqualList(self, maximums, [9.36197704595863, 12.810434295416874, 1.09], 1.0E-6) + assertAlmostEqualList(self, minimums, [-9.361977045958657, -0.048, -8.92088553607632], 1.0E-6) + assertAlmostEqualList(self, maximums, [9.36197704595863, 12.846826704128778, 1.09], 1.0E-6) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -80,8 +80,8 @@ def test_uterus1(self): self.assertEqual(result, RESULT_OK) result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 261.4231659225306, delta=1.0E-6) - self.assertAlmostEqual(volume, 182.6551617962469, delta=1.0E-6) + self.assertAlmostEqual(surfaceArea, 262.70532322657914, delta=1.0E-6) + self.assertAlmostEqual(volume, 183.66628268786462, delta=1.0E-6) fieldmodule.defineAllFaces() for annotationGroup in annotationGroups: diff --git a/tests/test_wholebody2.py b/tests/test_wholebody2.py index 62a1428d..ac641488 100644 --- a/tests/test_wholebody2.py +++ b/tests/test_wholebody2.py @@ -26,13 +26,20 @@ def test_wholebody2_core(self): parameterSetNames = scaffold.getParameterSetNames() self.assertEqual(parameterSetNames, ["Default", "Human 1 Coarse", "Human 1 Medium", "Human 1 Fine"]) options = scaffold.getDefaultOptions("Default") - self.assertEqual(12, len(options)) + self.assertEqual(19, len(options)) + self.assertEqual(2, options["Number of elements along head"]) + self.assertEqual(1, options["Number of elements along neck"]) + self.assertEqual(2, options["Number of elements along thorax"]) + self.assertEqual(2, options["Number of elements along abdomen"]) + self.assertEqual(5, options["Number of elements along arm to hand"]) + self.assertEqual(1, options["Number of elements along hand"]) + self.assertEqual(4, options["Number of elements along leg to foot"]) + self.assertEqual(2, options["Number of elements along foot"]) self.assertEqual(12, options["Number of elements around head"]) self.assertEqual(12, options["Number of elements around torso"]) self.assertEqual(8, options["Number of elements around arm"]) self.assertEqual(8, options["Number of elements around leg"]) self.assertEqual(1, options["Number of elements through shell"]) - self.assertEqual(5.0, options["Target element density along longest segment"]) self.assertEqual(False, options["Show trim surfaces"]) self.assertEqual(True, options["Use Core"]) self.assertEqual(2, options["Number of elements across core box minor"]) @@ -42,7 +49,7 @@ def test_wholebody2_core(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(13, len(annotationGroups)) # Needs updating as we add more annotation groups + self.assertEqual(32, len(annotationGroups)) # Needs updating as we add more annotation groups fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) @@ -111,27 +118,13 @@ def test_wholebody2_tube(self): Test creation of Whole-body scaffold without solid core. """ scaffold = MeshType_3d_wholebody2 - parameterSetNames = scaffold.getParameterSetNames() - self.assertEqual(parameterSetNames, ["Default", "Human 1 Coarse", "Human 1 Medium", "Human 1 Fine"]) options = scaffold.getDefaultOptions("Default") - self.assertEqual(12, len(options)) - self.assertEqual(12, options["Number of elements around head"]) - self.assertEqual(12, options["Number of elements around torso"]) - self.assertEqual(8, options["Number of elements around arm"]) - self.assertEqual(8, options["Number of elements around leg"]) - self.assertEqual(1, options["Number of elements through shell"]) - self.assertEqual(5.0, options["Target element density along longest segment"]) - self.assertEqual(False, options["Show trim surfaces"]) - self.assertEqual(True, options["Use Core"]) - self.assertEqual(2, options["Number of elements across core box minor"]) - self.assertEqual(1, options["Number of elements across core transition"]) - options["Use Core"] = False context = Context("Test") region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(13, len(annotationGroups)) + self.assertEqual(24, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) From eb3f8624a2e3f1c467553679dd5377825a4c9b83 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Fri, 4 Oct 2024 13:42:35 +1300 Subject: [PATCH 10/20] Improve junction sampling, body foot, curve-surface intersections --- .../meshtypes/meshtype_3d_wholebody2.py | 82 +++++++++++-------- src/scaffoldmaker/utils/tracksurface.py | 16 ++-- src/scaffoldmaker/utils/tubenetworkmesh.py | 71 +++++++++++----- tests/test_network.py | 34 ++++---- tests/test_uterus.py | 8 +- 5 files changed, 126 insertions(+), 85 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 20838c19..12d7ee83 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -1,7 +1,7 @@ """ Generates a 3D body coordinates using tube network mesh. """ -from cmlibs.maths.vectorops import add, cross, mult, set_magnitude +from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates from cmlibs.zinc.node import Node from scaffoldmaker.annotation.annotationgroup import ( @@ -11,8 +11,8 @@ from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base from scaffoldmaker.scaffoldpackage import ScaffoldPackage from scaffoldmaker.utils.interpolation import ( - computeCubicHermiteEndDerivative, interpolateLagrangeHermiteDerivative, sampleCubicHermiteCurvesSmooth, - smoothCubicHermiteDerivativesLine) + computeCubicHermiteEndDerivative, computeCubicHermiteStartDerivative, interpolateLagrangeHermiteDerivative, + sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine, DerivativeScalingMode) from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData import math @@ -68,6 +68,7 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Leg length"] = 10.0 options["Leg top diameter"] = 2.0 options["Leg bottom diameter"] = 0.7 + options["Foot height"] = 1.0 options["Foot length"] = 2.5 options["Foot thickness"] = 0.3 options["Foot width"] = 1.0 @@ -102,6 +103,7 @@ def getOrderedOptionNames(cls): "Leg length", "Leg top diameter", "Leg bottom diameter", + "Foot height", "Foot length", "Foot thickness", "Foot width", @@ -135,6 +137,7 @@ def checkOptions(cls, options): "Leg length", "Leg top diameter", "Leg bottom diameter", + "Foot height", "Foot length", "Foot thickness", "Foot width" @@ -200,6 +203,7 @@ def generateBaseMesh(cls, region, options): legLength = options["Leg length"] legTopRadius = 0.5 * options["Leg top diameter"] legBottomRadius = 0.5 * options["Leg bottom diameter"] + footHeight = options["Foot height"] footLength = options["Foot length"] halfFootThickness = 0.5 * options["Foot thickness"] halfFootWidth = 0.5 * options["Foot width"] @@ -250,21 +254,24 @@ def generateBaseMesh(cls, region, options): elementIdentifier += 1 left = 0 right = 1 - armElementsCount = 7 + armToHandElementsCount = 6 + handElementsCount = 1 armMeshGroup = armGroup.getMeshGroup(mesh) armToHandMeshGroup = armToHandGroup.getMeshGroup(mesh) handMeshGroup = handGroup.getMeshGroup(mesh) for side in (left, right): sideArmGroup = leftArmGroup if (side == left) else rightArmGroup - meshGroups = [bodyMeshGroup, armMeshGroup, sideArmGroup.getMeshGroup(mesh)] - for e in range(armElementsCount): + meshGroups = [bodyMeshGroup, armMeshGroup, armToHandMeshGroup, sideArmGroup.getMeshGroup(mesh)] + for e in range(armToHandElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + meshGroups = [bodyMeshGroup, armMeshGroup, handMeshGroup, sideArmGroup.getMeshGroup(mesh)] + for e in range(handElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) - if e < (armElementsCount - 1): - armToHandMeshGroup.addElement(element) - else: - handMeshGroup.addElement(element) elementIdentifier += 1 thoraxElementsCount = 3 abdomenElementsCount = 4 @@ -280,21 +287,24 @@ def generateBaseMesh(cls, region, options): for meshGroup in meshGroups: meshGroup.addElement(element) elementIdentifier += 1 - legElementsCount = 7 + legToFootElementsCount = 5 + footElementsCount = 2 legMeshGroup = legGroup.getMeshGroup(mesh) legToFootMeshGroup = legToFootGroup.getMeshGroup(mesh) footMeshGroup = footGroup.getMeshGroup(mesh) for side in (left, right): sideLegGroup = leftLegGroup if (side == left) else rightLegGroup - meshGroups = [bodyMeshGroup, legMeshGroup, sideLegGroup.getMeshGroup(mesh)] - for e in range(legElementsCount): + meshGroups = [bodyMeshGroup, legMeshGroup, legToFootMeshGroup, sideLegGroup.getMeshGroup(mesh)] + for e in range(legToFootElementsCount): + element = mesh.findElementByIdentifier(elementIdentifier) + for meshGroup in meshGroups: + meshGroup.addElement(element) + elementIdentifier += 1 + meshGroups = [bodyMeshGroup, legMeshGroup, footMeshGroup, sideLegGroup.getMeshGroup(mesh)] + for e in range(footElementsCount): element = mesh.findElementByIdentifier(elementIdentifier) for meshGroup in meshGroups: meshGroup.addElement(element) - if e < (legElementsCount - 2): - legToFootMeshGroup.addElement(element) - else: - footMeshGroup.addElement(element) elementIdentifier += 1 # set coordinates (outer) @@ -385,7 +395,7 @@ def generateBaseMesh(cls, region, options): shoulderAngleRadians = shoulderRotationFactor * shoulderLimitAngleRadians armStartX = thoraxStartX + shoulderDrop - halfShoulderWidth * math.sin(shoulderAngleRadians) nonHandArmLength = armLength - handLength - armScale = nonHandArmLength / (armElementsCount - 3) + armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count sd3 = [0.0, 0.0, armTopRadius] sid3 = mult(sd3, innerProportionDefault) hd3 = [0.0, 0.0, halfHandWidth] @@ -414,8 +424,8 @@ def generateBaseMesh(cls, region, options): setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) # main part of arm to wrist - for i in range(armElementsCount - 2): - xi = i / (armElementsCount - 3) + for i in range(armToHandElementsCount - 1): + xi = i / (armToHandElementsCount - 2) node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [armStartX + d1[0] * i, armStartY + d1[1] * i, d1[2] * i] @@ -429,6 +439,7 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 # hand + assert handElementsCount == 1 node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) hx = [armStartX + armLength * cosArmAngle, armStartY + armLength * sinArmAngle, 0.0] @@ -442,7 +453,8 @@ def generateBaseMesh(cls, region, options): # legs cos45 = math.cos(0.25 * math.pi) legStartX = abdomenStartX + abdomenLength + pelvisDrop - legScale = legLength / (legElementsCount - 2) + nonFootLegLength = legLength - footHeight + legScale = nonFootLegLength / (legToFootElementsCount - 1) pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] pid3 = mult(pd3, innerProportionDefault) for side in (left, right): @@ -459,14 +471,14 @@ def generateBaseMesh(cls, region, options): node = nodes.findNodeByIdentifier(legJunctionNodeIdentifier) fieldcache.setNode(node) pd1 = interpolateLagrangeHermiteDerivative(px, x, d1, 0.0) - pd2 = set_magnitude(cross(pd3, pd1), 0.5 * legTopRadius + 0.5 * halfTorsoWidth) # GRC + pd2 = set_magnitude(cross(pd3, pd1), 0.5 * legTopRadius + 0.5 * halfTorsoWidth) pid2 = mult(pd2, innerProportionDefault) version = 2 if (side == left) else 3 setNodeFieldVersionDerivatives(coordinates, fieldcache, version, pd1, pd2, pd3) setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, pd1, pid2, pid3) # main part of leg to ankle - for i in range(legElementsCount - 2): - xi = i / (legElementsCount - 2) + for i in range(legToFootElementsCount): + xi = i / legToFootElementsCount node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) x = [legStartX + d1[0] * i, legStartY + d1[1] * i, d1[2] * i] @@ -479,21 +491,19 @@ def generateBaseMesh(cls, region, options): setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) nodeIdentifier += 1 # foot - heelOffset = legBottomRadius * (1.0 - cos45) - fx = [add(add(legStart, mult(legDirn, legLength - legBottomRadius)), - [-heelOffset * cosLegAngle, -heelOffset * sinLegAngle, heelOffset]), + fx = [x, + add(add(legStart, mult(legDirn, legLength - halfFootThickness)), + [0.0, 0.0, 0.25 * footLength + legBottomRadius]), add(add(legStart, mult(legDirn, legLength - halfFootThickness)), [0.0, 0.0, footLength - legBottomRadius])] fd1 = smoothCubicHermiteDerivativesLine( - [x] + fx, [d1, set_magnitude(add(legDirn, legFront), legScale), - [0.0, 0.0, 2.0 * footLength]], - fixAllDirections=True, fixStartDerivative=True, fixEndDerivative=True)[1:] - halfAnkleThickness = math.sqrt(2.0 * legBottomRadius * legBottomRadius) - ankleRadius = legBottomRadius # GRC check - fd2 = [mult(legSide, ankleRadius), mult(legSide, halfFootWidth)] - fd3 = [set_magnitude(cross(fd1[0], fd2[0]), halfAnkleThickness), - set_magnitude(cross(fd1[1], fd2[1]), halfFootThickness)] - for i in range(2): + fx, [d1, [0.0, 0.0, 0.5 * footLength], [0.0, 0.0, 0.5 * footLength]], + fixAllDirections=True, fixStartDerivative=True) + fd2 = [d2, mult(legSide, halfFootWidth), mult(legSide, halfFootWidth)] + fd3 = [d3, + set_magnitude(sub(legFront, legDirn), legBottomRadius), + set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness)] + for i in range(1, 3): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) setNodeFieldParameters(coordinates, fieldcache, fx[i], fd1[i], fd2[i], fd3[i]) diff --git a/src/scaffoldmaker/utils/tracksurface.py b/src/scaffoldmaker/utils/tracksurface.py index e4f93bb2..09bcc78f 100644 --- a/src/scaffoldmaker/utils/tracksurface.py +++ b/src/scaffoldmaker/utils/tracksurface.py @@ -1132,10 +1132,10 @@ def findNearestPositionOnCurve(self, cx, cd1, loop=False, startCurveLocation=Non last_dxi = None for it in range(100): x, d = evaluateCoordinatesOnCurve(cx, cd1, curveLocation, loop, derivative=True) - surfacePosition = self.findNearestPosition(x, surfacePosition, instrument=instrument) + surfacePosition = self.findNearestPosition(x, surfacePosition, instrument=False) # instrument=instrument onOtherBoundary = self.positionOnBoundary(surfacePosition) - other_x = self.evaluateCoordinates(surfacePosition) - r = sub(other_x, x) + ox, od1, od2 = self.evaluateCoordinates(surfacePosition, derivatives=True) + r = sub(ox, x) mag_r = magnitude(r) if instrument: print("iter", it, "curve location", curveLocation, "surface position", surfacePosition, "mag_r", mag_r) @@ -1171,13 +1171,15 @@ def findNearestPositionOnCurve(self, cx, cd1, loop=False, startCurveLocation=Non if slope_factor > MAX_SLOPE_FACTOR: slope_factor = MAX_SLOPE_FACTOR u = mult(rTangent, slope_factor) - # limit by curvature and distance to other_x + # limit by curvature and distance to ox nm = curveLocation[0] np = (nm + 1) % nCount curveCurvature = getCubicHermiteCurvatureSimple(cx[nm], cd1[nm], cx[np], cd1[np], curveLocation[1])[0] - surfaceCurvature1 = self._getDirectionalCurvature(surfacePosition, direction=[1.0, 0.0])[0] - surfaceCurvature2 = self._getDirectionalCurvature(surfacePosition, direction=[0.0, 1.0])[0] - curvature = curveCurvature + surfaceCurvature1 + surfaceCurvature2 + curveSurfaceDirection = calculate_surface_delta_xi(od1, od2, u) + surfaceCurvature = ( + self._getDirectionalCurvature(surfacePosition, direction=normalize(curveSurfaceDirection))[0] + if any(curveSurfaceDirection) else 0.0) + curvature = curveCurvature + surfaceCurvature uNormal = sub(r, u) un = magnitude(uNormal) # GRC check: diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index cb81b5ab..4cf33908 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -975,11 +975,42 @@ def _createBoxBoundaryNodeIdsList(self, startSkipCount=None, endSkipCount=None): @classmethod def blendSampledCoordinates(cls, segment1, nodeIndexAlong1, segment2, nodeIndexAlong2): nodesCountAround = segment1._elementsCountAround - nodesCountRim = len(segment1._rimCoordinates[0][0]) + nodesCountRim = len(segment1._rimCoordinates[0][nodeIndexAlong1]) if ((nodesCountAround != segment2._elementsCountAround) or - (nodesCountRim != len(segment2._rimCoordinates[0][0]))): + (nodesCountRim != len(segment2._rimCoordinates[0][nodeIndexAlong2]))): return # can't blend unless these match + if segment1._isCore and segment2._isCore: + nodesCountAcrossMajor = len(segment1._boxCoordinates[0][nodeIndexAlong1]) + nodesCountAcrossMinor = len(segment1._boxCoordinates[0][nodeIndexAlong1][0]) + nodesCountTransition = len(segment1._transitionCoordinates[0][nodeIndexAlong1]) if segment1._transitionCoordinates else 0 + if ((nodesCountAcrossMajor != len(segment2._boxCoordinates[0][nodeIndexAlong2])) or + (nodesCountAcrossMinor != len(segment2._boxCoordinates[0][nodeIndexAlong2][0])) or + (nodesCountTransition != (len(segment2._transitionCoordinates[0][nodeIndexAlong2]) + if segment1._transitionCoordinates else 0))): + return # can't blend unless these match + # blend core coordinates + s1d2 = segment1._boxCoordinates[2][nodeIndexAlong1] + s2d2 = segment2._boxCoordinates[2][nodeIndexAlong2] + for m in range(nodesCountAcrossMajor): + for n in range(nodesCountAcrossMinor): + # harmonic mean magnitude + s1d2Mag = magnitude(s1d2[m][n]) + s2d2Mag = magnitude(s2d2[m][n]) + d2Mag = 2.0 / ((1.0 / s1d2Mag) + (1.0 / s2d2Mag)) + s2d2[m][n] = s1d2[m][n] = mult(s1d2[m][n], d2Mag / s1d2Mag) + for n3 in range(nodesCountTransition): + s1d2 = segment1._transitionCoordinates[2][nodeIndexAlong1][n3] + s2d2 = segment2._transitionCoordinates[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)) + s2d2[n1] = s1d2[n1] = mult(s1d2[n1], d2Mag / s1d2Mag) + elif segment1._isCore or segment2._isCore: + return # can't blend if both don't have core + # blend rim coordinates for n3 in range(nodesCountRim): s1d2 = segment1._rimCoordinates[2][nodeIndexAlong1][n3] @@ -989,9 +1020,7 @@ def blendSampledCoordinates(cls, segment1, nodeIndexAlong1, segment2, nodeIndexA 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 + s2d2[n1] = s1d2[n1] = mult(s1d2[n1], d2Mag / s1d2Mag) def getSampledElementsCountAlong(self): return len(self._sampledTubeCoordinates[0][0]) - 1 @@ -1358,17 +1387,17 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): """ :param n2Only: If set, create nodes only for that single n2 index along. Must be >= 0! """ - if (not n2Only) and generateData.isShowTrimSurfaces(): - dimension = generateData.getMeshDimension() - nodeIdentifier, elementIdentifier = generateData.getNodeElementIdentifiers() - faceIdentifier = elementIdentifier if (dimension == 2) else None - annotationGroup = generateData.getNewTrimAnnotationGroup() - nodeIdentifier, faceIdentifier = \ - self._rawTrackSurfaceList[0].generateMesh(generateData.getRegion(), nodeIdentifier, faceIdentifier, - group_name=annotationGroup.getName()) - if dimension == 2: - elementIdentifier = faceIdentifier - generateData.setNodeElementIdentifiers(nodeIdentifier, elementIdentifier) + # if (not n2Only) and generateData.isShowTrimSurfaces(): + # dimension = generateData.getMeshDimension() + # nodeIdentifier, elementIdentifier = generateData.getNodeElementIdentifiers() + # faceIdentifier = elementIdentifier if (dimension == 2) else None + # annotationGroup = generateData.getNewTrimAnnotationGroup() + # nodeIdentifier, faceIdentifier = \ + # self._rawTrackSurfaceList[0].generateMesh(generateData.getRegion(), nodeIdentifier, faceIdentifier, + # group_name=annotationGroup.getName()) + # if dimension == 2: + # elementIdentifier = faceIdentifier + # generateData.setNodeElementIdentifiers(nodeIdentifier, elementIdentifier) elementsCountAlong = len(self._rimCoordinates[0]) - 1 elementsCountRim = self.getElementsCountRim() @@ -1844,7 +1873,7 @@ def _sampleMidPoint(self, segmentsParameterLists): for s1 in range(segmentsCount - 1): # fxs1 = segmentsParameterLists[s1][0][0] fd2s1 = segmentsParameterLists[s1][2][0] - if segmentsIn[s1]: + if not segmentsIn[s1]: fd2s1 = [-d for d in fd2s1] norm_fd2s1 = normalize(fd2s1) for s2 in range(s1 + 1, segmentsCount): @@ -1871,13 +1900,13 @@ def _sampleMidPoint(self, segmentsParameterLists): md1.append(mult(add(hd1[s1], [-d for d in hd1[s2]]), 0.5)) # fxs2 = segmentsParameterLists[s2][0][0] fd2s2 = segmentsParameterLists[s2][2][0] - if not segmentsIn[s1]: - fd2s2 = [-d for d in fd2s1] + if segmentsIn[s2]: + fd2s2 = [-d for d in fd2s2] norm_fd2s2 = normalize(fd2s2) # reduce md2 up to 50% depending on how out-of line they are - md2Factor = 1.0 + 0.5 * dot(norm_fd2s1, norm_fd2s2) + md2Factor = 0.75 + 0.25 * dot(norm_fd2s1, norm_fd2s2) md2.append(mult(cd2, md2Factor)) - # smooth smx, smd2 with 2nd row from end coordinates and derivatives + # smooth md2 with 2nd row from end, which it actually interpolates to # tmd2 = smoothCubicHermiteDerivativesLine( # [fxs1, cx, fxs2], [fd2s1, cd2, fd2s2], fixStartDerivative=True, fixEndDerivative=True, # magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) diff --git a/tests/test_network.py b/tests/test_network.py index 9c5d45cd..a7a4ce63 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -124,7 +124,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.9345569273205805, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 1.931297913271377, delta=X_TOL) def test_2d_tube_network_snake(self): """ @@ -243,7 +243,7 @@ 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.045738080224775, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 4.045580778559924, delta=X_TOL) def test_2d_tube_network_trifurcation(self): """ @@ -288,7 +288,7 @@ def test_2d_tube_network_trifurcation(self): fieldcache = fieldmodule.createFieldcache() result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 2.799227811309674, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.7951498826590973, delta=X_TOL) def test_2d_tube_network_vase(self): """ @@ -407,9 +407,9 @@ def test_3d_tube_network_bifurcation(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.0350166554737855, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 1.9320679669979417, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 1.5635839608316708, delta=X_TOL) + self.assertAlmostEqual(volume, 0.034977175657495335, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 1.92967134964477, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 1.5609615408034399, delta=X_TOL) def test_3d_tube_network_bifurcation_core(self): """ @@ -470,8 +470,8 @@ def test_3d_tube_network_bifurcation_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.09934789293818859, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.0262176284480025, delta=X_TOL) + self.assertAlmostEqual(volume, 0.09907643906540035, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.0238210110948307, delta=X_TOL) def test_3d_tube_network_sphere_cube(self): """ @@ -564,8 +564,8 @@ def test_3d_tube_network_sphere_cube(self): self.assertEqual(result, RESULT_OK) self.assertAlmostEqual(volume, 0.07364074411579775, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 4.045725519817575, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 3.3315247370347674, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 4.0455683508806, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 3.3313407313622867, delta=X_TOL) def test_3d_tube_network_sphere_cube_core(self): """ @@ -652,8 +652,8 @@ def test_3d_tube_network_sphere_cube_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.21361144824524667, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 4.045725519817576, delta=X_TOL) + self.assertAlmostEqual(volume, 0.21360737563518303, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 4.0455683508805995, delta=X_TOL) def test_3d_tube_network_trifurcation_cross(self): @@ -746,9 +746,9 @@ def test_3d_tube_network_trifurcation_cross(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.047265446041689, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 2.6025349554689585, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 2.1185123279913802, delta=X_TOL) + self.assertAlmostEqual(volume, 0.047235196748105515, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.600683124988524, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 2.1164499963602124, delta=X_TOL) def test_3d_tube_network_trifurcation_cross_core(self): """ @@ -837,8 +837,8 @@ def test_3d_tube_network_trifurcation_cross_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.13518934925801437, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.7269545818490495, delta=X_TOL) + self.assertAlmostEqual(volume, 0.13499394208386956, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.7252510632808065, delta=X_TOL) def test_3d_box_network_bifurcation(self): """ diff --git a/tests/test_uterus.py b/tests/test_uterus.py index 8186b792..b626a935 100644 --- a/tests/test_uterus.py +++ b/tests/test_uterus.py @@ -65,8 +65,8 @@ def test_uterus1(self): coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - assertAlmostEqualList(self, minimums, [-9.361977045958657, -0.048, -8.92088553607632], 1.0E-6) - assertAlmostEqualList(self, maximums, [9.36197704595863, 12.846826704128778, 1.09], 1.0E-6) + assertAlmostEqualList(self, minimums, [-9.361977045958657, -0.048, -8.920886838294868], 1.0E-6) + assertAlmostEqualList(self, maximums, [9.36197704595863, 12.846826512420462, 1.09], 1.0E-6) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -80,8 +80,8 @@ def test_uterus1(self): self.assertEqual(result, RESULT_OK) result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 262.70532322657914, delta=1.0E-6) - self.assertAlmostEqual(volume, 183.66628268786462, delta=1.0E-6) + self.assertAlmostEqual(surfaceArea, 262.3194982053719, delta=1.0E-6) + self.assertAlmostEqual(volume, 183.07329124848016, delta=1.0E-6) fieldmodule.defineAllFaces() for annotationGroup in annotationGroups: From 340dc9782ac03df7984091bec84e04a6b4f8444c Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Thu, 10 Oct 2024 17:37:50 +1300 Subject: [PATCH 11/20] Fix body layout derivatives, core/shell scaling --- .../meshtypes/meshtype_3d_wholebody2.py | 134 +++++--- src/scaffoldmaker/utils/eft_utils.py | 108 ++++++- src/scaffoldmaker/utils/tubenetworkmesh.py | 301 ++++++++++-------- tests/test_wholebody2.py | 94 ++++-- 4 files changed, 433 insertions(+), 204 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index 12d7ee83..ef5631f6 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -3,6 +3,7 @@ """ from cmlibs.maths.vectorops import add, cross, mult, set_magnitude, sub from cmlibs.utils.zinc.field import Field, find_or_create_field_coordinates +from cmlibs.zinc.element import Element from cmlibs.zinc.node import Node from scaffoldmaker.annotation.annotationgroup import ( AnnotationGroup, findOrCreateAnnotationGroupForTerm, getAnnotationGroupForTerm) @@ -11,8 +12,8 @@ from scaffoldmaker.meshtypes.scaffold_base import Scaffold_base from scaffoldmaker.scaffoldpackage import ScaffoldPackage from scaffoldmaker.utils.interpolation import ( - computeCubicHermiteEndDerivative, computeCubicHermiteStartDerivative, interpolateLagrangeHermiteDerivative, - sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine, DerivativeScalingMode) + computeCubicHermiteEndDerivative, getCubicHermiteArcLength, interpolateLagrangeHermiteDerivative, + sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine) from scaffoldmaker.utils.networkmesh import NetworkMesh from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData import math @@ -346,7 +347,6 @@ def generateBaseMesh(cls, region, options): armJunctionNodeIdentifier = nodeIdentifier thoraxScale = thoraxLength / thoraxElementsCount - d2 = [0.0, halfTorsoWidth, 0.0] thoraxStartX = headLength + neckLength sx = [thoraxStartX, 0.0, 0.0] for i in range(thoraxElementsCount): @@ -355,16 +355,22 @@ def generateBaseMesh(cls, region, options): x = [thoraxStartX + thoraxScale * i, 0.0, 0.0] if i == 0: d1 = [0.5 * (neckScale + thoraxScale), 0.0, 0.0] + d2 = [0.0, 0.5 * (halfTorsoWidth + halfHeadWidth), 0.0] + d12 = [0.0, halfTorsoWidth - halfHeadWidth, 0.0] d3 = [0.0, 0.0, 0.5 * (halfHeadWidth + halfTorsoDepth)] - id2 = mult(d2, 0.5 * (innerProportionHead + innerProportionDefault)) + id2 = [0.0, 0.5 * (innerProportionHead * halfHeadWidth + innerProportionDefault * halfTorsoWidth), 0.0] + id12 = [0.0, innerProportionDefault * halfTorsoWidth - innerProportionHead * halfHeadWidth, 0.0] id3 = mult(d3, 0.5 * (innerProportionHead + innerProportionDefault)) else: d1 = [thoraxScale, 0.0, 0.0] + d2 = [0.0, halfTorsoWidth, 0.0] + d12 = None d3 = [0.0, 0.0, halfTorsoDepth] id2 = mult(d2, innerProportionDefault) + id12 = None id3 = mult(d3, innerProportionDefault) - setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) - setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12) nodeIdentifier += 1 abdomenScale = abdomenLength / abdomenElementsCount @@ -396,8 +402,8 @@ def generateBaseMesh(cls, region, options): armStartX = thoraxStartX + shoulderDrop - halfShoulderWidth * math.sin(shoulderAngleRadians) nonHandArmLength = armLength - handLength armScale = nonHandArmLength / (armToHandElementsCount - 2) # 2 == shoulder elements count - sd3 = [0.0, 0.0, armTopRadius] - sid3 = mult(sd3, innerProportionDefault) + d12_mag = (halfWristThickness - armTopRadius) / (armToHandElementsCount - 2) + d13_mag = (halfWristWidth - armTopRadius) / (armToHandElementsCount - 2) hd3 = [0.0, 0.0, halfHandWidth] hid3 = mult(hd3, innerProportionDefault) for side in (left, right): @@ -410,19 +416,49 @@ def generateBaseMesh(cls, region, options): # set leg versions 2 (left) and 3 (right) on leg junction node, and intermediate shoulder node sd1 = interpolateLagrangeHermiteDerivative(sx, x, d1, 0.0) nx, nd1 = sampleCubicHermiteCurvesSmooth([sx, x], [sd1, d1], 2, derivativeMagnitudeEnd=armScale)[0:2] - for n in range(2): - node = nodes.findNodeByIdentifier(nodeIdentifier if (n > 0) else armJunctionNodeIdentifier) + arcLengths = [getCubicHermiteArcLength(nx[i], nd1[i], nx[i + 1], nd1[i + 1]) for i in range(2)] + sd2_list = [] + sd3_list = [] + sNodeIdentifiers = [] + for i in range(2): + sNodeIdentifiers.append(nodeIdentifier if (i > 0) else armJunctionNodeIdentifier) + node = nodes.findNodeByIdentifier(sNodeIdentifiers[-1]) fieldcache.setNode(node) - version = 1 if (n > 0) else 2 if (side == left) else 3 - sd1 = nd1[n] - sd2 = set_magnitude(cross(sd3, sd1), armTopRadius) + version = 1 if (i > 0) else 2 if (side == left) else 3 + sd1 = nd1[i] + sDistance = sum(arcLengths[i:]) + sHalfHeight = armTopRadius + sDistance * -d12_mag + sHalfDepth = armTopRadius + sDistance * -d13_mag + sd3 = [0.0, 0.0, sHalfDepth] + sid3 = mult(sd3, innerProportionDefault) + sd2 = set_magnitude(cross(sd3, sd1), sHalfHeight) sid2 = mult(sd2, innerProportionDefault) - if n > 0: + sd2_list.append(sd2) + sd3_list.append(sd3) + if i > 0: for field in (coordinates, innerCoordinates): - field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, nx[n]) + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, nx[i]) nodeIdentifier += 1 setNodeFieldVersionDerivatives(coordinates, fieldcache, version, sd1, sd2, sd3) setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, sd1, sid2, sid3) + sd2_list.append([-armTopRadius * sinArmAngle, armTopRadius * cosArmAngle, 0.0]) + sd3_list.append([0.0, 0.0, armTopRadius]) + for i in range(2): + node = nodes.findNodeByIdentifier(sNodeIdentifiers[i]) + fieldcache.setNode(node) + version = 1 if (i > 0) else 2 if (side == left) else 3 + sd12 = sub(sd2_list[i + 1], sd2_list[i]) + sd13 = sub(sd3_list[i + 1], sd3_list[i]) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sd12) + coordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sd13) + sid12 = mult(sd12, innerProportionDefault) + sid13 = mult(sd13, innerProportionDefault) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, sid12) + innerCoordinates.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, sid13) + d12 = [-d12_mag * sinArmAngle, d12_mag * cosArmAngle, 0.0] + id12 = mult(d12, innerProportionDefault) + d13 = [0.0, 0.0, d13_mag] + id13 = mult(d13, innerProportionDefault) # main part of arm to wrist for i in range(armToHandElementsCount - 1): xi = i / (armToHandElementsCount - 2) @@ -435,8 +471,8 @@ def generateBaseMesh(cls, region, options): d3 = [0.0, 0.0, halfWidth] id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) - setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) - setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # hand assert handElementsCount == 1 @@ -455,6 +491,9 @@ def generateBaseMesh(cls, region, options): legStartX = abdomenStartX + abdomenLength + pelvisDrop nonFootLegLength = legLength - footHeight legScale = nonFootLegLength / (legToFootElementsCount - 1) + d12_mag = (legBottomRadius - legTopRadius) / (armToHandElementsCount - 2) + d13_mag = (legBottomRadius - legTopRadius) / (armToHandElementsCount - 2) + pd3 = [0.0, 0.0, 0.5 * legTopRadius + 0.5 * halfTorsoDepth] pid3 = mult(pd3, innerProportionDefault) for side in (left, right): @@ -473,9 +512,17 @@ def generateBaseMesh(cls, region, options): pd1 = interpolateLagrangeHermiteDerivative(px, x, d1, 0.0) pd2 = set_magnitude(cross(pd3, pd1), 0.5 * legTopRadius + 0.5 * halfTorsoWidth) pid2 = mult(pd2, innerProportionDefault) + pd12 = sub(mult(legSide, legTopRadius), pd2) + pd13 = sub([0.0, 0.0, legTopRadius], pd3) + pid12 = mult(pd12, innerProportionDefault) + pid13 = mult(pd13, innerProportionDefault) version = 2 if (side == left) else 3 - setNodeFieldVersionDerivatives(coordinates, fieldcache, version, pd1, pd2, pd3) - setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, pd1, pid2, pid3) + setNodeFieldVersionDerivatives(coordinates, fieldcache, version, pd1, pd2, pd3, pd12, pd13) + setNodeFieldVersionDerivatives(innerCoordinates, fieldcache, version, pd1, pid2, pid3, pid12, pid13) + d12 = [-d12_mag * sinLegAngle, d12_mag * cosLegAngle, 0.0] + id12 = mult(d12, innerProportionDefault) + d13 = [0.0, 0.0, d13_mag] + id13 = mult(d13, innerProportionDefault) # main part of leg to ankle for i in range(legToFootElementsCount): xi = i / legToFootElementsCount @@ -487,8 +534,8 @@ def generateBaseMesh(cls, region, options): d3 = [0.0, 0.0, radius] id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) - setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3) - setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3) + setNodeFieldParameters(coordinates, fieldcache, x, d1, d2, d3, d12, d13) + setNodeFieldParameters(innerCoordinates, fieldcache, x, d1, id2, id3, id12, id13) nodeIdentifier += 1 # foot fx = [x, @@ -503,26 +550,19 @@ def generateBaseMesh(cls, region, options): fd3 = [d3, set_magnitude(sub(legFront, legDirn), legBottomRadius), set_magnitude(cross(fd1[2], fd2[2]), halfFootThickness)] + fd12 = sub(fd2[2], fd2[1]) + fd13 = sub(fd3[2], fd3[1]) + fid12 = mult(fd12, innerProportionDefault) + fid13 = mult(fd13, innerProportionDefault) for i in range(1, 3): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) - setNodeFieldParameters(coordinates, fieldcache, fx[i], fd1[i], fd2[i], fd3[i]) + setNodeFieldParameters(coordinates, fieldcache, fx[i], fd1[i], fd2[i], fd3[i], fd12, fd13) fid2 = mult(fd2[i], innerProportionDefault) fid3 = mult(fd3[i], innerProportionDefault) - setNodeFieldParameters(innerCoordinates, fieldcache, fx[i], fd1[i], fid2, fid3) + setNodeFieldParameters(innerCoordinates, fieldcache, fx[i], fd1[i], fid2, fid3, fid12, fid13) nodeIdentifier += 1 - smoothOptions = { - "Field": {"coordinates": True, "inner coordinates": False}, - "Smooth D12": True, - "Smooth D13": True} - cls.smoothSideCrossDerivatives(region, options, networkMesh, smoothOptions, None) - smoothOptions = { - "Field": {"coordinates": False, "inner coordinates": True}, - "Smooth D12": True, - "Smooth D13": True} - cls.smoothSideCrossDerivatives(region, options, networkMesh, smoothOptions, None) - return annotationGroups, networkMesh @classmethod @@ -694,11 +734,11 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Number of elements along hand"] = 2 options["Number of elements along leg to foot"] = 8 options["Number of elements along foot"] = 3 - options["Number of elements around head"] = 24 - options["Number of elements around torso"] = 24 + options["Number of elements around head"] = 20 + options["Number of elements around torso"] = 20 options["Number of elements around arm"] = 12 options["Number of elements around leg"] = 16 - options["Number of elements through shell"] = 1 + options["Number of elements through shell"] = 2 options["Number of elements across core box minor"] = 4 return options @@ -928,9 +968,11 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): mesh1d = fieldmodule.findMeshByDimension(1) is_exterior = fieldmodule.createFieldIsExterior() + is_face_xi3_0 = fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_0) skinGroup = findOrCreateAnnotationGroupForTerm(annotationGroups, region, get_body_term("skin epidermis")) - is_skin = is_exterior + is_skin = is_exterior if isCore else fieldmodule.createFieldAnd( + is_exterior, fieldmodule.createFieldNot(is_face_xi3_0)) skinGroup.getMeshGroup(mesh2d).addElementsConditional(is_skin) leftArmGroup = getAnnotationGroupForTerm(annotationGroups, get_body_term("left arm")) @@ -998,7 +1040,7 @@ def defineFaceAnnotations(cls, region, options, annotationGroups): spinalCordGroup.getMeshGroup(mesh1d).addElementsConditional(is_spinal_cord) -def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3): +def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3, d12=None, d13=None): """ Assign node field parameters x, d1, d2, d3 of field. :param field: Field parameters to assign. @@ -1007,15 +1049,21 @@ def setNodeFieldParameters(field, fieldcache, x, d1, d2, d3): :param d1: Parameters to set for Node.VALUE_LABEL_D_DS1. :param d2: Parameters to set for Node.VALUE_LABEL_D_DS2. :param d3: Parameters to set for Node.VALUE_LABEL_D_DS3. + :param d12: Optional parameters to set for Node.VALUE_LABEL_D2_DS1DS2. + :param d13: Optional parameters to set for Node.VALUE_LABEL_D2_DS1DS3. :return: """ field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_VALUE, 1, x) field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, 1, d1) field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, 1, d2) field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, 1, d3) + if d12: + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, 1, d12) + if d13: + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, 1, d13) -def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3): +def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3, d12=None, d13=None): """ Assign node field parameters d1, d2, d3 of field. :param field: Field to assign parameters of. @@ -1024,8 +1072,14 @@ def setNodeFieldVersionDerivatives(field, fieldcache, version, d1, d2, d3): :param d1: Parameters to set for Node.VALUE_LABEL_D_DS1. :param d2: Parameters to set for Node.VALUE_LABEL_D_DS2. :param d3: Parameters to set for Node.VALUE_LABEL_D_DS3. + :param d12: Optional parameters to set for Node.VALUE_LABEL_D2_DS1DS2. + :param d13: Optional parameters to set for Node.VALUE_LABEL_D2_DS1DS3. :return: """ field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS1, version, d1) field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS2, version, d2) field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D_DS3, version, d3) + if d12: + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS2, version, d12) + if d13: + field.setNodeParameters(fieldcache, -1, Node.VALUE_LABEL_D2_DS1DS3, version, d13) diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index e2b34343..3d57710d 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -5,7 +5,8 @@ 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 +from scaffoldmaker.utils.interpolation import ( + computeCubicHermiteEndDerivative, interpolateHermiteLagrangeDerivative, interpolateLagrangeHermiteDerivative) import copy import math @@ -175,9 +176,12 @@ def scaleEftNodeValueLabels(eft, localNodeIndexes, valueLabels, addScaleFactorIn def setEftScaleFactorIds(eft, globalScaleFactorIds, nodeScaleFactorIds): - ''' + """ Set general followed by node scale factor identifiers. - ''' + :param eft: Elementfieldtemplate to modify. + :param globalScaleFactorIds: List of global scale factor identifiers. + :param nodeScaleFactorIds: List of node scale factor identifiers. + """ eft.setNumberOfLocalScaleFactors(len(globalScaleFactorIds) + len(nodeScaleFactorIds)) s = 1 for id in globalScaleFactorIds: @@ -190,6 +194,37 @@ def setEftScaleFactorIds(eft, globalScaleFactorIds, nodeScaleFactorIds): s += 1 +def addEftNodeScaleFactorIds(eft, nodeScaleFactorIds): + """ + Add more node-based scale factors to EFT. + :param eft: Elementfieldtemplate to modify. + :param nodeScaleFactorIds: List of node scale factor identifiers. + :return: Local indexes of added scale factors. + """ + oldCount = eft.getNumberOfLocalScaleFactors() + addCount = len(nodeScaleFactorIds) + eft.setNumberOfLocalScaleFactors(oldCount + addCount) + s = oldCount + 1 + for id in nodeScaleFactorIds: + eft.setScaleFactorType(s, Elementfieldtemplate.SCALE_FACTOR_TYPE_NODE_GENERAL) + eft.setScaleFactorIdentifier(s, id) + return [oldCount + n for n in range(1, addCount + 1)] + + +def addScaleEftNodesValueLabel(eft, localNodeIndexes, valueLabel, nodeScaleFactorId): + """ + Create new node scale factors for each local node index with the nodeScaleFactorId and + scale each instance of just value label at the local node indexes by the respective scale factor. + :param eft: Elementfieldtemplate to modify. + :param localNodeIndexes: List of local node indexes to scale at. + :param valueLabel: Node value label to scale. + :param nodeScaleFactorId: Node-local scale factor ID to use. + """ + sfIndexes = addEftNodeScaleFactorIds(eft, [nodeScaleFactorId] * len(localNodeIndexes)) + for n in range(len(localNodeIndexes)): + scaleEftNodeValueLabels(eft, [localNodeIndexes[n]], [Node.VALUE_LABEL_D_DS3], [sfIndexes[n]]) + + def createEftElementSurfaceLayer(elementIn, eftIn, eftfactory, eftStd, removeNodeValueLabel=None): """ Create eft for layer above xi3 = 1, from either tricubic hermite or bicubic hermite linear. @@ -844,3 +879,70 @@ def determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts): deltas[on][ed] = otherElementDerivative return eft, scalefactors + + +CubicHermiteSerendipityValueLabels = [ + Node.VALUE_LABEL_VALUE, Node.VALUE_LABEL_D_DS1, Node.VALUE_LABEL_D_DS2, Node.VALUE_LABEL_D_DS3] + + +def getTricubicHermiteSerendipityElementNodeParameter(eft, scalefactors, nodeParameters, localNodeIndex, valueLabel): + """ + Get effective element parameter for basis' original valueLabel at local node index. + Currently assumes extra versions are not used, but this is not checked. + :param eft: Element field template. + :param scalefactors: List of scale factors. + :param nodeParameters: List over 8 (3-D) + local nodes in Zinc ordering of 4 parameter vectors x, d1, d2, d3 each with 3 components. + :param localNodeIndex: 1-based local node index on original basis. + :param valueLabel: A value label mapped by CubicHermiteSerendipityFunctionOffset. + :return: Parameter. + """ + parameter = [0.0, 0.0, 0.0] + bn = localNodeIndex - 1 + valueIndex = CubicHermiteSerendipityValueLabels.index(valueLabel) + func = 4 * bn + valueIndex + 1 + termCount = eft.getFunctionNumberOfTerms(func) + for term in range(1, termCount + 1): + component = nodeParameters[eft.getTermLocalNodeIndex(func, term) - 1][valueIndex] + scalefactorCount, scalefactorIndexes = eft.getTermScaling(func, term, 0) + if scalefactorCount > 0: + scalefactorCount, scalefactorIndexes = eft.getTermScaling(func, term, scalefactorCount) + if scalefactorCount == 1: + scalefactorIndexes = [scalefactorIndexes] + scalefactor = 1.0 + for sfi in scalefactorIndexes: + scalefactor *= scalefactors[sfi - 1] + component = mult(component, scalefactor) + parameter = add(parameter, component) + return parameter + + +def addTricubicHermiteSerendipityEftParameterScaling(eft, scalefactors, nodeParameters, localNodeIndexes, valueLabel): + """ + Scale tricubic hermite serendipity element field template value label at local nodes to fit actual parameters. + Parameter must currently be an unscaled single term expression at that local node. + :param eft: Element field template e.g. returned from determineCubicHermiteSerendipityEft. + Note: be careful to pass a separate eft to this function from any cached which do not needing scaling! + :param scalefactors: Existing scale factors for element + :param nodeParameters: As passed to determineCubicHermiteSerendipityEft: list over 8 (3-D) + local nodes in Zinc ordering of 4 parameter vectors x, d1, d2, d3 each with 3 components. + :param localNodeIndexes: Local node indexes to scale value label at. Currently must be in [5, 6, 7, 8]. + :param valueLabel: Single value label to scale. Currently only implemened for D_DS3. + :return: Modified eft, scalefactors + """ + assert len(nodeParameters) == 8 + assert valueLabel == Node.VALUE_LABEL_D_DS3 + newScalefactors = copy.copy(scalefactors) if scalefactors else [] + for ln in localNodeIndexes: + assert ln in [5, 6, 7, 8] + na = ln - 5 + nb = na + 4 + xa = nodeParameters[na][0] + da = getTricubicHermiteSerendipityElementNodeParameter(eft, scalefactors, nodeParameters, na + 1, valueLabel) + xb = nodeParameters[nb][0] + db = nodeParameters[nb][3] + dbScaled = computeCubicHermiteEndDerivative(xa, da, xb, db) + scalefactor = magnitude(dbScaled) / magnitude(db) + newScalefactors.append(scalefactor) + addScaleEftNodesValueLabel(eft, localNodeIndexes, Node.VALUE_LABEL_D_DS3, 3) + return eft, newScalefactors diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 4cf33908..cf0331d7 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -4,10 +4,11 @@ from cmlibs.maths.vectorops import add, cross, dot, magnitude, mult, normalize, set_magnitude, sub, rejection from cmlibs.zinc.element import Element, Elementbasis from cmlibs.zinc.node import Node -from scaffoldmaker.utils.eft_utils import determineCubicHermiteSerendipityEft, HermiteNodeLayoutManager +from scaffoldmaker.utils.eft_utils import ( + addTricubicHermiteSerendipityEftParameterScaling, determineCubicHermiteSerendipityEft, HermiteNodeLayoutManager) from scaffoldmaker.utils.interpolation import ( - computeCubicHermiteDerivativeScaling, computeCubicHermiteEndDerivative, DerivativeScalingMode, - evaluateCoordinatesOnCurve, getCubicHermiteTrimmedCurvesLengths, getNearestLocationOnCurve, + computeCubicHermiteDerivativeScaling, computeCubicHermiteEndDerivative, computeCubicHermiteStartDerivative, + DerivativeScalingMode, evaluateCoordinatesOnCurve, getCubicHermiteTrimmedCurvesLengths, getNearestLocationOnCurve, interpolateCubicHermite, interpolateCubicHermiteDerivative, interpolateHermiteLagrangeDerivative, interpolateLagrangeHermiteDerivative, interpolateSampleCubicHermite, sampleCubicHermiteCurves, @@ -50,11 +51,11 @@ def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurface self._standardElementtemplate = self._mesh.createElementtemplate() self._standardElementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE if (meshDimension == 3) else Element.SHAPE_TYPE_SQUARE) - elementbasis = self._fieldmodule.createElementbasis( + self._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._elementbasis.setFunctionType(3, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) + self._standardEft = self._mesh.createElementfieldtemplate(self._elementbasis) self._standardElementtemplate.defineField(self._coordinates, -1, self._standardEft) d3Defined = (meshDimension == 3) and not isLinearThroughWall @@ -71,12 +72,14 @@ def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurface d3Defined, limitDirections=[None, [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]], None]) self._nodeLayoutTransitionTriplePoint = None - - def getStandardEft(self): - return self._standardEft - def getStandardElementtemplate(self): - return self._standardElementtemplate + return self._standardElementtemplate, self._standardEft + + def createElementfieldtemplate(self): + """ + Create a new standard element field template for modifying. + """ + return self._mesh.createElementfieldtemplate(self._elementbasis) def getNodeLayout6Way(self): return self._nodeLayout6Way @@ -217,7 +220,7 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem 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 = [[[], [], [], []] for p in range(self._pathsCount)] - self._rimCoordinates = None + self._rimCoordinates = None # these are just shell coordinates; with core there may also be transition coords self._rimNodeIds = None self._rimElementIds = None # [e2][e3][e1] @@ -579,9 +582,9 @@ def _generateCoreCoordinates(self, n2, centre): Transition elements are determined by blending from the outside of the box to the shell. :param n2: Index along segment. :param centre: Centre coordinates of core. - :return: box coordinates cbx, cbd1, cbd3, transition coordinates ctx, ctd1, ctd3. Box coordinates - are over [minorBoxNodeCount][majorBoxNodeCount]. Transition coordinates are over [e3][e1] and are - only non-empty for at least 2 transition elements as they are the layers between the box and the shell. + :return: box coordinates cbx, cbd1, cbd3, transition coordinates ctx, ctd1, ctd3. + Box coordinates are over [minorBoxNodeCount][majorBoxNodeCount]. Transition coordinates are over [n3][n1] and + are only non-empty for at least 2 transition elements as they are the layers between the box and the shell. """ # sample radially across major, minor and both diagonals, like a Union Jack major_n1 = 0 @@ -718,76 +721,75 @@ def _generateCoreCoordinates(self, n2, centre): cbd1.append(row_d1) cbd3.append(row_d3) - if self._elementsCountTransition > 1: - for i in range(self._elementsCountTransition - 1): - for lst in (ctx, ctd1, ctd3): - lst.append([None] * self._elementsCountAround) - ix = self._rimCoordinates[0][n2][0] - id1 = self._rimCoordinates[1][n2][0] - id3 = self._rimCoordinates[3][n2][0] - start_bn3 = minorBoxSize // 2 - topLeft_n1 = minorBoxSize - start_bn3 - topRight_n1 = topLeft_n1 + majorBoxSize - bottomRight_n1 = topRight_n1 + minorBoxSize - bottomLeft_n1 = bottomRight_n1 + majorBoxSize - for n1 in range(self._elementsCountAround): - if n1 <= topLeft_n1: - bn1 = 0 - bn3 = start_bn3 + n1 - if n1 < topLeft_n1: - start_d1 = cbd3[bn1][bn3] - start_d3 = [-d for d in cbd1[bn1][bn3]] - else: - start_d1 = add(cbd3[bn1][bn3], cbd1[bn1][bn3]) - start_d3 = sub(cbd3[bn1][bn3], cbd1[bn1][bn3]) - elif n1 <= topRight_n1: - bn1 = n1 - topLeft_n1 - bn3 = minorBoxSize - if n1 < topRight_n1: - start_d1 = cbd1[bn1][bn3] - start_d3 = cbd3[bn1][bn3] - else: - start_d1 = sub(cbd1[bn1][bn3], cbd3[bn1][bn3]) - start_d3 = add(cbd1[bn1][bn3], cbd3[bn1][bn3]) - elif n1 <= bottomRight_n1: - bn1 = majorBoxSize - bn3 = minorBoxSize - (n1 - topRight_n1) - if n1 < bottomRight_n1: - start_d1 = [-d for d in cbd3[bn1][bn3]] - start_d3 = cbd1[bn1][bn3] - else: - start_d1 = [-d for d in add(cbd1[bn1][bn3], cbd3[bn1][bn3])] - start_d3 = sub(cbd1[bn1][bn3], cbd3[bn1][bn3]) - elif n1 <= bottomLeft_n1: - bn1 = majorBoxSize - (n1 - bottomRight_n1) - bn3 = 0 - if n1 < bottomLeft_n1: - start_d1 = [-d for d in cbd1[bn1][bn3]] - start_d3 = [-d for d in cbd3[bn1][bn3]] - else: - start_d1 = sub(cbd3[bn1][bn3], cbd1[bn1][bn3]) - start_d3 = [-d for d in add(cbd1[bn1][bn3], cbd3[bn1][bn3])] - else: - bn1 = 0 - bn3 = n1 - bottomLeft_n1 + for i in range(self._elementsCountTransition - 1): + for lst in (ctx, ctd1, ctd3): + lst.append([None] * self._elementsCountAround) + ix = self._rimCoordinates[0][n2][0] + id1 = self._rimCoordinates[1][n2][0] + id3 = self._rimCoordinates[3][n2][0] + start_bn3 = minorBoxSize // 2 + topLeft_n1 = minorBoxSize - start_bn3 + topRight_n1 = topLeft_n1 + majorBoxSize + bottomRight_n1 = topRight_n1 + minorBoxSize + bottomLeft_n1 = bottomRight_n1 + majorBoxSize + for n1 in range(self._elementsCountAround): + if n1 <= topLeft_n1: + bn1 = 0 + bn3 = start_bn3 + n1 + if n1 < topLeft_n1: start_d1 = cbd3[bn1][bn3] start_d3 = [-d for d in cbd1[bn1][bn3]] - start_x = cbx[bn1][bn3] - - nx = [start_x, ix[n1]] - nd3before = [[self._elementsCountTransition * d for d in start_d3], id3[n1]] - nd3 = [nd3before[0], computeCubicHermiteEndDerivative(nx[0], nd3before[0], nx[1], nd3before[1])] - tx, td3, pe, pxi, psf = sampleCubicHermiteCurvesSmooth( - nx, nd3, self._elementsCountTransition, - derivativeMagnitudeStart=magnitude(nd3[0]) / self._elementsCountTransition, - derivativeMagnitudeEnd=magnitude(nd3[1]) / self._elementsCountTransition) - delta_id1 = sub(id1[n1], start_d1) - td1 = interpolateSampleCubicHermite([start_d1, id1[n1]], [delta_id1, delta_id1], pe, pxi, psf)[0] - - for n3 in range(1, self._elementsCountTransition): - ctx[n3 - 1][n1] = tx[n3] - ctd1[n3 - 1][n1] = td1[n3] - ctd3[n3 - 1][n1] = td3[n3] + else: + start_d1 = add(cbd3[bn1][bn3], cbd1[bn1][bn3]) + start_d3 = sub(cbd3[bn1][bn3], cbd1[bn1][bn3]) + elif n1 <= topRight_n1: + bn1 = n1 - topLeft_n1 + bn3 = minorBoxSize + if n1 < topRight_n1: + start_d1 = cbd1[bn1][bn3] + start_d3 = cbd3[bn1][bn3] + else: + start_d1 = sub(cbd1[bn1][bn3], cbd3[bn1][bn3]) + start_d3 = add(cbd1[bn1][bn3], cbd3[bn1][bn3]) + elif n1 <= bottomRight_n1: + bn1 = majorBoxSize + bn3 = minorBoxSize - (n1 - topRight_n1) + if n1 < bottomRight_n1: + start_d1 = [-d for d in cbd3[bn1][bn3]] + start_d3 = cbd1[bn1][bn3] + else: + start_d1 = [-d for d in add(cbd1[bn1][bn3], cbd3[bn1][bn3])] + start_d3 = sub(cbd1[bn1][bn3], cbd3[bn1][bn3]) + elif n1 <= bottomLeft_n1: + bn1 = majorBoxSize - (n1 - bottomRight_n1) + bn3 = 0 + if n1 < bottomLeft_n1: + start_d1 = [-d for d in cbd1[bn1][bn3]] + start_d3 = [-d for d in cbd3[bn1][bn3]] + else: + start_d1 = sub(cbd3[bn1][bn3], cbd1[bn1][bn3]) + start_d3 = [-d for d in add(cbd1[bn1][bn3], cbd3[bn1][bn3])] + else: + bn1 = 0 + bn3 = n1 - bottomLeft_n1 + start_d1 = cbd3[bn1][bn3] + start_d3 = [-d for d in cbd1[bn1][bn3]] + start_x = cbx[bn1][bn3] + + nx = [start_x, ix[n1]] + nd3before = [[self._elementsCountTransition * d for d in start_d3], id3[n1]] + nd3 = [nd3before[0], computeCubicHermiteEndDerivative(nx[0], nd3before[0], nx[1], nd3before[1])] + tx, td3, pe, pxi, psf = sampleCubicHermiteCurvesSmooth( + nx, nd3, self._elementsCountTransition, + derivativeMagnitudeStart=magnitude(nd3[0]) / self._elementsCountTransition, + derivativeMagnitudeEnd=magnitude(nd3[1]) / self._elementsCountTransition) + delta_id1 = sub(id1[n1], start_d1) + td1 = interpolateSampleCubicHermite([start_d1, id1[n1]], [delta_id1, delta_id1], pe, pxi, psf)[0] + + for n3 in range(1, self._elementsCountTransition): + ctx[n3 - 1][n1] = tx[n3] + ctd1[n3 - 1][n1] = td1[n3] + ctd3[n3 - 1][n1] = td3[n3] # smooth td1 around: for n3 in range(1, self._elementsCountTransition): @@ -1051,7 +1053,13 @@ def getElementsCountRim(self): return elementsCountRim def getNodesCountRim(self): - return len(self._rimCoordinates[0][0]) + """ + :return: Number of transition + shell nodes + """ + nodesCountRim = len(self._rimCoordinates[0][0]) + if self._isCore: + nodesCountRim += (self._elementsCountTransition - 1) + return nodesCountRim def getRimCoordinatesListAlong(self, n1, n2List, n3): """ @@ -1200,16 +1208,25 @@ def getTriplePointLocation(self, e1): def getRimCoordinates(self, n1, n2, n3): """ - Get rim parameters at a point. + Get rim parameters (transition through shell) parameters at a point. + This was what rim coordinates should have been. :param n1: Node index around. :param n2: Node index along segment. - :param n3: Node index from inner to outer rim. + :param n3: Node index from first core transition row or inner to outer shell. :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) + transitionNodeCount = (len(self._transitionCoordinates[0][0]) + if (self._transitionCoordinates and self._transitionCoordinates[0]) else 0) + if n3 < transitionNodeCount: + return (self._transitionCoordinates[0][n2][n3][n1], + self._transitionCoordinates[1][n2][n3][n1], + self._transitionCoordinates[2][n2][n3][n1], + self._transitionCoordinates[3][n2][n3][n1] if self._transitionCoordinates[3] else None) + sn3 = n3 - transitionNodeCount + return (self._rimCoordinates[0][n2][sn3][n1], + self._rimCoordinates[1][n2][sn3][n1], + self._rimCoordinates[2][n2][sn3][n1], + self._rimCoordinates[3][n2][sn3][n1] if self._rimCoordinates[3] else None) def getRimNodeId(self, n1, n2, n3): """ @@ -1470,10 +1487,9 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): self._boxNodeIds[n2][n3].append(nodeIdentifier) # create rim nodes and transition nodes (if there are more than 1 layer of transition) - nodesCountRim = len(self._rimCoordinates[0][0]) self._rimNodeIds[n2] = [] if self._rimNodeIds[n2] is None else self._rimNodeIds[n2] - nloop = nodesCountRim + (elementsCountTransition - 1) if self._isCore else nodesCountRim - for n3 in range(nloop): + nodesCountRim = self.getNodesCountRim() + for n3 in range(nodesCountRim): n3p = n3 - (elementsCountTransition - 1) if self._isCore else n3 if self._isCore and elementsCountTransition > 1 and n3 < (elementsCountTransition - 1): # transition coordinates @@ -1511,8 +1527,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create elements annotationMeshGroups = generateData.getAnnotationMeshGroups(self._annotationTerms) mesh = generateData.getMesh() - elementtemplateStd = generateData.getStandardElementtemplate() - eftStd = generateData.getStandardEft() + elementtemplateStd, eftStd = generateData.getStandardElementtemplate() for e2 in range(startSkipCount, elementsCountAlong - endSkipCount): self._boxElementIds[e2] = [] self._rimElementIds[e2] = [] @@ -1537,10 +1552,8 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): elementIds.append(elementIdentifier) self._boxElementIds[e2].append(elementIds) - # create core transition elements + # create core transition elements first layer after box triplePointIndexesList = self.getTriplePointIndexes() - eftList = [None] * self._elementsCountAround - scalefactorsList = [None] * self._elementsCountAround ringElementIds = [] for e1 in range(self._elementsCountAround): nids, nodeParameters, nodeLayouts = [], [], [] @@ -1560,12 +1573,10 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): nids += [self._rimNodeIds[n2][0][n1]] nodeParameters.append(self.getRimCoordinates(n1, n2, 0)) nodeLayouts.append(None) - eft = eftList[e1] - scalefactors = scalefactorsList[e1] - if not eft: - eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) - eftList[e1] = eft - scalefactorsList[e1] = scalefactors + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + if self._elementsCountTransition == 1: + eft, scalefactors = addTricubicHermiteSerendipityEftParameterScaling( + eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3) elementtemplate = mesh.createElementtemplate() elementtemplate.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplate.defineField(coordinates, -1, eft) @@ -1581,17 +1592,38 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create regular rim elements - all elements outside first transition layer elementsCountRimRegular = elementsCountRim - 1 if self._isCore else elementsCountRim + elementtemplateTransition = generateData.getMesh().createElementtemplate() + elementtemplateTransition.setElementShapeType(Element.SHAPE_TYPE_CUBE) for e3 in range(elementsCountRimRegular): ringElementIds = [] + lastTransition = self._isCore and (e3 == (self._elementsCountTransition - 2)) for e1 in range(self._elementsCountAround): - e1p = (e1 + 1) % self._elementsCountAround + elementtemplate = elementtemplateStd + eft = eftStd + + n1p = (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]] + nids += [self._rimNodeIds[e2][n3][e1], self._rimNodeIds[e2][n3][n1p], + self._rimNodeIds[e2p][n3][e1], self._rimNodeIds[e2p][n3][n1p]] elementIdentifier = generateData.nextElementIdentifier() - element = mesh.createElement(elementIdentifier, elementtemplateStd) - element.setNodesByIdentifier(eftStd, nids) + scalefactors = [] + if lastTransition: + # get node parameters for computing scale factors + nodeParameters = [] + for n3 in (e3, e3 + 1): + for n2 in (e2, e2 + 1): + for n1 in (e1, n1p): + nodeParameters.append(self.getRimCoordinates(n1, n2, n3)) + eft = generateData.createElementfieldtemplate() + eft, scalefactors = addTricubicHermiteSerendipityEftParameterScaling( + eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3) + elementtemplateTransition.defineField(coordinates, -1, eft) + elementtemplate = elementtemplateTransition + element = mesh.createElement(elementIdentifier, elementtemplate) + element.setNodesByIdentifier(eft, nids) + if scalefactors: + element.setScaleFactors(eft, scalefactors) for annotationMeshGroup in annotationMeshGroups: annotationMeshGroup.addElement(element) ringElementIds.append(elementIdentifier) @@ -1625,7 +1657,6 @@ def __init__(self, inSegments: list, outSegments: list, useOuterTrimSurfaces): # parameters used for solid core self._isCore = self._segments[0].getIsCore() self._boxCoordinates = None # [nAlong][nAcrossMajor][nAcrossMinor] - self._transitionCoordinates = None self._boxNodeIds = None # sequence of segment indexes for bifurcation or trifurcation, proceding in increasing angle around a plane. # See: self._determineJunctionSequence() @@ -1908,9 +1939,10 @@ def _sampleMidPoint(self, segmentsParameterLists): md2.append(mult(cd2, md2Factor)) # smooth md2 with 2nd row from end, which it actually interpolates to # tmd2 = smoothCubicHermiteDerivativesLine( - # [fxs1, cx, fxs2], [fd2s1, cd2, fd2s2], fixStartDerivative=True, fixEndDerivative=True, + # [fxs1, cx, fxs2], [fd2s1, cd2, fd2s2], + # fixAllDirections=True, fixStartDerivative=True, fixEndDerivative=True, # magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - # md2.append(tmd2[1]) + # md2.append(mult(tmd2[1], md2Factor)) if d3Defined: md3.append(mult(add(hd3[s1], hd3[s2]), 0.5)) if segmentsCount == 2: @@ -2541,8 +2573,7 @@ def sample(self, targetElementLength): # sample rim coordinates elementsCountTransition = self._segments[0].getElementsCountTransition() - nodesCountRim = self._segments[0].getNodesCountRim() + (elementsCountTransition - 1) if self._isCore else ( - self._segments[0].getNodesCountRim()) + 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) @@ -2702,19 +2733,17 @@ def _generateBoxElements(self, s, n2, mesh, elementtemplate, coordinates, segmen def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, segment, generateData, elementsCountAround, boxBoundaryNodeIds, boxBoundaryNodeToBoxId): """ - Blackbox function for generating core transition elements at a junction. + Blackbox function for generating first row of core transition elements after box at a junction. """ annotationMeshGroups = generateData.getAnnotationMeshGroups(segment.getAnnotationTerms()) nodesCountAcrossMinor = self._segments[0].getCoreNodesCountAcrossMinor() nodesCountAcrossMajor = [self._segments[s].getCoreNodesCountAcrossMajor() for s in range(self._segmentsCount)] acrossMajorCounts = [segment.getElementsCountAcrossMajor() for segment in self._segments] - eftList = [None] * elementsCountAround - scalefactorsList = [None] * elementsCountAround triplePointIndexesList = segment.getTriplePointIndexes() - coreTransitionCount = self._segments[0].getElementsCountTransition() - coreBoxMajorCounts = [count - 2 * coreTransitionCount for count in acrossMajorCounts] + elementsCountTransition = self._segments[0].getElementsCountTransition() + coreBoxMajorCounts = [count - 2 * elementsCountTransition for count in acrossMajorCounts] is6WayTriplePoint = (self._segmentsCount == 3) and ((max(coreBoxMajorCounts) // 2) == min(coreBoxMajorCounts)) pSegment = acrossMajorCounts.index(max(acrossMajorCounts)) topMidIndex = (nodesCountAcrossMajor[pSegment] // 2) + (nodesCountAcrossMinor // 2) @@ -2808,12 +2837,10 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, a[-4], a[-2] = a[-2], a[-4] a[-3], a[-1] = a[-1], a[-3] - eft = eftList[e1] - scalefactors = scalefactorsList[e1] - if not eft: - eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) - eftList[e1] = eft - scalefactorsList[e1] = scalefactors + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + if elementsCountTransition == 1: + eft, scalefactors = addTricubicHermiteSerendipityEftParameterScaling( + eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3) elementtemplate.defineField(coordinates, -1, eft) element = mesh.createElement(elementIdentifier, elementtemplate) element.setNodesByIdentifier(eft, nids) @@ -2850,8 +2877,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): rimIndexesCount = len(self._rimIndexToSegmentNodeList) elementsCountTransition = self._segments[0].getElementsCountTransition() - nodesCountRim = self._segments[0].getNodesCountRim() + (elementsCountTransition - 1) if self._isCore else ( - self._segments[0].getNodesCountRim()) + nodesCountRim = self._segments[0].getNodesCountRim() elementsCountRim = self._segments[0].getElementsCountRim() if self._rimCoordinates: self._rimNodeIds = [[None] * rimIndexesCount for _ in range(nodesCountRim)] @@ -2927,7 +2953,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): layerNodeIds = self._rimNodeIds[n3] for n1 in range(elementsCountAround): rimIndex = self._segmentNodeToRimIndex[s][n1] - nodeIdentifier = self._rimNodeIds[n3][rimIndex] + nodeIdentifier = layerNodeIds[rimIndex] if nodeIdentifier is not None: continue nodeIdentifier = generateData.nextNodeIdentifier() @@ -2960,19 +2986,20 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): nids = [] nodeParameters = [] nodeLayouts = [] + lastTransition = self._isCore and (elementsCountTransition == (rim_e3 + 1)) + needParameters = (e3 == 0) or lastTransition 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: + if needParameters: rimCoordinates = self._segments[s].getRimCoordinates(n1, n2, n3) - nodeParameters.append(rimCoordinates if d3Defined else - (rimCoordinates[0], rimCoordinates[1], rimCoordinates[2], - None)) + 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: + if needParameters: nodeParameters.append( (self._rimCoordinates[0][n3][rimIndex], self._rimCoordinates[1][n3][rimIndex], @@ -2983,7 +3010,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): nodeLayout6Way if (segmentNodesCount == 3) else nodeLayout8Way) if not self._segmentsIn[s]: - for a in [nids, nodeParameters, nodeLayouts] if (e3 == 0) else [nids]: + for a in [nids, nodeParameters, nodeLayouts] if (needParameters) 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 @@ -2993,6 +3020,12 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) eftList[e1] = eft scalefactorsList[e1] = scalefactors + if lastTransition: + # need to generate eft again otherwise modifying object in eftList, mucking up outer layers + eft, scalefactors = determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts) + eft, scalefactors = addTricubicHermiteSerendipityEftParameterScaling( + eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3) + elementtemplate.defineField(coordinates, -1, eft) elementIdentifier = generateData.nextElementIdentifier() element = mesh.createElement(elementIdentifier, elementtemplate) diff --git a/tests/test_wholebody2.py b/tests/test_wholebody2.py index ac641488..f42e4761 100644 --- a/tests/test_wholebody2.py +++ b/tests/test_wholebody2.py @@ -25,7 +25,7 @@ def test_wholebody2_core(self): scaffold = MeshType_3d_wholebody2 parameterSetNames = scaffold.getParameterSetNames() self.assertEqual(parameterSetNames, ["Default", "Human 1 Coarse", "Human 1 Medium", "Human 1 Fine"]) - options = scaffold.getDefaultOptions("Default") + options = scaffold.getDefaultOptions("Human 1 Coarse") self.assertEqual(19, len(options)) self.assertEqual(2, options["Number of elements along head"]) self.assertEqual(1, options["Number of elements along neck"]) @@ -54,13 +54,13 @@ def test_wholebody2_core(self): fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(784, mesh3d.getSize()) + self.assertEqual(704, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(2562, mesh2d.getSize()) + self.assertEqual(2306, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(2809, mesh1d.getSize()) + self.assertEqual(2533, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(1032, nodes.getSize()) + self.assertEqual(932, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -69,14 +69,12 @@ def test_wholebody2_core(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-6 - assertAlmostEqualList(self, minimums, [0.0, -1.976, -0.6113507244855241], tol) - assertAlmostEqualList(self, maximums, [8.748998777022969, 1.968, 0.7035640657448223], tol) + assertAlmostEqualList(self, minimums, [0.0, -3.7000751482231564, -1.25], tol) + assertAlmostEqualList(self, maximums, [20.437483381451223, 3.7000751482231564, 2.15], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) isExterior = fieldmodule.createFieldIsExterior() - isExteriorXi3_1 = fieldmodule.createFieldAnd( - isExterior, fieldmodule.createFieldIsOnFace(Element.FACE_TYPE_XI3_1)) mesh2d = fieldmodule.findMeshByDimension(2) fieldcache = fieldmodule.createFieldcache() @@ -85,40 +83,82 @@ def test_wholebody2_core(self): result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExteriorXi3_1, coordinates, mesh2d) + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) surfaceAreaField.setNumbersOfPoints(4) result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 6.419169077227952, delta=tol) - self.assertAlmostEqual(surfaceArea, 35.880031911102506, delta=tol) + self.assertAlmostEqual(volume, 99.02238171504763, delta=tol) + self.assertAlmostEqual(surfaceArea, 229.82780303324995, delta=tol) + + # check some annotation groups: + + expectedSizes3d = { + "abdominal cavity": (40, 10.094264544167423), + "core": (428, 45.817610658773326), + "head": (64, 6.909618374858558), + "thoracic cavity": (40, 7.140116968045268), + "shell": (276, 53.20333000107456) + } + for name in expectedSizes3d: + term = get_body_term(name) + annotationGroup = getAnnotationGroupForTerm(annotationGroups, term) + size = annotationGroup.getMeshGroup(mesh3d).getSize() + self.assertEqual(expectedSizes3d[name][0], size, name) + volumeMeshGroup = annotationGroup.getMeshGroup(mesh3d) + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, volumeMeshGroup) + volumeField.setNumbersOfPoints(4) + fieldcache = fieldmodule.createFieldcache() + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) - # check some annotationGroups: expectedSizes2d = { - "skin epidermis": (308, 35.880031911102506) + "abdominal cavity boundary": (64, 27.459787335655104), + "diaphragm": (20, 3.0778936559347208), + "left arm skin epidermis": (68, 22.595664816330093), + "left leg skin epidermis": (68, 55.17540980396306), + "right arm skin epidermis": (68, 22.595664816330093), + "right leg skin epidermis": (68, 55.17540980396306), + "skin epidermis": (388, 229.82780303324995), + "thoracic cavity boundary": (64, 20.600420538940487) } for name in expectedSizes2d: term = get_body_term(name) annotationGroup = getAnnotationGroupForTerm(annotationGroups, term) size = annotationGroup.getMeshGroup(mesh2d).getSize() self.assertEqual(expectedSizes2d[name][0], size, name) - surfaceMeshGroup = annotationGroup.getMeshGroup(mesh2d) surfaceAreaField = fieldmodule.createFieldMeshIntegral(one, coordinates, surfaceMeshGroup) surfaceAreaField.setNumbersOfPoints(4) - fieldcache = fieldmodule.createFieldcache() result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) self.assertAlmostEqual(surfaceArea, expectedSizes2d[name][1], delta=tol) + expectedSizes1d = { + "spinal cord": (8, 10.855253444098556) + } + for name in expectedSizes1d: + term = get_body_term(name) + annotationGroup = getAnnotationGroupForTerm(annotationGroups, term) + size = annotationGroup.getMeshGroup(mesh1d).getSize() + self.assertEqual(expectedSizes1d[name][0], size, name) + lineMeshGroup = annotationGroup.getMeshGroup(mesh1d) + lengthField = fieldmodule.createFieldMeshIntegral(one, coordinates, lineMeshGroup) + lengthField.setNumbersOfPoints(4) + fieldcache = fieldmodule.createFieldcache() + result, length = lengthField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + self.assertAlmostEqual(length, expectedSizes1d[name][1], delta=tol) + def test_wholebody2_tube(self): """ Test creation of Whole-body scaffold without solid core. """ scaffold = MeshType_3d_wholebody2 - options = scaffold.getDefaultOptions("Default") + options = scaffold.getDefaultOptions("Human 1 Coarse") options["Use Core"] = False context = Context("Test") region = context.getDefaultRegion() @@ -129,13 +169,13 @@ def test_wholebody2_tube(self): fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual(308, mesh3d.getSize()) + self.assertEqual(276, mesh3d.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(1254, mesh2d.getSize()) + self.assertEqual(1126, mesh2d.getSize()) mesh1d = fieldmodule.findMeshByDimension(1) - self.assertEqual(1603, mesh1d.getSize()) + self.assertEqual(1443, mesh1d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual(654, nodes.getSize()) + self.assertEqual(590, nodes.getSize()) datapoints = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_DATAPOINTS) self.assertEqual(0, datapoints.getSize()) @@ -144,8 +184,8 @@ def test_wholebody2_tube(self): self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) tol = 1.0E-6 - assertAlmostEqualList(self, minimums, [0.0, -1.976, -0.6113507244855241], tol) - assertAlmostEqualList(self, maximums, [8.748998777022969, 1.968, 0.7035640657448223], tol) + assertAlmostEqualList(self, minimums, [0.0, -3.7000751482231564, -1.25], tol) + assertAlmostEqualList(self, maximums, [20.437483381451223, 3.7000751482231564, 2.15], tol) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -172,13 +212,13 @@ def test_wholebody2_tube(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 3.1692513375640607, delta=tol) - self.assertAlmostEqual(outerSurfaceArea, 35.880031911102506, delta=tol) - self.assertAlmostEqual(innerSurfaceArea, 25.851093527837623, delta=tol) + self.assertAlmostEqual(volume, 53.20196079841706, delta=tol) + self.assertAlmostEqual(outerSurfaceArea, 225.72283168985595, delta=tol) + self.assertAlmostEqual(innerSurfaceArea, 155.95593103737943, delta=tol) # check some annotationGroups: expectedSizes2d = { - "skin epidermis": (308, 35.880031911102506) + "skin epidermis": (320, 228.9705874171508) } for name in expectedSizes2d: term = get_body_term(name) From 485839a9adcc100634d6e6a02a6031b20906c2e5 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Thu, 10 Oct 2024 17:54:12 +1300 Subject: [PATCH 12/20] Fix undefined name, hide private body layout --- src/scaffoldmaker/scaffolds.py | 1 - src/scaffoldmaker/utils/tubenetworkmesh.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/scaffoldmaker/scaffolds.py b/src/scaffoldmaker/scaffolds.py index cc15f536..6c35deb2 100644 --- a/src/scaffoldmaker/scaffolds.py +++ b/src/scaffoldmaker/scaffolds.py @@ -117,7 +117,6 @@ def __init__(self): MeshType_3d_uterus1, MeshType_3d_uterus2, MeshType_3d_wholebody1, - MeshType_1d_human_body_network_layout1, # GRC remove MeshType_3d_wholebody2 ] self._allPrivateScaffoldTypes = [ diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index cf0331d7..e546e8f9 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -1374,7 +1374,7 @@ def addSideD2ElementsToMeshGroup(self, side: bool, meshGroup): e1Start = (self._elementsCountAround // 4) if side else -((self._elementsCountAround + 2) // 4) e1Limit = e1Start + (self._elementsCountAround // 2) if (self._elementsCountAround % 4) == 2: - eLimit += 1 + e1Limit += 1 self._addRimElementsToMeshGroup(e1Start, e1Limit, 0, self.getElementsCountRim(), meshGroup) def addSideD3ElementsToMeshGroup(self, side: bool, meshGroup): From 09c3e605f5f3277fc806743f96bf832b5479d9b5 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Fri, 11 Oct 2024 11:16:23 +1300 Subject: [PATCH 13/20] Fix multiple transition node smoothing, add test --- src/scaffoldmaker/utils/tubenetworkmesh.py | 136 ++++++++++----------- tests/test_network.py | 63 ++++++++++ 2 files changed, 131 insertions(+), 68 deletions(-) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index e546e8f9..d0e3a8d9 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -721,81 +721,81 @@ def _generateCoreCoordinates(self, n2, centre): cbd1.append(row_d1) cbd3.append(row_d3) - for i in range(self._elementsCountTransition - 1): - for lst in (ctx, ctd1, ctd3): - lst.append([None] * self._elementsCountAround) - ix = self._rimCoordinates[0][n2][0] - id1 = self._rimCoordinates[1][n2][0] - id3 = self._rimCoordinates[3][n2][0] - start_bn3 = minorBoxSize // 2 - topLeft_n1 = minorBoxSize - start_bn3 - topRight_n1 = topLeft_n1 + majorBoxSize - bottomRight_n1 = topRight_n1 + minorBoxSize - bottomLeft_n1 = bottomRight_n1 + majorBoxSize - for n1 in range(self._elementsCountAround): - if n1 <= topLeft_n1: - bn1 = 0 - bn3 = start_bn3 + n1 - if n1 < topLeft_n1: + if self._elementsCountTransition > 1: + for i in range(self._elementsCountTransition - 1): + for lst in (ctx, ctd1, ctd3): + lst.append([None] * self._elementsCountAround) + ix = self._rimCoordinates[0][n2][0] + id1 = self._rimCoordinates[1][n2][0] + id3 = self._rimCoordinates[3][n2][0] + start_bn3 = minorBoxSize // 2 + topLeft_n1 = minorBoxSize - start_bn3 + topRight_n1 = topLeft_n1 + majorBoxSize + bottomRight_n1 = topRight_n1 + minorBoxSize + bottomLeft_n1 = bottomRight_n1 + majorBoxSize + for n1 in range(self._elementsCountAround): + if n1 <= topLeft_n1: + bn1 = 0 + bn3 = start_bn3 + n1 + if n1 < topLeft_n1: + start_d1 = cbd3[bn1][bn3] + start_d3 = [-d for d in cbd1[bn1][bn3]] + else: + start_d1 = add(cbd3[bn1][bn3], cbd1[bn1][bn3]) + start_d3 = sub(cbd3[bn1][bn3], cbd1[bn1][bn3]) + elif n1 <= topRight_n1: + bn1 = n1 - topLeft_n1 + bn3 = minorBoxSize + if n1 < topRight_n1: + start_d1 = cbd1[bn1][bn3] + start_d3 = cbd3[bn1][bn3] + else: + start_d1 = sub(cbd1[bn1][bn3], cbd3[bn1][bn3]) + start_d3 = add(cbd1[bn1][bn3], cbd3[bn1][bn3]) + elif n1 <= bottomRight_n1: + bn1 = majorBoxSize + bn3 = minorBoxSize - (n1 - topRight_n1) + if n1 < bottomRight_n1: + start_d1 = [-d for d in cbd3[bn1][bn3]] + start_d3 = cbd1[bn1][bn3] + else: + start_d1 = [-d for d in add(cbd1[bn1][bn3], cbd3[bn1][bn3])] + start_d3 = sub(cbd1[bn1][bn3], cbd3[bn1][bn3]) + elif n1 <= bottomLeft_n1: + bn1 = majorBoxSize - (n1 - bottomRight_n1) + bn3 = 0 + if n1 < bottomLeft_n1: + start_d1 = [-d for d in cbd1[bn1][bn3]] + start_d3 = [-d for d in cbd3[bn1][bn3]] + else: + start_d1 = sub(cbd3[bn1][bn3], cbd1[bn1][bn3]) + start_d3 = [-d for d in add(cbd1[bn1][bn3], cbd3[bn1][bn3])] + else: + bn1 = 0 + bn3 = n1 - bottomLeft_n1 start_d1 = cbd3[bn1][bn3] start_d3 = [-d for d in cbd1[bn1][bn3]] - else: - start_d1 = add(cbd3[bn1][bn3], cbd1[bn1][bn3]) - start_d3 = sub(cbd3[bn1][bn3], cbd1[bn1][bn3]) - elif n1 <= topRight_n1: - bn1 = n1 - topLeft_n1 - bn3 = minorBoxSize - if n1 < topRight_n1: - start_d1 = cbd1[bn1][bn3] - start_d3 = cbd3[bn1][bn3] - else: - start_d1 = sub(cbd1[bn1][bn3], cbd3[bn1][bn3]) - start_d3 = add(cbd1[bn1][bn3], cbd3[bn1][bn3]) - elif n1 <= bottomRight_n1: - bn1 = majorBoxSize - bn3 = minorBoxSize - (n1 - topRight_n1) - if n1 < bottomRight_n1: - start_d1 = [-d for d in cbd3[bn1][bn3]] - start_d3 = cbd1[bn1][bn3] - else: - start_d1 = [-d for d in add(cbd1[bn1][bn3], cbd3[bn1][bn3])] - start_d3 = sub(cbd1[bn1][bn3], cbd3[bn1][bn3]) - elif n1 <= bottomLeft_n1: - bn1 = majorBoxSize - (n1 - bottomRight_n1) - bn3 = 0 - if n1 < bottomLeft_n1: - start_d1 = [-d for d in cbd1[bn1][bn3]] - start_d3 = [-d for d in cbd3[bn1][bn3]] - else: - start_d1 = sub(cbd3[bn1][bn3], cbd1[bn1][bn3]) - start_d3 = [-d for d in add(cbd1[bn1][bn3], cbd3[bn1][bn3])] - else: - bn1 = 0 - bn3 = n1 - bottomLeft_n1 - start_d1 = cbd3[bn1][bn3] - start_d3 = [-d for d in cbd1[bn1][bn3]] - start_x = cbx[bn1][bn3] - - nx = [start_x, ix[n1]] - nd3before = [[self._elementsCountTransition * d for d in start_d3], id3[n1]] - nd3 = [nd3before[0], computeCubicHermiteEndDerivative(nx[0], nd3before[0], nx[1], nd3before[1])] - tx, td3, pe, pxi, psf = sampleCubicHermiteCurvesSmooth( - nx, nd3, self._elementsCountTransition, - derivativeMagnitudeStart=magnitude(nd3[0]) / self._elementsCountTransition, - derivativeMagnitudeEnd=magnitude(nd3[1]) / self._elementsCountTransition) - delta_id1 = sub(id1[n1], start_d1) - td1 = interpolateSampleCubicHermite([start_d1, id1[n1]], [delta_id1, delta_id1], pe, pxi, psf)[0] - - for n3 in range(1, self._elementsCountTransition): - ctx[n3 - 1][n1] = tx[n3] - ctd1[n3 - 1][n1] = td1[n3] - ctd3[n3 - 1][n1] = td3[n3] + start_x = cbx[bn1][bn3] + + nx = [start_x, ix[n1]] + nd3before = [[self._elementsCountTransition * d for d in start_d3], id3[n1]] + nd3 = [nd3before[0], computeCubicHermiteEndDerivative(nx[0], nd3before[0], nx[1], nd3before[1])] + tx, td3, pe, pxi, psf = sampleCubicHermiteCurvesSmooth( + nx, nd3, self._elementsCountTransition, + derivativeMagnitudeStart=magnitude(nd3[0]) / self._elementsCountTransition, + derivativeMagnitudeEnd=magnitude(nd3[1]) / self._elementsCountTransition) + delta_id1 = sub(id1[n1], start_d1) + td1 = interpolateSampleCubicHermite([start_d1, id1[n1]], [delta_id1, delta_id1], pe, pxi, psf)[0] + + for n3 in range(1, self._elementsCountTransition): + ctx[n3 - 1][n1] = tx[n3] + ctd1[n3 - 1][n1] = td1[n3] + ctd3[n3 - 1][n1] = td3[n3] # smooth td1 around: for n3 in range(1, self._elementsCountTransition): ctd1[n3 - 1] = smoothCubicHermiteDerivativesLoop(ctx[n3 - 1], ctd1[n3 - 1], fixAllDirections=False) - return cbx, cbd1, cbd3, ctx, ctd1, ctd3 def _determineCoreD2Derivatives(self, boxx, boxd1, boxd3, transx, transd1, transd3): diff --git a/tests/test_network.py b/tests/test_network.py index a7a4ce63..9efc1be5 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -473,6 +473,69 @@ def test_3d_tube_network_bifurcation_core(self): self.assertAlmostEqual(volume, 0.09907643906540035, delta=X_TOL) self.assertAlmostEqual(surfaceArea, 2.0238210110948307, delta=X_TOL) + def test_3d_tube_network_line_core_transition2(self): + """ + Test line 3-D tube network with solid core and 2 transition elements. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Default") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(13, len(settings)) + self.assertEqual(8, settings["Number of elements around"]) + self.assertEqual(1, settings["Number of elements through shell"]) + self.assertEqual([0], settings["Annotation numbers of elements around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) + self.assertFalse(settings["Use linear through shell"]) + self.assertFalse(settings["Show trim surfaces"]) + self.assertFalse(settings["Core"]) + self.assertEqual(2, settings["Number of elements across core box minor"]) + self.assertEqual(1, settings["Number of elements across core transition"]) + self.assertEqual([0], settings["Annotation numbers of elements across core box minor"]) + settings["Core"] = True + settings["Number of elements across core transition"] = 2 + + context = Context("Test") + region = context.getDefaultRegion() + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + + fieldmodule = region.getFieldmodule() + mesh3d = fieldmodule.findMeshByDimension(3) + + self.assertEqual(112, mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(165, 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.1, -0.1], X_TOL) + assertAlmostEqualList(self, maximums, [1.0, 0.1, 0.1], X_TOL) + + with ChangeManager(fieldmodule): + one = fieldmodule.createFieldConstant(1.0) + isExterior = fieldmodule.createFieldIsExterior() + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) + surfaceAreaField.setNumbersOfPoints(4) + result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.0313832204833548, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 0.6907602069977625, delta=X_TOL) + def test_3d_tube_network_sphere_cube(self): """ Test sphere cube 3-D tube network is generated correctly. From 5bf324612058795e1064a0a12eb14c1be9bafaec Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Mon, 14 Oct 2024 15:44:56 +1300 Subject: [PATCH 14/20] Improve midpoint sampling Fix some tube bifuraction cases. Use core box major/minor internally --- .../meshtypes/meshtype_3d_tubenetwork1.py | 11 +- .../meshtypes/meshtype_3d_wholebody2.py | 17 +- src/scaffoldmaker/utils/tubenetworkmesh.py | 328 +++++++++--------- tests/test_network.py | 128 ++++++- tests/test_uterus.py | 4 +- tests/test_wholebody2.py | 36 +- 6 files changed, 303 insertions(+), 221 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py index 20533431..e9b26042 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py @@ -222,13 +222,6 @@ def generateBaseMesh(cls, region, options): defaultCoreMajorCount = defaultAroundCount // 2 - defaultCoreBoxMinorCount + 2 * coreTransitionCount annotationAroundCounts = options["Annotation numbers of elements around"] annotationCoreBoxMinorCounts = options["Annotation numbers of elements across core box minor"] - annotationCoreMajorCounts = [] - for i in range(len(annotationCoreBoxMinorCounts)): - aroundCount = annotationAroundCounts[i] if annotationAroundCounts[i] \ - else defaultAroundCount - coreBoxMinorCount = annotationCoreBoxMinorCounts[i] if annotationCoreBoxMinorCounts[i] \ - else defaultCoreBoxMinorCount - annotationCoreMajorCounts.append(aroundCount // 2 - coreBoxMinorCount + 2 * coreTransitionCount) tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( networkMesh, @@ -238,9 +231,9 @@ def generateBaseMesh(cls, region, options): layoutAnnotationGroups=layoutAnnotationGroups, annotationElementsCountsAlong=options["Annotation numbers of elements along"], annotationElementsCountsAround=annotationAroundCounts, - defaultElementsCountAcrossMajor=defaultCoreMajorCount, + defaultElementsCountCoreBoxMinor=defaultCoreBoxMinorCount, elementsCountTransition=coreTransitionCount, - annotationElementsCountsAcrossMajor=annotationCoreMajorCounts, + annotationElementsCountsCoreBoxMinor=annotationCoreBoxMinorCounts, isCore=options["Core"], useOuterTrimSurfaces=options["Use outer trim surfaces"]) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index ef5631f6..a03f8185 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -633,13 +633,13 @@ class WholeBodyNetworkMeshBuilder(TubeNetworkMeshBuilder): def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], annotationElementsCountsAlong: list = [], annotationElementsCountsAround: list = [], - defaultElementsCountAcrossMajor: int = 4, elementsCountTransition: int = 1, - annotationElementsCountsAcrossMajor: list = [], isCore=False, useOuterTrimSurfaces=True): + defaultElementsCountCoreBoxMinor: int = 2, elementsCountTransition: int = 1, + annotationElementsCountsCoreBoxMinor: list = [], isCore=False, useOuterTrimSurfaces=True): super(WholeBodyNetworkMeshBuilder, self).__init__( networkMesh, targetElementDensityAlongLongestSegment, defaultElementsCountAround, elementsCountThroughWall, layoutAnnotationGroups, - annotationElementsCountsAlong, annotationElementsCountsAround, defaultElementsCountAcrossMajor, - elementsCountTransition, annotationElementsCountsAcrossMajor, isCore, useOuterTrimSurfaces) + annotationElementsCountsAlong, annotationElementsCountsAround, defaultElementsCountCoreBoxMinor, + elementsCountTransition, annotationElementsCountsCoreBoxMinor, isCore, useOuterTrimSurfaces) def generateMesh(self, generateData): super(WholeBodyNetworkMeshBuilder, self).generateMesh(generateData) @@ -872,11 +872,9 @@ def generateBaseMesh(cls, region, options): annotationAlongCounts = [] annotationAroundCounts = [] # implementation currently uses major count including transition - annotationCoreMajorCounts = [] for layoutAnnotationGroup in layoutAnnotationGroups: alongCount = 0 aroundCount = 0 - coreMajorCount = 0 name = layoutAnnotationGroup.getName() if "head" in name: alongCount = elementsCountAlongHead @@ -902,11 +900,8 @@ def generateBaseMesh(cls, region, options): elif "foot" in name: alongCount = elementsCountAlongFoot aroundCount = elementsCountAroundLeg - if aroundCount: - coreMajorCount = aroundCount // 2 - coreBoxMinorCount + 2 * coreTransitionCount annotationAlongCounts.append(alongCount) annotationAroundCounts.append(aroundCount) - annotationCoreMajorCounts.append(coreMajorCount) isCore = options["Use Core"] tubeNetworkMeshBuilder = WholeBodyNetworkMeshBuilder( @@ -917,9 +912,9 @@ def generateBaseMesh(cls, region, options): layoutAnnotationGroups=layoutAnnotationGroups, annotationElementsCountsAlong=annotationAlongCounts, annotationElementsCountsAround=annotationAroundCounts, - defaultElementsCountAcrossMajor=annotationCoreMajorCounts[-1], + defaultElementsCountCoreBoxMinor=coreBoxMinorCount, elementsCountTransition=coreTransitionCount, - annotationElementsCountsAcrossMajor=annotationCoreMajorCounts, + annotationElementsCountsCoreBoxMinor=[], isCore=isCore) meshDimension = 3 diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index d0e3a8d9..efb05b70 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -181,25 +181,22 @@ def getNewTrimAnnotationGroup(self): class TubeNetworkMeshSegment(NetworkMeshSegment): def __init__(self, networkSegment, pathParametersList, elementsCountAround, elementsCountThroughWall, - isCore=False, elementsCountAcrossMajor: int = 4, elementsCountTransition: int = 1): + isCore=False, elementsCountCoreBoxMinor: int = 2, elementsCountTransition: int = 1): """ :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. :param isCore: True for generating a solid core inside the tube, False for regular tube network. - :param elementsCountAcrossMajor: Number of elements across major axis of core ellipse. - :param elementsCountTranstion: Number of elements across transition zone between core box elements and - rim elements. + :param elementsCountCoreBoxMinor: Number of elements across core box minor axis. + :param elementsCountTransition: Number of elements across transition zone between core box elements and + shell elements. """ super(TubeNetworkMeshSegment, self).__init__(networkSegment, pathParametersList) self._isCore = isCore self._elementsCountAround = elementsCountAround - self._elementsCountAcrossMajor = elementsCountAcrossMajor # includes 2 * elementsCountTransition - self._elementsCountAcrossMinor = \ - self._elementsCountAround // 2 - elementsCountAcrossMajor + 4 * elementsCountTransition - self._elementsCountCoreBoxMajor = self._elementsCountAcrossMajor - 2 * elementsCountTransition - self._elementsCountCoreBoxMinor = self._elementsCountAcrossMinor - 2 * elementsCountTransition + self._elementsCountCoreBoxMajor = (elementsCountAround // 2) - elementsCountCoreBoxMinor + self._elementsCountCoreBoxMinor = elementsCountCoreBoxMinor self._elementsCountTransition = elementsCountTransition # if self._isCore and self._elementsCountTransition > 1: @@ -243,11 +240,11 @@ def getRawTubeCoordinates(self, pathIndex=0): def getIsCore(self): return self._isCore - def getElementsCountAcrossMajor(self): - return self._elementsCountAcrossMajor + def getElementsCountCoreBoxMajor(self): + return self._elementsCountCoreBoxMajor - def getElementsCountAcrossMinor(self): - return self._elementsCountAcrossMinor + def getElementsCountCoreBoxMinor(self): + return self._elementsCountCoreBoxMinor def getElementsCountAcrossTransition(self): return self._elementsCountTransition @@ -589,25 +586,27 @@ def _generateCoreCoordinates(self, n2, centre): # sample radially across major, minor and both diagonals, like a Union Jack major_n1 = 0 major_x, major_d1, major_d3 = self._getRadialCoreCrossing( - major_n1, (self._elementsCountAcrossMinor % 2) == 1, n2, centre) + major_n1, (self._elementsCountCoreBoxMinor % 2) == 1, n2, centre) minor_n1 = -((self._elementsCountAround + 3) // 4) minor_x, minor_d3, minor_d1 = self._getRadialCoreCrossing( - minor_n1, (self._elementsCountAcrossMajor % 2) == 1, n2, centre) + minor_n1, (self._elementsCountCoreBoxMajor % 2) == 1, n2, centre) minor_d1 = [[-d for d in v] for v in minor_d1] major_d3[1] = minor_d3[1] minor_d1[1] = major_d1[1] centreNormal = normalize(cross(major_d1[1], major_d3[1])) - majorBoxSize = self._elementsCountAcrossMajor - 2 * self._elementsCountTransition - minorBoxSize = self._elementsCountAcrossMinor - 2 * self._elementsCountTransition + majorBoxSize = self._elementsCountCoreBoxMajor + minorBoxSize = self._elementsCountCoreBoxMinor diag1_n1 = minorBoxSize // -2 diag1_x, diag1_d1, diag1_d3 = self._getRadialCoreCrossing(diag1_n1, False, n2, centre, centreNormal) diag2_n1 = diag1_n1 + minorBoxSize diag2_x, diag2_d1, diag2_d3 = self._getRadialCoreCrossing(diag2_n1, False, n2, centre, centreNormal) # sample to sides and corners of core box - majorXi = 2.0 * self._elementsCountTransition / self._elementsCountAcrossMajor + majorXi = (2.0 * self._elementsCountTransition / + (self._elementsCountCoreBoxMajor + 2 * self._elementsCountTransition)) majorXiR = 1.0 - majorXi - minorXi = 2.0 * self._elementsCountTransition / self._elementsCountAcrossMinor + minorXi = (2.0 * self._elementsCountTransition / + (self._elementsCountCoreBoxMinor + 2 * self._elementsCountTransition)) minorXiR = 1.0 - minorXi # following expression adjusted to look best across all cases boxDiagonalSize = math.sqrt(majorBoxSize * majorBoxSize + minorBoxSize * minorBoxSize) @@ -806,10 +805,10 @@ def _determineCoreD2Derivatives(self, boxx, boxd1, boxd3, transx, transd1, trans :return: D2 derivatives of box and rim components of the core. """ elementsCountAlong = len(boxx) - nodesCountAcrossMajor = len(boxx[0]) - nodesCountAcrossMinor = len(boxx[0][0]) + coreBoxMajorNodesCount = len(boxx[0]) + coreBoxMinorNodesCount = len(boxx[0][0]) - boxd2 = [[[None for _ in range(nodesCountAcrossMinor)] for _ in range(nodesCountAcrossMajor)] + boxd2 = [[[None for _ in range(coreBoxMinorNodesCount)] for _ in range(coreBoxMajorNodesCount)] for _ in range(elementsCountAlong)] transd2 = [[[None for _ in range(self._elementsCountAround)] for _ in range(self._elementsCountTransition - 1)] for _ in range(elementsCountAlong)] @@ -834,8 +833,8 @@ def get_d2(n2, x): sum_d2[c] += weight * id2[i][c] return [sum_d2[c] / sum_weight for c in range(3)] - for m in range(nodesCountAcrossMajor): - for n in range(nodesCountAcrossMinor): + for m in range(coreBoxMajorNodesCount): + for n in range(coreBoxMinorNodesCount): tx, td2 = [], [] for n2 in range(elementsCountAlong): x = boxx[n2][m][n] @@ -937,8 +936,8 @@ def _createBoxBoundaryNodeIdsList(self, startSkipCount=None, endSkipCount=None): boxBoundaryNodeToBoxId = [] elementsCountAlong = len(self._rimCoordinates[0]) - 1 - boxElementsCountRow = (self._elementsCountAcrossMajor - 2 * self._elementsCountTransition) + 1 - boxElementsCountColumn = (self._elementsCountAcrossMinor - 2 * self._elementsCountTransition) + 1 + coreBoxMajorNodesCount = self._elementsCountCoreBoxMajor + 1 + coreBoxMinorNodesCount = self._elementsCountCoreBoxMinor + 1 for n2 in range(elementsCountAlong + 1): if (n2 < startSkipCount) or (n2 > elementsCountAlong - endSkipCount) or self._boxNodeIds[n2] is None: boxBoundaryNodeIds.append(None) @@ -947,12 +946,12 @@ def _createBoxBoundaryNodeIdsList(self, startSkipCount=None, endSkipCount=None): else: boxBoundaryNodeIds.append([]) boxBoundaryNodeToBoxId.append([]) - for n3 in range(boxElementsCountRow): - if n3 == 0 or n3 == boxElementsCountRow - 1: + for n3 in range(coreBoxMajorNodesCount): + if n3 == 0 or n3 == coreBoxMajorNodesCount - 1: ids = self._boxNodeIds[n2][n3] if n3 == 0 else self._boxNodeIds[n2][n3][::-1] - n1List = list(range(boxElementsCountColumn)) if n3 == 0 else ( - list(range(boxElementsCountColumn - 1, -1, -1))) - boxBoundaryNodeIds[n2] += [ids[c] for c in range(boxElementsCountColumn)] + n1List = list(range(coreBoxMinorNodesCount)) if n3 == 0 else ( + list(range(coreBoxMinorNodesCount - 1, -1, -1))) + boxBoundaryNodeIds[n2] += [ids[c] for c in range(coreBoxMinorNodesCount)] for n1 in n1List: boxBoundaryNodeToBoxId[n2].append([n3, n1]) else: @@ -960,13 +959,13 @@ def _createBoxBoundaryNodeIdsList(self, startSkipCount=None, endSkipCount=None): boxBoundaryNodeIds[n2].append(self._boxNodeIds[n2][n3][n1]) boxBoundaryNodeToBoxId[n2].append([n3, n1]) - start = self._elementsCountAcrossMajor - 4 - 2 * (self._elementsCountTransition - 1) - idx = self._elementsCountAcrossMinor - 2 * (self._elementsCountTransition - 1) + start = self._elementsCountCoreBoxMajor - 2 + idx = self._elementsCountCoreBoxMinor + 2 for n in range(int(start), -1, -1): boxBoundaryNodeIds[n2].append(boxBoundaryNodeIds[n2].pop(idx + 2 * n)) boxBoundaryNodeToBoxId[n2].append(boxBoundaryNodeToBoxId[n2].pop(idx + 2 * n)) - nloop = self._elementsCountAcrossMinor // 2 - self._elementsCountTransition + nloop = self._elementsCountCoreBoxMinor // 2 for _ in range(nloop): boxBoundaryNodeIds[n2].insert(len(boxBoundaryNodeIds[n2]), boxBoundaryNodeIds[n2].pop(0)) boxBoundaryNodeToBoxId[n2].insert(len(boxBoundaryNodeToBoxId[n2]), @@ -983,19 +982,19 @@ def blendSampledCoordinates(cls, segment1, nodeIndexAlong1, segment2, nodeIndexA return # can't blend unless these match if segment1._isCore and segment2._isCore: - nodesCountAcrossMajor = len(segment1._boxCoordinates[0][nodeIndexAlong1]) - nodesCountAcrossMinor = len(segment1._boxCoordinates[0][nodeIndexAlong1][0]) + coreBoxMajorNodesCount = len(segment1._boxCoordinates[0][nodeIndexAlong1]) + coreBoxMinorNodesCount = len(segment1._boxCoordinates[0][nodeIndexAlong1][0]) nodesCountTransition = len(segment1._transitionCoordinates[0][nodeIndexAlong1]) if segment1._transitionCoordinates else 0 - if ((nodesCountAcrossMajor != len(segment2._boxCoordinates[0][nodeIndexAlong2])) or - (nodesCountAcrossMinor != len(segment2._boxCoordinates[0][nodeIndexAlong2][0])) or + if ((coreBoxMajorNodesCount != len(segment2._boxCoordinates[0][nodeIndexAlong2])) or + (coreBoxMinorNodesCount != len(segment2._boxCoordinates[0][nodeIndexAlong2][0])) or (nodesCountTransition != (len(segment2._transitionCoordinates[0][nodeIndexAlong2]) if segment1._transitionCoordinates else 0))): return # can't blend unless these match # blend core coordinates s1d2 = segment1._boxCoordinates[2][nodeIndexAlong1] s2d2 = segment2._boxCoordinates[2][nodeIndexAlong2] - for m in range(nodesCountAcrossMajor): - for n in range(nodesCountAcrossMinor): + for m in range(coreBoxMajorNodesCount): + for n in range(coreBoxMinorNodesCount): # harmonic mean magnitude s1d2Mag = magnitude(s1d2[m][n]) s2d2Mag = magnitude(s2d2[m][n]) @@ -1112,10 +1111,10 @@ def getTransitionCoordinatesListAlong(self, n1, n2List, n3): return paramsList - def getCoreNodesCountAcrossMajor(self): + def getCoreBoxMajorNodesCount(self): return len(self._boxCoordinates[0][0]) - def getCoreNodesCountAcrossMinor(self): + def getCoreBoxMinorNodesCount(self): return len(self._boxCoordinates[0][0][0]) def getElementsCountTransition(self): @@ -1167,7 +1166,7 @@ def getTriplePointIndexes(self): :return: A list of circular (n1) indexes used to identify triple points. """ elementsCountAround = self._elementsCountAround - nodesCountAcrossMinorHalf = self.getCoreNodesCountAcrossMinor() // 2 + nodesCountAcrossMinorHalf = self.getCoreBoxMinorNodesCount() // 2 triplePointIndexesList = [] for n in range(0, elementsCountAround, elementsCountAround // 2): @@ -1183,14 +1182,14 @@ def getTriplePointLocation(self, e1): and bottom right (location = -2). Location is None if not located at any of the four specified locations. :return: Location identifier. """ - em = (self._elementsCountAcrossMinor - 2) // 2 - (self._elementsCountTransition - 1) - eM = (self._elementsCountAcrossMajor - 2) // 2 - (self._elementsCountTransition - 1) + en = self._elementsCountCoreBoxMinor // 2 + em = self._elementsCountCoreBoxMajor // 2 ec = self._elementsCountAround // 4 - lftColumnElements = list(range(0, ec - eM)) + list(range(3 * ec + eM, self._elementsCountAround)) - topRowElements = list(range(ec - eM, ec + eM)) - rhtColumnElements = list((range(2 * ec - em, 2 * ec + em))) - btmRowElements = list(range(3 * ec - eM, 3 * ec + eM)) + lftColumnElements = list(range(0, ec - em)) + list(range(3 * ec + em, self._elementsCountAround)) + topRowElements = list(range(ec - em, ec + em)) + rhtColumnElements = list((range(2 * ec - en, 2 * ec + en))) + btmRowElements = list(range(3 * ec - em, 3 * ec + em)) idx = len(lftColumnElements) // 2 if e1 == topRowElements[0] or e1 == lftColumnElements[idx - 1]: @@ -1468,15 +1467,15 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create core box nodes if self._boxCoordinates: self._boxNodeIds[n2] = [] if self._boxNodeIds[n2] is None else self._boxNodeIds[n2] - nodesCountAcrossMajor = self.getCoreNodesCountAcrossMajor() - nodesCountAcrossMinor = self.getCoreNodesCountAcrossMinor() - for n3 in range(nodesCountAcrossMajor): + coreBoxMajorNodesCount = self.getCoreBoxMajorNodesCount() + coreBoxMinorNodesCount = self.getCoreBoxMinorNodesCount() + for n3 in range(coreBoxMajorNodesCount): self._boxNodeIds[n2].append([]) rx = self._boxCoordinates[0][n2][n3] rd1 = self._boxCoordinates[1][n2][n3] rd2 = self._boxCoordinates[2][n2][n3] rd3 = self._boxCoordinates[3][n2][n3] - for n1 in range(nodesCountAcrossMinor): + for n1 in range(coreBoxMinorNodesCount): nodeIdentifier = generateData.nextNodeIdentifier() node = nodes.createNode(nodeIdentifier, nodetemplate) fieldcache.setNode(node) @@ -1534,8 +1533,8 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): e2p = e2 + 1 if self._isCore: # create box elements - elementsCountAcrossMinor = self.getCoreNodesCountAcrossMinor() - 1 - elementsCountAcrossMajor = self.getCoreNodesCountAcrossMajor() - 1 + elementsCountAcrossMinor = self.getCoreBoxMinorNodesCount() - 1 + elementsCountAcrossMajor = self.getCoreBoxMajorNodesCount() - 1 for e3 in range(elementsCountAcrossMajor): e3p = e3 + 1 elementIds = [] @@ -1592,8 +1591,6 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create regular rim elements - all elements outside first transition layer elementsCountRimRegular = elementsCountRim - 1 if self._isCore else elementsCountRim - elementtemplateTransition = generateData.getMesh().createElementtemplate() - elementtemplateTransition.setElementShapeType(Element.SHAPE_TYPE_CUBE) for e3 in range(elementsCountRimRegular): ringElementIds = [] lastTransition = self._isCore and (e3 == (self._elementsCountTransition - 2)) @@ -1618,6 +1615,8 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): eft = generateData.createElementfieldtemplate() eft, scalefactors = addTricubicHermiteSerendipityEftParameterScaling( eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3) + elementtemplateTransition = mesh.createElementtemplate() + elementtemplateTransition.setElementShapeType(Element.SHAPE_TYPE_CUBE) elementtemplateTransition.defineField(coordinates, -1, eft) elementtemplate = elementtemplateTransition element = mesh.createElement(elementIdentifier, elementtemplate) @@ -1902,7 +1901,7 @@ def _sampleMidPoint(self, segmentsParameterLists): sideFactor = 1.0 outFactor = 1.0 # only used as relative proportion with non-zero sideFactor for s1 in range(segmentsCount - 1): - # fxs1 = segmentsParameterLists[s1][0][0] + fxs1 = segmentsParameterLists[s1][0][0] fd2s1 = segmentsParameterLists[s1][2][0] if not segmentsIn[s1]: fd2s1 = [-d for d in fd2s1] @@ -1929,20 +1928,24 @@ def _sampleMidPoint(self, segmentsParameterLists): 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)) - # fxs2 = segmentsParameterLists[s2][0][0] + fxs2 = segmentsParameterLists[s2][0][0] fd2s2 = segmentsParameterLists[s2][2][0] if segmentsIn[s2]: fd2s2 = [-d for d in fd2s2] norm_fd2s2 = normalize(fd2s2) # reduce md2 up to 50% depending on how out-of line they are - md2Factor = 0.75 + 0.25 * dot(norm_fd2s1, norm_fd2s2) - md2.append(mult(cd2, md2Factor)) + norm_cd2 = normalize(cd2) + # md2Factor = 0.75 + 0.25 * dot(norm_fd2s1, norm_fd2s2) + md2Factor1 = 0.75 + 0.25 * dot(norm_fd2s1, norm_cd2) + md2Factor2 = 0.75 + 0.25 * dot(norm_fd2s2, norm_cd2) + md2Factor = md2Factor1 * md2Factor2 + # md2.append(mult(cd2, md2Factor)) # smooth md2 with 2nd row from end, which it actually interpolates to - # tmd2 = smoothCubicHermiteDerivativesLine( - # [fxs1, cx, fxs2], [fd2s1, cd2, fd2s2], - # fixAllDirections=True, fixStartDerivative=True, fixEndDerivative=True, - # magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) - # md2.append(mult(tmd2[1], md2Factor)) + tmd2 = smoothCubicHermiteDerivativesLine( + [fxs1, cx, fxs2], [fd2s1, cd2, fd2s2], + fixAllDirections=True, fixStartDerivative=True, fixEndDerivative=True, + magnitudeScalingMode=DerivativeScalingMode.HARMONIC_MEAN) + md2.append(mult(tmd2[1], md2Factor)) if d3Defined: md3.append(mult(add(hd3[s1], hd3[s2]), 0.5)) if segmentsCount == 2: @@ -1993,9 +1996,9 @@ def _sampleMidPoint(self, segmentsParameterLists): for s in range(1, segmentsCount): angle += angleIncrement deltaAngle += angles[sequence[s]] - angle - magSum += 1.0 / magnitude(rd[sequence[s]]) + magSum += magnitude(rd[sequence[s]]) deltaAngle = deltaAngle / segmentsCount - d2Mean = segmentsCount / magSum + d2Mean = magSum / segmentsCount angle = deltaAngle sd = [None] * segmentsCount @@ -2032,11 +2035,12 @@ def _sampleMidPoint(self, segmentsParameterLists): print("TubeNetworkMeshJunction._sampleMidPoint not fully implemented for segmentsCount =", segmentsCount) si = (1, 2) - # get harmonic mean of d1 magnitude around midpoints + # regular mean of d1 magnitude around midpoints magSum = 0.0 for s in range(segmentsCount): - magSum += 1.0 / magnitude(md1[s]) - d1Mean = segmentsCount / magSum + magSum += magnitude(md1[s]) + d1Mean = magSum / segmentsCount + # overall harmonic mean of derivatives to err on the low side for the diagonal derivatives dMean = 2.0 / (1.0 / d1Mean + 1.0 / d2Mean) @@ -2067,7 +2071,7 @@ def _determineJunctionSequence(self): """ assert self._segmentsCount == 3 or self._segmentsCount == 4 - if self._segmentsCount == 3 and self._isCore: + if self._segmentsCount == 3: self._sequence = [0, 1, 2] elif self._segmentsCount == 4: # only support plus + shape for now, not tetrahedral @@ -2104,12 +2108,12 @@ def _determineJunctionSequence(self): else: return - def _sampleBifurcation(self, aroundCounts, acrossMajorCounts): + def _sampleBifurcation(self, aroundCounts, coreBoxMajorCounts): """ Blackbox function for sampling bifurcation coordinates. The rim coordinates are first sampled, then the box coordinates are sampled, if Core option is enabled. - :param aroundCounts: Number of elements around the tube. - :param acrossMajorCounts: Number of elements across major axis of the solid core. + :param aroundCounts: Number of elements around the 3 tube segments. + :param coreBoxMajorCounts: Number of elements across core box major axis of 3 tube segments. :return rimIndexesCount, boxIndexesCount: Total number of rimIndexes and boxIndexes, respectively. """ assert self._segmentsCount == 3 @@ -2133,7 +2137,7 @@ def _sampleBifurcation(self, aroundCounts, acrossMajorCounts): else: startNodeIndex1 = connectionCounts[s1] // 2 if self._segmentsIn[s2]: - startNodeIndex2 = connectionCounts[s2] // 2 + startNodeIndex2 = connectionCounts[s1] // 2 else: startNodeIndex2 = (aroundCounts[s2] - connectionCounts[s1]) // 2 for n in range(connectionCounts[s1] + 1): @@ -2167,29 +2171,29 @@ def _sampleBifurcation(self, aroundCounts, acrossMajorCounts): if self._isCore: elementsCountTransition = self._segments[0].getElementsCountTransition() sequence = self._sequence - majorBoxCounts = [acrossMajorCounts[s] - 2 * elementsCountTransition for s in range(3)] - majorConnectionCounts = [(majorBoxCounts[s] + majorBoxCounts[s - 2] - majorBoxCounts[s - 1]) // 2 for s + majorConnectionCounts = [(coreBoxMajorCounts[s] + coreBoxMajorCounts[s - 2] - coreBoxMajorCounts[s - 1]) // 2 for s in range(3)] - midIndexes = [majorBoxCounts[s] - majorConnectionCounts[s - 1] for s in range(3)] + midIndexes = [(coreBoxMajorCounts[s] - majorConnectionCounts[s]) if self._segmentsIn[s] + else majorConnectionCounts[s] for s in range(3)] # check compatible numbers of elements across major and minor directions - lastAcrossMinorCount = None + lastCoreBoxMinorCount = None for s in range(3): - acrossMinorCount = self._segments[s].getElementsCountAcrossMinor() - if lastAcrossMinorCount: - if acrossMinorCount != lastAcrossMinorCount: + coreBoxMinorCount = self._segments[s].getElementsCountCoreBoxMinor() + if lastCoreBoxMinorCount: + if coreBoxMinorCount != lastCoreBoxMinorCount: print("Can't make core bifurcation between different element counts across minor axis", - acrossMinorCount, "vs", lastAcrossMinorCount) + coreBoxMinorCount, "vs", lasCoreBoxMinorCount) return 0, 0 else: - lastAcrossMinorCount = acrossMinorCount - if majorBoxCounts[s] != (majorConnectionCounts[s - 1] + majorConnectionCounts[s]): - print("Can't make core bifurcation between elements counts across major axis", acrossMajorCounts) + lastCoreBoxMinorCount = coreBoxMinorCount + if coreBoxMajorCounts[s] != (majorConnectionCounts[s - 1] + majorConnectionCounts[s]): + print("Can't make core bifurcation between elements count across box major axis", coreBoxMajorCounts) return 0, 0 - nodesCountAcrossMinor = self._segments[0].getCoreNodesCountAcrossMinor() - nodesCountAcrossMajorList = [self._segments[s].getCoreNodesCountAcrossMajor() for s in range(3)] + coreBoxMinorNodesCount = self._segments[0].getCoreBoxMinorNodesCount() + nodesCountAcrossMajorList = [self._segments[s].getCoreBoxMajorNodesCount() for s in range(3)] self._segmentNodeToBoxIndex = \ - [[[None for _ in range(nodesCountAcrossMinor)] for _ in range(nodesCountAcrossMajorList[s])] + [[[None for _ in range(coreBoxMinorNodesCount)] for _ in range(nodesCountAcrossMajorList[s])] for s in range(self._segmentsCount)] for s1 in range(3): s2 = (s1 + 1) % 3 if sequence == [0, 1, 2] else (s1 - 1) % 3 @@ -2197,7 +2201,7 @@ def _sampleBifurcation(self, aroundCounts, acrossMajorCounts): midIndex2 = midIndexes[s2] for m in range(majorConnectionCounts[s1] + 1): m1 = (midIndex1 + m) if self._segmentsIn[s1] else (midIndex1 - m) - for n in range(nodesCountAcrossMinor): + for n in range(coreBoxMinorNodesCount): if m1 == midIndex1: indexGroup = [[0, midIndexes[0], n], [1, midIndexes[1], n], [2, midIndexes[2], n]] else: @@ -2215,12 +2219,12 @@ def _sampleBifurcation(self, aroundCounts, acrossMajorCounts): return rimIndexesCount, boxIndexesCount - def _sampleTrifurcation(self, aroundCounts, acrossMajorCounts): + def _sampleTrifurcation(self, aroundCounts, coreBoxMajorCounts): """ Blackbox function for sampling trifurcation coordinates. The rim coordinates are first sampled, then the box coordinates are sampled, if Core option is enabled. - :param aroundCounts: Number of elements around the tube. - :param acrossMajorCounts: Number of elements across major axis of the solid core. + :param aroundCounts: Number of elements around the 4 tube segments. + :param coreBoxMajorCounts: Number of elements across core box major axis of the 4 tube segments. :return rimIndexesCount, boxIndexesCount: Total number of rimIndexes and boxIndexes, respectively. """ assert self._segmentsCount == 4 @@ -2323,13 +2327,12 @@ def _sampleTrifurcation(self, aroundCounts, acrossMajorCounts): boxIndexesCount = 0 if self._isCore: elementsCountTransition = self._segments[0].getElementsCountTransition() - majorBoxCounts = [acrossMajorCounts[s] - 2 * elementsCountTransition for s in range(4)] - pairCount02 = majorBoxCounts[sequence[0]] + majorBoxCounts[sequence[2]] - pairCount13 = majorBoxCounts[sequence[1]] + majorBoxCounts[sequence[3]] + pairCount02 = coreBoxMajorCounts[sequence[0]] + coreBoxMajorCounts[sequence[2]] + pairCount13 = coreBoxMajorCounts[sequence[1]] + coreBoxMajorCounts[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] - freeAcrossCounts = [majorBoxCounts[sequence[s]] - throughCounts[s] for s in range(self._segmentsCount)] + freeAcrossCounts = [coreBoxMajorCounts[sequence[s]] - throughCounts[s] for s in range(self._segmentsCount)] if freeAcrossCounts[0] == freeAcrossCounts[2]: count03 = freeAcrossCounts[3] // 2 count12 = freeAcrossCounts[1] // 2 @@ -2343,25 +2346,25 @@ def _sampleTrifurcation(self, aroundCounts, acrossMajorCounts): - freeAcrossCounts[s - 1] + (s % 2)) // 2) for s in range(self._segmentsCount)] # check compatible numbers of elements across major and minor directions - lastAcrossMinorCount = None + lastCoreBoxMinorCount = None for s in range(self._segmentsCount): - acrossMinorCount = self._segments[s].getElementsCountAcrossMinor() - if lastAcrossMinorCount: - if acrossMinorCount != lastAcrossMinorCount: + coreBoxMinorCount = self._segments[s].getElementsCountCoreBoxMinor() + if lastCoreBoxMinorCount: + if coreBoxMinorCount != lastCoreBoxMinorCount: print("Can't make core trifurcation between different element counts across minor axis", - acrossMinorCount, "vs", lastAcrossMinorCount) + coreBoxMinorCount, "vs", lastCoreBoxMinorCount) return 0, 0 else: - lastAcrossMinorCount = acrossMinorCount - if (majorBoxCounts[sequence[s]] != ( + lastCoreBoxMinorCount = coreBoxMinorCount + if (coreBoxMajorCounts[sequence[s]] != ( majorConnectionCounts[s - 1] + throughCounts[s] + majorConnectionCounts[s])): - print("Can't make tube core box junction between major box counts", majorBoxCounts) + print("Can't make tube core box junction between box major counts", coreBoxMajorCounts) return 0, 0 - nodesCountAcrossMinor = self._segments[0].getCoreNodesCountAcrossMinor() - nodesCountAcrossMajorList = [self._segments[s].getCoreNodesCountAcrossMajor() for s in range(4)] - # midIndexes = [majorBoxCounts[s] - majorConnectionCounts[s] for s in range(3)] - midIndexes = [nodesCountAcrossMajor // 2 for nodesCountAcrossMajor in nodesCountAcrossMajorList] + coreBoxMinorNodesCount = self._segments[0].getCoreBoxMinorNodesCount() + coreBoxMajorNodesCounts = [self._segments[s].getCoreBoxMajorNodesCount() for s in range(4)] + # midIndexes = [coreBoxMajorCounts[s] - majorConnectionCounts[s] for s in range(3)] + midIndexes = [nodesCountAcrossMajor // 2 for nodesCountAcrossMajor in coreBoxMajorNodesCounts] halfThroughCounts = [throughCounts[s] // 2 for s in range(4)] connectingIndexesList = [[] for s in range(4)] @@ -2377,7 +2380,7 @@ def _sampleTrifurcation(self, aroundCounts, acrossMajorCounts): self._boxIndexToSegmentNodeList = [] self._segmentNodeToBoxIndex = \ - [[[None for _ in range(nodesCountAcrossMinor)] for _ in range(nodesCountAcrossMajorList[s])] + [[[None for _ in range(coreBoxMinorNodesCount)] for _ in range(coreBoxMajorNodesCounts[s])] for s in range(self._segmentsCount)] for os1 in range(self._segmentsCount): @@ -2387,8 +2390,8 @@ def _sampleTrifurcation(self, aroundCounts, acrossMajorCounts): s3 = sequence[(os1 + 2) % self._segmentsCount] s4 = sequence[(os1 + 3) % self._segmentsCount] aStartNodeIndex = midIndexes[s1] - nodesCountAcrossMajor1 = nodesCountAcrossMajorList[s1] - nodesCountAcrossMajor2 = nodesCountAcrossMajorList[s2] + nodesCountAcrossMajor1 = coreBoxMajorNodesCounts[s1] + nodesCountAcrossMajor2 = coreBoxMajorNodesCounts[s2] connectingIndexes1 = connectingIndexesList[s1] connectingIndexes2 = connectingIndexesList[s2] connectingIndexes3 = connectingIndexesList[s3] @@ -2396,9 +2399,9 @@ def _sampleTrifurcation(self, aroundCounts, acrossMajorCounts): throughIndexes = list(range(connectingIndexes1[0] + 1, connectingIndexes1[1])) \ if throughCounts[os1] else [None] - for m in range((majorBoxCounts[s1] // 2) + 1): + for m in range((coreBoxMajorCounts[s1] // 2) + 1): m1 = (m + aStartNodeIndex) if self._segmentsIn[s1] else (aStartNodeIndex - m) - for n in range(nodesCountAcrossMinor): + for n in range(coreBoxMinorNodesCount): indexGroup = None if m1 in throughIndexes: i = m1 - midIndexes[s1] @@ -2458,7 +2461,7 @@ def _optimiseRimIndexes(self, aroundCounts, rimIndexesCount, boxIndexesCount): if self._isCore: # core is a regular grid with 2 or 4 permutations -- latter only if same major and minor counts segment = self._segments[s] - if segment.getElementsCountAcrossMajor() == segment.getElementsCountAcrossMinor(): + if segment.getElementsCountCoreBoxMajor() == segment.getElementsCountCoreBoxMinor(): count = 4 else: count = 2 @@ -2514,8 +2517,8 @@ def _optimiseRimIndexes(self, aroundCounts, rimIndexesCount, boxIndexesCount): for s in range(self._segmentsCount): segment = self._segments[s] segmentRotationCases.append(4 * minIndexes[s] // aroundCounts[s]) - segmentMajorBoxSizes.append(segment.getElementsCountAcrossMajor() - 2 * elementsCountTransition) - segmentMinorBoxSizes.append(segment.getElementsCountAcrossMinor() - 2 * elementsCountTransition) + segmentMajorBoxSizes.append(segment.getElementsCountCoreBoxMajor()) + segmentMinorBoxSizes.append(segment.getElementsCountCoreBoxMinor()) for boxIndex in range(boxIndexesCount): segmentNodeList = self._boxIndexToSegmentNodeList[boxIndex] for segmentNode in segmentNodeList: @@ -2550,16 +2553,16 @@ def sample(self, targetElementLength): return aroundCounts = [segment.getElementsCountAround() for segment in self._segments] - acrossMajorCounts = [segment.getElementsCountAcrossMajor() for segment in self._segments] + coreBoxMajorCounts = [segment.getElementsCountCoreBoxMajor() for segment in self._segments] # determine junction sequence self._determineJunctionSequence() if self._segmentsCount == 3: - rimIndexesCount, boxIndexesCount = self._sampleBifurcation(aroundCounts, acrossMajorCounts) + rimIndexesCount, boxIndexesCount = self._sampleBifurcation(aroundCounts, coreBoxMajorCounts) elif self._segmentsCount == 4: - rimIndexesCount, boxIndexesCount = self._sampleTrifurcation(aroundCounts, acrossMajorCounts) + rimIndexesCount, boxIndexesCount = self._sampleTrifurcation(aroundCounts, coreBoxMajorCounts) else: print("Tube network mesh not implemented for", self._segmentsCount, "segments at junction") @@ -2620,18 +2623,18 @@ def _createBoxBoundaryNodeIdsList(self, s): """ boxBoundaryNodeIds = [] boxBoundaryNodeToBoxId = [] - elementsCountAcrossMajor = self._segments[s].getElementsCountAcrossMajor() - elementsCountAcrossMinor = self._segments[s].getElementsCountAcrossMinor() + elementsCountCoreBoxMajor = self._segments[s].getElementsCountCoreBoxMajor() + elementsCountCoreBoxMinor = self._segments[s].getElementsCountCoreBoxMinor() elementsCountTransition = self._segments[s].getElementsCountAcrossTransition() - boxElementsCountRow = (elementsCountAcrossMajor - 2 * elementsCountTransition) + 1 - boxElementsCountColumn = (elementsCountAcrossMinor - 2 * elementsCountTransition) + 1 + coreBoxMajorNodesCount = elementsCountCoreBoxMajor + 1 + coreBoxMinorNodesCount = elementsCountCoreBoxMinor + 1 - for n3 in range(boxElementsCountRow): - if n3 == 0 or n3 == boxElementsCountRow - 1: + for n3 in range(coreBoxMajorNodesCount): + if n3 == 0 or n3 == coreBoxMajorNodesCount - 1: ids = self._boxNodeIds[s][n3] if n3 == 0 else self._boxNodeIds[s][n3][::-1] - n1List = list(range(boxElementsCountColumn)) if n3 == 0 else ( - list(range(boxElementsCountColumn - 1, -1, -1))) - boxBoundaryNodeIds += [ids[c] for c in range(boxElementsCountColumn)] + n1List = list(range(coreBoxMinorNodesCount)) if n3 == 0 else ( + list(range(coreBoxMinorNodesCount - 1, -1, -1))) + boxBoundaryNodeIds += [ids[c] for c in range(coreBoxMinorNodesCount)] for n1 in n1List: boxBoundaryNodeToBoxId.append([n3, n1]) else: @@ -2639,13 +2642,13 @@ def _createBoxBoundaryNodeIdsList(self, s): boxBoundaryNodeIds.append(self._boxNodeIds[s][n3][n1]) boxBoundaryNodeToBoxId.append([n3, n1]) - start = elementsCountAcrossMajor - 4 - 2 * (elementsCountTransition - 1) - idx = elementsCountAcrossMinor - 2 * (elementsCountTransition - 1) + start = elementsCountCoreBoxMajor - 2 + idx = elementsCountCoreBoxMinor + 2 for n in range(int(start), -1, -1): boxBoundaryNodeIds.append(boxBoundaryNodeIds.pop(idx + 2 * n)) boxBoundaryNodeToBoxId.append(boxBoundaryNodeToBoxId.pop(idx + 2 * n)) - nloop = elementsCountAcrossMinor // 2 - elementsCountTransition + nloop = elementsCountCoreBoxMinor // 2 for _ in range(nloop): boxBoundaryNodeIds.insert(len(boxBoundaryNodeIds), boxBoundaryNodeIds.pop(0)) boxBoundaryNodeToBoxId.insert(len(boxBoundaryNodeToBoxId), @@ -2666,12 +2669,11 @@ def _generateBoxElements(self, s, n2, mesh, elementtemplate, coordinates, segmen Blackbox function for generating core box elements at a junction. """ annotationMeshGroups = generateData.getAnnotationMeshGroups(segment.getAnnotationTerms()) - boxElementsCountAcrossMinor = self._segments[0].getCoreNodesCountAcrossMinor() - 1 - boxElementsCountAcrossMajor = [self._segments[s].getCoreNodesCountAcrossMajor() - 1 + boxElementsCountAcrossMinor = self._segments[0].getCoreBoxMinorNodesCount() - 1 + boxElementsCountAcrossMajor = [self._segments[s].getCoreBoxMajorNodesCount() - 1 for s in range(self._segmentsCount)] - acrossMajorCounts = [segment.getElementsCountAcrossMajor() for segment in self._segments] - is6WayTriplePoint = True if (((max(acrossMajorCounts) - 2) // 2) == (min(acrossMajorCounts) - 2) - and (self._segmentsCount == 3)) else False + coreBoxMajorCounts = [segment.getElementsCountCoreBoxMajor() for segment in self._segments] + is6WayTriplePoint = ((self._segmentsCount == 3) and ((max(coreBoxMajorCounts) // 2) == min(coreBoxMajorCounts))) eftList = [[None] * boxElementsCountAcrossMinor for _ in range(boxElementsCountAcrossMajor[s])] scalefactorsList = [[None] * boxElementsCountAcrossMinor for _ in range(boxElementsCountAcrossMajor[s])] @@ -2736,17 +2738,16 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, Blackbox function for generating first row of core transition elements after box at a junction. """ annotationMeshGroups = generateData.getAnnotationMeshGroups(segment.getAnnotationTerms()) - nodesCountAcrossMinor = self._segments[0].getCoreNodesCountAcrossMinor() - nodesCountAcrossMajor = [self._segments[s].getCoreNodesCountAcrossMajor() for s in + coreBoxMinorNodesCount = self._segments[0].getCoreBoxMinorNodesCount() + coreBoxMajorNodesCounts = [self._segments[s].getCoreBoxMajorNodesCount() for s in range(self._segmentsCount)] - acrossMajorCounts = [segment.getElementsCountAcrossMajor() for segment in self._segments] + coreBoxMajorCounts = [segment.getElementsCountCoreBoxMajor() for segment in self._segments] triplePointIndexesList = segment.getTriplePointIndexes() elementsCountTransition = self._segments[0].getElementsCountTransition() - coreBoxMajorCounts = [count - 2 * elementsCountTransition for count in acrossMajorCounts] is6WayTriplePoint = (self._segmentsCount == 3) and ((max(coreBoxMajorCounts) // 2) == min(coreBoxMajorCounts)) - pSegment = acrossMajorCounts.index(max(acrossMajorCounts)) - topMidIndex = (nodesCountAcrossMajor[pSegment] // 2) + (nodesCountAcrossMinor // 2) + pSegment = coreBoxMajorCounts.index(max(coreBoxMajorCounts)) + topMidIndex = (coreBoxMajorNodesCounts[pSegment] // 2) + (coreBoxMinorNodesCount // 2) bottomMidIndex = elementsCountAround - topMidIndex midIndexes = [topMidIndex, bottomMidIndex] @@ -2883,10 +2884,11 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): self._rimNodeIds = [[None] * rimIndexesCount for _ in range(nodesCountRim)] if self._boxCoordinates: - nodesCountAcrossMinor = self._segments[0].getCoreNodesCountAcrossMinor() - acrossMajorCounts = [segment.getElementsCountAcrossMajor() for segment in self._segments] - self._boxNodeIds = [[[None for _ in range(nodesCountAcrossMinor)] for _ in range(acrossMajorCounts[s])] - for s in range(self._segmentsCount)] + coreBoxMinorNodesCount = self._segments[0].getCoreBoxMinorNodesCount() + self._boxNodeIds = [ + [[None for n in range(coreBoxMinorNodesCount)] + for m in range(self._segments[s].getCoreBoxMajorNodesCount())] + for s in range(self._segmentsCount)] coordinates = generateData.getCoordinates() fieldcache = generateData.getFieldcache() @@ -2919,9 +2921,10 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): # create box nodes bx, bd1, bd2, bd3 = (self._boxCoordinates[0], self._boxCoordinates[1], self._boxCoordinates[2], self._boxCoordinates[3]) - nodesCountAcrossMajor = self._segments[s].getCoreNodesCountAcrossMajor() - for n3 in range(nodesCountAcrossMajor): - for n1 in range(nodesCountAcrossMinor): + coreBoxMajorNodesCount = self._segments[s].getCoreBoxMajorNodesCount() + coreBoxMinorNodesCount = self._segments[s].getCoreBoxMinorNodesCount() + for n3 in range(coreBoxMajorNodesCount): + for n1 in range(coreBoxMinorNodesCount): boxIndex = self._segmentNodeToBoxIndex[s][n3][n1] segmentNodeList = self._boxIndexToSegmentNodeList[boxIndex] nodeIdentifiersCheck = [] @@ -3042,8 +3045,8 @@ class TubeNetworkMeshBuilder(NetworkMeshBuilder): def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], annotationElementsCountsAlong: list = [], annotationElementsCountsAround: list = [], - defaultElementsCountAcrossMajor: int = 4, elementsCountTransition: int = 1, - annotationElementsCountsAcrossMajor: list = [], isCore=False, useOuterTrimSurfaces=False): + defaultElementsCountCoreBoxMinor: int = 2, elementsCountTransition: int = 1, + annotationElementsCountsCoreBoxMinor: list = [], isCore=False, useOuterTrimSurfaces=False): """ :param networkMesh: Description of the topology of the network layout. :param targetElementDensityAlongLongestSegment: @@ -3056,9 +3059,10 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg :param annotationElementsCountsAround: List in same order as layoutAnnotationGroups, specifying fixed number around segment with any elements in the annotation group. Client must ensure exclusive map from segments. Groups with zero value or past end of this list use the defaultElementsCountAround. - :param defaultElementsCountAcrossMajor: + :param defaultElementsCountCoreBoxMinor: Number of elements across the core box in the minor/d3 direction. :param elementsCountTransition: - :param annotationElementsCountsAcrossMajor: + :param annotationElementsCountsCoreBoxMinor: List in same order as layoutAnnotationGroups, specifying numbers of + elements across core box minor/d3 direction. :param isCore: Set to True to define solid core box and transition elements. :param useOuterTrimSurfaces: Set to True to use common trim surfaces calculated from outer. """ @@ -3074,9 +3078,9 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg self._layoutInnerCoordinates = None self._isCore = isCore self._useOuterTrimSurfaces = useOuterTrimSurfaces - self._defaultElementsCountAcrossMajor = defaultElementsCountAcrossMajor + self._defaultElementsCountCoreBoxMinor = defaultElementsCountCoreBoxMinor self._elementsCountTransition = elementsCountTransition - self._annotationElementsCountsAcrossMajor = annotationElementsCountsAcrossMajor + self._annotationElementsCountsCoreBoxMinor = annotationElementsCountsCoreBoxMinor def createSegment(self, networkSegment): pathParametersList = [get_nodeset_path_ordered_field_parameters( @@ -3087,7 +3091,7 @@ def createSegment(self, networkSegment): self._layoutNodes, self._layoutInnerCoordinates, pathValueLabels, networkSegment.getNodeIdentifiers(), networkSegment.getNodeVersions())) elementsCountAround = self._defaultElementsCountAround - elementsCountAcrossMajor = self._defaultElementsCountAcrossMajor + elementsCountCoreBoxMinor = self._defaultElementsCountCoreBoxMinor i = 0 for layoutAnnotationGroup in self._layoutAnnotationGroups: @@ -3102,17 +3106,17 @@ def createSegment(self, networkSegment): annotationElementsCountAcrossMinor = [] i = 0 for layoutAnnotationGroup in self._layoutAnnotationGroups: - if i >= len(self._annotationElementsCountsAcrossMajor): + if i >= len(self._annotationElementsCountsCoreBoxMinor): break - if self._annotationElementsCountsAcrossMajor[i] > 0: + if self._annotationElementsCountsCoreBoxMinor[i] > 0: if networkSegment.hasLayoutElementsInMeshGroup( layoutAnnotationGroup.getMeshGroup(self._layoutMesh)): - elementsCountAcrossMajor = self._annotationElementsCountsAcrossMajor[i] + elementsCountCoreBoxMinor = self._annotationElementsCountsCoreBoxMinor[i] break i += 1 return TubeNetworkMeshSegment(networkSegment, pathParametersList, elementsCountAround, - self._elementsCountThroughWall, self._isCore, elementsCountAcrossMajor, + self._elementsCountThroughWall, self._isCore, elementsCountCoreBoxMinor, self._elementsCountTransition) def createJunction(self, inSegments, outSegments): diff --git a/tests/test_network.py b/tests/test_network.py index 9efc1be5..3339e40e 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -124,7 +124,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.931297913271377, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 1.9298310795249618, delta=X_TOL) def test_2d_tube_network_snake(self): """ @@ -243,7 +243,7 @@ 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.045580778559924, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 4.040053575361621, delta=X_TOL) def test_2d_tube_network_trifurcation(self): """ @@ -288,7 +288,7 @@ def test_2d_tube_network_trifurcation(self): fieldcache = fieldmodule.createFieldcache() result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 2.7951498826590973, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.791976505648992, delta=X_TOL) def test_2d_tube_network_vase(self): """ @@ -407,9 +407,9 @@ def test_3d_tube_network_bifurcation(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.034977175657495335, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 1.92967134964477, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 1.5609615408034399, delta=X_TOL) + self.assertAlmostEqual(volume, 0.0349284962620723, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 1.9284739468709864, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 1.5595435883893172, delta=X_TOL) def test_3d_tube_network_bifurcation_core(self): """ @@ -470,8 +470,98 @@ def test_3d_tube_network_bifurcation_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.09907643906540035, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.0238210110948307, delta=X_TOL) + self.assertAlmostEqual(volume, 0.09883609668362349, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.0226236083210507, delta=X_TOL) + + def test_3d_tube_network_converging_bifurcation_core(self): + """ + Test converging bifurcation 3-D tube network with solid core and 12, 12, 8 elements around. + """ + scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Bifurcation") + settings = scaffoldPackage.getScaffoldSettings() + networkLayoutScaffoldPackage = settings["Network layout"] + networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() + self.assertTrue(networkLayoutSettings["Define inner coordinates"]) + self.assertEqual(13, len(settings)) + self.assertEqual(8, settings["Number of elements around"]) + self.assertEqual(1, settings["Number of elements through shell"]) + self.assertEqual([0], settings["Annotation numbers of elements around"]) + self.assertEqual(4.0, settings["Target element density along longest segment"]) + self.assertEqual([0], settings["Annotation numbers of elements along"]) + self.assertFalse(settings["Use linear through shell"]) + self.assertFalse(settings["Show trim surfaces"]) + self.assertFalse(settings["Core"]) + self.assertEqual(2, settings["Number of elements across core box minor"]) + self.assertEqual(1, settings["Number of elements across core transition"]) + self.assertEqual([0], settings["Annotation numbers of elements across core box minor"]) + settings["Core"] = True + settings["Number of elements around"] = 12 + settings["Annotation numbers of elements around"] = [8] + + context = Context("Test") + region = context.getDefaultRegion() + + # add a user-defined annotation group to network layout to vary elements count around. Must generate first + tmpRegion = region.createRegion() + tmpFieldmodule = tmpRegion.getFieldmodule() + networkLayoutScaffoldPackage.generate(tmpRegion) + + annotationGroup1 = networkLayoutScaffoldPackage.createUserAnnotationGroup(("segment 3", "SEGMENT:3")) + group = annotationGroup1.getGroup() + mesh1d = tmpFieldmodule.findMeshByDimension(1) + meshGroup = group.createMeshGroup(mesh1d) + mesh_group_add_identifier_ranges(meshGroup, [[3, 3]]) + self.assertEqual(1, meshGroup.getSize()) + self.assertEqual(1, annotationGroup1.getDimension()) + identifier_ranges_string = identifier_ranges_to_string(mesh_group_to_identifier_ranges(meshGroup)) + self.assertEqual("3", identifier_ranges_string) + networkLayoutScaffoldPackage.updateUserAnnotationGroups() + + self.assertTrue(region.isValid()) + scaffoldPackage.generate(region) + annotationGroups = scaffoldPackage.getAnnotationGroups() + self.assertEqual(1, len(annotationGroups)) + + fieldmodule = region.getFieldmodule() + mesh3d = fieldmodule.findMeshByDimension(3) + + mesh3d = fieldmodule.findMeshByDimension(3) + self.assertEqual(336, mesh3d.getSize()) + nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) + self.assertEqual(460, nodes.getSize()) + coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() + self.assertTrue(coordinates.isValid()) + + # check annotation group transferred to 3D tube + annotationGroup = annotationGroups[0] + self.assertEqual("segment 3", annotationGroup.getName()) + self.assertEqual("SEGMENT:3", annotationGroup.getId()) + self.assertEqual(80, annotationGroup.getMeshGroup(fieldmodule.findMeshByDimension(3)).getSize()) + + X_TOL = 1.0E-6 + + minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) + assertAlmostEqualList(self, minimums, [0.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() + mesh2d = fieldmodule.findMeshByDimension(2) + fieldcache = fieldmodule.createFieldcache() + + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, mesh3d) + volumeField.setNumbersOfPoints(4) + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + surfaceAreaField = fieldmodule.createFieldMeshIntegral(isExterior, coordinates, mesh2d) + surfaceAreaField.setNumbersOfPoints(4) + result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + + self.assertAlmostEqual(volume, 0.09854306375590477, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.0220707879327655, delta=X_TOL) def test_3d_tube_network_line_core_transition2(self): """ @@ -626,9 +716,9 @@ def test_3d_tube_network_sphere_cube(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.07364074411579775, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 4.0455683508806, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 3.3313407313622867, delta=X_TOL) + self.assertAlmostEqual(volume, 0.07331492814968889, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 4.04004649646839, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 3.325814823258647, delta=X_TOL) def test_3d_tube_network_sphere_cube_core(self): """ @@ -715,8 +805,8 @@ def test_3d_tube_network_sphere_cube_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.21360737563518303, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 4.0455683508805995, delta=X_TOL) + self.assertAlmostEqual(volume, 0.21259898629834004, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 4.04004649646839, delta=X_TOL) def test_3d_tube_network_trifurcation_cross(self): @@ -809,13 +899,13 @@ def test_3d_tube_network_trifurcation_cross(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.047235196748105515, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 2.600683124988524, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 2.1164499963602124, delta=X_TOL) + self.assertAlmostEqual(volume, 0.047184101028608746, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.5994192161530587, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 2.1144506806009487, delta=X_TOL) def test_3d_tube_network_trifurcation_cross_core(self): """ - Test trifurcation cross 3-D tube network with solid coreis generated correctly with + Test trifurcation cross 3-D tube network with solid core is generated correctly with variable elements count around. """ scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Trifurcation cross") @@ -900,8 +990,8 @@ def test_3d_tube_network_trifurcation_cross_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.13499394208386956, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.7252510632808065, delta=X_TOL) + self.assertAlmostEqual(volume, 0.1347351954323967, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.7245696029563424, delta=X_TOL) def test_3d_box_network_bifurcation(self): """ diff --git a/tests/test_uterus.py b/tests/test_uterus.py index b626a935..50b85d81 100644 --- a/tests/test_uterus.py +++ b/tests/test_uterus.py @@ -80,8 +80,8 @@ def test_uterus1(self): self.assertEqual(result, RESULT_OK) result, volume = volumeField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(surfaceArea, 262.3194982053719, delta=1.0E-6) - self.assertAlmostEqual(volume, 183.07329124848016, delta=1.0E-6) + self.assertAlmostEqual(surfaceArea, 262.06878921216844, delta=1.0E-6) + self.assertAlmostEqual(volume, 182.50188979389358, delta=1.0E-6) fieldmodule.defineAllFaces() for annotationGroup in annotationGroups: diff --git a/tests/test_wholebody2.py b/tests/test_wholebody2.py index f42e4761..2da4483f 100644 --- a/tests/test_wholebody2.py +++ b/tests/test_wholebody2.py @@ -68,7 +68,7 @@ def test_wholebody2_core(self): coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() self.assertTrue(coordinates.isValid()) minimums, maximums = evaluateFieldNodesetRange(coordinates, nodes) - tol = 1.0E-6 + tol = 1.0E-4 assertAlmostEqualList(self, minimums, [0.0, -3.7000751482231564, -1.25], tol) assertAlmostEqualList(self, maximums, [20.437483381451223, 3.7000751482231564, 2.15], tol) @@ -88,17 +88,17 @@ def test_wholebody2_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 99.02238171504763, delta=tol) - self.assertAlmostEqual(surfaceArea, 229.82780303324995, delta=tol) + self.assertAlmostEqual(volume, 99.01720012090396, delta=tol) + self.assertAlmostEqual(surfaceArea, 229.87327935824305, delta=tol) # check some annotation groups: expectedSizes3d = { - "abdominal cavity": (40, 10.094264544167423), - "core": (428, 45.817610658773326), + "abdominal cavity": (40, 10.078325917475231), + "core": (428, 45.79157371575808), "head": (64, 6.909618374858558), - "thoracic cavity": (40, 7.140116968045268), - "shell": (276, 53.20333000107456) + "thoracic cavity": (40, 7.135159387520382), + "shell": (276, 53.227959536902574) } for name in expectedSizes3d: term = get_body_term(name) @@ -114,13 +114,13 @@ def test_wholebody2_core(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "abdominal cavity boundary": (64, 27.459787335655104), + "abdominal cavity boundary": (64, 27.452684317517825), "diaphragm": (20, 3.0778936559347208), - "left arm skin epidermis": (68, 22.595664816330093), - "left leg skin epidermis": (68, 55.17540980396306), - "right arm skin epidermis": (68, 22.595664816330093), - "right leg skin epidermis": (68, 55.17540980396306), - "skin epidermis": (388, 229.82780303324995), + "left arm skin epidermis": (68, 22.605445370196083), + "left leg skin epidermis": (68, 55.21582811667045), + "right arm skin epidermis": (68, 22.605445370196083), + "right leg skin epidermis": (68, 55.21582811667045), + "skin epidermis": (388, 229.87327935824305), "thoracic cavity boundary": (64, 20.600420538940487) } for name in expectedSizes2d: @@ -137,7 +137,7 @@ def test_wholebody2_core(self): self.assertAlmostEqual(surfaceArea, expectedSizes2d[name][1], delta=tol) expectedSizes1d = { - "spinal cord": (8, 10.855253444098556) + "spinal cord": (8, 10.856804626156244) } for name in expectedSizes1d: term = get_body_term(name) @@ -212,13 +212,13 @@ def test_wholebody2_tube(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 53.20196079841706, delta=tol) - self.assertAlmostEqual(outerSurfaceArea, 225.72283168985595, delta=tol) - self.assertAlmostEqual(innerSurfaceArea, 155.95593103737943, delta=tol) + self.assertAlmostEqual(volume, 53.22727912319599, delta=tol) + self.assertAlmostEqual(outerSurfaceArea, 225.76830801484903, delta=tol) + self.assertAlmostEqual(innerSurfaceArea, 155.86248051318998, delta=tol) # check some annotationGroups: expectedSizes2d = { - "skin epidermis": (320, 228.9705874171508) + "skin epidermis": (320, 229.01606374214393) } for name in expectedSizes2d: term = get_body_term(name) From dc9445d827fd38b4af101cbf056769c8ae8f4cd8 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Mon, 14 Oct 2024 19:47:50 +1300 Subject: [PATCH 15/20] Fix trifurcation rim --- src/scaffoldmaker/utils/tubenetworkmesh.py | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index efb05b70..219ddb99 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -199,8 +199,6 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem self._elementsCountCoreBoxMinor = elementsCountCoreBoxMinor self._elementsCountTransition = elementsCountTransition - # if self._isCore and self._elementsCountTransition > 1: - # self._elementsCountAround = (elementsCountAround - 8 * (self._elementsCountTransition - 1)) assert elementsCountThroughWall > 0 self._elementsCountThroughWall = elementsCountThroughWall self._rawTubeCoordinatesList = [] @@ -2181,13 +2179,14 @@ def _sampleBifurcation(self, aroundCounts, coreBoxMajorCounts): coreBoxMinorCount = self._segments[s].getElementsCountCoreBoxMinor() if lastCoreBoxMinorCount: if coreBoxMinorCount != lastCoreBoxMinorCount: - print("Can't make core bifurcation between different element counts across minor axis", - coreBoxMinorCount, "vs", lasCoreBoxMinorCount) + print("Can't make core bifurcation between different box minor axis element counts", + coreBoxMinorCount, "vs", lastCoreBoxMinorCount) return 0, 0 else: lastCoreBoxMinorCount = coreBoxMinorCount - if coreBoxMajorCounts[s] != (majorConnectionCounts[s - 1] + majorConnectionCounts[s]): - print("Can't make core bifurcation between elements count across box major axis", coreBoxMajorCounts) + if ((majorConnectionCounts[s] < 0) or + (coreBoxMajorCounts[s] != (majorConnectionCounts[s - 1] + majorConnectionCounts[s]))): + print("Can't make core bifurcation between box major axis element counts", coreBoxMajorCounts) return 0, 0 coreBoxMinorNodesCount = self._segments[0].getCoreBoxMinorNodesCount() @@ -2252,7 +2251,8 @@ def _sampleTrifurcation(self, aroundCounts, coreBoxMajorCounts): - freeAroundCounts[s - 1] + (s % 2)) // 2) for s in range(self._segmentsCount)] for s in range(self._segmentsCount): - if (aroundCounts[sequence[s]] != (connectionCounts[s - 1] + throughCounts[s] + connectionCounts[s])): + if ((connectionCounts[s] < 1) or + (aroundCounts[sequence[s]] != (connectionCounts[s - 1] + throughCounts[s] + connectionCounts[s]))): print("Can't make tube junction between elements counts around", aroundCounts) return 0, 0 @@ -2275,8 +2275,8 @@ def _sampleTrifurcation(self, aroundCounts, coreBoxMajorCounts): else: startNodeIndex2 = (aroundCounts[s2] - os1ConnectionCount) // 2 if self._segmentsIn[s3]: - startNodeIndex3h = os2ConnectionCount // -2 - startNodeIndex3l = startNodeIndex3h - (os2ConnectionCount - os1ConnectionCount) + startNodeIndex3l = os2ConnectionCount // -2 + startNodeIndex3h = startNodeIndex3l - (os2ConnectionCount - os1ConnectionCount) else: startNodeIndex3l = (aroundCounts[s3] - os2ConnectionCount) // 2 startNodeIndex3h = startNodeIndex3l + (os2ConnectionCount - os1ConnectionCount) @@ -2290,7 +2290,7 @@ def _sampleTrifurcation(self, aroundCounts, coreBoxMajorCounts): segmentIndexes.append(s2) nodeIndexes.append(n2 % aroundCounts[s2]) if halfThroughCount and ((n <= 0) or (n >= os1ConnectionCount)): - n3 = ((startNodeIndex3l if n <= 0 else startNodeIndex3h) + + n3 = ((startNodeIndex3l if (n <= 0) else startNodeIndex3h) + ((os2ConnectionCount - n) if self._segmentsIn[s3] else n)) segmentIndexes.append(s3) nodeIndexes.append(n3 % aroundCounts[s3]) @@ -2351,14 +2351,14 @@ def _sampleTrifurcation(self, aroundCounts, coreBoxMajorCounts): coreBoxMinorCount = self._segments[s].getElementsCountCoreBoxMinor() if lastCoreBoxMinorCount: if coreBoxMinorCount != lastCoreBoxMinorCount: - print("Can't make core trifurcation between different element counts across minor axis", + print("Can't make core trifurcation between different box minor axis element counts", coreBoxMinorCount, "vs", lastCoreBoxMinorCount) return 0, 0 else: lastCoreBoxMinorCount = coreBoxMinorCount - if (coreBoxMajorCounts[sequence[s]] != ( - majorConnectionCounts[s - 1] + throughCounts[s] + majorConnectionCounts[s])): - print("Can't make tube core box junction between box major counts", coreBoxMajorCounts) + if ((majorConnectionCounts[s] < 0) or (coreBoxMajorCounts[sequence[s]] != ( + majorConnectionCounts[s - 1] + throughCounts[s] + majorConnectionCounts[s]))): + print("Can't make core trifurcation between box major axis element counts", coreBoxMajorCounts) return 0, 0 coreBoxMinorNodesCount = self._segments[0].getCoreBoxMinorNodesCount() From bf324e04e804c3444a2f1877316110a020bac3d6 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Mon, 21 Oct 2024 14:29:26 +1300 Subject: [PATCH 16/20] Fix bifurcation 6-way triple points Fix core-shell scale factor calculation --- .../meshtypes/meshtype_1d_network_layout1.py | 7 +- .../meshtypes/meshtype_3d_wholebody2.py | 19 +-- src/scaffoldmaker/utils/eft_utils.py | 154 ++++++++++++++---- src/scaffoldmaker/utils/tubenetworkmesh.py | 137 ++++++++-------- tests/test_network.py | 79 ++++++--- tests/test_wholebody2.py | 32 ++-- 6 files changed, 265 insertions(+), 163 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py index af0a5f16..b77dd282 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_1d_network_layout1.py @@ -71,7 +71,7 @@ def generateBaseMesh(cls, region, options): :param options: Dict containing options. See getDefaultOptions(). :return: [] empty list of AnnotationGroup, NetworkMesh """ - parameterSetName = options['Base parameter set'] + parameterSetName = options["Base parameter set"] structure = options["Structure"] defineInnerCoordinates = options["Define inner coordinates"] networkMesh = NetworkMesh(structure) @@ -265,6 +265,7 @@ def editStructure(cls, region, options, networkMesh, functionOptions, editGroupN with ChangeManager(fieldmodule): clearRegion(region) structure = options["Structure"] = functionOptions["Structure"] + options["Base parameter set"] = "Default" # to not assign coordinates for one of the special sets networkMesh.build(structure) networkMesh.create1DLayoutMesh(region) coordinates = find_or_create_field_coordinates(fieldmodule).castFiniteElement() @@ -403,8 +404,8 @@ def makeSideDerivativesNormal(cls, region, options, networkMesh, functionOptions print("Make side derivatives normal: inner coordinates field not defined") return False, False useCoordinates = coordinates if functionOptions["Field"]["coordinates"] else innerCoordinates - makeD2Normal = functionOptions['Make D2 normal'] - makeD3Normal = functionOptions['Make D3 normal'] + makeD2Normal = functionOptions["Make D2 normal"] + makeD3Normal = functionOptions["Make D3 normal"] if not (makeD2Normal or makeD3Normal): return False, False nodeset = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index a03f8185..d05e75ed 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -590,23 +590,11 @@ def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurface region, meshDimension, isLinearThroughWall, isShowTrimSurfaces, coordinateFieldName, startNodeIdentifier, startElementIdentifier) # annotation groups are created on demand: - self._coreGroup = None - self._shellGroup = None self._leftGroup = None self._rightGroup = None self._dorsalGroup = None self._ventralGroup = None - def getCoreMeshGroup(self): - if not self._coreGroup: - self._coreGroup = self.getOrCreateAnnotationGroup(("core", "")) - return self._coreGroup.getMeshGroup(self._mesh) - - def getShellMeshGroup(self): - if not self._shellGroup: - self._shellGroup = self.getOrCreateAnnotationGroup(("shell", "")) - return self._shellGroup.getMeshGroup(self._mesh) - def getLeftMeshGroup(self): if not self._leftGroup: self._leftGroup = self.getOrCreateAnnotationGroup(("left", "")) @@ -643,9 +631,7 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg def generateMesh(self, generateData): super(WholeBodyNetworkMeshBuilder, self).generateMesh(generateData) - # build core, shell, left, right annotation groups - coreMeshGroup = generateData.getCoreMeshGroup() if self._isCore else None - shellMeshGroup = generateData.getShellMeshGroup() if self._isCore else None + # build left, right, dorsal, ventral annotation groups leftMeshGroup = generateData.getLeftMeshGroup() rightMeshGroup = generateData.getRightMeshGroup() dorsalMeshGroup = generateData.getDorsalMeshGroup() @@ -653,9 +639,6 @@ def generateMesh(self, generateData): for networkSegment in self._networkMesh.getNetworkSegments(): # print("Segment", networkSegment.getNodeIdentifiers()) segment = self._segments[networkSegment] - if self._isCore: - segment.addCoreElementsToMeshGroup(coreMeshGroup) - segment.addShellElementsToMeshGroup(shellMeshGroup) annotationTerms = segment.getAnnotationTerms() for annotationTerm in annotationTerms: if "left" in annotationTerm[0]: diff --git a/src/scaffoldmaker/utils/eft_utils.py b/src/scaffoldmaker/utils/eft_utils.py index 3d57710d..6e7366e9 100644 --- a/src/scaffoldmaker/utils/eft_utils.py +++ b/src/scaffoldmaker/utils/eft_utils.py @@ -1,7 +1,7 @@ ''' Utility functions for element field templates shared by mesh generators. ''' -from cmlibs.maths.vectorops import add, cross, dot, magnitude, mult, normalize, sub +from cmlibs.maths.vectorops import add, cross, dot, magnitude, matrix_inv, mult, normalize, sub, transpose from cmlibs.zinc.element import Elementbasis, Elementfieldtemplate from cmlibs.zinc.node import Node from cmlibs.zinc.result import RESULT_OK @@ -549,16 +549,18 @@ def _determinePermutations(self): 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)) + # don't allow if another direction is within i j k octant by projecting onto basis + a = transpose([normDir[i], normDir[k], normDir[j]] if swizzle else + [normDir[i], normDir[j], normDir[k]]) + a_inv = matrix_inv(a) 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 + for n in range(3): + if dot(a_inv[n], normDir[m]) < 0.0: + break # outside octant + else: + break # another direction is within i j k octant else: ii, jj, kk = (i, k, j) if swizzle else (i, j, k) permutations.append((self._directions[ii], self._directions[jj], self._directions[kk])) @@ -664,16 +666,12 @@ def __init__(self): 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._nodeLayout6WayTriplePointTop1 = 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], [-1.0, -1.0, 0.0], - [1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]) - self._nodeLayout6WayTriplePointTop2 = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, -1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]) - self._nodeLayout6WayTriplePointBottom1 = 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], [-1.0, -1.0, 0.0], - [1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, -1.0]]) - self._nodeLayout6WayTriplePointBottom2 = HermiteNodeLayout( - [[-1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [-1.0, -1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, -1.0]]) + self._nodeLayoutBifurcationCoreTransitionTopGeneral = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, -1.0], [1.0, 0.0, 1.0], [-1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, -1.0, 1.0], [1.0, 1.0, 1.0], [-1.0, -1.0, 1.0]]) + self._nodeLayoutBifurcationCoreTransitionBottomGeneral = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, 1.0], [1.0, 0.0, -1.0], [-1.0, 0.0, -1.0], [0.0, 1.0, -1.0], [0.0, -1.0, -1.0], [1.0, 1.0, -1.0], [-1.0, -1.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( @@ -688,6 +686,7 @@ def __init__(self): [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 0.0, -1.0]]) self._nodeLayoutTriplePointBottomRight = HermiteNodeLayout( [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]]) + self.nodeLayoutsBifurcation6WayTriplePoint = {} def getNodeLayoutRegularPermuted(self, d3Defined, limitDirections=None): """ @@ -738,24 +737,107 @@ def getNodeLayoutTriplePoint(self): self._nodeLayoutTriplePointBottomLeft, self._nodeLayoutTriplePointBottomRight] return nodeLayouts - def getNodeLayout6WayBifurcation(self): + def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajorSegment, top): """ - Get node layout for a special case of 6-way bifurcation, used in conjunction with a node layout for - 6-way bifurcation transition and a node layout for 6-way triple point. - :return: HermiteNodeLayout. - """ - return self._nodeLayout6Way12_d3Defined - - def getNodeLayout6WayTriplePoint(self): + Get node layout for special case of 3-way junction on core transition corner of box. + :param segmentsIn: Directions of 3 segments relative to junction. + :param sequence: Sequence of bifurcation: [0, 1, 2] (normal) or [0, 2, 1] (reverse) + :param maxMajorSegment: Which segment has the most major elements, i.e. has elements going to both others. + :param top: True for top, False for bottom. + :return: HermiteNodeLayout """ - Get node layout for a special case where a node is located at both 6-way junction and one of the triple-point - corners of core box elements. Used in conjunction with 6-way bifurcation and 6-way bifurcation transition node - layouts. There are two corners (Top, and Bottom) each with its specific pair of node layouts. - :return: HermiteNodeLayout. - """ - nodeLayouts = [self._nodeLayout6WayTriplePointTop1, self._nodeLayout6WayTriplePointTop2, - self._nodeLayout6WayTriplePointBottom1, self._nodeLayout6WayTriplePointBottom2] - return nodeLayouts + reverse = sequence == [0, 2, 1] + sir = (segmentsIn[0], segmentsIn[1], segmentsIn[2], reverse) + layoutIndex = maxMajorSegment + if not top: + layoutIndex += 3 + nodeLayouts = self.nodeLayoutsBifurcation6WayTriplePoint.get(sir) + if not nodeLayouts: + if sir in [(False, False, False, False), (False, False, False, True), + (True, False, False, False), (True, False, False, True), + (True, True, True, False), (True, True, True, True)]: + nodeLayoutBifurcationCoreTransitionMOP = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, -1.0], [-1.0, 0.0, 1.0]]) + nodeLayoutBifurcationCoreTransitionOMP = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, -1.0], [0.0, -1.0, 1.0]]) + nodeLayoutBifurcationCoreTransitionPPP = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, -1.0], [1.0, 1.0, 1.0]]) + nodeLayoutBifurcationCoreTransitionMOM = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, 1.0], [-1.0, 0.0, -1.0]]) + nodeLayoutBifurcationCoreTransitionOMM = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, 1.0], [0.0, -1.0, -1.0]]) + nodeLayoutBifurcationCoreTransitionPPM = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, 1.0], [1.0, 1.0, -1.0]]) + nodeLayouts = ( + nodeLayoutBifurcationCoreTransitionPPP, + nodeLayoutBifurcationCoreTransitionMOP, + nodeLayoutBifurcationCoreTransitionOMP, + nodeLayoutBifurcationCoreTransitionPPM, + nodeLayoutBifurcationCoreTransitionMOM, + nodeLayoutBifurcationCoreTransitionOMM) + self.nodeLayoutsBifurcation6WayTriplePoint[(False, False, False, False)] = nodeLayouts + self.nodeLayoutsBifurcation6WayTriplePoint[(True, False, False, False)] = nodeLayouts + self.nodeLayoutsBifurcation6WayTriplePoint[(True, True, True, False)] = nodeLayouts + nodeLayoutsReverse = ( + nodeLayoutBifurcationCoreTransitionPPP, + nodeLayoutBifurcationCoreTransitionOMP, + nodeLayoutBifurcationCoreTransitionMOP, + nodeLayoutBifurcationCoreTransitionPPM, + nodeLayoutBifurcationCoreTransitionOMM, + nodeLayoutBifurcationCoreTransitionMOM) + self.nodeLayoutsBifurcation6WayTriplePoint[(False, False, False, True)] = nodeLayoutsReverse + self.nodeLayoutsBifurcation6WayTriplePoint[(True, False, False, True)] = nodeLayoutsReverse + self.nodeLayoutsBifurcation6WayTriplePoint[(True, True, True, True)] = nodeLayoutsReverse + nodeLayouts = self.nodeLayoutsBifurcation6WayTriplePoint.get(sir) + elif sir in [(True, True, False, False), (True, True, False, True)]: + nodeLayoutBifurcationCoreTransitionPOP = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, -1.0], [1.0, 0.0, 1.0]]) + nodeLayoutBifurcationCoreTransitionOPP = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, -1.0], [0.0, 1.0, 1.0]]) + nodeLayoutBifurcationCoreTransitionMMP = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], + [0.0, 0.0, -1.0], [-1.0, -1.0, 1.0]]) + nodeLayoutBifurcationCoreTransitionPOM = HermiteNodeLayout( + [[-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, 1.0], [1.0, 0.0, -1.0]]) + nodeLayoutBifurcationCoreTransitionOPM = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], [-1.0, -1.0, 0.0], + [0.0, 0.0, 1.0], [0.0, 1.0, -1.0]]) + nodeLayoutBifurcationCoreTransitionMMM = HermiteNodeLayout( + [[1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [1.0, 1.0, 0.0], + [0.0, 0.0, 11.0], [-1.0, -1.0, -1.0]]) + nodeLayouts = ( + nodeLayoutBifurcationCoreTransitionPOP, + nodeLayoutBifurcationCoreTransitionOPP, + nodeLayoutBifurcationCoreTransitionMMP, + nodeLayoutBifurcationCoreTransitionPOM, + nodeLayoutBifurcationCoreTransitionOPM, + nodeLayoutBifurcationCoreTransitionMMM) + self.nodeLayoutsBifurcation6WayTriplePoint[(True, True, False, False)] = nodeLayouts + nodeLayoutsReverse = ( + nodeLayoutBifurcationCoreTransitionOPP, + nodeLayoutBifurcationCoreTransitionPOP, + nodeLayoutBifurcationCoreTransitionMMP, + nodeLayoutBifurcationCoreTransitionOPM, + nodeLayoutBifurcationCoreTransitionPOM, + nodeLayoutBifurcationCoreTransitionMMM) + self.nodeLayoutsBifurcation6WayTriplePoint[(True, True, False, True)] = nodeLayoutsReverse + nodeLayouts = self.nodeLayoutsBifurcation6WayTriplePoint.get(sir) + if not nodeLayouts: + print("getNodeLayoutBifurcation6WayTriplePoint not implemented for case:", sir) + if top: + return self._nodeLayoutBifurcationCoreTransitionTopGeneral + else: + return self._nodeLayoutBifurcationCoreTransitionBottomGeneral + return nodeLayouts[layoutIndex] def determineCubicHermiteSerendipityEft(mesh, nodeParameters, nodeLayouts): """ @@ -903,7 +985,9 @@ def getTricubicHermiteSerendipityElementNodeParameter(eft, scalefactors, nodePar func = 4 * bn + valueIndex + 1 termCount = eft.getFunctionNumberOfTerms(func) for term in range(1, termCount + 1): - component = nodeParameters[eft.getTermLocalNodeIndex(func, term) - 1][valueIndex] + termValueLabel = eft.getTermNodeValueLabel(func, term) + termValueIndex = CubicHermiteSerendipityValueLabels.index(termValueLabel) + component = nodeParameters[eft.getTermLocalNodeIndex(func, term) - 1][termValueIndex] scalefactorCount, scalefactorIndexes = eft.getTermScaling(func, term, 0) if scalefactorCount > 0: scalefactorCount, scalefactorIndexes = eft.getTermScaling(func, term, scalefactorCount) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 219ddb99..1a04ed1a 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -65,13 +65,15 @@ def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurface 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]]]) - self._nodeLayout6WayTriplePoint = self._nodeLayoutManager.getNodeLayout6WayTriplePoint() - self._nodeLayoutBifurcation = self._nodeLayoutManager.getNodeLayout6WayBifurcation() self._nodeLayoutTrifurcation = None self._nodeLayoutTransition = self._nodeLayoutManager.getNodeLayoutRegularPermuted( d3Defined, limitDirections=[None, [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]], None]) self._nodeLayoutTransitionTriplePoint = None + # annotation groups are created if core: + self._coreGroup = None + self._shellGroup = None + def getStandardElementtemplate(self): return self._standardElementtemplate, self._standardEft @@ -90,35 +92,6 @@ def getNodeLayout8Way(self): def getNodeLayoutFlipD2(self): return self._nodeLayoutFlipD2 - def getNodeLayout6WayTriplePoint(self, location): - """ - Special node layout for generating core transition elements, where a node is located at both 6-way junction - and one of the triple point corners of core box elements. There are two layouts specific to top and bottom - corner: Top (location = 1); and bottom right (location = 2). - :param location: Location identifier. - :return: Node layout. - """ - nodeLayouts = self._nodeLayoutManager.getNodeLayout6WayTriplePoint() - location = abs(location) - # assert location in [1, 2] - - if location == 1: # "Top1" - nodeLayout = nodeLayouts[0] - elif location == 3: # "Top1" - nodeLayout = nodeLayouts[1] - elif location == 2: # "Bottom1" - nodeLayout = nodeLayouts[2] - elif location == 4: # "Bottom2" - nodeLayout = nodeLayouts[3] - - return nodeLayout - - def getNodeLayoutBifurcation(self): - """ - Special node layout for generating core elements for bifurcation. - """ - return self._nodeLayoutBifurcation - def getNodeLayoutTrifurcation(self, location): """ Special node layout for generating core elements for trifurcation. There are two layouts specific to @@ -164,6 +137,10 @@ def getNodeLayoutTransitionTriplePoint(self, location): self._nodeLayoutTransitionTriplePoint = nodeLayout return self._nodeLayoutTransitionTriplePoint + def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajorSegment, top): + return self._nodeLayoutManager.getNodeLayoutBifurcation6WayTriplePoint( + segmentsIn, sequence, maxMajorSegment, top) + def getNodetemplate(self): return self._nodetemplate @@ -173,6 +150,16 @@ def isLinearThroughWall(self): def isShowTrimSurfaces(self): return self._isShowTrimSurfaces + def getCoreMeshGroup(self): + if not self._coreGroup: + self._coreGroup = self.getOrCreateAnnotationGroup(("core", "")) + return self._coreGroup.getMeshGroup(self._mesh) + + def getShellMeshGroup(self): + if not self._shellGroup: + self._shellGroup = self.getOrCreateAnnotationGroup(("shell", "")) + return self._shellGroup.getMeshGroup(self._mesh) + def getNewTrimAnnotationGroup(self): self._trimAnnotationGroupCount += 1 return self.getOrCreateAnnotationGroup(("trim surface " + "{:03d}".format(self._trimAnnotationGroupCount), "")) @@ -2010,22 +1997,19 @@ def _sampleMidPoint(self, segmentsParameterLists): # 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) + # 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) + si = ((0, 1) if ((segmentsIn == [True, True, False]) or (segmentsIn == [False, False, True])) else + (2, 0) if ((segmentsIn == [True, False, True]) or (segmentsIn == [False, True, False])) else + (1, 2)) elif segmentsCount == 4: si = sequence[1:3] @@ -2068,16 +2052,20 @@ def _determineJunctionSequence(self): The function determines plus sequence [0, 1, 2, 3], [0, 1, 3, 2] or [0, 2, 1, 3]. """ assert self._segmentsCount == 3 or self._segmentsCount == 4 - + 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)) if self._segmentsCount == 3: - self._sequence = [0, 1, 2] + up = cross(outDirections[0], outDirections[1]) + d3 = self._segments[0].getPathParameters()[4][-1 if self._segmentsIn[s] else 0] + if dot(up, d3) < 0.0: + self._sequence = [0, 2, 1] # reverse sequence relative to d3 + else: + self._sequence = [0, 1, 2] elif self._segmentsCount == 4: # only support plus + shape for now, not tetrahedral # determine plus + sequence [0, 1, 2, 3], [0, 1, 3, 2], or [0, 2, 1, 3] - 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)) ns = None for s in range(1, self._segmentsCount): if dot(outDirections[0], outDirections[s]) > -0.9: @@ -2168,7 +2156,6 @@ def _sampleBifurcation(self, aroundCounts, coreBoxMajorCounts): boxIndexesCount = 0 if self._isCore: elementsCountTransition = self._segments[0].getElementsCountTransition() - sequence = self._sequence majorConnectionCounts = [(coreBoxMajorCounts[s] + coreBoxMajorCounts[s - 2] - coreBoxMajorCounts[s - 1]) // 2 for s in range(3)] midIndexes = [(coreBoxMajorCounts[s] - majorConnectionCounts[s]) if self._segmentsIn[s] @@ -2195,7 +2182,7 @@ def _sampleBifurcation(self, aroundCounts, coreBoxMajorCounts): [[[None for _ in range(coreBoxMinorNodesCount)] for _ in range(nodesCountAcrossMajorList[s])] for s in range(self._segmentsCount)] for s1 in range(3): - s2 = (s1 + 1) % 3 if sequence == [0, 1, 2] else (s1 - 1) % 3 + s2 = (s1 + 1) % 3 midIndex1 = midIndexes[s1] midIndex2 = midIndexes[s2] for m in range(majorConnectionCounts[s1] + 1): @@ -2681,7 +2668,6 @@ def _generateBoxElements(self, s, n2, mesh, elementtemplate, coordinates, segmen nodeLayout6Way = generateData.getNodeLayout6Way() nodeLayout8Way = generateData.getNodeLayout8Way() nodeLayoutFlipD2 = generateData.getNodeLayoutFlipD2() - nodeLayoutBifurcation = generateData.getNodeLayoutBifurcation() e2 = n2 if self._segmentsIn[s] else 0 for e3 in range(boxElementsCountAcrossMajor[s]): @@ -2702,7 +2688,7 @@ def _generateBoxElements(self, s, n2, mesh, elementtemplate, coordinates, segmen nodeParameters.append(self._getBoxCoordinates(boxIndex)) segmentNodesCount = len(self._boxIndexToSegmentNodeList[boxIndex]) if is6WayTriplePoint and (segmentNodesCount == 3) and self._segmentsCount == 3: - nodeLayouts.append(nodeLayoutBifurcation) + nodeLayouts.append(nodeLayout6Way) elif self._segmentsIn[s] and (segmentNodesCount == 3) and self._segmentsCount == 4: location = 1 if e3 < boxElementsCountAcrossMajor[s] // 2 else 2 nodeLayoutTrifurcation = generateData.getNodeLayoutTrifurcation(location) @@ -2744,18 +2730,19 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, coreBoxMajorCounts = [segment.getElementsCountCoreBoxMajor() for segment in self._segments] triplePointIndexesList = segment.getTriplePointIndexes() + triplePointIndexesList.sort() elementsCountTransition = self._segments[0].getElementsCountTransition() - is6WayTriplePoint = (self._segmentsCount == 3) and ((max(coreBoxMajorCounts) // 2) == min(coreBoxMajorCounts)) - pSegment = coreBoxMajorCounts.index(max(coreBoxMajorCounts)) - topMidIndex = (coreBoxMajorNodesCounts[pSegment] // 2) + (coreBoxMinorNodesCount // 2) - bottomMidIndex = elementsCountAround - topMidIndex - midIndexes = [topMidIndex, bottomMidIndex] + maxCoreBoxMajorCount = max(coreBoxMajorCounts) + maxMajorSegment = coreBoxMajorCounts.index(maxCoreBoxMajorCount) + # whether there are bifurcation core transition triple points on the corners of any box + is6WayTriplePoint = ((self._segmentsCount == 3) and + (maxCoreBoxMajorCount == (coreBoxMajorCounts[maxMajorSegment - 1] + + coreBoxMajorCounts[maxMajorSegment - 2]))) nodeLayout6Way = generateData.getNodeLayout6Way() nodeLayout8Way = generateData.getNodeLayout8Way() nodeLayoutFlipD2 = generateData.getNodeLayoutFlipD2() nodeLayoutTransition = generateData.getNodeLayoutTransition() - nodeLayoutBifurcation = generateData.getNodeLayoutBifurcation() e2 = n2 if self._segmentsIn[s] else 0 for e1 in range(elementsCountAround): @@ -2783,17 +2770,14 @@ def _generateTransitionElements(self, s, n2, mesh, elementtemplate, coordinates, nodeParameters.append(self._getBoxCoordinates(boxIndex)) segmentNodesCount = len(self._boxIndexToSegmentNodeList[boxIndex]) if segmentNodesCount == 3: # 6-way node - if is6WayTriplePoint: # Special 6-way triple point case - if (n1 in triplePointIndexesList or (s == pSegment and n1 in midIndexes)): - # 6-way AND triple-point node - only applies to bifurcations - location = (midIndexes.index(n1) + 1) if oLocation == 0 else oLocation - if (s == 1 and n1 == n1p) or (s == 2 and n1 == e1): - location = 3 if abs(location) == 1 else location - elif (s == 1 and n1 != n1p) or (s == 2 and n1 != e1): - location = 4 if abs(location) == 2 else location - nodeLayout = generateData.getNodeLayout6WayTriplePoint(location) + if is6WayTriplePoint: + top = triplePointIndexesList[0] <= n1 <= triplePointIndexesList[1] + bottom = triplePointIndexesList[2] <= n1 <= triplePointIndexesList[3] + if top or bottom: + nodeLayout = generateData.getNodeLayoutBifurcation6WayTriplePoint( + self._segmentsIn, self._sequence, maxMajorSegment, top) else: - nodeLayout = nodeLayoutBifurcation + nodeLayout = nodeLayout6Way elif self._segmentsCount == 4 and self._segmentsIn[s]: # Trifurcation case location = \ 1 if (e1 < elementsCountAround // 4) or (e1 >= 3 * elementsCountAround // 4) else 2 @@ -3127,6 +3111,17 @@ def createJunction(self, inSegments, outSegments): """ return TubeNetworkMeshJunction(inSegments, outSegments, self._useOuterTrimSurfaces) + def generateMesh(self, generateData): + super(TubeNetworkMeshBuilder, self).generateMesh(generateData) + # build core, shell + if self._isCore: + coreMeshGroup = generateData.getCoreMeshGroup() + shellMeshGroup = generateData.getShellMeshGroup() + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + segment.addCoreElementsToMeshGroup(coreMeshGroup) + segment.addShellElementsToMeshGroup(shellMeshGroup) + class TubeEllipseGenerator: """ diff --git a/tests/test_network.py b/tests/test_network.py index 3339e40e..6f9763c3 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -11,6 +11,7 @@ from cmlibs.zinc.field import Field from cmlibs.zinc.node import Node from cmlibs.zinc.result import RESULT_OK +from scaffoldmaker.annotation.annotationgroup import findAnnotationGroupByName from scaffoldmaker.meshtypes.meshtype_1d_network_layout1 import MeshType_1d_network_layout1 from scaffoldmaker.meshtypes.meshtype_2d_tubenetwork1 import MeshType_2d_tubenetwork1 from scaffoldmaker.meshtypes.meshtype_3d_boxnetwork1 import MeshType_3d_boxnetwork1 @@ -520,7 +521,9 @@ def test_3d_tube_network_converging_bifurcation_core(self): self.assertTrue(region.isValid()) scaffoldPackage.generate(region) annotationGroups = scaffoldPackage.getAnnotationGroups() - self.assertEqual(1, len(annotationGroups)) + self.assertEqual(3, len(annotationGroups)) + self.assertTrue(findAnnotationGroupByName(annotationGroups, "core") is not None) + self.assertTrue(findAnnotationGroupByName(annotationGroups, "shell") is not None) fieldmodule = region.getFieldmodule() mesh3d = fieldmodule.findMeshByDimension(3) @@ -533,8 +536,8 @@ def test_3d_tube_network_converging_bifurcation_core(self): self.assertTrue(coordinates.isValid()) # check annotation group transferred to 3D tube - annotationGroup = annotationGroups[0] - self.assertEqual("segment 3", annotationGroup.getName()) + annotationGroup = findAnnotationGroupByName(annotationGroups, "segment 3") + self.assertTrue(annotationGroup is not None) self.assertEqual("SEGMENT:3", annotationGroup.getId()) self.assertEqual(80, annotationGroup.getMeshGroup(fieldmodule.findMeshByDimension(3)).getSize()) @@ -723,6 +726,7 @@ def test_3d_tube_network_sphere_cube(self): def test_3d_tube_network_sphere_cube_core(self): """ Test sphere cube 3-D tube network with solid core is generated correctly. + Use different number of elements around on some segments to mix it up. """ scaffoldPackage = ScaffoldPackage(MeshType_3d_tubenetwork1, defaultParameterSetName="Sphere cube") settings = scaffoldPackage.getScaffoldSettings() @@ -742,6 +746,7 @@ def test_3d_tube_network_sphere_cube_core(self): self.assertEqual(1, settings["Number of elements across core transition"]) self.assertEqual([0], settings["Annotation numbers of elements across core box minor"]) settings["Number of elements through shell"] = 2 + settings["Annotation numbers of elements around"] = [12] settings["Core"] = True context = Context("Test") @@ -755,8 +760,8 @@ def test_3d_tube_network_sphere_cube_core(self): "To field": {"coordinates": False, "inner coordinates": True}, "From field": {"coordinates": True, "inner coordinates": False}, "Mode": {"Scale": True, "Offset": False}, - "D2 value": 0.8, - "D3 value": 0.8} + "D2 value": 0.75, + "D3 value": 0.75} editGroupName = "meshEdits" MeshType_1d_network_layout1.assignCoordinates(tmpRegion, networkLayoutSettings, networkMesh, functionOptions, editGroupName=editGroupName) @@ -770,24 +775,42 @@ def test_3d_tube_network_sphere_cube_core(self): self.assertEqual(RESULT_OK, result) networkLayoutScaffoldPackage.setMeshEdits(meshEditsString) + # add a user-defined annotation group to network layout to vary elements count around. Must generate first + annotationGroup1 = networkLayoutScaffoldPackage.createUserAnnotationGroup(("group1", "")) + group = annotationGroup1.getGroup() + tmpFieldmodule = tmpRegion.getFieldmodule() + mesh1d = tmpFieldmodule.findMeshByDimension(1) + meshGroup = group.createMeshGroup(mesh1d) + mesh_group_add_identifier_ranges(meshGroup, [[2, 2], [5, 5], [8, 8], [10, 10]]) + self.assertEqual(4, meshGroup.getSize()) + self.assertEqual(1, annotationGroup1.getDimension()) + identifier_ranges_string = identifier_ranges_to_string(mesh_group_to_identifier_ranges(meshGroup)) + self.assertEqual("2,5,8,10", identifier_ranges_string) + networkLayoutScaffoldPackage.updateUserAnnotationGroups() + self.assertTrue(region.isValid()) scaffoldPackage.generate(region) + annotationGroups = scaffoldPackage.getAnnotationGroups() + self.assertEqual(3, len(annotationGroups)) + self.assertTrue(findAnnotationGroupByName(annotationGroups, "core") is not None) + self.assertTrue(findAnnotationGroupByName(annotationGroups, "shell") is not None) + self.assertTrue(findAnnotationGroupByName(annotationGroups, "group1") is not None) fieldmodule = region.getFieldmodule() mesh3d = fieldmodule.findMeshByDimension(3) - self.assertEqual((8 * 3 + 4) * 4 * 12, mesh3d.getSize()) + self.assertEqual(1600, mesh3d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual((3 * 3 + 8 * 3) * 3 * 12 + (11 * 3 + 3 * 4) * 8, nodes.getSize()) + self.assertEqual(1836, nodes.getSize()) mesh2d = fieldmodule.findMeshByDimension(2) - self.assertEqual(4224, mesh2d.getSize()) + self.assertEqual(5024, 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.5665130262270113, -0.5965021612011158, -0.5986773876363235], X_TOL) - assertAlmostEqualList(self, maximums, [0.5665130262270113, 0.5965021612011159, 0.5986773876363234], X_TOL) + assertAlmostEqualList(self, minimums, [-0.5762017364104554, -0.5965021612011158, -0.5909194631968028], X_TOL) + assertAlmostEqualList(self, maximums, [0.5762017364104554, 0.5965021612011158, 0.5909194631968027], X_TOL) with ChangeManager(fieldmodule): one = fieldmodule.createFieldConstant(1.0) @@ -805,8 +828,24 @@ def test_3d_tube_network_sphere_cube_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.21259898629834004, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 4.04004649646839, delta=X_TOL) + self.assertAlmostEqual(volume, 0.20985014947157313, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 4.033518137808064, delta=X_TOL) + + expectedSizes3d = { + "core": (704, 0.12129037249755871), + "shell": (896, 0.08855977697401322) + } + for name in expectedSizes3d: + annotationGroup = findAnnotationGroupByName(annotationGroups, name) + size = annotationGroup.getMeshGroup(mesh3d).getSize() + self.assertEqual(expectedSizes3d[name][0], size, name) + volumeMeshGroup = annotationGroup.getMeshGroup(mesh3d) + volumeField = fieldmodule.createFieldMeshIntegral(one, coordinates, volumeMeshGroup) + volumeField.setNumbersOfPoints(4) + fieldcache = fieldmodule.createFieldcache() + result, volume = volumeField.evaluateReal(fieldcache, 1) + self.assertEqual(result, RESULT_OK) + self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=X_TOL) def test_3d_tube_network_trifurcation_cross(self): @@ -899,9 +938,9 @@ def test_3d_tube_network_trifurcation_cross(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.047184101028608746, delta=X_TOL) - self.assertAlmostEqual(outerSurfaceArea, 2.5994192161530587, delta=X_TOL) - self.assertAlmostEqual(innerSurfaceArea, 2.1144506806009487, delta=X_TOL) + self.assertAlmostEqual(volume, 0.047176020014987795, delta=X_TOL) + self.assertAlmostEqual(outerSurfaceArea, 2.5987990744712053, delta=X_TOL) + self.assertAlmostEqual(innerSurfaceArea, 2.113990124755675, delta=X_TOL) def test_3d_tube_network_trifurcation_cross_core(self): """ @@ -951,7 +990,7 @@ def test_3d_tube_network_trifurcation_cross_core(self): self.assertTrue(region.isValid()) scaffoldPackage.generate(region) annotationGroups = scaffoldPackage.getAnnotationGroups() - self.assertEqual(1, len(annotationGroups)) + self.assertEqual(3, len(annotationGroups)) fieldmodule = region.getFieldmodule() @@ -963,8 +1002,8 @@ def test_3d_tube_network_trifurcation_cross_core(self): self.assertTrue(coordinates.isValid()) # check annotation group transferred to 3D tube - annotationGroup = annotationGroups[0] - self.assertEqual("straight", annotationGroup.getName()) + annotationGroup = findAnnotationGroupByName(annotationGroups, "straight") + self.assertTrue(annotationGroup is not None) self.assertEqual("STRAIGHT:1", annotationGroup.getId()) self.assertEqual(256, annotationGroup.getMeshGroup(fieldmodule.findMeshByDimension(3)).getSize()) @@ -990,8 +1029,8 @@ def test_3d_tube_network_trifurcation_cross_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 0.1347351954323967, delta=X_TOL) - self.assertAlmostEqual(surfaceArea, 2.7245696029563424, delta=X_TOL) + self.assertAlmostEqual(volume, 0.1346381132294423, delta=X_TOL) + self.assertAlmostEqual(surfaceArea, 2.7234652881096166, delta=X_TOL) def test_3d_box_network_bifurcation(self): """ diff --git a/tests/test_wholebody2.py b/tests/test_wholebody2.py index 2da4483f..b44e0cbb 100644 --- a/tests/test_wholebody2.py +++ b/tests/test_wholebody2.py @@ -49,7 +49,7 @@ def test_wholebody2_core(self): region = context.getDefaultRegion() self.assertTrue(region.isValid()) annotationGroups = scaffold.generateMesh(region, options)[0] - self.assertEqual(32, len(annotationGroups)) # Needs updating as we add more annotation groups + self.assertEqual(32, len(annotationGroups)) fieldmodule = region.getFieldmodule() self.assertEqual(RESULT_OK, fieldmodule.defineAllFaces()) @@ -88,17 +88,17 @@ def test_wholebody2_core(self): result, surfaceArea = surfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 99.01720012090396, delta=tol) - self.assertAlmostEqual(surfaceArea, 229.87327935824305, delta=tol) + self.assertAlmostEqual(volume, 98.99087587225652, delta=tol) + self.assertAlmostEqual(surfaceArea, 229.8973868830034, delta=tol) # check some annotation groups: expectedSizes3d = { - "abdominal cavity": (40, 10.078325917475231), - "core": (428, 45.79157371575808), + "abdominal cavity": (40, 10.081327011840981), + "core": (428, 45.78886468970665), "head": (64, 6.909618374858558), - "thoracic cavity": (40, 7.135159387520382), - "shell": (276, 53.227959536902574) + "thoracic cavity": (40, 7.135491643165788), + "shell": (276, 53.20466827197639) } for name in expectedSizes3d: term = get_body_term(name) @@ -114,14 +114,14 @@ def test_wholebody2_core(self): self.assertAlmostEqual(volume, expectedSizes3d[name][1], delta=tol) expectedSizes2d = { - "abdominal cavity boundary": (64, 27.452684317517825), + "abdominal cavity boundary": (64, 27.46017763836879), "diaphragm": (20, 3.0778936559347208), - "left arm skin epidermis": (68, 22.605445370196083), + "left arm skin epidermis": (68, 22.627795339108015), "left leg skin epidermis": (68, 55.21582811667045), - "right arm skin epidermis": (68, 22.605445370196083), + "right arm skin epidermis": (68, 22.62779533911023), "right leg skin epidermis": (68, 55.21582811667045), - "skin epidermis": (388, 229.87327935824305), - "thoracic cavity boundary": (64, 20.600420538940487) + "skin epidermis": (388, 229.8973868830034), + "thoracic cavity boundary": (64, 20.606925296069125) } for name in expectedSizes2d: term = get_body_term(name) @@ -212,13 +212,13 @@ def test_wholebody2_tube(self): result, innerSurfaceArea = innerSurfaceAreaField.evaluateReal(fieldcache, 1) self.assertEqual(result, RESULT_OK) - self.assertAlmostEqual(volume, 53.22727912319599, delta=tol) - self.assertAlmostEqual(outerSurfaceArea, 225.76830801484903, delta=tol) - self.assertAlmostEqual(innerSurfaceArea, 155.86248051318998, delta=tol) + self.assertAlmostEqual(volume, 53.20377108353156, delta=tol) + self.assertAlmostEqual(outerSurfaceArea, 225.79241553960935, delta=tol) + self.assertAlmostEqual(innerSurfaceArea, 155.88335089354402, delta=tol) # check some annotationGroups: expectedSizes2d = { - "skin epidermis": (320, 229.01606374214393) + "skin epidermis": (320, 229.04017126690428) } for name in expectedSizes2d: term = get_body_term(name) From 1c2681c21206b77341b671db7a3495df5eca4db1 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Mon, 21 Oct 2024 14:51:42 +1300 Subject: [PATCH 17/20] Review fixes --- src/scaffoldmaker/utils/tubenetworkmesh.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 1a04ed1a..89df3cb2 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -1263,10 +1263,7 @@ def setBoxElementId(self, e1, e2, e3, elementIdentifier): :param e3: Element index across core box minor / d3 direction. :param elementIdentifier: Element identifier. """ - self._elementsCountCoreBoxMajor - self._elementsCountCoreBoxMinor if not self._boxElementIds[e2]: - elementsCountRim = self.getElementsCountRim() self._boxElementIds[e2] = [ [None] * self._elementsCountCoreBoxMinor for _ in range(self._elementsCountCoreBoxMajor)] self._boxElementIds[e2][e3][e1] = elementIdentifier @@ -1319,6 +1316,10 @@ def _addRimElementsToMeshGroup(self, e1Start, e1Limit, e3Start, e3Limit, meshGro meshGroup.addElement(element) def addCoreElementsToMeshGroup(self, meshGroup): + """ + Ensure all core elements in core box or rim arrays are in mesh group. + :param meshGroup: Zinc MeshGroup to add elements to. + """ if not self._isCore: return self._addBoxElementsToMeshGroup(0, self._elementsCountCoreBoxMajor, @@ -1388,6 +1389,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): """ :param n2Only: If set, create nodes only for that single n2 index along. Must be >= 0! """ + # keeping this code to enable display of raw segment trim surfaces for future diagnostics # if (not n2Only) and generateData.isShowTrimSurfaces(): # dimension = generateData.getMeshDimension() # nodeIdentifier, elementIdentifier = generateData.getNodeElementIdentifiers() From 87738d219d1f37da59f4c98f8883a223be7f4b0c Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Mon, 21 Oct 2024 15:06:57 +1300 Subject: [PATCH 18/20] Fix annotation around counts check --- src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py index e9b26042..0aede4e2 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py @@ -112,7 +112,7 @@ def checkOptions(cls, options): if options["Number of elements around"] < 8: options["Number of elements around"] = 8 elif options["Number of elements around"] % 4: - options["Number of elements around"] += 4 - options["Number of elements around"] % 4 + options["Number of elements around"] += 4 - (options["Number of elements around"] % 4) annotationAroundCounts = options["Annotation numbers of elements around"] minAroundCount = options["Number of elements around"] @@ -126,7 +126,7 @@ def checkOptions(cls, options): if annotationAroundCounts[i] < 8: annotationAroundCounts[i] = 8 elif annotationAroundCounts[i] % 4: - annotationAroundCounts[i] += 4 - annotationAroundCounts[i] + annotationAroundCounts[i] += 4 - (annotationAroundCounts[i] % 4) if annotationAroundCounts[i] < minAroundCount: minAroundCount = annotationAroundCounts[i] From cbbc0efbbca05c0b4a0411486349403b350fc8b6 Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Tue, 22 Oct 2024 12:22:07 +1300 Subject: [PATCH 19/20] Improve network mesh generator argument order --- src/scaffoldmaker/annotation/uterus_terms.py | 15 +- .../meshtypes/meshtype_2d_tubenetwork1.py | 10 +- .../meshtypes/meshtype_3d_tubenetwork1.py | 28 +--- .../meshtypes/meshtype_3d_uterus2.py | 57 +++---- .../meshtypes/meshtype_3d_wholebody2.py | 110 ++---------- src/scaffoldmaker/utils/boxnetworkmesh.py | 3 +- src/scaffoldmaker/utils/networkmesh.py | 11 ++ src/scaffoldmaker/utils/tubenetworkmesh.py | 158 +++++++++++++----- tests/test_network.py | 15 +- tests/test_uterus.py | 4 +- 10 files changed, 190 insertions(+), 221 deletions(-) diff --git a/src/scaffoldmaker/annotation/uterus_terms.py b/src/scaffoldmaker/annotation/uterus_terms.py index 16a7dc48..0060690e 100644 --- a/src/scaffoldmaker/annotation/uterus_terms.py +++ b/src/scaffoldmaker/annotation/uterus_terms.py @@ -9,6 +9,7 @@ ("dorsal cervix junction with vagina", "None"), ("dorsal top left horn", "None"), ("dorsal top right horn", "None"), + ("dorsal uterus", "None"), ("external cervical os", "UBERON:0013760", "FMA:76836", "ILX:0736534"), ("fundus of uterus", "None"), ("internal cervical os", "UBERON:0013759", "FMA:17747", "ILX:0729495"), @@ -18,6 +19,7 @@ ("left transverse cervical ligament", "None"), ("left uterine horn", "UBERON:0009020"), ("left uterine tube", "UBERON:0001303", "FMA:18484", "ILX:0734218"), + ("left uterus", "None"), ("lumen of body of uterus", "None"), ("lumen of fallopian tube", "None"), ("lumen of left horn", "None"), @@ -33,6 +35,7 @@ ("right transverse cervical ligament", "None"), ("right uterine horn", "UBERON:0009022"), ("right uterine tube", "UBERON:0001302", "FMA:18483", "ILX:0724908"), + ("right uterus", "None"), ("serosa of body of uterus", "None"), ("serosa of left uterine tube", "None"), ("serosa of left horn", "None"), @@ -41,7 +44,7 @@ ("serosa of uterine cervix", "None"), ("serosa of uterus", "UBERON:0001297"), ("serosa of vagina", "None"), - ("uterine cervix", "UBERON:0000002","FMA:17740", "ILX:0724162"), + ("uterine cervix", "UBERON:0000002", "FMA:17740", "ILX:0724162"), ("uterine horn", "UBERON:000224"), ("uterine lumen", "UBERON:0013769"), ("uterine wall", "UBERON:0000459", "FMA:17560", "ILX:0735839"), @@ -50,15 +53,17 @@ ("vagina orifice", "UBERON:0012317", "FMA:19984", "ILX:0729556"), ("ventral cervix junction with vagina", "None"), ("ventral top left horn", "None"), - ("ventral top right horn", "None")] + ("ventral top right horn", "None"), + ("ventral uterus", "None")] + def get_uterus_term(name: str): """ Find term by matching name to any identifier held for a term. Raise exception if name not found. - :return ( preferred name, preferred id ) + :return: ( preferred name, preferred id ) """ for term in uterus_terms: if name in term: - return (term[0], term[1]) - raise NameError("Uterus annotation term '" + name + "' not found.") \ No newline at end of file + return term[0], term[1] + raise NameError("Uterus annotation term '" + name + "' not found.") diff --git a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py index ca175c2a..48b89cc7 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py @@ -34,8 +34,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Target element density along longest segment"] = 12.0 return options - @staticmethod - def getOrderedOptionNames(): + @classmethod + def getOrderedOptionNames(cls): return [ "Network layout", "Number of elements around", @@ -119,11 +119,11 @@ def generateBaseMesh(cls, region, options): tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( networkMesh, targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], - defaultElementsCountAround=options["Number of elements around"], - elementsCountThroughWall=1, layoutAnnotationGroups=networkLayout.getAnnotationGroups(), annotationElementsCountsAlong=options["Annotation numbers of elements along"], - annotationElementsCountsAround=options["Annotation numbers of elements around"]) + defaultElementsCountAround=options["Number of elements around"], + annotationElementsCountsAround=options["Annotation numbers of elements around"], + elementsCountThroughShell=1) tubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( region, 2, diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py index 0aede4e2..c69e4184 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py @@ -53,8 +53,8 @@ def getDefaultOptions(cls, parameterSetName="Default"): options["Target element density along longest segment"] = 12.0 return options - @staticmethod - def getOrderedOptionNames(): + @classmethod + def getOrderedOptionNames(cls): return [ "Network layout", "Number of elements around", @@ -212,31 +212,21 @@ def generateBaseMesh(cls, region, options): layoutRegion = region.createRegion() networkLayout = options["Network layout"] networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters - layoutAnnotationGroups = networkLayout.getAnnotationGroups() networkMesh = networkLayout.getConstructionObject() - defaultAroundCount = options["Number of elements around"] - coreTransitionCount = options["Number of elements across core transition"] - defaultCoreBoxMinorCount = options["Number of elements across core box minor"] - # implementation currently uses major count including transition - defaultCoreMajorCount = defaultAroundCount // 2 - defaultCoreBoxMinorCount + 2 * coreTransitionCount - annotationAroundCounts = options["Annotation numbers of elements around"] - annotationCoreBoxMinorCounts = options["Annotation numbers of elements across core box minor"] - tubeNetworkMeshBuilder = TubeNetworkMeshBuilder( networkMesh, targetElementDensityAlongLongestSegment=options["Target element density along longest segment"], - defaultElementsCountAround=defaultAroundCount, - elementsCountThroughWall=options["Number of elements through shell"], - layoutAnnotationGroups=layoutAnnotationGroups, + layoutAnnotationGroups=networkLayout.getAnnotationGroups(), annotationElementsCountsAlong=options["Annotation numbers of elements along"], - annotationElementsCountsAround=annotationAroundCounts, - defaultElementsCountCoreBoxMinor=defaultCoreBoxMinorCount, - elementsCountTransition=coreTransitionCount, - annotationElementsCountsCoreBoxMinor=annotationCoreBoxMinorCounts, + defaultElementsCountAround=options["Number of elements around"], + annotationElementsCountsAround=options["Annotation numbers of elements around"], + elementsCountThroughShell=options["Number of elements through shell"], isCore=options["Core"], + elementsCountTransition=options["Number of elements across core transition"], + defaultElementsCountCoreBoxMinor=options["Number of elements across core box minor"], + annotationElementsCountsCoreBoxMinor=options["Annotation numbers of elements across core box minor"], useOuterTrimSurfaces=options["Use outer trim surfaces"]) - tubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( region, 3, diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py index b1c18601..13f712e9 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py @@ -18,46 +18,30 @@ class UterusTubeNetworkMeshGenerateData(TubeNetworkMeshGenerateData): - def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurfaces, - coordinateFieldName="coordinates", startNodeIdentifier=1, startElementIdentifier=1): + def __init__(self, region, meshDimension, coordinateFieldName="coordinates", + startNodeIdentifier=1, startElementIdentifier=1, isLinearThroughWall=False, isShowTrimSurfaces=False): """ :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) + region, meshDimension, coordinateFieldName, startNodeIdentifier, startElementIdentifier, + isLinearThroughWall, isShowTrimSurfaces) 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")) + # force these annotation group names in base class + self._leftGroup = self.getOrCreateAnnotationGroup(get_uterus_term("left uterus")) + self._rightGroup = self.getOrCreateAnnotationGroup(get_uterus_term("right uterus")) + self._dorsalGroup = self.getOrCreateAnnotationGroup(get_uterus_term("dorsal uterus")) + self._ventralGroup = self.getOrCreateAnnotationGroup(get_uterus_term("ventral uterus")) 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 = [], annotationElementsCountsAlong: list = [], - annotationElementsCountsAround: list = [], useOuterTrimSurfaces=True): - super(UterusTubeNetworkMeshBuilder, self).__init__( - networkMesh, targetElementDensityAlongLongestSegment, defaultElementsCountAround, - elementsCountThroughWall, layoutAnnotationGroups, - annotationElementsCountsAlong, annotationElementsCountsAround, useOuterTrimSurfaces=useOuterTrimSurfaces) + """ + Adds left, right, dorsal, ventral, fundus annotations. + Future: derive from BodyTubeNetworkMeshBuilder to get left/right/dorsal/ventral. + """ def generateMesh(self, generateData): super(UterusTubeNetworkMeshBuilder, self).generateMesh(generateData) @@ -490,11 +474,12 @@ def generateBaseMesh(cls, region, options): 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, annotationElementsCountsAlong=annotationAlongCounts, - annotationElementsCountsAround=annotationElementsCountsAround) + defaultElementsCountAround=options['Number of elements around'], + annotationElementsCountsAround=annotationElementsCountsAround, + elementsCountThroughShell=options["Number of elements through wall"], + useOuterTrimSurfaces=True) uterusTubeNetworkMeshBuilder.build() generateData = UterusTubeNetworkMeshGenerateData( region, 3, @@ -633,10 +618,10 @@ 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")) + leftGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("left uterus")) + rightGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("right uterus")) + dorsalGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("dorsal uterus")) + ventralGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("ventral uterus")) if isHuman: leftUterineTubeGroup = getAnnotationGroupForTerm(annotationGroups, get_uterus_term("left uterine tube")) diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index d05e75ed..ae7f5f5f 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -15,9 +15,10 @@ computeCubicHermiteEndDerivative, getCubicHermiteArcLength, interpolateLagrangeHermiteDerivative, sampleCubicHermiteCurvesSmooth, smoothCubicHermiteDerivativesLine) from scaffoldmaker.utils.networkmesh import NetworkMesh -from scaffoldmaker.utils.tubenetworkmesh import TubeNetworkMeshBuilder, TubeNetworkMeshGenerateData +from scaffoldmaker.utils.tubenetworkmesh import BodyTubeNetworkMeshBuilder, TubeNetworkMeshGenerateData import math + class MeshType_1d_human_body_network_layout1(MeshType_1d_network_layout1): """ Defines body network layout. @@ -169,7 +170,6 @@ def checkOptions(cls, options): options[key] = 60.0 return dependentChanges - @classmethod def generateBaseMesh(cls, region, options): """ @@ -292,7 +292,7 @@ def generateBaseMesh(cls, region, options): footElementsCount = 2 legMeshGroup = legGroup.getMeshGroup(mesh) legToFootMeshGroup = legToFootGroup.getMeshGroup(mesh) - footMeshGroup = footGroup.getMeshGroup(mesh) + footMeshGroup = footGroup.getMeshGroup(mesh) for side in (left, right): sideLegGroup = leftLegGroup if (side == left) else rightLegGroup meshGroups = [bodyMeshGroup, legMeshGroup, legToFootMeshGroup, sideLegGroup.getMeshGroup(mesh)] @@ -379,7 +379,6 @@ def generateBaseMesh(cls, region, options): id2 = mult(d2, innerProportionDefault) id3 = mult(d3, innerProportionDefault) abdomenStartX = thoraxStartX + thoraxLength - px = None for i in range(abdomenElementsCount + 1): node = nodes.findNodeByIdentifier(nodeIdentifier) fieldcache.setNode(node) @@ -487,7 +486,6 @@ def generateBaseMesh(cls, region, options): nodeIdentifier += 1 # legs - cos45 = math.cos(0.25 * math.pi) legStartX = abdomenStartX + abdomenLength + pelvisDrop nonFootLegLength = legLength - footHeight legScale = nonFootLegLength / (legToFootElementsCount - 1) @@ -578,83 +576,6 @@ def getInteractiveFunctions(cls): return interactiveFunctions -class WholeBodyTubeNetworkMeshGenerateData(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(WholeBodyTubeNetworkMeshGenerateData, self).__init__( - region, meshDimension, isLinearThroughWall, isShowTrimSurfaces, - coordinateFieldName, startNodeIdentifier, startElementIdentifier) - # annotation groups are created on demand: - self._leftGroup = None - self._rightGroup = None - self._dorsalGroup = None - self._ventralGroup = None - - def getLeftMeshGroup(self): - if not self._leftGroup: - self._leftGroup = self.getOrCreateAnnotationGroup(("left", "")) - return self._leftGroup.getMeshGroup(self._mesh) - - def getRightMeshGroup(self): - if not self._rightGroup: - self._rightGroup = self.getOrCreateAnnotationGroup(("right", "")) - return self._rightGroup.getMeshGroup(self._mesh) - - def getDorsalMeshGroup(self): - if not self._dorsalGroup: - self._dorsalGroup = self.getOrCreateAnnotationGroup(("dorsal", "")) - return self._dorsalGroup.getMeshGroup(self._mesh) - - def getVentralMeshGroup(self): - if not self._ventralGroup: - self._ventralGroup = self.getOrCreateAnnotationGroup(("ventral", "")) - return self._ventralGroup.getMeshGroup(self._mesh) - - -class WholeBodyNetworkMeshBuilder(TubeNetworkMeshBuilder): - - def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, - defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], - annotationElementsCountsAlong: list = [], annotationElementsCountsAround: list = [], - defaultElementsCountCoreBoxMinor: int = 2, elementsCountTransition: int = 1, - annotationElementsCountsCoreBoxMinor: list = [], isCore=False, useOuterTrimSurfaces=True): - super(WholeBodyNetworkMeshBuilder, self).__init__( - networkMesh, targetElementDensityAlongLongestSegment, defaultElementsCountAround, - elementsCountThroughWall, layoutAnnotationGroups, - annotationElementsCountsAlong, annotationElementsCountsAround, defaultElementsCountCoreBoxMinor, - elementsCountTransition, annotationElementsCountsCoreBoxMinor, isCore, useOuterTrimSurfaces) - - def generateMesh(self, generateData): - super(WholeBodyNetworkMeshBuilder, self).generateMesh(generateData) - # build left, right, dorsal, ventral annotation groups - leftMeshGroup = generateData.getLeftMeshGroup() - rightMeshGroup = generateData.getRightMeshGroup() - dorsalMeshGroup = generateData.getDorsalMeshGroup() - ventralMeshGroup = generateData.getVentralMeshGroup() - for networkSegment in self._networkMesh.getNetworkSegments(): - # print("Segment", networkSegment.getNodeIdentifiers()) - segment = self._segments[networkSegment] - annotationTerms = segment.getAnnotationTerms() - for annotationTerm in annotationTerms: - if "left" in annotationTerm[0]: - segment.addAllElementsToMeshGroup(leftMeshGroup) - break - if "right" in annotationTerm[0]: - segment.addAllElementsToMeshGroup(rightMeshGroup) - break - else: - # segment on main axis - segment.addSideD2ElementsToMeshGroup(False, leftMeshGroup) - segment.addSideD2ElementsToMeshGroup(True, rightMeshGroup) - segment.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) - segment.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) - - class MeshType_3d_wholebody2(Scaffold_base): """ Generates a 3-D hermite bifurcating tube network with core representing the human body. @@ -774,7 +695,8 @@ def getOptionScaffoldPackage(cls, optionName, scaffoldType, parameterSetName=Non @classmethod def checkOptions(cls, options): dependentChanges = False - if not options["Body network layout"].getScaffoldType() in cls.getOptionValidScaffoldTypes("Body network layout"): + if (options["Body network layout"].getScaffoldType() not in + cls.getOptionValidScaffoldTypes("Body network layout")): options["Body network layout"] = ScaffoldPackage(MeshType_1d_human_body_network_layout1) for key in [ "Number of elements along head", @@ -785,7 +707,7 @@ def checkOptions(cls, options): "Number of elements along hand", "Number of elements along leg to foot", "Number of elements along foot" - ]: + ]: if options[key] < 1: options[key] = 1 minElementsCountAround = None @@ -844,8 +766,7 @@ def generateBaseMesh(cls, region, options): elementsCountAroundTorso = options["Number of elements around torso"] elementsCountAroundArm = options["Number of elements around arm"] elementsCountAroundLeg = options["Number of elements around leg"] - coreBoxMinorCount = options["Number of elements across core box minor"] - coreTransitionCount = options['Number of elements across core transition'] + isCore = options["Use Core"] layoutRegion = region.createRegion() networkLayout.generate(layoutRegion) # ask scaffold to generate to get user-edited parameters @@ -854,7 +775,6 @@ def generateBaseMesh(cls, region, options): annotationAlongCounts = [] annotationAroundCounts = [] - # implementation currently uses major count including transition for layoutAnnotationGroup in layoutAnnotationGroups: alongCount = 0 aroundCount = 0 @@ -885,24 +805,24 @@ def generateBaseMesh(cls, region, options): aroundCount = elementsCountAroundLeg annotationAlongCounts.append(alongCount) annotationAroundCounts.append(aroundCount) - isCore = options["Use Core"] - tubeNetworkMeshBuilder = WholeBodyNetworkMeshBuilder( + tubeNetworkMeshBuilder = BodyTubeNetworkMeshBuilder( networkMesh, targetElementDensityAlongLongestSegment=2.0, # not used for body - defaultElementsCountAround=options["Number of elements around head"], - elementsCountThroughWall=options["Number of elements through shell"], layoutAnnotationGroups=layoutAnnotationGroups, annotationElementsCountsAlong=annotationAlongCounts, + defaultElementsCountAround=options["Number of elements around head"], annotationElementsCountsAround=annotationAroundCounts, - defaultElementsCountCoreBoxMinor=coreBoxMinorCount, - elementsCountTransition=coreTransitionCount, + elementsCountThroughShell=options["Number of elements through shell"], + isCore=isCore, + elementsCountTransition=options['Number of elements across core transition'], + defaultElementsCountCoreBoxMinor=options["Number of elements across core box minor"], annotationElementsCountsCoreBoxMinor=[], - isCore=isCore) + useOuterTrimSurfaces=True) meshDimension = 3 tubeNetworkMeshBuilder.build() - generateData = WholeBodyTubeNetworkMeshGenerateData( + generateData = TubeNetworkMeshGenerateData( region, meshDimension, isLinearThroughWall=False, isShowTrimSurfaces=options["Show trim surfaces"]) diff --git a/src/scaffoldmaker/utils/boxnetworkmesh.py b/src/scaffoldmaker/utils/boxnetworkmesh.py index 6c477ffd..10cc9cb1 100644 --- a/src/scaffoldmaker/utils/boxnetworkmesh.py +++ b/src/scaffoldmaker/utils/boxnetworkmesh.py @@ -186,7 +186,7 @@ def getNodeIdentifier(self): def setNodeIdentifier(self, nodeIdentifier): """ - Store the node identifier so it can be reused by adjacent segments. + Store the node identifier to reference from adjacent segments. :param nodeIdentifier: Identifier of generated node at junction. """ self._nodeIdentifier = nodeIdentifier @@ -222,7 +222,6 @@ def sample(self, targetElementLength): for segment, nodeIndex in segmentIndexes: segment.setSampledD1(nodeIndex, d1Mean) - def generateMesh(self, generateData: BoxNetworkMeshGenerateData): pass # nothing to do for box network diff --git a/src/scaffoldmaker/utils/networkmesh.py b/src/scaffoldmaker/utils/networkmesh.py index 58adc7a1..3d4f4f10 100644 --- a/src/scaffoldmaker/utils/networkmesh.py +++ b/src/scaffoldmaker/utils/networkmesh.py @@ -714,6 +714,17 @@ class NetworkMeshBuilder(ABC): def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, layoutAnnotationGroups, annotationElementsCountsAlong=[]): + """ + Abstract base class for building meshes from a NetworkMesh network layout. + :param networkMesh: Description of the topology of the network layout. + :param targetElementDensityAlongLongestSegment: Real value which longest segment path in network is divided by + to get target element length, which is used to determine numbers of elements along except when set for a segment + through annotationElementsCountsAlong. + :param layoutAnnotationGroups: Annotation groups defined on the layout to mirror on the final mesh. + :param annotationElementsCountsAlong: List in same order as layoutAnnotationGroups, specifying fixed number of + elements along segment with any elements in the annotation group. Client must ensure exclusive map from + segments. Groups with zero value or past end of this list use the targetElementDensityAlongLongestSegment. + """ self._networkMesh = networkMesh self._targetElementDensityAlongLongestSegment = targetElementDensityAlongLongestSegment self._layoutAnnotationGroups = layoutAnnotationGroups diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index 89df3cb2..ac58b047 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -27,8 +27,8 @@ class TubeNetworkMeshGenerateData(NetworkMeshGenerateData): Data for passing to TubeNetworkMesh generateMesh functions. """ - def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurfaces, - coordinateFieldName="coordinates", startNodeIdentifier=1, startElementIdentifier=1): + def __init__(self, region, meshDimension, coordinateFieldName="coordinates", + startNodeIdentifier=1, startElementIdentifier=1, isLinearThroughWall=False, isShowTrimSurfaces=False): """ :param isLinearThroughWall: Callers should only set if 3-D with no core. :param isShowTrimSurfaces: Tells junction generateMesh to make 2-D trim surfaces. @@ -70,9 +70,14 @@ def __init__(self, region, meshDimension, isLinearThroughWall, isShowTrimSurface d3Defined, limitDirections=[None, [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0]], None]) self._nodeLayoutTransitionTriplePoint = None - # annotation groups are created if core: + # annotation groups created if core: self._coreGroup = None self._shellGroup = None + # annotation groups created on demand: + self._leftGroup = None + self._rightGroup = None + self._dorsalGroup = None + self._ventralGroup = None def getStandardElementtemplate(self): return self._standardElementtemplate, self._standardEft @@ -160,6 +165,26 @@ def getShellMeshGroup(self): self._shellGroup = self.getOrCreateAnnotationGroup(("shell", "")) return self._shellGroup.getMeshGroup(self._mesh) + def getLeftMeshGroup(self): + if not self._leftGroup: + self._leftGroup = self.getOrCreateAnnotationGroup(("left", "")) + return self._leftGroup.getMeshGroup(self._mesh) + + def getRightMeshGroup(self): + if not self._rightGroup: + self._rightGroup = self.getOrCreateAnnotationGroup(("right", "")) + return self._rightGroup.getMeshGroup(self._mesh) + + def getDorsalMeshGroup(self): + if not self._dorsalGroup: + self._dorsalGroup = self.getOrCreateAnnotationGroup(("dorsal", "")) + return self._dorsalGroup.getMeshGroup(self._mesh) + + def getVentralMeshGroup(self): + if not self._ventralGroup: + self._ventralGroup = self.getOrCreateAnnotationGroup(("ventral", "")) + return self._ventralGroup.getMeshGroup(self._mesh) + def getNewTrimAnnotationGroup(self): self._trimAnnotationGroupCount += 1 return self.getOrCreateAnnotationGroup(("trim surface " + "{:03d}".format(self._trimAnnotationGroupCount), "")) @@ -167,13 +192,13 @@ def getNewTrimAnnotationGroup(self): class TubeNetworkMeshSegment(NetworkMeshSegment): - def __init__(self, networkSegment, pathParametersList, elementsCountAround, elementsCountThroughWall, + def __init__(self, networkSegment, pathParametersList, elementsCountAround, elementsCountThroughShell, isCore=False, elementsCountCoreBoxMinor: int = 2, elementsCountTransition: int = 1): """ :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. + :param elementsCountThroughShell: Number of elements between inner and outer tube if 3-D, 1 if 2-D. :param isCore: True for generating a solid core inside the tube, False for regular tube network. :param elementsCountCoreBoxMinor: Number of elements across core box minor axis. :param elementsCountTransition: Number of elements across transition zone between core box elements and @@ -186,8 +211,8 @@ def __init__(self, networkSegment, pathParametersList, elementsCountAround, elem self._elementsCountCoreBoxMinor = elementsCountCoreBoxMinor self._elementsCountTransition = elementsCountTransition - assert elementsCountThroughWall > 0 - self._elementsCountThroughWall = elementsCountThroughWall + assert elementsCountThroughShell > 0 + self._elementsCountThroughShell = elementsCountThroughShell self._rawTubeCoordinatesList = [] self._rawTrackSurfaceList = [] for pathParameters in pathParametersList: @@ -403,7 +428,7 @@ def sample(self, fixedElementsCountAlong, targetElementLength): [[ring] for ring in self._sampledTubeCoordinates[0][2]], None) else: - wallFactor = 1.0 / self._elementsCountThroughWall + wallFactor = 1.0 / self._elementsCountThroughShell ox, od1, od2 = self._sampledTubeCoordinates[0][0:3] ix, id1, id2 = self._sampledTubeCoordinates[1][0:3] rx, rd1, rd2, rd3 = [], [], [], [] @@ -415,15 +440,15 @@ def sample(self, fixedElementsCountAlong, targetElementLength): itx, itd1, itd2 = ix[n2], id1[n2], id2[n2] # wx, wd3 = self._determineWallCoordinates(otx, otd1, otd2, itx, itd1, itd2, coreCentre, arcCentre) 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 + for n3 in range(self._elementsCountThroughShell + 1): + oFactor = n3 / self._elementsCountThroughShell 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: + elif n3 == self._elementsCountThroughShell: x, d1, d2 = otx[n1], otd1[n1], otd2[n1] else: x = add(mult(itx[n1], iFactor), mult(otx[n1], oFactor)) @@ -883,22 +908,22 @@ def _determineWallCoordinates(self, ox, od1, od2, ix, id1, id2, coreCentre, arcC it = cross(ic, id1[n1]) else: ot, it = cross(od1[n1], od2[n1]), cross(id1[n1], id2[n1]) - scalefactor = magnitude(sub(ox[n1], ix[n1])) / self._elementsCountThroughWall + scalefactor = magnitude(sub(ox[n1], ix[n1])) / self._elementsCountThroughShell od3 = mult(normalize(ot), scalefactor) id3 = mult(normalize(it), scalefactor) else: - wallFactor = 1.0 / self._elementsCountThroughWall + wallFactor = 1.0 / self._elementsCountThroughShell od3 = id3 = mult(sub(ox[n1], ix[n1]), wallFactor) txm, td3m, pe, pxi, psf = sampleCubicHermiteCurves( - [ix[n1], ox[n1]], [id3, od3], self._elementsCountThroughWall, arcLengthDerivatives=True) + [ix[n1], ox[n1]], [id3, od3], self._elementsCountThroughShell, arcLengthDerivatives=True) td3m = smoothCubicHermiteDerivativesLine(txm, td3m, fixStartDirection=True, fixEndDirection=True) tx.append(txm) td3.append(td3m) - for n3 in range(self._elementsCountThroughWall + 1): + for n3 in range(self._elementsCountThroughShell + 1): wx.append([]) wd3.append([]) for n1 in range(self._elementsCountAround): @@ -1333,7 +1358,7 @@ def addShellElementsToMeshGroup(self, meshGroup): :param meshGroup: Zinc MeshGroup to add elements to. """ elementsCountRim = self.getElementsCountRim() - elementsCountShell = self._elementsCountThroughWall + elementsCountShell = self._elementsCountThroughShell e3ShellStart = elementsCountRim - elementsCountShell self._addRimElementsToMeshGroup(0, self._elementsCountAround, e3ShellStart, elementsCountRim, meshGroup) @@ -2975,6 +3000,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): nids = [] nodeParameters = [] nodeLayouts = [] + elementIdentifier = generateData.nextElementIdentifier() lastTransition = self._isCore and (elementsCountTransition == (rim_e3 + 1)) needParameters = (e3 == 0) or lastTransition for n3 in [e3, e3 + 1] if (meshDimension == 3) else [e3]: @@ -3016,7 +3042,6 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): eft, scalefactors, nodeParameters, [5, 6, 7, 8], Node.VALUE_LABEL_D_DS3) elementtemplate.defineField(coordinates, -1, eft) - elementIdentifier = generateData.nextElementIdentifier() element = mesh.createElement(elementIdentifier, elementtemplate) element.setNodesByIdentifier(eft, nids) if scalefactors: @@ -3027,46 +3052,57 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): class TubeNetworkMeshBuilder(NetworkMeshBuilder): + """ + Builds contiguous tube network meshes with smooth element size transitions at junctions, optionally with solid core. + """ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSegment: float, - defaultElementsCountAround: int, elementsCountThroughWall: int, layoutAnnotationGroups: list = [], - annotationElementsCountsAlong: list = [], annotationElementsCountsAround: list = [], - defaultElementsCountCoreBoxMinor: int = 2, elementsCountTransition: int = 1, - annotationElementsCountsCoreBoxMinor: list = [], isCore=False, useOuterTrimSurfaces=False): - """ + layoutAnnotationGroups: list=[], annotationElementsCountsAlong: list=[], + defaultElementsCountAround: int=8, annotationElementsCountsAround: list=[], + elementsCountThroughShell: int=1, isCore=False, elementsCountTransition: int=1, + defaultElementsCountCoreBoxMinor: int=2, annotationElementsCountsCoreBoxMinor: list=[], + useOuterTrimSurfaces=True): + """ + Builds contiguous tube network meshes with smooth element size transitions at junctions, optionally with solid + core. :param networkMesh: Description of the topology of the network layout. - :param targetElementDensityAlongLongestSegment: - :param defaultElementsCountAround: - :param elementsCountThroughWall: - :param layoutAnnotationGroups: - :param annotationElementsCountsAlong: List in same order as layoutAnnotationGroups, specifying fixed - number along segment with any elements in the annotation group. Client must ensure exclusive map from segments. - Groups with zero value or past end of this list use the targetElementDensityAlongLongestSegment. - :param annotationElementsCountsAround: List in same order as layoutAnnotationGroups, specifying fixed - number around segment with any elements in the annotation group. Client must ensure exclusive map from segments. - Groups with zero value or past end of this list use the defaultElementsCountAround. + :param targetElementDensityAlongLongestSegment: Real value which longest segment path in network is divided by + to get target element length, which is used to determine numbers of elements along except when set for a segment + through annotationElementsCountsAlong. + :param layoutAnnotationGroups: Annotation groups defined on the layout to mirror on the final mesh. + :param annotationElementsCountsAlong: List in same order as layoutAnnotationGroups, specifying fixed number of + elements along segment with any elements in the annotation group. Client must ensure exclusive map from + segments. Groups with zero value or past end of this list use the targetElementDensityAlongLongestSegment. + :param defaultElementsCountAround: Number of elements around segments to use unless overridden by + annotationElementsCountsAround. + :param annotationElementsCountsAround: List in same order as layoutAnnotationGroups, specifying fixed number of + elements around segments with any elements in the annotation group. Client must ensure exclusive map from + segments. Groups with zero value or past end of this list use the defaultElementsCountAround. + :param elementsCountThroughShell: Number of elements through shell/wall >= 1. + :param isCore: Set to True to define solid core box and transition elements. + :param elementsCountTransition: Number of rows of elements transitioning between core box and shell >= 1. :param defaultElementsCountCoreBoxMinor: Number of elements across the core box in the minor/d3 direction. - :param elementsCountTransition: :param annotationElementsCountsCoreBoxMinor: List in same order as layoutAnnotationGroups, specifying numbers of - elements across core box minor/d3 direction. - :param isCore: Set to True to define solid core box and transition elements. - :param useOuterTrimSurfaces: Set to True to use common trim surfaces calculated from outer. + elements across core box minor/d3 direction for segments with any elements in the annotation group. Client must + ensure exclusive map from segments. Groups with zero value or past end of this list use the + defaultElementsCountCoreBoxMinor. + :param useOuterTrimSurfaces: Set to False to use separate trim surfaces on inner and outer tubes. Ignored if + no inner path. """ super(TubeNetworkMeshBuilder, self).__init__( networkMesh, targetElementDensityAlongLongestSegment, layoutAnnotationGroups, annotationElementsCountsAlong) self._defaultElementsCountAround = defaultElementsCountAround - self._elementsCountThroughWall = elementsCountThroughWall - self._layoutAnnotationGroups = layoutAnnotationGroups self._annotationElementsCountsAround = annotationElementsCountsAround + self._elementsCountThroughShell = elementsCountThroughShell + self._isCore = isCore + self._elementsCountTransition = elementsCountTransition + self._defaultElementsCountCoreBoxMinor = defaultElementsCountCoreBoxMinor + self._annotationElementsCountsCoreBoxMinor = annotationElementsCountsCoreBoxMinor layoutFieldmodule = self._layoutRegion.getFieldmodule() self._layoutInnerCoordinates = layoutFieldmodule.findFieldByName("inner coordinates").castFiniteElement() if not self._layoutInnerCoordinates.isValid(): self._layoutInnerCoordinates = None - self._isCore = isCore - self._useOuterTrimSurfaces = useOuterTrimSurfaces - self._defaultElementsCountCoreBoxMinor = defaultElementsCountCoreBoxMinor - self._elementsCountTransition = elementsCountTransition - self._annotationElementsCountsCoreBoxMinor = annotationElementsCountsCoreBoxMinor + self._useOuterTrimSurfaces = useOuterTrimSurfaces if self._layoutInnerCoordinates else False def createSegment(self, networkSegment): pathParametersList = [get_nodeset_path_ordered_field_parameters( @@ -3102,7 +3138,7 @@ def createSegment(self, networkSegment): i += 1 return TubeNetworkMeshSegment(networkSegment, pathParametersList, elementsCountAround, - self._elementsCountThroughWall, self._isCore, elementsCountCoreBoxMinor, + self._elementsCountThroughShell, self._isCore, elementsCountCoreBoxMinor, self._elementsCountTransition) def createJunction(self, inSegments, outSegments): @@ -3125,6 +3161,40 @@ def generateMesh(self, generateData): segment.addShellElementsToMeshGroup(shellMeshGroup) +class BodyTubeNetworkMeshBuilder(TubeNetworkMeshBuilder): + """ + Specialization of TubeNetworkMeshBuilder adding annotations for left, right, dorsal, ventral regions. + Requires network layout to follow these conventions: + - consistently annotates fully left or right features with names including "left" or "right", respectively. + - along central body, +d2 direction is left, -d2 direction is right. + - +d3 direction is ventral, -d3 is dorsal. + """ + + def generateMesh(self, generateData): + super(BodyTubeNetworkMeshBuilder, self).generateMesh(generateData) + # build left, right, dorsal, ventral annotation groups + leftMeshGroup = generateData.getLeftMeshGroup() + rightMeshGroup = generateData.getRightMeshGroup() + dorsalMeshGroup = generateData.getDorsalMeshGroup() + ventralMeshGroup = generateData.getVentralMeshGroup() + for networkSegment in self._networkMesh.getNetworkSegments(): + segment = self._segments[networkSegment] + annotationTerms = segment.getAnnotationTerms() + for annotationTerm in annotationTerms: + if "left" in annotationTerm[0]: + segment.addAllElementsToMeshGroup(leftMeshGroup) + break + if "right" in annotationTerm[0]: + segment.addAllElementsToMeshGroup(rightMeshGroup) + break + else: + # segment on main axis + segment.addSideD2ElementsToMeshGroup(False, leftMeshGroup) + segment.addSideD2ElementsToMeshGroup(True, rightMeshGroup) + segment.addSideD3ElementsToMeshGroup(False, ventralMeshGroup) + segment.addSideD3ElementsToMeshGroup(True, dorsalMeshGroup) + + class TubeEllipseGenerator: """ Generates tube ellipse curves with even-sized elements with specified radius, phase angle, diff --git a/tests/test_network.py b/tests/test_network.py index 6f9763c3..1679c02b 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -133,8 +133,6 @@ def test_2d_tube_network_snake(self): """ scaffoldPackage = ScaffoldPackage(MeshType_2d_tubenetwork1, defaultParameterSetName="Snake") settings = scaffoldPackage.getScaffoldSettings() - networkLayoutScaffoldPackage = settings["Network layout"] - networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertEqual(12.0, settings["Target element density along longest segment"]) MeshType_2d_tubenetwork1.checkOptions(settings) @@ -177,7 +175,7 @@ def test_2d_tube_network_snake(self): def test_2d_tube_network_sphere_cube(self): """ - Test 2D bifurcation is generated correctly. + Test 2D sphere cube is generated correctly. """ scaffoldPackage = ScaffoldPackage(MeshType_2d_tubenetwork1, defaultParameterSetName="Sphere cube") settings = scaffoldPackage.getScaffoldSettings() @@ -297,8 +295,6 @@ def test_2d_tube_network_vase(self): """ scaffoldPackage = ScaffoldPackage(MeshType_2d_tubenetwork1, defaultParameterSetName="Vase") settings = scaffoldPackage.getScaffoldSettings() - networkLayoutScaffoldPackage = settings["Network layout"] - networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertEqual(12.0, settings["Target element density along longest segment"]) MeshType_2d_tubenetwork1.checkOptions(settings) @@ -368,7 +364,6 @@ def test_3d_tube_network_bifurcation(self): scaffoldPackage.generate(region) fieldmodule = region.getFieldmodule() - mesh3d = fieldmodule.findMeshByDimension(3) mesh3d = fieldmodule.findMeshByDimension(3) self.assertEqual(8 * 4 * 3, mesh3d.getSize()) @@ -445,7 +440,7 @@ def test_3d_tube_network_bifurcation_core(self): self.assertEqual((8 * 4 * 3) * 2 + (4 * 4 * 3), mesh3d.getSize()) nodes = fieldmodule.findNodesetByFieldDomainType(Field.DOMAIN_TYPE_NODES) - self.assertEqual((8 * 4 * 3 + 3 * 3 + 2) * 2 + (9 * 4 * 3 + 3 * 4), nodes.getSize()) + self.assertEqual((8 * 4 * 3 + 3 * 3 + 2) * 2 + (9 * 4 * 3 + 3 * 4), nodes.getSize()) coordinates = fieldmodule.findFieldByName("coordinates").castFiniteElement() self.assertTrue(coordinates.isValid()) @@ -526,7 +521,6 @@ def test_3d_tube_network_converging_bifurcation_core(self): self.assertTrue(findAnnotationGroupByName(annotationGroups, "shell") is not None) fieldmodule = region.getFieldmodule() - mesh3d = fieldmodule.findMeshByDimension(3) mesh3d = fieldmodule.findMeshByDimension(3) self.assertEqual(336, mesh3d.getSize()) @@ -847,7 +841,6 @@ def test_3d_tube_network_sphere_cube_core(self): self.assertEqual(result, RESULT_OK) self.assertAlmostEqual(volume, expectedSizes3d[name][1], 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. @@ -892,7 +885,6 @@ def test_3d_tube_network_trifurcation_cross(self): self.assertEqual(1, len(annotationGroups)) fieldmodule = region.getFieldmodule() - mesh3d = fieldmodule.findMeshByDimension(3) mesh3d = fieldmodule.findMeshByDimension(3) self.assertEqual(144, mesh3d.getSize()) @@ -1038,8 +1030,6 @@ def test_3d_box_network_bifurcation(self): """ scaffoldPackage = ScaffoldPackage(MeshType_3d_boxnetwork1, defaultParameterSetName="Bifurcation") settings = scaffoldPackage.getScaffoldSettings() - networkLayoutScaffoldPackage = settings["Network layout"] - networkLayoutSettings = networkLayoutScaffoldPackage.getScaffoldSettings() self.assertEqual(3, len(settings)) self.assertEqual(4.0, settings["Target element density along longest segment"]) self.assertEqual([0], settings["Annotation numbers of elements along"]) @@ -1221,7 +1211,6 @@ def test_3d_tube_network_loop_core(self): self.assertAlmostEqual(volume, 0.0982033864405135, delta=1.0E-6) self.assertAlmostEqual(surfaceArea, 1.9683574196198823, delta=1.0E-6) - def test_3d_tube_network_loop_two_segments(self): """ Test loop 3-D tube network is generated with 2 segments with fixed element boundary between them. diff --git a/tests/test_uterus.py b/tests/test_uterus.py index 50b85d81..12c97e3e 100644 --- a/tests/test_uterus.py +++ b/tests/test_uterus.py @@ -134,8 +134,8 @@ def test_uterus1(self): for annotationGroup in annotationGroups: annotationGroup.addSubelements() scaffold.defineFaceAnnotations(refineRegion, options, annotationGroups) - for annotation in annotationGroups: - if annotation not in oldAnnotationGroups: + for annotationGroup in annotationGroups: + if annotationGroup not in oldAnnotationGroups: annotationGroup.addSubelements() self.assertEqual(34, len(annotationGroups)) # From 34d22fd9375674d2c28789fef5b158cde0d6525a Mon Sep 17 00:00:00 2001 From: Richard Christie Date: Tue, 22 Oct 2024 12:40:25 +1300 Subject: [PATCH 20/20] Switch from wall to shell --- .../meshtypes/meshtype_2d_tubenetwork1.py | 2 +- .../meshtypes/meshtype_3d_tubenetwork1.py | 2 +- .../meshtypes/meshtype_3d_uterus2.py | 6 +-- .../meshtypes/meshtype_3d_wholebody2.py | 2 +- src/scaffoldmaker/utils/tubenetworkmesh.py | 49 +++++++++---------- 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py index 48b89cc7..08073b7a 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_2d_tubenetwork1.py @@ -127,7 +127,7 @@ def generateBaseMesh(cls, region, options): tubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( region, 2, - isLinearThroughWall=True, + isLinearThroughShell=True, isShowTrimSurfaces=options["Show trim surfaces"]) tubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py index c69e4184..bb9ab084 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_tubenetwork1.py @@ -230,7 +230,7 @@ def generateBaseMesh(cls, region, options): tubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( region, 3, - isLinearThroughWall=options["Use linear through shell"], + isLinearThroughShell=options["Use linear through shell"], isShowTrimSurfaces=options["Show trim surfaces"]) tubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py index 13f712e9..e1f515eb 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_uterus2.py @@ -19,14 +19,14 @@ class UterusTubeNetworkMeshGenerateData(TubeNetworkMeshGenerateData): def __init__(self, region, meshDimension, coordinateFieldName="coordinates", - startNodeIdentifier=1, startElementIdentifier=1, isLinearThroughWall=False, isShowTrimSurfaces=False): + startNodeIdentifier=1, startElementIdentifier=1, isLinearThroughShell=False, isShowTrimSurfaces=False): """ :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, coordinateFieldName, startNodeIdentifier, startElementIdentifier, - isLinearThroughWall, isShowTrimSurfaces) + isLinearThroughShell, isShowTrimSurfaces) self._fundusGroup = self.getOrCreateAnnotationGroup(get_uterus_term("fundus of uterus")) # force these annotation group names in base class self._leftGroup = self.getOrCreateAnnotationGroup(get_uterus_term("left uterus")) @@ -483,7 +483,7 @@ def generateBaseMesh(cls, region, options): uterusTubeNetworkMeshBuilder.build() generateData = UterusTubeNetworkMeshGenerateData( region, 3, - isLinearThroughWall=options["Use linear through wall"], + isLinearThroughShell=options["Use linear through wall"], isShowTrimSurfaces=options["Show trim surfaces"]) uterusTubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() diff --git a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py index ae7f5f5f..f965e7f8 100644 --- a/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py +++ b/src/scaffoldmaker/meshtypes/meshtype_3d_wholebody2.py @@ -824,7 +824,7 @@ def generateBaseMesh(cls, region, options): tubeNetworkMeshBuilder.build() generateData = TubeNetworkMeshGenerateData( region, meshDimension, - isLinearThroughWall=False, + isLinearThroughShell=False, isShowTrimSurfaces=options["Show trim surfaces"]) tubeNetworkMeshBuilder.generateMesh(generateData) annotationGroups = generateData.getAnnotationGroups() diff --git a/src/scaffoldmaker/utils/tubenetworkmesh.py b/src/scaffoldmaker/utils/tubenetworkmesh.py index ac58b047..f58aa670 100644 --- a/src/scaffoldmaker/utils/tubenetworkmesh.py +++ b/src/scaffoldmaker/utils/tubenetworkmesh.py @@ -28,14 +28,14 @@ class TubeNetworkMeshGenerateData(NetworkMeshGenerateData): """ def __init__(self, region, meshDimension, coordinateFieldName="coordinates", - startNodeIdentifier=1, startElementIdentifier=1, isLinearThroughWall=False, isShowTrimSurfaces=False): + startNodeIdentifier=1, startElementIdentifier=1, isLinearThroughShell=False, isShowTrimSurfaces=False): """ - :param isLinearThroughWall: Callers should only set if 3-D with no core. + :param isLinearThroughShell: 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._isLinearThroughShell = isLinearThroughShell self._isShowTrimSurfaces = isShowTrimSurfaces self._trimAnnotationGroupCount = 0 # incremented to make unique annotation group names for trim surfaces @@ -44,7 +44,7 @@ def __init__(self, region, meshDimension, coordinateFieldName="coordinates", 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: + if (meshDimension == 3) and not isLinearThroughShell: self._nodetemplate.setValueNumberOfVersions(self._coordinates, -1, Node.VALUE_LABEL_D_DS3, 1) # get element template and eft for standard case @@ -53,12 +53,12 @@ def __init__(self, region, meshDimension, coordinateFieldName="coordinates", else Element.SHAPE_TYPE_SQUARE) self._elementbasis = self._fieldmodule.createElementbasis( meshDimension, Elementbasis.FUNCTION_TYPE_CUBIC_HERMITE_SERENDIPITY) - if (meshDimension == 3) and isLinearThroughWall: + if (meshDimension == 3) and isLinearThroughShell: self._elementbasis.setFunctionType(3, Elementbasis.FUNCTION_TYPE_LINEAR_LAGRANGE) self._standardEft = self._mesh.createElementfieldtemplate(self._elementbasis) self._standardElementtemplate.defineField(self._coordinates, -1, self._standardEft) - d3Defined = (meshDimension == 3) and not isLinearThroughWall + d3Defined = (meshDimension == 3) and not isLinearThroughShell self._nodeLayoutManager = HermiteNodeLayoutManager() self._nodeLayout6Way = self._nodeLayoutManager.getNodeLayout6Way12(d3Defined) self._nodeLayout8Way = self._nodeLayoutManager.getNodeLayout8Way12(d3Defined) @@ -149,8 +149,8 @@ def getNodeLayoutBifurcation6WayTriplePoint(self, segmentsIn, sequence, maxMajor def getNodetemplate(self): return self._nodetemplate - def isLinearThroughWall(self): - return self._isLinearThroughWall + def isLinearThroughShell(self): + return self._isLinearThroughShell def isShowTrimSurfaces(self): return self._isShowTrimSurfaces @@ -428,7 +428,7 @@ def sample(self, fixedElementsCountAlong, targetElementLength): [[ring] for ring in self._sampledTubeCoordinates[0][2]], None) else: - wallFactor = 1.0 / self._elementsCountThroughShell + shellFactor = 1.0 / self._elementsCountThroughShell ox, od1, od2 = self._sampledTubeCoordinates[0][0:3] ix, id1, id2 = self._sampledTubeCoordinates[1][0:3] rx, rd1, rd2, rd3 = [], [], [], [] @@ -438,8 +438,8 @@ def sample(self, fixedElementsCountAlong, targetElementLength): r.append([]) otx, otd1, otd2 = ox[n2], od1[n2], od2[n2] itx, itd1, itd2 = ix[n2], id1[n2], id2[n2] - # wx, wd3 = self._determineWallCoordinates(otx, otd1, otd2, itx, itd1, itd2, coreCentre, arcCentre) - wd3 = [mult(sub(otx[n1], itx[n1]), wallFactor) for n1 in range(self._elementsCountAround)] + # wx, wd3 = self._determineShellCoordinates(otx, otd1, otd2, itx, itd1, itd2, coreCentre, arcCentre) + wd3 = [mult(sub(otx[n1], itx[n1]), shellFactor) for n1 in range(self._elementsCountAround)] for n3 in range(self._elementsCountThroughShell + 1): oFactor = n3 / self._elementsCountThroughShell iFactor = 1.0 - oFactor @@ -868,7 +868,7 @@ def get_d2(n2, x): return boxd2, transd2 - def _determineWallCoordinates(self, ox, od1, od2, ix, id1, id2, coreCentre, arcCentre): + def _determineShellCoordinates(self, ox, od1, od2, ix, id1, id2, coreCentre, arcCentre): """ Calculates rim coordinates and d3 derivatives based on the centre point of the solid core. :param ox, od1, od2: Coordinates and (d1 and d2) derivatives for outermost rim. @@ -912,8 +912,8 @@ def _determineWallCoordinates(self, ox, od1, od2, ix, id1, id2, coreCentre, arcC od3 = mult(normalize(ot), scalefactor) id3 = mult(normalize(it), scalefactor) else: - wallFactor = 1.0 / self._elementsCountThroughShell - od3 = id3 = mult(sub(ox[n1], ix[n1]), wallFactor) + shellFactor = 1.0 / self._elementsCountThroughShell + od3 = id3 = mult(sub(ox[n1], ix[n1]), shellFactor) txm, td3m, pe, pxi, psf = sampleCubicHermiteCurves( [ix[n1], ox[n1]], [id3, od3], self._elementsCountThroughShell, arcLengthDerivatives=True) @@ -1047,14 +1047,14 @@ def getSampledTubeCoordinatesRing(self, pathIndex, nodeIndexAlong): def getElementsCountShell(self): """ - :return: Number of elements through the non-core shell wall. + :return: Number of elements through the non-core shell. """ return max(1, len(self._rimCoordinates[0][0]) - 1) def getElementsCountRim(self): """ :return: Number of elements radially outside core box if core is on, - otherwise same as number through shell wall. + otherwise same as number through shell. """ elementsCountRim = self.getElementsCountShell() if self._isCore: @@ -1406,7 +1406,7 @@ 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: Node IDs arrays through rim and around, or None if not set. """ return self._rimNodeIds[n2] @@ -1437,7 +1437,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): # create nodes nodes = generateData.getNodes() - isLinearThroughWall = generateData.isLinearThroughWall() + isLinearThroughShell = generateData.isLinearThroughShell() nodetemplate = generateData.getNodetemplate() for n2 in range(elementsCountAlong + 1) if (n2Only is None) else [n2Only]: if (n2 < startSkipCount) or (n2 > elementsCountAlong - endSkipCount): @@ -1513,7 +1513,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData, n2Only=None): rx = self._rimCoordinates[0][n2][n3p] rd1 = self._rimCoordinates[1][n2][n3p] rd2 = self._rimCoordinates[2][n2][n3p] - rd3 = None if isLinearThroughWall else self._rimCoordinates[3][n2][n3p] + rd3 = None if isLinearThroughShell else self._rimCoordinates[3][n2][n3p] ringNodeIds = [] for n1 in range(self._elementsCountAround): nodeIdentifier = generateData.nextNodeIdentifier() @@ -1658,10 +1658,10 @@ def __init__(self, inSegments: list, outSegments: list, useOuterTrimSurfaces): self._useOuterTrimSurfaces = useOuterTrimSurfaces 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 + # based on the outer surface, and reused through the rim 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' + # rim coordinates sampled in the junction are indexed by n3 (indexed outware through the rim) 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] @@ -2905,13 +2905,12 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): 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 + d3Defined = (meshDimension == 3) and not generateData.isLinearThroughShell() nodeLayout6Way = generateData.getNodeLayout6Way() nodeLayout8Way = generateData.getNodeLayout8Way() @@ -3028,7 +3027,7 @@ def generateMesh(self, generateData: TubeNetworkMeshGenerateData): for a in [nids, nodeParameters, nodeLayouts] if (needParameters) 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 + # exploit efts being same through the rim eft = eftList[e1] scalefactors = scalefactorsList[e1] if not eft: @@ -3078,7 +3077,7 @@ def __init__(self, networkMesh: NetworkMesh, targetElementDensityAlongLongestSeg :param annotationElementsCountsAround: List in same order as layoutAnnotationGroups, specifying fixed number of elements around segments with any elements in the annotation group. Client must ensure exclusive map from segments. Groups with zero value or past end of this list use the defaultElementsCountAround. - :param elementsCountThroughShell: Number of elements through shell/wall >= 1. + :param elementsCountThroughShell: Number of elements through shell >= 1. :param isCore: Set to True to define solid core box and transition elements. :param elementsCountTransition: Number of rows of elements transitioning between core box and shell >= 1. :param defaultElementsCountCoreBoxMinor: Number of elements across the core box in the minor/d3 direction.