Meshfree Module

GearMaster · GM_modules.Meshfree · v0.3 · Author: Simone Lucertini

Overview

The Meshfree module provides tools to generate the node (point cloud) distribution required by the Meshless Local Petrov-Galerkin (MLPG) method directly from a GM_CadModel geometry.

Unlike classical FEA, MLPG does not require a mesh. Only a scattered set of nodes with associated normals and boundary flags is needed. This module handles:

  • Extracting planar faces from a BRep (OCC) CAD model.
  • Placing boundary nodes along face edges at a uniform spacing h.
  • Filling the interior with quasi-random nodes via the Bridson Poisson Disk algorithm.
  • Saving and loading point clouds in the .gmpcloud format (with embedded display settings).
  • Visualising the cloud interactively using the PointCloudEditor (PyVista + PyQt5).
  • Generating point clouds for individual CAD entities directly from the editor GUI.
  • Defining boundary condition joints on selected CAD sub-entities via the interactive editor.
  • Packaging geometry, point cloud, and joints into a unified GM_MfreeModel container.
  • Saving and loading the complete analysis model in the .gmmfm format (single portable file).
Status 2-D planar geometry is fully supported. 3-D solid sampling is planned for a future release.

Quick Start

1 — Load a CAD model

from GM_modules.Cad import GM_CadModel

model = GM_CadModel()
model.load("my_part.step")

2 — Generate the point cloud

from GM_modules.Meshfree.src.generator import PointCloudGenerator

gen = PointCloudGenerator(h=2.0, seed=42)
cloud = gen.generate(model)
print(cloud)
# GM_PointCloud(name='my_part', dim=2, n=312, boundary=48, interior=264)

3 — Save and reload

cloud.save("my_part.gmpcloud")

from GM_modules.Meshfree.src.point_cloud import GM_PointCloud
cloud2 = GM_PointCloud.load("my_part.gmpcloud")

4 — Visualise with the interactive editor

from GM_modules.Meshfree.src.point_cloud_editor import PointCloudEditor

editor = PointCloudEditor(model, cloud)
editor.show(title="My Part — Point Cloud")

5 — Visualise with plain pyvista

import pyvista as pv

plotter = pv.Plotter()
cloud.add_to_pyvista_plotter(plotter, color="red", boundary_color="blue")
plotter.show()

6 — Build a complete meshfree model and apply joints

from GM_modules.Meshfree.src.mfree_model import GM_MfreeModel

# Wrap geometry + cloud into a unified container
mfree = GM_MfreeModel(cad_model=model, point_cloud=cloud, name="my_part")

# Open the editor with the model attached.
# Apply joints via: RMB on viewport → Apply Joint → External…
editor = PointCloudEditor(model, cloud)
editor.mfree_model = mfree
editor.show(title="My Part — Meshfree Editor")

# Save the complete model (geometry + cloud + joints) to a single file.
mfree.save("my_part.gmmfm")

# Reload later (e.g. in a solver node).
mfree2 = GM_MfreeModel.load("my_part.gmmfm")

Dependencies

PackagePurposeInstall
pythonocc-core ≥ 7.7 BRep face / edge extraction from STEP geometry conda install -c conda-forge pythonocc-core
numpy Array operations, Bridson algorithm bundled with conda base
matplotlib 2-D containment test (matplotlib.path.Path) conda install matplotlib
pyvista 3-D visualisation of the point cloud conda install -c conda-forge pyvista
joblib Parallel entity processing (optional) conda install joblib

MLPG Method — Brief Overview

The Meshless Local Petrov-Galerkin method (Atluri & Zhu, 1998) solves PDEs by defining a weak form on a local support domain around each node, without requiring a global mesh. The inputs required by any MLPG solver are:

  • Interior nodes: scattered inside the problem domain.
  • Boundary nodes: placed on the domain boundary, with associated outward normals for applying Neumann conditions.
  • A characteristic length h that controls the support radius of the basis / weight functions (e.g. MLS, Wendland RBF).

This module handles step 1 (node generation). The assembly of the local weak forms and the linear system will be implemented in a subsequent module.

Rule of thumb Choose h as roughly 1/10 of the smallest characteristic dimension of the domain. The module does not automatically scale h to the geometry — you must provide it in the same units as the CAD model (usually mm).

The GM_PointCloud Object

GM_PointCloud is the central data container. Every node is represented by aligned arrays:

AttributeShapedtypeDescription
points(N, 3)float64 3-D coordinates. For 2-D problems z = 0.
normals(N, 3)float64 Unit outward normals. Interior nodes carry the face normal.
is_boundary(N,)bool True for nodes on geometry edges.
entity_ids(N,)str (U64) ID of the CAD entity each node belongs to.
dimensionscalarint 2 or 3.
namescalarstr Human-readable label (default = model name).
display_settingsscalarPointDisplaySettings Viewport rendering options (representation, normal mode, point size, gaussian opacity/emissive). Persisted inside .gmpcloud files.

Useful methods

MethodReturnsDescription
filter_by_entity(id)GM_PointCloud Subset for a single CAD entity.
filter_boundary()GM_PointCloud Only boundary nodes.
filter_interior()GM_PointCloud Only interior nodes.
merge(clouds, name)GM_PointCloud Static method; concatenate multiple clouds.
save(path)None Write .gmpcloud archive.
load(path)GM_PointCloud Static method; read .gmpcloud archive.
to_pyvista()pyvista.PolyData Convert to pyvista point-cloud mesh.
add_to_pyvista_plotter(p, ...)None Add boundary + interior actors to an open plotter.

Samplers

All samplers derive from BaseSampler and share the same interface:

sampler = SurfaceSampler2D(h=2.0)
cloud   = sampler.sample(entity)   # entity is a CadEntity leaf

SurfaceSampler2D Implemented

Places boundary nodes uniformly along every edge of a planar face at arc-length spacing h. Uses OCC GCPnts_UniformAbscissa for accurate arc-length discretisation.

Outward in-plane normal at each node is computed as: n = T × face_normal, where T is the edge tangent in the CCW wire-traversal direction. This guarantees the correct outward direction for a standard FACE_OUTER_BOUND.

InteriorSampler2D Implemented

Fills the interior using the Fast Poisson Disk algorithm (Bridson 2007). The steps are:

  • Generate candidate nodes in the 2-D axis-aligned bounding box of the face with minimum separation h.
  • Reject candidates outside the face boundary using matplotlib.path.Path.contains_points().
  • Lift surviving 2-D points to 3-D world coordinates.
  • Assign all normals equal to the face normal (suitable for MLPG).
Reproducibility Pass seed=<int> to InteriorSampler2D (or to PointCloudGenerator) to get deterministic results across runs.

SurfaceSampler3D Planned

Will place nodes on the surface of 3-D solid / shell entities. Not yet implemented; calling sample() raises NotImplementedError.

InteriorSampler3D Planned

Will fill the interior of 3-D solids using a 3-D extension of the Bridson algorithm. Not yet implemented.

The .gmpcloud File Format

A .gmpcloud file is a ZIP archive containing five entries:

🗜 archive.gmpcloud (ZIP)
📄 manifest.json
🔢 points.npy
🔢 normals.npy
🔢 flags.npy
🔢 entity_ids.npy
EntryContent
manifest.json Metadata: format, version, name, dimension, n_points, n_boundary, n_interior. Optional "extra" key for user metadata (e.g. h value).
points.npy Node coordinates, float64, shape (N, 3).
normals.npy Unit outward normals, float64, shape (N, 3).
flags.npy uint8, shape (N, 2). Column 0 = is_boundary. Column 1 reserved.
entity_ids.npy Unicode str array (U64), shape (N,).
Compatibility The design mirrors the .gmcad format used by the CAD module — ZIP + JSON manifest + binary numpy payloads — making the pattern consistent across the GearMaster ecosystem.

GM_MfreeModel

GM_MfreeModel is the unified container that wraps all data needed for a meshfree analysis: CAD geometry, the point cloud, and boundary condition joints. It is the central object exchanged between nodes in the GearMaster Visual workflow:

CAD_Editor → Meshfree_Editor → [GM_MfreeModel] → Mfree_Solver
PropertyTypeDescription
.cad_modelGM_CadModel BRep geometry; always present.
.point_cloudGM_PointCloud | None Meshfree node distribution; None until generated.
.jointslist[GM_Joint] Boundary condition joints; empty list until defined.
.has_point_cloudbool True when a non-empty cloud is attached.
.has_jointsbool True when at least one joint is defined.

Persistence via the .gmmfm format (see below):

model.save("analysis.gmmfm")
model2 = GM_MfreeModel.load("analysis.gmmfm")

Boundary Conditions — Joints

Boundary conditions in GearMaster Meshfree are defined as Joints. A joint ties a scoping region (one or more CAD faces, edges, or vertices) to a reference point and prescribes which degrees of freedom are constrained on all cloud nodes found within that region.

Joint types

TypeDescription
Fixed All 6 DOFs (Tx, Ty, Tz, Rx, Ry, Rz) are locked. Classic fixed support.
Custom User selects which DOFs are locked via toggle buttons in the dialog: dark red = locked, grey = free.

Defining a joint in the PointCloudEditor

  1. Select one or more faces / edges / vertices on the CAD geometry in the 3-D viewport.
  2. Right-click in the viewport → Apply Joint → External…
  3. Fill in the dialog: name, type, behaviour, DOFs, and optionally adjust the reference point coordinates.
  4. Click OK. The joint appears in the Boundary Conditions panel and its actors (a violet sphere + connector lines) remain visible in the viewport.

Data stored per joint

FieldDescription
nameHuman-readable label.
scopingList of CAD sub-entities (entity id + type + index).
joint_type"Fixed" or "Custom".
behaviour"Rigid" (only option currently).
reference_point3-D coordinates [mm] of the constraint anchor (geometric centroid of scoping by default).
constrained_dofsList of locked DOF names, e.g. ["Tx","Ty","Tz","Rx","Ry","Rz"].
connected_point_indicesIndices into the cloud of all nodes belonging to the scoping region.
Live 3-D preview While the Apply Joint dialog is open a live preview is rendered in the viewport: a violet sphere marks the reference point and thin lines connect it to all scoped cloud nodes. Editing the coordinate fields updates the sphere position in real time.

The .gmmfm File Format

A .gmmfm (GearMaster Meshfree Model) file is a ZIP archive that bundles the complete analysis model — geometry, point cloud, and joints — into a single portable file.

🗜 model.gmmfm (ZIP)
📄 manifest.json
📦 geometry.gmcad
📦 pointcloud.gmpc
📄 joints.json
EntryPresenceContent
manifest.jsonAlways Format version (1.1), model name, has_point_cloud and has_joints flags.
geometry.gmcadAlways Embedded CAD model in the standard .gmcad format.
pointcloud.gmpcWhen cloud exists Embedded point cloud in the standard .gmpcloud format.
joints.jsonWhen joints exist JSON array of serialised GM_Joint objects, including connected_point_indices.
Self-contained for the solver connected_point_indices are serialised inside joints.json, so a solver can load a .gmmfm file and use the joints immediately — without reopening the editor or recomputing geometry.

API — GM_PointCloud

from GM_modules.Meshfree.src.point_cloud import GM_PointCloud

GM_PointCloud is a Python dataclass. All constructor arguments are validated in __post_init__.

Constructor

GM_PointCloud(
    points           : np.ndarray,           # (N, 3) float64
    normals          : np.ndarray,           # (N, 3) float64
    is_boundary      : np.ndarray,           # (N,)   bool
    entity_ids       : np.ndarray,           # (N,)   str
    dimension        : int = 2,              # 2 or 3
    name             : str = "point_cloud",
    display_settings : PointDisplaySettings = PointDisplaySettings(),
)

Read-only properties

cloud.n_points          # int – total node count
cloud.n_boundary        # int – boundary node count
cloud.n_interior        # int – interior node count
cloud.unique_entity_ids # list[str] – sorted unique entity identifiers

API — PointDisplaySettings

from GM_modules.Meshfree.src.point_display_settings import PointDisplaySettings

Lightweight dataclass that controls how a GM_PointCloud is rendered inside the PointCloudEditor. Persisted in manifest.json of every .gmpcloud file (format version "1.1").

Fields

FieldTypeDefaultDescription
representationstr"flat" "flat" — circular flat points rendered on the GPU (fast, zero performance cost).
"gaussian" — gaussian splat shader (smooth, halo-like blobs).
normal_modestr"none" "none" — uniform solid colour per entity.
"color" — Z-component of the surface normal mapped to a diverging coolwarm colormap.
"disc" — each point replaced by a disc glyph oriented along its normal (performance-sensitive: a warning is shown above 5 000 points).
point_sizefloat6.0 Base point size in screen pixels. Boundary nodes are rendered at point_size × 1.4.
gaussian_opacityfloat1.0 Opacity for gaussian splats, in the range [0, 1]. Has no effect in flat mode.
gaussian_emissiveboolFalse When True, gaussian splats are rendered with a glowing/emissive look. Has no effect in flat mode.

Serialisation

d        = settings.to_dict()                    # dict – JSON-serialisable
settings = PointDisplaySettings.from_dict(d)     # restore from dict (tolerant of missing keys)
Backward compatibility from_dict fills any missing keys with the field defaults, so older .gmpcloud files (format "1.0") that contain no display_settings entry are loaded without errors.

API — SurfaceSampler2D

from GM_modules.Meshfree.src.samplers.surface_sampler_2d import SurfaceSampler2D

sampler = SurfaceSampler2D(
    h     : float,             # arc-length spacing along edges
    n_max : int | None = None, # hard node limit per entity
)
cloud = sampler.sample(entity)  # entity: CadEntity leaf with planar face(s)
ParameterDefaultDescription
hrequired Spacing between consecutive boundary nodes (model units).
n_maxNone If set, randomly subsample to at most n_max nodes.

API — InteriorSampler2D

from GM_modules.Meshfree.src.samplers.interior_sampler_2d import InteriorSampler2D

sampler = InteriorSampler2D(
    h     : float,             # minimum inter-node distance
    n_max : int | None = None, # hard node limit per entity
    k     : int        = 30,   # Bridson candidate count per active sample
    seed  : int | None = None, # random seed (None = non-deterministic)
)
cloud = sampler.sample(entity)
ParameterDefaultDescription
hrequired Minimum distance between any two interior nodes.
n_maxNone Hard upper limit; subsample randomly when exceeded.
k30 Bridson algorithm candidate trials per active sample. Higher values → denser packing, slower runtime.
seedNone Integer seed for np.random.default_rng. Pass the same value to reproduce the exact same cloud.

API — PointCloudGenerator

from GM_modules.Meshfree.src.generator import PointCloudGenerator

gen = PointCloudGenerator(
    h                  : float,
    n_max_per_entity   : int | None = None,
    k                  : int        = 30,
    seed               : int | None = None,
    n_jobs             : int        = 1,
    verbose            : bool       = False,
)
cloud = gen.generate(model)         # model: GM_CadModel (loaded)
cloud = gen.generate_entity(entity) # single entity convenience
ParameterDefaultDescription
hrequired Characteristic spacing, forwarded to all samplers.
n_max_per_entityNone Applied independently to surface and interior clouds.
k30 Bridson k parameter (interior sampler).
seedNone Random seed for interior sampling.
n_jobs1 Parallel workers via joblib. -1 = all CPU cores; 1 = sequential.
verboseFalse Print progress summary after generation.
3-D entities Entities that contain no planar faces are detected as 3-D. The corresponding samplers are not yet implemented; the generator will skip them silently and emit a UserWarning.

API — I/O Functions

from GM_modules.Meshfree.src.io.gmpcloud_io import save_gmpcloud, load_gmpcloud

# Save
path = save_gmpcloud(cloud, "output.gmpcloud", extra_metadata={"h": 2.0})

# Load
cloud = load_gmpcloud("output.gmpcloud")

# Convenience wrappers on GM_PointCloud:
cloud.save("output.gmpcloud")
cloud2 = GM_PointCloud.load("output.gmpcloud")

The .gmpcloud extension is appended automatically when absent. extra_metadata is an optional dictionary stored in manifest.json under the key "extra"; it is not read back by load_gmpcloud.

Format version 1.1 Files written by this release use format "1.1". The manifest now includes a "display_settings" object that stores all PointDisplaySettings fields. Older files at format "1.0" (without "display_settings") are read transparently — missing fields fall back to the dataclass defaults.

API — PointCloudEditor

from GM_modules.Meshfree.src.point_cloud_editor import PointCloudEditor

editor = PointCloudEditor(
    model              : GM_CadModel,
    cloud              : GM_PointCloud | None = None,
    linear_deflection  : float = 0.1,
    angular_deflection : float = 0.5,
    background_color   : str   = "white",
    mode               : str   = "view",   # "view" or "edit"
)
editor.show(
    title              : str             = "",
    window_size        : tuple[int, int] = (1280, 800),
    blocking           : bool            = True,
)

PointCloudEditor extends CadEditor with a Point Cloud panel in the left sidebar. CAD geometry and point cloud share the same PyVista 3-D viewport.

Left panel — Point Cloud tree

Each loaded entity appears as a collapsible tree node with Interior and Boundary child items. Checkboxes toggle actor visibility in the viewport directly.

Meshfree menu

Menu itemDescription
Generate Point Cloud… Placeholder for future global generation over the entire model.
Point Display… Opens the Point Display dialog (enabled only when a cloud is loaded). See below for details.

Per-entity generation — CAD tree right-click

Right-click any leaf entity in the CAD feature tree and choose Generate Point Cloud.

  • A dialog asks for the node spacing h (model units).
  • If a cloud already exists for that entity, a confirmation dialog offers OK (overwrite) or Cancel.
  • The new entity sub-cloud is merged into the global GM_PointCloud and the viewport updates immediately.
  • Colour assignment cycles through a fixed palette and remains stable across overwrite operations.

Point Display dialog

Accessed via Meshfree → Point Display….

GroupControlsEffect
Point Representation Radio buttons: Flat / Gaussian Switches between GPU circular points and gaussian splat shader.
Gaussian Splats Settings Opacity slider (0–100 %) + Emissive checkbox
(shown only when Gaussian is selected)
Controls transparency and glowing appearance of gaussian splats.
Normal Display Radio buttons: None / Color / Disc None: uniform colour.
Color: maps normal Z to a diverging colormap.
Disc: glyph discs oriented along normals. A performance warning is shown when the cloud exceeds 5 000 points.

The dialog has three buttons:

  • Apply — applies settings immediately; dialog stays open for further adjustments.
  • OK — applies settings and closes the dialog.
  • Cancel — reverts to the settings that were active when the dialog was opened, then closes.
Settings persistence Display settings are stored in cloud.display_settings and saved automatically when the cloud is written to a .gmpcloud file. They are restored when the file is reloaded.

Left panel — Boundary Conditions tree

Below the Point Cloud panel a second tree widget lists all joints under a collapsible “Joints” root node. Each joint entry shows:

  • Scoping — summary of selected sub-entities (e.g. 2 faces).
  • TypeFixed or Custom.
  • BehaviourRigid.
  • Ref. point — coordinates of the reference point.
  • Ref. point ID / Constrained DOFs / Connected points — read-only computed values (rendered in grey).

Joint context menu (RMB in BC tree)

TargetActionEffect
“Joints” root item Remove All Joints Deletes all joints from the model and removes their viewport actors.
Specific joint item Edit Joint Reopens the configuration dialog pre-filled with the joint’s current values. Live preview is active during editing.
Specific joint item Remove Joint Deletes the joint from the model and removes its viewport actors.

Apply Joint dialog

Opened via RMB on the viewport → Apply Joint → External… (requires a selection).

FieldDescription
NameJoint label; auto-incremented default provided.
Joint TypeFixed or Custom. Selecting Custom activates DOF toggle buttons.
BehaviourCurrently only Rigid.
Reference PointX / Y / Z [mm]; pre-filled with the geometric centroid of the scoping. Fully editable; live preview updates on every valid keystroke.
Constrained DOFs Fixed: all 6 pills shown in dark red (read-only).
Custom: each button toggles — dark red = locked, grey = free.

API — GM_MfreeModel

from GM_modules.Meshfree.src.mfree_model import GM_MfreeModel

Constructor

GM_MfreeModel(
    cad_model   : GM_CadModel,
    point_cloud : GM_PointCloud | None = None,
    name        : str                  = "",
)

Joint management

model.add_joint(joint)            # add a GM_Joint (raises ValueError on duplicate id)
model.remove_joint(joint_id)      # remove by id (raises KeyError if not found)
joint = model.get_joint(joint_id) # retrieve by id (raises KeyError if not found)
model.joints                      # list[GM_Joint] – copy of the internal list
model.has_joints                  # bool

Persistence

path  = model.save("analysis.gmmfm")   # returns resolved Path
model = GM_MfreeModel.load("analysis.gmmfm")

API — GM_Joint

from GM_modules.Meshfree.src.joints import GM_Joint, ScopingItem

Constructor

GM_Joint(
    name       : str,
    scoping    : list[ScopingItem],
    joint_type : str         = "Fixed",      # "Fixed" | "Custom"
    behaviour  : str         = "Rigid",
    joint_id   : str | None  = None,         # auto-generated 8-char hex if None
)

Key attributes

AttributeTypeDescription
idstrUnique 8-char uppercase hex identifier.
reference_pointnp.ndarray (3,) | None Constraint anchor [mm]. Populated by the editor after geometry computation.
constrained_dofslist[str] Locked DOFs. Default all 6: ["Tx","Ty","Tz","Rx","Ry","Rz"].
connected_point_indicesnp.ndarray (K,) int | None Indices of cloud nodes in the scoping region. Set by the editor; serialised to .gmmfm.

Serialisation

d     = joint.to_dict()        # JSON-serialisable dict
joint = GM_Joint.from_dict(d)  # reconstruct (tolerant of missing keys)

ScopingItem

ScopingItem(
    entity_id        : str,   # CAD entity identifier
    sub_entity_type  : str,   # "face" | "edge" | "vertex"
    sub_entity_index : int,
)

Module File Structure

GM_modules/Meshfree/
├── __init__.py
├── src/
│   ├── __init__.py                # exports GM_PointCloud, PointCloudGenerator, GM_MfreeModel
│   ├── mfree_model.py             # GM_MfreeModel container
│   ├── point_cloud.py             # GM_PointCloud dataclass
│   ├── point_display_settings.py  # PointDisplaySettings dataclass
│   ├── generator.py               # PointCloudGenerator orchestrator
│   ├── joints.py                  # GM_Joint, ScopingItem, JOINT_TYPES, BEHAVIOURS
│   ├── joint_dialog.py            # Qt dialog for boundary condition joints
│   ├── point_cloud_editor.py      # PointCloudEditor + _PointCloudEditorWindow
│   ├── samplers/
│   │   ├── __init__.py
│   │   ├── base_sampler.py        # BaseSampler ABC + OCC helpers
│   │   ├── surface_sampler_2d.py  # boundary nodes on planar edges
│   │   ├── interior_sampler_2d.py # interior nodes (Bridson + rejection)
│   │   ├── surface_sampler_3d.py  # PLACEHOLDER
│   │   └── interior_sampler_3d.py # PLACEHOLDER
│   └── io/
│       ├── __init__.py
│       ├── gmpcloud_io.py         # save / load .gmpcloud
│       └── gmmfm_io.py            # save / load .gmmfm
├── tests/
│   ├── test_point_cloud_io.py     # 37 tests
│   ├── test_samplers_2d.py        # 49 tests
│   └── test_generator.py          # 30 tests
├── examples/
│   └── example_generate_point_cloud.py
└── docs/
    └── manual.html   ← this file

Roadmap

FeatureStatus
GM_PointCloud dataclass + I/O (.gmpcloud) Done
SurfaceSampler2D (boundary nodes, planar faces) Done
InteriorSampler2D (Bridson + rejection, planar faces) Done
PointCloudGenerator (orchestrator + joblib parallelism) Done
PointCloudEditor interactive GUI (per-entity generation, display settings) Done
GM_MfreeModel container + .gmmfm I/O Done
Boundary conditions — Joints (Fixed / Custom DOF) in PointCloudEditor Done
BC panel: RMB edit/remove, Custom DOF toggle, live 3-D preview Done
SurfaceSampler3D (nodes on 3-D solid surfaces) Planned
InteriorSampler3D (3-D Poisson disk) Planned
MLPG solver (local weak form assembly) Planned