1classdef Environment < Ensemble
2 % An environment model defined by a collection of network sub-models
3 % coupled with an environment transition rule that selects
the active
6 % Copyright (c) 2012-2026, Imperial College London
12 num_stages; % Expected number of stages (optional,
for API consistency)
13 proc; % Markovian representation of each stage transition
14 holdTime; % holding times
15 probEnv; % steady-stage probability of
the environment
16 probOrig; % probability that a request originated from phase
17 resetFun; % function implementing
the reset policy
for queue lengths
18 resetEnvRatesFun; % function implementing
the reset policy
for environment rates
19 resetStateFun; % function implementing
the reset policy
for the full state-probability vector (SolverENV statevec analyzer)
20 nodeFailures; % cell array of node breakdown/repair descriptors recorded by addNodeBreakdown/addNodeRepair
24 function self = Environment(name, num_stages)
25 % SELF = ENVIRONMENT(NAME, NUM_STAGES)
26 % NAME - Name of
the environment
27 % NUM_STAGES - (Optional) Expected number of stages. If provided, used
for validation only.
28 % Stages can still be added/removed dynamically.
34 self.num_stages = num_stages;
35 self.nodeFailures = {};
36 self.envGraph = digraph();
37 self.envGraph.Nodes.Model = cell(0);
38 self.envGraph.Nodes.Type = cell(0);
41 function name = addStage(self, name, type, model)
42 wcfg = warning; % store warning configuration
43 warning(
'off',
'MATLAB:table:RowsAddedExistingVars');
44 self.envGraph = self.envGraph.addnode(name);
45 warning(wcfg); % restore warning configuration
46 self.envGraph.Nodes.Model{end} = model;
47 self.envGraph.Nodes.Type{end} = type;
48 E = height(self.envGraph.Nodes);
50 if self.envGraph.Nodes.Model{1}.getNumberOfStatefulNodes ~= model.getNumberOfStatefulNodes
51 line_error(mfilename,
'Unsupported feature. Random environment stages must map to networks with identical number of stateful nodes.');
56 self.env{e,h} = Disabled.getInstance();
57 self.env{h,e} = Disabled.getInstance();
60 self.ensemble{E} = model;
63 function self = addTransition(self, fromName, toName, distrib, resetFun, resetEnvRatesFun, resetStateFun)
64 self.envGraph = self.envGraph.addedge(fromName, toName);
65 e = self.envGraph.findnode(fromName);
66 h = self.envGraph.findnode(toName);
67 self.env{e,h} = distrib;
68 if nargin<5 %~exist(
'resetFun',
'var')
69 self.resetFun{e,h} = @(q) q;
71 self.resetFun{e,h} = resetFun;
73 if nargin<6 %~exist(
'resetEnvRatesFun',
'var')
74 self.resetEnvRatesFun{e,h} = @(originalDist, QExit, UExit, TExit) originalDist;
76 self.resetEnvRatesFun{e,h} = resetEnvRatesFun;
78 if nargin<7 %~exist(
'resetStateFun',
'var')
79 self.resetStateFun{e,h} = @(pi) pi; % identity: carry
the exit distribution unchanged
81 self.resetStateFun{e,h} = resetStateFun;
86 E = height(self.envGraph.Nodes);
87 T = height(self.envGraph.Edges);
91 e = self.envGraph.findnode(self.envGraph.Edges.EndNodes{t,1});
92 h = self.envGraph.findnode(self.envGraph.Edges.EndNodes{t,2});
93 self.envGraph.Edges.Distribution{t} = self.env{e,h};
96 % analyse holding times
104 if isa(self.env{e,h},
'Disabled')
105 emmap{e}{h} = {0,0}; % multiclass
MMAP representation
107 emmap{e}{h} = self.env{e,h}.getProcess; % multiclass
MMAP representation
111 emmap{e}{h}{2+j} = emmap{e}{h}{2};
113 emmap{e}{h}{2+j} = 0 * emmap{e}{h}{2};
117 self.holdTime{e} = emmap{e}{e};
119 self.holdTime{e}{1} = krons(self.holdTime{e}{1},emmap{e}{h}{1});
121 self.holdTime{e}{j} = krons(self.holdTime{e}{j},emmap{e}{h}{j});
122 completion_rates = self.holdTime{e}{j}*ones(length(self.holdTime{e}{j}),1);
123 self.holdTime{e}{j} = 0*self.holdTime{e}{j};
124 self.holdTime{e}{j}(:,1) = completion_rates;
126 self.holdTime{e} = mmap_normalize(self.holdTime{e});
128 count_lambda = mmap_count_lambda(self.holdTime{e}); % completiom rates
for the different transitions
129 Pemb(e,:) = count_lambda/sum(count_lambda);
135 A = zeros(E); I=eye(E);
137 lambda(e) = 1/map_mean(self.holdTime{e});
139 A(e,h) = -lambda(e)*(I(e,h)-Pemb(e,h));
144 penv = ctmc_solve_reducible(A);
146 self.probOrig = zeros(E);
149 self.probOrig(h,e) = penv(h) * lambda(h) * Pemb(h,e);
152 self.probOrig(:,e) = self.probOrig(:,e) / sum(self.probOrig(:,e));
156 %T = A(lambda>0, lambda>0), % subgenerator
157 %t = A(lambda>0, lambda==0), % exit vector
161 function env = getEnv(self)
165 function self = setEnv(self, env)
169 function self = setStageName(self, stageId, name)
170 self.stageNames{stageId} = name;
173 function self = setStageType(self, stageId, stageCategory)
174 if ischar(stageCategory)
175 self.stageTypes(stageId) = categorical(stageCategory);
176 elseif iscategorical(stageCategory)
177 self.stageTypes(stageId) = stageCategory;
179 line_error(mfilename,
'Stage type must be of type categorical, e.g., categorical("My Semantics").');
183 function ET = getStageTable(self)
184 E = height(self.envGraph.Nodes);
187 type = categorical([]);
188 if isempty(self.probEnv)
193 type(e,1) = self.envGraph.Nodes.Type{e};
195 Prob = self.probEnv(:);
196 Name = categorical(self.envGraph.Nodes.Name(:));
197 Model = self.ensemble(:);
199 HoldT{e,1} = self.holdTime{e};
202 ET = Table(Stage, Name, Type, Prob, HoldT, Model);
205 function ET = getStageT(self)
206 % GETSTAGET Short alias
for getStageTable
207 ET = self.getStageTable();
210 function
RT = getReliabilityTable(self)
211 %
RT = GETRELIABILITYTABLE()
212 % Compute system-wide reliability metrics (MTTF, MTTR, MTBF, Availability)
215 %
RT - Table with columns: Metric, Value, Unit, Description
218 % env = Environment(
'ServerEnv');
219 % env.addNodeFailureRepair(model,
'Server', Exp(0.1), Exp(1.0), Exp(0.5));
221 % reliabilityTable = env.getReliabilityTable();
223 % Step 1: Initialize and validate
224 if isempty(self.probEnv)
228 E = height(self.envGraph.Nodes);
230 line_error(mfilename, 'Environment has no stages. Add stages before computing reliability metrics.');
233 % Step 2: Identify stage types
234 stageNames = self.envGraph.Nodes.Name;
235 upIdx = find(strcmp(stageNames, 'UP'));
238 line_error(mfilename, 'No UP stage found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
241 % Find all DOWN stages
242 downIdx = find(startsWith(stageNames, 'DOWN_'));
245 line_error(mfilename, 'No DOWN stages found. Use addNodeBreakdown/addNodeRepair to configure breakdown/repair transitions.');
248 % Step 3: Extract breakdown rates (UP -> DOWN_*)
251 if ~isa(self.env{upIdx, h},
'Disabled')
252 lambda_h = 1 / self.env{upIdx, h}.getMean();
253 breakdownRates(end+1) = lambda_h;
257 if isempty(breakdownRates)
258 line_error(mfilename,
'No breakdown transitions found (UP -> DOWN_*).');
261 % Total failure rate (competing risks)
262 lambda_total = sum(breakdownRates);
263 MTTF = 1 / lambda_total;
265 % Step 4: Extract repair rates (DOWN_* -> UP)
270 if ~isa(self.env{e, upIdx}, 'Disabled
')
271 mu_e = 1 / self.env{e, upIdx}.getMean();
272 repairRates(end+1) = mu_e;
273 downProbs(end+1) = self.probEnv(e);
277 if isempty(repairRates)
278 line_error(mfilename, 'No repair transitions found (DOWN_* -> UP).
');
281 % Normalize probabilities over DOWN states only
282 totalDownProb = sum(downProbs);
284 downProbsNorm = downProbs / totalDownProb;
285 % Weighted average repair time
286 MTTR = sum(downProbsNorm ./ repairRates);
288 % Fallback: simple average if no steady-state probability
289 MTTR = mean(1 ./ repairRates);
292 % Step 5: Compute derived metrics
295 % Availability from steady-state probabilities
296 availUp = self.probEnv(upIdx);
297 availDown = sum(self.probEnv(downIdx));
298 Availability = availUp / (availUp + availDown);
300 % Step 6: Create output table
301 Metric = categorical({'MTTF
'; 'MTTR
'; 'MTBF
'; 'Availability
'});
302 Value = [MTTF; MTTR; MTBF; Availability];
303 Unit = categorical({'time units
'; 'time units
'; 'time units
'; 'probability
'});
304 Description = categorical({
305 'Mean time to failure (UP -> DOWN)
';
306 'Mean time to repair (DOWN -> UP)
';
307 'Mean time between failures (MTTF + MTTR)
';
308 'Steady-state probability of UP state
'
311 RT = Table(Metric, Value, Unit, Description);
314 function RT = relT(self)
315 % RELT Short alias for getReliabilityTable
316 RT = self.getReliabilityTable();
319 function RT = getRelT(self)
320 % GETRELT Short alias for getReliabilityTable
321 RT = self.getReliabilityTable();
324 function RT = relTable(self)
325 % RELTABLE Short alias for getReliabilityTable
326 RT = self.getReliabilityTable();
329 function RT = getRelTable(self)
330 % GETRELTABLE Short alias for getReliabilityTable
331 RT = self.getReliabilityTable();
334 function printStageTable(self)
335 % PRINTSTAGETABLE Print a formatted table showing all stages, their properties, and transitions
337 % Displays stage names, types, associated networks, and transition rates.
340 % env = Environment('MyEnv
');
341 % env.addStage('UP
', 'operational
', model1);
342 % env.addStage('DOWN
', 'failed
', model2);
343 % env.addTransition('UP
', 'DOWN
', Exp(0.1));
344 % env.printStageTable();
346 E = height(self.envGraph.Nodes);
347 fprintf('Stage Table:\n
');
348 fprintf('============\n
');
351 stageName = self.envGraph.Nodes.Name{e};
352 stageType = self.envGraph.Nodes.Type{e};
353 model = self.envGraph.Nodes.Model{e};
355 fprintf('Stage %d: %s (Type: %s)\n
', e, stageName, stageType);
357 fprintf(' - Network: %s\n
', model.getName());
358 fprintf(' - Nodes: %d\n
', model.getNumberOfNodes());
359 fprintf(' - Classes: %d\n
', model.getNumberOfClasses());
364 T = height(self.envGraph.Edges);
366 fprintf('\nTransitions:\n
');
368 fromName = self.envGraph.Edges.EndNodes{t, 1};
369 toName = self.envGraph.Edges.EndNodes{t, 2};
370 e = self.envGraph.findnode(fromName);
371 h = self.envGraph.findnode(toName);
372 if ~isa(self.env{e, h}, 'Disabled
')
373 rate = 1 / self.env{e, h}.getMean();
374 fprintf(' %s -> %s: rate = %.4f\n
', fromName, toName, rate);
380 function self = addNodeBreakdown(self, baseModel, nodeOrName, breakdownDist, downServiceDist, varargin)
381 % SELF = ADDNODEBREAKDOWN(BASEMODEL, NODEORNAME, BREAKDOWNDIST, DOWNSERVICEDIST, RESETFUN)
382 % Adds UP and DOWN stages for a node that can break down and repair
385 % baseModel - The base network model with normal (UP) service rates
386 % nodeOrName - Node object or name of the node that can break down
387 % breakdownDist - Distribution for time until breakdown (UP->DOWN transition)
388 % downServiceDist - Service distribution when the node is down
389 % resetFun - (Optional) Reset policy for queue lengths on breakdown.
390 % Either a function handle @(q) -> q, or one of the named
391 % policies 'keep
' (identity) and 'clear
' (empty the queues).
392 % Default: 'keep
'. Only named policies are serializable.
395 % model = Network('MyNetwork
');
396 % queue = Queue(model, 'Server1
', SchedStrategy.FCFS);
397 % class = ClosedClass(model, 'Jobs
', 10, queue, 0);
398 % queue.setService(class, Exp(2)); % UP service rate
400 % env = Environment('ServerEnv
');
401 % env.addNodeBreakdown(model, 'Server1
', Exp(0.1), Exp(0.5));
402 % % Or using node object:
403 % env.addNodeBreakdown(model, queue, Exp(0.1), Exp(0.5));
405 % Extract node name if a Node object is passed
406 if isa(nodeOrName, 'Node
')
407 nodeName = nodeOrName.name;
409 nodeName = nodeOrName;
415 resetSpec = varargin{1};
417 [resetFun, resetName] = Environment.resolveResetPolicy(resetSpec);
419 % Create UP stage (if this is the first call)
420 if height(self.envGraph.Nodes) == 0
421 upModel = baseModel.copy();
422 self.addStage('UP
', 'operational
', upModel);
425 % Create DOWN stage with modified service rate for the specified node
426 downModel = baseModel.copy();
427 nodes = downModel.getNodes();
429 for i = 1:length(nodes)
430 if strcmp(nodes{i}.name, nodeName)
437 line_error(mfilename, sprintf('Node
"%s" not found in
the base model.
', nodeName));
440 % Update service distribution for the down node
441 classes = downModel.getClasses();
442 for c = 1:length(classes)
443 nodes{nodeIdx}.setService(classes{c}, downServiceDist);
447 downStageName = sprintf('DOWN_%s
', nodeName);
448 self.addStage(downStageName, 'failed
', downModel);
450 % Add breakdown transition (UP -> DOWN)
451 self.addTransition('UP
', downStageName, breakdownDist, resetFun);
453 % Record the descriptor so that the breakdown can be serialized
454 % declaratively (linemodel_save "nodeFailures"). The expanded
455 % stages/transitions carry the structure, but the reset policy is a
456 % function and is only recoverable from this record.
459 nf.breakdown = breakdownDist;
460 nf.downService = downServiceDist;
462 nf.breakdownResetPolicy = resetName;
463 nf.repairResetPolicy = '';
464 self.nodeFailures{end+1} = nf;
467 function self = addNodeRepair(self, nodeOrName, repairDist, varargin)
468 % SELF = ADDNODEREPAIR(NODEORNAME, REPAIRDIST, RESETFUN)
469 % Adds repair transition from DOWN to UP stage for a previously added breakdown
472 % nodeOrName - Node object or name of the node that can be repaired
473 % repairDist - Distribution for repair time (DOWN->UP transition)
474 % resetFun - (Optional) Reset policy for queue lengths on repair.
475 % Either a function handle @(q) -> q, or one of the named
476 % policies 'keep
' (identity) and 'clear
' (empty the queues).
477 % Default: 'keep
'. Only named policies are serializable.
480 % env.addNodeRepair('Server1
', Exp(1.0));
481 % % Or using node object:
482 % env.addNodeRepair(queue, Exp(1.0));
484 % Extract node name if a Node object is passed
485 if isa(nodeOrName, 'Node
')
486 nodeName = nodeOrName.name;
488 nodeName = nodeOrName;
494 resetSpec = varargin{1};
496 [resetFun, resetName] = Environment.resolveResetPolicy(resetSpec);
498 downStageName = sprintf('DOWN_%s
', nodeName);
500 % Verify DOWN stage exists
501 if isempty(self.envGraph.findnode(downStageName))
502 line_error(mfilename, sprintf('DOWN stage for node
"%s" not found. Call addNodeBreakdown first.
', nodeName));
505 % Add repair transition (DOWN -> UP)
506 self.addTransition(downStageName, 'UP
', repairDist, resetFun);
508 % Complete the descriptor recorded by addNodeBreakdown.
509 idx = self.findNodeFailure(nodeName);
511 self.nodeFailures{idx}.repair = repairDist;
512 self.nodeFailures{idx}.repairResetPolicy = resetName;
516 function self = registerNodeFailure(self, nodeName, breakdownDist, repairDist, downServiceDist, breakdownPolicy, repairPolicy)
517 % SELF = REGISTERNODEFAILURE(NODENAME, BREAKDOWNDIST, REPAIRDIST, DOWNSERVICEDIST, BREAKDOWNPOLICY, REPAIRPOLICY)
518 % Attach a node breakdown/repair descriptor to stages that already exist.
520 % This is the counterpart of addNodeBreakdown/addNodeRepair for the case
521 % where the UP and DOWN_<node> stages and their transitions have already
522 % been built (for instance by linemodel_load reading the expanded
523 % stages/transitions form). It records the descriptor and applies the
524 % queue-length reset policies, which the expanded form cannot carry.
525 downStageName = sprintf('DOWN_%s
', nodeName);
526 upIdx = self.envGraph.findnode('UP
');
527 downIdx = self.envGraph.findnode(downStageName);
528 if isempty(upIdx) || upIdx == 0
529 line_error(mfilename, sprintf(['Cannot register a node failure on
"%s": no UP stage
is ' ...
530 'defined in this environment.
'], nodeName));
532 if isempty(downIdx) || downIdx == 0
533 line_error(mfilename, sprintf(['Cannot register a node failure on
"%s": no
"%s" stage
is ' ...
534 'defined in this environment.
'], nodeName, downStageName));
537 [breakdownFun, breakdownName] = Environment.resolveResetPolicy(breakdownPolicy);
538 self.resetFun{upIdx, downIdx} = breakdownFun;
540 if ~isempty(repairDist)
541 [repairFun, repairName] = Environment.resolveResetPolicy(repairPolicy);
542 self.resetFun{downIdx, upIdx} = repairFun;
547 nf.breakdown = breakdownDist;
548 nf.downService = downServiceDist;
549 nf.repair = repairDist;
550 nf.breakdownResetPolicy = breakdownName;
551 nf.repairResetPolicy = repairName;
552 idx = self.findNodeFailure(nodeName);
554 self.nodeFailures{idx} = nf;
556 self.nodeFailures{end+1} = nf;
560 function idx = findNodeFailure(self, nodeName)
561 % IDX = FINDNODEFAILURE(NODENAME)
562 % Index of the node-failure descriptor for NODENAME, or 0 if absent.
564 for i = 1:length(self.nodeFailures)
565 if strcmp(self.nodeFailures{i}.node, nodeName)
572 function self = addNodeFailureRepair(self, baseModel, nodeOrName, breakdownDist, repairDist, downServiceDist, varargin)
573 % SELF = ADDNODEFAILUREREPAIR(BASEMODEL, NODEORNAME, BREAKDOWNDIST, REPAIRDIST, DOWNSERVICEDIST, RESETBREAKDOWN, RESETREPAIR)
574 % Convenience method to add both breakdown and repair for a node
577 % baseModel - The base network model with normal (UP) service rates
578 % nodeOrName - Node object or name of the node that can break down and repair
579 % breakdownDist - Distribution for time until breakdown
580 % repairDist - Distribution for repair time
581 % downServiceDist - Service distribution when the node is down
582 % resetBreakdown - (Optional) Reset function for breakdown transition
583 % resetRepair - (Optional) Reset function for repair transition
586 % env = Environment('ServerEnv
');
587 % env.addNodeFailureRepair(model, 'Server1
', Exp(0.1), Exp(1.0), Exp(0.5));
588 % % Or using node object:
589 % env.addNodeFailureRepair(model, queue, Exp(0.1), Exp(1.0), Exp(0.5));
591 % Extract node name if a Node object is passed
592 if isa(nodeOrName, 'Node
')
593 nodeName = nodeOrName.name;
595 nodeName = nodeOrName;
598 resetBreakdown = 'keep
';
599 resetRepair = 'keep
';
602 resetBreakdown = varargin{1};
605 resetRepair = varargin{2};
608 self.addNodeBreakdown(baseModel, nodeName, breakdownDist, downServiceDist, resetBreakdown);
609 self.addNodeRepair(nodeName, repairDist, resetRepair);
612 function self = setBreakdownResetPolicy(self, nodeOrName, resetFun)
613 % SELF = SETBREAKDOWNRESETPOLICY(NODEORNAME, RESETFUN)
614 % Update the reset policy for breakdown transitions (UP -> DOWN) of a node
617 % nodeOrName - Node object or name of the node
618 % resetFun - Reset policy for queue lengths on breakdown. Either a
619 % function handle, or one of the named policies 'keep
'
620 % (identity) and 'clear
' (empty the queues). Only named
621 % policies are serializable.
622 % Example: @(q) 0*q to clear queues, @(q) q to keep jobs
625 % env.setBreakdownResetPolicy('Server1
', @(q) 0*q);
626 % % Or using node object:
627 % env.setBreakdownResetPolicy(queue, 'clear
');
629 % Extract node name if a Node object is passed
630 if isa(nodeOrName, 'Node
')
631 nodeName = nodeOrName.name;
633 nodeName = nodeOrName;
636 [resetFun, resetName] = Environment.resolveResetPolicy(resetFun);
637 downStageName = sprintf('DOWN_%s
', nodeName);
639 % Find UP and DOWN stage indices
640 upIdx = self.envGraph.findnode('UP
');
641 downIdx = self.envGraph.findnode(downStageName);
643 if isempty(upIdx) || upIdx == 0
644 line_error(mfilename, 'UP stage not found. Call addNodeBreakdown first.
');
646 if isempty(downIdx) || downIdx == 0
647 line_error(mfilename, sprintf('DOWN stage for node
"%s" not found. Call addNodeBreakdown first.
', nodeName));
650 % Update the reset function for the breakdown transition (UP -> DOWN)
651 self.resetFun{upIdx, downIdx} = resetFun;
652 idx = self.findNodeFailure(nodeName);
654 self.nodeFailures{idx}.breakdownResetPolicy = resetName;
658 function self = setRepairResetPolicy(self, nodeOrName, resetFun)
659 % SELF = SETREPAIRRESETPOLICY(NODEORNAME, RESETFUN)
660 % Update the reset policy for repair transitions (DOWN -> UP) of a node
663 % nodeOrName - Node object or name of the node
664 % resetFun - Reset policy for queue lengths on repair. Either a
665 % function handle, or one of the named policies 'keep
'
666 % (identity) and 'clear
' (empty the queues). Only named
667 % policies are serializable.
668 % Example: @(q) q to keep jobs, @(q) 0*q to clear queues
671 % env.setRepairResetPolicy('Server1
', @(q) q);
672 % % Or using node object:
673 % env.setRepairResetPolicy(queue, 'keep
');
675 % Extract node name if a Node object is passed
676 if isa(nodeOrName, 'Node
')
677 nodeName = nodeOrName.name;
679 nodeName = nodeOrName;
682 [resetFun, resetName] = Environment.resolveResetPolicy(resetFun);
683 downStageName = sprintf('DOWN_%s
', nodeName);
685 % Find UP and DOWN stage indices
686 upIdx = self.envGraph.findnode('UP
');
687 downIdx = self.envGraph.findnode(downStageName);
689 if isempty(upIdx) || upIdx == 0
690 line_error(mfilename, 'UP stage not found. Call addNodeBreakdown first.
');
692 if isempty(downIdx) || downIdx == 0
693 line_error(mfilename, sprintf('DOWN stage for node
"%s" not found. Call addNodeBreakdown first.
', nodeName));
696 % Update the reset function for the repair transition (DOWN -> UP)
697 self.resetFun{downIdx, upIdx} = resetFun;
698 idx = self.findNodeFailure(nodeName);
700 self.nodeFailures{idx}.repairResetPolicy = resetName;
706 function [resetFun, resetName] = resolveResetPolicy(spec)
707 % [RESETFUN, RESETNAME] = RESOLVERESETPOLICY(SPEC)
708 % Resolve a queue-length reset policy given either a named policy or a
711 % Named policies (the only serializable ones):
712 % 'keep
' - carry the queue lengths across the transition, @(q) q
713 % 'clear
' - empty the queues on the transition, @(q) 0*q
715 % A function handle is returned unchanged and reported as 'custom
':
716 % an arbitrary reset function cannot be reproduced from JSON.
717 if ischar(spec) || isstring(spec)
718 switch lower(char(spec))
726 line_error(mfilename, sprintf(['Unknown reset policy
"%s". Use
''keep
'',
''clear
'',
' ...
727 'or a function handle @(q) -> q.
'], char(spec)));
729 elseif isa(spec, 'function_handle
')
731 resetName = 'custom
';
733 line_error(mfilename, ['Reset policy must be a function handle @(q) -> q, or one of
the ' ...
734 'named policies
''keep
'' and
''clear
''.
']);