Output Channels

Last updated on 19th April 2024

To output a simple field at every tick (timestep), and be able to display these fields on the console, or output to parquet files, use the annotation @Variable as described in Outputting Data]. Many times the data being produced by the model is more complex than a simple value that needs to be output at every tick. For example, you may need to output a specific field every time a certain event takes place, or you may need to output a more complex object. In these cases, data output channels can be used to output different types of data easily to parquet.

Creating an output channel

Output channels need to be created as part of the model initialisation (more about model initialisation here).

Channel creation and management is done via the model context. Create a new channel with the following code.

  @Override
  public void init() {
    getContext()
        .getChannels()
        .createOutputChannel()
        .setId("channel unique id")
        .setSchema(getSpecificChannelSchema())
        .addLabel("simudyne:parquet")
        .build();

    // Other init code
  }
  • setId() -> This is required, and should be a unique string. This will be useful later for retreiving the channel.
  • setSchema() -> This is required. The schema defines the type of data you will be exporting with this channel. It is very important that the schema defined here matches the data you will export on this channel, to avoid errors later. See below for how to define this schema.
  • addLabel() -> This can be used multiple times to add as many labels as you need to. Labels can be used as personal identifiers for channels. The Simudyne SDK will only output the data written to this channel to parquet if the label 'simudyne:parquet' is added to the channel.
  • build() -> This is the final method that needs to be called otherwise the channel will not be created.

Defining the schema

Schema definition is vital for having the channel work as expected. Simudyne SDK schema definition uses the following types:

  • SchemaRecord -> This is used for complex values - a field that will hold nested fields. SchemaRecord needs a name and a list of 'children' - subfields under this record. There are also optional metadata fields that can be set.
  • SchemaField -> This is used for simple values. A SchemaField needs a name and a field type (Boolean, Int, Long etc). There are also optional metadata fields that can be set.
  • SchemaArray -> This is used for fields that will hold multiple values. The array can be an array of simple values (SchemaFields) or an array of complex values (SchemaRecords). SchemaArray needs a name and a an item type, which is the type of data the array will hold. Accepted data types are SchemaRecord, SchemaField, SchemaEnum or SchemaArray for a nested array. There are also optional metadata fields that can be set.
  • SchemaEnum -> This is used for a field whose value will always be one of a known set of strings. A SchemaEnum needs a name and a list of possible strings that can be used for this field. There are also optional metadata fields that can be set.

The schema for a channel must be a SchemaRecord. For example, to output data that may look like this in parquet:

  id        | name     | price   | quantity |  usedFor             | supplier
  -----------------------------------------------------------------------------
  "123-456" | apples   | 2.39    | 345      | ["crumble", "pie"]   | "x-company"
  "123-457" | apples   | 2.9     | 445      | ["crumble", "pie"]   | "y-company"
  "123-550" | avocados | 3.18    | 287      | ["guacamole"]        | "y-company"
  

Create a schema as follows :

    new SchemaRecord("Food prices")
        .add(new SchemaField("id", FieldType.String))
        .add(new SchemaField("name", FieldType.String))
        .add(new SchemaField("price", FieldType.Double))
        .add(new SchemaField("quantity", FieldType.Long))
        .add(new SchemaArray<>("usedFor", new SchemaField("", FieldType.String), Subtype.None))
        .add(new SchemaEnum("supplier", Arrays.asList("x-company", "y-company")));

Sending data to the output channels

It is possible to write to channels from the model, and from the agents. Use the id of the channels to get the specific channel to write to.

The following example writes an entry to the channel for every OrderPlaced message it recieves.

    getMessagesOfType(OrderPlaced.class).forEach(
      order -> 
        getContext().getChannels()
          .getOutputChannelWriterById("channel unique id")
          .write(getSpecificValueRecord(order))
    );

Creating value record objects to write to channels

Simudyne SDK value definition uses the following types:

  • ValueRecord
  • ValueField
  • ValueArray

These objects directly correspond to the Schema objects mentioned above. When creating the value for a field where the schema is SchemaEnum, ValueField should be used.

The example below creates a single row to write, matching the schema above.

    new ValueRecord("Food prices")
      .addField("id", "123-456")
      .addField("name", "apples")
      .addField("price", 2.39)
      .addField("quantity", 345)
      .add(new ValueArray<>("userFor", 
        Arrays.asList(ValueField.make("", "crumble"), ValueField.make("", "pie"))))
      .addField("supplier", "x-company");

Accessing channels from the agents

Calling `getContext()` from inside an agent will return an `AgentContext` object which gives a limited view onto the ModelContext and Channels. This is because Agents are only able to retreive channels, not create channels.