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=...):
From an Existing Mesh File
Built-in Geometry Constructors
All constructors are static methods on jno.domain (equivalently Geometries).
1D
line
Physical groups: "interior", "left", "right", "boundary".
2D
rect
Unstructured triangular mesh generated by pygmsh.
Physical groups: "interior", "boundary", "bottom", "top", "left", "right".
equi_distant_rect
Structured (equidistant) triangular mesh — avoids mesh randomness.
Physical groups: same as rect.
disk
Circular domain (polygon approximation).
Physical groups: "interior", "boundary".
l_shape
L-shaped domain.
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
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).