Main Content

このページの内容は最新ではありません。最新版の英語を参照するには、ここをクリックします。

プログラムによるチャートのリファクタリング

この例では、Stateflow® API を使用して Stateflow チャートを読みやすくする方法を説明します。一貫性がないステートの名前、深い入れ子にされたステート、不要なジャンクションなどの一般的な様式のパターンをプログラムで検出して修正できます。Stateflow API の使用の詳細については、Stateflow API の概要を参照してください。

モデルを開く

このモデルには、昆虫の動作をエミュレートする Stateflow チャートが含まれています。このモデルの内容は次のとおりです。

  • 昆虫は箱の中に入れられています。

  • 昆虫はその目の前にある物体のみを検知できます。

  • 昆虫は、壁にぶつかるか捕食者または被食者を検知するまで直線上を動きます。

  • 昆虫は壁にぶつかると跳ね返り、反対方向に動き続けます。

  • 昆虫は捕食者を認識すると、速度を上げて捕食者から遠ざかります。

  • 昆虫は被食者を認識すると、速度を上げて被食者の方に向かいます。

  • 昆虫は 12 時間ごとに止まって休憩します。

このチャートでは、ステートの命名規則に一貫性がなく、大文字で始まるステート名とそうでないステート名が混在しています。さらに、チャートの階層構造は深く、入れ子にされたステートのレベルは 3 を超えています。最後に、チャートには、入力遷移と出力遷移がそれぞれ 1 つのみである余分なジャンクションが含まれています。

model = "sfInsectExample";
open_system(model)

Stateflow chart with inconsistent state names, deeply nested states, and unnecessary junctions.

チャートのStateflow.Chartオブジェクトにアクセスするには、関数findを呼び出します。

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

一貫性がないステートの名前の修正

チャートを読みやすくするには、チャート内のすべてのステートで同じ命名規則を使用します。たとえば、すべてのステート名が大文字で始まる命名規則を使用できます。この命名規則に従っていない名前をもつステートを検索するには、オプションの引数 -regexp を指定して関数findを呼び出し、正規表現を指定します。詳細については、正規表現を参照してください。

misnamedStates = find(ch,"-isa","Stateflow.State", ...
    "-regexp","Name","^[a-z]\w*");

検索結果を検査するには、各Stateflow.Stateオブジェクトの Path プロパティと Name プロパティにアクセスして、モデルから各ステートへのパスを表示します。

numMisnamedStates = numel(misnamedStates);
misnamedStateNames = "";

for i = 1:numMisnamedStates
    state = misnamedStates(i);
    misnamedStateNames = misnamedStateNames + ...
        newline + " * " + state.Path + "/" + state.Name;
end

disp("The names of these states do not start with a capital letter:" + ...
    newline + misnamedStateNames)
The names of these states do not start with a capital letter:

 * sfInsectExample/BehavioralLogic/Awake/Safe/searching
 * sfInsectExample/BehavioralLogic/Awake/Danger/xBump
 * sfInsectExample/BehavioralLogic/Awake/Danger/yBump
 * sfInsectExample/BehavioralLogic/Awake/Safe/searching/normal
 * sfInsectExample/BehavioralLogic/Awake/Safe/searching/xBump
 * sfInsectExample/BehavioralLogic/Awake/Safe/searching/yBump

特定された各ステート名の先頭の文字を対応する大文字に置き換えるには、関数renameReferencesを呼び出して各Stateflow.Stateオブジェクトの Name プロパティを変更します。

for i = 1:numMisnamedStates
    state = misnamedStates(i);
    newName = [upper(state.Name(1)) state.Name(2:end)];
    renameReferences(state,newName)
end

Chart with renamed states Searching, Normal, XBump, and YBump.

ステートの深い階層構造の簡略化

入れ子にされたステートのレベルが多すぎるチャートは、わかりにくくなる可能性があります。レベルが 3 を超えるステート階層がチャートに必要な場合は、サブチャートを作成してチャートの複雑度を低減できます。詳細については、サブチャートを使用したモーダル ロジックのカプセル化を参照してください。

チャートで深い入れ子にされたステートを検索するには、オプションの引数 -function を指定してfindを呼び出し、補助関数 getDepth を評価する関数ハンドルを指定します。この関数は、特定のステートと最も近い先祖チャートまたはサブチャートの間にあるレベルの数を計算します。この関数のコードを見るには、ステートの深さの取得を参照してください。

maxDepth = 3;
deepStates = find(ch,"-isa","Stateflow.State", ...
    "-function", @(s)(getDepth(s) > maxDepth));

numDeepStates = numel(deepStates);
deepStateNames = "";

for i = 1:numDeepStates
    state = deepStates(i);
    deepStateNames = deepStateNames + ...
        newline + " * " + state.Path + "/" + state.Name;
end

disp("These states occur at a depth greater than " + maxDepth + ":" + ...
    newline + deepStateNames)
These states occur at a depth greater than 3:

 * sfInsectExample/BehavioralLogic/Awake/Safe/Searching/Normal
 * sfInsectExample/BehavioralLogic/Awake/Safe/Searching/XBump
 * sfInsectExample/BehavioralLogic/Awake/Safe/Searching/YBump

ステートの階層構造が深いチャートを簡略化するには、Stateflow.Stateオブジェクトの IsSubchart プロパティを true に設定してステートをサブチャートに変換します。このプロセスを大規模なチャート用に自動化するには、補助関数 convertStatesToSubcharts を使用して、階層の特定の深さにある非リーフ ステートを再帰的に検索し、それらのステートをサブチャートに変換します。また、この関数はコンテンツ プレビューを無効にし、新しいサブチャートのサイズを変更します。この関数のコードを見るには、ステートからサブチャートへの変換を参照してください。

convertStatesToSubcharts(ch,maxDepth)

Chart with a subchart called Searching.

余分なジャンクションの削除

遷移パスが長いと、チャートの複雑度が増すことがあります。たとえば、遷移元を遷移先に分岐なしで接続する場合は、複数の余分なジャンクションがある一連の遷移を使用するよりも、単一の遷移を使用した方が簡単です。

余分なジャンクションを特定するには、オプションの引数 -function を指定してfindを呼び出し、補助関数 isSuperfluous へのハンドルを指定します。この関数は、特定のジャンクションが次の条件を満たすかどうかをチェックします。

  • ジャンクションの入力遷移と出力遷移がそれぞれ 1 つのみである。

  • 出力遷移が入力遷移と同じ方向に続いている。

  • 入力遷移のアクションが出力遷移のトリガーまたは条件より前にない。

  • 両方の遷移がトリガーでガードされていない。

この関数のコードを見るには、余分なジャンクションの特定を参照してください。

superfluousJunctions = find(ch,"-isa","Stateflow.Junction", ...
    "-function", @(j)(isSuperfluous(j)));

numSuperfluousJunctions = numel(superfluousJunctions);
superfluousJunctionIDs = "";

for i = 1:numSuperfluousJunctions
    junction = superfluousJunctions(i);
    superfluousJunctionIDs = superfluousJunctionIDs + newline + ...
        " * Junction " + junction.SSIdNumber + " in " + junction.Path;
end

disp("These junctions are part of a transition path " + ...
    "that you can replace with a single transition:" + ...
    newline + superfluousJunctionIDs)
These junctions are part of a transition path that you can replace with a single transition:

 * Junction 161 in sfInsectExample/BehavioralLogic/Awake/Danger
 * Junction 163 in sfInsectExample/BehavioralLogic/Awake/Danger
 * Junction 153 in sfInsectExample/BehavioralLogic/Awake/Danger
 * Junction 155 in sfInsectExample/BehavioralLogic/Awake/Danger

補助関数 removeJunctions は、余分な各ジャンクション前後の 2 つの遷移を単一の遷移に置き換えます。チャートの配置を維持するために、新しい遷移は元の入力遷移と同じ時点から開始され、元の出力遷移と同じ時点で終了します。この関数のコードを見るには、ジャンクションの削除を参照してください。

removeJunctions(superfluousJunctions)

Chart with unnecessary junctions removed.

モデルを閉じる

新しいモデルに変更を保存し、モデルを閉じます。

newModel = model+"Refactored";
sfsave(model,newModel)
close_system(newModel)

補助関数

ステートの深さの取得

この関数は、特定のステートと最も近い先祖チャートまたはサブチャートの間にあるレベルの数を返します。

function depth = getDepth(state)

parent = getParent(state);

if isa(parent,"Stateflow.Chart")
    depth = 1;
elseif parent.isSubchart
    depth = 1;
else
    depth = getDepth(parent)+1;
end
end

ステートからサブチャートへの変換

この関数は、親チャートまたはサブチャートからの特定の深さにある非リーフ ステートを識別します。この関数は、各Stateflow.Stateオブジェクトの IsSubchart プロパティと ContentPreviewEnabled プロパティを変更して、それらのステートをサブチャートに変換し、新しいサブチャートのコンテンツ プレビューを無効にします。可能であれば、この関数はサブチャートの中心を固定したまま、サブチャートのサイズを既定の 90 行 60 列に縮小します。このプロセスは、新しいサブチャートごとに再帰的に繰り返されます。

function convertStatesToSubcharts(parent,maxDepth)

statesToConvert = find(parent,"-isa","Stateflow.State", ...
    "-function", @(s) (getDepth(s) == maxDepth), ...
    "-function", @(s) (~isempty(getChildren(s))), ...
    IsSubchart=false);

for i = 1:numel(statesToConvert)
    state = statesToConvert(i);
    state.IsSubchart = true;
    state.ContentPreviewEnabled = false;

    reduceWidth(state,90)
    reduceHeight(state,60)
    
    convertStatesToSubcharts(state,maxDepth);
end
end

function reduceWidth(state,newWidth)
pos = state.Position;
if pos(3) > newWidth
    state.Position = [pos(1)+pos(3)/2-newWidth/2 pos(2) newWidth pos(4)];
end
end

function reduceHeight(state,newHeight)
pos = state.Position;
if pos(4) > 60
    state.Position = [pos(1) pos(2)+pos(4)/2-newHeight/2 pos(3) newHeight];
end
end

余分なジャンクションの特定

この関数は、特定のジャンクションが次の条件を満たすかどうかをチェックします。

  • ジャンクションの入力遷移と出力遷移がそれぞれ 1 つのみである。

  • 出力遷移が入力遷移と同じ方向に続いている。

  • 入力遷移のアクションが出力遷移のトリガーまたは条件より前にない。

  • 両方の遷移がトリガーでガードされていない。

function tf = isSuperfluous(junction)

transitionIn = sinkedTransitions(junction);
transitionOut = sourcedTransitions(junction);

tf = oneIncomingTransition && oneOutgoingTransition && ...
    transitionsContinueInSameDirection && ...
    noActionBeforeTriggerOrCondition && ...
    noDoubleTriggers;

function tf = oneIncomingTransition
    tf = (numel(transitionIn)==1);
end

function tf = oneOutgoingTransition
    tf = (numel(transitionOut)==1);
end

function tf = transitionsContinueInSameDirection
    tolerance = 1.5;
    theta = abs(transitionIn.DestinationOClock-transitionOut.SourceOClock);
    tf = (abs(theta-6) < tolerance);
end

function tf = noActionBeforeTriggerOrCondition
    tf = isempty(transitionIn.ConditionAction) || ...
        (isempty(transitionOut.Trigger) && isempty(transitionOut.Condition));
end

function tf = noDoubleTriggers
    tf = isempty(transitionIn.Trigger) || isempty(transitionOut.Trigger);
end
end

ジャンクションの削除

この関数は、余分な各ジャンクション前後の遷移を単一の遷移に置き換えます。チャートの配置を維持するために、新しい遷移はジャンクションに入る元の遷移と同じ時点から開始され、ジャンクションから出る元の遷移と同じ時点で終了します。この関数は、次の規則に従って元の遷移のラベル文字列をマージします。

  • 1 つの遷移のみが空でないトリガーをもつことができる。

  • 空でない条件は AND 演算子 && を使用して結合する。

  • 空でない条件アクションと遷移アクションは横に並べて結合する。最初のアクションの末尾がコンマまたはセミコロンでない場合は、アクション間にコンマを追加する。

詳細については、遷移のアクションの定義を参照してください。

function removeJunctions(junctionsToRemove)

for i = 1:numel(junctionsToRemove)
    junction = junctionsToRemove(i);

    transitionIn = sinkedTransitions(junction);
    transitionOut = sourcedTransitions(junction);

    newSourceEndpoint = transitionIn.SourceEndpoint;
    newSourceOClock = transitionIn.SourceOClock;
    newDestinationEndpoint = transitionOut.DestinationEndpoint;
    newMidPoint = (newSourceEndpoint+newDestinationEndpoint)/2;
    newLabelString = mergeLabelStrings;

    transitionIn.Destination = transitionOut.Destination;
    transitionIn.SourceOClock = newSourceOClock;
    transitionIn.DestinationEndpoint = newDestinationEndpoint;
    transitionIn.MidPoint = newMidPoint;
    transitionIn.LabelString = newLabelString;
    transitionIn.LabelPosition = [newMidPoint 0 0];
    transitionIn.LabelPosition(1) = transitionIn.LabelPosition(1)-transitionIn.LabelPosition(3)/2;
    transitionIn.LabelPosition(2) = transitionIn.LabelPosition(2)-transitionIn.LabelPosition(4)/2;
    delete(junction)
    delete(transitionOut)
end

function label = mergeLabelStrings

    trigger1 = transitionIn.Trigger;
    trigger2 = transitionOut.Trigger;
    if isempty(trigger1)
        label = trigger2;
    elseif isempty(trigger2)
        label = trigger1;
    else
        error("Unable to merge transitions with multiple triggers " + ...
            trigger1 + " and " + trigger2 + ".")
    end

    condition1 = transitionIn.Condition;
    condition2 = transitionOut.Condition;
    if ~isempty(condition1) && ~isempty(condition2)
        label = label+"["+condition1+" && "+condition2+"]";
    elseif ~isempty(condition1)
        label = label+"["+condition1+"]";
    elseif ~isempty(condition2)
        label = label+"["+condition2+"]";
    end

    action1 = transitionIn.ConditionAction;
    action2 = transitionOut.ConditionAction;
    if ~isempty(action1) && ~isempty(action2)
        if endsWith(action1,";") || endsWith(action1,",")
            label = label+"{"+action1+action2+"}";
        else
            label = label+"{"+action1+","+action2+"}";
        end
    elseif ~isempty(action1)
        label = label+"{"+action1+"}";
    elseif ~isempty(action2)
        label = label+"{"+action2+"}";
    end

    transaction1 = transitionIn.TransitionAction;
    transaction2 = transitionOut.TransitionAction;
    if ~isempty(transaction1) && ~isempty(transaction2)
        if endsWith(transaction1,";") || endsWith(action1,",")
            label = label+"/{"+transaction1+transaction2+"}";
        else
            label = label+"/{"+transaction1+","+transaction2+"}";
        end
    elseif ~isempty(transaction1)
        label = label+"/{"+transaction1+"}";
    elseif ~isempty(transaction2)
        label = label+"/{"+transaction2+"}";
    end
end
end

参考

関数

オブジェクト

関連するトピック