The Parameter - SimDynamX/SpaceTeamsPRO GitHub Wiki

The Fundamentals → The Parameter

Up to date for Platform 0.54.2

Written by Connor Jakubik.


Note

The .h links in here won't work for typical users because they are links to the internal source code copy. These C++ header files are however available inside the install directory of STP.

Param Basics

What is a Param?

A Param (short for Parameter) is a general holder of data that can be any of the Space Teams compatible types. The Space Teams Platform uses the Param for almost all simulation data that changes through time. In a Space Teams simulation, the code behind Params handles thread-safety and network replication of value changes to all connected programs. As a result, the user can program Space Teams functionality across multiple programs without implementing synchronization code themselves. Outside of a simulation, Params can be used as a type-safe data structure for various purposes including editing Config files.

Tip

Type safety means the formatting and contents of a value are checked and assured to conform to a certain "type" standard. This is important for simulation data, where code that saves high precision data to a file without specialized settings could result in loss of precision or data corruption.

Param Maps

We define a way to organize Param values with names and groupings. The data structure suitable for this task is a dictionary (C++: map, Python: dict, JSON: object). A dictionary maps key-values to value-values, often with unique keys. The values are heterogenous types to enable more natural and type-safe programming of functionality.

Here's an example of a dictionary (in this case, a JSON object):

{
  "name": "John", 
  "age": 30, 
  "single": true
}

In Space Teams code, we represent both sim initial conditions data and live sim state through several maps of keys to parameters. The ParamMap class (C++ spaceteams::GenParamMap, Python st.ParamMap) is our implementation of this map. Each key is a unique string made up of alphanumeric characters and # and _, and is at most 31 characters long. Each value is a Param value, and may be of one of the below listed types.

Param Types

This is the list of Space Teams valid Parameter types:

  • bool (true/false)
  • Integer Variants
    • The maximum value of each of these before overflow is 2^(number) - 1.
    • Signed types have half the range but can be negative.
    • unsigned (positive only)
      • uint8
      • uint16
      • uint32
      • uint64
    • signed
      • int8
      • int16
      • int32 (most common general-use integer)
      • int64
  • duration
    • microsecond (1E-6 s) precision
  • timestamp
    • microsecond (1E-6 s) precision
    • Using the TAI time zone, which is like UTC but without leapseconds (ahead by 37 seconds as of 2023/01/25).
  • uuid (Universally Unique ID)
  • EntityID (Currently, an alias for UUID)
  • EntityRef
    • Explained lower on this page.
  • float (32-bit fractional number)
  • double (64-bit fractional number)
  • char (ASCII 1-byte character)
  • Vectors
    • doubleV2 (2D vector)
    • doubleV3 (3D vector)
    • doubleV4 (4D vector, sometimes quaternion)
  • Matrices
    • doubleM2x2 (2x2 matrix)
    • doubleM3x3 (3x3 matrix)
    • doubleM4x4 (4x4 matrix)
    • doubleM5x5 (5x5 matrix)
    • doubleM6x6 (6x6 matrix)
  • string (text in ASCII 1-byte characters)
  • array
    • This is a special param type, which is a dynamic-size array of a certain other param type.
  • map
    • Explained lower on this page.
  • See more details where SCVarType is mentioned in CoreSCTypes.h.
    • enum SCVarType defines the whole list of Param types
    • SCVarTypeToName shows the mapping between SCVarType enum values and their corresponding labels (which are what is used to define a param type on the human-facing parts of the Platform)
    • SCVarTypeToName_CppFormat shows the mapping between SCVarType enum values and their corresponding C++ class (which is what you use for the C++ template argument)

Besides storing a mapping of keys to Param values, ParamMap provides an interface of HasParam/GetParam/SetParam/AddParam/DeleteParam functions that handle the "hard parts" of error-handling, thread-safety, change-tracking, and more. Using a ParamMap or one of its subclasses, you can read and modify parameter values and disregard the "extra work" required to implement higher-level features such as replicating changes to all other computers in a simulation.

Most of the time, you'll be using GenParamMaps subclasses (like Entity or System), but all of them have the same Param interface. Here is an example of this interface in action. We assume here that there is an already constructed GenParamMap "pmap" that we can access with the pointer GenParamMap* pmap.

Tip

Typically in C++ we actually use std::shared_ptr<GenParamMap> instead of GenParamMap*. In Python this is transparent to the user; the st.ParamMap object always represents a reference-counted pointer.

// C++
// Get (a copy of) the value of the Mass param, and
// save that to an intermediate variable.
double mass = pmap->GetParam<double>("Mass");
// Find a new mass value through some calculation.
double new_mass = 0.9 * mass;
// Set the Mass param to the new value
pmap->SetParam<double>("Mass", new_mass);

// Add a new parameter
bool is_heavy = true;
pmap->AddParam<bool>("IsHeavy", is_heavy);

// Use a parameter if it's there
if(pmap->HasParam<SC_DoubleV3>("SomeVector"))
    doSomething(pmap->GetParam<SC_DoubleV3>("SomeVector"));
# Python
# Get (a copy of) the value of the Mass param, and
# save that to an intermediate variable.
mass = pmap.GetParam(st.VarType.double, "Mass")
# Find a new mass value through some calculation.
new_mass = 0.9 * mass
# Set the Mass param to the new value
pmap.SetParam(st.VarType.double, "Mass", new_mass)

# Add a new parameter
is_heavy = True
pmap.AddParam(st.VarType.bool, "IsHeavy", is_heavy)

# Use a parameter if it's there
if pmap.HasParam(st.VarType.doubleV3, "SomeVector"):
    doSomething(pmap.GetParam(st.VarType.doubleV3, "SomeVector"))

Creating an empty Param Map

Creating a fresh Param Map is necessary for a few tasks such as generating a payload for the global st.SimGlobals.Publish() function. Note that this is not the same as manipulating parameters on an Entity, as the standalone Param Map does not replicate changes to anything in a Space Teams sim.

// C++
auto my_pmap = std::make_shared<GenParamMap>();
# Python
my_pmap = st.ParamMap()

ParamMap usage in the Platform

GenParamMap is the base class for many Platform classes:

  • Entity
    • Object in the running simulation, represented by Param values.
    • There is significance to certain parameter values that is automatically handled by some Platform applications to render or interact with the Entity in a certain way.
      • For example, the Location parameter is used to communication the location of the entity relative to some reference frame. This is used to update the graphical representation of that entity.
    • Entities use the change-tracking functionality of GenParamMap to detect changes, additions, and deletions of Params, and replicate those changes to other
  • EntityConfig
    • A ParamMap that represents the "initial values" of an Entity's parameters.
    • An EntityConfig object can be used to spawn an Entity with matching initial Param values during a running simulation.
    • An EntityConfig that has been saved to a file can be used in a SimConfig as a "Template" (starting point for defining an Entity).
    • During a running simulation, code somewhere in the Platform can spawn an Entity with its initial Param values coming from an EntityConfig.
  • System
    • Logic that reads and modifies Entity parameters in its own update loop.
    • Many of these make up all the "behavior" of a simulation.
    • Usually uses Params for configuration settings or EntityRef references.
    • Further explained in another wiki doc.
  • SystemConfig
    • A ParamMap that represents the "initial values" of a System's parameters.

Important

Only Entities have automatic replication of param changes across the simulation. Systems on-purpose do not have their per-instance parameters replicate to discourage the user from using them to store data that should exist on an Entity. _Config objects are meant to be used as a format for sim data that is not currently representing a live object inside a running sim.

Entity Params

The C++ code from earlier can be directly reused for the example of accessing and modifying parameters on an Entity:

// C++
// Get (a copy of) the value of the Mass param, and
// save that to an intermediate variable.
double mass = entity->GetParam<double>("Mass");
// Find a new mass value through some calculation.
double new_mass = 0.9 * mass;
// Set the Mass param to the new value
entity->SetParam<double>("Mass", new_mass);

// Add a new parameter
bool is_heavy = true;
entity->AddParam<bool>("IsHeavy", is_heavy);

// Use a parameter if it's there
if(pmap->HasParam<SC_DoubleV3>("SomeVector"))
    doSomething(entity->GetParam<SC_DoubleV3>("SomeVector"));
# Python
# Get (a copy of) the value of the Mass param, and
# save that to an intermediate variable.
mass = entity.GetParam(st.VarType.double, "Mass")
# Find a new mass value through some calculation.
new_mass = 0.9 * mass
# Set the Mass param to the new value
entity.SetParam(st.VarType.double, "Mass", new_mass)

# Add a new parameter
is_heavy = True
entity.AddParam(st.VarType.bool, "IsHeavy", is_heavy)

# Use a parameter if it's there
if entity.HasParam(st.VarType.doubleV3, "SomeVector"):
    doSomething(entity.GetParam(st.VarType.doubleV3, "SomeVector"))

The difference for using Entity instead of GenParamMap is the Entity has utility code in the Platform core that automatically replicates the Param value changes from SetParam<double>("Mass" and AddParam<bool>("IsHeavy" to other computers in the multiuser simulation. So, any param-changing code only needs to run on one computer, and the Platform core netcode keeps every parameter value in sync across the internet by sending change data to other users.

At this point, the Platform's ParamMap-based data structures are starting to resemble more enterprise database software than some normal simulation data structure.

System Instance Params

Systems have parameters too!

Note

Some C++ Systems use functions with Inst in front of Param. This is deprecated functionality. It will likely still work, but avoid using this for new code.

// C++
void SomeSystem::init()
{
    if(GetParam<bool>("WaitDuringInit"))
    {
        std::this_thread::sleep_for(100ms);
    }
}
# Python, in a ST Python System

this = st.GetThisSystem()

if this.GetParam(st.VarType.bool, "WaitDuringInit"):
    time.sleep(0.1)

Notably, Systems do NOT have their per-instance param changes replicated to any other computers. This is because Systems are each only supposed to run on one computer in the multiuser simulation.


Nested Param

For organization purposes and other reasons, GenParamMap supports nesting. It does this by itself being another GenParam subclass.

In order to access parameters that aren't at the top level of the ParamMap, use Nested param accessors, which just change out the single key argument (C++ param_key_t, Python str) for a keys list (C++ nested_param_key_t, Python list[str]), which is easy to create with List Initialization as demonstrated below.

// C++
// Get the mass
double mass = entity->GetParam<double>({"Dynamics","Mass"});
// Reduce the mass
double new_mass = 0.9 * mass;
// Set this new value
entity->SetParam<double>({"Dynamics","Mass"}, new_mass);

// Use a parameter if it's there
if(entity->HasParam<SC_DoubleV3>({"This","That","TheOther","SomeVector"}))
    doSomething();

// Add a new parameter
bool is_heavy = true;
entity->AddParam<bool>({"n1","n2","n3","n4","n5","IsHeavy"}, is_heavy);
// AddParam will generate any missing keys in the nested path if needed.

// Nested Param functions will not compile when you are using initializer-list and there is only one element.
entity->AddParam<bool>({"IsHeavy_2"}, is_heavy); // This errors!
entity->AddParam<bool>("IsHeavy_2", is_heavy); // Use this instead in this case.
# Python (shortened)
# Python nested keys are just a list of keys:
mass = entity.GetParam(st.VarType.double, ["Dynamics","Mass"])
if entity.HasParam(st.VarType.doubleV3, ["This","That","TheOther","SomeVector"]):
    doSomething()

EntityRef

An EntityRef parameter is a wrapper around a pointer to an Entity, which can be used across the network like any other parameter. Use the (C++ SC_EntityRef, Python st.Entity) type as the type for param Get/Set/Add. In C++ code, SC_EntityRef wraps a non-owning Entity::ref (std::weak_ptr), so you'll need to .lock() it to get an owning Entity::owned (std::shared_ptr). Setting or Adding an EntityRef in C++ code requires passing a SC_EntityRef as input. You can construct a SC_EntityRef by passing an Entity::owned or Entity::ref value into a SC_EntityRef constructor.

Usage example:

// C++
// "SomeOtherEntity" is the key for an EntityRef param holding a reference to the "other" entity.
// Notice how the key of the param does not necessarily correspond with the "other" entity's name.
Entity::owned other_en = entity->GetParam<SC_EntityRef>("SomeOtherEntity").lock<Entity>();
//NOTE: can also omit <Entity> portion if only need access to Entity_Base functions

// Get (a copy of) the value of the Mass param, and
// save that to an intermediate variable.
double mass = other_en->GetParam<double>("Mass");
# Python
# "SomeOtherEntity" is the key for an EntityRef param holding a reference to the "other" entity.
# Notice how the key of the param does not necessarily correspond with the "other" entity's name.
other_en = entity->GetParam(st.VarType.entityRef, "SomeOtherEntity")
#NOTE: No need to .lock() in python; we have manipulated the Python API functions to do .lock() internally.
# This could result in an exception if the referenced entity has been deleted from the sim.

# Get (a copy of) the value of the Mass param, and
# save that to an intermediate variable.
mass = other_en.GetParam(st.VarType.double, "Mass")

ParamArrays

Creating Dynamic Arrays

You can create dynamically-sized arrays of GenParams using the special GenParamArray class. The C++ type for these ParamArrays is a std::vector<T> where T is the corresponding C++ type of the array element objects. In Python, list is used. These arrays must be of a homogenous param type, and support static- or dynamic-sized types (as in memory-size, where a string would be dynamic).

Getting a ParamArray from a GenParamMap

As with the GenParams, you'll almost always be using them as part of a GenParamMap. As with before, there are Get/Set/Add functions to help out with this. Here is an example using all of these, assuming we already have an GenParamMap* pmap pointer available to us.

// C++
// Get a dynamic array
std::vector<double> my_array = pmap->GetParamArray<double>("MyArray");
// Add a new value to the returned array
my_array.push_back(42.0);
// NOTE: This DOES NOT modify the "MyArray" parameter on the param map
// To do that, we need to set the new value
pmap->SetParamArray<double>("MyArray", my_array);
// Add a new array to the param map
std::vector<bool> new_array = { true, false, false, true };
pmap->AddParamArray<bool>("NewArray", new_array);
// pmap now has an array at key "NewArray" with value [ true, false, false, true ]
# Python
# Get a dynamic array (will by a python List object)
my_array = pmap.GetParamArray(st.VarType.double, "MyArray")
# Add a new value to the returned array
my_array.append(42.0)
# NOTE: This DOES NOT modify the "MyArray" parameter on the param map
# To do that, we need to set the new value
pmap.SetParamArray(st.VarType.double, "MyArray", my_array)
# Add a new array to the param map
new_array = [ True, False, False, True ]
pmap.AddParamArray(st.VarType.bool, "NewArray", new_array)
# pmap now has an array at key "NewArray" with value [ true, false, false, true ]

Advanced Info - Param C++ Implementation Details

GenParam

Params are implemented in C++ through the GenParam class (GenParam.h). GenParam objects use polymorphism to cleanly handle holding a "generic" value. All GenParam objects have a common set of shared functionality, which means they can be handled in a common way externally.

A few of the features GenParam ensures exist for all param types:

  • Store and check assumed param type when accessed.
  • Store a timestamp for the simulation-time when the param was last modified.
  • Lots of raw memory manipulation stuff necessary for network socket communication.
  • Print a string showing the value of the parameter.

GenParams use C++ template (function<something>(....)) functions, which means the programmer specifies the exact param type using the corresponding C++ class for any use of a Param function. This can be done as follows.

GenParam<double>* my_param = new GenParam<double>(7.8);

It is possible to construct a param without specifying the initial value, but it's always best to pass in an initial value when possible. Once a GenParam has been constructed, you can retrieve its data as follows.

double x = my_param->Get();

You can forget the above GenParam syntax; it is HIGHLY likely that you won't ever run into it again. This is because the Platform presents Params only through accessor functions on the objects that hold params: GenParamMap and its subclasses like Entity and System.

⚠️ **GitHub.com Fallback** ⚠️