pralin/compose allows to define combination of algorithms according to a YAML specification.

The root of a valid pralin/compose file is a dictionnary with a single key compose. For example, the following compute 1+a and output the result:

compose:
  inputs:
    - a
  outputs:
    - add[0]
  process:
    - pralin/arithmetic/addition:
        id: add
        inputs: [1, a]

Expressions

pralin/compose allows to define values as integers, floating points or strings.

strings and references

Strings are either interpreted as a string or a reference to an output. Reference to outputs take the form ìd[number] where ìd reference a node in the graph, and number is the index of its output. If it is required to define a string of the form ìd[number] and to use it as a string. Then it needs to be tagged with !!str.

In the following example, str1 and str2 are interprated as strings, while str3 is interprated as output index 1 of operation op:

str1: "op[1"
str2: !!str "op[1]"
str3: "op[1]"

Other types of references are inputs to the graph, they can take an arbitrary identifier, and care should be taken to avoid confusion with string values.

sequence

By default, a sequence of value is interpreted as a list of references to other states. The actual value is the value of the last modified referenced state. In the following example, if neither op0 nor op1 has been executed, then the value is 1, otherwise it is set to the corresponding output of the last executed nodes between op0 and op1.

[1, "op0[1]", "op1[2]"]

It is possible to create a value as a state, then it is necesserary to prefix it with !!seq, the following creates a list with 3 values: 1, the last value from output 1 of op0 and the last value from output 2 of op1:

!!seq [1, "op0[1]", "op1[2]"]

map

By default a map is triggered as an error, but similarly to sequences, if they are prefixed with !!map they can be used to create an any_value_map, using:

!!map { a: 1, b: "op0[1]", c: "op1[2]" }

More complex maps can be created:

!!map { a: 1, b: !!seq ["op0[1]", 3 ], c: !!map { d: "op1[2]", e: "op0[0]" } }

parameters

It is possible to define parameters given as argument to the graph. They can be used by using the tag !param followed by the name. For instance:

!param name_of_parameter

When using the !param tag, it is recommended to define the tag namespace at the top of the YAML file with:

%TAG ! tag:cyloncore.com/pralin,2023:

compose

compose is the root node of the pralin/compose computation graph. It is defined by a dicitonnary with the following keys: ìnputs, outputs, process, variables.

The following examples shows a graph, with one node adding 1 to an input value.

compose:
  inputs:
    - value
  outputs:
    - add[0]
  process:
    - pralin/arithmetic/addition:
        id: add
        inputs: [value, 1]

inputs

inputs should be an array of name for each inputs.

outputs

outputs should be an array of values or a dictionnary for naming outputs.

process

process is an array of dictionnaries defining the computation nodes, see bellow for a full description of the different nodes

variables

variables allow to give a name to values that are used in different places in the computation graph. They are implemnted using YAML references. They need to be defined in variables element of the compose dictionnary, by adding a tag starting with & (for instance, &tagname). They can then be used by referencing the tag with * (for instance, *tagname).

The following example defines a variable called counter which is set to either 0 or the index 0 of add outputs. The variable is then used to set the result output:

variables:
  counter: &counter [0, 'add[0]']
outputs:
  result: *counter

parameters

It is possible to define global parameters in the graph, with default value. It is also possible to define parameters that needs to be defined externaly to the graph, using the !required tag, as follow:

parameters:
  default_value: 1
  required_parameter: !required null

In this case, if the default_value is not redefined it will be set to 1. However, if required_parameter is not set in the computation graph, this should trigger an error.

Nodes

This sections conver the different processing nodes

computation node

A computation node in the graph corresponds to a \ref pralin::algorithm_instance and executes its computations. In a process, this node is initiated by a key of the shape module/algorithm_name where module is the name of the library where the àlgorithm_name is defined (the same as what is used for lookup in algorithms_registry).

It supports the following elements:

  • id a string used to identify the node, in particular to use its outputs as inputs for a different node
  • ìnputs an array defining the input value from the computation
  • parameters a dictionnary defining the arguments for the computation node

The following example defines a computation node for the addition algorithm from the artithmetic module, it will result in the addition of the constant 4 with the index 0 output of other_op:

arithmetic/addition:
  id: add_1
  inputs: [4, 'other_op[0]']

The following example defines a computation node for the test_parameters algorithm from the test_ops module. It takes three parameters as aruments integer, floating_point and string. Note the tuse of !!str should be interpreted as a string, and not a reference to output index 2 of tp:

test_ops/test_parameters:
  id: tp
  parameters:
    integer: 45
    floating_point: 82.0
    string: !!str "tp[2]"

Sometimnes an input can take multiple expressions, then the expression is represented as an array. For instance, the following can be used to define an incremental counter:

arithmetic/addition:
  id: inc
  inputs: [[0, 'inc[0]'], 1]

In this example, the first input of the addition is set to 0 or inc[0]. In practice, the input is set to the last computed value. In this case, it is initialised to 0 and then update with the result of the increment operation. For complex type that cannot be represented in yaml, it is possible to use the default keyword, for instance:

complex/recusrive/operation:
  id: op
  inputs: [[default, 'op[0]']]

conditional

conditional executes a set of operations if a condition is true. As show in the exmaple bellow:

conditional:
  condition: [true, 'inf_1[0]']
  process:
    - pralin/arithmetic/addition:
        id: add_1
        inputs: [ *counter_1, 1]

conditional has an alternative form where it test the equality between two values, defined by on and equals. In the following example add_0 is only exxecuted if mode == "mode_1":

conditional:
  on: mode
  equals: "mode_1"
  process:
    - pralin/arithmetic/addition:
        id: add_0
        inputs: [1, 2]

repeat_while

repeat_while repeat the execution of the nodes defined in process until the condition is false.

repeat_while:
  condition: [true, 'inf[0]']
  process:
    - pralin/arithmetic/addition:
        id: add
        inputs: [ *counter, 1]
    - pralin/arithmetic/inferior:
        id: inf
        inputs: [ *counter, 5]

parallel

parallel allows the execution of children nodes defined in process in different threads. The implementation uses a tasks queue, with a limited number of jobs (default is set by the number of cores on the computer). pralin does not handle automatically thread starvation, it is up to the user to make sure this does not happen. In particular, while it is possible to nest parallel inside an other parallel block, this could lead to dead locks, as the parent parallel threads is not released until the children are done.

Example of use:

parallel:
  process:
    - pralin/arithmetic/inferior:
        id: inf_1
        inputs: [ *counter_1, end_value_1]
    - pralin/arithmetic/inferior:
        id: inf_2
        inputs: [ *counter_2, end_value_2]

The two operations inf_1 and inf_2 are executed in different thread.

sequence

sequence allows the executions of children in sequence. Most constructions blocks (repeat_while, conditional….) default to using a sequence internally.

Example of use:

sequence:
  process:
    - pralin/arithmetic/inferior:
        id: inf_1
        inputs: [ *counter_1, end_value_1]
    - pralin/arithmetic/inferior:
        id: inf_2
        inputs: [ *counter_2, end_value_2]

template

template is used to create strings by formatting different values from the composition graph.

Example of use:

compose:
  inputs:
    - input_value
  outputs:
    result: "st[0]"
  parameters:
    param_0: "h: "
  process:
    - pralin/arithmetic/addition:
        id: add_0
        inputs: [1, "input_value"]
    - template:
        id: st
        template: "{}{}->{}\n"
        inputs: [!param param_0, "input_value", "add_0[0]"]