Tree operations

This page describes some maia.pytree features that operate on whole CGNSTrees, such as copying, displaying of comparing trees. These features are less CGNS SIDS-aware than the one listed in Node inspection or Node creation presets pages.

Tree construction

Copy

The following functions return a copy of the input tree:

shallow_copy(t)

Create a shallow copy of the input tree.

Values of the nodes are not copied, but only known as a shared referenced by the output tree. Other data, such as names, labels and tree structure are truly copied.

Parameters

t (CGNSTree) – Input tree

Returns

CGNSTree – Copied tree

Example

>>> zone = PT.new_Zone(type='Unstructured', size=[[9,4,0]])
>>> zone_dupl = PT.shallow_copy(zone)
>>> zone_dupl[1] *= 2
>>> PT.get_value(zone)
array([[18,  8,  0]], dtype=int32)
deep_copy(t)

Create a deep copy of the input tree.

Values of the nodes are copied, and both trees consequently do not share any reference.

Parameters

t (CGNSTree) – Input tree

Returns

CGNSTree – Copied tree

Example

>>> zone = PT.new_Zone(type='Unstructured', size=[[9,4,0]])
>>> zone_dupl = PT.deep_copy(zone)
>>> zone_dupl[1] *= 2
>>> PT.get_value(zone)
array([[9, 4, 0]], dtype=int32)

Logical operations

The following functions construct a new tree from logical operations:

union(*trees)

Create a new tree from the union of the input trees.

Nodes existing on more than one input trees keep the value and label of their first appearance. Note also that output values are shared references to input trees. Uses deep_copy() afterwards if you want an independant copy.

Important

Input root nodes must have the same name. An exception will be raised otherwise.

Parameters

trees (CGNSTree) – Input trees

Returns

CGNSTree – Tree created from union

Example

>>> tree1 = PT.yaml.to_node('''
... Base CGNSBase_t:
...   Zone1 Zone_t:
...     ZoneGridConnectivity ZoneGridConnectivity_t:
...       match GridConnectivity1to1_t "Zone3":
...   Zone2 Zone_t:
... ''')
>>> tree2 = PT.yaml.to_node('''
... Base CGNSBase_t:
...   Zone2 Zone_t:
...   Zone3 Zone_t:
...     ZoneGridConnectivity ZoneGridConnectivity_t:
...       match GridConnectivity1to1_t "Zone1":
... ''')
>>> tree = PT.union(tree1, tree2)
>>> PT.print_tree(tree)
Base CGNSBase_t
├───Zone1 Zone_t
│   └───ZoneGridConnectivity ZoneGridConnectivity_t
│       └───match GridConnectivity1to1_t "Zone3"
├───Zone2 Zone_t
└───Zone3 Zone_t
    └───ZoneGridConnectivity ZoneGridConnectivity_t
        └───match GridConnectivity1to1_t "Zone1"
intersection(*trees, comp_func=<function is_same_node>)

Create a new tree from the intersection of the input trees.

At each tree level, input nodes are considered equal if the binary predicate comp_func(n1, n2) returns True. If not provided, the function is_same_node() is used.

If the intersection is empty, result contains only the root node. Note also that output values are shared references to first input tree. Uses deep_copy() afterwards if you want an independant copy.

Important

Input root nodes must have the same name. An exception will be raised otherwise.

Parameters
  • trees (CGNSTree) – Input trees

  • comp_func (Callable) – Binary predicate used for comparison (see above)

Returns

CGNSTree – Tree created from intersection

Example

>>> tree1 = PT.yaml.to_cgns_tree('''
... CGNSLibraryVersion CGNSLibraryVersion_t R4 [4.2]:
... Base CGNSBase_t:
...   Zone1 Zone_t:
...     ZoneGridConnectivity ZoneGridConnectivity_t:
...       match GridConnectivity1to1_t "Zone3":
...   Zone2 Zone_t:
... ''')
>>> tree2 = PT.yaml.to_cgns_tree('''
... CGNSLibraryVersion CGNSLibraryVersion_t R4 [4.2]:
... Base CGNSBase_t:
...   Zone2 Zone_t:
...   Zone3 Zone_t:
...     ZoneGridConnectivity ZoneGridConnectivity_t:
...       match GridConnectivity1to1_t "Zone1":
... ''')
>>> tree = PT.intersection(tree1, tree2)
>>> PT.print_tree(tree)
CGNSTree CGNSTree_t
├───CGNSLibraryVersion CGNSLibraryVersion_t R4 [4.2]
└───Base CGNSBase_t
    └───Zone2 Zone_t
difference(t1, t2, comp_func=<function is_same_node>)

Create a new tree from the difference of the input trees.

At each tree level, input nodes are considered equal if the binary predicate comp_func(n1, n2) returns True. If not provided, the function is_same_node() is used.

This operation is not symmetric. Ouput contains nodes belonging only to first input. Note also that output values are shared references to input tree t1. Uses deep_copy() afterwards if you want an independant copy.

Important

Input root nodes must have the same name. An exception will be raised otherwise.

Parameters
  • t1 (CGNSTree) – First CGNS node

  • t2 (CGNSTree) – Second CGNS node

  • comp_func (Callable) – Binary predicate used for comparison (see above)

Returns

CGNSTree – Tree created from difference

Example

>>> tree1 = PT.yaml.to_cgns_tree('''
... CGNSLibraryVersion CGNSLibraryVersion_t R4 [4.2]:
... Base CGNSBase_t:
...   Zone1 Zone_t:
...     ZoneGridConnectivity ZoneGridConnectivity_t:
...       match GridConnectivity1to1_t "Zone3":
...   Zone2 Zone_t:
... ''')
>>> tree2 = PT.yaml.to_cgns_tree('''
... CGNSLibraryVersion CGNSLibraryVersion_t R4 [4.2]:
... Base CGNSBase_t:
...   Zone2 Zone_t:
...   Zone3 Zone_t:
...     ZoneGridConnectivity ZoneGridConnectivity_t:
...       match GridConnectivity1to1_t "Zone1":
... ''')
>>> tree = PT.difference(tree1, tree2)
>>> PT.print_tree(tree)
CGNSTree CGNSTree_t
└───Base CGNSBase_t
    └───Zone1 Zone_t
        └───ZoneGridConnectivity ZoneGridConnectivity_t
            └───match GridConnectivity1to1_t "Zone3"

Tree editing

Visitor patterns

These functions allow users to apply a function to every node in a tree, which is traversed in a depth-first search manner.

scan(tree, callable, ancestors=False)

Recursively apply the callable function to every node of the input tree.

If ancestors==False, the callable must have the signature f(node:CGNSTree) -> None. Otherwise, the expected signature is f(nodes:List[CGNSTree]) -> None, and the function will be called on the current node and its ancestors.

Parameters
  • tree (CGNSTree) – Input tree

  • callable (function) – User function to apply on each node

  • ancestors (bool) – If True, also pass ancestors to callable function

Examples

>>> tree = PT.yaml.to_node('''
... Base CGNSBase_t:
...   Zone1 Zone_t:
...     ZoneGridConnectivity ZoneGridConnectivity_t:
...       match GridConnectivity1to1_t "Zone3":
... ''')
>>> PT.scan(tree, lambda n: PT.update_node(n, name=PT.get_name(n).upper()))
>>> PT.print_tree(tree)
BASE CGNSBase_t
└───ZONE1 Zone_t
    └───ZONEGRIDCONNECTIVITY ZoneGridConnectivity_t
        └───MATCH GridConnectivity1to1_t "Zone3"
>>> tree = PT.yaml.to_node('''
... Base CGNSBase_t:
...   Zone1 Zone_t:
...     ZoneGridConnectivity ZoneGridConnectivity_t:
...       match GridConnectivity1to1_t "Zone3":
... ''')
>>> gc_paths = []
>>> def gc_collector(nodes):
...   if PT.get_label(nodes[-1]) == 'GridConnectivity1to1_t':
...     gc_paths.append('/'.join([PT.get_name(n) for n in nodes]))
>>> PT.scan(tree, gc_collector, ancestors=True)
>>> print(gc_paths)
['Base/Zone1/ZoneGridConnectivity/match']
visit(tree, visitor, ancestors=False)

Recursively evaluate the visitor functions for every node of the tree.

This is the complex (but more flexible) version of scan(); visitor must be an object exposing the following functions:

class Visitor:
  # The visitor interface can expose the following functions.
  # You may omit some functions; in this case, an empty implementation is insered.

  def pre(self, node:CGNSTree) -> Optional[Step]:
    # Called when `node` is visited for the first time
  def down(self, parent:CGNSTree, child:CGNSTree) -> None:
    # Called when moving down from `parent` to `child`
  def up(self, child:CGNSTree, parent:CGNSTree) -> None:
    # Called when moving back from `child` to `parent`
  def post(self, node:CGNSTree) -> None:
    # Called when all the children of `node` are completed

If ancestors=True, the argument of pre and post becomes nodes:List[CGNSTree]; these functions are called on the current node and its ancestors.

At each level, the return value of pre function can be used to decide how to continue the recursion:

class Step(Enum):
  INTO = 0 # continue normally: go down in children of current node
  OVER = 1 # stop current level: do not visit children, go up and continue
  OUT  = 2 # stop visit process: rewind to top level and exit

If pre returns nothing, the recusion continues normally, which is equivalent to Step.INTO.

Parameters
  • tree (CGNSTree) – Input tree

  • visitor (visitor object) – Functions to apply on each node

  • ancestors (bool) – If True, also pass ancestors to visitor functions

Examples

>>> tree = PT.yaml.to_node('''
... CGNSTree CGNSTree_t:
...   CGNSLibraryVersion CGNSLibraryVersion_t:
...   Base CGNSBase_t:
...     Zone2 Zone_t:
...     Zone1 Zone_t:
...       Tri  Elements_t [5, 0]:
...       Quad Elements_t [7, 0]:
... ''')
>>> class TreeSorter:
...   # A visitor that sort children of nodes, until max level is reached
...   def __init__(self, level):
...     self.level = level
...   def pre(self, nodes):
...     last = nodes[-1]
...     PT.set_children(last, sorted(PT.get_children(last)))
...     if len(nodes) >= self.level:
...       return PT.Step.OVER
>>> PT.visit(tree, TreeSorter(2), ancestors=True)
>>> PT.print_tree(tree)
CGNSTree CGNSTree_t
├───Base CGNSBase_t
│   ├───Zone1 Zone_t
│   │   ├───Tri Elements_t I4 [5 0]
│   │   └───Quad Elements_t I4 [7 0]
│   └───Zone2 Zone_t
└───CGNSLibraryVersion CGNSLibraryVersion_t

Removing nodes

Functions removing nodes reuses the concept of predicate described in the Node searching page, which we advise users to read in first place.

However, there is much less variability and options when using remove functions: there is no equivalent of chaining searches (no predicates version), and the only additional parameter is the depth until which nodes are inspected for suppression. In addition, only the variant removing all the matches is provided, leading to the following generic function:

rm_nodes_from_predicate(root, predicate, **kwargs)

Remove all the nodes in the input tree matching the given predicate.

The search can be fine-tuned with the following kwargs:

  • depth (int): Stop exploring nodes once depth is reached (0 beeing the node itself, and None meaning unlimited). Defaults to None.

Parameters
  • root (CGNSTree) – Tree in which nodes are removed

  • predicate (callable) – condition to remove nodes, which must have the following signature: f(n:CGNSTree) -> bool

  • **kwargs – Additional options (see above)

Example

>>> zone = PT.yaml.to_node('''
... Zone Zone_t:
...   FamilyName FamilyName_t 'ROW1':
...   ZoneBC ZoneBC_t:
...     bc1 BC_t:
...       FamilyName FamilyName_t 'BC1':
...       Index_i IndexArray_t:
...     bc2 BC_t:
...       FamilyName FamilyName_t 'BC2':
...       Index_ii IndexArray_t:
... ''')
>>> PT.rm_children_from_label(zone, 'FamilyName_t')
>>> len(PT.get_nodes_from_label(zone, 'FamilyName_t'))
2
>>> PT.rm_nodes_from_label(zone, 'FamilyName_t')
>>> len(PT.get_nodes_from_label(zone, 'FamilyName_t'))
0

Note

This function admits the following shorcuts:

  • rm_nodes_from_name(), rm_nodes_from_label(), rm_nodes_from_value(), rm_nodes_from_name_and_label() (embedded predicate)

  • rm_children_from_name(), rm_children_from_label(), rm_children_from_value(), rm_children_from_name_and_label() (embedded predicate + depth=1)

For convenience, we also provide the keep_children_from_predicate() from which remove the children nodes that do not match the predicate. Note that unlike rm_nodes_from_predicate(), this function is limited to the first level of children.

keep_children_from_predicate(root, predicate)

Remove all the children of root node expect the ones matching the given predicate.

Parameters
  • root (CGNSTree) – Tree in which nodes are removed

  • predicate (callable) – condition to keep nodes, which must have the following signature: f(n:CGNSTree) -> bool

Example

>>> zone = PT.yaml.to_node('''
... Zone Zone_t:
...   FamilyName FamilyName_t 'ROW1':
...   ZoneBC ZoneBC_t:
...     bc1 BC_t:
...       FamilyName FamilyName_t 'BC1':
...       Index_i IndexArray_t:
...     bc2 BC_t:
...       FamilyName FamilyName_t 'BC2':
...       Index_ii IndexArray_t:
... ''')
>>> PT.keep_children_from_label(zone, 'FamilyName_t')
>>> PT.print_tree(zone)
Zone Zone_t
└───FamilyName FamilyName_t "ROW1"

Note

This function admits the following shorcuts:

  • keep_children_from_name(), keep_children_from_label(), keep_children_from_value(), keep_children_from_name_and_label() (embedded predicate)

When the node to remove is known, it is also possible to directly remove it from its path:

rm_node_from_path(root, path)

Remove the node in input tree matching the given path.

A path is a str containing a full list of names, separated by '/', leading to the node to remove. Root name should not be included in path. Wildcards are not accepted in path.

Parameters
  • root (CGNSTree) – Tree in which the search is performed

  • path (str) – path of the node to remove

Example

>>> zone = PT.new_Zone('Zone')
>>> PT.new_FlowSolution('FS', fields={'Density' : [1.], 'Temperature' : [273.]}, parent=zone)
>>> PT.rm_node_from_path(zone, 'FS/Density')
>>> PT.print_tree(zone)
Zone Zone_t
├───ZoneType ZoneType_t "Null"
└───FS FlowSolution_t
    └───Temperature DataArray_t R4 [273.]

See also

Also exists as pop_node_from_path(), which removes the node and returns it.

Adjusting nodes

These functions are high level shortcuts for current modifications of the tree structure. Only light operations such as moving nodes or editing metadata are performed here.

All these fonctions operate inplace on the input tree.

subregion_fields_to_bcdataset(tree, mode='move')

Move the data fields from ZoneSubRegion nodes to their related BC node, if existing.

ZoneSubRegion nodes must be explicitly related to a BC node through the BCRegionName descriptor. Data fields will be added in a BCDataSet named as the ZoneSubRegion (under a DirichletData node). This function is particularly useful for viewing BC data using Paraview.

The operation performed depends of the value of mode argument:

  • if mode == 'move', fields are removed from the ZSR node (which is itself preserved);

  • if mode == 'copy', fields remains in the ZSR node and a copy is placed in the BCDataSet;

  • if mode == 'view', fields in the ZSR and those added in BCDataSet share the same memory.

Parameters
  • tree (CGNSTree) – Input tree, starting at Zone_t level or higher

  • mode (str, optional) – Controls how the fields are created (see above). Defaults to 'move'.

Example

>>> zone = PT.yaml.to_node('''
... Zone Zone_t:
...   ZoneBC ZoneBC_t:
...     Wing BC_t 'BCWall':
...   WingExtraction ZoneSubRegion_t:
...     field DataArray_t [10,20,30,40]:
...     BCRegionName Descriptor_t "Wing":
... ''')
>>> PT.subregion_fields_to_bcdataset(zone)
>>> PT.print_tree(zone)
Zone Zone_t
├───ZoneBC ZoneBC_t
│   └───Wing BC_t "BCWall"
│       └───WingExtraction BCDataSet_t "UserDefined"
│           └───DirichletData BCData_t
│               └───field DataArray_t I4 [10 20 30 40]
└───WingExtraction ZoneSubRegion_t
    └───BCRegionName Descriptor_t "Wing"
subregion_fields_from_bcdataset(tree, mode='move')

Move the data fields to ZoneSubRegion nodes from their related BC node, if existing.

ZoneSubRegion nodes must be explicitly related to a BC node through the BCRegionName descriptor. Data fields are taken from a full (without Subset) BCDataSet node, preferentially named as the ZoneSubRegion node (see subregion_fields_to_bcdataset()); if such a node does not exists, the last full BCDataSet is taken.

The operation performed depends of the value of mode argument:

  • if mode == 'move', fields are removed from the BCDataSet node (which is itself preserved);

  • if mode == 'copy', fields remains in the BCDataSet node and a copy is placed in the ZSR;

  • if mode == 'view', fields in the BCDataSet and those added in ZSR share the same memory.

Parameters
  • tree (CGNSTree) – Input tree, starting at Zone_t level or higher

  • mode (str, optional) – Controls how the fields are created (see above). Defaults to 'move'.

Example

>>> zone = PT.yaml.to_node('''
... Zone Zone_t:
...   ZoneBC ZoneBC_t:
...     Wing BC_t 'BCWall':
...       WingExtraction BCDataSet_t "UserDefined":
...         DirichletData BCData_t:
...           field DataArray_t I4 [10, 20, 30, 40]:
...   WingExtraction ZoneSubRegion_t:
...     BCRegionName Descriptor_t "Wing":
... ''')
>>> PT.subregion_fields_from_bcdataset(zone)
>>> PT.print_tree(zone)
Zone Zone_t
├───ZoneBC ZoneBC_t
│   └───Wing BC_t "BCWall"
│       └───WingExtraction BCDataSet_t "UserDefined"
└───WingExtraction ZoneSubRegion_t
    ├───BCRegionName Descriptor_t "Wing"
    └───field DataArray_t I4 [10 20 30 40]

Tree comparisons

maia.pytree offers two ways to compare trees. We can either simply check if two trees are identical or not with the functions is_same_node() and is_same_tree(), or get a full report of differences using diff_tree(). The later also allow users to define their own comparison function for data arrays.

Checking trees equality

is_same_node(node1, node2, abs_tol=0, type_tol=False)

Compare two nodes.

Nodes are considered equal if they have the same name, label and value. Note that no check is performed on their children.

Parameters
  • node1 (CGNSTree) – first tree

  • node2 (CGNSTree) – second tree

  • abs_tol (float) – absolute tolerance used for value comparison, performed by np.allclose function

  • type_tol (bool) – if True, allow comparaison of compatible but different types (I4/I8 or R4/R8). Otherwise, nodes are considered to differ.

Returns

bool – True if nodes are identical

Example

>>> zone1 = PT.new_Zone(type='Unstructured', size=[[9,4,0]], family='ROTOR')
>>> zone2 = PT.new_Zone(type='Unstructured', size=[[9,4,0]], family='STATOR')
>>> PT.is_same_node(zone1, zone2)
True
is_same_tree(t1, t2, abs_tol=0, type_tol=False)

Compare recursively two trees.

Trees are considered equal if they recursively have the same children (order does not matters), in the sense of is_same_node().

See is_same_node() for arguments description.

Returns

bool – True if trees are identical

Example

>>> zone1 = PT.new_Zone(type='Unstructured', size=[[9,4,0]], family='ROTOR')
>>> zone2 = PT.new_Zone(type='Unstructured', size=[[9,4,0]], family='STATOR')
>>> PT.is_same_tree(zone1, zone2)
False

Reporting trees differences

diff_tree(t1, t2, strict_value_type=True, comp=None)

Report the differences between two trees.

This function is similar to is_same_tree(), but returns a full report of differences between the two input trees. In addition, it is possible to provide a custom comparison function for numerical arrays or to choose one in the following list:

Parameters
  • t1 (CGNSTree) – first tree

  • t2 (CGNSTree) – second tree

  • strict_value_type (bool) – if True, allow comparaison of compatible but different types (I4/I8 or R4/R8). Otherwise, nodes are considered to differ.

  • comp – comparison function to check the value of nodes (see above)

Returns

(DiffReport) – Difference report. First value indicates if trees are identical, second and third store the differences between trees, encoded as strings (respectivly errors and warnings).

Example

>>> zone1 = PT.new_Zone(type='Unstructured', size=[[9,4,0]], family='ROW1')
>>> zone2 = PT.new_Zone(type='Unstructured', size=[[9,4,0]], family='ROW2')
>>> PT.diff_tree(zone1, zone2)
DiffReport(status=False, errors='/Zone/FamilyName -- Values differ: ROW1 <> ROW2\n', warnings='')

Comparison methods

The following comparison methods are exposed in the maia.pytree.compare module:

EqualArray()

A callable object generating a report for diff_tree(), using an exact point-to-point comparison.

Example

>>> sol1 = PT.new_FlowSolution(fields={'Density' : [1., 1.002, 1.]})
>>> sol2 = PT.new_FlowSolution(fields={'Density' : [1., 1.001, 1.]})
>>> comp = PT.compare.EqualArray()
>>> PT.diff_tree(sol1, sol2, comp=comp)
DiffReport(
  status=False,
  errors='/FlowSolution/Density -- Values differ: [1.    1.002 1.   ] <> [1.    1.001 1.   ]\n',
  warnings=''
  )
CloseArray(rtol=1e-05, atol=1e-08)

A callable object generating a report for diff_tree(), using a point-to-point with tolerance comparison (see np.isclose documentation).

Parameters
  • rtol (float) – relative tolerance

  • atol (float) – absolute tolerance

Example

>>> sol1 = PT.new_FlowSolution(fields={'Density' : [1., 1.002, 1.]})
>>> sol2 = PT.new_FlowSolution(fields={'Density' : [1., 1.001, 1.]})
>>> comp = PT.compare.CloseArray(rtol=0, atol=1E-2)
>>> PT.diff_tree(sol1, sol2, comp=comp)
DiffReport(status=True, errors='', warnings='')

To define a custom comparison method, users must provide a callable object comparing two nodes. Note that this method will only be called on nodes having numerics (excluding str) values of same shape, since diff_tree() will report a difference otherwise. This callback must have the following signature:

def __call__(nodes_1: List[CGNSTree],
             nodes_2: List[CGNSTree]) -> Tuple[bool,str,str]:

where nodes_1 and nodes_2 are the two nodes on which comparison is performed, stacked with their ancestors.

As an example, here is a dummy comparison method definition:

class CustomComparator:
  def __call__(self, nodes_1, nodes_2):
    if 'BC_t' in [PT.get_label(n) for n in nodes_1]:
      return (True, '', '') # If node is below a BC, considerer it's ok
    else:
      return (False, 'Uncomparable value\n', '')

Comparison reports

Comparison reports are named tuple made of 3 values:

DiffReport (NamedTuple)

A NamedTuple storing the output of diff_tree()

Fields

  1. status (bool): True if trees are identical

  2. errors (str): differences between the two trees

  3. warnings (str) : minor differences between the two trees

The choice to write in errors or warning report is let to the comparison object. To illustrate the content of a DiffReport, consider the following example:

>>> zone1 = PT.yaml.to_node('''
  Zone Zone_t I4 [[9,4,0]]:
    ZoneType ZoneType_t "Unstructured":
    FlowSolution FlowSolution_t:
      GridLocation GridLocation_t "CellCenter":
      Density DataArray_t [1., 1, 1, 1]:
      Pressure DataArray_t [1E5, 1E5, 1E5, 1E5]:
    SomeData ZoneSubRegion_t:
      PointList IndexArray_t [2,4,6,8]:
      Descriptor Descriptor_t "Even vtx":
  ''')
>>> zone2 = PT.yaml.to_node('''
  Zone Zone_t I8 [[9,4,0]]:
    ZoneType ZoneType_t "Unstructured":
    FamilyName FamilyName_t "ROTOR":
    FlowSolution FlowSolution_t:
      GridLocation GridLocation_t "CellCenter":
      Density DataArray_t [1., 1.1, 1, 1]:
    SomeData DiscreteData_t:
      PointList IndexArray_t [1,3,5,7,9]:
      Descriptor Descriptor_t "Odd vtx":
  ''')
>>> report = PT.diff_tree(zone1, zone2)

Printing the errors report with >>> print(report.errors) gives

/Zone -- Value types differ: I4 <> I8
/Zone/FlowSolution/Density -- Values differ: [1. 1. 1. 1.] <> [1.  1.1 1.  1. ]
< /Zone/FlowSolution/Pressure
/Zone/SomeData -- Labels differ: ZoneSubRegion_t <> DiscreteData_t
/Zone/SomeData/Descriptor -- Values differ: Even vtx <> Odd vtx
/Zone/SomeData/PointList -- Value shape differ: (4,) <> (5,)
> /Zone/FamilyName

Following diff convention, symbols < (resp. >) are used to mark nodes existing only in first (resp. second) tree. Nodes existing in both trees, but with different labels or values are shown using the following pattern : {path} -- {diff_kind}: {details}. If the <> symbol is used to display details, the value to its left (resp. right) corresponds to the first (resp. second) input node.

Note

Users implementing their own comp object must only write the equivalent of {details} in the report. Other data will be insered automatically.