Learned Model#
This is the design document for the Python learned model used in the simple_planning_simulator package.
Purpose / Use cases#
This library creates an interface between models in Python and PSIM (C++). It is used to quickly deploy learned Python models in PSIM without a need for complex C++ implementation.
Design#
The idea behind this package is that the model we want to use for simulation consists of multiple sub-models (e.g., steering model, drive model, vehicle kinematics, etc.). These sub-models are implemented in Python and can be trainable. Each sub-model has string names for all of its inputs/outputs, which are used to create model interconnections automatically (see image below). This allows us to easily switch sub-models for better customization of the simulator.

Assumptions / Known limits#
To use this package python3 and pybind11 need to be installed. The only assumption on Python sub-models is their interface.
class PythonSubmodelInterface:
    def forward(self, action, state):  # Required
        """
        Calculate forward pass through the model and returns next_state.
        """
        return list()
    def get_state_names(self):  # Required
        """
        Return list of string names of the model states (outputs).
        """
        return list()
    def get_action_names(self):  # Required
        """
        Return list of string names of the model actions (inputs).
        """
        return list()
    def reset(self):  # Required
        """
        Reset model. This function is called after load_params().
        """
        pass
    def load_params(self, path):  # Required
        """
        Load parameters of the model.
        Inputs:
            - path: Path to a parameter file to load by the model.
        """
        pass
    def dtSet(self, dt):  # Required
        """
        Set dt of the model.
        Inputs:
            - dt: time step
        """
        pass
API#
To successfully create a vehicle model an InterconnectedModel class needs to be set up correctly.
InterconnectedModel class#
Constructor#
The constructor takes no arguments.
void addSubmodel(std::tuple<std::string, std::string, std::string> model_descriptor)#
Add a new sub-model to the model.
Inputs:
- model_descriptor: Describes what model should be used. The model descriptor contains three strings:
- The first string is a path to a python module where the model is implemented.
 - The second string is a path to the file where model parameters are stored.
 - The third string is the name of the class that implements the model.
 
 
Outputs:
- None
 
void generateConnections(std::vector<char *> in_names, std::vector<char*> out_names)#
Generate connections between sub-models and inputs/outputs of the model.
Inputs:
- in_names: String names for all of the model inputs in order.
 - out_names: String names for all of the model outputs in order.
 
Outputs:
- None
 
void initState(std::vector<double> new_state)#
Set the initial state of the model.
Inputs:
- new_state: New state of the model.
 
Outputs:
- None
 
std::vector<double> updatePyModel(std::vector<double> psim_input)#
Calculate the next state of the model by calculating the next state of all of the sub-models.
Inputs:
- psim_input: Input to the model.
 
Outputs:
- next_state: Next state of the model.
 
dtSet(double dt)#
Set the time step of the model.
Inputs:
- dt: time step
 
Outputs:
- None
 
Example#
Firstly we need to set up the model.
InterconnectedModel vehicle;
// Example of model descriptors
std::tuple<char*, char*, char*> model_descriptor_1 = {
    (char*)"path_to_python_module_with_model_class_1",
    (char*)nullptr,  // If no param file is needed you can pass 'nullptr'
    (char*)"ModelClass1"
    };
std::tuple<char*, char*, char*> model_descriptor_2 =   {
    (char*)"path_to_python_module_with_model_class_2",
    (char*)"/path_to/param_file",
    (char*)"ModelClass2"  // Name of the python class. Needs to use the interface from 'Assumptions'
    };
// Create sub-models based on descriptors
vehicle.addSubmodel(model_descriptor_1);
vehicle.addSubmodel(model_descriptor_2);
// Define STATE and INPUT names of the system
std::vector<char*> state_names = {(char*)"STATE_NAME_1", (char*)"STATE_NAME_2"};
std::vector<char*> input_names = {(char*)"INPUT_NAME_1", (char*)"INPUT_NAME_2"};
// Automatically connect sub-systems with model input
vehicle.generateConnections(input_names, state_names);
// Set the time step of the model
vehicle.dtSet(dt);
After the model is correctly set up, we can use it the following way.
// Example of an model input
std::vector<double> vehicle_input = {0.0, 1.0}; // INPUT_NAME_1, INPUT_NAME_2
// Example of an model state
std::vector<double> current_state = {0.2, 0.5}; // STATE_NAME_1, STATE_NAME_2
// Set model state
vehicle.initState(current_state);
// Calculate the next state of the model
std::vector<double> next_state = vehicle.updatePyModel(vehicle_input);