├── the_model.jpg ├── the_model.slx ├── results_mle2.png ├── results_mrt.png ├── CallingSimFromPython.pptx ├── SECURITY.md ├── plot_results.m ├── LICENSE.md ├── call_sim_the_model.m ├── call_sim_the_model_using_matlab_engine.py ├── README.md ├── call_sim_the_model_using_matlab_runtime.py └── sim_the_model.m /the_model.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/Call-Simulink-from-Python/HEAD/the_model.jpg -------------------------------------------------------------------------------- /the_model.slx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/Call-Simulink-from-Python/HEAD/the_model.slx -------------------------------------------------------------------------------- /results_mle2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/Call-Simulink-from-Python/HEAD/results_mle2.png -------------------------------------------------------------------------------- /results_mrt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/Call-Simulink-from-Python/HEAD/results_mrt.png -------------------------------------------------------------------------------- /CallingSimFromPython.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mathworks/Call-Simulink-from-Python/HEAD/CallingSimFromPython.pptx -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting Security Vulnerabilities 2 | 3 | If you believe you have discovered a security vulnerability, please report it to 4 | [security@mathworks.com](mailto:security@mathworks.com). Please see 5 | [MathWorks Vulnerability Disclosure Policy for Security Researchers](https://www.mathworks.com/company/aboutus/policies_statements/vulnerability-disclosure-policy.html) 6 | for additional information. -------------------------------------------------------------------------------- /plot_results.m: -------------------------------------------------------------------------------- 1 | function figHndl = plot_results(res, plotTitle) 2 | %PLOT_RESULTS Plot results from call_sim_the_model 3 | 4 | % By Murali Yeddanapudi, 19-Sep-2022 5 | 6 | figHndl = figure; hold on; cols = colororder; 7 | 8 | plot(res{1}.x1.Time, res{1}.x1.Data, 'Color', cols(1,:), ... 9 | 'DisplayName', 'x1 from 1st sim with default setting'); 10 | plot(res{2}.x1.Time, res{2}.x1.Data, 'Color', cols(2,:), ... 11 | 'DisplayName', 'x1 from 2nd sim with limits on dx2'); 12 | plot(res{3}.x1.Time, res{3}.x1.Data, 'Color', cols(3,:), ... 13 | 'DisplayName', 'x1 from 3rd sim with input u'); 14 | plot(res{4}.x1.Time, res{4}.x1.Data, 'Color', cols(4,:), ... 15 | 'DisplayName', 'x1 from 4th sim with limits on dx2 and input u'); 16 | stairs(res{3}.u.Time, res{3}.u.Data, 'Color', cols(5,:), ... 17 | 'DisplayName','input u in 3rd and 4th sims'); 18 | 19 | hold off; grid; ylim([-4 3]); 20 | title(plotTitle,'Interpreter','none'); 21 | set(get(gca,'Children'),'LineWidth',2); 22 | legend('Location','southeast'); 23 | 24 | end 25 | 26 | % To call this MATLAB function from Python make sure to install the correct version of Python 27 | % https://www.mathworks.com/help/matlab/matlab_external/install-supported-python-implementation.html?s_tid=srchtitle_Configure%20Your%20System%20to%20Use%20Python_1 28 | % https://www.mathworks.com/content/dam/mathworks/mathworks-dot-com/support/sysreq/files/python-compatibility.pdf 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, The MathWorks, Inc. 2 | All rights reserved. 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 5 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 6 | 3. In all cases, the software is, and all modifications and derivatives of the software shall be, licensed to you solely for use in conjunction with MathWorks products and service offerings. 7 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /call_sim_the_model.m: -------------------------------------------------------------------------------- 1 | %% Simulate the_model in deployed mode multiple times and plot the results 2 | 3 | % By Murali Yeddanapudi on 04-Mar-2022 4 | 5 | %% 1st sim: with default parameter values 6 | res{1} = sim_the_model(); 7 | 8 | %% 2nd sim: with dx2min and dx2max parameter values 9 | stopTime = nan; % Use deafult value 10 | tunablePrms.dx2min = -3; % Specify new value for dx2min 11 | tunablePrms.dx2max = 4; % Specify new value for dx2max 12 | res{end+1} = sim_the_model('StopTime', stopTime, ... 13 | 'TunableParameters',tunablePrms); 14 | 15 | %% 3rd sim: simulate with a non-zero iinput signal 16 | stopTime = nan; % Use deafult value 17 | tunablePrms = []; % Use default values 18 | % Note that, in the model the input u is sampled at a fixed time interval uST (=1) 19 | % So the time axis for the input values is implicit at 1s (=uST) interval 20 | u = [0 2 zeros(1,3) -2*ones(1,2) 0]; 21 | % => u(t) = 2 for t in [1,2), -2 for t in [6,8), 0 otherwise 22 | res{end+1} = sim_the_model(StopTime=stopTime, ... 23 | ExternalInput=u); 24 | 25 | %% 4th sim: with dx2min, dx2max and non-zero input signal 26 | tunablePrms.dx2min = -3; % Specify new value for dx2min 27 | tunablePrms.dx2max = 4; % Specify new value for dx2max 28 | u = [0 2 zeros(1,3) -2*ones(1,2) 0]; 29 | res{end+1} = sim_the_model(TunableParameters=tunablePrms, ... 30 | ExternalInput=u); 31 | 32 | %% Plot some results from the simulations 33 | plot_results(res, 'Results from calling sim_the_model in MATLAB'); 34 | 35 | -------------------------------------------------------------------------------- /call_sim_the_model_using_matlab_engine.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Example showing how to simulate a Simulink model (called the_model) with different 4 | parameter and external input signal values using the MATLAB Engine API for Python. 5 | 6 | The example requires: 7 | 1. MATLAB and Simulink products installed and licensed 8 | 2. MATLAB Engine API installed as a Python package 9 | https://www.mathworks.com/help/matlab/matlab_external/install-the-matlab-engine-for-python.html 10 | 11 | @author: Murali Yeddanapudi 12 | Created on Tue Mar 1 2022 13 | """ 14 | # Install numpy (For example, pip install numpy) 15 | import numpy as np 16 | import matlab.engine 17 | 18 | mle = matlab.engine.start_matlab(); # start the matlab engine 19 | 20 | # Since we are using MATLAB Engine we do not have to configureForDeployment. 21 | # This allows more flexibility to simulate the model in normal mode, and change 22 | # non-tunable parameters. 23 | configureForDeployment = 0 24 | 25 | # Allocate res list to hold the results from 4 calls to sim_the_model 26 | res = [0]*4; 27 | 28 | ## 1st sim: with default parameter values 29 | res[0] = mle.sim_the_model() 30 | 31 | ## 2nd sim: with new values for dx2min and dx2max parameters 32 | tunableParams = { 33 | 'dx2min': -3.0, # Specify a new value for dx2min 34 | 'dx2max': 4.0 # Specify a new value for dx2max 35 | } 36 | res[1] = mle.sim_the_model('TunableParameters',tunableParams, 37 | 'ConfigureForDeployment',configureForDeployment) 38 | 39 | ## 3rd sim: with a non zero input signal 40 | # Note that, in the model the input u is sampled at a fixed time interval 41 | # uST (=1) which cannot be changed since the model is compiled for deployment. 42 | # So the time axis for the input values is implicit at 1s (=uST) interval 43 | # u = [0 2 zeros(1,3) -2*ones(1,2) 0]; 44 | u = np.concatenate([np.zeros(1), 2*np.ones(1), np.zeros(3), -2*np.ones(2), np.zeros(1)]) 45 | # => u(t) = 2 for t in [1,2), -2 for t in [6,8), 0 otherwise 46 | externalInput = matlab.double(u.tolist()) # convert numpy array into matlab array 47 | res[2] = mle.sim_the_model('ExternalInput',externalInput, 48 | 'ConfigureForDeployment',configureForDeployment) 49 | 50 | ## 4th sim: with dx2min, dx2max and non-zero input signal 51 | tunableParams = { 52 | 'dx2min': -3.0, # Specify a new value for dx2min 53 | 'dx2max': 4.0 # Specify a new value for dx2max 54 | } 55 | u = np.concatenate([np.zeros(1), 2*np.ones(1), np.zeros(3), -2*np.ones(2), np.zeros(1)]) 56 | externalInput = matlab.double(u.tolist()) # convert numpy array into matlab array 57 | res[3] = mle.sim_the_model('TunableParameters',tunableParams, 58 | 'ExternalInput',externalInput, 59 | 'ConfigureForDeployment',configureForDeployment) 60 | 61 | ## callback into MATLAB to plot the results 62 | mle.plot_results(res, "Results from sim_the_model using MATLAB Engine") 63 | 64 | input("Press enter to close the MATLAB figure and exit ...") 65 | mle.quit() # stop the matlab engine 66 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simulate a Simulink® model from Python® 2 | 3 | This example illustrates two ways to simulate a Simulink model (named the_model) via a wrapper MATLAB® function (named sim_the_model) from Python. 4 | 5 | ![the_model](the_model.jpg) 6 | 7 | The first approach, shown in call_sim_the_model_using_matlab_engine.py, uses the [MATLAB Engine API for 8 | Python](https://www.mathworks.com/help/matlab/matlab_external/install-the-matlab-engine-for-python.html) to call the wrapper function sim_the_model.m multiple times passing in different parameters and external input signals. 9 | 10 | ![results_mle](results_mle2.png) 11 | 12 | The second approach uses [MATLAB Compiler 13 | SDK](https://www.mathworks.com/help/compiler_sdk/gs/create-a-python-application-with-matlab-code.html) and [Simulink Compiler](https://www.mathworks.com/help/slcompiler/ug/deploy-from-matlab-command-line.html) to first build a Python package around the wrapper function sim_the_model.m as shown in 14 | build_python_package_around_sim_the_model.m. We can then call this package to run the warpper function sim_the_model.m multiple times passing in different parameters and external input signals as show in call_sim_the_model_using_matlab_runtime.py. 15 | 16 | ![results_mrt](results_mrt.png) 17 | 18 | This example includes the following files: 19 | 20 | * the_model.slx: the Simulink model we will simulate in the example; 21 | * sim_the_model.m: the wrapper MATLAB function to simulate a Simulink model with the specified parameter and input signal values; 22 | * call_sim_the_model.m: MATLAB script used to call the sim_them_model multiple times in MATLAB with different inputs and parameters; 23 | * plot_results.m: MATLAB script used by call_sim_the_model to plot the results; 24 | * call_sim_the_model_using_matlab_runtime.py: Python script to call sim_the_model packaged function multiple times and plot the results; 25 | * call_sim_the_model_using_matlab_engine.py: Python script that uses MATLAB Engine API to call sim_the_model.m multiple time and plot the results; 26 | * CallingSimFromPython.pptx: complementary presentation slides describing the demo structure and setup 27 | 28 | The model the_model.slx and the wrapper MATLAB function sim_the_model.m illustrate implementation choices that make data marshaling between Python and sim command in MATLAB relatively straight forward and can be used with any Simulink model. These are: 29 | 30 | * Parameterizing the Simulink model using workspace variables makes it easy run sim with new parameter values passed in from Python. 31 | * Labeling the logged signals in the model with valid identifiers, makes it easy to pack the results into a MATLAB struct and return to Python. 32 | * Extracting the time and data values as numeric vectors from sim command output and returning these to Python makes data marshaling relatively easy. 33 | 34 | 35 | 36 | This example has been tested with MATLAB R2022b and Python 3.8. The following MathWorks products are needed for using this example: 37 | 38 | * [MATLAB](https://www.mathworks.com/products/matlab.html); 39 | * [Simulink](https://www.mathworks.com/products/simulink.html); 40 | * [MATLAB Compiler](https://www.mathworks.com/products/compiler.html); 41 | * [MATLAB Compiler SDK](https://www.mathworks.com/products/matlab-compiler-sdk.html); 42 | * [Simulink Compiler](https://www.mathworks.com/products/simulink-compiler.html); 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /call_sim_the_model_using_matlab_runtime.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Example showing how call a python package to simulate a Simulink 4 | model (called the_model) with different tunable parameter and 5 | external input signal values. 6 | 7 | To run this script, you need the following prerequisites: 8 | 1. Install the MATLAB Runtime (R2021b or later) 9 | https://www.mathworks.com/products/compiler/matlab-runtime.html 10 | 11 | 2. Install "sim_the_model" python package. See instructions below. 12 | 13 | Notes: 14 | 1. Run build_python_package_around_sim_the_model.m script in MATLAB 15 | (R2021b or later) to create sim_the_model_python_package. This 16 | script requires the following products: 17 | - MATLAB 18 | - Simulink 19 | - MATLAB Compiler 20 | - Simulink Compiler 21 | - MATLAB Compiler SDK 22 | 23 | After your run the script, follow the instructions displayed on the 24 | MATLAB command window to install the sim_the_model_python_package 25 | 26 | 2. sim_the_model_python_package is a wrapper to run sim_the_model.m 27 | MATLAB function using deployed version of the Simulink model 28 | (the_model) and the MATLAB Runtime 29 | 30 | 3. Both MATLAB Runtime, and sim_the_model_python_package (once it is 31 | built) can be distributed freely and do not require licenses. 32 | 33 | By: Murali Yeddanapudi on 01-Mar-2022 34 | """ 35 | 36 | import matlab 37 | import numpy as np 38 | 39 | # Specify the path to sim_the_model_python_package. Note that build_python_package_around_sim_the_model.m 40 | # script installs the python package in this location. If you change it, then you need to update the 41 | # code here, or remove it altogether if you install sim_the_model package in a location on the python 42 | # search path. 43 | import sys 44 | sys.path.append(".\\sim_the_model_python_package\\Lib\\site-packages") 45 | import sim_the_model 46 | 47 | # initialize sim_the_model package 48 | mlr = sim_the_model.initialize() 49 | 50 | # Allocate res list to hold the results from 4 calls to sim_the_model 51 | res = [0]*4; 52 | 53 | ## 1st sim: with default parameter values 54 | res[0] = mlr.sim_the_model() 55 | 56 | ## 2nd sim: with new values for dx2min and dx2max parameters 57 | tunableParams = { 58 | 'dx2min': -3.0, # Specify a new value for dx2min 59 | 'dx2max': 4.0 # Specify a new value for dx2max 60 | } 61 | res[1] = mlr.sim_the_model('TunableParameters',tunableParams) 62 | 63 | ## 3rd sim: with a non zero input signal 64 | # Note that, in the model the input u is sampled at a fixed time interval 65 | # uST (=1) which cannot be changed since the model is compiled for deployment. 66 | # So the time axis for the input values is implicit at 1s (=uST) interval 67 | # u = [0 2 zeros(1,3) -2*ones(1,2) 0]; 68 | u = np.concatenate([np.zeros(1), 2*np.ones(1), np.zeros(3), -2*np.ones(2), np.zeros(1)]) 69 | # => u(t) = 2 for t in [1,2), -2 for t in [6,8), 0 otherwise 70 | externalInput = matlab.double(u.tolist()) # convert numpy array into matlab array 71 | res[2] = mlr.sim_the_model('ExternalInput',externalInput) 72 | 73 | ## 4th sim: with dx2min, dx2max and non-zero input signal 74 | tunableParams = { 75 | 'dx2min': -3.0, # Specify a new value for dx2min 76 | 'dx2max': 4.0 # Specify a new value for dx2max 77 | } 78 | u = np.concatenate([np.zeros(1), 2*np.ones(1), np.zeros(3), -2*np.ones(2), np.zeros(1)]) 79 | externalInput = matlab.double(u.tolist()) # convert numpy array into matlab array 80 | res[3] = mlr.sim_the_model('TunableParameters',tunableParams,'ExternalInput',externalInput) 81 | 82 | ## Plot the results 83 | # TODO: Replace this code using plotly 84 | import matplotlib.pyplot as plt 85 | cols = plt.rcParams['axes.prop_cycle'].by_key()['color'] 86 | fig, ax = plt.subplots(1,1,sharex=True) 87 | ax.plot(res[0]['x1']['Time'], res[0]['x1']['Data'], color=cols[0], label="x1 from 1st sim with default setting") 88 | ax.plot(res[1]['x1']['Time'], res[1]['x1']['Data'], color=cols[1], label="x1 from 2nd sim with limits on dx2") 89 | ax.plot(res[2]['x1']['Time'], res[2]['x1']['Data'], color=cols[2], label="x1 from 3rd sim with input u") 90 | ax.plot(res[3]['x1']['Time'], res[3]['x1']['Data'], color=cols[3], label="x1 from 4th sim with limits on dx2 and input u") 91 | ax.step(res[3]['u']['Time'], res[3]['u']['Data'], where='post', color=cols[4], label="input u in 3rd and 4th sims") 92 | ax.grid(); ax.set_ylim([-4, 3]) 93 | lg = ax.legend(fontsize='x-small'); lg.set_draggable(True) 94 | ax.set_title("Results from sim_the_model using MATLAB Runtime") 95 | plt.show() 96 | 97 | mlr.terminate() # stop the MATLAB Runtime 98 | -------------------------------------------------------------------------------- /sim_the_model.m: -------------------------------------------------------------------------------- 1 | function res = sim_the_model(args) 2 | % Utility function to simulate a Simulink model (named 'the_model') with 3 | % the specified parameter and input signal values. 4 | % 5 | % Inputs: 6 | % StopTime: 7 | % Simulation stop time, default is nan 8 | % TunableParameters 9 | % A struct where the fields are the tunanle referenced 10 | % workspace variables with the values to use for the 11 | % simulation. 12 | % ExternalInput: 13 | % External Input signal, defualt is empty 14 | % ConfigureForDeployment: 15 | % Sepcify if the simulation input should be configured 16 | % for deployment, default is true 17 | % 18 | % Values of nan or empty for the above inputs indicate that sim should 19 | % run with the default values set in the model. 20 | % 21 | % Outputs: 22 | % res: A structure with the time and data values of the logged signals. 23 | 24 | % By: Murali Yeddanapudi, 20-Feb-2022 25 | 26 | arguments 27 | args.StopTime (1,1) double = nan 28 | args.TunableParameters = [] 29 | args.ExternalInput (1,:) {mustBeNumericOrLogical} = [] 30 | args.ConfigureForDeployment (1,1) {mustBeNumericOrLogical} = true 31 | args.InputFcn (1,1) {mustBeFunctionHandle} = @emptyFunction 32 | args.OutputFcn (1,1) {mustBeFunctionHandle} = @emptyFunction 33 | args.OutputFcnDecimation (1,1) {mustBeInteger, mustBePositive} = 1 34 | end 35 | 36 | %% Create the SimulationInput object 37 | % Note that the name of the model is hard-coded to 'the_model' 38 | si = Simulink.SimulationInput('the_model'); 39 | 40 | %% Load the StopTime into the SimulationInput object 41 | if ~isnan(args.StopTime) 42 | si = si.setModelParameter('StopTime', num2str(args.StopTime)); 43 | end 44 | 45 | %% Load the specified tunable parameters into the simulation input object 46 | if isstruct(args.TunableParameters) 47 | tpNames = fieldnames(args.TunableParameters); 48 | for itp = 1:numel(tpNames) 49 | tpn = tpNames{itp}; 50 | tpv = args.TunableParameters.(tpn); 51 | si = si.setVariable(tpn, tpv); 52 | end 53 | end 54 | 55 | %% Load the external input into the SimulationInput object 56 | if ~isempty(args.ExternalInput) 57 | % In the model, the external input u is a discrete signal with sample 58 | % time 'uST'. Hence the time points where it is sampled are set, i.e., 59 | % they are multiples of uST: 0, uST, 2*uST, 3*uST, .. We only specify 60 | % the data values here using the struct with empty time field as 61 | % described in Guy's blog post: 62 | % https://blogs.mathworks.com/simulink/2012/02/09/using-discrete-data-as-an-input-to-your-simulink-model/ 63 | uStruct.time = []; 64 | uStruct.signals.dimensions = 1; 65 | % values needs to be column vector 66 | uStruct.signals.values = reshape(args.ExternalInput,numel(args.ExternalInput),1); 67 | si.ExternalInput = uStruct; 68 | end 69 | 70 | %% Configure for deployment 71 | if args.ConfigureForDeployment 72 | si = simulink.compiler.configureForDeployment(si); 73 | elseif ismcc || isdeployed 74 | error("Simulation needs to be configured for deployment"); 75 | end 76 | 77 | %% InputFcn 78 | if ~isequal(args.InputFcn, @emptyFunction) 79 | si = simulink.compiler.setExternalInputsFcn(si, args.InputFcn); 80 | end 81 | 82 | %% OutputFcn 83 | prevSimTime = nan; 84 | function locPostStepFcn(simTime) 85 | so = simulink.compiler.getSimulationOutput('the_model'); 86 | res = extractResults(so, prevSimTime); 87 | stopRequested = feval(args.OutputFcn, simTime, res); 88 | if stopRequested 89 | simulink.compiler.stopSimulation('the_model'); 90 | end 91 | prevSimTime = simTime; 92 | end 93 | if ~isequal(args.OutputFcn, @emptyFunction) 94 | si = simulink.compiler.setPostStepFcn(si, @locPostStepFcn, ... 95 | 'Decimation', args.OutputFcnDecimation); 96 | end 97 | 98 | %% call sim 99 | so = sim(si); 100 | 101 | %% Extract the simulation results 102 | % Package the time and data values of the logged signals into a structure 103 | res = extractResults(so,nan); 104 | 105 | end % sim_the_model_using_matlab_runtime 106 | 107 | function res = extractResults(so, prevSimTime) 108 | % Package the time and data values of the logged signals into a structure 109 | ts = simulink.compiler.internal.extractTimeseriesFromDataset(so.logsout); 110 | for its=1:numel(ts) 111 | if isfinite(prevSimTime) 112 | idx = find(ts{its}.Time > prevSimTime); 113 | res.(ts{its}.Name).Time = ts{its}.Time(idx); 114 | res.(ts{its}.Name).Data = ts{its}.Data(idx); 115 | else 116 | res.(ts{its}.Name).Time = ts{its}.Time; 117 | res.(ts{its}.Name).Data = ts{its}.Data; 118 | end 119 | end 120 | end 121 | 122 | function mustBeFunctionHandle(fh) 123 | if ~isa(fh,'function_handle') && ~ischar(fh) && ~isstring(fh) 124 | throwAsCaller(error("Must be a function handle")); 125 | end 126 | end 127 | 128 | function emptyFunction 129 | end 130 | --------------------------------------------------------------------------------