Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Imprinting and matching #1353

Merged
merged 12 commits into from
Jul 4, 2023
Merged

Imprinting and matching #1353

merged 12 commits into from
Jul 4, 2023

Conversation

adam-urbanczyk
Copy link
Member

@adam-urbanczyk adam-urbanczyk commented Jun 20, 2023

  • Extract nested iterator
  • Add tests
  • Refactor toVTK to use the new iterator
  • toVTK rendering improvements -> split into triangles with normals and edges/points without
  • Refactor other methods to use the new iterator

@adam-urbanczyk adam-urbanczyk marked this pull request as draft June 20, 2023 06:22
@adam-urbanczyk adam-urbanczyk linked an issue Jun 20, 2023 that may be closed by this pull request
@adam-urbanczyk
Copy link
Member Author

adam-urbanczyk commented Jun 20, 2023

@shimwell this PR still needs some polishing, but I think that the functionality is there. Could you verify that this is what you want:

b1 = cq.Workplane().box(1, 1, 1)
b2 = cq.Workplane(origin=(1,0,0)).box(1,1,1)

assy = cq.Assembly().add(b1, color=cq.Color("red")).add(b2)

r, o = cq.occ_impl.assembly.imprint(assy)

where r is the imprinted shape, and o is a dict mapping solids from r to original ids:

s1,s2 = r.Solids()
assert(s1 in o)
assert(s2 in o)

and last, but not least:

assert(len(r.Faces())== 11)

@shimwell
Copy link
Contributor

That does look ideal from your description above and the code above.

I shall try to install this branch and run some models to see how it behaves with curves, faces that are partially shared, faces share between more that two volumes and anything else I can think of.

Many thanks for all of this targeted development

@shimwell
Copy link
Contributor

Ok I think I understand how to correlate the original assembly objects to the imprinted solids.

I adapted the code to print out:

  • imprinted solids IDs
  • correspondence dictionary between original objects IDs and imprinted solids IDs
  • original assembly objects IDs

I notice the assembly has 3 objects so guess the first one is the assembly structure itself. I wonder if that is always present. But i can see matching numbers in the original, imprinted and the correspondence dict which is just what I needed.

Note to self the ID appears to be made from the assembly ID and the solid ID. Perphaps use stri.split('./') when compairing

import cadquery as cq

b1 = cq.Workplane().box(1, 1, 1)
b2 = cq.Workplane(origin=(1,0,0)).box(1,1,1)

assy = cq.Assembly().add(b1, color=cq.Color("red")).add(b2)

r, o = cq.occ_impl.assembly.imprint(assy)

print('imprinted solids', r.Solids, '\n')

for k, v in o.items():
    print('imprinted solid', k, 'corresponding original solid id', v)

print('\noriginal solds ID', assy.objects.keys())

pint out

imprinted solids <bound method Shape.Solids of <cadquery.occ_impl.shapes.Shape object at 0x7f53abcc17f0>> 

imprinted solid <cadquery.occ_impl.shapes.Solid object at 0x7f53abcc15b0> corresponding original solid id ('9910a432-0fb2-11ee-a766-a434d95c2a9c/9910a5e0-0fb2-11ee-a766-a434d95c2a9c',)
imprinted solid <cadquery.occ_impl.shapes.Solid object at 0x7f53abcc1640> corresponding original solid id ('9910a432-0fb2-11ee-a766-a434d95c2a9c/9910a6a8-0fb2-11ee-a766-a434d95c2a9c',)

original solds ID dict_keys(['9910a432-0fb2-11ee-a766-a434d95c2a9c', '9910a5e0-0fb2-11ee-a766-a434d95c2a9c', '9910a6a8-0fb2-11ee-a766-a434d95c2a9c'])

So in this case the first original solid is the first imprinted solid
The second original solid is the second imprinted solid.

@adam-urbanczyk
Copy link
Member Author

I notice the assembly has 3 objects so guess the first one is the assembly structure itself. I wonder if that is always present. But i can see matching numbers in the original, imprinted and the correspondence dict which is just what I needed.

Yes the top level assy is empty - it has no Shape object. In general it does not have to be like that.

@shimwell
Copy link
Contributor

I've ran a few tests on odd shapes and checked the number of faces is correct and this works in the cases I tried.

The unique ID dictionaries also work nicely.

I've been trying to pass the resulting imprinted assembly into gmsh by trying the _address attribute but am not having much luck, perhaps I am doing something wrong but it looks like the _address is not available for TopoDS_Compound.

import cadquery as cq
small = cq.Workplane().box(1, 1, 1)
big = cq.Workplane(origin=(1,0,0)).box(1,2,2)
assy = cq.Assembly()
assy.add(small)
assy.add(big)
r, o = cq.occ_impl.assembly.imprint(assy)
print(len(r.Faces()))  # prints 12 which is correct
topods = r.wrapped  # perhaps this is not the best way to get a topoDS
object_to_pass_to_gmsh=topods._address()

error message

OCP.TopoDS.TopoDS_Compound' object has no attribute '_address'

@adam-urbanczyk
Copy link
Member Author

I think you need to install ocp=7.7.1. It is currently available via cadquery/dev.

@codecov
Copy link

codecov bot commented Jun 23, 2023

Codecov Report

Merging #1353 (7f36652) into master (b96eb8a) will decrease coverage by 0.02%.
The diff coverage is 98.03%.

❗ Current head 7f36652 differs from pull request most recent head 8d66ce9. Consider uploading reports for the commit 8d66ce9 to get more accurate results

@@            Coverage Diff             @@
##           master    #1353      +/-   ##
==========================================
- Coverage   94.16%   94.14%   -0.02%     
==========================================
  Files          26       26              
  Lines        5584     5601      +17     
  Branches      954      952       -2     
==========================================
+ Hits         5258     5273      +15     
- Misses        194      195       +1     
- Partials      132      133       +1     
Impacted Files Coverage Δ
cadquery/__init__.py 87.50% <0.00%> (ø)
cadquery/assembly.py 96.72% <100.00%> (+0.11%) ⬆️
cadquery/occ_impl/assembly.py 97.27% <100.00%> (-0.83%) ⬇️
cadquery/occ_impl/exporters/assembly.py 100.00% <100.00%> (ø)

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@adam-urbanczyk
Copy link
Member Author

@shimwell did you manage? FYI: I think we'll switch to 7.7.1 after releasing CQ 2.3, but that should not stop you form using it in a local dev env.

@shimwell
Copy link
Contributor

I have been trying but struggling to get the environment to include everything (ocp7.7.1, gmsh, cadquery imprint branch). I've been trying to conda install from the ocp dev tar.gz, pip install from the cadquery branch and conda install gmsh from conda forge but keep getting package conflicts.

I was able to test a bunch of shapes with share surfaces end up with the right number of surfaces so I think this PR does what it aims to do.

As for the gmsh integration I think that get easier to test with releases

@adam-urbanczyk
Copy link
Member Author

Hm, env.yaml does not seem to support labels.

environment.yml Outdated Show resolved Hide resolved
@shimwell
Copy link
Contributor

shimwell commented Jun 25, 2023

Trying that environment.yml you with a pip install . and a conda install from the url get me the environment.

name: cadquery
channels:
  - conda-forge
  - cadquery
dependencies:
  - python=3.9
  - ipython
  - ocp=7.7.*
  - pyparsing>=2.1.9
  - sphinx=5.0.1
  - sphinx_rtd_theme
  - black=19.10b0
  - click=8.0.4
  - mypy
  - codecov
  - pytest
  - pytest-cov
  - ezdxf
  - ipython
  - typing_extensions
  - nptyping=2.0.1
  - nlopt
  - path
  - casadi
  - multimethod >=1.7,<2.0
  - python-gmsh
  - gmsh
  - pip
  - pip:
    - --editable=.
    - git+https://github.com/CadQuery/OCP-stubs.git
cd cadquery  # repo folder imprint branch
pip install -e .
conda install https://anaconda.org/CadQuery/ocp/7.7.1.0/download/linux-64/ocp-7.7.1.0-py39_0.tar.bz2

Then running this script

import cadquery as cq
import gmsh
small = cq.Workplane().box(1, 1, 1)
big = cq.Workplane(origin=(1,0,0)).box(1,2,2)
assy = cq.Assembly()
assy.add(small)
assy.add(big)
r, o = cq.occ_impl.assembly.imprint(assy)
print(len(r.Faces()))  # prints 12 which is correct
topods = r.wrapped
object_to_pass_to_gmsh=topods._address()
gmsh.initialize()
gmsh.option.setNumber("General.Terminal", 1)
volumes = gmsh.model.occ.importShapesNativePointer(object_to_pass_to_gmsh)
gmsh.model.occ.synchronize()
gmsh.option.setNumber("Mesh.Algorithm", 1)
gmsh.option.setNumber("Mesh.MeshSizeMin", 20)
gmsh.option.setNumber("Mesh.MeshSizeMax", 30)
gmsh.model.mesh.generate(2)
gmsh.write("gmsh_of_cadquery_in_memory.msh")

Get me to this new (for me) error when importing gmsh.

Traceback (most recent call last):
  File "/home/j/cad_imprint_test/cq_to_gmsh_in_memory.py", line 15, in <module>
    import gmsh
  File "/home/j/miniforge/envs/cadquery/lib/python3.9/site-packages/gmsh.py", line 86, in <module>
    lib = CDLL(libpath)
  File "/home/j/miniforge/envs/cadquery/lib/python3.9/ctypes/__init__.py", line 374, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: /home/j/miniforge/envs/cadquery/lib/libgmsh.so.4.11: undefined symbol: _ZNK20XCAFDoc_MaterialTool11GetMaterialERK9TDF_LabelRN11opencascade6handleI24TCollection_HAsciiStringEES7_RdS7_S7_

@adam-urbanczyk
Copy link
Member Author

Looks like you are using gmsh that was not built against 7.7.1 but does not pin the occt version fully. You could try installing the latest version/build of gmsh, but I'd propose to keep this discussion outside of this PR. You can still test with saving to disk, the order of solids should be stable after export/import. Keys of the output dict are added in that order too.

@adam-urbanczyk
Copy link
Member Author

Let me know what you think @lorenzncode @jmwright . Assembly.__iter__ can be probably used in more places.

@adam-urbanczyk adam-urbanczyk marked this pull request as ready for review June 29, 2023 19:28
@shimwell
Copy link
Contributor

shimwell commented Jun 30, 2023

from the example at the top I am just wondering why the values of the o dictionary are tuples of len 1 instead of strings

r, o = cq.occ_impl.assembly.imprint(assy)
all_values = list(o.values())
type(all_values[0])
>>> <class 'tuple'>
len(all_values[0])
>>> 1

@lorenzncode
Copy link
Member

from the example at the top I am just wondering why the values of the o dictionary are tuples of len 1 instead of strings

My understanding is because there can be more than one origin per solid in the result.
Say there is an intersection such as:

b1 = cq.Workplane().box(1, 1, 1)
b2 = cq.Workplane(origin=(-0.2,0,0)).box(1,1,1)

@jmwright
Copy link
Member

@adam-urbanczyk I tested the refactor against the KiCAD generator and the files seem unchanged, so that's good. The rest of the code looks good to me, but I see that changes are still being pushed to this PR.

@shimwell
Copy link
Contributor

shimwell commented Jun 30, 2023

There is a line in the environment.yml file that says ocp=7.7.*.

Do we need to change this to ocp>7.7.1 as I think this _address() method needs OCP version 7.7.1 or above?

I guess _address() is not strictly part of this PR so I guess this version pin update can wait

cadquery/assembly.py Outdated Show resolved Hide resolved
cadquery/assembly.py Show resolved Hide resolved
cadquery/occ_impl/assembly.py Outdated Show resolved Hide resolved
cadquery/occ_impl/assembly.py Outdated Show resolved Hide resolved
@adam-urbanczyk
Copy link
Member Author

@jmwright yes, sorry. Some scope creep. I'm fully done with the refactoring. Last change toVTK is to get better rendering. In another PR I'll add a CQ-native show_object function for debugging and testing.

afbeelding

@shimwell not in this PR.

Copy link
Member

@jmwright jmwright left a comment

Choose a reason for hiding this comment

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

@adam-urbanczyk This still seems to work fine with the KiCAD generator.

@adam-urbanczyk
Copy link
Member Author

Alright, I'll merge tomorrow. @shimwell any last thoughts?

@shimwell
Copy link
Contributor

shimwell commented Jul 3, 2023

No further thoughts from me, I've tested this with the workflow I'm hoping to use it in and am getting good results. All good from my point of view

@shimwell
Copy link
Contributor

shimwell commented Jan 3, 2024

Just to mention I've been using _address() feature introduced by this PR and I think it occasionally throws up errors. I shall investigate more and write up a minimal example for an issue but at the moment I'm seeing this error over here

object_to_pass_to_gmsh=topods._address()
gmsh.initialize()
gmsh.option.setNumber("General.Terminal", 1)
volumes = gmsh.model.occ.importShapesNativePointer(object_to_pass_to_gmsh)
     @staticmethod
    def importShapesNativePointer(shape, highestDimOnly=True):
        """
        gmsh.model.occ.importShapesNativePointer(shape, highestDimOnly=True)
    
        Imports an OpenCASCADE `shape' by providing a pointer to a native
        OpenCASCADE `TopoDS_Shape' object (passed as a pointer to void). The
        imported entities are returned in `outDimTags' as a vector of (dim, tag)
        pairs. If the optional argument `highestDimOnly' is set, only import the
        highest dimensional entities in `shape'. In Python, this function can be
        used for integration with PythonOCC, in which the SwigPyObject pointer of
        `TopoDS_Shape' must be passed as an int to `shape', i.e., `shape =
        int(pythonocc_shape.this)'. Warning: this function is unsafe, as providing
        an invalid pointer will lead to undefined behavior.
    
        Return `outDimTags'.
    
        Types:
        - `shape': pointer
        - `outDimTags': vector of pairs of integers
        - `highestDimOnly': boolean
        """
        api_outDimTags_, api_outDimTags_n_ = POINTER(c_int)(), c_size_t()
        ierr = c_int()
        lib.gmshModelOccImportShapesNativePointer(
            c_void_p(shape),
            byref(api_outDimTags_), byref(api_outDimTags_n_),
            c_int(bool(highestDimOnly)),
            byref(ierr))
        if ierr.value != 0:
>           raise Exception(logger.getLastError())
E           Exception: OpenCASCADE exception GeomAdaptor_Surface::UContinuity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Track and Match CQ Objects On an Export-Import Round Trip
4 participants