0

I was wondering if it is possible to import an object in a Matlab Level-2 S-function within Simulink.

In the past, I have applied reinforcement learning to dynamic models in Matlab. As a result, I have created some classes that deal with the generation and update of the policy. Now, I need to move to Simulink because I have a more complex dynamic system. I am familiar with C S-functions, but because I already have the Matlab code in two classes I was thinking about using a Matlab S-function that uses these objects.

My work flow is as follows: the main Matlab function where policy object is initialized calls the Simulink file with the dynamic model. In the S-function, the policy object is call to select an action (which is the output of the control system). The policy object (in fact its weights) is then updated in the main Matlab function after a number of simulations of the Simulink file.

So, what I would need is a way to import the policy object in a Matlab S-function in Simulink. I have tried to import it as parameter, but only numeric values are accepted. I cannot keep the object only within the S-function (hence, initialising it within the initialisation function) because I need to update its weights in the main Matlab script.

Is this possible? Any suggestions would be greatly appreciated!

An example of the policy class is as follows:

classdef Policy
    %% Accessible properties:
    properties
        a;                  % selected action index
        actions;            % actions list
        basis;              % type of basis function
        centres;            % list of centres of the RBFs
        exploration_rate;   % exploration rate
        mu;                 % width of each RBF
        nbasis;             % no. basis functions overall
        states;             % list of discrete states
        weights;            % weights of the linear function approximation
    end

    %% Protected properties:
    properties (Access = protected)
        na;                 % no. actions
        ns;                 % no. discrete states
        nrbf;               % no. radial basis functions per action
        state;              % current state
        Q;                  % Q value for each action-state pair
    end

    %% Accessible methods:
    methods %(Access = protected)
        %% Initialization function:
        function obj = Policy(actions,states,epsilon,basis,mu)  
            %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
            % Input:
            % actions: actions list
            % states:  states list or centres of the RBFs
            % epsilon: initial exploration rate
            % delta:   discount factor
            % basis:   type of basis functions
            % mu:      width of each RBF
            %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

            if nargin<4
                basis = 'basis_exact';
            end

            obj.actions = actions;
            obj.states = states;
            obj.exploration_rate = epsilon;

            switch basis
                case 'basis_exact'
                    obj.basis = basis;
                    obj.states = states;
                    obj.ns = size(states,1);
                case 'basis_rbf'
                    obj.basis = basis;
                    obj.centres = states;
                    obj.mu = mu;
                    obj.nrbf = size(states,1);
                otherwise
                    error(['Only exact and radial basis functions',...
                    'supported']);
            end
        end

        %% Setter function for the features' weights:
        function obj = set_weights(obj,weights)
            obj.weights = weights;
        end

        %% Update the exploration rate with the given rate:
        function obj = update_epsilon(obj,rate)
            obj.exploration_rate = obj.exploration_rate*rate;
        end

        %% Select an action:
        function obj = select_action(obj,state)            
            % Store the current state:
            obj.state = state;
            % Compute the state-action values for the current state:
            obj = obj.qvalues();
            % Get the current action with an epsilon-greedy policy:
            obj.a = obj.eGreedy();
        end

        %% Evaluate the features:
        function phi = get_features(obj,state,action)
            % Store the current state:
            obj.state = state;
            % Get the features:
            phi = feval(obj.basis,action);
        end
    end

    %% Protected methods:
    methods (Access=protected)
        %% Find the discrete state:
        function s = discretizeState(obj,x)            
            % Copy the row vector entries (continuous states) to all rows:
            x = repmat(x,obj.ns,1);
            % Select the row using the minimum Eucledian distance:
            [~,s] = min(sum((obj.states-x).^2,2).^0.5);
        end

        %% Get the Q-value function for current state and action:
        function q = qvalue(obj,action)
            phi = feval(obj.basis,action);
            q = phi' * obj.weights;
        end

        %% Get the Q-value functions for the current state:
        function obj = qvalues(obj)            
            % Initialize the Q-values for the current state:
            obj.Q = zeros(obj.na,1);
            % Calculate the state-action values for the current state:
            for a=1:obj.na
                obj.Q(a) = obj.qvalue(a);
            end
        end

        %% Get an action with an epsilon-greedy exploration policy:
        function a = eGreedy(obj)
            % Generate a random number:
            r = rand;

            % Select the action that maximises Q(s)
            if (r>obj.exploration_rate)             
                [~,a] = max(obj.Q); % value, action 
            % Choose a random action:
            else                       
                a = randi(obj.na);  % random integer based on a uniform
            end                     % distribution
        end

        %% Find the features for the exact basis functions:
        function phi = basis_exact(obj,action)
            %Initialize the features:
            phi = zeros(obj.nbasis,1);

            % Find the current discrete state:
            s = discretizeState(obj.state);

            % Find the starting position of the block:
            base = (action-1) * obj.ns;

            % Set the indicator:
            phi(base+s) = 1;   
        end

        %% Find the features for the radial basis functions:
        function phi = basis_rbf(obj, action)
            %Initialize the features:
            phi = zeros(obj.nbasis,1);

            % Find the starting position:
            base = (action-1) * (obj.nbasis/obj.na);
            % This is because the matrix Theta is converted into a line 
            % vector

            % Compute the RBFs:
            for i=1:obj.nrbf
                phi(base+i) = exp(-norm(obj.state-obj.centres(i,:))^2/...
                    (2*obj.mu));
            end
            % ... and the constant:
            phi(base+obj.nrbf+1) = 1;
        end
    end
end
Enrico Anderlini
  • 447
  • 6
  • 21
  • Enter `sfundemos` at the MATLAB command prompt. You can browse some good examples. There are also templates with comments: `msfuntmpl` and `msfuntmpl_basic`. Enter those two and press `Ctrl + D`. In general you can use every MATLAB Object in your Level 2 MATLAB-S-Function. Just make sure the source files are on the MATLAB path. – Sven-Eric Krüger Oct 24 '17 at 09:03
  • Thank you for that! So, if I want to use the same Matlab object in the main file and in the Matlab S-function, I do not need to import it as a parameter, do I? – Enrico Anderlini Oct 24 '17 at 09:26
  • This is where it gets tricky and I am not sure anymore. You can declare a variable as `global` or inside your S-Function you take the object from your base workspace via `obj=evalin('base', 'OBJ')`, work with it and write it back via `assignin('base' 'OBJ', obj)`. I just don't know if this is enough to have your MATLAB-Script and your Simulink-Model refer to the same object in the memory. Have it a try!... – Sven-Eric Krüger Oct 26 '17 at 13:54
  • Thank you for that! I will give it a go. So far, I have found that using a global variable also works, although I feel that is much more dangerous. See answer below on a similar example. Hopefully, your method works better. – Enrico Anderlini Oct 27 '17 at 15:00

1 Answers1

0

Ok, after some trials, I have finally found that the best way is actually to use global variables, i.e. to set the object as global in both the main Matlab script and the Level 2 S-function.

Here you can find a quick example, which I hope will help you save a day of work.

Test.m class:

classdef Test
    properties
        a;
        b;
    end

    methods
        function obj = Test(a,b)
            obj.a = a;
            obj.b = b;
        end

        function obj = change_a(obj,c)
            obj.a = obj.a + c;
        end

        function c = get_c(obj)
            c = obj.a*obj.b;
        end
    end
end

Main Matlab script - trial.m:

clear;
close all;

global test;

test = Test(0,1);

% Simulink file:
sfile = 't1';
% Load the Simulink file:
load_system(sfile);

% Run the simulation:
sout = sim(sfile,'StopTime','5.0');

% Plot data:
t = sout.tout;
c = sout.get('logsout').getElement('c').Values.Data;
figure;
plot(t,c);

t1.slx Simulink file: <code>t1.slx</code> Simulink file test_class.m level 2 Matlab S-function:

function test_class(block)
% rl_control.m      e.anderlini@ucl.ac.uk     23/10/2017
    setup(block);
end

%% Set up the block:
function setup(block)
%   % Register number of dialog parameters:   
%   block.NumDialogPrms = 3;

    % Register number of input and output ports:
    block.NumInputPorts  = 1;
    block.NumOutputPorts = 1;

    % Set up functional port properties to dynamically inherited:
    block.SetPreCompInpPortInfoToDynamic;
    block.SetPreCompOutPortInfoToDynamic;

    % Set up the input ports:
    block.InputPort(1).Dimensions        = 1;        
    block.InputPort(1).DirectFeedthrough = true;

    % Set up the output port:
    block.OutputPort(1).Dimensions       = 1;        

    % Set block sample time to continuous:
    block.SampleTimes = [0 0];

%     % Setup Dwork:
%     block.NumContStates = 1;

    % Set the block simStateCompliance to default:
    block.SimStateCompliance = 'DefaultSimState';

    % Register methods:
%     block.RegBlockMethod('InitializeConditions',    @InitConditions);  
    block.RegBlockMethod('Outputs',                 @Output);  
%     block.RegBlockMethod('Derivatives',             @Derivative);  
end

% %% Initial conditions:
% function InitConditions(block)
%     % Initialize Dwork:
%     block.ContStates.Data = block.DialogPrm(3).Data;
% end

%% Set up the output:
function Output(block)
    global test;
    test = test.change_a(block.InputPort(1).Data);
    c = test.get_c();
    block.OutputPort(1).Data = c;
end

I have tested it and checked that it works. The global variable allows me to use the same object and change it as I wish.

Enrico Anderlini
  • 447
  • 6
  • 21