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)returnsTrue. If not provided, the functionis_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)returnsTrue. If not provided, the functionis_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. Usesdeep_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 signaturef(node:CGNSTree) -> None. Otherwise, the expected signature isf(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();visitormust 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 ofpreandpostbecomesnodes:List[CGNSTree]; these functions are called on the current node and its ancestors.At each level, the return value of
prefunction 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
prereturns nothing, the recusion continues normally, which is equivalent toStep.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 oncedepthis reached (0 beeing the node itself, andNonemeaning unlimited). Defaults toNone.
- 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
modeargument: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
modeargument: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.allclosefunctiontype_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:maia.pytree.compare.EqualArray: two arrays are equal if all their elements are one-to-one exactly equal. This is the default comparison method.maia.pytree.compare.CloseArray: two arrays are equal if all their elements are one-to-one close up to a given tolerance.
- 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
status (bool):
Trueif trees are identicalerrors (str): differences between the two trees
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.