# -*- coding: utf-8 -*-
"""Tools to create or manage PLC.
Please note there is currently no collision or intersection check at all.
Volunteers welcome to help creating, adapting or interfacing a basic
geometry system. A lot of things are needed:
* 2D
* 3D
* More geometric primitives
* Boolean operations (union, intersection, difference)
* Collision recognizing
* Cubic spline interpolation for polygons (partly done)
* GUI .. interactive creation
*
"""
import math
import os
from os import system
import functools
import numpy as np
import pygimli as pg
def _polyCreateDefaultEdges(poly, boundaryMarker=1, isClosed=True, **kwargs):
"""INTERNAL."""
nEdges = poly.nodeCount() - 1 + isClosed
bm = None
if hasattr(boundaryMarker, '__len__'):
if len(boundaryMarker) == nEdges:
bm = boundaryMarker
else:
raise Exception("marker length != nEdges", len(boundaryMarker),
nEdges)
else:
bm = [boundaryMarker] * nEdges
for i in range(poly.nodeCount() - 1):
poly.createEdge(poly.node(i), poly.node(i + 1), bm[i])
if isClosed:
poly.createEdge(poly.node(poly.nodeCount() - 1), poly.node(0), bm[-1])
def setPolyRegionMarker(poly, marker=1, area=0.0, markerPosition=None,
isHole=False, **kwargs):
"""Internal to set region markers to single elementary geometry.
Internal to set region markers.
If no absolute marker position is given.
The region marker is placed 1mm beside the first node in direction to the
geometry center.
Parameters
----------
poly : :gimliapi:`GIMLI::Mesh`
The Polygon that will get a marker
marker : int[1]
The region marker, every resulting mesh cell will get this marker.
area : float[0]
The region max cell size, every resulting mesh cell will get a cell
size lower than area in m² or m³ for 3D, respectively.
markerPosition : pg.Pos
Absolute marker position if you don't want the marker in the center of
the geometry.
isHole : bool [False]
Marks the geometry as a hole and will be cut in any merge mesh.
Keyword Arguments
-----------------
**kwargs
Additional kwargs
"""
pos = None
if markerPosition is not None:
pos = markerPosition
else:
center = pg.center(poly.positions())
p0 = poly.node(0).pos()
# region marker near node 0; 1mm in direction to the center
# should be safer than the center itself
pos = p0 + (center-p0).norm() * 0.001
if isHole is True:
poly.addHoleMarker(pos)
else:
poly.addRegionMarker(pos, marker=marker, area=area)
[docs]
def createRectangle(start=None, end=None, pos=None, size=None, **kwargs):
"""Create rectangle polygon.
Create rectangle with start position and a given size.
Give either start and end OR pos and size.
Parameters
----------
start : [x, y]
Left upper corner. Default [-0.5, 0.5]
end : [x, y]
Right lower corner. Default [0.5, -0.5]
pos : [x, y]
Center position. The rectangle will be moved.
size : [x, y]
Factors for x and y by which the rectangle, defined by **start** and
**width**, are scaled.
Keyword Arguments
-----------------
**kwargs
Additional kwargs
marker : int [1]
Marker for the resulting triangle cells after mesh generation
markerPosition : floats [x, y] [pos + (end - start) * 0.2]
Absolute position of the marker (works for both regions and
holes).
area : float [0]
Maximum cell size for resulting triangles after mesh generation
isHole : bool [False]
The polygon will become a hole instead of a triangulation
boundaryMarker : int [1]
Marker for the resulting boundary edges
leftDirection : bool [True]
TODO Rotational direction
pnts: [[x, y],]
Return squared rectangle of origin-aligned boundingbox for pnts.
minBB: False
Return squared rectangle of non-origin-aligned minimum bounding box
for pnts.
minBBOffset: [1.0, 1.0]
Offset for minimal boundingbox in x and y direction in relative
extent .. whatever that means for non-aligned boxes.
Returns
-------
poly : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
Examples
--------
>>> # no need to import matplotlib, pygimli show does.
>>> import pygimli as pg
>>> import pygimli.meshtools as mt
>>> r1 = mt.createRectangle(pos=[1, -1], size=[4.0, 4.0],
... marker=1, area=0.1, markerPosition=[0, -2])
>>> r2 = mt.createRectangle(start=[0.5, -0.5], end=[2, -2],
... marker=2, area=1.1)
>>> pnts3 = [[-0.5, 0], [0, 0.2], [-0.2, 0.5], [-0.3, 0.25]]
>>> r3 = mt.createRectangle(pnts=pnts3, marker=3, area=0.2)
>>> pnts4 = [[1.5, 0], [2.0, 0.2], [1.8, 0.5], [1.7, 0.25]]
>>> r4 = mt.createRectangle(pnts=pnts4, marker=4, minBB=True)
>>> pnts5 = [[-0.5, -1], [0, -0.8], [-0.2, -0.5], [-0.3, -0.75]]
>>> r5 = mt.createRectangle(pnts=pnts5, marker=5,
... minBB=True, minBBOffset=[1.2, 1.2])
>>> ax, _ = pg.show(mt.mergePLC([r1, r2, r3, r4, r5]))
>>> pg.viewer.mpl.drawSensors(ax, pnts3)
>>> pg.viewer.mpl.drawSensors(ax, pnts4)
>>> pg.viewer.mpl.drawSensors(ax, pnts5)
"""
pnts = kwargs.pop('pnts', None)
if pnts is not None:
if len(pnts) == 1:
return createRectangle(pos=pnts[0], size=[1, 1], **kwargs)
if len(pnts) == 2:
return createRectangle(start=pnts[0], end=pnts[1], **kwargs)
minBB = kwargs.pop('minBB', False)
minBBBoundary = kwargs.pop('minBBOffset', [1.0, 1.0])
if not minBB:
xMin = min(pg.x(pnts))
xMax = max(pg.x(pnts))
yMin = min(pg.y(pnts))
yMax = max(pg.y(pnts))
bb = np.asarray([[xMin, yMin], [xMax, yMax]])
bbScale = (bb[1]-bb[0])*(minBBBoundary-np.asarray([1.0, 1.0]))
return createRectangle(start=bb[0]-bbScale, end=bb[1]+bbScale,
**kwargs)
else:
# create convex hull
m = pg.meshtools.createMesh(pnts)
if m.boundaryCount() == 0:
# probably linear pnts so no convex hull
for i in range(m.nodeCount()-1):
m.createBoundary([i, i+1])
bs = pg.meshtools.createLine(pnts[0], pnts[-1])
else:
bs = m.createSubMesh(m.boundaries([b.id()
for b in m.boundaries() if b.outside()]))
def getBB(m, off, rot):
# Rotate hull and find bb
m2 = pg.Mesh(m)
m2.translate(-off)
m2.transform(rot)
m2.translate(off)
# Increase bb if zero width or lenght
bb = m2.bb()
if (bb[1]-bb[0])[0] < 1e-12:
bb[1][0] += 0.5
bb[0][0] -= 0.5
if (bb[1]-bb[0])[1] < 1e-12:
bb[1][1] += 0.5
bb[0][1] -= 0.5
return bb
off = 0
minSize = [9e99, None, None, None]
for b in bs.boundaries():
# normalize to origin
off = b.node(0).pos()
# rotation to origin x axis
rot = pg.core.getRotation((b.node(1).pos()-off), [1, 0])
# get bounding box for normalized mesh
bb = getBB(bs, off, rot)
# compare size off bb and collect minimum size
s = (bb[1]-bb[0]).abs()
if s < minSize[0]:
minSize[0] = s
minSize[1] = bb
minSize[2] = b.node(1).pos()
minSize[3] = off
# Rotate bb back and create rectangle
bb = minSize[1]
off = minSize[3]
bbScale = (bb[1]-bb[0])*(minBBBoundary-np.asarray([1.0, 1.0]))
r = createRectangle(start=bb[0]-bbScale, end=bb[1]+bbScale,
**kwargs)
rot = pg.core.getRotation([1, 0], minSize[2]-off)
r.translate(-off)
r.transform(rot)
r.translate(off)
# pg.show(r)
# pg.wait()
return r
if start is None:
start = [-0.5, 0.5]
if end is None:
end = [0.5, -0.5]
poly = pg.Mesh(dim=2, isGeometry=True)
sPos = pg.Pos(start)
ePos = pg.Pos(end)
verts = [sPos, [sPos[0], ePos[1]], ePos, [ePos[0], sPos[1]]]
# TODO refactor with polyCreatePolygon
if kwargs.pop("leftDirection", False):
for v in verts[::-1]:
poly.createNode(v)
else:
for v in verts:
poly.createNode(v)
# Note that we do not support the usage of start/end AND size/pos. Only one
# of the pairs. Otherwise strange things will happen with the region
# markers!
if size is not None:
poly.scale(size)
if pos is not None:
poly.translate(pos)
_polyCreateDefaultEdges(poly, **kwargs)
sPos = poly.bb()[0]
ePos = poly.bb()[1]
kwargs['markerPosition'] = kwargs.pop('markerPosition',
sPos + (ePos - sPos) * 0.2)
setPolyRegionMarker(poly, **kwargs)
return poly
[docs]
def createWorld(start, end, marker=1, area=0., layers=None, worldMarker=True,
**kwargs):
"""Create simple rectangular 2D or 3D world.
Create simple rectangular [hexagonal] world with appropriate boundary
conditions. Surface boundary is set pg.core.MARKER_BOUND_HOMOGEN_NEUMANN,
and inner subsurface is set to pg.core.MARKER_BOUND_MIXED, i.e., -2 OR
Numbered: 1, 2, 3, 4, 5, 6 for left, right, bottom, top, front and back,
if worldMarker is set to false and no layers are given. With layers, it is
numbered in ascending order.
TODO
----
* 3D with layers
Parameters
----------
start: [x, y, [z]]
Upper/Left/[Front] Corner
end: [x, y, [z]]
Lower/Right/[Back] Corner
marker: int
Marker for the resulting triangle cells after mesh generation.
area: float | list
Maximum cell size for resulting triangles after mesh generation.
If area is a float set it global, if area is a list set it per layer.
layers: [float] [None]
List of depth coordinates for some layers.
worldMarker: bool [True]
Specify boundary markers:
True: [-1, -2] for [surface, subsurface] boundaries
False: ascending order [1, 2, 3, 4 ..]
Other Parameters
----------------
Forwarded to createCube
Returns
-------
poly : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
Examples
--------
>>> from pygimli.meshtools import createWorld
>>> from pygimli.viewer.mpl import drawMesh
>>> import matplotlib.pyplot as plt
>>> world = createWorld(start=[-5, 0], end=[5, -5], layers=[-1,-2,-3])
>>>
>>> fig, ax = plt.subplots()
>>> drawMesh(ax, world)
>>> plt.show()
"""
if len(start) == 3 and len(end) == 3:
if layers is not None:
pg.critical("3D with layers is not yet implemented.")
world = createCube(size=pg.Pos(end)-pg.Pos(start),
pos=(pg.Pos(end)+pg.Pos(start))/2.0,
area=area, **kwargs)
for i, b in enumerate(world.boundaries()):
if worldMarker is True:
if b.norm()[2] == 1.0:
b.setMarker(pg.core.MARKER_BOUND_HOMOGEN_NEUMANN)
else:
b.setMarker(pg.core.MARKER_BOUND_MIXED)
else:
if b.norm() == [-1, 0, 0]:
b.setMarker(1)
elif b.norm() == [1, 0, 0]:
b.setMarker(2)
elif b.norm() == [0, 0, -1]:
b.setMarker(3)
elif b.norm() == [0, 0, 1]:
b.setMarker(4)
elif b.norm() == [0, -1, 0]:
b.setMarker(5)
elif b.norm() == [0, 1, 0]:
b.setMarker(6)
return world
z = np.array(start[1], dtype=float)
if layers is not None:
z = np.append(z, np.array(layers, dtype=float))
z = np.append(z, end[1])
# ensure - decreasing order if layers are out of bounding box
z = np.sort(z)[::-1]
poly = pg.Mesh(dim=2, isGeometry=True)
if isinstance(area, float) or isinstance(area, int):
area = np.ones(len(z)-1) * float(area)
if len(area) < len(z) - 1:
pg.warn('Missing {} area value, padding with zeros'.format(
(len(z) - 1) - len(area)))
_area = np.zeros(len(z)-1)
_area[0:len(area)] = area
area = _area
for i, depth in enumerate(z):
n = poly.createNode([start[0], depth])
if i > 0:
if len(z) == 2:
poly.addRegionMarker(n.pos() + [0.2, 0.2],
marker=marker, area=area[0])
else:
poly.addRegionMarker(n.pos() + [0.2, 0.2],
marker=i, area=area[i - 1])
for i, depth in enumerate(z[::-1]):
poly.createNode([end[0], depth])
_polyCreateDefaultEdges(poly,
boundaryMarker=range(1, poly.nodeCount() + 1))
if worldMarker:
for b in poly.boundaries():
if b.norm()[1] == 1.0:
b.setMarker(pg.core.MARKER_BOUND_HOMOGEN_NEUMANN)
else:
b.setMarker(pg.core.MARKER_BOUND_MIXED)
elif layers is None:
for b in poly.boundaries():
if b.norm() == [-1, 0]:
b.setMarker(1)
elif b.norm() == [1, 0]:
b.setMarker(2)
elif b.norm() == [0, -1]:
b.setMarker(3)
elif b.norm() == [0, 1]:
b.setMarker(4)
if layers is not None:
for i in range(len(layers)):
poly.createEdge(poly.node(i + 1),
poly.node(poly.nodeCount() - i - 2),
poly.boundaryCount() + 1)
# pg.warnNonEmptyArgs(kwargs)
return poly
[docs]
def createCircle(pos=None, radius=1, nSegments=12, start=0, end=2.*math.pi,
**kwargs):
"""Create simple circle polygon.
Create simple circle polygon with given attributes.
Parameters
----------
pos : [x, y] [[0.0, 0.0]]
Center position
radius : float | [a,b] [1]
radius or halfaxes of the circle
nSegments : int [12]
Discrete amount of segments for the circle.
start : double [0]
Starting angle in radians
end : double [2*pi]
Ending angle in radians
**kwargs:
marker: int [1]
Marker for the resulting triangle cells after mesh generation
markerPosition : floats [x, y] [0.0, 0.0]
Position of the marker (works for both regions and holes)
area: float [0]
Maximum cell size for resulting triangles after mesh generation
isHole: bool [False]
The polygon will become a hole instead of a triangulation
boundaryMarker: int [1]
Marker for the resulting boundary edges
leftDirection: bool [True]
Rotational direction
isClosed: bool [True]
Add closing edge between last and first node.
Returns
-------
poly : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
Examples
--------
>>> # no need to import matplotlib. pygimli's show does
>>> import math
>>> import pygimli as pg
>>> from pygimli.viewer.mpl import drawMesh
>>> import pygimli.meshtools as mt
>>> c0 = mt.createCircle(pos=(-5.0, 0.0), radius=2, nSegments=6)
>>> c1 = mt.createCircle(pos=(-2.0, 2.0), radius=1, area=0.01, marker=2)
>>> c2 = mt.createCircle(pos=(0.0, 0.0), nSegments=5, start=0, end=math.pi)
>>> c3 = mt.createCircle(pos=(5.0, 0.0), nSegments=3, start=math.pi,
... end=1.5*math.pi, isClosed=False)
>>> plc = mt.mergePLC([c0, c1, c2, c3])
>>> fig, ax = pg.plt.subplots()
>>> drawMesh(ax, plc, fillRegion=False)
>>> pg.wait()
"""
pg.renameKwarg('segments', 'nSegments', kwargs, '1.2') # 20210312
nSegments = kwargs.pop('nSegments', nSegments)
# TODO refactor with polyCreatePolygon
if pos is None:
pos = [0.0, 0.0]
poly = pg.Mesh(dim=2, isGeometry=True)
dPhi = (end - start) / (nSegments)
nPhi = nSegments + 1
if abs((end % (2. * math.pi) - start)) < 1e-6:
nPhi = nSegments
for i in range(0, nPhi):
if kwargs.pop('leftDirection', True):
phi = start + i * dPhi
else:
phi = start - i * dPhi
xp = np.cos(phi)
yp = np.sin(phi)
poly.createNode([xp, yp])
if hasattr(radius, '__len__'):
poly.scale(radius)
else:
poly.scale([radius, radius])
poly.translate(pos)
_polyCreateDefaultEdges(poly, **kwargs)
if kwargs.pop('isClosed', True):
setPolyRegionMarker(poly, **kwargs)
# need a better way mess with these or wrong kwargs
# pg.warnNonEmptyArgs(kwargs)
return poly
[docs]
def createLine(start, end, nSegments=1, **kwargs):
"""Create simple line polygon.
Create simple line polygon from start to end.
Parameters
----------
start : [x, y]
start position
end : [x, y]
end position
nSegments : int
Discrete amount of segments for the line
Keyword Arguments
-----------------
boundaryMarker : int [1]
Marker for the resulting boundary edges
leftDirection : bool [True]
Rotational direction
Returns
-------
poly : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
Examples
--------
>>> # no need to import matplotlib. pygimli's show does
>>> import pygimli as pg
>>> import pygimli.meshtools as mt
>>>
>>> w = mt.createWorld(start=[0, 0], end=[3, 3])
>>> l1 = mt.createLine(start=[1, 1], end=[1, 2], nSegments=1,
... leftDirection=False)
>>> l2 = mt.createLine(start=[1, 1], end=[2, 1], nSegments=20,
... leftDirection=True)
>>>
>>> ax, _ = pg.show(mt.createMesh([w, l1, l2,]))
>>> ax, _ = pg.show([w, l1, l2,], ax=ax, fillRegion=False)
>>> pg.wait()
"""
pg.renameKwarg('segments', 'nSegments', kwargs, '1.2') # 20210312
nSegments = kwargs.pop('nSegments', nSegments)
# TODO refactor with polyCreatePolygon
poly = pg.Mesh(dim=2, isGeometry=True)
startPos = pg.RVector3(start)
endPos = pg.RVector3(end)
a = endPos - startPos
dt = 1. / nSegments
left = kwargs.pop('leftDirection', True)
for i in range(0, nSegments + 1):
if left:
p = startPos + a * (dt * i)
else:
p = endPos - a * (dt * i)
poly.createNode(p)
_polyCreateDefaultEdges(poly, isClosed=False, **kwargs)
return poly
[docs]
def createPolygon(verts, isClosed=False, addNodes=0, interpolate='linear',
**kwargs):
"""Create a polygon from a list of vertices.
All vertices need to be unique and duplicate vertices will be ignored.
If you want the polygon be a closed region you can set the 'isClosed' flag.
Closed region can be attributed by assigning a region marker.
The automatic region marker is placed in the center of all vertices.
Parameters
----------
verts : []
* List of x y pairs [[x0, y0], ... ,[xN, yN]]
isClosed : bool [True]
Add closing edge between last and first node.
addNodes : int [1], iterable
Constant or (for each) Number of additional nodes to be added,
equidistant between sensors.
interpolate : str ['linear']
Interpolation rule for addNodes. 'linear' or 'spline'. TODO 'harmfit'
**kwargs:
marker : int [None]
Marker for the resulting triangle cells after mesh generation.
markerPosition : floats [x, y] [0.0, 0.0]
Position (absolute) of the marker (works for both regions and
holes)
area : float [0]
Maximum cell size for resulting triangles after mesh generation
isHole : bool [False]
The polygon will become a hole instead of a triangulation
boundaryMarker : int [1]
Marker for the resulting boundary edges
leftDirection : bool [True]
Rotational direction
Returns
-------
poly : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
Examples
--------
>>> # no need to import matplotlib, pygimli show does.
>>> import pygimli as pg
>>> import pygimli.meshtools as mt
>>> p1 = mt.createPolygon([[0.0, 0.0], [1.0, 0.0], [1.0, 1.0]],
... isClosed=True, marker=3, area=0.1)
>>> p2 = mt.createPolygon([[0.3, 0.15], [0.85, 0.15], [0.85, 0.7]],
... isClosed=True, isHole=True)
>>> p3 = mt.createPolygon([[-0.1, 0.2], [-1.1, 0.2], [-1.1, 1.2], [-0.1, 1.2]],
... isClosed=True, addNodes=3, marker=2)
>>> p4 = mt.createPolygon([[-0.1, 0.2], [-1.1, 0.2], [-1.1, 1.2], [-0.1, 1.2]],
... isClosed=True, addNodes=5, interpolate='spline',
... marker=4)
>>> ax, _ = pg.show(mt.mergePLC([p1, p2, p3, p4]), showNodes=True)
>>> pg.wait()
"""
poly = pg.Mesh(dim=2, isGeometry=True)
if hasattr(addNodes, '__iter__') or addNodes > 0:
if isClosed:
verts = np.array(verts)
verts = np.vstack([verts, verts[0]])
tV = pg.utils.cumDist(verts)
if isinstance(addNodes, int) and addNodes > 0:
addNodes = np.full(len(tV)-1, addNodes)
if len(addNodes) != len(tV)-1:
print(addNodes)
pg.error('Amount of addNodes does not match needed length:',
len(tV)-1)
tI = []
for i, t in enumerate(tV[0:len(tV)-1]):
tI.append(t)
for j in range(addNodes[i]):
dt = (tV[i+1]-tV[i]) / (addNodes[i]+1)
tI.append(tV[i] + dt*(j+1))
if not isClosed:
tI.append(tV[-1])
verts = pg.meshtools.interpolate(verts, tI,
method=interpolate,
periodic=isClosed)
if kwargs.pop("leftDirection", False):
for v in verts[::-1]:
if isinstance(v, float) or isinstance(v, int):
poly.createNodeWithCheck([v, 0], warn=True)
else:
poly.createNodeWithCheck(v, warn=True)
else:
for v in verts:
if isinstance(v, float) or isinstance(v, int):
poly.createNodeWithCheck([v, 0], warn=True)
else:
poly.createNodeWithCheck(v, warn=True)
_polyCreateDefaultEdges(poly, isClosed=isClosed,
boundaryMarker=kwargs.pop('boundaryMarker', 1))
if isClosed:
setPolyRegionMarker(poly, **kwargs)
return poly
[docs]
def merge(*args, **kwargs):
"""Little syntactic sugar to merge.
All args are forwarded to mergeMeshes if isGeometry is not set.
Otherwise it considers the mesh as PLC to merge.
Args
----
List of meshes or comma separated list of meshes that will be forwarded to
mergeMeshes or meshPLC.
"""
if len(args) == 1 and isinstance(args[0], list):
return merge(*args[0], **kwargs)
for arg in args:
if hasattr(arg, 'isGeometry'):
if arg.isGeometry():
return mergePLC([*args], **kwargs)
return pg.meshtools.mergeMeshes([*args], **kwargs)
[docs]
def mergePLC(plcs, tol=1e-3):
"""Merge multiply polygons.
Merge multiply polygons into a single polygon.
Common nodes and common edges will be checked and removed.
When a node touches an edge, the edge will be splited.
3D only OOC with polytools
TODO:
* Crossing or Node/Edge intersections will NOT be
recognized yet.
* Edge on Node touch
Parameters
----------
plcs: [:gimliapi:`GIMLI::Mesh`]
List of PLC that want to be merged into one new PLC
tol : double
Tolerance to check for duplicated nodes. [1e-3]
Returns
-------
plc : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
Examples
--------
>>> import pygimli as pg
>>> import pygimli.meshtools as mt
>>> from pygimli.viewer.mpl import drawMesh
>>> world = mt.createWorld(start=[-10, 0], end=[10, -10], marker=1)
>>> c1 = mt.createCircle([-1, -4], radius=1.5, area=0.1,
... marker=2, nSegments=5)
>>> c2 = mt.createCircle([-6, -5], radius=[1.5, 3.5], isHole=1)
>>> r1 = mt.createRectangle(pos=[3, -5], size=[2, 2], marker=3)
>>> r2 = mt.createRectangle(start=[4, -4], end=[6, -6],
... marker=4, area=0.1)
>>> plc = mt.mergePLC([world, c1, c2, r1, r2])
>>> fig, ax = pg.plt.subplots()
>>> drawMesh(ax, plc)
>>> drawMesh(ax, mt.createMesh(plc))
>>> pg.wait()
"""
if plcs[0].dim() == 3:
return mergePLC3D(plcs, tol)
# tmp = pg.optImport('tempfile')
# names = []
# for p in plcs:
# _, namePLC = tmp.mkstemp(suffix='.poly')
# pg.meshtools.exportPLC(p, namePLC)
# names.append(namePLC)
# for n in names[1:]:
# syscal = 'polyMerge {0} {1} {0}'.format(names[0], n)
# pg.debug(syscal)
# os.system(syscal)
# plc = readPLC(names[0])
# for n in names:
# try:
# pg.debug('Remove:', n)
# os.remove(n)
# except Exception:
# print("can't remove:", n)
# return plc
# handle 2D geometries
plc = pg.Mesh(dim=2, isGeometry=True)
for p in plcs:
nodes = []
for n in p.nodes():
nn = plc.createNodeWithCheck(n.pos(), tol,
warn=False, edgeCheck=True)
if n.marker() != 0:
nn.setMarker(n.marker())
nodes.append(nn)
for e in p.boundaries():
plc.createEdge(nodes[e.node(0).id()], nodes[e.node(1).id()],
e.marker())
if len(p.regionMarkers()) > 0:
for rm in p.regionMarkers():
plc.addRegionMarker(rm)
if len(p.holeMarker()) > 0:
for hm in p.holeMarker():
plc.addHoleMarker(hm)
return plc
[docs]
def mergePLC3D(plcs, tol=1e-3):
"""Merge a list of 3D PLC into one.
Experimental replacement for polyMerge. Don't expect too much.
Works if:
* all plcs are free and does not have any contact to each other
* contact of two facets if the second is completely within the first
"""
if len(plcs) < 2:
pg.critical("Give at least 2 PLCs.")
if plcs[0].dim() != 3:
pg.warn("2D poly found. redirect to mergePLC")
return mergePLC(plcs, tol)
p0 = pg.Mesh(plcs[0])
for p in plcs[1:]:
for b in p.boundaries():
p0.copyBoundary(b)
if len(p.regionMarkers()) > 0:
for rm in p.regionMarkers():
p0.addRegionMarker(rm)
for hm in p.holeMarker():
p0.addHoleMarker(hm)
return p0
[docs]
def createParaDomain2D(*args, **kwargs):
"""API change here .. use createParaMeshPLC instead."""
pg.deprecated("use createParaMeshPLC")
return createParaMeshPLC(*args, **kwargs)
[docs]
def createParaMeshPLC(sensors, paraDX=1, paraDepth=-1, paraBoundary=2,
paraMaxCellSize=0.0, boundary=-1, boundaryMaxCellSize=0,
balanceDepth=True,
isClosed=False, addNodes=1, **kwargs):
"""Create a geometry (PLC) for an inversion parameter mesh.
Create an inversion mesh geometry (PLC) for a given list of
sensor positions. Sensor positions are assumed to be on the surface and
must be unique and sorted along x coordinate.
You can create a parameter mesh without sensors if you just set [xMin,
xMax] as sensors.
The PLC is a :gimliapi:`GIMLI::Mesh` and contain nodes, edges and two
region markers, one for the parameters domain (marker=2) and a larger
boundary around the outside (marker=1).
TODO:
* additional topographic points
* spline interpolation between sensorpoints or addpoints for non closed
* subsurface sensors (partly .. see example)
Parameters
----------
sensors : [RVector3] | DataContainer with sensorPositions() | [xMin, xMax]
Sensor positions. Must be sorted and unique in positive x direction.
Depth need to be y-coordinate.
paraDX : float [1]
Relative distance for refinement nodes between two sensors (1=none),
e.g., 0.5 means 1 additional node between two neighboring sensors
e.g., 0.33 means 2 additional equidistant nodes between two sensors
paraDepth : float[-1], optional
Maximum depth in m for parametric domain.
Automatic (<=0) results in 0.4 * maximum sensor span range in m
balanceDepth: bool [True]
Equal depth for the parametric domain.
paraBoundary : float, optional
Margin for parameter domain in absolute sensor distances. 2 (default).
paraMaxCellSize: double, optional
Maximum cell size for parametric region in m²
boundaryMaxCellSize: double, optional
Maximum cells size in the boundary region in m²
boundary : float, optional
Boundary width to be appended for domain prolongation in absolute
para domain width.
Values lover 0 force the boundary to be 4 times para domain width.
isClosed : bool [False]
Create a closed geometry from sensor positions.
Region marker is 1. Boundary marker is -1 (homogeneous Neumann)
addNodes : int [1]
Number of additional nodes to be added equidistant between sensors.
trapRatio : float [0]
Form a trapezoidal shape instead of a rectangle.
The value is a ratio of the total length to put inside at depth.
Returns
-------
poly: :gimliapi:`GIMLI::Mesh`
Piecewise linear complex (PLC) containing nodes and edges
Examples
--------
>>> # no need to import matplotlib, pygimli show does.
>>> import pygimli as pg
>>> import pygimli.meshtools as mt
>>> # Create the simplest paramesh PLC with a para box of 10 m without
>>> # sensors
>>> p = mt.createParaMeshPLC([0,10])
>>> # you can add subsurface sensors now with
>>> for z in range(1,4):
... n = p.createNode((5,-z), -99)
>>> ax,_ = pg.show(p)
"""
if isClosed:
plc = createPolygon(sensors, isClosed=True, addNodes=addNodes,
boundaryMarker=-1, marker=1,
area=paraMaxCellSize, **kwargs)
return plc
noSensors = False
if hasattr(sensors, 'sensorPositions'): # obviously a DataContainer type
sensors = sensors.sensorPositions()
elif isinstance(sensors, np.ndarray):
if sensors.ndim == 1:
sensors = [pg.RVector3(s, 0) for s in sensors]
else: # assume 2d array with 2 or 3 values per item
sensors = [pg.RVector3(s) for s in sensors]
elif isinstance(sensors, list):
if len(sensors) == 2:
# guess we have just a desired Pbox with
sensors = [pg.RVector3(sensors[0], 0.0),
pg.RVector3(sensors[1], 0.0)]
noSensors = True
paraBoundary = 0
eSpacing = kwargs.pop('eSpacing', sensors[0].distance(sensors[1]))
iz = 1
xMin, yMin, zMin = sensors[0][0], sensors[0][1], sensors[0][2]
xMax, yMax, zMax = xMin, yMin, zMin
for e in sensors:
xMin = min(xMin, e[0])
xMax = max(xMax, e[0])
yMin = min(yMin, e[1])
yMax = max(yMax, e[1])
zMin = min(zMin, e[2])
zMax = max(zMax, e[2])
if abs(yMin) < 1e-8 and abs(yMax) < 1e-8:
iz = 2
paraBound = eSpacing * paraBoundary
if paraDepth <= 0.0:
paraDepth = 0.4 * (xMax - xMin)
poly = pg.Mesh(dim=2, isGeometry=True)
# define para domain without surface
n1 = poly.createNode([xMin - paraBound, sensors[0][iz]])
xStart = xMin - paraBound
xEnd = xMax + paraBound
trapRatio = np.minimum(kwargs.pop("trapRatio", 0), 0.45)
dxTrap = (xEnd - xStart) * trapRatio
if balanceDepth:
bD = min(sensors[0][iz] - paraDepth, sensors[-1][iz] - paraDepth)
n2 = poly.createNode([xStart + dxTrap, bD])
n3 = poly.createNode([xEnd - dxTrap, bD])
else:
n2 = poly.createNode([xStart + dxTrap, sensors[0][iz] - paraDepth])
n3 = poly.createNode([xEnd - dxTrap, sensors[-1][iz] - paraDepth])
n4 = poly.createNode([xMax + paraBound, sensors[-1][iz]])
if boundary < 0:
boundary = 4
bound = abs(xMax - xMin) * boundary
if bound > paraBound:
# define world without surface
n11 = poly.createNode(n1.pos() - [bound, 0.])
n12 = poly.createNode(n11.pos() - [0., bound + paraDepth])
n14 = poly.createNode(n4.pos() + [bound, 0.])
n13 = poly.createNode(n14.pos() - [0., bound + paraDepth])
poly.createEdge(n1, n11, pg.core.MARKER_BOUND_HOMOGEN_NEUMANN)
poly.createEdge(n11, n12, pg.core.MARKER_BOUND_MIXED)
poly.createEdge(n12, n13, pg.core.MARKER_BOUND_MIXED)
poly.createEdge(n13, n14, pg.core.MARKER_BOUND_MIXED)
poly.createEdge(n14, n4, pg.core.MARKER_BOUND_HOMOGEN_NEUMANN)
poly.addRegionMarker(n12.pos() + [1e-3, 1e-3], 1, boundaryMaxCellSize)
poly.createEdge(n1, n2, 1)
poly.createEdge(n2, n3, 1)
poly.createEdge(n3, n4, 1)
poly.addRegionMarker(n2.pos() + [1e-3, 1e-3], 2, paraMaxCellSize)
# define surface
nSurface = []
nSurface.append(n1)
if paraDX == 0.0:
paraDX = 1.0
if not noSensors:
for i, e in enumerate(sensors):
if iz == 2:
e.rotateX(-math.pi / 2)
# nSurface.append(poly.createNode(e, pg.core.MARKER_NODE_SENSOR))
if addNodes > 1:
nSurface.append(poly.createNode(e, pg.core.MARKER_NODE_SENSOR))
if i < len(sensors) - 1:
e1 = sensors[i + 1]
if iz == 2:
e1.rotateX(-math.pi / 2)
for j in range(addNodes):
nSurface.append(poly.createNode(
e + (e1 - e) * (j+1)/(addNodes+1)))
elif paraDX >= 0.5:
nSurface.append(poly.createNode(e, pg.core.MARKER_NODE_SENSOR))
if i < len(sensors) - 1:
e1 = sensors[i + 1]
if iz == 2:
e1.rotateX(-math.pi / 2)
nSurface.append(poly.createNode((e + e1) * 0.5))
elif paraDX < 0.5:
if i > 0:
e1 = sensors[i - 1]
if iz == 2:
e1.rotateX(-math.pi / 2)
nSurface.append(poly.createNode(e - (e - e1) * paraDX))
nSurface.append(poly.createNode(e, pg.core.MARKER_NODE_SENSOR))
if i < len(sensors) - 1:
e1 = sensors[i + 1]
if iz == 2:
e1.rotateX(-math.pi / 2)
nSurface.append(poly.createNode(e + (e1 - e) * paraDX))
nSurface.append(n4)
for i in range(len(nSurface) - 1, 0, -1):
poly.createEdge(nSurface[i], nSurface[i - 1],
pg.core.MARKER_BOUND_HOMOGEN_NEUMANN)
return poly
[docs]
def createParaMeshSurface(sensors, paraBoundary=None, boundary=-1,
surfaceMeshQuality=30, surfaceMeshArea=0,
addTopo=None):
r"""Create surface mesh for an 3D inversion parameter mesh.
Topographic information (non-zero z-coodinate) can be from sensors
together with addTopo, or in addTopo alone if provided.
Outside boundary corners are set to median of all topography.
Args
----
sensors: DataContainer with sensorPositions()
Sensor positions.
paraBoundary: [float, float] [1.1, 1.1]
Margin for parameter domain in relative extend.
boundary: [float, float] [10., 10.]
Boundary width to be appended for domain prolongation in relative
para domain size.
surfaceMeshQuality: float [30]
Quality of the surface mesh.
surfaceMeshArea: float [0]
Max cell Size for parametric domain.
addTopo: [[x,y,z],]
Number of additional nodes for topography.
Returns
-------
surface: :gimliapi:`GIMLI::Mesh`
3D Surface mesh
Examples
--------
>>> # no need to import matplotlib, pygimli show does.
>>> import numpy as np
>>> import pygimli as pg
>>> import pygimli.meshtools as mt
>>> # very simple design: 10 sensors on 1D profile in 3D topography
>>> x = np.linspace(-10, 10, 10)
>>> topo = [[15, -15, 10], [-15, 15, -10]]
>>> surface = mt.createParaMeshSurface(np.asarray([x, x, x*0]).T,
... paraBoundary=[1.2, 1.2],
... boundary=[2, 2],
... surfaceMeshQuality=30,
... addTopo=topo)
>>> _ = pg.show(surface, showMesh=True, color='white')
"""
if hasattr(sensors, 'sensors'):
sensors = sensors.sensors()
sensors = np.asarray(sensors)
if paraBoundary is None:
paraBoundary = [1.1, 1.1]
elif isinstance(paraBoundary, (int, float)):
paraBoundary = [paraBoundary, paraBoundary]
if boundary is None:
boundary = [10.0, 10.0]
# find maximum extent
boundaryRect = pg.meshtools.createRectangle(pnts=sensors[:, 0:2],
minBB=False,
minBBOffset=boundary)
for i in range(4):
boundaryRect.boundary(i).setMarker(i+1)
boundaryRect.node(i).setMarker((i % 4 + 1) * 10)
# collect all pnts with topography
if addTopo is not None:
if min(sensors[:, 2]) != max(sensors[:, 2]) and sensors[0][2] != 0.0:
pnts = np.vstack((sensors, addTopo))
else:
pnts = np.asarray(addTopo)
else:
pnts = np.array(sensors)
# add maximal extent corners to median topo
boundaryRect.translate([0, 0, np.median(pnts[:, 2])])
pnts = np.vstack((boundaryRect.positions(), pnts))
# create mesh for topo interpolation
pntsSurface = pg.meshtools.createMesh(pnts[:, 0:2])
# find parameter extent
paraRect = pg.meshtools.createRectangle(pnts=sensors[:, 0:2],
minBB=True,
minBBOffset=paraBoundary,
area=surfaceMeshArea,
)
for i in range(4):
paraRect.boundary(i).setMarker(i + 5)
paraRect.node(i).setMarker((i % 4 + 5) * 10)
# create surface mesh of sensors and with maximal and parameter extent
surfacePLC = boundaryRect + paraRect + sensors[:, 0:2]
surface = pg.meshtools.createMesh(surfacePLC, quality=surfaceMeshQuality)
# interpolate Topography to surface
sZ = pg.interpolate(pntsSurface, pnts[:, 2], surface.positions())
for n in surface.nodes():
n.translate(0, 0, sZ[n.id()])
# create 3D surfacemesh
s = pg.meshtools.createSurface(
surface, boundaryMarker=pg.core.MARKER_BOUND_HOMOGEN_NEUMANN)
return s
# pg.show(surface, showMesh=True)
[docs]
def createParaMeshPLC3D(sensors, paraDX=0, paraDepth=-1, paraBoundary=None,
paraMaxCellSize=0.0, boundary=None,
boundaryMaxCellSize=0,
surfaceMeshQuality=30, surfaceMeshArea=0,
addTopo=None, isClosed=False, **kwargs):
r"""Create a geometry (PLC) for an 3D inversion parameter mesh.
Todo
----
* 3D without TOPO
* better paraBoundary scale for with and height
Args
----
sensors: Sensor list or pg.DataContainer with .sensors()
Sensor positions.
paraDX : float [1]
Absolute distance for node refinement (0=none).
Refinement node will be placed below the surface.
paraDepth : float[-1], optional
Maximum depth in m for parametric domain.
Automatic (<=0) results in 0.4 * maximum sensor span range in m.
Depth is set to median sensors depth + paraDepth.
paraBoundary: [float, float] | float [1.1, 1.1]
Margin for parameter domain in relative extend.
paraMaxCellSize: double, optional
Maximum cell size for parametric region in m³
boundaryMaxCellSize: double, optional
Maximum cells size in the boundary region in m³
boundary: [float, float] | float [10., 10.]
Boundary width to be appended for domain prolongation in relative
para domain size.
surfaceMeshQuality: float [30]
Quality of the surface mesh.
surfaceMeshArea: float [0]
Max boundary size for surface area in parametric region.
addTopo: [[x,y,z],]
Number of additional nodes for topography.
Returns
-------
poly: :gimliapi:`GIMLI::Mesh`
Piecewise linear complex (PLC) containing nodes and edges
"""
if hasattr(sensors, 'sensors'):
sensors = sensors.sensors()
sensors = np.asarray(sensors)
if boundary is None:
boundary = [10.0, 10.0]
elif isinstance(boundary, (float, int)):
boundary = [boundary, boundary]
surface = pg.meshtools.createParaMeshSurface(
sensors, paraBoundary=paraBoundary, boundary=boundary,
surfaceMeshQuality=surfaceMeshQuality,
surfaceMeshArea=surfaceMeshArea,
addTopo=addTopo)
# find depth and paradepth
xSpan = (max(sensors[:, 0]) - min(sensors[:, 0]))
ySpan = (max(sensors[:, 1]) - min(sensors[:, 1]))
if paraDepth == -1:
paraDepth = (0.4*(max(xSpan, ySpan)))
paraDepth = np.median(sensors[:, 2]) - paraDepth
depth = paraDepth - max(boundary[0]*xSpan, boundary[1]*ySpan)/2
def sortP(p):
base = pg.core.Line(p[0], p[1]).at(-1e7)
def cmp_(p1, p2):
if p1.distSquared(base) < p2.distSquared(base):
return -1
else:
return 1
p.sort(key=functools.cmp_to_key(cmp_))
bounds = [surface]
# close outer surfaces
bttm = []
for i in range(4):
p = [n.pos() for n in surface.nodes() if n.marker() == i+1]
p.append(surface.nodes(
surface.nodeMarkers() == (i % 4 + 1) * 10)[0].pos())
p.append(surface.nodes(
surface.nodeMarkers() == ((i + 1) % 4 + 1) * 10)[0].pos())
sortP(p)
p0 = pg.Pos(p[-1])
p0[2] = depth
p.append(p0)
p1 = pg.Pos(p[0])
p1[2] = depth
p.append(p1)
m = pg.meshtools.createPolygon(p, isClosed=True)
f = pg.meshtools.createFacet(m, boundaryMarker=-2)
bounds.append(f)
bttm.append(p0)
bttm.append(p1)
m = pg.meshtools.createRectangle(pnts=bttm, minBB=True)
m.translate([0, 0, depth])
bttmA = pg.meshtools.createFacet(m, boundaryMarker=-2)
bounds.append(bttmA)
# close para surfaces
bttm = []
for i in range(4):
p = [n.pos() for n in surface.nodes() if n.marker() == i+5]
p.append(surface.nodes(
surface.nodeMarkers() == (i % 4 + 5) * 10)[0].pos())
p.append(surface.nodes(
surface.nodeMarkers() == ((i + 1) % 4 + 5) * 10)[0].pos())
sortP(p)
p0 = pg.Pos(p[-1])
p0[2] = paraDepth
p.append(p0)
p1 = pg.Pos(p[0])
p1[2] = paraDepth
p.append(p1)
m = pg.meshtools.createPolygon(p[::-1], isClosed=True)
f = pg.meshtools.createFacet(m, boundaryMarker=1)
bounds.append(f)
bttm.append(p0)
bttm.append(p1)
m = pg.meshtools.createRectangle(pnts=bttm, minBB=True)
m.translate([0, 0, paraDepth])
bttmP = pg.meshtools.createFacet(m, boundaryMarker=1)
bounds.append(bttmP)
pdPLC = pg.meshtools.mergePLC(bounds)
if paraDX > 0:
for s in sensors:
pdPLC.createNode(s - [0.0, 0.0, paraDX])
pdPLC.addRegionMarker(pg.center(bttmA.positions()) + [0.0, 0.0, 0.1],
marker=1, area=boundaryMaxCellSize)
pdPLC.addRegionMarker(pg.center(bttmP.positions()) + [0.0, 0.0, 0.1],
marker=2, area=paraMaxCellSize)
return pdPLC
[docs]
def readPLC(filename, comment='#'):
r"""Read in a piece-wise linear complex object (PLC) from .poly file.
A PLC is a pyGIMLi geometry, e.g., created using `mt.exportPLC`.
Read 2D :term:`Triangle` or 3D :term:`Tetgen` PLC files.
Parameters
----------
filename: string
Filename *.poly
comment: string ('#')
String containing all characters that define a comment line.
Identified lines will be ignored during import.
Returns
-------
poly :
:gimliapi:`GIMLI::Mesh`
See Also
--------
exportPLC
"""
with open(filename, 'r') as fi:
content = fi.readlines()
# Filter comment lines
comment_lines = []
for i, line in enumerate(content):
if line[0] in comment:
comment_lines.append(i)
for j in comment_lines[::-1]:
del(content[j])
# Read header
headerLine = content[0].split('\r\n')[0].split()
if len(headerLine) != 4:
raise Exception("Format unknown! header size != 4", headerLine)
fromOne = 0
nVerts = int(headerLine[0])
dimension = int(headerLine[1])
nPointsAttributes = int(headerLine[2])
haveNodeMarker = int(headerLine[3])
poly = pg.Mesh(dim=dimension, isGeometry=False)
# isGeometry forces expensive checks: we assume the plc is valid so we set
# this flag in the end
# Nodes section
for i in range(nVerts):
row = content[1 + i].split('\r\n')[0].split()
if len(row) == (1 + dimension + nPointsAttributes + haveNodeMarker):
if i == 0:
fromOne = int(row[0])
if dimension == 2:
n = poly.createNode((float(row[1]), float(row[2])))
elif dimension == 3:
n = poly.createNode((float(row[1]), float(row[2]),
float(row[3])))
if haveNodeMarker:
n.setMarker(int(row[-1]))
else:
print(i, len(row), row,
(1 + dimension + nPointsAttributes + haveNodeMarker))
raise Exception("Poly file seams corrupt: node section line: " +
content[1 + i])
# Segment section
row = content[1 + nVerts].split()
if len(row) != 2:
raise Exception("Format unknown for segment section " + row)
nSegments = int(row[0])
haveBoundaryMarker = int(row[1])
if dimension == 2:
for i in range(nSegments):
row = content[2 + nVerts + i].split()
if len(row) == (3 + haveBoundaryMarker):
marker = 0
if haveBoundaryMarker:
marker = int(row[3])
poly.createEdge(
poly.node(int(row[1]) - fromOne),
poly.node(int(row[2]) - fromOne), marker)
else:
segment_offset = 0
for i in range(nSegments):
row = content[2 + nVerts + i + segment_offset].split()
numBounds = int(row[0])
numHoles = int(row[1])
# if numHoles != '0':
# pg.error("Can't handle 3D faces with holes yet")
marker = 0
if haveBoundaryMarker:
marker = int(row[2])
face = None
for k in range(numBounds):
boundRow = content[2 + nVerts + i + segment_offset + 1]\
.split()
# nNodes = int(boundRow[0])
nodeIdx = [int(_b) for _b in boundRow[1:]]
if k == 0:
face = poly.createPolygonFace(poly.nodes(nodeIdx),
marker=marker, check=True)
else:
if len(nodeIdx) == 2:
if nodeIdx[0] == nodeIdx[1]:
face.addSecondaryNode(poly.node(nodeIdx[0]))
else:
face.addSubface(nodeIdx)
segment_offset += 1
for k in range(numHoles):
r = content[2 + nVerts + i + segment_offset + 1]\
.split()
face.addHoleMarker([float(hm) for hm in r[1:]])
segment_offset += 1
nSegments += segment_offset
# Hole section
row = content[2 + nVerts + nSegments].split()
if len(row) != 1:
raise Exception("Format unknown for hole section " + row)
nHoles = int(row[0])
for i in range(nHoles):
row = content[3 + nVerts + nSegments + i].split()
if len(row) == 3:
poly.addHoleMarker([float(row[1]), float(row[2])])
elif len(row) == 4 and dimension == 3:
poly.addHoleMarker([float(row[1]), float(row[2]), float(row[3])])
else:
raise Exception("Poly file seams corrupt: hole section line (3):" +
row + " : " + str(i) + " " + str(len(row)))
if (3 + nVerts + nSegments + nHoles) < len(content):
# Region section
row = content[3 + nVerts + nSegments + nHoles].split()
if len(row) != 1:
raise Exception("Format unknown for region section " + row)
nRegions = int(row[0])
for i in range(nRegions):
row = content[4 + nVerts + nSegments + nHoles + i].split()
if len(row) == 5:
poly.addRegionMarker([float(row[1]), float(row[2])],
marker=int(float(row[3])),
area=float(row[4]))
elif len(row) == 6 and dimension == 3:
poly.addRegionMarker([float(row[1]), float(row[2]),
float(row[3])],
marker=int(float(row[4])),
area=float(row[5]))
else:
raise Exception("Poly file seams corrupt: region section " +
"line (5): " + str(i) + " " + str(len(row)))
poly.setGeometry(True)
return poly
[docs]
def exportPLC(poly, fname, **kwargs):
r"""Export a piece-wise linear complex (PLC) to a .poly file (2D or 3D).
Chooses from poly.dimension() and forwards accordingly to
:gimliapi:`GIMLI::Mesh::exportAsTetgenPolyFile`
or :py:mod:`pygimli.meshtools.writeTrianglePoly`
Parameters
----------
poly : :gimliapi:`GIMLI::Mesh`
The polygon to be written.
fname : string
Filename of the file to write (\\*.n, \\*.e).
Examples
--------
>>> import pygimli as pg
>>> import tempfile, os
>>> fname = tempfile.mktemp() + '.poly' # Create temporary filename.
>>> world2d = pg.meshtools.createWorld(start=[-10, 0], end=[20, -10])
>>> pg.meshtools.exportPLC(world2d, fname)
>>> read2d = pg.meshtools.readPLC(fname)
>>> print(read2d)
Mesh: Nodes: 4 Cells: 0 Boundaries: 4
>>> world3d = pg.createGrid([0, 1], [0, 1], [-1, 0])
>>> pg.meshtools.exportPLC(world3d, fname)
>>> os.remove(fname)
See Also
--------
readPLC
"""
if poly.dimension() == 2:
exportTrianglePoly(poly, fname, **kwargs)
else:
exportTetgenPoly(poly, fname, **kwargs)
def exportTrianglePoly(poly, fname, float_format='.15e'):
r"""Write :term:`Triangle` poly.
Write :term:`Triangle` :cite:`Shewchuk96b` ASCII file.
See: ://www.cs.cmu.edu/~quake/triangle.html
Parameters
----------
poly : :gimliapi:`GIMLI::Mesh`
mesh PLC holding nodes, edges, holes & regions
fname : string
Target filename *.poly
float_format : string
format string for floats according to str.format()
verbose : boolean [False]
Be verbose during import.
"""
if fname.rfind('.poly') == -1:
fname = fname + '.poly'
if float_format[0] != '{':
pfmt = '{:' + float_format + '}'
else:
pfmt = float_format
with open(fname, 'w') as fid:
fid.write('{:d}\t2\t0\t1\n'.format(poly.nodeCount()))
nm = poly.nodeMarkers()
bm = poly.boundaryMarkers()
fmt = '{:d}' + ('\t' + pfmt) * 2 + '\t{:d}\n'
for i, p in enumerate(poly.positions()):
fid.write(fmt.format(i, p.x(), p.y(), nm[i]))
fid.write('{:d}\t1\n'.format(poly.boundaryCount()))
for i, b in enumerate(poly.boundaries()):
fid.write('{:d}\t{:d}\t{:d}\t{:d}\n'.format(i, b.node(0).id(),
b.node(1).id(), bm[i]))
fid.write('{:d}\n'.format(len(poly.holeMarker())))
fmt = '{:d}' + ('\t' + pfmt) * 2 + '\n'
for i, h in enumerate(poly.holeMarker()):
fid.write(fmt.format(i, h.x(), h.y()))
fid.write('{:d}\n'.format(len(poly.regionMarkers())))
fmt = '{:d}' + ('\t' + pfmt) * 3 + '\t{:.15e}\n'
for i, r in enumerate(poly.regionMarkers()):
fid.write(fmt.format(i, r.x(), r.y(), r.marker(), r.area()))
return
def writeTrianglePoly(*args, **kwargs):
"""Backward compatibility.
Please use :py:mod:`pygimli.meshtools.exportTrianglePoly`.
"""
return exportTrianglePoly(*args, **kwargs)
def exportTetgenPoly(poly, filename, float_format='.12e', **kwargs):
r"""Export PLC as tetgen poly file.
Write given piecewise linear complex (mesh/poly) into Ascii file in
:term:`Tetgen` .poly format.
Parameters
----------
filename: string
Name in which the result will be written. The recommended file
ending is '.poly'.
poly: :gimliapi:`GIMLI::Mesh`
Piecewise linear complex as :gimliapi:`GIMLI::Mesh` to be exported.
float_format: format string ('.12e')
Format that will be used to write float values in the Ascii file.
Default is the exponential float form with a precision of 12 digits.
kwargs:
* extraBoundaries:
Add additional polygons (#c42 still needed?)
"""
if filename[-5:] != '.poly':
filename = filename + '.poly'
polytxt = ''
sep = '\t' # standard tab seperated file
linesep = '\n' # os.linesep does not work in mingwshell, testit!!
assert poly.dim() == 3, 'Exit, only for 3D meshes.'
boundary_marker = 1
attribute_count = 0
# Part 1/4: node list
# intro line
# <nodecount> <dimension (3)> <# of attributes> <boundary markers (0 or 1)>
polytxt += '{0}{5}{1}{5}{2}{5}{3}{4}'.format(poly.nodeCount(), 3,
attribute_count,
boundary_marker,
linesep, sep)
# loop over positions, attributes and marker(node)
# <point idx> <x> <y> <z> [attributes] [boundary marker]
point_str = '{:d}' # index of the point
for i in range(3):
# coords as float with given precision
point_str += sep + '{:%s}' % (float_format)
point_str += sep + '{:d}' + linesep # node marker
for j, node in enumerate(poly.nodes()):
fill = [node.id()]
fill.extend([pos for pos in node.pos()])
fill.append(node.marker())
polytxt += point_str.format(*fill)
# Part 2/4: boundary list
# intro line
# <# of facets> <boundary markers (0 or 1)>
nBoundaries = poly.boundaryCount()
# look for extra boundaries present in either the PLC or in kwargs
extraBoundaries = []
if 'extraBoundaries' in kwargs:
extraBoundaries += kwargs.pop('extraBoundaries', [])
if hasattr(poly, 'extraBoundaries'):
extraBoundaries += poly.extraBoundaries
if len(extraBoundaries) > 0:
print("Detected ", len(extraBoundaries), " extra boundaries!")
nBoundaries += len(extraBoundaries)
polytxt += '{0:d}{2}1{1}'.format(nBoundaries, linesep, sep)
# loop over facets, each facet can contain an arbitrary number of holes
# and polygons, in our case, there is always one polygon per facet.
hole_str = '{:d}'
for m in range(3):
hole_str += sep + '{:%s}' % float_format
hole_str += linesep
for bound in poly.boundaries():
# one line per facet
# <# of polygons> [# of holes] [boundary marker]
try:
nSubs = bound.subfaceCount()
except BaseException:
nSubs = 0
try:
nHoles = len(bound.holeMarkers())
except BaseException:
nHoles = 0
npolys = 1 + nSubs + len(bound.secondaryNodes())
polytxt += '{3}{2}{4}{2}{0:d}{1}'.format(bound.marker(), linesep,
sep, npolys, nHoles)
# inner loop over polygons
# <# of corners> <corner 1> <corner 2> ... <corner #>
for k in range(1):
poly_str = '{:d}'.format(bound.nodeCount())
poly_str += sep + sep.join(['{:d}'.format(n) for n in bound.ids()])
polytxt += '{0}{1}'.format(poly_str, linesep)
# loop over subfaces
for k in range(nSubs):
sub = bound.subface(k)
poly_str = '{:d}'.format(len(sub))
poly_str += sep + sep.join(['{:d}'.format(n.id()) for n in sub])
polytxt += '{0}{1}'.format(poly_str, linesep)
# inner loop over holes
if nHoles > 0:
for n, hole in enumerate(bound.holeMarkers()):
polytxt += hole_str.format(n, *hole)
# not necessary yet ?! why is there an extra hole section?
# because this is for 2D holes in facets only
# loop over secondaryNodes add them as single points
for k in range(len(bound.secondaryNodes())):
ind = bound.secondaryNodes()[k].id()
poly_str = '{:d}'.format(2)
poly_str += sep + '{0:d} {0:d}'.format(ind)
polytxt += '{0}{1}'.format(poly_str, linesep)
# part 2b: extra boundaries that cannot be part of mesh class
for nodes in extraBoundaries:
# <# of polygons> [# of holes] [boundary marker]
npolys = 1
polytxt += '1{2}0{2}{0:d}{1}'.format(111, linesep, sep)
# <# of corners> <corner 1> <corner 2> ... <corner #>
poly_str = '{:d}'.format(len(nodes))
for ind in nodes:
poly_str += sep + '{:d}'.format(ind)
polytxt += '{0}{1}'.format(poly_str, linesep)
# part 3/4: hole list
# intro line
# <# of holes>
holes = poly.holeMarker()
polytxt += '{:d}{}'.format(len(holes), linesep)
# loop over hole markers
# <hole #> <x> <y> <z>
for n, hole in enumerate(holes):
polytxt += hole_str.format(n, *hole)
# part 4/4: region attributes and volume constraints (optional)
# intro line
# <# of regions>
regions = poly.regionMarkers()
polytxt += '{:d}{}'.format(len(regions), linesep)
# loop over region markers
# <region #> <x> <y> <z> <region number> <region attribute>
region_str = '{:d}'
for o in range(3):
region_str += sep + '{:%s}' % (float_format)
region_str += sep + '{:d}%s{:%s}' % (sep, float_format) + linesep
for p, region in enumerate(regions):
polytxt += region_str.format(p, region.x(), region.y(), region.z(),
region.marker(),
region.area())
# writing file
with open(filename, 'w') as out:
out.write(polytxt)
[docs]
def syscallTetgen(filename, quality=1.2, area=0, preserveBoundary=False,
verbose=False, tetgen='tetgen'):
"""Create a mesh from a PLC by system-calling :term:`Tetgen`.
Create a :term:`Tetgen` :cite:`Si2004` mesh from a PLC.
Parameters
----------
filename: str
quality: float [1.2]
Refines mesh (to improve mesh quality). [1.1 ... ]
area: float [0.0]
Maximum cell size (m³)
preserveBoundary: bool [False]
Preserve PLC boundary mesh
verbose: bool [False]
be verbose
tetgen: str | path ['tetgen']
Binary for tetgen. Given as complete path or simple the binary name
if its known in the system path.
Returns
-------
mesh : :gimliapi:`GIMLI::Mesh`
"""
filebody = filename.replace('.poly', '')
# tetgen -pazVAC -q1.2 $MESH
# test -O2
syscal = tetgen + ' -pzAC'
if area > 0:
syscal += 'a' + str(area)
else:
syscal += 'a'
syscal += 'q' + str(quality)
if not verbose:
syscal += 'Q'
else:
pass
# syscal += 'V'
if preserveBoundary:
syscal += 'Y'
syscal += ' ' + filebody + '.poly'
if verbose:
print(syscal)
pg.debug(syscal)
system(syscal)
mesh = None
if os.path.isfile(filebody + '.1.node'):
# system('meshconvert -it -BD -o ' + filebody + ' ' + filebody + '.1')
mesh = pg.meshtools.readTetgen(filebody + '.1')
try:
os.remove(filebody + '.1.node')
os.remove(filebody + '.1.ele')
os.remove(filebody + '.1.face')
except BaseException as e:
print(e)
else:
# system('meshconvert -it -BD -o ' + filebody + ' ' + filebody + '-1')
mesh = pg.meshtools.readTetgen(filebody + '-1')
try:
os.remove(filebody + '-1.node')
os.remove(filebody + '-1.ele')
os.remove(filebody + '-1.face')
except BaseException as e:
print(e)
# mesh = pg.Mesh(filebody)
return mesh
def polyCreateWorld(filename, x=None, depth=None, y=None, marker=0,
maxCellSize=0, verbose=True):
"""Create the PLC of a default world.
Out-of-core wrapper for dcfemlib::polytools::polyCreateWorld
Todo
* needs to be converted to the Python-only tools.
Parameters
----------
filename : str
file name
x : float
x dimension
y : float
y dimension
depth : float
z dimension
marker : int
region marker
maxCellSize : float
maximum cell size
verbose : bool
be verbose
"""
if depth is None:
print("Please specify worlds depth.")
return
if x is None:
print("Please specify worlds x dimension.")
return
dimension = 3
z = depth
if y is None:
dimension = 2
syscal = 'polyCreateWorld -d ' + str(dimension) \
+ ' -x ' + str(x) \
+ ' -y ' + str(y) \
+ ' -z ' + str(z) \
+ ' -m ' + str(marker) \
if maxCellSize > 0:
syscal += " -a " + str(maxCellSize)
syscal = syscal + ' ' + filename
if verbose:
print(syscal)
os.system(syscal)
[docs]
def createSurface(mesh, boundaryMarker=None, verbose=True):
"""Convert a 2D mesh into a 3D surface mesh.
Parameters
----------
mesh: :gimliapi:`GIMLI::Mesh`
The 2D input mesh.
boundaryMarker: int[0]
Boundary marker for the resulting faces.
If None the cell markers of the mesh are taken.
Returns
-------
:gimliapi:`GIMLI::Mesh`
The 3D surface mesh.
"""
if mesh.dimension() != 2:
pg.error("Need two dimensional mesh")
if mesh.cellCount() == 0:
pg.error("Need a two dimensional mesh with cells")
# think to use this
# s = surface.createHull()
surface = pg.Mesh(dim=3, isGeometry=True)
[surface.createNode(n.pos(), n.marker()).id() for n in mesh.nodes()]
for c in mesh.cells():
surface.createBoundary(c.ids(), marker=c.marker())
if boundaryMarker is not None:
surface.setBoundaryMarkers(np.full(surface.boundaryCount(),
boundaryMarker))
return surface
[docs]
def createFacet(mesh, boundaryMarker=None):
"""Create coplanar PLC from a 2d mesh or PLC.
Parameters
----------
mesh : pg.Mesh
2D mesh or PLC to be converted to a 3D facet
boundaryMarker : int
boundary marker for the facet, otherwise taken from 2d region markers
Returns
-------
plc : pyGIMLi mesh
plc of the created facet
Todo
----
* mesh with cell into plc with boundaries
* poly account for inner edges
"""
if mesh.dimension() != 2:
pg.error("need two dimensional mesh or poly")
if mesh.cellCount() > 0:
pg.critical("Implementme")
poly = pg.Mesh(dim=3, isGeometry=True)
nodes = [poly.createNode(n.pos()).id() for n in mesh.nodes()]
if boundaryMarker is None:
for rm in mesh.regionMarkers():
boundaryMarker = rm.marker()
continue
b = poly.createBoundary(nodes, marker=boundaryMarker or 0)
for h in mesh.holeMarker():
b.addHoleMarker(h)
return poly
[docs]
def createCube(size=[1.0, 1.0, 1.0], pos=None,
start=None, end=None,
rot=None, boundaryMarker=0, **kwargs):
"""Create cube PLC as geometrie definition.
Create cube PLC as geometrie definition.
You can either give size and center position or start and end position.
Parameters
----------
size: [x, y, z]
x, y, and z-size of the cube. Default = [1.0, 1.0, 1.0] in m
pos: [x, y, z]
The center position, default is at the origin.
start: [x, y, z]
Left Front Bottom corner.
end: [x, y, z]
Right Back Top corner.
rot: pg.Pos [None]
Rotate on the center.
boundaryMarker: int[0]
Boundary marker for the resulting faces.
** kwargs:
Marker related arguments:
See :py:mod:`pygimli.meshtools.polytools.setPolyRegionMarker`
Examples
--------
>>> import pygimli.meshtools as mt
>>> cube = mt.createCube()
>>> print(cube)
Mesh: Nodes: 8 Cells: 0 Boundaries: 6
>>> cube = mt.createCube([10, 10, 1])
>>> print(cube.bb())
[RVector3: (-5.0, -5.0, -0.5), RVector3: (5.0, 5.0, 0.5)]
>>> cube = mt.createCube([10, 10, 1], pos=[-4.0, 0.0, 0.0])
>>> print(pg.center(cube.positions()))
RVector3: (-4.0, 0.0, 0.0)
Returns
-------
poly : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
"""
if start is not None and end is not None:
size = pg.Pos(end) - pg.Pos(start)
pos = pg.Pos(start) + pg.Pos(size)/2
poly = pg.Mesh(3, isGeometry=True)
for y in [-0.5, 0.5]:
poly.createNode(-0.5, y, -0.5)
poly.createNode(+0.5, y, -0.5)
poly.createNode(+0.5, y, +0.5)
poly.createNode(-0.5, y, +0.5)
faces = [[4, 5, 1, 0],
[5, 6, 2, 1],
[6, 7, 3, 2],
[7, 4, 0, 3],
[0, 1, 2, 3],
[7, 6, 5, 4], ]
if isinstance(boundaryMarker, list):
for i, f in enumerate(faces):
poly.createPolygonFace(poly.nodes(f), marker=boundaryMarker[i])
else:
for f in faces:
poly.createPolygonFace(poly.nodes(f), marker=boundaryMarker)
poly.scale(size)
if rot is not None:
poly.rotate(rot)
if pos is not None:
poly.translate(pos)
setPolyRegionMarker(poly, **kwargs)
return poly
[docs]
def extrude(p2, z=-1.0, boundaryMarker=0, **kwargs):
"""Create 3D body by extruding a closed 2D poly into z direction.
Parameters
----------
p2 : :gimliapi:`GIMLI::Mesh`
2D geometry
z : float [-1.0]
2D geometry
Keyword Arguments
-----------------
** kwargs:
Marker related arguments:
See :py:mod:`pygimli.meshtools.polytools.setPolyRegionMarker`
Returns
-------
poly : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
"""
if p2.dimension() != 2:
pg.error("need two dimensional mesh or poly")
if p2.cellCount() > 0:
pg.critical("Implementme")
poly = pg.Mesh(3, isGeometry=True)
top = []
for n in p2.nodes():
top.append(poly.createNode(n.pos()).id())
bot = []
for n in p2.nodes():
bot.append(poly.createNode(n.pos() + [0.0, 0.0, z]).id())
N = len(top)
poly.createPolygonFace(poly.nodes(top), marker=boundaryMarker)
poly.createPolygonFace(poly.nodes(bot[::-1]), marker=boundaryMarker)
for i in range(len(top)):
poly.createPolygonFace(poly.nodes([i, N + i,
N + (i + 1) % N,
(i+1) % N]),
marker=boundaryMarker)
setPolyRegionMarker(poly, **kwargs)
return poly
[docs]
def createCylinder(radius=1, height=1, nSegments=8,
pos=None, rot=None, boundaryMarker=0, **kwargs):
"""Create PLC of a cylinder.
Out of core wrapper for dcfemlib::polytools.
Note, there is a bug in the old polytools which ignores the area settings
for marker == 0.
Parameters
----------
radius : float
Radius of the cylinder.
height : float
Height of the cylinder
nSegments : int [8]
Number of segments of the cylinder.
pos : pg.Pos [None]
The center position, default is at the origin.
Keyword Arguments
----------------
** kwargs:
Marker related arguments:
See :py:mod:`pygimli.meshtools.polytools.setPolyRegionMarker`
Returns
-------
poly : :gimliapi:`GIMLI::Mesh`
The resulting polygon is a :gimliapi:`GIMLI::Mesh`.
"""
circ = createCircle(radius=radius, nSegments=nSegments)
poly = extrude(circ, z=height, boundaryMarker=boundaryMarker, **kwargs)
# move it to z=0
poly.translate([0.0, 0.0, -height/2])
if rot is not None:
c = pg.center(poly.positions())
poly.translate(-c)
poly.rotate(rot)
poly.translate(c)
if pos is not None:
poly.translate(pos)
return poly
def boundaryPlaneIntersectionLines(boundaries, plane):
"""Create Lines from boundaries that intersect a plane."""
lines = []
for b in boundaries:
ps = []
for i, n in enumerate(b.shape().nodes()):
line = pg.Line(n.pos(), b.shape().node(
(i + 1) % b.shape().nodeCount()).pos())
p = plane.intersect(line, 1e-8, True)
if p.valid():
ps.append(p)
if len(ps) == 2:
lines.append(list(zip([ps[0].x(), ps[1].x()],
[ps[0].z(), ps[1].z()])))
return lines
if __name__ == "__main__":
pass