linemesh

class nekmeshpy.linemesh.LineMesh[source]

Bases: object

A 1-D mesh: a PointMesh of the shared points with (L,2) line connectivity and a sparse per-line element_tags.

Its point tags are the point mesh’s own element tags – a point is one object the whole mesh shares, so naming it is naming that object, not naming it once per incident line. point_tags reads them through.

EDGE_POINTS = array([[0],        [1]])
__init__(point_mesh, lines, interior=None, element_tags=None)[source]

Construct from the rung below: point_mesh (a PointMesh, or a bare (N,3) array promoted to an untagged one), the required lines (L,2) connectivity, the per-line interior nodes, and an optional Tags naming whichever lines are tagged.

Passing an array is the authoring form and the one every factory building fresh geometry wants – there are no point tags to carry yet. Pass a PointMesh when there are: an operation rebuilding this mesh around moved coordinates has to hand the tags on rather than let the promotion drop them.

Parameters:
  • point_mesh (PointMesh | NDArray[float64])

  • lines (NDArray[int64])

  • interior (NDArray[float64] | None)

  • element_tags (Tags | None)

Return type:

None

property corners: NDArray[int64]

(L,2) incidence into the point mesh – the same array lines holds, under the name the rungs above use for this slot (corners on QuadMesh / HexMesh).

A point is its own corner, so at this rung the incidence into the rung below and the corner connectivity are one table; the two names exist so code that walks every rung can ask for either without a special case.

property points: NDArray[float64]

The (N,3) coordinates – a derived view of the point mesh’s own array, so mesh.points[:] = X repositions every rung built on it.

property point_tags: Tags

The tags on the shared points, over point ids.

This is point_mesh’s own element_tags read through, not a table of its own: rung N’s side tags are rung N-1’s element tags all the way up the ladder, and the bottom is no exception.

property order: int

Global polynomial order (1 = linear).

property n_points: int

Number of points.

property n_lines: int

Number of line elements.

property n_point_tags: int

Number of tagged end points.

property point_group_tags: list[str]

Sorted unique tags of the tagged end points present on the mesh.

property element_group_tags: list[str]

Sorted unique non-empty per-line element tags present on the mesh.

Operations

linemesh.assemble

Variable-arity LineMesh operations – the only ones that build a numbering.

class nekmeshpy.linemesh.assemble.Seam[source]

Bases: NamedTuple

One stated interface between two line meshes – the line rung’s counterpart of hexmesh.Seam, naming point groups.

A slice at this rung is a single point, so loft’s first_tag / last_tag name the two chain ends and are the natural way to make a seam addressable here. a and b name a mesh by its position in attach’s meshes or by the mesh itself; a mesh appearing twice must be named by index.

a: int | LineMesh

Alias for field number 0

tag_a: str | NDArray[int64]

Alias for field number 1

b: int | LineMesh

Alias for field number 2

tag_b: str | NDArray[int64]

Alias for field number 3

own: str

Alias for field number 4

attach_tag: str | None

Alias for field number 5

static __new__(_cls, a, tag_a, b, tag_b, own='a', attach_tag=None)

Create new instance of Seam(a, tag_a, b, tag_b, own, attach_tag)

Parameters:
nekmeshpy.linemesh.assemble.attach(meshes, seams)[source]

Join line meshes at the point groups each Seam names, in one pass.

No tolerance: within a named pair of groups the pairing is nearest-neighbour, and what proves it is bijectivity – equal counts plus an injective map is a one-to-one correspondence however far apart the two sides sit.

The line rung has nothing below it but points, so a weld here reconciles nothing: every high-order node of a line is private, so the interiors simply concatenate. That also means moving a shared end under own= leaves the incident lines’ interiors where they were – the same caveat the rungs above carry.

Welding two chain ends together is the common case, and closing a loop is the same call with both ends stated:

ring = linemesh.attach([arc_p, arc_q], [Seam(0, "A1", 1, "A1"),
                                        Seam(0, "A2", 1, "A2")])
Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.assemble.components(mesh)[source]

The mesh split into its connected pieces – one LineMesh per group of lines reachable through shared points, in the order their first line appears.

A single chain or loop comes back as a one-element list; what makes this worth calling is the mesh that turns out to be two.

Parameters:

mesh (LineMesh)

Return type:

list[LineMesh]

nekmeshpy.linemesh.assemble.loft(points, *, loop=False, interior=None, element_tags=None, first_tag=None, last_tag=None, order=1)[source]

Loft a stack of point “profiles” into a 1-D mesh – the bottom rung of the uniform sweep primitive shared with QuadMesh.loft and HexMesh.loft.

A slice here is a single point, so the three tag arguments the upper rungs take as “one tag or an Tags over the slice” reduce to one tag each: element_tags names every lofted line, and first_tag / last_tag name the chain’s two end points.

Parameters:
  • points (NDArray[float64])

  • loop (bool)

  • interior (NDArray[float64] | None)

  • element_tags (str | None)

  • first_tag (str | None)

  • last_tag (str | None)

  • order (int)

Return type:

LineMesh

nekmeshpy.linemesh.assemble.loft_fn(f, fractions, *, loop=False, order=1, element_tags=None, first_tag=None, last_tag=None)[source]

Loft a curve given as its own analytic parametrization – loft with the profiles evaluated rather than handed in, so every node (corners and the private high-order interior) comes from calling f and nothing is ever placed on a chord.

Parameters:
  • f (Callable[[NDArray[float64]], NDArray[float64]])

  • fractions (float | NDArray[float64])

  • loop (bool)

  • order (int)

  • element_tags (str | None)

  • first_tag (str | None)

  • last_tag (str | None)

Return type:

LineMesh

nekmeshpy.linemesh.assemble.loft_spline(points, *, loop=False, element_tags=None, first_tag=None, last_tag=None, order=1)[source]

loft with the high-order nodes read off a cubic spline through the whole stack instead of the chord of their own element.

Same arguments, same numbering, same tags – and the corners are the points given, untouched, because the spline interpolates them. Only the private interior nodes differ: loft places them on the straight line between their element’s two endpoints, which is the “high order in storage, linear in geometry” trap, while here they sit on a curve fitted through the neighbouring points as well. A chain of points around a bend comes out bent rather than faceted.

Nothing is resampled: this is not a smoother, and it does not move what it was given.

Parameters:
  • points (NDArray[float64])

  • loop (bool)

  • element_tags (str | None)

  • first_tag (str | None)

  • last_tag (str | None)

  • order (int)

Return type:

LineMesh

nekmeshpy.linemesh.assemble.merge(meshes, *, tol=1e-07)[source]

Merge line meshes into one, welding coincident topological end points (the degree-1 chain ends – the 1-D analogue of the boundary vertices QuadMesh.merge/HexMesh.merge weld).

tol is a fraction of conform.bbox_scale – the largest of the x/y/z ranges over every point handed in – so the coincidence radius is tol * bbox_scale. Not a distance: if you know one, divide by conform.bbox_scale(...).

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.assemble.refine(mesh)[source]

Uniform H-refinement: split every line into 2 at its own true midpoint.

Exact at any order – the midpoint, and every child’s own curved interior, are read off the stored polynomial map via the internal core.interp order-N kernel, not a straight-line average of the corners. Line i’s new midpoint is point id mesh.n_points + i; child 2*i + q (q = 0 nearer the first corner, 1 the second) is the half of line i on that side. One call is one level – call refine again for the next.

No conformality check is needed here: unlike the quad/hex rungs, a line’s two children only touch its own corners, which are already-shared points – there is no seam between two different parent lines to verify.

Parameters:

mesh (LineMesh)

Return type:

LineMesh

nekmeshpy.linemesh.assemble.remove(mesh, which)[source]

The complement of select(): everything which does not name.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.assemble.select(mesh, which)[source]

The named lines as a curve of their own, renumbered from zero.

which is a tag string (every line carrying it), a (L,) boolean mask, or an array of line ids. Kept lines hold their relative order and their tags; points no kept line touches are dropped. The inverse of merge(), and one of the three operations that manufacture a numbering.

Parameters:
Return type:

LineMesh

linemesh.shape

Shape factories for the LineMesh rung – the ones owning a shape model rather than being generic over any input.

nekmeshpy.linemesh.shape.arc(radius, n, *, center=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0), start_theta=0.0, end_theta=3.141592653589793, element_tag='', first_tag=None, last_tag=None, order=1)[source]

An open circular arc: n line elements over n+1 points evenly spaced in angle from start_theta to end_theta on the circle of radius about center, in the plane with the given normal (default +z).

first_tag / last_tag name the two end points, which is what makes an end addressable by linemesh.attach.

Parameters:
  • radius (float)

  • n (int)

  • center (NDArray[float64])

  • normal (NDArray[float64])

  • start_theta (float)

  • end_theta (float)

  • element_tag (str)

  • first_tag (str | None)

  • last_tag (str | None)

  • order (int)

Return type:

LineMesh

nekmeshpy.linemesh.shape.arclength_fractions(f, n, *, t_range=(0.0, 1.0), samples=1001)[source]

The (n+1,) parameter values spanning t_range – from t_range[0] to t_range[1] – at which f must be evaluated for the resulting n+1 points to be evenly spaced by arc length: hand the result straight to linemesh.assemble.loft_fn as its fractions, with no further scaling (loft_fn(f, arclength_fractions(f, n, t_range=...), order=N)).

Parameters:
Return type:

NDArray[float64]

nekmeshpy.linemesh.shape.circle(radius, n, *, center=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0), start_theta=0.0, element_tag='', order=1)[source]

A closed loop of n points evenly spaced on a circle of radius about center in the plane with the given normal (default +z).

Parameters:
  • radius (float)

  • n (int)

  • center (NDArray[float64])

  • normal (NDArray[float64])

  • start_theta (float)

  • element_tag (str)

  • order (int)

Return type:

LineMesh

nekmeshpy.linemesh.shape.line(start, end, fractions, *, element_tag='', first_tag=None, last_tag=None, order=1)[source]

A straight open line from start to end sampled at normalized arc-length fractions in [0, 1] (0 = start, 1 = end): the graded-edge sibling of circle/rectangle.

first_tag / last_tag name the two end points, as on loft – a slice at this rung is a single point. That is what makes an end addressable by linemesh.attach.

Parameters:
  • start (NDArray[float64])

  • end (NDArray[float64])

  • fractions (float | NDArray[float64])

  • element_tag (str)

  • first_tag (str | None)

  • last_tag (str | None)

  • order (int)

Return type:

LineMesh

nekmeshpy.linemesh.shape.on_surface(curve, surface, *, order=1, element_tag='')[source]

Mesh a SurfaceCurve by evaluating surface on it – one element between consecutive nodes of curve.fr, exact on the surface at every node.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.shape.path_fractions(path, *, target_length=None, layers=None, fractions=None)[source]

Resolve a Path and exactly one of target_length / layers / fractions into the sweep stations themselves.

Parameters:
Return type:

NDArray[float64]

nekmeshpy.linemesh.shape.rectangle(width, height, n, *, center=(0.0, 0.0, 0.0), normal=(0.0, 0.0, 1.0), side_tags=None, element_tags=None, order=1)[source]

A closed rectangle loop of the given width x height about center in the plane with the given normal (default +z), discretized into n line elements (n must be a positive multiple of 4): n // 4 evenly spaced per side, running CCW from the lower-left corner (bottom / right / top / left), corners always landing on a point.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.shape.sweep_fractions(breaks, total_length, target)[source]

Normalized sweep stations in [0, 1] that put a node exactly on every junction of a piecewise path, subdividing each piece at roughly target.

Parameters:
Return type:

NDArray[float64]

linemesh.morph

Fixed-arity, rung-preserving LineMesh operations (delta 0).

nekmeshpy.linemesh.morph.blend(a, b, fractions)[source]

Linearly morph between two conformal profiles a and b (equal point count and identical lines connectivity – which is exactly what makes both open or both closed), one profile per fraction t with points (1-t)*a + t*b – so t=0 reproduces a and t=1 reproduces b. Each profile returned carries a’s point_tags and empty element_tags.

Parameters:
Return type:

list[LineMesh]

nekmeshpy.linemesh.morph.mirror(mesh, normal, point=array([0., 0., 0.]))[source]

A new curve reflected through the plane with normal through point.

This is the one rung where a mirror is only the coordinate map. The rungs above pair it with a re-winding, because a reflection has determinant -1 and a quad or a hex has a signed Jacobian to put back the right way round; a line element has no such sign, and the region fills take their orientation from the loop they are handed rather than from its traversal direction, so a reflected loop fills exactly as its original did. What does invert is the direction of travel – compose with reverse() if a caller depends on it.

Use it to build the half you can mesh and recover the whole: linemesh.merge([half, mirror(half, normal=(1, 0, 0))]).

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.morph.offset(mesh, distance, normal=None, crease=np.float64(0.5235987755982988))[source]

A new curve displaced by distance along its in-plane normal – the tangent (computed from the underlying GLL nodes, so high-order interior nodes follow the same rule as corners) rotated a quarter turn within the plane given by normal.

normal need not be supplied for a planar curve: it defaults to the curve’s own least-squares plane. At a point shared by more than one line (a chain’s interior corners, or a junction where several lines meet), the offset direction is the average of every incident line’s own direction at that point, renormalized – so a private high-order node (touched by exactly one line) is simply displaced by its own element’s direction.

This is the building block for skinning: loft a curve and its offset copy to get a thin, perpendicular boundary-layer quad strip.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.morph.reverse(mesh)[source]

The same curve traversed the other way, by relabelling rather than re-placing – so the high-order nodes ride along on the true geometry instead of being straight-subdivided between the reversed points.

An open chain sends point i to N-1-i. A closed one pins point 0 and sends i to (N-i) mod N instead: the first map would also rotate the ring, so line k would come back spanning k-1 -> k where it spanned k -> k+1, and every factory that meshes a loop exactly reads line k as the segment leaving point k. Both maps are involutions, so reverse is its own inverse either way.

Parameters:

mesh (LineMesh)

Return type:

LineMesh

nekmeshpy.linemesh.morph.rotate(mesh, angle, axis=array([0., 0., 1.]), center=array([0., 0., 0.]))[source]

A new curve rotated by angle radians about the line through center along axis (right-handed, axis need not be normalized). The map is rigid, so element quality is unchanged.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.morph.scale(mesh, factor, center=array([0., 0., 0.]))[source]

A new curve scaled about center by factor – a scalar (uniform) or a (3,) per-axis vector. Every factor must be positive.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.morph.transform(mesh, matrix, offset=array([0., 0., 0.]))[source]

A new curve with every node mapped through the affine p @ matrix.T + offset.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.morph.transform_fn(mesh, fn)[source]

A new curve with every node – corners and the private high-order interior alike – mapped through an arbitrary fn; transform() for a warp that is not affine. fn is called once per node block with an (N, 3) array and must return (N, 3). Connectivity and tags ride through verbatim.

This is the one rung – with PointMesh below it – where the map is only coordinates: a line element has no signed Jacobian, the same reason mirror() needs no re-winding here. A curved fn keeps a curved curve curved; a straight-subdivided one (order 1, or built from an explicit point array) stays faceted between its corners – fn moves the nodes it is given, it does not add any.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.morph.translate(mesh, vector)[source]

A new curve shifted rigidly by vector (3,). Bit-exact: the offset is added without a matmul, so translating by 0 returns the identical coordinates.

Parameters:
Return type:

LineMesh

linemesh.query

Read-only LineMesh queries – the operations that leave the ladder.

nekmeshpy.linemesh.query.bounds(mesh, *, high_order=False)[source]

The axis-aligned bounding box of the mesh’s nodes.

At high_order=False that is the corners only, which does not enclose a curved element; pass high_order=True to include the stored interior nodes. Neither bounds the polynomial itself – a curve can bulge past its own nodes – so read this as an extent, not a guarantee.

Parameters:
Return type:

Bounds

nekmeshpy.linemesh.query.boundary_elements(mesh)[source]

Sorted unique line ids with at least one degree-1 end point – the 1-D sibling of QuadMesh.boundary_elements / HexMesh.boundary_elements, and vectorized the same way.

Parameters:

mesh (LineMesh)

Return type:

NDArray[int64]

nekmeshpy.linemesh.query.boundary_points(mesh)[source]

Sorted unique topological boundary point ids: the degree-1 ends. Empty for a closed loop; the two ends for an open chain.

Parameters:

mesh (LineMesh)

Return type:

NDArray[int64]

nekmeshpy.linemesh.query.centroid(mesh, *, high_order=False)[source]

The length-weighted centroid integral x ds / integral ds – the mass property, not the mean of the points (which would weight a dense region of nodes over a long sparse one).

Parameters:
Return type:

NDArray[float64]

nekmeshpy.linemesh.query.element_blocks(mesh)[source]

(L, order+1, 3) – each line’s own node block, assembled natively from the shared corner points and this mesh’s private interior nodes. Nothing is resampled or deduplicated: the block is what the B-rep already stores, gathered into the element lattice order (start corner -> interior -> end corner).

The line rung of a query every rung above spells the same way – see quadmesh.element_blocks.

Parameters:

mesh (LineMesh)

Return type:

NDArray[float64]

nekmeshpy.linemesh.query.element_lengths(mesh, *, high_order=False)[source]

(L,) arc length of each line element.

Straight chord by default; high_order=True integrates the curve the mesh actually stores, which is the reading that differs on anything built at order > 1. The line rung of quadmesh.element_areas / hexmesh.element_volumes.

Parameters:
Return type:

NDArray[float64]

nekmeshpy.linemesh.query.length(mesh, *, high_order=False)[source]

Total arc length – element_lengths() summed.

Parameters:
Return type:

float

linemesh.tag

Vocabulary-only LineMesh operations: rename the tags, touch nothing else.

Separate from morph, which is the delta-0 rung for operations on the geometry. These change neither coordinates nor connectivity nor numbering – only what the two tag tables call things – so a caller reading the sibling list can tell at a glance that a retag cannot have moved a node.

nekmeshpy.linemesh.tag.retag_element(mesh, tags)[source]

The same mesh with its element_tags changed, by what tags is: a mapping renames the existing names (a tag the map does not name is left alone, the map applies simultaneously, a key naming nothing raises, a rename to NO_TAG untags); a string names every element; a Tags or per-element sequence replaces the table. Geometry and the side tags are untouched.

Parameters:
Return type:

LineMesh

nekmeshpy.linemesh.tag.retag_point(mesh, mapping)[source]

The same mesh with its point_tags renamed through mapping, rows kept in stored order (see retag_element() for the map’s own rules).

Renaming a tag to "" drops its rows rather than storing an empty name: a side-tag table is a named subset, so leaving it is leaving the table. That is how a boundary name that has stopped meaning anything is retired.

Parameters:
Return type:

LineMesh