Main Content

Refactor Charts Programmatically

You can use the Stateflow® API to programmatically refactor common issues that affect the legibility of Stateflow charts. You can programmatically analyze a chart to identify issues with confusingly nested states, inconsistent state names, or extraneous transitions, and then fix these issues by using the Stateflow API.

Open the Model

In this example, you use a poorly designed model that emulates the behavior of an insect. The chart describes this behavior:

  • The insect is confined to a box.

  • The insect moves in a straight line by default.

  • When the insect hits a wall, the insect bounces off the wall and continues in the opposite direction.

  • When the insect sees prey, the insect redirects its trajectory to head towards the prey at an increased speed.

  • When the insect sees a predator, the insect redirects its trajectory to run away from the predator at an increased speed.

  • The insect can see in a directional cone of finite length, which means the insect must be close to an object and facing it in order to detect its presence.

  • After 12 hours, the insect is tired and has to rest.

This Stateflow chart has an inconsistent naming scheme in which some state names start with a capital letter and some state names do not. Additionally, the chart contains extraneous transitions and states that are nested more than three levels deep.

Chart Depth Issues

To interact with the Stateflow chart by using the API, use the find function to access the Stateflow.Chart object for the chart.

ch = find(sfroot,"-isa","Stateflow.Chart",Name="Behavioral Logic");

Flagging Chart Depth Issues

To improve chart legibility, avoid nesting charts deeper than three levels deep, where possible. Instead, use subcharts. You can search for states that are nested too deeply by using find and its optional arguments to create an array:

all_states = find(ch,"-isa","Stateflow.State");
shallow_states = find(ch,"-isa","Stateflow.State","-depth",3);
deep_states = setdiff(all_states,shallow_states);

You can inspect the contents of that array in MATLAB through several means. For example, you can write a script to print the contents of the array to the Command Window. This script includes information regarding the hierarchy, which can help you find specific states easily in a large or complex Stateflow chart.

num_deep_states = length(deep_states);
deep_state_names = "";
for i=1:num_deep_states
    deep_path_text = getHierarchy(deep_states(i));
    deep_state_names = deep_state_names + newline + " * " + deep_path_text;
end

warning("The following states have been flagged for having a depth greater than 3." + ... 
    newline + "Consider creating subcharts instead." + deep_state_names)

The for loop in this script iterates through each problem state. The getHierarchy helper function, shown below, traces the parentage of the state to the original model, which shows you how to navigate to the flagged state. The script calls getParent in a loop to trace the parents of the state as far back as the model. The warning function prints the paths to all the problem states in the Command Window.

function path_text = getHierarchy(state)    
    path_text = state.Path + "." + state.Name;    

    indices = strfind(path_text,"/");

    for i = 3:numel(indices)
        path_text = replaceBetween(path_text,indices(i),indices(i),".");
    end
end

For the insect behavior example, this script flags the substates under searching. These substates are four levels deep.

Warning: The following states have been flagged for having a depth greater than 3.
Consider creating subcharts instead.
 * insect_behavior_example/Behavioral Logic/Awake.Safe.searching.normal
 * insect_behavior_example/Behavioral Logic/Awake.Safe.searching.yBump
 * insect_behavior_example/Behavioral Logic/Awake.Safe.searching.xBump 

Fixing Chart Depth Issues

After you identify issues with the depth of the states in your chart, you can fix these issues programmatically.

To convert a Stateflow.State to a subchart, access the IsSubchart property and set it to true. To automate this process for a large-scale chart, the helper function createSubchart looks for states at a depth of three which contain other nested states, then converts the states at a depth of three to subcharts. Then, it repeats the process by calling itself recursively with the new subcharts as parents.

function createSubchart(parent,depth)
   
    states = setdiff( ...
        find(parent,"-isa","Stateflow.State","-depth",depth), ...
        find(parent,"-isa","Stateflow.State","-depth",depth-1));
    
    for i=1:length(states)
        children = setdiff(find(states(i),'-isa','Stateflow.State', ...
            '-depth',1),states(i));
        for j=1:length(children)
            states(i).isSubchart = true;
            createSubchart(states(i),depth);
            break
        end
    end
end

State Name Issues

Flagging State Name Issues

To improve readability, use the same naming conventions throughout the chart. You can use the find function to identify the states that do not start with capital letters.

correct_states = find(ch,"-isa","Stateflow.State", ...
    "-regexp","Name","^[A-Z]\w*");
misnamed_states = setdiff(all_states,correct_states);

When using find to search, techniques you can use include:

  1. Logical expressions such as and to add several conditions for your search.

  2. Regular expressions to search for open-ended combinations of alphanumeric characters. In this example, regexp searches for states whose names start with an uppercase letter and are composed entirely of alphanumeric characters or underscores.

You can view the results in the misnamed_states variable or use a script to print the results to the Command Window. For this example, use this script:

bad_state_names = "";
for j=1:length(misnamed_states)
    misnamed_path_text = getHierarchy(misnamed_states(j));
    bad_state_names = bad_state_names + newline + " * " + misnamed_path_text;
end

warning("The following states have been flagged for an improper naming scheme." + ...
    newline + "Consider renaming these states to start with a capital letter." + ...
    bad_state_names)

The script outputs the states that start with lowercase letters:

Warning: The following states have been flagged for an improper naming scheme. 
Consider renaming these states to start with a capital letter.
 * insect_behavior_example/Behavioral Logic/Awake.Danger.yBump
 * insect_behavior_example/Behavioral Logic/Awake.Safe.searching.normal
 * insect_behavior_example/Behavioral Logic/Awake.Safe.searching
 * insect_behavior_example/Behavioral Logic/Awake.Danger.xBump
 * insect_behavior_example/Behavioral Logic/Awake.Safe.searching.yBump
 * insect_behavior_example/Behavioral Logic/Awake.Safe.searching.xBump 

Fixing State Name Issues

After identifying the list of state names, replace the first letter of each identified state in misnamed_states with its uppercase equivalent by using upper.

for k=1:length(misnamed_states)
    state = misnamed_states(k);
    state.Name = [upper(state.Name(1)) state.Name(2:end)];
end

Note

Similarly, you can refactor chart data names with the following code:

rt = sfroot;
rt.find('-isa', 'Stateflow.Data')
data.refactor('newName')

Unnecessary Transitions

Flagging Unnecessary Transitions

Unnecessary transitions may increase the complexity of your charts. For example, transitions may not be necessary if you have several sequential transitions. In this example, you can define an unnecessary transition as connected to a junction or chain of junctions with only one sourced transition and one sunk transition. This code uses functions sourcedTransitions and sinkedTransitions to identify these transitions.

function is_linear = checkLinearity(junction)
    is_linear = false;
    source_transitions = sourcedTransitions(junction);
    sink_transitions = sinkedTransitions(junction);
    if (length(source_transitions)<=1) && (length(sink_transitions)<=1)
        is_linear = true;
    end
end

You can view the results in the is_linear variable. Alternatively, you can use a script to print the results to the Command Window. This code uses the names of the parents to which these transitions belong. Because most transitions do not have a unique name, the code prints the name of the parent state, box, or subchart.

junctions = find(ch,"-isa","Stateflow.Junction");
bad_transition_parent_names = "";
flagged_junctions = [];

for k=1:length(junctions)
    if checkLinearity(junctions(k))
        flagged_junctions = [flagged_junctions junctions(k)];
        bad_transition_parent_names = bad_transition_parent_names + ...
            newline + " * " + getHierarchy(getParent(junctions(k)));
    end
end

warning("The following states contain transitions that have been flagged for" + ...
    newline + "superfluousness. You may be able to combine transitions within these states into" + ...
    newline + "one transition with several actions." + ...
    bad_transition_parent_names)

The script prints the names of the transitions with only one sourced transition and one sunk transition:

Warning: The following states contain transitions that have been flagged for 
superfluousness. You may be able to combine transitions within these states into 
one transition with several actions.
 * insect_behavior_example/Behavioral Logic/Awake.Danger
 * insect_behavior_example/Behavioral Logic/Awake.Danger
 * insect_behavior_example/Behavioral Logic/Awake.Danger 

Fixing Unnecessary Transitions

In this example, the variable flagged_junctions contains the list of transitions with only one sourced transition and one sunk transition. This image shows some of the identified transitions.

An image of the Danger state, which contains extra transitions going from yBump to Normal and from Normal to xBump.

This script deletes unnecessary transitions in the chart and replaces them with a single transition. The script:

  1. Locates the start transition and end transition of each chain of one or more unnecessary transitions using the helper function findEnds, which uses the Stateflow.Transition API object properties to navigate through the chart programmatically. It uses Source and Destination to move in opposite directions over the chain. Its sole input is the flagged_junctions array.

  2. Uses the outputs of findEnds to navigate the chain from the start transition until it reaches the end transition.

    1. Stores any guards or actions from the intermediate transitions in variables cat_guard and cat_action using the helper function buildConcatLabelStr. To see more about the formatting involved in constructing a label string, see Specify Labels in States and Transitions Programmatically.

    2. Deletes the intermediate transitions and junctions.

  3. Builds a new, concatenated label from cat_guard and cat_action and applies it to the start transition.

  4. Redirects the destination of the start transition to the terminus of the end transition. The start transition now encompasses the length of the original chain.

[starts,ends] = findEnds(flagged_junctions);

for n=1:length(starts)

    current = starts(n);
    cat_guard = "";
    cat_action = "";

    while current ~= ends(n)
        [current,cat_guard,cat_action] = buildConcatLabelStr(current,...
            cat_guard,cat_action);

        previous = current;
        current = sourcedTransitions(current.Destination);
        delete(previous.Destination);

        if ~ismember(previous,starts)
            delete(previous);
        end

    end
    [~,cat_guard,cat_action] = buildConcatLabelStr(current,cat_guard,...
        cat_action);

    temp_string = "";

    if strlength(cat_guard)
        temp_string = "[" + cat_guard + "]";
    end
    if strlength(cat_action)
        temp_string = temp_string + "{" + cat_action + "}";
    end

    starts(n).LabelString = temp_string;
    starts(n).Destination = ends(n).Destination;
    delete(ends(n));
end

function [starts,ends]=findEnds(flagged_junctions)

    starts = [];
    ends = [];

    while ~isempty(flagged_junctions)
        junction = flagged_junctions(1);
        flagged_junctions = flagged_junctions(2:end);
    
        [startPoint,flagged_junctions_1] = findEndPoint(...
            sinkedTransitions(junction),flagged_junctions,-1);

        [endPoint,flagged_junctions_2] = findEndPoint(...
            sourcedTransitions(junction),flagged_junctions,1);

        starts = [starts startPoint];
        ends = [ends endPoint];
        flagged_junctions = intersect(flagged_junctions_1,flagged_junctions_2);
    end

    function [start_trans,flagged_junc] = findEndPoint(transition,flagged_junc,dir)
        if dir == 1
            next = transition.Destination;
            nextTrans = sourcedTransitions(next);
        else
            next = transition.Source;
            nextTrans = sinkedTransitions(next);
        end
        if isa(next,'Stateflow.Junction') && checkLinearity(next)
            flagged_junc = flagged_junc(~ismember(flagged_junc,next));
            [start_trans,flagged_junc] = findEndPoint(nextTrans,flagged_junc,dir);
        else
            start_trans = transition;
        end
    end

end

function [current,cat_guard,cat_action] = buildConcatLabelStr( ...
    current,cat_guard,cat_action)

guard = current.Condition;
action = current.ConditionAction;

if strlength(guard)
    if ~strlength(cat_guard)
        cat_guard = guard;
    else
        cat_guard = cat_guard + "&&" + guard;
    end
end

if strlength(action)
    if ~strlength(cat_action)
        cat_action = action;
    else
        cat_action = cat_action + ";" + action;
    end
end
end

After the code completes, the resulting chart contains no junctions with only one sourced transition and one sunk transition.

An image of the Danger state. Now, the transitions between Normal, XBump, and YBump are singular instead of chained unnecessarily.

See Also

Functions

Objects

Related Topics