Skip to documentation
DocumentationAll documentsEnglish

NPUSH(nged)

NAME

npush - push matrices down a tree hierarchy

On this page

SYNOPSIS

npush [-h] [-?] [-v] [-f] [-x] [-r] [-s] [-d max_depth] [-L] [-D] {arg1 [arg2]...}

DESCRIPTION

The npush command moves matrices down a combination hierarchy while preserving the apparent placement of the affected geometry. Depending on the selected options, it can perform a full push, halt at regions or solids, stop at a specified depth, confine changes to the named trees, or preview the changes without writing them.

Background: Matrices in BRL-CAD

BRL-CAD uses a global coordinate system in which all objects are placed. This means each region must uniquely occupy its own portion of that space in order to avoid overlapping with another region. Naively, this means that all objects defined in a .g file need to individually describe distinct volumes using unique primitives. While possible, such an approach is quite wasteful if different regions define the same shape and vary only in the positioning of that shape. A classic example is as a standard bolt used hundereds of times in assembling a machine - ideally such a shape should only need to be described once, and then instances of that shape are positioned to identify individual bolts in the overall model.

COMB trees, which describe organized hierarchies of individual shapes (or other COMB trees) in BRL-CAD, are the standard mechanism used in .g files to reusing otherwise identical object definitions in different positions and orientations. Matrices associated with instances of objects listed in COMB trees position those individual parts, and matrices at different portions of the COMB tree hierarchy can position smaller or large components of the whole. For example, the tree:

    sph3/
        u sph3_1.r/R
                u sph3.s
        u sph3_2.r/R
                u sph3.s

appears at first glance to define an overlap, since it includes two different regions both of which are defined using the same sphere object: sph3.s. However, if we inspect sph3_1.r and sph3_2.r more closely, we see that their definitions incorporate matrices over the instances of sph3.s:

sph3_1.r:  REGION id=1002 (air=0, los=100, GIFTmater=1) --
   u sph3.s [10, 0, 0] scale 1
sph3_2.r:  REGION id=1003 (air=0, los=100, GIFTmater=1) --
   u sph3.s [20, 0, 0] scale 1

The instance of sph3.s in sph3_1.r is offset in the X direction by 10mm, and the sph3_2.r instance is offset by 20mm. In addition, if we inspect sph3’s tree:

sph3:  --
   u sph3_1.r
   u sph3_2.r [100, 0, 0] scale 1

we see that the instance of sph3_2.r in sph3 also has a matrix offsetting its position. So the instance of sph.s defined by sph3/sph3_2.r/sph.s is offset in total by 120mm in the positive X direction relative to the global definition of sph.s

As useful as these matrices are, users frequently need to consolidate multiple levels of positioning within a COMB tree, or even eliminate them completely if a client’s code is not able to properly interpret the matrix information of a COMB hierarchy for its own processing. Thus, specific logic is available in BRL-CAD to accomplish these tasks.

Overview

The GED command npush defines logic for "moving" matrix operations within a COMB tree, without changing the position of individual object instances in the tree definition.

A Note on Depth

It is worth taking a little time to define what we are referring to when we discuss the concept of depth when it comes to applying matrix pushing operations. Consider the hierarchy:

sph_009/
        u [M1] sph9_g1/
                u [M2] sph9_g2/
                        u [M3] sph9_g3/
                                u [M4] sph9_g4/
                                        u [M5] sph9.r/R
                                                u [M6] sph9.s

M1, M2, etc. are matrices applied at each level of the hierarchy. When discussing M1, we would refer to that matrix as being at "depth 1" in the hierarchy, as it is defined at one level below the top of the tree. Similarly, M2 would be defined as being at depth 2, and so on.

When discussing depth in push operations, pushing a matrix to a depth means we propagate the matrices down the tree to define a matrix at the specified depth. For example, in the above example, a push of the matrices to depth 4 would result in:

sph_009/
        u sph9_g1/
                u sph9_g2/
                        u sph9_g3/
                                u [M1234] sph9_g4/
                                        u [M5] sph9.r/R
                                                u [M6] sph9.s

where M1234 is the accumulated application of M1, M2, M3 and M4. Note the new matrix is applied at depth 4, the depth specified as the depth limit in the push. Similarly, a depth limit of 6 would result in:

sph_009/
        u sph9_g1/
                u sph9_g2/
                        u sph9_g3/
                                u sph9_g4/
                                        u sph9.r/R
                                                u [M123456] sph9.s

where M123456 is the accumulated application of M1, M2, M3, M4, M5 and M6. Note that the final step of eliminating the matrix by updating the parameters of sph.s was not taken. That operation is defined conceptually as being at one depth lower than the matrix applied to the object. Thus, it would require a depth limit of 7 to completely eliminate all the matrices in the example and update sph.s:

sph_009/
        u sph9_g1/
                u sph9_g2/
                        u sph9_g3/
                                u sph9_g4/
                                        u sph9.r/R
                                                u sph9.s (updated)

Note that if an object or comb exists at different levels of the hierarchy, depth limits may produce copies of an object even if there wouldn’t otherwise be a need for those copies. Take the following example:

sph_010/
        u [M1] sph10_f/
                u [M1] sph10_g/
                        u [M1] sph10.r/R
                                u [M2] sph10.s
        u [M3] sph10_a.r/R
                u [M3] sph10.s

where 3*M1+M2 = 2*M3 (i.e., the matricies on each branch of sph_010’s tree individually evaluate out to the same result). Since sph10.s ends up in the same place in both branches of sph_010, a full push of all matrices in this tree shouldn’t need to create a copy of sph10.s with new paramenters. However, if a push is limited to depth 3, we get a different outcome in that respect:

sph_010/
        u sph10_f/
                u sph10_g/
                        u [M1*3] sph10.r/R
                                u [M2] sph10.s
        u sph10_a.r/R
                u sph10.s_01 (updated sph10.s copy)

Depth 3 is deep enough that the parameters of sph10.s in the second branch should be updated. However, in the first branch, there is an instance of sph10.s that will remain unmodified with a depth limit of three. Thus, the database needs to contain both the modified and unmodified sph10.s in order to define the requested comb tree. A subsequent full push would result in sph10.s being identical to sph10.s_01, but (at least for now) logic to automatically recognize this case and simplify it is not built in to the push logic.

The same phenomena can occur with comb trees. Consider the following example:

sph_011/
        u sph11_f/
                u [M1] sph11_g/
                        u sph11.s
        u sph11_h/
                u [M2] sph11_g/
                        u sph11.s

If sph_011 is pushed to depth 3, the matrices will be pushed to the level of sph11_g’s instance references (sph11.s, in this case.) However, if M1 != M2, sph11_g would need to have two different matrices in its tree for the same instance. Since a depth limit of 3 does not push the matrices down to parameter setting of sph11.s, a new comb must be created:

sph_011/
        u sph11_f/
                u sph11_g/
                        u [M1] sph11.s
        u sph11_h/
                u sph11_g_01/ (updated sph11_g copy)
                        u [M2] sph11.s

Properties of Depth-Limited Push

The -d option has several properties that are worth understanding before use.

Depth is counted at comb boundaries

The depth counter advances each time the push crosses into a new comb (combination) object — one level per comb boundary. It does not advance at internal boolean-operator nodes within a single comb’s tree. For example, a region that contains two solids unioned together is one comb and contributes one level of depth, regardless of how many boolean operators appear inside it.

Copies may be required when the same object appears at multiple depths

When the same comb or solid appears at two different depth levels within the push scope, and those depths fall on different sides of the max_depth boundary, the algorithm must produce different post-push tree content for each depth context. Without -f this situation is reported as a conflict and the operation fails; with -f the algorithm creates copies as needed to satisfy both contexts.

Consider a tree where region r appears once at depth 1 (directly under the top-level group) and once at depth 2 (via an intermediate grouping comb), and in both cases the accumulated matrix from the root to r evaluates to the same value T20:

top/
    u [T20] r/R
            u solid
    u [T10] grp/
            u [T10] r/R
                    u solid

With -d 2 the solid reached via top/r is at depth 2, which is within the limit, so its parameters can be updated directly (apply-to-solid). The solid reached via top/grp/r is at depth 3, which exceeds the limit, so its parameters must remain unchanged and the matrix must be left in the tree above it instead. Because r cannot simultaneously apply and not apply its matrix to the same solid, a copy of r is required. With -f the result is:

top/
    u r/R
            u solid (parameters updated with T20 applied)
    u grp/
            u r_0/R (copy of r)
                    u [T20] solid_0 (copy of solid, parameters unchanged)

Copy creation is conservative

When -d and -f are used together, the algorithm uses the depth at which each comb object appears as part of its instance identity. This ensures correctness in all cases: a comb appearing at depth D1 and the same comb at depth D2 (with the same accumulated matrix) are always treated as potentially distinct instances when a depth limit is active.

In practice this is the right behavior whenever the comb’s subtree has solids that span the max_depth boundary depending on which depth context is used. In cases where the same comb appears at multiple depths but all of its solid descendants are well within the depth limit from every context, the algorithm may still create copies even though the post-push trees would be identical. This is an accepted tradeoff: the conservative strategy is simple and always correct. Any redundant copies created this way will be geometrically identical to their originals and can be cleaned up manually if desired.

A comb at exactly max_depth with multiple matrix contexts is not a conflict

When a comb appears at exactly max_depth through every path in the push scope — never shallower — it is a push leaf in all contexts. Its internal tree content is never read or modified by the push; only the matrix stored on the reference to it (in its parent comb) is updated. Multiple parents can therefore each hold a different matrix above the same comb without any conflict: no copy of the comb is required.

The following tree illustrates this with -d 2:

sph_011/
        u sph11_f/
                u [T10] sph11_g/   ← depth 2 = max_depth
                        u sph11.s
        u sph11_h/
                u [T20] sph11_g/   ← depth 2 = max_depth
                        u sph11.s

sph11_g appears at depth 2 via both sph11_f and sph11_h. Because depth 2 equals max_depth, sph11_g is a push leaf in both contexts. The push accumulates the IDN matrices from sph_011 down through sph11_f and sph11_h, leaving the T10 and T20 references to sph11_g at the leaf boundary. No copies are needed.

Note that this is the depth-limited analog of the region-halted push property. In both cases the object is a comb leaf whose internal content the push never touches, so multiple matrix contexts are harmless. The difference is that the region boundary is type-based while the depth boundary is count-based.

If the same comb appears at both a depth strictly less than max_depth (non-leaf) and at max_depth (leaf) somewhere in the tree, it is no longer a pure comb leaf: the non-leaf context causes the push to modify its internal tree. In that situation the leaf contexts do require copies, and -f must be used.

External object impact is not guaranteed

Unlike a full push with -f, a depth-limited push does not guarantee that shared objects referenced from outside the push scope are updated. The depth limit constrains how far changes propagate. Objects outside the push scope may therefore continue to reference intermediate combs or solids whose definitions have changed, which may or may not affect their geometry depending on the specific tree structure. Use -L to explicitly prevent any changes from affecting objects outside the specified tree.

Pushing relative to Regions and Shapes

In addition to numerical depth limits, npush offers two additional specifiers that are more aware of specific types of objects in trees. The r option will push matrices down to a level just above region references, and the s option will push matrices to a level just above primitive shapes. For example, using the following hierarchy:

sph_010/
        u [M1] sph10_f/
                u [M1] sph10_g/
                        u [M1] sph10.r/R
                                u [M2] sph10.s
        u [M3] sph10_a.r/R
                u [M3] sph10.s

a r push would result in the following:

sph_010/
        u sph10_f/
                u sph10_g/
                        u [M1*3] sph10.r/R
                                u [M2] sph10.s
        u [M3] sph10_a.r/R
                u [M3] sph10.s

A s push, on the other hand, will take the matrices in both branches deeper:

sph_010/
        u sph10_f/
                u sph10_g/
                        u sph10.r/R
                                u [M1*3+M2] sph10.s
        u sph10_a.r/R
                u [M3*2] sph10.s

Note that neither option alters the parameters of primitive shapes.

Worse still is a case where multiple copies of identical objects are referenced at multiple levels. Consider the following example:

sph_016/
        u [M1] sph16a/R
                u [M2] sph16_1/
                        u sph16.c/
                                u sph16.s
                u sph16_1/
                        u sph16.c/
                                u sph16.s
        u [M3] sph16a/R
                u [M4] sph16_1/
                        u sph16.c/
                                u sph16.s
                u sph16_1/
                        u sph16.c/
                                u sph16.s

If we push to depth 3, we end up with four different versions of the sph16.c tree:

sph_016/
        u sph16a/R
                u sph16_1/
                        u [M12] sph16.c/
                                u sph16.s
                u sph16_1/
                        u [M1] sph16.c/
                                u sph16.s
        u sph16a/R
                u sph16_1/
                        u [M34] sph16.c/
                                u sph16.s
                u sph16_1/
                        u [M3] sph16.c/
                                u sph16.s

The first stage of the answer is to create three new combinations and redefine the sph16a tree:

sph_016/
        u sph16a/R
                u sph16_1_01/
                        u [M12] sph16.c/
                                u sph16.s
                u sph16_1/
                        u [M1] sph16.c/
                                u sph16.s
        u sph16a/R
                u sph16_1_02/
                        u [M34] sph16.c/
                                u sph16.s
                u sph16_1_03/
                        u [M3] sph16.c/
                                u sph16.s

That addresses sph16_1, but the tree updates to sph16a created a new problem: now we have two different versions of the sph16a tree. To resolve that problem, we must create a new comb to replace one of the conflicting sph16a instances and redefine the sph_016 tree:

sph_016/
        u sph16a/R
                u sph16_1_01/
                        u [M12] sph16.c/
                                u sph16.s
                u sph16_1/
                        u [M1] sph16.c/
                                u sph16.s
        u sph16a_01/R
                u sph16_1_02/
                        u [M34] sph16.c/
                                u sph16.s
                u sph16_1_03/
                        u [M3] sph16.c/
                                u sph16.s

Unlike sph16a and sph16_1, sph_016 has not seen any changes to its volumetric definition. sph_016 is the "top level" object specified for this push, and as such constitutes a reliable termination point for any need to propagate tree changes. Even if any combs elsewhere in the database reference sph_016, they will not see any volumetric changes as a consequence of the operation and do not need to redefine their trees to preserve original sph_016 trees for external use in copies of sph_016.

Properties of Region-Halted Push

The push boundary is object-type based

With -r the push terminates at the first region encountered in each tree branch. Unlike a depth-limited push, this boundary is determined by object type (whether a comb has its region flag set) rather than by a counted depth from the root. Matrices are accumulated through all non-region combs and placed on the reference that leads into the region. The region’s internal tree and the primitives inside it are never touched by the push.

The same region at different matrix contexts is not a conflict

Because regions are leaves under -r, their internal content is never modified. If the same region appears in multiple places in the tree with different accumulated matrices, each parent simply holds a separate reference to the same region with a different matrix. No copy of the region is required. The following example illustrates this property:

sph_019/                        ← before push
        u [T10] sph19_a/
                u [T10] sph19.r/R
                        u sph19.s
        u [T15] sph19_b/
                u [T5]  sph19.r/R
                        u sph19.s
        u [T30] sph19.r/R
                        u sph19.s

After a region-halted push, sph19_a and sph19_b lose their top-level matrices (pushed down to the region reference), while the direct reference to sph19.r keeps its matrix and the region itself is unchanged:

sph_019/                        ← after npush -r
        u sph19_a/
                u [T20] sph19.r/R
                        u sph19.s
        u sph19_b/
                u [T20] sph19.r/R
                        u sph19.s
        u [T30] sph19.r/R
                        u sph19.s

sph19.r is referenced three times with two distinct accumulated matrices (T20 and T30). No copy is created because the region’s content is unaffected by the push.

Genuine conflicts still require force

A real conflict arises when the same non-region comb or solid appears in multiple branches with different accumulated matrices, and none of those branches have a region boundary above them within the push scope. In that situation npush without -f will report a conflict and refuse to proceed, because the object’s database content cannot simultaneously satisfy two different matrix contexts. Adding -f instructs npush to create copies as needed.

External object impact is not guaranteed

A region-halted push does not modify regions or primitives inside them. Objects outside the push scope that share those regions or primitives will therefore not see volumetric changes from the push. Non-region combs above the region boundary may be modified (their child-reference matrices are updated), but this only affects parents that are part of the push scope. Use -L to explicitly prevent any changes from reaching objects outside the specified tree.

OPTIONS

-h, -?, --help

Print help and exit.

-v, --verbosity

Verbose processing output (primarily used for debugging).

-f, --force

Force creation of new objects if necessary to avoid conflicts while pushing matrices.

-x, --xpush

Enable xpush behavior explicitly. This is equivalent to the force-copying push mode used by -f.

-r, --regions

Halt push operations at the hierarchy level above regions. See the region-halted push discussion of the properties of this mode.

-s, --solids

Halt push operations at the hierarchy level above solids.

-d max_depth, --max-depth max_depth

Halt push operations at the hierarchy level specified by max_depth. See the depth limited push section for a description of the properties of depth-limited push that are worth understanding before using this option.

-L, --local

Ensure that the push does not change geometry outside the specified trees.

-D, --dry-run

Calculate the changes but do not apply them.

EXAMPLES

Example 1. Push all matrices under a group down to the region level

mged>npush -r group

Push all matrices under given group down to the region level (matrices will sit in group above regions).

Example 2. Push all matrices under a group down to the solid level

mged>npush -s group

Push all matrices under given group down to the solid level (matrices will sit below regions).

SEE ALSO

push(nged), xpush(nged), pull(nged)

AUTHOR

BRL-CAD Team

BUG REPORTS

Reports of bugs or problems should be submitted via electronic mail to devs@brlcad.org