Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
mandiblemodel.py 15.32 KiB
# -*- coding: utf-8 -*-
# mandible model

from wrap import *
import boneload
import os

# enable full parallelism
StrVectorBase.useTBB()
StrMatrixBase.useTBB()
ContactInteraction.useTBB()

def parms(d={}):
    """default parameters of the model
    """
    p = {}

    # Geomagic/MeshLab input files
    path = 'dolicorhynchops/10k'
    p['mandible'] = f'{path}/mandible.ply' # mandible mesh (.ply/.geo/.msh)
    p['teeth'] = f'{path}/teeth.off'    # one or several vertices (.ply/.off) (used if p['food']=='fixteeth')
    p['LTMJ'] = [0.1458893, -73.13895, 227.3909] # left temporomandibular joint - 1 vertex (.ply/.off/coordinates)
    p['RTMJ'] = f'{path}/RTMJ.off'      # right temporomandibular joint - 1 vertex (.ply/.off/coordinates) 
    p['muscles'] = [
        { 'file': f'{path}/Lmuscle.ply', 'force': 100., 'focalpt':f'{path}/LmuscleF.off', 'method':'T'},
        { 'file': f'{path}/Rmuscle.off', 'force': 100., 'focalpt':f'{path}/RmuscleF.off', 'method':'T'}
    ]
    p['food'] = 'fixteeth'  # type of food: 'fixteeth' or 'cylinder'
    p['fixations'] = {  
        'teeth': ['x','y','z'],  # constrained degrees of freedom
        'LTMJ': ['x','y','z'],
        'RTMJ': ['x','y','z']
    }
    # rigid cylinder (used if p['food']=='cylinder') 
    R = 20
    p['cylinder'] = {
        'origin': [-R-8, 0, -150],
        'radius': R,
        'width': 100,
        'friction': 0.1,
        'penalty': 1e3
    }

    # material properties
    p['density'] = 1.850e-9  # [T/mm³] - bone: 1.850 kg/l
    p['Young'] = 17000.      # [MPa] elastic modulus - bone: 17-20 GPa
    p['Poisson'] = 0.3       # [-] Poisson's ratio

    # numerical parameters
    p['tolNR'] = 1e-6        # [-] equilibrium tolerance
    p['dt0'] = 1.0           # [s] time step size

    p.update(d)
    return p


def getMetafor(p={}):

    p = parms(p)

    import json
    print(f'parameters={json.dumps(p, indent=4, sort_keys=True)}')

    metafor = Metafor()
    domain = metafor.getDomain()
    geometry = domain.getGeometry()
    geometry.setDim3D()

    # load main mandible mesh (volume or surface - it will be meshed in 3D)
    import_mesh(domain, p['mandible'])

    # define groups

    # group #1 is the group containing all volume elements
    groupset = geometry.getGroupSet()    
    groups = {}
    groups['all'] = groupset(1)

    print("the mandible mesh has %d nodes" % groups['all'].getNumberOfMeshPoints())

    # extract external surface of the mesh => groups['surf']
    #   nods_no: tags of the nodes
    #   nods_pos: coordinates of these nodes 
    groups['surf'] = groupset.add(Group(groupset.getLargestNo()+1))
    groups['surf'].addMeshPointsFromObject(groups['all'], BoundarySelector())
    nods_no, nods_pos = extract_nodes_from_group(groups['surf'])

    # load other surface meshes  => groups['LTMJ'], ...
    for meshname in ['LTMJ', 'RTMJ']:
        create_group(p[meshname], nods_no, nods_pos, domain, groups, meshname)

    # load muscle groups
    mgroups = {}                    # stores muscle group data and loads
    for muscle in p['muscles']:
        # load focal point
        if isinstance(muscle['focalpt'], str):
            fullpath = os.path.join(os.path.dirname(__file__), muscle['focalpt'])
            focalnodes, _ = boneload.load_msh(fullpath)
        else:  # coordinates in array or tuple
            focalnodes = [ muscle['focalpt'] ]

        # load surface mesh => groups[name (from filename)]
        name, nodes, tris, ntags = \
            create_group(muscle['file'], nods_no, nods_pos, domain, groups)
        # create loads on the surface
        loads = boneload.create_loads(nodes, tris, \
            total_force=muscle['force'], target=focalnodes[0], method=muscle['method'])
        # store data in a structure
        mgroups[name] = MuscleGroup(nodes, tris, ntags, loads)
        
    # ------------------------------------------------------------------

    # material 
    materials = domain.getMaterialSet()
    materials.define (1, ElastHypoMaterial)
    materials(1).put(MASS_DENSITY,     p['density']) 
    materials(1).put(ELASTIC_MODULUS,  p['Young'])
    materials(1).put(POISSON_RATIO,    p['Poisson'])

    prp1 = ElementProperties(TetraVolume3DElement)
    prp1.put(MATERIAL, 1)
    prp1.put (CAUCHYMECHVOLINTMETH, VES_CMVIM_STD)

    app = FieldApplicator(1)
    app.push(groups['all'])
    app.addProperty(prp1)
    domain.getInteractionSet().add(app)

    # boundary conditions
    fct = PieceWiseLinearFunction()
    fct.setData(0.0, 0.0)
    fct.setData(1.0, 1.0)

    txt2key = { 'x': TX, 'y':TY, 'z':TZ }

    # axis of rotation
    for gname in ['LTMJ', 'RTMJ']:
        for d in p['fixations'][gname]:
            domain.getLoadingSet().define(groups[gname], Field1D(txt2key[d],RE), 0.0)

    mshpts = geometry.getMesh().getPointSet()
    for name, gr in mgroups.items():
        # print(f'len(gr.ntags)={len(gr.ntags)}')
        for i,load in enumerate(gr.loads):
            # print(f'i={i}')
            target = mshpts(gr.ntags[i])
            domain.getLoadingSet().define(target, Field1D(TX,GF1), load.x[0], fct)
            domain.getLoadingSet().define(target, Field1D(TY,GF1), load.x[1], fct)
            domain.getLoadingSet().define(target, Field1D(TZ,GF1), load.x[2], fct)

    # type of food

    if p['food']=='cylinder':
        # contact with a rigid cylinder
        cylinder = Cylinder(domain, center=p['cylinder']['origin'], \
                            width=p['cylinder']['width'], \
                            radius=p['cylinder']['radius'])

        mater2 = materials.define(2, CoulombContactMaterial)
        mater2.put(COEF_FROT_DYN, p['cylinder']['friction'])
        mater2.put(COEF_FROT_STA, p['cylinder']['friction'])
        mater2.put(PEN_TANGENT, p['cylinder']['penalty'])
        mater2.put(PEN_NORMALE, p['cylinder']['penalty'])
        mater2.put(PROF_CONT, p['cylinder']['radius']/2.)

        prp2 = ElementProperties(Contact3DElement)
        prp2.put(MATERIAL, 2)

        ci = RdContactInteraction(2)
        ci.push(groups['surf'])
        ci.setTool(cylinder.skin)
        ci.addProperty(prp2)
        domain.getInteractionSet().add(ci)
    elif p['food']=='fixteeth':
        # teeth are fixed along X
        create_group(p['teeth'], nods_no, nods_pos, domain, groups, 'teeth')
        for d in p['fixations']['teeth']:
            domain.getLoadingSet().define(groups['teeth'], Field1D(txt2key[d],RE), 0.0)
    else:
        # p['food'] is a filename
        ply2mtf(domain, p['food'])

    # Time integration scheme

    ti = AlphaGeneralizedTimeIntegration(metafor)
    metafor.setTimeIntegration(ti)

    tsm = metafor.getTimeStepManager()
    tsm.setInitialTime(0.0, p['dt0'])
    tsm.setNextTime(1.0, 1, 1.0)

    mim = metafor.getMechanicalIterationManager()
    mim.setResidualTolerance(p['tolNR'])

    # parallel solver
    solman = metafor.getSolverManager()
    solver = DSSolver()
    solman.setSolver(solver)

    # history curves
    valuesmanager = metafor.getValuesManager()
    idx=1
    valuesmanager.add(idx, MiscValueExtractor(metafor,EXT_T),'time'); idx+=1
    for name, group in groups.items():
        if name in ['all','surf']: continue
        for key, c in zip([TX,TY,TZ],['x','y','z']):
            fname = f'{name}_F{c}'
            valuesmanager.add(idx, DbNodalValueExtractor(group, Field1D(key,GF1)), SumOperator(), fname)
            metafor.getTestSuiteChecker().checkExtractor(idx)
            idx+=1
    if p['food'] == 'cylinder':
        for key, c in zip([TX,TY,TZ],['x','y','z']):
            fname = f'cylinder_F{c}'
            valuesmanager.add(idx, InteractionValueExtractor(ci, key), fname)
            metafor.getTestSuiteChecker().checkExtractor(idx)
            idx+=1
    
    return metafor

# ------------------------------------------------------------------------------
# utility functions involing Metafor objects


def import_mesh(domain, filename):
    """import the volume mesh
    input file type can be:
    - .msh: the mesh is simply loaded
    - .geo: the mesh is generated
    - .ply/.stl: a .geo is written in the workspace and loaded into gmsh
    """
    # import gmsh file (accept .geo, .msh, .ply or .stl)
    mandible_gmsh = filename
    # make the path absolute
    if not os.path.isabs(mandible_gmsh):
        mandible_gmsh = os.path.join(os.path.dirname(__file__), mandible_gmsh)
    # check whether file exists
    if not os.path.isfile(mandible_gmsh):
        raise Exception(f'{mandible_gmsh} not found!')
    # extract extension
    noext,ext = os.path.splitext(mandible_gmsh)
    if ext in ['.ply', '.stl']:
        plyfile = mandible_gmsh
        # create 2 workspace files
        basename = os.path.join(os.getcwd(), os.path.basename(noext))
        mshfile = f"{basename}.msh"
        geofile = f"{basename}.geo"
        # if msh exists, we can load it directly if the ply file is older than
        # the msh file. Otherwise, we create a geo file
        if os.path.isfile(mshfile) and \
            os.path.getmtime(mshfile) > os.path.getmtime(plyfile):
            mandible_gmsh=mshfile
        else:
            print(f'creating {geofile}')
            f = open(geofile, "w")
            f.write(f'Merge "{plyfile}";\n')
            f.write('Surface Loop(1) = {1};\n')
            f.write('Volume(1) = {1};\n')
            f.write('Physical Volume(1) = { 1 };\n')
            f.write('Mesh.MshFileVersion = 2.2;\n')
            f.close()
            mandible_gmsh=geofile
    elif ext in ['.geo', '.msh']:
        pass
    else:
        raise Exception(f'Unknown extension: {ext}, please use .ply, .stl, .msh or .geo')

    print(f'importing {mandible_gmsh}')
    # from toolbox.gmsh import GmshImport # FIXME: does not work!
    from toolbox.gmshOld import GmshImport
    f = os.path.join(os.path.dirname(__file__), mandible_gmsh)
    mesher = GmshImport(f, domain)
    mesher.execute()


def extract_nodes_from_group(group):
    """input: a Group object of Metafor (a group of mesh points)
    output: 2 arrays containing the node numbers and the node coordinates
    """
    nods_no = []
    nods_pos = []
    for i in range(group.getNumberOfMeshPoints()):
        nods_no.append(group.getMeshPoint(i).getNo())
        pos = group.getMeshPoint(i).getPos(Configuration().currentConf)
        nods_pos.append([ pos.get1(), pos.get2(), pos.get3()])
    # print(f'nods_no={nods_no}')
    return nods_no, nods_pos


def create_group(mesh_file_or_coord, all_no, all_coords, domain, groups, gname=None):
    """loads the mesh file "mesh_file_or_coord" and identify vertices among vertices
    stored in all_coords. 
    Then, the routine creates a group named "gname" (built from mesh_file_or_coord if None)

    mesh_file_or_coord can be either:
      a filename
      an array of points: [ [x1,y1,z1], [x2,y2,z2], ... ]
      one single point: [x1,y1,z1]
    """  
    if isinstance(mesh_file_or_coord, str):
        # load mesh file
        print(f'\n* create_group {mesh_file_or_coord}...')
        fullpath = os.path.join(os.path.dirname(__file__), mesh_file_or_coord) 
        nodes, tris = boneload.load_msh(fullpath)
    elif type(mesh_file_or_coord) in [list, tuple]:
        print(f'\n* create_group {gname}...')
        if type(mesh_file_or_coord[0]) in [list, tuple]:
            # array of points
            nodes = mesh_file_or_coord
        else:
            # one single point
            nodes = [ mesh_file_or_coord ]
        print(f'nodes={nodes}')
        print(f'mesh_file_or_coord={mesh_file_or_coord}')
        tris = []
    else:
        raise Exception(f'Data is not understood: {mesh_file_or_coord}')
    # identify nodes of the mesh among the given vertices of the main mesh
    # ntags = boneload.identify_nodes_brutal(nodes, all_no, all_coords)
    ntags = boneload.identify_nodes_SaP(nodes, all_no, all_coords)

    # create a metafor group
    groupset = domain.getGeometry().getGroupSet()
    if gname is None:
        gname, _ = os.path.splitext(os.path.basename(mesh_file_or_coord))
    group = groupset.add(Group(groupset.getLargestNo()+1))
    groups[gname] = group

    # add all the identified vertices to the group
    nodeset = domain.getGeometry().getMesh().getPointSet()
    for tag in ntags:
        group.addMeshPoint(nodeset(tag))

    print(f'group #{group.getNo()} ({gname}) ' \
    f'created from {mesh_file_or_coord} ({group.getNumberOfMeshPoints()} nodes)')
    
    return gname, nodes, tris, ntags


class MuscleGroup:
    """convenient structure for storing muscle data and loads
    """
    def __init__(self, nodes, tris, ntags, loads):
        self.nodes = nodes 
        self.tris = tris 
        self.ntags = ntags
        self.loads = loads


def ply2mtf(domain, meshfile):
    """load a surface into Metafor's memory from a ply file
    """
    fullpath = os.path.join(os.path.dirname(__file__), meshfile)
    nodes, tris = boneload.load_msh(fullpath)

    mesh = domain.getGeometry().getMesh()
    meshPtSet = mesh.getPointSet()
    
    # create group
    groupset = domain.getGeometry().getGroupSet()
    grp = groupset.add(Group(groupset.getLargestNo()+1))

    # create mesh points
    idx0 = meshPtSet.getLargestNo()+1
    for i, n in enumerate(nodes):
        pt = meshPtSet.define(idx0+i, n[0], n[1], n[2])
        grp.addMeshPoint(pt)

    skinset = mesh.getSkinSet()
    skin = skinset.add(Skin(skinset.getLargestNo()+1))
    print(f'ply2mtf: creating Skin #{skin.getNo()}')
    sideset = mesh.getSideSet()

    # create triangles
    idx1 = sideset.getLargestNo()+1
    for i,tnods in enumerate(tris):
        mesh.define(idx1+i, CELL_TRIANGLE, grp,  [ meshPtSet(idx0+n) for n in tnods])
        skin.push(sideset(idx1+i))

    print(f'ply2mtf: creating Group #{grp.getNo()} meshed with {grp.getNumberOfMeshPoints()} nodes')


class Cylinder:
    def __init__(self, domain, center, width, radius):
        geometry = domain.getGeometry()

        ox, oy, oz = center

        pointset = geometry.getPointSet()
        idx = pointset.getLargestNo()
        p1 = pointset.define(idx+1, ox,        oy-width/2, oz-radius)
        p2 = pointset.define(idx+2, ox+radius, oy-width/2, oz)
        p3 = pointset.define(idx+3, ox,        oy-width/2, oz+radius)
        p4 = pointset.define(idx+4, ox-radius, oy-width/2, oz)

        p11 = pointset.define(idx+11, ox,        oy+width/2, oz-radius)
        p12 = pointset.define(idx+12, ox+radius, oy+width/2, oz)
        p13 = pointset.define(idx+13, ox,        oy+width/2, oz+radius)
        p14 = pointset.define(idx+14, ox-radius, oy+width/2, oz)

        curveset = geometry.getCurveSet()
        idx = curveset.getLargestNo()
        l1 = curveset.add(Arc(idx+1, p1, p2, p3))
        l2 = curveset.add(Arc(idx+2, p3, p4, p1))
        l11 = curveset.add(Arc(idx+11, p11, p12, p13))
        l12 = curveset.add(Arc(idx+12, p13, p14, p11))
        l21 = curveset.add(Line(idx+21, p1, p11))
        l22 = curveset.add(Line(idx+22, p3, p13))

        wireset = geometry.getWireSet()
        idx = wireset.getLargestNo()
        w1 = wireset.add(Wire(idx+1, [l1, l22, l11, l21])) 
        w2 = wireset.add(Wire(idx+2, [l2, l21, l12, l22]))

        srfset = geometry.getSurfaceSet()
        idx = srfset.getLargestNo()
        srf1 = srfset.add(Ruled(idx+1, l1, l11))
        srf2 = srfset.add(Ruled(idx+2, l12, l2))

        sideset = geometry.getSideSet()
        idx = sideset.getLargestNo()
        s1 = sideset.add(Side(idx+1, [w1])); s1.setSurface(srf1)
        s2 = sideset.add(Side(idx+2, [w2])); s2.setSurface(srf2)

        skinset = geometry.getSkinSet()
        idx = skinset.getLargestNo()
        sk1 = skinset.add(Skin(idx+1, [s1, s2]))

        self.skin = sk1