volmdlr_tools.decomposition package#
Decomposition of a solid into the maximal volumes it is made of: lumps of material with no concave edge, none contained in another. Volumes overlap by design — see How a part is broken into simple blocks for what that means.
The way in is Decomposer, which holds a part and the
settings to decompose it under and answers questions about the result — including where each
volume touches the part, which the result itself carries no field for.
Module contents#
Decomposition of a solid into cells and, from them, its maximal volumes.
The way in is Decomposer: hand it a solid and the readings to decompose it
under, and ask it what came out. Behind it are the two stages as functions —
decompose_into_cells(), which cuts a solid into the cells that tile it, and
decompose(), which grows the part’s maximal volumes over those cells — for a caller
that wants one answer rather than something to ask. Everything needed to make and read
either call is exported here; the modules behind it speak the modelling kernel’s own types
and are not part of this surface.
decompose() is the two stages in one call. A caller that wants to look between them —
to read what each cell face was labelled, or which volumes came out concave — runs
collections_of() and then decomposition_of() on the same cells instead.
- class volmdlr_tools.decomposition.CellDecomposition(cells: tuple[Solid, ...], cell_face_origins: tuple[frozenset[int], ...], source: Solid, dropped_face_ids: tuple[int, ...] = (), uncut_face_ids: tuple[int, ...] = (), unbounded_face_ids: tuple[int, ...] = (), unpaired_interior_faces: tuple[int, ...] = (), concavity: Literal['all', 'sharp'] = 'sharp')#
Bases:
objectThe cells one solid decomposes into, and what the run could not use.
- Parameters:
cells – the cells, which together tile the solid.
cell_face_origins – per cell, the indices of the input faces behind it.
dropped_face_ids – the input faces carrying a concave edge whose extension is not in the cut: its surface has no unique extension, or cutting with its pieces broke the partition and they were left out.
uncut_face_ids – the faces whose extension kept no piece at all, because the arrangement left no piece of its sheet carrying the concave edge that asked for one, inside the material. The cut those edges asked for is missing, and a partition missing a cut still tiles the solid, so nothing else reports it.
unbounded_face_ids – the faces whose extension was selected with one or more of its bounding neighbours’ sheets missing. The selection then spread further than those bounds would have let it, which adds cuts nobody’s concave edge asked for.
unpaired_interior_faces – the cut-derived faces interior to the solid that only one cell holds, as indices into the partition’s own faces. A cut interior to the solid has material on both sides, so anything here means the partition has a gap. Named rather than counted so the gap can be looked at against the shape it is in.
concavity – which junctions were read as concave when the cuts were chosen. Carried so that whatever grows volumes over these cells reads them the same way: a run that cut at tangent junctions and then judged maximality without them would disagree with itself.
source – the copy of the solid the cells were actually cut from. Anything asked of the cells afterwards has to be asked against this, not against the caller’s own solid, whose faces are different objects.
- cell_face_origins: tuple[frozenset[int], ...]#
- cells: tuple[Solid, ...]#
- concavity: Literal['all', 'sharp'] = 'sharp'#
- dropped_face_ids: tuple[int, ...] = ()#
- source: Solid#
- unbounded_face_ids: tuple[int, ...] = ()#
- uncut_face_ids: tuple[int, ...] = ()#
- unpaired_interior_faces: tuple[int, ...] = ()#
- class volmdlr_tools.decomposition.CellFace(face_id: int, label: CellFaceLabel, surface: str | None)#
Bases:
NamedTupleOne face of one cell, as the cell holding it sees it.
- Parameters:
face_id – the face’s index in the partition, which is what two cells sharing a face agree on.
label – what the face is, from this cell’s side of it.
surface – the surface it lies on, or
Nonefor one whose kind the identity does not cover.
- face_id: int#
Alias for field number 0
- label: Literal['real', 'extension', 'complement', 'unclassified']#
Alias for field number 1
- surface: str | None#
Alias for field number 2
- class volmdlr_tools.decomposition.CellFaces(labels: tuple[tuple[CellFace, ...], ...], owners: dict[int, tuple[int, ...]])#
Bases:
objectThe faces of a partition, indexed both by the cell holding them and by themselves.
- Parameters:
labels – per cell, one
CellFaceper face it holds.owners – per face index, the cells holding it. A face interior to the solid has two; one on the solid’s own surface has one.
- nuclei() tuple[int, ...]#
Return the cells a collection may grow from: those with no face left to grow across.
Such a cell can only be interior to a maximal volume, and every maximal volume holds one, which is what bounds how many volumes a partition can yield.
- owners: dict[int, tuple[int, ...]]#
- real_surfaces(cells: frozenset[int] | set[int]) set[str]#
Return the surfaces on which the given cells hold real material.
- class volmdlr_tools.decomposition.Collections(maximal: tuple[frozenset[int], ...], concave: tuple[frozenset[int], ...], unfusable: tuple[int, ...])#
Bases:
objectThe collections a partition yields.
- Parameters:
maximal – the cell sets whose volume has no concave edge. No two are equal and none is contained in another.
concave – the cell sets whose volume still has concave edges. The rule is local, so a few come back this way; each is decomposed again from the beginning rather than grown, which is the caller’s job because it means cutting a fresh solid.
unfusable – the nuclei whose cells would not fuse into one solid, so there was never a volume to judge. The one way growth can return nothing at all.
- concave: tuple[frozenset[int], ...]#
- maximal: tuple[frozenset[int], ...]#
- unfusable: tuple[int, ...]#
- class volmdlr_tools.decomposition.Decomposer(solid: Solid, *, volume_tolerance: float = 1e-06, localisation: Literal['material', 'none'] = 'material', concavity: Literal['all', 'sharp'] = 'sharp', close_maximality: bool = False, name: str = '')#
Bases:
objectDecompose a solid into its maximal volumes, and read the result off by asking.
The run is made the first time anything is asked and kept afterwards, so every reading is of the same decomposition and a part is never decomposed twice by accident:
decomposer = Decomposer.from_step("part.step") print(decomposer.summary()) decomposer.volumes_of_face(12) # which maximal volumes reach that face decomposer.faces_of_volume(0) # which faces of the part that volume is made of decomposer.display_3d() # one colour per volume, grey for what is left
Whatever is not asked here is on
decomposition, which is the whole result.- Parameters:
solid – the solid to decompose. It is not modified.
volume_tolerance – the relative volume error allowed between the cells and the solid.
localisation – what bounds each extension face. See
Localisation.concavity – which reentrant junctions ask for a cut, and are read as concave when a volume is judged. See
Concavity.close_maximality – whether to merge returned volumes whose union is still free of concave edges.
name – what to call the part in
summary()and in the view.
- property cells: tuple[Solid, ...]#
The cells the volumes were collected over, which together tile the solid.
- property decomposition: Decomposition#
The maximal volumes the solid decomposes into, decomposing it on first ask.
- display_3d(*, original: bool = True) str#
Open the coloured view in a browser.
- Parameters:
original – whether to include the part itself.
- Returns:
the path of the page written, so a caller has something to open when the browser does not.
- property face_volumes: tuple[frozenset[int], ...]#
Per face of the solid, the maximal volumes reaching it, indexed by face.
The other way round from
volume_faces, and over every face of the solid rather than only the reached ones, so a face no volume reaches is an empty set here rather than a missing key. Volumes overlap by design, so a face is regularly reached by more than one.
- faces_of_volume(index: int) tuple[int, ...]#
Return the faces of the solid one maximal volume is bounded by, in order.
- Parameters:
index – which volume, indexing
volumes.
- classmethod from_step(path: str | Path, **settings) Decomposer#
Read a solid from a STEP file and hold it ready to decompose.
A file holding an assembly is refused rather than read as its first part: a decomposition is of one solid, and which one a caller meant is not for this to guess.
- Parameters:
path – the file to read.
settings – passed on to the constructor.
- Returns:
a decomposer over the solid the file holds, named after the file.
- Raises:
ValueError – when the file holds anything but exactly one solid.
- run() Decomposition#
Decompose now, rather than whenever something is first asked.
The run takes minutes on a large part, so a caller that wants to choose where it spends them says so here.
- summary() str#
Return one line saying what the run produced and what it left out.
- to_volume_model(*, original: bool = True) VolumeModel#
Build the model to look at: one shape per volume, and what no volume reached.
The part goes in first so the volumes draw over it. It is what keeps a single volume legible when the rest are switched off — on its own a maximal volume is a lump with no say in where it sits, and half of reading one is seeing which part of the shape it is.
- Parameters:
original – whether to include the part itself.
- property volume_faces: tuple[frozenset[int], ...]#
Per maximal volume, the faces of the solid it is bounded by.
A volume is a union of cells and each cell names the faces of the solid behind it, so this is where a volume touches the part. It can be empty: a volume made only of cells deep inside the solid is bounded by cuts alone.
- property volumes: tuple[Solid, ...]#
The maximal volumes: none has a concave edge, and none is contained in another.
- volumes_of_face(face_id: int) tuple[int, ...]#
Return the maximal volumes reaching one face of the solid, in order.
- Parameters:
face_id – which face of the solid handed to the constructor.
- class volmdlr_tools.decomposition.Decomposition(volumes: tuple[Solid, ...], volume_cells: tuple[frozenset[int], ...], nuclei: tuple[int, ...], stuck_nuclei: tuple[int, ...], cell_decomposition: CellDecomposition, concave_volumes: tuple[int, ...] = ())#
Bases:
objectThe maximal volumes a solid decomposes into, and what the run could not do.
How much of the part they explain is
covered_fraction, and what they leave out isuncovered_cells. Neither follows from the counts below.- Parameters:
volumes – the maximal volumes: each has no concave edge, and none is contained in another.
volume_cells – per volume, the cells it is the union of. Indices into
cell_decomposition.cells.nuclei – every cell a volume was grown from. Each is either inside one of the volumes or named in
stuck_nuclei, and how the two divide says that growth finished — not how much of the shape it reached, which iscovered_fraction.stuck_nuclei – the nuclei inside none of the volumes: those whose cells would not fuse into one solid, so there was never a volume to judge, and those whose volume was discarded as held inside another while the nucleus itself was not — the two volumes read as one lump, and the cells the lump lacks are the discarded one’s own.
concave_volumes – indices into
volumesof those that still hold a concave edge, so are not maximal. A volume reaches this only by surviving the recursion — the method could not separate it, and it is reported rather than dropped, because dropping it would take its material out of the reading without saying so.cell_decomposition – the partition the volumes were collected over, and its own account of the extension faces the run had to drop.
- cell_decomposition: CellDecomposition#
- concave_volumes: tuple[int, ...] = ()#
- property covered_fraction: float#
The share of the solid’s material lying inside at least one returned volume.
Answers a different question from
stuck_nuclei, and the two can disagree completely. Every nucleus reaching a volume or a report says growth finished; it says nothing about how much of the part was reached, because only a nucleus starts anything. A region holding no nucleus lies in no volume however plainly it is one, and the count of stuck nuclei stays at zero while it does.Measured against the solid, not against the cells, so material a cut lost counts as unexplained rather than disappearing from both sides of the ratio.
- nuclei: tuple[int, ...]#
- stuck_nuclei: tuple[int, ...]#
- property uncovered_cells: tuple[int, ...]#
The cells no returned volume is made of, in order.
What the reading leaves out, named rather than only counted, so it can be looked at against the shape it came from.
- volume_cells: tuple[frozenset[int], ...]#
- volumes: tuple[Solid, ...]#
- exception volmdlr_tools.decomposition.DecompositionError#
Bases:
ExceptionThe solid could not be split into cells that tile it.
- class volmdlr_tools.decomposition.Grown(collections: Collections, faces: CellFaces, measured: Measured)#
Bases:
objectWhat growing a partition’s collections produced, before it is read as a decomposition.
- Parameters:
collections – the cells collected around each nucleus.
faces – the labelled faces the collections were grown over.
measured – everything the geometry was asked while growing, volumes included.
- collections: Collections#
- exception volmdlr_tools.decomposition.IndecomposableError#
Bases:
DecompositionErrorThe method has nothing left to do to this solid: an answer rather than a fault.
Where a re-decomposition is meant to stop. Separate from the faults — a partition that does not tile its solid, regions that will not join, volumes that will not intersect — so that stopping and breaking cannot be caught as one. A kind of
DecompositionErrorstill, so a caller catching the general kind is unaffected.
- class volmdlr_tools.decomposition.Measured(volumes: dict[frozenset[int], ~volmdlr.shapes.Solid], verdicts: dict[frozenset[int], ~volmdlr_tools.decomposition.growth.Verdict | None], unmerged: set[frozenset[int]] = <factory>)#
Bases:
objectEvery question growth asked of the geometry, and the answer it got.
Kept because fusing is the expensive part of the whole decomposition and the volume growth measured in order to call a collection maximal is the very volume the caller wants back — so it is fused once rather than twice. The record is also what a cached fixture of this stage is made from.
- Parameters:
volumes – per cell set measured, the volume it fused to. A set that would not fuse is absent.
verdicts – per cell set measured, what the volume it fused to still was, or
Nonewhere it would not fuse.unmerged – the cell sets whose volume kept the faces of its cells, because merging them did not leave the volume alone. Such a volume is right but is still a mosaic of cell fragments, and is not one the method can be applied to again.
- unmerged: set[frozenset[int]]#
- volumes: dict[frozenset[int], Solid]#
- class volmdlr_tools.decomposition.Recursion(separated: tuple[tuple[Solid, frozenset[int]], ...] = (), unseparated: tuple[tuple[Solid, frozenset[int]], ...] = ())#
Bases:
objectWhat decomposing the concave collections again produced.
- Parameters:
separated – volumes a recursion took out of a concave collection, each with the cells of that collection it holds.
unseparated – volumes a recursion could not take apart, likewise. These still have concave edges and are named as such on the result.
- separated: tuple[tuple[Solid, frozenset[int]], ...] = ()#
- unseparated: tuple[tuple[Solid, frozenset[int]], ...] = ()#
- class volmdlr_tools.decomposition.Verdict(concavities: int, permanent: int, crossable: frozenset[str])#
Bases:
objectWhat a fused collection is.
- Parameters:
concavities – how many of the volume’s face pairs meet at a concave edge. Zero means the collection is a maximal volume; anything else means it has to be decomposed again rather than grown.
permanent – how many of those are between two faces of the part’s own boundary. Reported because it says whether a volume is concave for a shape the part has or for one the cutting made, which is worth knowing when a recursion does not converge. Nothing acts on it.
crossable – the surfaces of the volume’s own making that form one of the rest. Reported for the same reason and likewise acted on by nothing.
- concavities: int#
- crossable: frozenset[str]#
- permanent: int#
- volmdlr_tools.decomposition.collections_of(partition: CellDecomposition, point_tolerance: float | None = None, *, close_maximality: bool = False) Grown#
Grow a maximal volume from every nucleus of an existing partition.
Separate from
decompose()because the two stages fail for unrelated reasons and are worth being able to run apart: this one reads a partition and needs no cutting.- Parameters:
partition – the cells, and the copy of the solid they were cut from.
point_tolerance – how close a point must be to a face of the solid to lie on it. Derived from the solid’s own size when not given.
close_maximality – whether to merge returned volumes whose union is still maximal.
- volmdlr_tools.decomposition.decompose(solid: Solid, *, volume_tolerance: float = 1e-06, localisation: Literal['material', 'none'] = 'material', concavity: Literal['all', 'sharp'] = 'sharp', close_maximality: bool = False) Decomposition#
Decompose a solid into the maximal volumes it is made of.
A maximal volume lies inside the solid, has no concave edge, has every one of its faces on the same surface as a face of the solid that it meets or overlaps, and is not contained in another volume meeting those three. Volumes overlap by design: a part usually reads as material in more than one way, and every reading is returned.
The cells around each nucleus are collected by a rule that reads only local face labels, so a few collections come back holding a concave edge. Each of those is decomposed again from the beginning, as a solid in its own right, and its pieces stand in for it. Any the method cannot separate, and any whose faces could not be merged back into whole faces first, are returned and named in
Decomposition.concave_volumes.- Parameters:
solid – the solid to decompose. It is not modified.
volume_tolerance – the relative volume error allowed between the cells and the solid.
localisation – what bounds each extension face. See
Localisation.concavity – which reentrant junctions ask for a cut, and are read as concave when a volume is judged. See
Concavity.close_maximality – whether to merge returned volumes whose union is still free of concave edges. Beyond the published method, which discards a contained volume but never merges two; without it some returned volumes are contained in a valid union of others, and the definition’s fourth condition says such a volume is not maximal.
- Raises:
ValueError – when a localisation or concavity reading is not one on offer.
IndecomposableError – when the solid holds no material, or its partition holds no nucleus to grow from — the method has nothing to do to it.
DecompositionError – when the solid cannot be cut into cells that tile it, when the regions a dropped sheet separated will not join back into one cell, or when two grown volumes will not intersect so containment cannot be decided.
- volmdlr_tools.decomposition.decompose_into_cells(solid: Solid, *, volume_tolerance: float = 1e-06, localisation: Literal['material', 'none'] = 'material', concavity: Literal['all', 'sharp'] = 'sharp') CellDecomposition#
Decompose a solid into the cells that tile it.
Only faces carrying a concave edge are extended. The sheets are arranged against each other and against the solid’s own faces, of each extension only the pieces its concave edges ask for are kept, and the solid is cut once with those. An extension whose own pieces break the split is left out, and its face named on the result.
- Parameters:
solid – the solid to decompose. It is not modified.
volume_tolerance – the relative volume error allowed between the cells and the solid.
localisation – what bounds each extension face. Only measurement has a reason to pass anything but the default.
concavity – which reentrant junctions ask for a cut. See
Concavity.
- Raises:
ValueError – when a localisation or concavity reading is not one on offer.
IndecomposableError – when the solid encloses no material, so there is nothing to cut.
DecompositionError – when the sheets cannot be arranged, when the cells do not add back up to the solid, or when the regions a dropped sheet separated will not join back into one cell. A cut leaves a skin of the thickness its surfaces are held to, so a part asking for many of them can overshoot a tight tolerance with none of its material anywhere lost — read the reported error against the part’s size before reading it as a failure of the cutting.
- volmdlr_tools.decomposition.decomposition_of(partition: CellDecomposition, grown: Grown, recursion: Recursion = Recursion(separated=(), unseparated=())) Decomposition#
Assemble what collecting produced into the answer a caller reads.
Separate from
decompose()so that anything holding the pieces already — a suite running the two stages apart, a cached fixture of the collections — reads the same result rather than a rebuilt approximation of it.- Parameters:
grown – what growing the partition’s collections produced.
recursion – what decomposing the concave collections again produced.
Settings#
The values accepted by the localisation and concavity arguments.
Extension faces — the surfaces a solid is cut with to produce its cells.
An extension face is a face enlarged far past its own extent. It removes nothing: it only partitions. Only faces carrying a concave edge are extended, and each is extended together with the neighbours it meets across the non-concave edges that run into one of its concave edges. Those neighbours’ sheets are what bound the extension, so it stops at the tangent or convex boundary of its own face rather than sweeping the whole part; they are bounds only, and contribute a cut of their own only where they carry a concave edge themselves.
What is decided here is which surfaces take part and what bounds each. Which pieces of a
surface survive is decided in cells, before the solid is
cut: the sheets are arranged against each other and against the solid’s own faces, the
pieces the concave edges ask for are read off that arrangement, and only those become the
tools of the cut. So the solid is cut where its concave edges asked and nowhere else.
The sheets are arranged all together rather than one at a time, which is a departure from the published construction and a deliberate one: it extends each face against a copy of the solid holding only that face’s sheet and its neighbours’, and trimming a sheet on its own that way does not close — two sheets meeting along a tangent line have that line computed twice, and the pieces come out slightly apart, which on a part made of cylinders is every corner. One arrangement computes each shared line once.
A neighbour is extended to bound with whether or not it carries a concave edge of its own. A sheet that no concave edge asks anything of cuts nothing — only a face’s own concave edges select pieces of its sheet — so such a sheet is a bound and nothing else.
Only analytic surfaces are extended, since a free-form face has no unique extension. A face
that cannot be extended is accounted for as such rather than silently lost. Enlarging a face
is itself a question about that face and nothing else, so it is asked of
faces.
- volmdlr_tools.decomposition.extension.Concavity
Which reentrant junctions are read as concave, and so drive a cut.
"all"reads every one the graph reports, tangent junctions included."sharp"reads only those where the two faces actually meet at an angle, which is what a junction looks like once the blend that used to round it has been removed: the faces still touch tangentially, and cutting there divides material the part does not divide.alias of
Literal[‘all’, ‘sharp’]
- volmdlr_tools.decomposition.extension.Localisation
What bounds an extension face.
"material"bounds it by the sheets of the neighbours its face meets across non-concave edges, and by the solid — the published construction."none"bounds it by nothing but the solid, and is the unlocalised reading the published one is worth measuring against rather than a way anyone should decompose.alias of
Literal[‘material’, ‘none’]