Skip to content

Using HLA FOMs

Sen is able to use HLA FOMs to model types and classes.

How Sen uses a FOM

Sen uses HLA FOM XML as an interface definition format. The code generator reads your FOM modules at build time and emits Sen types and classes from them, so a shared model can serve as your ICD instead of an STL. For example, you could use the RPR FOM and Link 16 FOM (SISO standards), NETN FOM (NATO product), etc.

Sen can read several module sets at once, each one a directory of FOM files whose name becomes the Sen package. A name is looked for in the module that mentions it and then through that module's dependencies, so a class declared in more than one module becomes a single class rather than one per module. BaseEntity is declared in seven of the example modules and comes out as one class in the package of the directory it was first read from, rpr.BaseEntity. The empty classes NETN uses to reach RPR's hierarchy resolve to it the same way, which is how netn.NETNAircraft comes to extend rpr.Aircraft, and 69 of the NETN classes inherit from an RPR one like this.

These OMT elements are read: objectClass, attribute (name, updateType, sharing, transportation), interactionClass (name, transportation), parameter, basicData, simpleData, enumeratedData, arrayData, fixedRecordData, variantRecordData, units and semantics.

<modelIdentification> gives the model's name, which is how a mappings file names a document, and its reference entries of type Dependency, which are where the module dependency graph comes from. A dependency naming a document that is not in the set is an error at generation time, and one on the MIM is ignored. Version, POC, useLimitation and useHistory do not reach the generated model.

transportation is honored, and maps like this:

FOM transportation Attribute → property Interaction → method Interaction → event
HLAreliable reliable, as [confirmed] reliable, as [confirmed] reliable, as [confirmed]
HLAbestEffort best-effort multicast best-effort unicast, as [bestEffort] best-effort multicast

A FOM's transportation says whether delivery has to be reliable, and nothing more. Sen's modes say that too, but they also say whether a message goes to every subscriber or to one, so mapping HLAbestEffort means choosing something the FOM never stated. The table shows what Sen chooses.

[confirmed] and [bestEffort] are the modes you write in STL. Best-effort multicast is what a property or event gets when you write neither, which is why those two cells name no attribute. Quality of service describes all three.

A transportation value Sen does not recognize is a hard error at generation time. That means the OMT's own extension point, where a FOM declares its own transportation types, is not supported.

Example of a diagram generated by Sen:

Class diagram generated from the RPR, NETN and Link 16 FOMs

You can download the image to get a better look.

Put your HLA FOMs XMLs in folders (for example: rpr, netn).

In CMake you can do as follows to generate C++ code:

sen_generate_cpp(
  TARGET
    fom_lib
  HLA_FOM_DIRS
    rpr
    netn
  GEN_HDR_FILES
    fom_headers
  HLA_MAPPINGS_FILE
    mapping.xml
)

The HLA_MAPPINGS_FILE option is for dealing with interactions. More on that later.

You can also generate UML diagrams:

sen_generate_uml(
  TARGET
    fom_lib_uml
  OUT
    ${CMAKE_CURRENT_BINARY_DIR}/fom.plantuml
  HLA_FOM_DIRS
    rpr
    netn
  CLASSES_ONLY
)

Or a browsable reference over the whole FOM, which is usually the quickest way to find a name in one:

sen_generate_html(
  TARGET
    fom_reference
  OUT
    ${CMAKE_CURRENT_BINARY_DIR}/reference
  TITLE
    "RPR and NETN"
  HLA_FOM_DIRS
    rpr
    netn
)

Code generation covers the rest of what Sen writes from a FOM.

Data type mappings

  • HLA Enumerations are mapped to Sen enumerations. The declared representation picks the underlying integer type, and a representation Sen does not know is an error at generation time. Each enumerator keeps the value the FOM gives it, because in HLA the value is normative and the name is documentation. Two enumerators whose names collide after conversion to camel case get a numeric suffix. A <value> holding a list or a range is not supported: only the leading number is read, and the rest is ignored without a message. HLAother alternatives are not handled.
  • HLA Records are mapped to Sen structs. A record that draws its fields from another with <include> becomes a struct extending that one, which is what Sen's single inheritance is for.
  • HLA Variant Records are mapped to Sen variants.
  • HLA Arrays of HLAASCIIchar or HLAunicodeChar with a Dynamic cardinality are mapped to Sen strings.2 With any other cardinality they follow the array rules below.
  • HLA Arrays with a fixed cardinality are mapped to Sen bounded sequences with that maximum. Sen does not generate a fixed-size array here, because on an interface accepting fewer elements is more useful than forcing the exact count.
  • HLA Arrays with a range are mapped to Sen bounded sequences, taking the upper bound as the maximum, as long as that bound is below 1048576.
  • HLA Arrays with a Dynamic cardinality, and arrays whose range reaches that limit, are mapped to Sen unbounded sequences. RPR uses [1..2147483647] in several places, and those are unbounded.
  • HLA Simple Types with units are mapped to Sen quantities.
  • Type names in HLA are transformed to UpperCamelCase.
  • Enumerator names in HLA are transformed to lowerCamelCase.

A FOM may use the same type for more than one alternative, telling them apart with a discriminant enumeration. RPR does this in SpatialVariantStruct, where one structure serves both world and body axis coordinates.

In C++ a variant becomes a std::variant holding the alternatives in the FOM's order, so an alternative is picked by index, and where a type repeats that is the only way, since std::get<Type> cannot say which of the two you meant. Sen generates a constant for each alternative, named after its enumerator in the FOM, so you can write the index by name. The discriminant enumeration itself carries the FOM's own values and is not the index.

The Sen Query Language does not work this way. A query names the alternative by its type, as in spatial.SpatialFPStruct.worldLocation.x, and a type name is the only thing a query path can say. So where a FOM uses one type for two alternatives, a query has no way to name one of them. SpatialFPStruct is both the world-axis and the body-axis alternative of SpatialVariantStruct, and a query mentioning it cannot express which one you mean.

HLA data representations are mapped as follows:

HLA Representation Sen
HLAfloat32BE,HLAfloat32LE f32
HLAfloat64BE, HLAfloat64LE f64
HLAinteger16BE, HLAinteger16LE i16
HLAinteger32BE, HLAinteger32LE i32
HLAinteger64BE, HLAinteger64LE i64
HLAoctet u8
HLAoctetPairBE, HLAoctetPairLE u161

A FOM may also declare its own representations, as RPR does with RPRunsignedInteger32BE and friends. You do not need to register these with Sen: it reads every basicData entry in your FOM and picks the Sen type from the encoding text, which must begin with one of the following.

encoding starts with Sen
8-bit unsigned integer u8
16-bit unsigned integer u16
32-bit unsigned integer u32
64-bit unsigned integer u64
16-bit signed integer i16
32-bit signed integer i32
64-bit signed integer i64

Anything else stops the generation with an "unknown encoding" error naming the representation. Note that there is no 8-bit signed entry, because Sen has no i8.

The types the standard defines are mapped by name. Sen does not read their declarations, which is why it can skip the MIM:

HLA Type Sen
HLAASCIIchar, HLAbyte u8
HLAunicodeChar u16
HLAcount, HLAseconds,HLAmsec,HLAindex i32
HLAinteger64Time i64
HLAfloat64Time f64
HLAboolean bool
HLAASCIIstring, HLAunicodeString string

Some FOMs define their own version of a type the standard already has. These are the predefined mappings for those:

FOM Type Sen
RPR RPRboolean bool

HLA does not define a standard for naming units, so Sen matches the unit text in your FOM against the list below. The match is exact, spaces and brackets included, and a unit that is not on the list simply produces a plain number instead of a quantity. The list is what published models have been seen to use, not a standard anybody publishes.

HLA Unit Sen Unit
"meter per second squared (m/(s^2))" m_per_s_sq
"degree (deg)" deg
"radian (rad)" rad
"radian per second (rad/s)" rad_per_s
"meter (m)" m
"hertz (Hz)", "interrogations/second" hz
"kilogram (kg)" kg
"revolutions per minute (RPM)", "RPM" rpm
"degree Celsius (C)" degC
"microsecond" us
"millisecond (ms)" ms
"second (s)" s
"meter per second (m/s)" m_per_s
"micron" um
"decimeter per second (dm/s)" dm_per_s

Class mapping

The HLA class hierarchy inherits from hla.ObjectRoot, the class that HLAobjectRoot is mapped to. In generated C++ it appears as hla::ObjectRootBase, deriving from sen::NativeObject and so from Object.

That base class carries one property, rtiId. It exists because HLA object instances carry an instance ID assigned by an RTI, so a bridge to an HLA federation has somewhere to put it. Sen never populates it: nothing in Sen talks to an RTI, so the property stays empty unless your own code writes to it. It is completely independent of the identification Sen provides in the Object class.

HLA class attributes are mapped to Sen class properties, with the attribute name converted to lowerCamelCase. Five names are already taken by the Object class, so an attribute that would collide with one of them gets NonSen appended: name, id, localName, lastCommitTime and propertyUntyped become nameNonSen, idNonSen and so on. If a generated property has a name you did not expect, this is why.

Property mode is mapped as follows:

updateType sharing Sen
Static PublishSubscribe or Publish staticRW
Static Subscribe or Neither staticRO
anything else anything else dynamicRO

So an attribute that changes over time arrives read-only by default: other objects can watch it, but only its owner sets it. That is usually what a FOM means, since an attribute is normally published by whoever owns the entity.

When you do want a dynamic property writable from outside, say so in the mappings file instead of changing the FOM.

The mappings file

The FOM describes a data model; it does not describe how you want to use it. The mappings file is where that goes. You pass it to the code generator with HLA_MAPPINGS_FILE, and it decorates the imported model without touching the FOM itself. The standard's XML stays exactly as published, and your project's decisions live alongside it.

It can make a property writable, and turn an interaction into a method or an event.

<senMapping>
    <class name="BaseEntity.PhysicalEntity.Munition">
        <property name="launcherFlashPresent" writable="true"/>
        <event hlaInteraction="MunitionDetonation" pack="true"/>
    </class>
</senMapping>

The class name is the class's path in the FOM hierarchy, not its Sen name: BaseEntity.PhysicalEntity.Munition, not rpr.Munition. Property names, by contrast, are the Sen names: lowerCamelCase, as generated.

Properties

writable="true" gives the property a public setter. The FOM decides everything else about it.

Interactions

An interaction is sent once and delivered to every federate subscribed to its class, so it carries several distinct communication patterns:

  • True broadcasts.
  • Requests made to one or multiple "back-ends" (where the receiver may or may not be defined, and a request ID is usually provided).
  • Responses to requests (where the request id is sent back to all potential senders for them to manually correlate).
  • Messages between participants.

In HLA, interactions are mapped as a hierarchy which is parallel to the class hierarchy.

In Sen:

  • Classes for objects that have properties, methods and can emit events.
  • Method calls, made to some specific instance, with arguments, and which might return a value.
  • Events, which interested listeners can react to.

To map HLA interactions to the Sen world you need to provide an XML file defining how you want to map those interactions to Sen methods or events.

Methods that do not return.

If you want to map an interaction as a method that doesn't return anything, you need to define the class that will hold the method and the interaction from where the method name and the arguments will be obtained. For example:

<class name="WeaponServer">
    <method hlaInteraction="CreateMunition"/>
    <method hlaInteraction="DeleteMunition"/>
</class>

If you don't want your method to have all the parameters that are defined in the HLA interaction, you can ignore them as follows:

<class name="WeatherServer">
    <method hlaInteraction="METOC_Interaction.Request.RequestWeatherCondition">
        <ignore parameter="EventId"/>
        <ignore parameter="ServiceId"/>
    </method>
</class>

Methods that return.

You can use the definition of an interaction to define the return value of a method. For example:

<class name="WeatherServer">
    <method hlaInteraction="METOC_Interaction.Request.RequestLandSurfaceCondition">
        <ignore parameter="EventId"/>
        <ignore parameter="ServiceId"/>

        <return hlaInteraction="METOC_Interaction.Response.WeatherCondition.LandSurfaceCondition">
            <ignore parameter="EventId"/>
            <ignore parameter="Status"/>
        </return>
    </method>
</class>

In this case, the return type will be a Sen structure holding all the parameters of the interaction.

Returning a plain type instead of a structure.

If the answer is a single value and not a set of parameters, name a FOM datatype directly with dataType and leave hlaInteraction off:

<class name="TerrainServer">
    <method hlaInteraction="RequestGroundHeight">
        <return dataType="HeightFloat32"/>
    </method>
</class>

The two are alternatives: hlaInteraction builds a structure from a response interaction, dataType uses a datatype the FOM already defines.

Methods that return errors.

If you don't want to return error codes, you can throw a std::exception, and Sen will propagate it to the caller.

Local-only methods.

Set the local attribute to true on the method node, for example:

<class name="MyClass">
    <method hlaInteraction="MyInteraction" local="true">
        <return hlaInteraction="MyResultInteraction"/>
    </method>
</class>

Events.

Events can be injected into classes in a similar way:

<class name="BaseEntity.PhysicalEntity">
    <event hlaInteraction="Collision">
        <ignore parameter="EventIdentifier"/>
        <ignore parameter="IssuingObjectIdentifier"/>
    </event>

    <event hlaInteraction="CollisionElastic">
        <ignore parameter="EventIdentifier"/>
        <ignore parameter="IssuingObjectIdentifier"/>
    </event>

    <event hlaInteraction="WeaponFire">
        <ignore parameter="EventIdentifier"/>
    </event>
</class>

Packing Arguments.

By default, each parameter in an interaction is mapped to an argument. If you want to pack them into a single structure, set the "pack" attribute to "true" (this works for methods and events). For example:

<class name="BaseEntity.PhysicalEntity.Munition">
    <event hlaInteraction="MunitionDetonation" pack="true">
        <ignore parameter="EventIdentifier"/>
        <ignore parameter="MunitionObjectIdentifier"/>
    </event>
</class>

The struct takes the interaction's name, rpr::MunitionDetonation here, and holds the parameters left after the ignores. Sen also generates a struct with every parameter the interaction declares, because a FOM's interaction is a type in its own right; where a mapping packs and ignores at once, that one takes a Full suffix, rpr::MunitionDetonationFull. An interaction no mapping packs has one struct under its own name and no suffixed twin.

<ignore> and pack together: a behaviour change

<ignore> used to have no effect on a packed callable. The complete struct and the packed one were both looked up by the interaction's name, so whichever was built first won, and that was the complete one; the ignored parameters stayed in the payload. They are now left out, which is what the mapping asked for, and the struct holding all of them is the one that took a new name rather than the one your code names. If you pack and ignore together, check whether any code of yours reads a parameter it asked to drop.

<ignore> without pack is unaffected: those parameters were, and are, simply left out of the argument list.

The changelog names the release this lands in.

Optionals.

There's no formal support for truly optional values in HLA. However, the Sen code generator treats an attribute or an interaction parameter as optional when its documentation (semantics) starts with one of the following:

  • "Optional."
  • "Optional ("
  • "Optional:"
  • "Optional,"

Adding members to a published FOM

A FOM you did not write is a FOM you cannot edit. When a project needs one more attribute on PhysicalEntity, the usual answer is a subclass, which puts the attribute on a different class from the one everybody else publishes and subscribes to. Extensions are the other answer: an extra XML file that adds the attribute to PhysicalEntity itself, leaving the published module untouched.

You pass those files with HLA_EXTENSIONS_FILE, beside the FOM directories:

add_sen_package(
  TARGET site_model
  HLA_FOM_DIRS hla/rpr hla/netn
  HLA_EXTENSIONS_FILE extensions/site_physical_entity.xml
  NO_SCHEMA
)

and on the command line with --extensions, which takes files or directories of them:

cli_gen cpp fom --directories=hla/rpr --extensions=extensions/site_physical_entity.xml

An extension is an object model like any module, carrying only what it adds. It can sit beside the modules or anywhere else; a file named as an extension is never also read as a module. To add an attribute to BaseEntity.PhysicalEntity, nest the object classes down to it and put the attribute on the innermost one:

<?xml version="1.0" encoding="UTF-8"?>
<objectModel xmlns="http://standards.ieee.org/IEEE1516-2010">
    <objects>
        <objectClass>
            <name>HLAobjectRoot</name>
            <objectClass>
                <name>BaseEntity</name>
                <objectClass>
                    <name>PhysicalEntity</name>
                    <attribute>
                        <name>SiteTrackNumber</name>
                        <dataType>Integer32</dataType>
                        <updateType>Conditional</updateType>
                        <sharing>PublishSubscribe</sharing>
                        <transportation>HLAreliable</transportation>
                        <semantics>The track number this site assigns to the entity.</semantics>
                    </attribute>
                </objectClass>
            </objectClass>
        </objectClass>
    </objects>
</objectModel>

Nested, not dotted: the class is identified by its path in the hierarchy, the same path a mappings file uses. It needs no <modelIdentification>, no name and no Dependency reference, and a class node in an extension needs no <semantics> — carrying a member is what makes it a contribution. An extension does not join the set that Dependency references resolve against, so no module can name it as a dependency.

What can be added: an attribute to an object class, an enumerator to an enumeration, a field to a fixed record, an alternative to a variant record, and a parameter to an interaction class. Each goes in the element the FOM uses for it, under the type's own name:

<dataTypes>
    <enumeratedDataTypes>
        <enumeratedData>
            <name>DesignatorCodeEnum16</name>
            <enumerator>
                <name>SiteLocalCode</name>
                <value>60000</value>
            </enumerator>
        </enumeratedData>
    </enumeratedDataTypes>
</dataTypes>

What the generated code looks like

The type stays where its own module put it. A contributed attribute on PhysicalEntity appears in rpr.PhysicalEntity, in the file generated for RPR-Physical_v2.0.xml, with the same accessors, change notifications and serialization as the attributes the module declares. No new file appears and no existing one is removed. Measured on the bundled RPR modules: one contributed attribute changes two of the 56 generated files and nothing else.

A contributed interaction parameter arrives the same way, as the last argument of the event or method the mapping binds that interaction to.

Members are appended, never interleaved. That matters in one place: the generated constructor takes every static read-write property in FOM declaration order, so a contributed Static attribute with PublishSubscribe sharing adds a parameter at the end:

PhysicalEntityBase(std::string name, EntityTypeStruct entityType,
                   EntityIdentifierStruct entityIdentifier, EntityTypeStruct alternateEntityType,
                   Integer32 siteCallsign);   // contributed

Every existing construction site then stops compiling until it passes the new argument. This is loud rather than silent, and within one project everybody rebuilds, but it is worth knowing before you choose: declare the attribute Conditional instead and it becomes a dynamic read-only property, which stays out of the constructor. Accessor names are alphabetical and so do not move.

A type that only an extension declares is adopted by the module that referenced it and takes that module's package, because the generator emits one file per module and a file of its own would have to import the module that uses it and be imported by it.

When several modules declare one type

A type named by more than one module is one type, holding the union of what they declare. This is how HLA 4 defines merging over a set of modules, and Sen uses its two words for the two roles: the declaration carrying <semantics> is the complete definition and every other is an extension of it.

The complete definition decides the package. If rpr/RPR-Physical_v2.0.xml defines BaseEntity.PhysicalEntity and a module in another directory adds an attribute to it, there is one rpr.PhysicalEntity carrying both, and the adding module generates no class of its own. Which declaration is the complete definition does not depend on the order the files are read, so the same directories give the same model on every machine; a directory is read in name order for the same reason.

This matters most for what it stops being silent about. A module that declared part of a type and carried no semantics used to have its members dropped, and nothing said so.

What is refused

A member declared twice has to agree. Two declarations of one attribute with different types, modes or transports are a contradiction rather than a merge, and generation stops naming both files:

property 'camouflageType' of 'PhysicalEntity' is declared twice and the two do not agree:
'.../RPR-Physical_v2.0.xml' and '.../site_physical_entity.xml'

An exact repeat is kept once instead. The same holds for the type's own header: a record's <include>, an enumeration's <representation>, a variant's <discriminant>. A file whose root is not an object model is refused, naming the file. A contribution to a class no module declares is dropped, because a contribution adds to a definition and there is none to add to.

On the wire, and in your source

A property, an enumerator and a record field are identified by name — a hash of it for a property, the written value for an enumerator — so adding one does not renumber the others, and a participant that does not know it ignores it under the default relaxed compatibility. A variant alternative is not: its discriminant is the position it was read in, so a contributed alternative takes the next key and the order the extension files are given decides which. Give them in a fixed order, and treat a contributed alternative as a change every participant has to take together. It is also not free in C++ source, for the constructor reason above. Run-time compatibility covers what a participant without the extension does with a message that carries it.

An example

examples/packages/fom_extensions adds one member of each kind to types the bundled RPR FOM declares, one file per kind: a property on BaseEntity.PhysicalEntity, an enumerator on DesignatorCodeEnum16, a field on UniformGeomRecStruct, and an alternative on GridAxisTypeVariantStruct. The files sit in the example's own directory, not in examples/packages/hla_fom/rpr, which stays exactly as published.

examples/packages/fom_extensions/CMakeLists.txt
sen_generate_uml(
  TARGET fom_extensions_uml
  OUT ${CMAKE_CURRENT_BINARY_DIR}/fom_extensions.plantuml
  HLA_FOM_DIRS
    ../hla_fom/rpr
    HLA_EXTENSIONS_FILE
    extensions/site_physical_entity.xml
    extensions/site_enumerations.xml
    extensions/site_records.xml
    extensions/site_variants.xml
    extensions/site_interaction.xml
  CLASSES_ONLY
)

Build fom_extensions_uml and the diagram shows the contributed members on the published classes. The target is on demand rather than part of the default build.

No package is generated from it, which is deliberate: a second package over the same FOM directory would produce a second, different rpr model, and two components claiming one class with different members is the clash libs/kernel/test/integration/type_clash exists to catch. That extensions reach generated C++ is held elsewhere — by the parser's own tests, and by test/util/fom_extension, a file that names an accessor from the module and one from the extension and so does not compile if a contributed attribute went missing.

Extensions are a stopgap for what IEEE 1516-2025 (HLA 4) calls extended FOM module merging, using the same vocabulary: a complete definition owns the type, and an extension adds to it. When Sen reads HLA 4 modules directly, an extension becomes an ordinary module and the separate input goes away; what an extension file says will not have to change.

Customizing the generated code

The same codegen_settings.json that customizes STL-declared classes also works on classes imported from a FOM. checkedProperties asks your implementation for approval before an external write is applied, and deferredMethods, which lets an implementation answer when it chooses. Customizing the generated code explains both.

The one thing that differs for FOM classes is how you name the class, and it is not the name the mappings file uses:

{
  "classes": {
    "rpr.Munition": {
      "checkedProperties": ["launcherFlashPresent"]
    }
  }
}

The key is the Sen qualified name: the package, then the class. The package is the FOM directory name in lower case, so HLA_FOM_DIRS rpr netn gives packages rpr and netn, and the class is its generated name without the hierarchy path. The generated FOM reference lists every name in this form, which is the quickest way to find the one you want.

So the same class is written differently depending on which file you are in:

File Name to use Example
The mappings XML Path in the FOM hierarchy BaseEntity.PhysicalEntity.Munition
codegen_settings.json Sen qualified name rpr.Munition

Why these are not in the mappings file

The mappings file decides the interface, the settings file decides the implementation. Whether an interaction becomes a method or an event, what its arguments are, and whether a property has a public setter all change what other components see, so they belong to the model. Whether your implementation validates a write before accepting it, or answers a call later instead of immediately, changes nothing about the interface, and another component cannot tell, so it belongs to your project rather than to the shared model.

That matters most when the FOM is shared. Two organizations agreeing on the same model can make opposite implementation choices without either of them having to change anything the other reads.

If you have an older mappings file

checked on a property and deferred on a <return> used to be written in the mappings file, and still work so that existing projects keep building. They print a warning asking you to move them here, and that is the direction to go: they are implementation choices, and they should not sit in the file that defines the interface.

What Sen does not do

Sen is not an RTI and does not natively join HLA federations. As with any other protocol, that is done by writing an adapter. The same applies to DIS. Sen participants talk to other Sen participants over Sen's own transport, and anything outside that is reached through a bridge.3 The mental model maps the vocabulary across.

Sen builds a C++ representation of what a FOM expresses. It does not reproduce how that FOM is encoded. Attributes describing the wire format, such as size, endian and the width behind a representation, are read where they help pick a type and dropped otherwise, because Sen does not implement that wire format. Where the standard's type system is redundant Sen collapses it: booleans are declared as enumerations, in more than one way, and all of them become bool.

These are decisions, and they are separate from the list below of things that are not supported yet. An adapter bridging to a real federation reads the FOM itself for encoding detail, because that is a transport concern and this layer is an API.

Sen generates no encoder or decoder, and does not need to. Encoding is a wire concern and belongs to whatever is on the wire: an adapter or gateway does it against the FOM, using the encoding helpers its RTI provides.

Sen reads the IEEE 1516.2-2010 DIF, and only that one. A FOM is recognized by the namespace on its objectModel element, so a file declaring a different one is refused by name rather than read into a model missing most of what it declares:

'site.xml' is not a FOM Sen can read: it declares
'http://standards.ieee.org/IEEE1516-2025'. Sen reads the IEEE 1516.2-2010 DIF,
'http://standards.ieee.org/IEEE1516-2010'

The namespace found is quoted back rather than matched against a list, so a FOM from a standard later than this page knows about still gets a message naming the version it carries. A file with no namespace at all, which is the shape standards before 2010 used, is refused the same way. The namespace must be the element's default: Sen does not resolve a prefix, and a hla:objectModel is refused for that rather than silently skipped.

Only files with an .xml extension are processed. HLA 4's own module merging is not read yet; adding members to a published FOM is the stopgap for it.

These are not supported yet: ownership, order, updateCondition and dimensions. A FOM declaring DivestAcquire on an attribute imports cleanly and that declaration is currently dropped, so plan on Sen's own ownership model for now; see Main concepts. A FOM declaring TimeStamp order imports the same way; Time management covers what that means. These are areas we intend to grow into rather than deliberate omissions.

The MIM is skipped on purpose. Its two roots are recognized so class hierarchies resolve, and the rest of it describes managing a federation, which needs an RTI.

One more thing before you import a shared FOM: when two participants disagree about a type, Sen adapts rather than refuses, and some of those adaptations lose data: numbers clamp, sequences truncate. Under the default relaxed compatibility each of those is warned about, and strict refuses the conversion instead. Run-time compatibility has the settings, and is worth reading if your FOM is shared across organizations.

Time management

Sen's stepped mode is not a system without HLA time management. It is one point inside it, the most constrained one: under virtualTime every kernel is in effect both time-regulating and time-constrained, with one lookahead and one timestep shared by all of them, and a barrier at every step.

The services you would reach for all serve the same purpose. Per-federate lookahead, LBTS, the distinction between a time advance request and a next message request, the four combinations of regulating and constrained: every one of them exists so that participants can sit at different logical times at once. The barrier forbids that, so they collapse into advancing everyone by delta. Nothing is left to declare or negotiate, and everything arrives in receive order.

Lookahead is the exception, in that it survives but nobody declares it. processNoFlush(delta) cycles the components without publishing what they produced, and flushOutputs() publishes it, so a value written in step T cannot be read before T+1. Lookahead is delta, and the double buffer is what enforces it. A federate that understates its lookahead corrupts the federation; a Sen component has no way to understate it, because setNext cannot make a value visible in the same step.

Within one process stepping is deterministic, and across processes it is not yet. And a component that cannot be stepped keeps following the real clock, ether among them, because the network does not step.

Simulation time itself belongs to your model. The execution time from getTime() can serve as it directly, since Sen does not interpret what it means; see the execution model. If your model needs a time of its own, a scenario clock or a time of day, carry it as an ordinary property and Sen will move that around without interpreting it either. Every object also has a lastCommitTime, which follows virtual time when the kernel is stepped.

In your FOM, order is read and dropped, while transportation is honored and maps as the table above shows. In the modules shipped with the examples that costs nothing: 15 declarations ask for TimeStamp order, all of them in RPR-Minefield_v2.0.xml, and all 15 also declare HLAreliable, so they arrive reliably and in order anyway. That is true of these modules and is not a general rule, because the two elements are independent: a FOM can ask for HLAbestEffort with TimeStamp, wanting ordering without reliability, and Sen has no way to express that. Sen also reads none of the <tags> section, which is where RPR keeps the DIS timestamp.


  1. The OMT treats an octet pair as two opaque bytes with no arithmetic meaning. Mapping it to u16 gives it one, so the rules in Run-time compatibility, which widen and clamp, will apply arithmetic semantics to something the standard says has none. Treat it as storage rather than as a number. ↩

  2. In C++ a Sen string is rendered as std::string, while a bounded sequence becomes sen::StaticVector<T, size>, because the standard library has no bounded-capacity container of that kind. See Sequences and arrays. Support for fixed and bounded std::string-like containers is planned, and will make this more convenient for C++ users. ↩

  3. Adapters of this kind do exist, but they are currently internal to Airbus and are not part of this distribution. They may be open-sourced in the future; nothing is committed. ↩