function out = petriFreeChoiceDemo(mode) % PETRIFREECHOICEDEMO Slide 23 - Simple Subclass 3: Free-Choice Nets % petriFreeChoiceDemo opens the interactive teaching demonstration. % h = petriFreeChoiceDemo returns the figure handle. % net = petriFreeChoiceDemo('model') returns the SAME model and firing functions % without opening a window, for exercises and automated tests. % % Base MATLAB only; no App Designer project, Simulink or extra files needed. % To study or modify the net, go to makeModel near the end of this file. % All arcs have weight one in this example. Pre(p,t) and Post(p,t) give % tokens consumed and produced. Firing is atomic: Mnext = M-Pre(:,t)+Post(:,t). % The optional animation is an illustration, NOT a timed-net simulation. % Version 2, 11 October 2026. See README.txt for verification limitations. if nargin == 0, mode = 'gui'; end if ~(ischar(mode) && any(strcmpi(mode, {'gui','model'}))) error('PetriDemo:Mode', 'Use no argument, or the character vector ''model''.'); end net = makeModel(); validateModel(net); pre = net.Pre; post = net.Post; capacity = net.K; net.enabled = @(state) enabledAt(state, pre, post, capacity); net.fire = @(state,t) fireAt(state, t, pre, post, capacity); if strcmpi(mode,'model'), out = net; return; end % Only the marking and the firing history are simulation state. M = net.M0; history = {}; % Each entry stores the pre-firing marking and transition. sequence = []; busy = false; message = 'Choose a green transition. Grey transitions are not enabled.'; screen = get(0,'ScreenSize'); fw = min(1280, max(800, screen(3)-60)); fh = min(820, max(600, screen(4)-90)); fig = figure('Name',net.title,'NumberTitle','off','Color','w', ... 'MenuBar','none','ToolBar','none','Resize','on', ... 'Position',[max(1,(screen(3)-fw)/2) max(1,(screen(4)-fh)/2) fw fh], ... 'Tag','petriFreeChoiceDemo'); out = fig; setappdata(fig,'PetriDemoModel',net); makeText([0.035 0.935 0.94 0.045],net.title,18,true); makeText([0.035 0.890 0.94 0.036],net.subtitle,11,false); ax = axes('Parent',fig,'Units','normalized','Position',[0.025 0.415 0.70 0.46]); hold(ax,'on'); axis(ax,net.limits); axis(ax,'equal'); axis(ax,'off'); % Graphical geometry is separate from the Petri-net firing rules. placeR = 0.46; transHalfSize = [0.20 0.44]; np = numel(net.places); nt = numel(net.transitions); inPaths = cell(np,nt); outPaths = cell(np,nt); for t=1:nt for p=1:np reciprocal = (pre(p,t)>0 && post(p,t)>0); if pre(p,t)>0 inPaths{p,t} = arcPath(net.placeXY(p,:),net.transXY(t,:), ... true,false,net.inVia{p,t},reciprocal); drawArrow(inPaths{p,t},pre(p,t)); end if post(p,t)>0 outPaths{p,t} = arcPath(net.transXY(t,:),net.placeXY(p,:), ... false,true,net.outVia{p,t},reciprocal); drawArrow(outPaths{p,t},post(p,t)); end end end for p=1:np xy=net.placeXY(p,:); rectangle('Parent',ax,'Position',[xy-placeR 2*placeR 2*placeR], ... 'Curvature',[1 1],'FaceColor','w','EdgeColor',[0.04 0.32 0.28],'LineWidth',2); text(ax,net.placeLabelXY(p,1),net.placeLabelXY(p,2), ... sprintf('p%d: %s',p,net.places{p}),'Interpreter','none', ... 'HorizontalAlignment','center','FontSize',10,'FontWeight','bold'); end transitionShape=gobjects(nt,1); for t=1:nt xy=net.transXY(t,:); transitionShape(t)=rectangle('Parent',ax, ... 'Position',[xy-transHalfSize 2*transHalfSize], ... 'FaceColor',[0.78 0.78 0.78],'EdgeColor',[0.18 0.18 0.18],'LineWidth',1.7); text(ax,net.transLabelXY(t,1),net.transLabelXY(t,2), ... sprintf('t%d: %s',t,net.transitions{t}),'Interpreter','none', ... 'HorizontalAlignment','center','FontSize',10,'FontWeight','bold'); end makeText([0.745 0.835 0.23 0.035],'WHAT TO NOTICE',12,true); lessonText=makeText([0.745 0.638 0.23 0.192],net.lesson,10.5,false); %#ok makeText([0.745 0.596 0.23 0.034],'CURRENT SITUATION',12,true); notice=makeText([0.745 0.415 0.23 0.174],'',10.5,false); % A button has the same index as the corresponding column of Pre/Post. fireButtons=gobjects(nt,1); for t=1:nt fireButtons(t)=makeButton([0.035+(t-1)*0.187 0.350 0.175 0.045], ... ['Fire ' net.transitions{t}],@(~,~)onFire(t),sprintf('Fire%d',t)); end stepButton=makeButton([0.035 0.288 0.125 0.045],'Random step',@(~,~)onRandom(),'RandomStep'); undoButton=makeButton([0.171 0.288 0.087 0.045],'Undo',@(~,~)onUndo(),'Undo'); resetButton=makeButton([0.269 0.288 0.087 0.045],'Reset',@(~,~)onReset(),'Reset'); animationBox=uicontrol('Parent',fig,'Style','checkbox','Units','normalized', ... 'Position',[0.375 0.288 0.15 0.045],'String','Animate firing','Value',1, ... 'BackgroundColor','w','FontSize',10,'Tag','Animate'); makeButton([0.555 0.288 0.175 0.045],'Show Pre / Post',@(~,~)showModel(),'Model'); makeButton([0.745 0.288 0.225 0.045],'Classroom experiment', ... @(~,~)helpdlg(net.experiment,'Try this experiment'),'Experiment'); makeText([0.035 0.256 0.30 0.027],'MARKING (place order = matrix row order)',10,true); markingTable=uitable('Parent',fig,'Units','normalized', ... 'Position',[0.035 0.065 0.30 0.188], ... 'ColumnName',{'Place','M0','M'},'ColumnEditable',[false false false], ... 'ColumnWidth',{155 58 58},'RowName',[],'FontSize',10,'Tag','MarkingTable'); makeText([0.355 0.256 0.615 0.027],'FIRING HISTORY (Undo and Reset are interface actions, not transitions)',10,true); historyBox=uicontrol('Parent',fig,'Style','listbox','Units','normalized', ... 'Position',[0.355 0.065 0.615 0.188],'FontName','FixedWidth','FontSize',10, ... 'BackgroundColor','w','String',{'Initial marking'},'Tag','History'); status=makeText([0.035 0.005 0.94 0.054],'',10.5,false); updateView(); function onFire(t) if busy || ~isgraphics(fig), return; end enabled=net.enabled(M); if ~enabled(t) message=['Not fired. ' disabledReason(t)]; updateView(); return; end busy=true; cleanup=onCleanup(@releaseBusy); %#ok set([fireButtons;stepButton;undoButton;resetButton;animationBox], 'Enable','off'); oldM=M; if get(animationBox,'Value')~=0, animateFiring(t); end if ~isgraphics(fig), return; end % The model changes ONCE, after the purely visual animation. M=net.fire(oldM,t); history{end+1}=struct('before',oldM,'transition',t,'after',M); sequence(end+1)=t; message=sprintf('Fired t%d (%s): M = %s.',t,net.transitions{t},mat2str(M.')); % onCleanup re-enables controls even if an error or window close occurs. end function releaseBusy() busy=false; if isgraphics(fig) delete(findobj(ax,'Tag','PNGhost')); updateView(); end end function onRandom() if busy || ~isgraphics(fig), return; end choices=find(net.enabled(M)); if isempty(choices) message='No transition is enabled. Use Undo or Reset to explore again.'; updateView(); return; end % This is a GUI selection policy, not a probability model of the net. onFire(choices(randi(numel(choices)))); end function onUndo() if busy || ~isgraphics(fig), return; end if isempty(history), return; end M=history{end}.before; history(end)=[]; sequence(end)=[]; message='The previous marking has been restored (interface Undo).'; updateView(); end function onReset() if busy || ~isgraphics(fig), return; end M=net.M0; history={}; sequence=[]; message='Reset to M0. The firing history is empty.'; updateView(); end function updateView() if ~isgraphics(fig), return; end assert(all(net.invariantWeights*M == net.invariantValues), ... 'PetriDemo:Invariant','A model invariant has been violated.'); enabled=net.enabled(M); renderTokens(M); data=cell(np,3); for p=1:np data(p,:)={sprintf('p%d: %s',p,net.places{p}),net.M0(p),M(p)}; end set(markingTable,'Data',data); for t=1:nt if enabled(t), colour=[0.40 0.78 0.48]; else, colour=[0.78 0.78 0.78]; end set(transitionShape(t),'FaceColor',colour); set(fireButtons(t),'Enable',onOff(enabled(t)&&~busy), ... 'TooltipString',disabledReason(t)); end set(stepButton,'Enable',onOff(any(enabled)&&~busy)); set(undoButton,'Enable',onOff(~isempty(history)&&~busy)); set([resetButton;animationBox],'Enable',onOff(~busy)); set(notice,'String',currentNote()); logLines=cell(1,numel(history)+1); logLines{1}=['0: M0 = ' mat2str(net.M0.')]; for k=1:numel(history) entry=history{k}; logLines{k+1}=sprintf('%d: t%d %-12s %s -> %s',k,entry.transition, ... net.transitions{entry.transition},mat2str(entry.before.'),mat2str(entry.after.')); end set(historyBox,'String',logLines,'Value',numel(logLines), ... 'ListboxTop',max(1,numel(logLines)-4)); set(status,'String',{message; ... 'Green = enabled; grey = disabled. Animation illustrates firing, not elapsed physical time.'}); snapshot=struct('marking',M,'sequence',sequence,'enabled',enabled); setappdata(fig,'PetriDemoState',snapshot); drawnow; end function s=disabledReason(t) missing=find(Mnet.K) s='Firing would exceed a place capacity.'; else s='Enabled: all input requirements and capacity constraints are satisfied.'; end end function renderTokens(marking) delete(findobj(ax,'Tag','PNToken')); for p=1:np c=net.placeXY(p,:); count=marking(p); if count==1 plot(ax,c(1),c(2),'ko','MarkerFaceColor','k','MarkerSize',7,'Tag','PNToken'); elseif count>1 && count<=6 angles=2*pi*(0:count-1)/count; plot(ax,c(1)+0.23*cos(angles),c(2)+0.23*sin(angles),'ko', ... 'MarkerFaceColor','k','MarkerSize',6,'Tag','PNToken'); elseif count>6 % The numeral is the EXACT count, not a capacity or truncation. text(ax,c(1),c(2),sprintf('%d',count), ... 'HorizontalAlignment','center','FontWeight','bold','FontSize',13, ... 'Interpreter','none','Tag','PNToken'); end end end function animateFiring(t) set(transitionShape(t),'FaceColor',[1.0 0.75 0.22]); set(status,'String','Visualising consumption and production; the logical firing is atomic.'); renderTokens(M-pre(:,t)); for stage=1:2 if ~isgraphics(fig), return; end if stage==1, weights=pre(:,t); paths=inPaths(:,t); else, weights=post(:,t); paths=outPaths(:,t); end activePlaces=find(weights>0); moving=gobjects(numel(activePlaces),1); for k=1:numel(activePlaces) xy=pointOnPath(paths{activePlaces(k)},0); moving(k)=plot(ax,xy(1),xy(2),'o','MarkerSize',8, ... 'MarkerFaceColor',[0.93 0.42 0.08],'MarkerEdgeColor','k','Tag','PNGhost'); end for frame=1:12 if ~isgraphics(fig), return; end for k=1:numel(activePlaces) xy=pointOnPath(paths{activePlaces(k)},frame/12); set(moving(k),'XData',xy(1),'YData',xy(2)); end drawnow; if ~isgraphics(fig), return; end pause(0.015); end delete(moving(isgraphics(moving))); end end function path=arcPath(from,to,fromPlace,toPlace,via,reciprocal) if isempty(via) && reciprocal % Opposite directions bend to opposite sides: both arrowheads % remain visible for a place-transition self-loop. u=(to-from)/norm(to-from); normal=[-u(2) u(1)]; control=(from+to)/2+0.95*normal; first=border(from,control-from,fromPlace); last=border(to,control-to,toPlace); a=linspace(0,1,31).'; path=bsxfun(@times,(1-a).^2,first) + ... bsxfun(@times,2*a.*(1-a),control) + bsxfun(@times,a.^2,last); else path=[from;via;to]; path(1,:)=border(from,path(2,:)-from,fromPlace); path(end,:)=border(to,path(end-1,:)-to,toPlace); end end function xy=border(center,direction,isPlace) u=direction/norm(direction); if isPlace, distance=placeR; else, distance=min(transHalfSize./max(abs(u),1e-12)); end xy=center+(distance+0.025)*u; end function drawArrow(path,weight) colour=[0.68 0.37 0.13]; plot(ax,path(:,1),path(:,2),'-','Color',colour,'LineWidth',1.5); v=path(end,:)-path(end-1,:); u=v/norm(v); side=[-u(2) u(1)]; tip=path(end,:); triangle=[tip;tip-0.20*u+0.085*side;tip-0.20*u-0.085*side]; patch('Parent',ax,'XData',triangle(:,1),'YData',triangle(:,2), ... 'FaceColor',colour,'EdgeColor','none'); if weight~=1 xy=pointOnPath(path,0.5)+[0 0.20]; text(ax,xy(1),xy(2),num2str(weight), ... 'HorizontalAlignment','center','FontSize',10,'FontWeight','bold'); end end function h=makeText(position,str,fontSize,bold) if bold, weight='bold'; else, weight='normal'; end h=uicontrol('Parent',fig,'Style','text','Units','normalized', ... 'Position',position,'String',str,'BackgroundColor','w', ... 'HorizontalAlignment','left','FontSize',fontSize,'FontWeight',weight); end function h=makeButton(position,str,callback,tag) h=uicontrol('Parent',fig,'Style','pushbutton','Units','normalized', ... 'Position',position,'String',str,'FontSize',10, ... 'Callback',callback,'Tag',tag,'Interruptible','off','BusyAction','cancel'); end function showModel() if busy || ~isgraphics(fig), return; end txt=sprintf(['Rows: %s\nColumns: %s\n\nPre =\n%s\nPost =\n%s\n' ... 'M0 = %s\n\nEnabling: M >= Pre(:,t), and Mnext <= K.\n' ... 'Firing: Mnext = M - Pre(:,t) + Post(:,t).\n\n%s'], ... strjoin(net.places,', '),strjoin(net.transitions,', '), ... strjoin(cellstr(num2str(net.Pre)),sprintf('\n')), ... strjoin(cellstr(num2str(net.Post)),sprintf('\n')),mat2str(net.M0.'),net.modelNote); helpdlg(txt,'Petri-net model: matrices and firing rule'); end function lines=currentNote() if M(1)==1 state='Start launches the two branches.'; elseif M(6)==1 state='Meal served. This run is complete.'; elseif M(2)==1 state='Pasta and Salad: choose exactly one.'; elseif M(5)==0 state='Main ready. The side is still needed.'; else state='Both branches ready: Serve enabled.'; end lines={state;'';sprintf('Main ready: %d Side ready: %d',M(4),M(5)); ... 'Choosing the main does not consume'; 'the SidePending token.';''; ... 'Choice and concurrency coexist.'}; end end function enabled=enabledAt(M,Pre,Post,K) % Componentwise comparisons; no implicit expansion or hidden enabling flags. validateattributes(M,{'numeric'},{'real','finite','integer','nonnegative','column','numel',size(Pre,1)}); if any(M>K), error('PetriDemo:Capacity','The current marking exceeds K.'); end candidate=bsxfun(@minus,M,Pre)+Post; enabled=all(bsxfun(@ge,M,Pre),1) & all(bsxfun(@le,candidate,K),1); end function next=fireAt(M,t,Pre,Post,K) validateattributes(t,{'numeric'},{'scalar','integer','>=',1,'<=',size(Pre,2)}); enabled=enabledAt(M,Pre,Post,K); if ~enabled(t), error('PetriDemo:Disabled','Transition %d is not enabled.',t); end next=M-Pre(:,t)+Post(:,t); end function validateModel(net) assert(isequal(size(net.Pre),size(net.Post)),'Pre and Post must have equal size.'); assert(all(net.Pre(:)>=0 & net.Pre(:)==floor(net.Pre(:))),'Invalid Pre weights.'); assert(all(net.Post(:)>=0 & net.Post(:)==floor(net.Post(:))),'Invalid Post weights.'); assert(size(net.Pre,1)==numel(net.M0) && size(net.Pre,1)==numel(net.places)); assert(size(net.Pre,2)==numel(net.transitions)); assert(all(net.M0>=0 & net.M0==floor(net.M0)) && all(net.M0<=net.K)); assert(all(all(net.invariantWeights*(net.Post-net.Pre)==0)),'Invalid invariant.'); assert(isequal(net.invariantWeights*net.M0,net.invariantValues),'Invalid invariant value.'); end function xy=pointOnPath(path,fraction) lengths=sqrt(sum(diff(path,1,1).^2,2)); s=[0;cumsum(lengths)]; d=min(1,max(0,fraction))*s(end); i=find(s<=d,1,'last'); if i>=size(path,1), xy=path(end,:); return; end alpha=(d-s(i))/max(lengths(i),eps); xy=(1-alpha)*path(i,:)+alpha*path(i+1,:); end function s=onOff(condition) if condition, s='on'; else, s='off'; end end function net=makeModel() % A meal order has two branches: choose one main dish AND prepare a side. % Pasta and Salad share MainChoice and have IDENTICAL complete presets. % Pack side has a separate input, so it is independent of that choice. net.title='Slide 23 - Simple Subclass 3: Free-Choice Nets'; net.subtitle='Prepare one meal: choose Pasta OR Salad, while the side dish can be prepared independently. Serve needs both.'; net.places={'Order','MainChoice','SidePending','MainReady','SideReady','Served'}; net.transitions={'Start','Pasta','Salad','Pack side','Serve'}; net.Pre=[1 0 0 0 0; 0 1 1 0 0; 0 0 0 1 0; 0 0 0 0 1; 0 0 0 0 1; 0 0 0 0 0]; net.Post=[0 0 0 0 0; 1 0 0 0 0; 1 0 0 0 0; 0 1 1 0 0; 0 0 0 1 0; 0 0 0 0 1]; net.M0=[1;0;0;0;0;0]; net.K=inf(6,1); net.invariantWeights=[1 1 0 1 0 1; 1 0 1 0 1 1]; net.invariantValues=[1;1]; net.placeXY=[1.1 4.4; 5.6 6.0; 5.6 2.6; 10.6 6.0; 10.6 2.6; 15.3 4.4]; net.transXY=[3.2 4.4; 8.1 6.8; 8.1 4.8; 8.1 2.6; 13.0 4.4]; net.limits=[0 17 0.5 8.5]; net.placeLabelXY=net.placeXY+[zeros(6,1) 0.80*ones(6,1)]; net.placeLabelXY(3,:)=[5.6 1.75]; net.placeLabelXY(5,:)=[10.6 1.75]; net.transLabelXY=net.transXY+[zeros(5,1) -0.85*ones(5,1)]; net.transLabelXY(2,:)=[8.1 7.7]; net.inVia=cell(6,5); net.outVia=cell(6,5); net.lesson={'If two transitions share an input,'; 'their complete input sets are equal.';''; ... 'Inputs(Pasta) = Inputs(Salad)'; '= {MainChoice}.'; ... 'Pasta OR Salad: a conflict.'; 'Pack side: an independent task.'; ... 'Serve synchronises both branches.'}; net.experiment={'1. Fire Start: Pasta, Salad and Pack side are all enabled.'; ... '2. Fire Pasta. Salad becomes disabled, but Pack side remains enabled.'; ... '3. Fire Pack side and Serve.'; ... '4. Reset. Try Start, Pack side, Salad, Serve instead.'; ... 'Question: what changes if an extra input place is required only by Pasta? The identical-preset condition is violated.'}; net.modelNote='The shared-input transitions Pasta and Salad have the same single input place. This example satisfies both the slide condition and the stricter single-choice-place convention.'; end