Skip to content

Domain & Geometry

The jno.domain class manages mesh generation, physical group labelling, and sampling of collocation points. It wraps pygmsh for mesh generation and meshio for I/O.


Creating a Domain

From a Built-in Geometry

Pass a geometry constructor to jno.domain(constructor=...):

import jno

domain = jno.domain(constructor=jno.domain.rect(mesh_size=0.05))

From an Existing Mesh File

domain = jno.domain('./mesh.msh')

Built-in Geometry Constructors

All constructors are static methods on jno.domain (equivalently Geometries).

1D

line

jno.domain.line(x_range=(0, 1), mesh_size=0.1)

Physical groups: "interior", "left", "right", "boundary".


2D

rect

Unstructured triangular mesh generated by pygmsh.

jno.domain.rect(x_range=(0,1), y_range=(0,1), mesh_size=0.1)

Physical groups: "interior", "boundary", "bottom", "top", "left", "right".

equi_distant_rect

Structured (equidistant) triangular mesh — avoids mesh randomness.

jno.domain.equi_distant_rect(x_range=(0,1), y_range=(0,1), nx=20, ny=20)

Physical groups: same as rect.

disk

Circular domain (polygon approximation).

jno.domain.disk(center=(0,0), radius=1.0, mesh_size=0.1, num_points=32)

Physical groups: "interior", "boundary".

l_shape

L-shaped domain.

jno.domain.l_shape(size=1.0, mesh_size=0.1, separate_boundary=False)

Physical groups: "interior", "boundary".
With separate_boundary=True: also "bottom", "right_lower", "inner_horizontal", "inner_vertical", "top", "left".

rectangle_with_hole

Rectangle with a centred rectangular hole (hollow domain).

jno.domain.rectangle_with_hole(outer_size=1.0, hole_size=0.4, mesh_size=0.1, separate_boundary=False)

Physical groups: "interior", "boundary".
With separate_boundary=True: outer sides + "_bottom", "_right", "_top", "_left", "_boundary" for the hole.

rectangle_with_holes

Rectangle with multiple user-defined holes.

holes = [
    {"origin": (0.3, 0.3), "size": (0.2, 0.2), "type": "obstacle"},
    {"origin": (0.7, 0.3), "size": (0.15, 0.3), "type": "heater"},
]
jno.domain.rectangle_with_holes(outer_size=(2.0,1.0), holes=holes, mesh_size=0.05)

Physical groups: "interior", "boundary", "hole_boundary".
With separate_boundary=True: outer sides + per-hole sides (e.g. "obstacle_boundary", "heater_bottom", …).

rect_pml

Rectangle with top and bottom Perfectly-Matched-Layer (PML) absorbing regions — useful for wave equations.

jno.domain.rect_pml(
    x_range=(0,1), y_range=(0,1),
    mesh_size=0.1,
    pml_thickness_top=0.2,
    pml_thickness_bottom=0.2,
)

Physical groups: "interior", "pml_bottom", "pml_top", "boundary", "bottom", "top", "left", "right".


3D

cube

jno.domain.cube(x_range=(0,1), y_range=(0,1), z_range=(0,1), mesh_size=0.1)

Physical groups: "interior", "boundary", "bottom", "top", "front", "back", "left", "right".


Sampling Variables

from jax import numpy as jnp
# Unpack spatial coordinates and (if time is set) a time variable
x, y, t = domain.variable("interior")

# Slice a specific coordinate range [0, None] means "all"
x, y = domain.variable("interior", (None, None))

# With boundary normals (outward unit normals at each boundary point)
xb, yb, tb, nx, ny = domain.variable("boundary", normals=True)

# With boundary normals AND view-factor matrix (for radiation problems)
xb, yb, tb, nx, ny, VF = domain.variable("boundary", normals=True, view_factor=True)

# Inject point data (sensor or observation locations)
xs, ys = domain.variable("sensor", 0.5 * jnp.ones((2, 1, 2)), point_data=True, split=True)

# Attach a tensor (e.g., spatially-varying PDE parameter, one row per batch sample)
k = domain.variable("k", jnp.array([[1.0], [2.0], [3.0]]))  # (B, 1)

Time-Dependent Problems

domain = jno.domain(
    constructor=jno.domain.rect(mesh_size=0.05),
    time=(t_start, t_end, n_steps),
)

x, y, t   = domain.variable("interior")   # interior + time
x0, y0, t0 = domain.variable("initial")   # initial slice (t=0)

The solver automatically uses jax.lax.scan over time steps.


Operator Learning (Multiple Batch Samples)

Multiply a domain by an integer B to replicate it across B independent batch samples. This is the standard setup for learning a PDE solution operator over a family of parameters.

domain = 40 * jno.domain(constructor=jno.domain.rect(mesh_size=0.05))

# Attach B×4 parameter vectors
theta = ...  # shape (B, 4)
θ = domain.variable("θ", theta)

Mesh Connectivity (for Finite Differences)

Some schemes (e.g., scheme="finite_difference" in jnn.grad / jnn.laplacian) require the mesh topology to be pre-processed:

domain = jno.domain(
    constructor=jno.domain.rect(mesh_size=0.05),
    compute_mesh_connectivity=True,   # pre-compute FD stencils
)

Visualisation

jno.domain.rect(x_range=(0,1), y_range=(0,1), mesh_size=0.1) # Create a domain (e.g., rectangle)
domain.variable("interior") # Set variables to trigger mesh generation
domain.plot("domain.png")   # saves a figure with mesh, boundaries, and normals

Custom Geometries

You can define your own pygmsh-compatible geometry constructor:

def my_geometry(mesh_size=0.1):
    def constructor(geo):
        # Use the pygmsh OpenCASCADE kernel to add points, lines, surfaces, ...
        p0 = geo.add_point([0, 0], mesh_size=mesh_size)
        p1 = geo.add_point([1, 0], mesh_size=mesh_size)
        # ...
        geo.add_physical(surface, "interior")
        geo.add_physical(lines, "boundary")
        return geo, 2, mesh_size   # (geo, spatial_dim, mesh_size)
    return constructor

domain = jno.domain(constructor=my_geometry(mesh_size=0.05))

The constructor receives a pygmsh.occ.Geometry object and must return (geo, spatial_dim, mesh_size).