05 — Mapping and comparison¶
How do you compare a field on one mesh with a field on a different mesh? They have different vertices, so you cannot subtract them elementwise. You need a mapping first.
You will learn:
- what
create_mappingreturns and how to read it - the direction rule: it always maps the larger mesh onto the smaller
ptsvselemmapping- how to compare scalar fields once mapped
Self-contained: builds two meshes at different resolutions.
Setup¶
Two spheres of the same radius but different resolution — the same geometry, discretised differently. That is exactly the situation mapping exists for.
import numpy as np
import pandas as pd
import vtk
from imatools.core import mesh as cmesh
from imatools.core import metrics as cmetrics
def make_sphere(resolution, radius=10.0):
s = vtk.vtkSphereSource()
s.SetRadius(radius)
s.SetThetaResolution(resolution)
s.SetPhiResolution(resolution)
s.Update()
return s.GetOutput()
left = make_sphere(12) # finer
right = make_sphere(8) # coarser
print(f"left: {left.GetNumberOfPoints():3d} pts / {left.GetNumberOfCells():3d} cells")
print(f"right: {right.GetNumberOfPoints():3d} pts / {right.GetNumberOfCells():3d} cells")
Creating a mapping¶
create_mapping is a pure function: VTK objects in, a dict out. It does not read or
write files — that orchestration lives in the CLI. map_type picks the granularity:
map_type |
Matches |
|---|---|
"pts" |
point to nearest point |
"elem" |
cell to nearest cell |
mapping = cmesh.create_mapping(left, right, "L", "R", map_type="pts")
print("keys:", list(mapping.keys()))
The left_id / right_id strings become the column names, so name them for your data
("pre"/"post", "caseA"/"caseB") rather than leaving them generic.
It is a plain dict — hand it to pandas.
df = pd.DataFrame(mapping)
df.head()
The direction rule¶
create_mappingalways maps the larger mesh onto the smaller one, decided from the objects themselves — not from the order you pass them.
So the row count equals the smaller mesh's point count. Swapping the arguments does not invert the mapping.
print("rows in mapping: ", len(df))
print("smaller mesh points: ", min(left.GetNumberOfPoints(), right.GetNumberOfPoints()))
flipped = pd.DataFrame(cmesh.create_mapping(right, left, "R", "L", map_type="pts"))
print("rows when swapped: ", len(flipped), "- same, direction is not caller-controlled")
Reading the columns¶
L/R— the matched indices in each meshdistance_manual— how far apart the matched pair isX_l, Y_l, Z_l/X_r, Y_r, Z_r— their coordinates
distance_manual is your quality check: large values mean a point had no good partner, and
the mapping should not be trusted there.
d = df["distance_manual"]
print(f"distance min={d.min():.4f} mean={d.mean():.4f} max={d.max():.4f}")
print(f"exact matches (d==0): {(d == 0).sum()} of {len(d)}")
Some points coincide exactly (both spheres share poles), most sit slightly apart — expected when mapping between resolutions.
Element mapping¶
elem_map = pd.DataFrame(cmesh.create_mapping(left, right, "L", "R", map_type="elem"))
print("elem mapping rows:", len(elem_map))
print("smaller mesh cells:", min(left.GetNumberOfCells(), right.GetNumberOfCells()))
Comparing fields¶
With a mapping you can line two fields up and compare them. core.metrics has the
comparison helpers; here is compare_scalar_field on two synthetic fields.
rng = np.random.default_rng(42)
n = len(df)
field_a = rng.uniform(0, 1, n)
field_b = field_a + rng.normal(0, 0.05, n) # same signal, small noise
result = cmetrics.compare_scalar_field(field_a, field_b)
print(result)
From the CLI¶
imatools-mesh covers the single-pair case; imatools-comparisons drives a whole cohort
from a manifest CSV (comparison_dir,case_left,case_right):
# one pair
imatools-mesh map -in1 left.vtk -in2 right.vtk -map elem
imatools-mesh map-stats -m MAPPING/
# a cohort
imatools-comparisons map-fibres --manifest manifest.csv -n in -map pts
imatools-comparisons compare --manifest manifest.csv -n lat -f 1
Recap¶
| Task | Call |
|---|---|
| Map points | cmesh.create_mapping(a, b, "A", "B", map_type="pts") |
| Map cells | cmesh.create_mapping(a, b, "A", "B", map_type="elem") |
| Inspect | pd.DataFrame(mapping) |
| Check quality | the distance_manual column |
| Compare fields | cmetrics.compare_scalar_field(s0, s1) |
Remember: the mapping direction is decided by mesh size, not argument order.
That is the tour: mesh I/O -> scalar fields -> scar -> labels -> mapping.