Physics-guided#

Adding inductive bias to the model training can be useful to improve the generalization capability of the model. For Physics-AI, one way to do this is via designing neural network architectures that are specifically designed to operate on data encountered in this domain. FNOs, that use FFTs, Mesh Graph Networks that operate on directly on simulation meshes, DoMINO model that operates on point clouds and has stencils similar to some numerical methods are a great way to introduce inductive bias.

Additionally, one can also add the governing equations as loss functions to further regularize the model predictions to obey the physics of the problem.

In this tutorial, we’ll learn how to add physics-based loss terms to your model training using PhysicsNeMo. The physicsnemo.sym namespace — upstreamed in v2.0 from the PhysicsNeMo-Sym repository, which has since been archived — provides the symbolic PDE definition, automatic spatial derivative computation, and physics-informed residual evaluation utilities used in this guide. It is installed via pip install "nvidia-physicsnemo[sym]". We will explore the relevant utilities below, followed by sample end-to-end training workflows.

Note

If you are migrating an existing physicsnemo-sym project that used the Solver / Domain / Constraint / Geometry abstractions, the PhysicsNeMo v2.0 Migration Guide describes the mapping to the explicit PyTorch loop used throughout this tutorial.

Adding Physics-based Losses#

Many AI models trained on physical data are designed to minimize the difference between the model predictions and the true data. Typical loss functions used for this purpose include MSE, RMSE, MAE, etc. Choosing the right loss function is an important step for AI model training, and there is a lot of active research in this area. For Physics-AI applications, we can take this idea even further and craft loss functions that are suitable for the data. More specifically, the data used to train models in the Physics-AI space typically comes from experimental measurements or from results of a different numerical method. The system being studied is usually governed by physical laws (such as conservation of mass, energy, etc.), and these methods or measurements aim to satisfy these constraints. Adding these governing laws or equations as loss functions can help the neural network satisfy these equations better and make the predictions more physically interpretable.

Let’s look at an example from the molecular dynamics domain. Assume a system of molecules at equilibrium. Suppose we are training a neural network to predict the forces on each molecule given the positions of the molecules. Since the system is in equilibrium, we want to enforce that the total sum of all the forces on each of the molecules is zero. This can be added as a constraint simply by doing the following:

...
# Assume a model forward pass
# Model here is an mesh graph net and the system of molecules is represented
# as a graph.
# model outputs forces at each node (molecule) as a tensor of shape (N,3)
# where N is the number of molecules in the system
out = model(node_features, edge_features, input_graph)
loss_data = torch.nn.functional.l1_loss(out, true_out)                              # Regression / Data loss
loss_physics = (1 / torch.shape(out)[0]) * torch.sum(torch.sum(out, dim=1)).abs()   # Sum of all forces, can also be written as torch.mean(out).abs()
# define a lagrange multiplier / weight for the physics loss
physics_weight = 0.001
loss = loss_data + physics_weight * loss_physics
loss.backward()
optimizer.step()
...

Adding this, enforces an equilibrium condition on the model’s prediction and is a demonstration of how physics knowledge can be incorporated in your training workflow. A full example using this loss can be found in the Molecular Dynamics Example

Adding PDE losses#

Sometimes, the governing equations for the problem can include Partial Differential Equations. This is especially true for problems in physics and other scientific domains. Computing local residuals of these PDEs on a model predictions involves computing spatial and temporal gradients and the methods used can vary based on the model architecture.

physicsnemo.sym aims to simplify this process. The PhysicsInformer utility (physicsnemo.sym.eq.phy_informer.PhysicsInformer) can compute the PDE losses on point clouds, grids, or graphs/meshes using techniques such as Automatic Differentiation, Finite Difference, Meshless Finite Difference, Least Squares, and Spectral Differentiation.

The PhysicsInformer class requires equations to compute as a parameter. The equation must be a physicsnemo.sym.eq.pde.PDE subclass. Custom PDEs are also supported.

Some general comments for the PhysicsInformer:

  • Using PhysicsInformer, you can physics-inform almost any model architecture (point-cloud based, grid based, graph based, etc.), e.g. MLPs, DeepONets, FNOs, CNNs, Diffusion Models, Graph networks, etc.

  • The utility supports gradients via Automatic Differentiation, Spectral, Finite Difference, Meshless Finite Difference and Least Squares methods. (See following section for more info)

  • Given the PDE governing equations and required_outputs, this utility constructs the computational graph, and computes the gradients efficiently, to output the residuals.

  • This utility simplifies the spatial derivative computation. If using transient PDEs, the temporal derivative will have to be computed separately and passed as an input to the forward() call. Refer to the physicsnemo.sym API docs for more details.

  • Different spatial gradient computing methods require different inputs to the forward() call. To identify the inputs that need to be passed to the forward() call, you can access the value of .required_inputs property.

The various spatial derivative methods and their applicability based on the model output type can be summarized below. Each grad_method string passed to PhysicsInformer selects an internal Gradients* module documented in the physicsnemo.sym API reference:

  • "autodiff"GradientsAutoDiff. Suitable for outputs from models which are differentiable w.r.t. their inputs. Model inputs must include coordinates. Ideal for MLP-type architectures, and can operate on point clouds, structured grids, or unstructured meshes as long as the differentiability and input constraints are satisfied. Computationally expensive but more accurate than other numerical gradient methods.

  • "meshless_finite_difference"GradientsMeshlessFiniteDifference. Numerical gradient method suitable for models that can predict field values on stencil points in addition to the original points. The points can come from either a point cloud or grid (structured or unstructured); point clouds are the most suitable applications. Fast, but can present numerical instability if fd_dx is too small.

  • "finite_difference"GradientsFiniteDifference. Numerical gradient method suitable for models that output predictions on structured grids with uniform spacing (each dimension can have a different spacing of its own).

  • "spectral"GradientsSpectral. Numerical gradient method suitable for models that output predictions on structured grids with uniform spacing (each dimension can have a different spacing of its own) and have periodic boundaries.

  • "least_squares"GradientsLeastSquares. Numerical gradient method most suitable for models that predict output on unstructured grids or structured grids with non-uniform spacing.

Defining PDEs#

PDEs in PhysicsNeMo are defined by subclassing physicsnemo.sym.eq.pde.PDE and declaring the equations symbolically with SymPy. The class populates a self.equations dictionary mapping a residual name to a SymPy expression; PhysicsInformer then handles the per-term spatial-derivative computation at training time.

Note

In PhysicsNeMo v2.0, pre-built PDE classes (NavierStokes, AdvectionDiffusion, Diffusion, LinearElasticity, WaveEquation, etc.) are no longer shipped. The recommended pattern is to define the equations you need inline in your training script, which keeps the governing physics explicit and easy to edit. See the PhysicsNeMo v2.0 Migration Guide for the rationale and a side-by-side mapping.

The example below defines incompressible 3D Navier–Stokes inline. The per-gradient-method examples that follow all reuse this same class; for a 2D variant, drop the z symbol and the momentum_z residual.

from sympy import Symbol, Function, Number
from physicsnemo.sym.eq.pde import PDE


class NavierStokes(PDE):
    """Incompressible Navier-Stokes (steady, 3D, rho=1)."""

    def __init__(self, nu=0.01):
        self.dim = 3
        x, y, z = Symbol("x"), Symbol("y"), Symbol("z")
        u = Function("u")(x, y, z)
        v = Function("v")(x, y, z)
        w = Function("w")(x, y, z)
        p = Function("p")(x, y, z)
        nu = Number(nu)

        laplacian = lambda f: f.diff(x, 2) + f.diff(y, 2) + f.diff(z, 2)
        advection = lambda f: u * f.diff(x) + v * f.diff(y) + w * f.diff(z)

        self.equations = {
            "continuity": u.diff(x) + v.diff(y) + w.diff(z),
            "momentum_x": advection(u) + p.diff(x) - nu * laplacian(u),
            "momentum_y": advection(v) + p.diff(y) - nu * laplacian(v),
            "momentum_z": advection(w) + p.diff(z) - nu * laplacian(w),
        }

For complete, runnable PINN training scripts that define their PDEs inline in this style, see:

  • LDC PINN example — forward, purely physics-driven Navier–Stokes on a unit-square cavity.

  • Inverse PINN example — recovers unknown advection–diffusion coefficients from data, and shows how to define a coefficient as a symbolic field (Function) so the network can supply its values at training time.

The physicsnemo.sym API reference documents the PDE base class and its members.

Computing PDE losses using Automatic Differentiation#

The code below shows an example of using the autodiff method to compute the residuals. A few things to note when using the autodiff method:

  • Ensure the model is differentiable enough for the PDE being used. - C1Continuous for a First-Order PDE - C2Continuous for a Second-Order PDE - …

  • E.g. a model that uses ReLU activation function will have it’s second derivatives zero. So using automatic differentiation based gradients is not recommended.

  • For all spatial coordinate tensors (e.g., x, y, and z), call the method x.requires_grad_(True) to enable gradient tracking.

  • Coordinates is a tensor of shape (N, D) shaped tensor, where D is the number of spatial dimensions.

  • This method is accurate but more computationally expensive compared to some other numerical methods due to automatic differentiation.

import torch
import numpy as np
from physicsnemo.sym.eq.phy_informer import PhysicsInformer
# NavierStokes class as defined in "Defining PDEs" section above


class Model(torch.nn.Module):
    """Define a dummy model"""
    def __init__(self):
        super(Model, self).__init__()

    def forward(self, x_input):
        x, y, z = x_input[:, 0:1], x_input[:, 1:2], x_input[:, 2:3]

        # compute u, v, w, p
        u = x * y * z
        v = x * y ** 2 * z
        w = x ** 2 * y * z
        p = x * y * z ** 2

        return torch.cat([u, v, w, p], dim=1)

steps = 100
x = torch.linspace(0, 2 * np.pi, steps=steps).requires_grad_(True)  # requires_grad_ is set to True to enable Automatic Differentiation
y = torch.linspace(0, 2 * np.pi, steps=steps).requires_grad_(True)
z = torch.linspace(0, 2 * np.pi, steps=steps).requires_grad_(True)
xx, yy, zz = torch.meshgrid(x, y, z, indexing="ij")

# instantiate model
model = Model()

# use the inline-defined NavierStokes PDE
ns = NavierStokes(nu=0.01)
coords = torch.stack([xx, yy, zz], dim=-1).reshape(-1, 3)   # Coords shape: (1000000, 3)

# instantiate PhysicsInformer with autodiff method.
# choosing NavierStokes PDE will enable us to query continuity, and momentum in x, y, z directions
phy_informer = PhysicsInformer(
    required_outputs=["continuity", "momentum_x"],
    equations=ns,
    grad_method="autodiff",
    device=coords.device,
)

# model forward pass
# this needs to be differentiable as explained above for auto-diff gradients to work
# if the model does not satisfy these requirements, follow along this tutorial to
# see numerical ways to compute the derivatives.
out = model(coords)

# compute the residuals
# this returns a dict containing tensors for required_outputs
residuals = phy_informer.forward(
    {
        "coordinates": coords,
        "u": out[:, 0:1],
        "v": out[:, 1:2],
        "w": out[:, 2:3],
        "p": out[:, 3:4],
    },
)

A full example using this loss can be found in the Physics Informed Darcy Flow Example

Computing PDE losses using Mesh-less Finite Difference#

The code below shows an example of using the meshless_finite_difference method to compute the residuals. A few things to note when using the meshless_finite_difference method:

  • In addition to the outputs at the original data points, outputs are needed on the stencil points. The user is responsible for evaluating the model at the shifted coordinates and supplying those values to PhysicsInformer.forward under the names it expects. Stencil points follow the convention "u>>x::1" = u(i+1, j), "u>>x::-1" = u(i-1, j), "u>>x::1&&y::1" = u(i+1, j+1), "u>>x::-1&&y::-1" = u(i-1, j-1), and so on. To identify the exact inputs PhysicsInformer expects in forward(), read its .required_inputs property.

  • fd_dx is a hyperparameter. Smaller value typically yields more accurate gradients, but can lead to numerical instability. A value of 0.001 is a good value to start, assuming the variation of spatial coordinates in the problem is $mathcal{O}(1)$.

import torch
import numpy as np
from physicsnemo.sym.eq.phy_informer import PhysicsInformer
# NavierStokes class as defined in "Defining PDEs" section above


class Model(torch.nn.Module):
    """Define a dummy model"""
    def __init__(self):
        super(Model, self).__init__()

    def forward(self, x_input):
        x, y, z = x_input[:, 0:1], x_input[:, 1:2], x_input[:, 2:3]

        # compute u, v, w, p
        u = x * y * z
        v = x * y ** 2 * z
        w = x ** 2 * y * z
        p = x * y * z ** 2

        return torch.cat([u, v, w, p], dim=1)

steps = 100
x = torch.linspace(0, 2 * np.pi, steps=steps)
y = torch.linspace(0, 2 * np.pi, steps=steps)
z = torch.linspace(0, 2 * np.pi, steps=steps)
xx, yy, zz = torch.meshgrid(x, y, z, indexing="ij")

# instantiate model
model = Model()

# use the inline-defined NavierStokes PDE
ns = NavierStokes(nu=0.01)
coords = torch.stack([xx, yy, zz], dim=-1).reshape(-1, 3)   # Coords shape: (1000000, 3)

# instantiate PhysicsInformer with meshless_finite_difference method.
phy_informer = PhysicsInformer(
    required_outputs=["continuity", "momentum_x"],
    equations=ns,
    grad_method="meshless_finite_difference",
    fd_dx=0.001,
    device=coords.device,
)

# model forward pass at the original data points
out = model(coords)

# Evaluate the model at stencil points (axis-aligned ±dx offsets).
# In a real PINN this is just six additional model calls; here we build a
# tiny helper to keep the example compact.
dx = 0.001

def shift(coords, axis: int, sign: int):
    offset = torch.zeros_like(coords)
    offset[:, axis] = sign * dx
    return model(coords + offset)

po_posx, po_negx = shift(coords, 0, +1), shift(coords, 0, -1)
po_posy, po_negy = shift(coords, 1, +1), shift(coords, 1, -1)
po_posz, po_negz = shift(coords, 2, +1), shift(coords, 2, -1)

# compute the residuals
# pass all the variables computed on stencil points
# this returns a dict containing tensors for required_outputs
residuals = phy_informer.forward(
    {
        "u": out[:, 0:1],
        "v": out[:, 1:2],
        "w": out[:, 2:3],
        "p": out[:, 3:4],
        "u>>x::1": po_posx[:, 0:1],
        "v>>x::1": po_posx[:, 1:2],
        "w>>x::1": po_posx[:, 2:3],
        "p>>x::1": po_posx[:, 3:4],
        "u>>x::-1": po_negx[:, 0:1],
        "v>>x::-1": po_negx[:, 1:2],
        "w>>x::-1": po_negx[:, 2:3],
        "p>>x::-1": po_negx[:, 3:4],
        "u>>y::1": po_posy[:, 0:1],
        "v>>y::1": po_posy[:, 1:2],
        "w>>y::1": po_posy[:, 2:3],
        "p>>y::1": po_posy[:, 3:4],
        "u>>y::-1": po_negy[:, 0:1],
        "v>>y::-1": po_negy[:, 1:2],
        "w>>y::-1": po_negy[:, 2:3],
        "p>>y::-1": po_negy[:, 3:4],
        "u>>z::1": po_posz[:, 0:1],
        "v>>z::1": po_posz[:, 1:2],
        "w>>z::1": po_posz[:, 2:3],
        "p>>z::1": po_posz[:, 3:4],
        "u>>z::-1": po_negz[:, 0:1],
        "v>>z::-1": po_negz[:, 1:2],
        "w>>z::-1": po_negz[:, 2:3],
        "p>>z::-1": po_negz[:, 3:4],
    },
)

Computing PDE losses using Finite Difference#

The code below shows an example of using the finite_difference method to compute the residuals. A few things to note when using the finite_difference method:

  • This method uses the second-order central finite difference scheme to compute the gradients on a structured grid.

  • fd_dx parameter is based on the grid spacing of the input.

import torch
import numpy as np
from physicsnemo.sym.eq.phy_informer import PhysicsInformer
# NavierStokes class as defined in "Defining PDEs" section above


class Model(torch.nn.Module):
    """Define a dummy model"""
    def __init__(self):
        super(Model, self).__init__()

    def forward(self, x_input):
        x, y, z = x_input[:, 0:1], x_input[:, 1:2], x_input[:, 2:3]

        # compute u, v, w, p
        u = x * y * z
        v = x * y ** 2 * z
        w = x ** 2 * y * z
        p = x * y * z ** 2

        return torch.cat([u, v, w, p], dim=1)

steps = 100
x = torch.linspace(0, 2 * np.pi, steps=steps)
y = torch.linspace(0, 2 * np.pi, steps=steps)
z = torch.linspace(0, 2 * np.pi, steps=steps)
xx, yy, zz = torch.meshgrid(x, y, z, indexing="ij")

# instantiate model
model = Model()

# use the inline-defined NavierStokes PDE
ns = NavierStokes(nu=0.01)
coords = torch.stack([xx, yy, zz], dim=0).unsqueeze(0)  # Coords shape: (1, 3, 100, 100, 100)

# instantiate PhysicsInformer with finite_difference method.
phy_informer = PhysicsInformer(
    required_outputs=["continuity", "momentum_x"],
    equations=ns,
    grad_method="finite_difference",
    fd_dx=(2 * np.pi / steps),  # computed based on the grid spacing
    device=coords.device,
)

# model forward pass
out = model(coords)

# compute the residuals
# this returns a dict containing tensors for required_outputs
residuals = phy_informer.forward(
    {
        "u": out[:, 0:1],
        "v": out[:, 1:2],
        "w": out[:, 2:3],
        "p": out[:, 3:4],
    },
)

A full example using this loss can be found in the Physics Informed Darcy Flow Example

Computing PDE losses using Spectral Derivatives#

The code below shows an example of using the spectral method to compute the residuals. A few things to note when using the spectral method:

  • This method works well for periodic domains, while for non-periodic domains, it is known to produce artifacts at the boundaries. Appropriate padding is required.

  • bounds parameter is based on the size of the domain.

import torch
import numpy as np
from physicsnemo.sym.eq.phy_informer import PhysicsInformer
# NavierStokes class as defined in "Defining PDEs" section above


class Model(torch.nn.Module):
    """Define a dummy model"""
    def __init__(self):
        super(Model, self).__init__()

    def forward(self, x_input):
        x, y, z = x_input[:, 0:1], x_input[:, 1:2], x_input[:, 2:3]

        # compute u, v, w, p
        u = x * y * z
        v = x * y ** 2 * z
        w = x ** 2 * y * z
        p = x * y * z ** 2

        return torch.cat([u, v, w, p], dim=1)

steps = 100
x = torch.linspace(0, 2 * np.pi, steps=steps)
y = torch.linspace(0, 2 * np.pi, steps=steps)
z = torch.linspace(0, 2 * np.pi, steps=steps)
xx, yy, zz = torch.meshgrid(x, y, z, indexing="ij")

# instantiate model
model = Model()

# use the inline-defined NavierStokes PDE
ns = NavierStokes(nu=0.01)
coords = torch.stack([xx, yy, zz], dim=0).unsqueeze(0)  # Coords shape: (1, 3, 100, 100, 100)

# instantiate PhysicsInformer with spectral method.
phy_informer = PhysicsInformer(
    required_outputs=["continuity", "momentum_x"],
    equations=ns,
    grad_method="spectral",
    bounds=[2 * np.pi, 2 * np.pi, 2 * np.pi],
    device=coords.device,
)

# model forward pass
out = model(coords)

# compute the residuals
# this returns a dict containing tensors for required_outputs
residuals = phy_informer.forward(
    {
        "u": out[:, 0:1],
        "v": out[:, 1:2],
        "w": out[:, 2:3],
        "p": out[:, 3:4],
    },
)

Computing PDE losses using Least-Squares Method#

The code below shows an example of using the least_squares method to compute the residuals. A few things to note when using the least_squares method:

  • This method is designed to compute gradients for unstructured meshes / grids.

  • All gradient and residual quantities are computed on the node points.

  • This method also requires connectivity information, which can typically be pre-computed. Alternatively, you can also use physicsnemo.sym.eq.spatial_grads.spatial_grads.compute_connectivity_tensor function to compute the connectivity tensor.

import torch
import numpy as np
from physicsnemo.sym.eq.phy_informer import PhysicsInformer
# NavierStokes class as defined in "Defining PDEs" section above


class Model(torch.nn.Module):
    """Define a dummy model"""
    def __init__(self):
        super(Model, self).__init__()

    def forward(self, x_input):
        x, y, z = x_input[:, 0:1], x_input[:, 1:2], x_input[:, 2:3]

        # compute u, v, w, p
        u = x * y * z
        v = x * y ** 2 * z
        w = x ** 2 * y * z
        p = x * y * z ** 2

        return torch.cat([u, v, w, p], dim=1)

steps = 100
x = torch.linspace(0, 2 * np.pi, steps=steps)
y = torch.linspace(0, 2 * np.pi, steps=steps)
z = torch.linspace(0, 2 * np.pi, steps=steps)
xx, yy, zz = torch.meshgrid(x, y, z, indexing="ij")

# instantiate model
model = Model()

# use the inline-defined NavierStokes PDE
ns = NavierStokes(nu=0.01)
coords = torch.stack([xx, yy, zz], dim=-1).reshape(-1, 3)  # Coords shape: (1000000, 3)

# Sample code to compute node ids and edges. This information is typically
# available from the mesh / graph representation.
edge_ids = []
if steps > 1:
    # Edges in the i-direction
    edges_i = torch.stack([index[: -steps * steps], index[steps * steps :]], dim=1)
    edge_ids.append(edges_i)

    # Edges in the j-direction
    edges_j = torch.stack([index[:-steps], index[steps:]], dim=1)
    edge_ids.append(edges_j)

    # Edges in the k-direction
    edges_k = torch.stack([index[:-1], index[1:]], dim=1)
    edge_ids.append(edges_k)

edge_ids = torch.cat(edge_ids).to(device)

node_ids = torch.arange(coords_unstructured.size(0)).reshape(-1, 1).to(device)

# instantiate PhysicsInformer with least_squares method.
phy_informer = PhysicsInformer(
    required_outputs=["continuity", "momentum_x"],
    equations=ns,
    grad_method="least_squares",
    bounds=[2 * np.pi, 2 * np.pi, 2 * np.pi],
    device=coords.device,
    compute_connectivity=True   # Compute connectivity using the node and edge information
)

# model forward pass
out = model(coords)

# compute the residuals
# pass the connectivity information
# this returns a dict containing tensors for required_outputs
residuals = phy_informer.forward(
    {
        "coordinates": coords,
        "nodes": node_ids,  # can be obtained from the graph representation, eg. graph.nodes()
        "edges": edge_ids,  # can be obtained from the graph representation, eg. graph.edges()
        "u": out[:, 0:1],
        "v": out[:, 1:2],
        "w": out[:, 2:3],
        "p": out[:, 3:4],
    },
)

A full example using this loss can be found in the Stokes Flow Example

Using the gradients directly#

If you only need access to spatial gradients without the need to compute the residuals, you can use the physicsnemo.sym.eq.gradients.GradientCalculator directly. Refer to the physicsnemo.sym API docs for more details.

Using geometry information#

PhysicsNeMo provides several ways to incorporate geometry information into your training pipelines — for example, computing signed distance fields for implicit geometry representation, or sampling point clouds for boundary and interior constraints.

Computing Signed Distance Fields#

Mathematically, the signed distance field (SDF) is defined as the orthogonal distance from a given point to the nearest boundary or surface of a geometric shape. It is widely used to describe geometry in mathematics, rendering, and similar applications. In physics-informed learning, it is also used as a geometric input to neural networks.

The recommended path for computing SDFs in PhysicsNeMo v2.0 is the physicsnemo.nn.functional.signed_distance_field functional, a Warp-backed, torch.autograd-compatible operator that returns both the signed distance and the corresponding closest hit point on the mesh. It pairs naturally with PyVista for STL/mesh I/O:

import torch
import pyvista as pv
from physicsnemo.nn.functional import signed_distance_field

# Download the Stanford Bunny STL from https://commons.wikimedia.org/wiki/File:Stanford_Bunny.stl
mesh = pv.read("Stanford_Bunny.stl")

# Convert to the (n_vertices, 3) and (n_faces, 3) tensors expected by the functional
mesh_vertices = torch.as_tensor(mesh.points, dtype=torch.float32, device="cuda")
mesh_indices = torch.as_tensor(
    mesh.faces.reshape(-1, 4)[:, 1:], dtype=torch.int32, device="cuda"
)

# Query points (here, a single point at the origin)
query_points = torch.zeros((1, 3), dtype=torch.float32, device="cuda")

# Returns (sdf, hit_points)
sdf, hit_points = signed_distance_field(mesh_vertices, mesh_indices, query_points)

A few examples using SDF during training / inference can be found in the External Aerodynamics using DoMINO Example and the Datacenter CFD example.

Geometry primitives, tessellation, and point-cloud sampling#

Note

Geometry-module utilities from the legacy physicsnemo-sym repository — CSG primitives, Tessellation, GeometryDatapipe, and the sample_interior() / sample_boundary() helpers — are not part of the upstreamed physicsnemo.sym. For new workflows, use the PhysicsNeMo-Mesh module together with PyVista for STL I/O, surface/volume sampling, and point-cloud generation. If you have an existing project that depends on the old geometry abstractions, see the archived PhysicsNeMo-Sym repository and the PhysicsNeMo v2.0 Migration Guide for the recommended replacements.