Skip to content

Better imprint for assy - #2069

Merged
jmwright merged 8 commits into
masterfrom
adam-imprint-fix
Jul 26, 2026
Merged

Better imprint for assy#2069
jmwright merged 8 commits into
masterfrom
adam-imprint-fix

Conversation

@adam-urbanczyk

@adam-urbanczyk adam-urbanczyk commented Jul 16, 2026

Copy link
Copy Markdown
Member

Low level fix enabling faster imprints for well behaved models.

NB: this PR also contains a typing fix related to Numba version update.


Tangentially related to this PR (I added explicit test for this use case) - it is possible to imprint subassemblies separately and on per subassy basis use different imprint glue settings in order to optimize performance:

res1, origins1 = cq.occ_impl.assembly.imprint(complex_assy.sub1, glue='partial')
res2, origins2 = cq.occ_impl.assembly.imprint(complex_assy.sub2, glue='full')

# combine the results
res: Compound = res1 | res2
origins: dict[Shape, Shape] = origins1 | origins2

@codecov

codecov Bot commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.70%. Comparing base (b47f7b7) to head (2da3b69).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #2069   +/-   ##
=======================================
  Coverage   95.70%   95.70%           
=======================================
  Files          30       30           
  Lines        9479     9479           
  Branches     1410     1410           
=======================================
  Hits         9072     9072           
  Misses        253      253           
  Partials      154      154           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@lorenzncode lorenzncode left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @adam-urbanczyk !

Comment thread cadquery/occ_impl/assembly.py Outdated
Comment thread cadquery/occ_impl/assembly.py Outdated
shimwell added a commit to fusion-energy/cad-to-dagmc-mesher that referenced this pull request Jul 21, 2026
OCC's BRepMesh_IncrementalMesh simple constructor leaves
IMeshTools_Parameters.AngleInterior unset, and OCC then meshes the
interior grid of doubly-periodic BSpline faces (full-torus shells) with
cells that violate the angular tolerance by an order of magnitude in a
band next to the u-seam: 150 cm chords at a 0.1 cm deflection request,
sagging several cm off the true surface. On nested thin shells (the
complex-cad radial build: first wall, vacuum vessel, nuclear shield,
thermal shield in contact) the seam bands of adjacent surfaces cross
each other, so every shell is watertight and consistently oriented but
the volumes locally interpenetrate, and DAGMC loses ~1e-3 of all
particles ("No intersection found with DAGMC cell N").

Route every BRepMesh call through a new run_brepmesh() helper that sets
Deflection/Angle and DeflectionInterior/AngleInterior explicitly via the
IMeshTools_Parameters overload. On the radial build this removes all
seam-band triangles (max edge 147 cm to 41 cm) and takes OpenMC lost
particles from 191/200k (and 386/2M in a void streaming run) to 0.

Also:
- _imprint_assembly: use the BOPAlgo_Builder based imprint with
  glue="partial" when the installed cadquery has it
  (CadQuery/cadquery#2069, faster and lower RAM); fall back to the old
  single-argument imprint otherwise.
- Skip imprinting for single-solid assemblies: it is a no-op there, and
  the BOPAlgo_Builder imprint returns a Null shape for fewer than two
  arguments.
- Add the complex-cad radial build transport driver to the local sweep
  defaults (tier 3, lost=0 baseline).
@adam-urbanczyk

Copy link
Copy Markdown
Member Author

@jmwright would you be able to take a look?

@jmwright jmwright left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This MRE:

import cadquery as cq
from cadquery.occ_impl.assembly import imprint
imprint(cq.Assembly().add(cq.Workplane().box(1, 1, 1)))

gives this error:

Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    imprint(cq.Assembly().add(cq.Workplane().box(1, 1, 1)))
    ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jwright/repos/cadquery/cadquery/occ_impl/assembly.py", line 885, in imprint
    res = Shape(builder.Shape())
  File "/home/jwright/repos/cadquery/cadquery/occ_impl/shapes.py", line 445, in __init__
    self.wrapped = downcast(obj)
                   ~~~~~~~~^^^^^
  File "/home/jwright/repos/cadquery/cadquery/occ_impl/shapes.py", line 419, in downcast
    f_downcast: Any = downcast_LUT[shapetype(obj)]
                                   ~~~~~~~~~^^^^^
  File "/home/jwright/repos/cadquery/cadquery/occ_impl/shapes.py", line 409, in shapetype
    raise ValueError("Null TopoDS_Shape object")

Comment thread cadquery/occ_impl/assembly.py Outdated
Comment thread cadquery/occ_impl/shapes.py Outdated
Co-authored-by: Jeremy Wright <wrightjmf@gmail.com>
@adam-urbanczyk

Copy link
Copy Markdown
Member Author

This MRE:

import cadquery as cq
from cadquery.occ_impl.assembly import imprint
imprint(cq.Assembly().add(cq.Workplane().box(1, 1, 1)))

gives this error:

Traceback (most recent call last):
  File "<python-input-4>", line 1, in <module>
    imprint(cq.Assembly().add(cq.Workplane().box(1, 1, 1)))
    ~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/jwright/repos/cadquery/cadquery/occ_impl/assembly.py", line 885, in imprint
    res = Shape(builder.Shape())
  File "/home/jwright/repos/cadquery/cadquery/occ_impl/shapes.py", line 445, in __init__
    self.wrapped = downcast(obj)
                   ~~~~~~~~^^^^^
  File "/home/jwright/repos/cadquery/cadquery/occ_impl/shapes.py", line 419, in downcast
    f_downcast: Any = downcast_LUT[shapetype(obj)]
                                   ~~~~~~~~~^^^^^
  File "/home/jwright/repos/cadquery/cadquery/occ_impl/shapes.py", line 409, in shapetype
    raise ValueError("Null TopoDS_Shape object")

Imprint needs at least 2 shapes to do something meaningful . The previous version does not indeed throw, I'll add an explicit check for this.

@adam-urbanczyk
adam-urbanczyk requested a review from jmwright July 26, 2026 08:02
@adam-urbanczyk

adam-urbanczyk commented Jul 26, 2026

Copy link
Copy Markdown
Member Author

Done, I also added a check for no solids at all.

@jmwright jmwright left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jmwright
jmwright merged commit 6ab9565 into master Jul 26, 2026
7 checks passed
shimwell added a commit to fusion-energy/model_benchmark_zoo that referenced this pull request Jul 26, 2026
The zoo had no model where two solids meet over part of a curved surface.
Every existing multi-solid model either shares an exactly coincident face,
because the enclosing body was cut by the same primitive, or does not touch at
all. Partial contact on a curved surface is the case that separates OCCT's
gluing options, so add two models that cover it.

TwoAnnularSectors puts a shorter sector directly outside a taller one, both
spanning the same 90 degrees. The contact patch is a band that reaches the
angular edges of the first sector's outer face, so the imprint edges run clean
across that face. Every gluing option handles this, which makes it the control
case.

TwoAnnularSectorsPartialArc makes the second sector narrower as well as
shorter, so the patch is strictly interior to the first sector's outer face and
reaches its boundary in neither the axial nor the angular direction. Imprinting
then has to split the larger curved face around an island:

    glue          solids  shared faces  shared area
    full               2             0            0
    partial            2             1      23.5619
    None               2             1      23.5619

The shared area matches the analytic 6 * radians(45) * 5. Full gluing returns
the two solids with no shared face at all, so the DAGMC model would not be
watertight at that interface. This is the geometry that shows why partial is
the right default, following CadQuery/cadquery#2069.

Solid volumes are analytically exact, 212.0575 and 176.7146 for the band model
and 212.0575 and 88.3573 for the partial arc model, and the CSG regions agree.
Both models reproduce their CSG reference under the gmsh mesher, to 0.01
percent on material 1 and 0.10 percent on material 2.

Adds the models, tests for both suites, examples, preview images and README
entries. Both fail under the tolerance and angular_tolerance meshing path with
about 19 percent error on material 2, which is the same cad_to_dagmc defect
described in the parent pull request and not a property of these models.
@shimwell

shimwell commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

I am now using this feature and it is super useful.

I have a use case where I have some CQ geometry and I don't know if I can get away with 'full' imprinting (which is fast but can be wrong) or if I need 'partial' imprinting (which is slower but always correct).

The CQ models are large and prepared upstream so without a lot of inspection I'm never going to know if I can go for full imprint in some surfaces.

Is there any possibility of having another mode 'auto' which know when 'full' can be used and 'partial' is necessary. For example can we use check the faces/edges and somehow conservatively identify fully coincident contacts or partial face overlap

@lorenzncode

Copy link
Copy Markdown
Member

I am now using this feature and it is super useful.

I have a use case where I have some CQ geometry and I don't know if I can get away with 'full' imprinting (which is fast but can be wrong) or if I need 'partial' imprinting (which is slower but always correct).

The CQ models are large and prepared upstream so without a lot of inspection I'm never going to know if I can go for full imprint in some surfaces.

Is there any possibility of having another mode 'auto' which know when 'full' can be used and 'partial' is necessary. For example can we use check the faces/edges and somehow conservatively identify fully coincident contacts or partial face overlap

I'm not an OCCT expert but according to the docs isn't glue faster because it skips computations based on assumptions up front?

https://dev.opencascade.org/doc/overview/html/specification__boolean_operations.html#specification__boolean_7

The performance improvement in gluing mode is achieved by excluding the most time consuming computations and in some case can go up to 90%:

Exclude computation of FACE/FACE intersections for partial coincidence;
Exclude computation of VERTEX/FACE, EDGE/FACE and FACE/FACE intersections for full coincidence.
By setting the Gluing option for the operation user should guarantee that the arguments are really coinciding. The algorithm does not check this itself. Setting inappropriate option for the operation is likely to lead to incorrect result.

If you run these computation in 'auto' mode then doesn't that move a time consuming part to a different function?

@jon-proximafusion

Copy link
Copy Markdown

I was thinking the CAD builder could define imprint method to sure on the assembly surfaces, perhaps as a metadata.

Then the code ingesting the CAD and doing the imprint could look for this metadata per surface as an optional clue to what imprint to use.

Perhaps not worth the additional code but could speed up the imprinting at the expense of tagging the surfaces

@lorenzncode

Copy link
Copy Markdown
Member

Assembly supports user metadata, so this could be tested now with a custom imprint wrapper.

from cadquery.occ_impl.assembly import imprint as imprint_assembly

def imprint_with_metadata(assy):
    glue = assy.metadata.get("imprint_glue")

    if glue not in (None, "partial", "full"):
        raise ValueError(f"Invalid glue mode: {glue!r}")

    return imprint_assembly(assy, glue=glue)

This works at the assembly level. I'm not clear on how the proposal would work "per surface".

@adam-urbanczyk

Copy link
Copy Markdown
Member Author

This maybe could be done calling BOPAlgo_PaveFiller directly and/or manipulating its state somehow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants