-
Notifications
You must be signed in to change notification settings - Fork 0
HybridSystem_demo
In this tutorial, we show how to create and solve a hybrid system using the HybridSystem class. For a brief introduction to hybrid systems, see here .
In this page, we will consider, as an example, a bouncing ball with state
where
We define a MATLAB implementation of the bouncing ball in hybrid.examples.BouncingBall.m . A script for running the bouncing ball example is provided at hybrid.examples.run_bouncing_ball (the prefix hybrid.examples. indicates the package namespace that contains BouncingBall.m and run_bouncing_ball.m ). The structure of BouncingBall.m is as follows.
classdef BouncingBall < HybridSystem % Define bouncing ball system as a subclass of HybridSystem. properties % Define variable class properties. ... end properties(SetAccess = immutable) % Define constant (i.e., "immutable") class properties. ... end methods % Define functions, including definitions of f, g, C, and D. ... end end
In the first line,
"classdef BouncingBall < HybridSystem"
specifies that
BouncingBall
is a subclass of the
HybridSystem
class, which means it inherits the attributes of that class. Within MATLAB classes,
properties
blocks define variables and constants that can be accessed on instances of the class. For
BouncingBall
, there are two
properties
blocks. The first defines variable variables for the system parameters
classdef BouncingBall < HybridSystem properties gamma = 9.8; % Acceleration due to gravity. lambda = 0.9; % Coefficient of bounce restitution. end ... end
The second properties block defines constants that cannot be modified. The block attribute SetAccess = immutable means that the values cannot be changed once an object is constructed. We define two "index" constants that are useful for referencing components of the state vector x .
classdef BouncingBall < HybridSystem ... properties(SetAccess = immutable) % The index of 'height' component within the state vector 'x'. height_index = 1; % The index of 'velocity' component within the state vector 'x'. velocity_index = 2; end ... end
For more complicated systems, it is sometimes useful to define constants for a range of indices. For example, if
then we could define the following immutable properties:
v_indices = 1:4; u_indices = 4 + (1:4); % These parentheses are important!
Then, we can extract u and v from x as
v = x(v_indices); u = x(u_indices);
Next, in BouncingBall , the methods block defines functions that can be called on BouncingBall objects. Every subclass of HybridSystem must define the functions flowMap , jumpMap , flowSetIndicator , and jumpSetIndicator . The indicator functions must return 1 inside their respective sets and 0 otherwise.
function xdot = flowMap(this, x, t, j) % Define the flow map f. % Set xdot to the value of f(x, t, j). ... end
function xplus = jumpMap(this, x, t, j) % Define the jump map g. % Set xplus to the value of g(x, t, j). ... end
function inC = flowSetIndicator(this, x, t, j) % Define the flow set C. % Set 'inC' to 1 if 'x' is in the flow set C and to 0 otherwise. ... end
function inD = jumpSetIndicator(this, x, t, j) % Define the flow set C. % Set 'inD' to 1 if 'x' is in the jump set D and to 0 otherwise. ... end
Notice that the first argument in each function method is this , which provides a reference to the object on which the function was called. The object's properties are referenced within the methods using this.gamma , this.lambda , etc. After the this argument, there must be one, two, or three arguments that are either (x) , (x, t) , or (x, t, j) , as needed. The full class definition of BouncingBall is shown next.
classdef BouncingBall < HybridSystem % A bouncing ball modeled as a HybridSystem subclass.% Define variable properties that can be modified. properties gamma = 9.8; % Acceleration due to gravity. lambda = 0.9; % Coefficient of restitution. end % Define constant properties that cannot be modified (i.e., "immutable"). properties(SetAccess = immutable) % The index of 'height' component % within the state vector 'x'. height_index = 1; % The index of 'velocity' component % within the state vector 'x'. velocity_index = 2; end methods function this = BouncingBall() % Constructor for instances of the BouncingBall class. % Call the constructor for the HybridSystem superclass and % pass it the state dimension. This is not strictly necessary, % but it enables more error checking. state_dim = 2; this = this@HybridSystem(state_dim); end % To define the data of the system, we implement % the abstract functions from HybridSystem.m function xdot = flowMap(this, x, t, j) % Extract the state components. v = x(this.velocity_index); % Define the value of f(x). xdot = [v; -this.gamma]; end function xplus = jumpMap(this, x) % Extract the state components. h = x(this.height_index); v = x(this.velocity_index); % Define the value of g(x). xplus = [h; -this.lambda*v]; end function inC = flowSetIndicator(this, x) % Extract the state components. h = x(this.height_index); v = x(this.velocity_index); % Set 'inC' to 1 if 'x' is in the flow set and to 0 otherwise. inC = (h >= 0) || (v >= 0); end function inD = jumpSetIndicator(this, x) % Extract the state components. h = x(this.height_index); v = x(this.velocity_index); % Set 'inD' to 1 if 'x' is in the jump set and to 0 otherwise. inD = (h <= 0) && (v <= 0); end end
end
An instance of the BouncingBall class is then created as follows:
bb_system = hybrid.examples.BouncingBall();
Values of the properties can be modified using dot indexing on the object:
bb_system.gamma = 3.72; bb_system.lambda = 0.8;
For more information about writing MATLAB classes, see the online MATLAB documentation .
WARNING: The functions flowMap , flowSetIndicator , and jumpSetIndicator must be deterministic—each time they are called for a given set of inputs (x, t, j) , they must return the same output values. In other words, flowMap , flowSetIndicator , and jumpSetIndicator must be functions in the mathematical sense. If they depend on system parameters stored as object properties, then those parameters must be constant during each execution of HybridSystem.solve (described below). It is okay to change the system parameters in between calls to solve , but all values that change during a solution, must be included in the state vector x .
The reason for this warning is that modifying system parameters within
flowMap
,
flowSetIndicator
, or
jumpSetIndicator
will produce unpredictable behavior because the hybrid solver does not move monotonically forward in time; Sometimes, the solver backtracks while computing a solution to
To protect against accidentally modifying system parameters, each parameter can be stored as an immutable object property that is set when the HybridSystem object is constructed. The value of an immutable property cannot be modified after an object is created. An example of how to implement an immutable property is included here:
classdef MyHybridSystem < HybridSystem properties(SetAccess=immutable) my_property % cannot be modified except in the constructor end methods function this = MyHybridSystem(my_property) % Constructor this.my_property = my_property; % set immutable property value. end end end
The value of my_property is set when a MyHybridSystem is constructed:
sys = MyHybridSystem(3.14); assert(sys.my_property == 3.14)
For more information about immutable properties see the MATLAB documentation here .
When implementing a hybrid system, it is helpful to verify that certain points are or are not in
% Define some points. x_ball_above_ground = [1; 0]; x_ball_at_ground = [0; 0]; % and stationary x_ball_below_ground = [-1; -1]; % and moving down% Check the above-ground point. bb_system.assertInC(x_ball_above_ground); bb_system.assertNotInD(x_ball_above_ground);
% Check the at-ground point. bb_system.assertInC(x_ball_at_ground); bb_system.assertInD(x_ball_at_ground);
% Check the below-ground point. bb_system.assertNotInC(x_ball_below_ground); bb_system.assertInD(x_ball_below_ground);
In addition to checking that a given point is in the desired set, the functions assertInC , assertInD check that the flow or jump map, respectively, can be evaluated at the point and produces a vector with the correct dimensions.
If an assertion fails, as in following code, then an error is thrown and execution is terminated (unless caught by a try-catch block):
try bb_system.assertInD(x_ball_above_ground) % This fails. catch e fprintf(2,'Error: %s\n', e.message); end
Error: The point x: [1;0] was expected to be inside D but was not.
A numerical approximation of a solution to the hybrid system is computed by calling the solve function. The solve function returns a HybridSolution object that contains information about the (approximate) solution (use the properties t , j , and x to recover the standard output from HyEQSolver ). To compute a solution, pass the initial state and time spans to the solve function ( solve is defined in the HybridSystem class and bb_system is a HybridSystem object because BouncingBall is a subclass of HybridSystem ). Optionally, a HybridSolverConfig object can be passed to the solver to set various configurations (see below).
x0 = [10, 0]; tspan = [0, 20]; jspan = [0, 30]; config = HybridSolverConfig('refine', 32); % Improves plot smoothness for demo. sol = bb_system.solve(x0, tspan, jspan, config); plotFlows(sol); % Display solution
The use of HybridSolverConfig is described below, in "Solver Configuration Options"
The option 'refine' for HybridSolverConfig causes computed ODE solutions to be smoothly interpolated between timesteps. For documentation about plotting hybrid arcs, see Plotting Hybrid Arcs .
The return value of the solve method is a HybridSolution object that contains information about the solution.
sol
sol = HybridSolution with properties:x0: [2×1 double] xf: [2×1 double] termination_cause: T_REACHED_END_OF_TSPAN t: [4111×1 double] j: [4111×1 double] x: [4111×2 double] flow_lengths: [15×1 double] jump_times: [14×1 double] shortest_flow_length: 0.1515 total_flow_length: 20 jump_count: 14
A description of each HybridSolution property is as follows:
- t : The continuous time values of the solution's hybrid time domain.
- j : The discrete time values of the solution's hybrid time domain.
- x : The state vector of the solution.
- x0 : The initial state of the solution.
- xf : The final state of the solution.
- flow_lengths : the durations of each interval of flow.
- jump_times : the continuous times when each jump occured.
- shortest_flow_length : the length of the shortest interval of flow.
- total_flow_length : the length of the entire solution in continuous time.
- jump_count : the number of discrete jumps.
- termination_cause : the reason that the solution terminated.
The possible values for termination_cause are
- STATE_IS_INFINITE
- STATE_IS_NAN
- STATE_NOT_IN_C_UNION_D
- T_REACHED_END_OF_TSPAN
- J_REACHED_END_OF_JSPAN
- CANCELED
Often, after calculating a solution to a hybrid system, we wish to manipulate the resulting data, such as evaluating a function along the solution, removing some of the components, or truncating the hybrid domain. Several functions to this end are included in the HybridArc class ( HybridSolution is a subclass of HybridArc , so the solutions generated by HybridSystem.solve are HybridArc objects). In particular, the functions are select , transform , restrictT and restrictJ . See doc('HybridArc') for details.
hybrid_arc = sol.select(1); % Pick the 1st component. hybrid_arc = hybrid_arc.transform(@(x) -x); % Negate the value. hybrid_arc = hybrid_arc.restrictT([1.5, 12]); % Truncate to t-values between 4.5 and 7. hybrid_arc = hybrid_arc.restrictJ([2, inf]); % Truncate to j-values >= 2.% Plot hybrid arcs clf hpb = HybridPlotBuilder(); hpb.color('black').legend('Original').plotFlows(sol.select(1)); hold on hpb.color('red').legend('Modified').plotFlows(hybrid_arc)
Example: Suppose we want to compute the total energy of the bouncing ball:
We can map the
HybridArc
object
sol
to a new
HybridArc
with the
transform
function. (Note that the state dimension before (
clf energy_fnc = @(x) bb_system.gamma*x(1) + 0.5*x(2)^2; plotFlows(sol.transform(energy_fnc)) title('Total Energy of Bouncing Ball') ylabel('Energy')
You can also construct a HybridArc directly from the values of t , j , and x as follows:
t = linspace(0, 10, 100)'; % Must be a column vector j = zeros(100, 1); x = t.^2; hybrid_arc = HybridArc(t, j, x)
hybrid_arc = HybridArc with properties:t: [100×1 double] j: [100×1 double] x: [100×1 double] flow_lengths: 10 jump_times: [0×1 double] shortest_flow_length: 10 total_flow_length: 10 jump_count: 0
To configure the hybrid solver, create a HybridSolverConfig object and pass it to solve as follows:
config = HybridSolverConfig('AbsTol', 1e-3); bb_system.solve(x0, tspan, jspan, config);
There are three categories of options that can be configured with HybridSolverConfig :
-
Jump/Flow Priority for determining the behavior of solutions in
$C \cap D$ . - ODE solver options.
- Hybrid solver progress updates options.
By default, the hybrid solver gives precedence to jumps when the solution is in the intersection of the flow and jump sets. This can be changed by setting the priority to one of the (case insensitive) strings 'flow' or 'jump' . For an example for how changing the priority affects solutions, see Behavior in the Intersection of C and D (the linked example uses Simulink instead of MATLAB, which has an additional "random" priority mode, not currently supported in the MATLAB HyEQ library).
config.priority('flow'); config.priority('jump');
The ODE solver function and solver options can be modified in config . To set the ODE solver, pass the name of the ODE solver function name to the odeSolver function. The default is 'ode45' . For guidence in picking an ODE solver, see this list .
config.odeSolver('ode23s');
Most of the options for the builtin MATLAB ODE solvers (described here ) can be set within HybridSolverConfig . A list of supported functions is provided below. To set an ODE solver option, use the odeOption function:
config.odeOption('AbsTol', 1e-3); % Set the absolute error tolerance for each time step.
Several commonly used options can be set by passing name-value pairs to the HybridSolverConfig constructor. The options that can be set this way are 'odeSolver' , 'RelTol' , 'AbsTol' , 'MaxStep' , 'Jacobian' , and 'Refine' .
config = HybridSolverConfig('odeSolver', 'ode23s', ... 'RelTol', 1e-3, 'AbsTol', 1e-4, ... 'MaxStep', 0.5, 'Refine', 4);% Display the options struct. config.ode_options
ans = struct with fields:AbsTol: 1.0000e-04 BDF: [] Events: [] InitialStep: [] Jacobian: [] JConstant: [] JPattern: [] Mass: [] MassSingular: [] MaxOrder: [] MaxStep: 0.5000 NonNegative: [] NormControl: [] OutputFcn: [] OutputSel: [] Refine: 4 RelTol: 1.0000e-03 Stats: [] Vectorized: [] MStateDependence: [] MvPattern: [] InitialSlope: []
Some of the ODE solver options have not been tested with the hybrid equation solver. The following table lists all ODE solver options and information about whether the HyEQ solver supports each. For a description of each option, see doc('odeset') .
ODE Option | Supported? |
---|---|
RelTol | Yes |
AbsTol | Yes |
NormControl | Untested |
NonNegative | Untested |
OutputFcn | No. Use 'progress' function. |
OutputSel | Untested |
Refine | Yes |
Stats | Yes, but will print stats after each interval of flow. |
InitialStep | Untested |
MaxStep | Yes |
Events | No |
Jacobian | Yes |
JPattern | Yes |
Vectorized | Untested, but probably won't work. |
Mass | Untested |
MStateDependence | Untested |
MvPattern | Untested |
MassSingular | Untested |
InitialSlope | Yes, for DAEs |
Computing a hybrid solution can take considerable time, so progress updates are displayed. Progress updates can be disabled by passing 'silent' to config.progess() .
config.progress('silent');% To restore the default behavior: config.progress('popup');
For brevity, 'silent' can be also be passed as the first argument to the HybridSolverConfig constructor.
HybridSolverConfig('silent', );
Alternatively, 'silent' can be passed directly to solve in place of config , if no other solver configurations are desired.
bb_system.solve(x0, tspan, jspan, 'silent');
We also provide a quicker way to create a hybrid system in-line instead of creating a new subclass of HybridSystem. This approach creates systems that are slower to solve and harder to debug, so use with care—creating a subclass of HybridSystem is the recommended method for defining complicated systems.
system_inline = HybridSystemBuilder() ... .f(@(x, t) tx) ... .g(@(x) -x/2) ... .C(@(x) 1) ... .D(@(x, t, j) abs(x) >= j) ... .build(); sol_inline = system_inline.solve(0.5, [0, 10], [0, 10]);
The functions f , g , C and D can have 1, 2, or 3 input arguments, namely (x) , (x,t) , or (x, t, j) .
The definition of hybrid systems can be made even more concise by passing function handles for f , g , C and D to the HybridSystem function, which constructs a HybridSystem with the given data. Again, this approach is slower and harder to debug, so it is not generally recommended.
f = @(x, t) tx; g = @(x) -x/2; C = @(x) 1; D = @(x, t, j) abs(x) >= j; system_inline = HybridSystem(f, g, C, D);
- Creating and Simulating Hybrid Systems
- Plotting Hybrid Arcs
- Creating and Simulating Composite Hybrid Subsystems
- Updating Code Designed for HyEQ Toolbox v2.04 to Use v3.0 Features.
- Example: Composite Hybrid System with Zero-order Hold Subsystem.
- Example: Composite Hybrid System with Switched Subsystem.