Skip to content

cc.par + boxes pipeline ​

This walkthrough shows the full chimera-component pipeline: generate two independent meshes (a body-fitted cubed-sphere wall + a flat-caps cylinder background), produce one .grd and one cc.par per component, generate the marker boxes that hole-cut the body, then merge the two pairs into a single Xall-ready assembly.

The reference example is at examples/05-chimera-assembly/chimera_assembly.py — every step in this guide maps to a function call in that script, which runs end-to-end without external data.

CLI-only path

The same pipeline runs end-to-end through shore subcommands alone — see the bash equivalent at examples/05-chimera-assembly/chimera_assembly.sh. It produces byte-identical assembly.{grd, cc.par, proc.input} outputs by chaining shore mesh, shore primitive flat-caps, shore boxes-from-stl, shore grd, shore cc-par, shore proc-input and the three *-merge commands. Pass --adjacency-json PATH to shore mesh and shore primitive flat-caps to emit the .adjacency.json sidecar that shore cc-par needs.

What you'll build ​

Two meshes covering the same domain [-10, 10]² × [-10, 50]:

MeshTopologyBlocksRole
Near-body wallCubed sphere6Wraps a unit-radius (diameter 1.0) sphere body at the origin, marched outward to wall-normal extent ~0.2.
BackgroundFlat-caps cylinder5Cylindrical domain with no central hole — central square + 4 trapezoidal sub-blocks.

Plus, packaged for the solver:

  • 2 binary .grd files (one per mesh) → 1 merged assembly.grd.
  • 2 ASCII cc.par files (one per mesh, the wall one with marker boxes) → 1 merged assembly.cc.par.
  • 1 ParaView .vtm per mesh (and optionally a standalone boxes .vtm) for visual inspection.

Step 1 — Generate the meshes ​

The two meshes are exactly what assembly_sphere_in_cylinder.py builds — see that walkthrough for the topology, parameter choices, and ParaView inspection. The short form:

python
from shore.mesh import CubedSphereTopology, Mesh
from shore.primitive import FlatCapsCylinder

# Near-body wall: 6 cubed-sphere blocks
wall = Mesh.from_stl(
    "sphere.stl",
    topology=CubedSphereTopology(),
    ni=30, nj=40, nk=13,
    theta_cap_deg=30.0,
)
wall.march(ds=0.01, growth=1.1)
wall.write_geo("wall")          # wall_sub0.geo .. wall_cap_south.geo

# Background: 5 flat-caps blocks
bg = FlatCapsCylinder.build(
    r_out=10.0, z0=-10.0, z1=50.0,
    ni=16, nc=24, nk=60,
    inner_frac=0.4,
)
bg.write_geo("background")      # background_center.geo .. background_sub_s.geo

Step 2 — Per-component .grd ​

Pack each mesh's .geo files into a binary .grd. The block ordering you choose here is the block ordering the companion cc.par will reference, so be consistent.

python
from shore.io.grd import write_grd

write_grd("wall.grd", [
    "wall_sub0.geo", "wall_sub1.geo",
    "wall_sub2.geo", "wall_sub3.geo",
    "wall_cap_north.geo", "wall_cap_south.geo",
])

write_grd("background.grd", [
    "background_center.geo",
    "background_sub_e.geo", "background_sub_n.geo",
    "background_sub_w.geo", "background_sub_s.geo",
])

CLI equivalent:

bash
shore grd wall_sub0.geo wall_sub1.geo wall_sub2.geo wall_sub3.geo \
          wall_cap_north.geo wall_cap_south.geo -o wall.grd
shore grd background_center.geo background_sub_e.geo background_sub_n.geo \
          background_sub_w.geo background_sub_s.geo -o background.grd

Step 3 — Per-component adjacency sidecars ​

shore cc-par needs one .adjacency.json per mesh — the JSON sidecar that captures every face's BC, partner, and axis_map. Both Mesh (cubed sphere) and FlatCapsCylinder already wire their internal seams; emit the sidecar with write_adjacency_json:

python
from shore.io.adjacency import write_adjacency_json

write_adjacency_json(wall.blocks, "wall.adjacency.json")
write_adjacency_json(bg.blocks,   "background.adjacency.json")

If you need to subdivide blocks (parallel decomposition), call shore split before this step — it produces the same JSON sidecar on the split block list.

Step 4 — Generate marker boxes from the body STL ​

Boxes hole-cut the background mesh's body-interior cells. Generate them from the STL with shore boxes-from-stl:

python
from shore.boxes import boxes_from_stl

boxes = boxes_from_stl(
    "sphere.stl",
    associated_block="sub0",        # any wall block; level/group inherited
    max_outward_gap=0.10,           # < wall-normal extent (~0.2)
    method="voxel-fill",            # default; covers body interior + surface
)

max_outward_gap must be smaller than the wall mesh's wall-normal thickness (so the boxes fit inside the wall). The voxel-fill algorithm + greedy merge produces a small number of slab-shaped boxes that cover the entire body. See Algorithm — marker boxes for the derivation.

CLI equivalent:

bash
shore boxes-from-stl sphere.stl --block sub0 --max-outward-gap 0.10 \
                     -o sphere_boxes.json

Step 5 — Per-component cc.par ​

For the wall mesh (body-fitted), FREE faces are chimera fringes → BC code 40 (offgen, the default). The cubed-sphere caps can use a different overset level if you want them to take priority over the equatorial sub-blocks. Bake the boxes into the wall's cc.par:

python
from shore.io.cc_par import BlockMeta, write_cc_par

write_cc_par(
    "wall.adjacency.json",
    "wall.cc.par",
    grd_filename="wall.grd",
    block_metadata={
        "sub0": BlockMeta(level=1, group=0, priority=1, free_face_bc=40),
        "sub1": BlockMeta(level=1, group=0, priority=1, free_face_bc=40),
        "sub2": BlockMeta(level=1, group=0, priority=1, free_face_bc=40),
        "sub3": BlockMeta(level=1, group=0, priority=1, free_face_bc=40),
        "cap_north": BlockMeta(level=2, group=0, priority=1, free_face_bc=40),
        "cap_south": BlockMeta(level=2, group=0, priority=1, free_face_bc=40),
    },
    boxes=boxes,
)

For the background mesh (far-field), FREE faces are extrapolation → BC code -9. No boxes needed (boxes live with the body):

python
write_cc_par(
    "background.adjacency.json",
    "background.cc.par",
    grd_filename="background.grd",
    block_metadata={
        "center": BlockMeta(level=0, group=1, priority=1, free_face_bc=-9),
        "sub_e":  BlockMeta(level=0, group=1, priority=1, free_face_bc=-9),
        "sub_n":  BlockMeta(level=0, group=1, priority=1, free_face_bc=-9),
        "sub_w":  BlockMeta(level=0, group=1, priority=1, free_face_bc=-9),
        "sub_s":  BlockMeta(level=0, group=1, priority=1, free_face_bc=-9),
    },
)

CLI equivalent:

bash
shore cc-par wall.adjacency.json wall.cc.par --grd wall.grd \
             --boxes sphere_boxes.json \
             --meta '{"cap_north": {"level": 2}, "cap_south": {"level": 2}}'

shore cc-par background.adjacency.json background.cc.par \
             --grd background.grd --free-bc -9

Step 6 — Per-component proc.input ​

cc.par is read by the overset preprocessor, not by Xall directly. Xall reads proc.input at MPI startup to learn which block lives on which MPI rank and what group / body indices each block carries. This sibling file is generated from the .grd (block ordering and weights):

python
from shore.io.proc_input import write_proc_input

# 4 MPI ranks; greedy weight-balanced assignment
write_proc_input("wall.grd", "wall.proc.input", np=4)
write_proc_input("background.grd", "background.proc.input", np=4)

CLI equivalent:

bash
shore proc-input wall.grd       wall.proc.input       --np 4
shore proc-input background.grd background.proc.input --np 4

For --np > 1 a balance summary is printed on stderr; if imbalance exceeds 5%, a warning recommends shore balance to plan a topology-safe set of splits. See shore proc-input for the full command reference and the --meta flag for explicit per-block proc / group / body overrides.

Want better balance?

If the imbalance summary fires the warning, run shore balance before going to merge:

bash
shore balance wall.grd -o wall.splits.toml --np 16 \
              --adjacency wall.adjacency.json
shore split -c wall.splits.toml wall_*.geo       # apply the plan
shore grd wall_split_*.geo -o wall.grd            # repack
shore proc-input wall.grd wall.proc.input --np 16 # re-balance

Then go back to step 5 (cc.par) and step 7 (merge) with the larger block list.

Step 7 — Merge into the assembly ​

Three coordinated calls — merge_grd, merge_cc_par, and merge_proc_input must use the same input ordering. The cc.par patch block fields, the proc.input block indices, and the .grd block list all share a single 1-based numbering, so the three merged files only agree if they were merged with the same ordering.

python
from shore.io.grd import merge_grd
from shore.io.cc_par import merge_cc_par
from shore.io.proc_input import merge_proc_input

merge_grd(
    ["background.grd", "wall.grd"],
    "assembly.grd",
)
merge_cc_par(
    ["background.cc.par", "wall.cc.par"],
    "assembly.cc.par",
    grd_filename="assembly.grd",
)
merge_proc_input(
    ["background.proc.input", "wall.proc.input"],
    "assembly.proc.input",
)

CLI equivalent:

bash
shore grd-merge        background.grd        wall.grd        -o assembly.grd
shore cc-par-merge     background.cc.par     wall.cc.par     -o assembly.cc.par     --grd assembly.grd
shore proc-input-merge background.proc.input wall.proc.input -o assembly.proc.input

The merged files describe one 11-block Xall job: 5 background blocks (indices 1..5) + 6 wall blocks (indices 6..11).

Step 8 — Visualise ​

ParaView is the cheapest sanity check — opening the merged .vtm shows that the wall sits where you think it does and that the boxes actually cover the body:

python
from shore.io.vtk import write_caps_vtm

# Wall + boxes in one MultiBlock
write_caps_vtm(
    "wall.vtm",
    [b.nodes for b in wall.blocks],
    labels=[b.label for b in wall.blocks],
    boxes=boxes,
)

# Background alone
write_caps_vtm(
    "background.vtm",
    [b.nodes for b in bg.blocks],
    labels=[b.label for b in bg.blocks],
)

Open both in ParaView. Switch the boxes to wireframe (the default is translucent solid) for a clearer view of how the boxes intersect the wall and the body STL.

Reference ​

Released under the MIT License.