1function model = linemodel_load(filename)
2% LINEMODEL_LOAD Load a LINE model from JSON.
4% MODEL = LINEMODEL_LOAD(FILENAME) loads a model from
the specified JSON
5% file (conforming to line-model.schema.json) and returns a Network,
6% LayeredNetwork, Workflow, or Environment
object.
9% filename - path to a .json file
12% model - Network, LayeredNetwork, Workflow, or Environment
object
15% model = linemodel_load('mm1.json');
16% solver = SolverMVA(model);
17% AvgTable = solver.getAvgTable();
19% Copyright (c) 2012-2026, Imperial College London
22jsonText = fileread(filename);
23doc = jsondecode(jsonText);
25if ~isfield(doc, 'model')
26 error('linemodel_load:noModel', 'JSON file does not contain a "model" field.');
34 model = json2network(data, jsonText);
36 model = json2layered(data);
38 model = json2workflow(data);
40 model = json2environment(data, jsonText);
42 error('linemodel_load:unknownType', 'Unsupported model type: %s', mtype);
47% =========================================================================
48% Network deserialization
49% =========================================================================
51function model = json2network(data, rawJson)
52% Reconstruct a Network from decoded JSON struct.
53% rawJson
is the original text, used for parsing routing keys with commas.
56if isfield(data, 'name')
57 modelName = data.name;
59model = Network(modelName);
61% --- Create
nodes (before classes, since ClosedClass needs refstat) ---
63if isfield(data,
'nodes')
77 node = create_node(model, nd, nd_name, nd_type);
78 nodeList{end+1} = node; %#ok<AGROW>
81node_map = containers.Map();
82for i = 1:length(nodeList)
83 node_map(nodeList{i}.name) = nodeList{i};
86% --- Deferred node linking (Fork/Join, Fork tasksPerLink) ---
87if isfield(data,
'nodes')
90 nds2 = num2cell(nds2);
92 for i = 1:length(nds2)
95 node2 = node_map(nd_name2);
96 % Join: link to paired Fork
97 if isfield(nd2,
'forkNode') && isa(node2,
'Join')
98 if node_map.isKey(nd2.forkNode)
99 node2.joinOf = node_map(nd2.forkNode);
102 % Fork: set tasksPerLink
103 if isfield(nd2, 'tasksPerLink') && isa(node2, 'Fork')
104 node2.setTasksPerLink(nd2.tasksPerLink);
109% --- Create classes ---
111if isfield(data,
'classes')
116 for i = 1:length(cls)
123 if isfield(cd,
'priority'), prio = cd.priority; end
124 jc = OpenClass(model, cname, prio);
125 case {
'Closed',
'SelfLooping'}
128 if isfield(cd,
'refNode') && node_map.isKey(cd.refNode)
129 refNode = node_map(cd.refNode);
132 if isfield(cd,
'priority'), prio = cd.priority; end
134 error(
'linemodel_load:noRefNode', ...
135 '%s class "%s" has no valid refNode.', ctype, cname);
137 if strcmp(ctype,
'SelfLooping')
138 jc = SelfLoopingClass(model, cname, pop, refNode, prio);
140 jc = ClosedClass(model, cname, pop, refNode, prio);
144 if isfield(cd, 'priority'), prio = cd.priority; end
145 sigType = SignalType.NEGATIVE;
146 if isfield(cd, 'signalType')
147 sigType = SignalType.fromText(cd.signalType);
149 if ~isfield(cd, 'openOrClosed')
150 % A bare Signal subclasses JobClass directly and
is neither
151 % open nor closed;
the writer marks it by omitting
152 % 'openOrClosed'. Only a bare Signal
is accepted by
153 % JobClass.setReplySignalClass.
154 jc = Signal(model, cname, sigType, prio);
155 elseif strcmp(cd.openOrClosed, 'Closed')
157 if isfield(cd, 'refNode') && node_map.isKey(cd.refNode)
158 refNode = node_map(cd.refNode);
161 error('linemodel_load:noRefNode', ...
162 'ClosedSignal "%s" has no valid refNode.', cname);
164 jc = ClosedSignal(model, cname, sigType, refNode, prio);
166 jc = OpenSignal(model, cname, sigType, prio);
168 % Removal distribution
169 if isfield(cd, 'removalDistribution')
170 remDist = json2dist(cd.removalDistribution);
172 jc.setRemovalDistribution(remDist);
176 if isfield(cd, 'removalPolicy')
177 jc.setRemovalPolicy(RemovalPolicy.fromText(cd.removalPolicy));
180 jc = OpenClass(model, cname);
182 if isfield(cd, 'deadline') && isfinite(cd.deadline)
183 jc.deadline = cd.deadline;
185 if isfield(cd, 'isReferenceClass') && cd.isReferenceClass
186 jc.setReferenceClass(true);
188 % Class-level (global) patience. A node-scoped 'patience' entry, restored
189 % later, overrides this for
the station it names.
190 if isfield(cd, 'patience') && ~isempty(cd.patience)
191 patDist = json2dist(cd.patience);
192 if ~isempty(patDist) && ~isa(patDist, 'Disabled')
193 if isfield(cd, 'impatienceType')
194 jc.setPatience(str_to_impatience(cd.impatienceType), patDist);
196 jc.setPatience(patDist);
200 classesList{end+1} = jc; %#ok<AGROW>
203class_map = containers.Map();
204for i = 1:length(classesList)
205 class_map(classesList{i}.name) = classesList{i};
208% --- Resolve signal targetClass associations ---
209if isfield(data,
'classes')
211 if isstruct(cls2), cls2 = num2cell(cls2); end
212 for i = 1:length(cls2)
214 if isfield(cd2,
'type') && strcmp(cd2.type,
'Signal') && isfield(cd2,
'targetClass')
215 if class_map.isKey(cd2.name) && class_map.isKey(cd2.targetClass)
216 sigCls = class_map(cd2.name);
217 sigCls.forJobClass(class_map(cd2.targetClass));
220 % Reply signal
binding (sn.syncreply). Resolved in a second pass because
221 %
the reply class may be declared after
the class that references it.
222 if isfield(cd2, 'replySignalClass') && class_map.isKey(cd2.name) ...
223 && class_map.isKey(cd2.replySignalClass)
224 class_map(cd2.name).setReplySignalClass(class_map(cd2.replySignalClass));
229% --- Set service/arrival distributions ---
230if isfield(data, '
nodes')
235 for i = 1:length(nds)
238 node = node_map(nd_name);
240 % An OI/PAS queue takes its parameterization from oiServiceRate below,
241 % never from per-
class distributions: Queue.setService rejects a
242 % distribution on such a queue. The JAR writer emits a per-class
243 % representative service for these queues anyway (it reads it back and
244 % then overrides it), so files from
the JAR must be tolerated by skipping
245 % that block rather than failing on it.
246 isOIQueue = isa(node, 'Queue') && ~isa(node, 'Delay') && ...
247 (node.schedStrategy == SchedStrategy.PAS || node.schedStrategy == SchedStrategy.OI);
248 if isfield(nd, 'service') && ~isempty(nd.service) && ~isOIQueue
250 svcFields = fieldnames(svc);
251 for f = 1:length(svcFields)
252 cname = svcFields{f};
253 distJson = svc.(cname);
254 if ~class_map.isKey(cname)
257 jc = class_map(cname);
258 dist = json2dist(distJson);
260 if isa(node,
'Source')
261 node.setArrival(jc, dist);
262 elseif isa(node, 'Place')
263 % Assigning a service process turns
the Place into a
264 % queueing Place and installs
the server section matching
265 %
the scheduling strategy set in create_node.
266 node.setService(jc, dist);
267 elseif isa(node, 'Queue') || isa(node, 'Delay')
268 % Pass DPS/GPS weight if available
270 if isfield(nd, 'schedParams')
271 sp_tmp = nd.schedParams;
272 if isfield(sp_tmp, cname)
273 weight = sp_tmp.(cname);
276 node.setService(jc, dist, weight);
282 % Batch arrivals, applied after setArrival because setArrivalBatch
283 % validates
the batch law independently of
the interarrival process.
284 if isfield(nd, 'arrivalBatch') && ~isempty(nd.arrivalBatch) && isa(node, 'Source')
285 batchFields = fieldnames(nd.arrivalBatch);
286 for f = 1:numel(batchFields)
287 cname = batchFields{f};
288 if ~class_map.isKey(cname)
291 bdist = json2dist(nd.arrivalBatch.(cname));
293 node.setArrivalBatch(class_map(cname), bdist);
298 % Marked (
MMAP) arrival
binding: rebind
the shared MarkedMAP so mark
299 % k drives
the k-th listed
class (overwrites
the per-
class copies set
300 % in
the loop above with one shared object).
301 if isfield(nd,
'markedClasses') && ~isempty(nd.markedClasses) && isa(node,
'Source')
302 markedNames = nd.markedClasses;
303 if ischar(markedNames), markedNames = {markedNames}; end
305 for f = 1:numel(markedNames)
306 mnm = markedNames{f};
307 if class_map.isKey(mnm)
308 markedList{end+1} = class_map(mnm); %#ok<AGROW>
311 if ~isempty(markedList)
312 firstDist = node.getArrivalProcess(markedList{1}.index);
313 if isa(firstDist,
'MarkedMAP')
314 node.setMarkedArrival(firstDist, markedList);
319 % ClassSwitch matrix (dict format: classSwitchMatrix)
320 if isfield(nd, 'classSwitchMatrix') && isa(node, 'ClassSwitch')
321 csm_data = nd.classSwitchMatrix;
322 classes = model.getClasses();
325 class_idx = containers.Map();
327 class_idx(classes{ci}.name) = ci;
329 fromFields = fieldnames(csm_data);
330 for fi = 1:length(fromFields)
331 fromName = fromFields{fi};
332 if ~class_idx.isKey(fromName),
continue; end
333 ri = class_idx(fromName);
334 toStruct = csm_data.(fromName);
335 toFields = fieldnames(toStruct);
336 for ti = 1:length(toFields)
337 toName = toFields{ti};
338 if ~class_idx.isKey(toName),
continue; end
339 ci = class_idx(toName);
340 mat(ri, ci) = toStruct.(toName);
343 node.server = node.server.updateClassSwitch(mat);
344 % Legacy 2D array format: csMatrix (from older JAR saves)
345 elseif isfield(nd,
'csMatrix') && isa(node,
'ClassSwitch')
350 node.server = node.server.updateClassSwitch(mat);
353 % DPS scheduling parameters (weights already passed via setService above)
355 % Departure discipline of a queueing Place's depository. Read after
the
356 % service loop above, which
is what creates
the departureDiscipline slots.
357 if isfield(nd, 'departureDiscipline') && isa(node, 'Place')
358 ddData = nd.departureDiscipline;
359 ddFields = fieldnames(ddData);
360 for ddi = 1:length(ddFields)
361 cname = ddFields{ddi};
362 if class_map.isKey(cname)
363 node.setDepartureDiscipline(class_map(cname), ...
364 str_to_depdisc(ddData.(cname)));
369 % Per-
class buffer capacity.
Station, not Queue: a queueing Place has a
370 % per-class capacity too and Place does not extend Queue.
371 if isfield(nd, 'classCap') && isa(node, 'Station')
372 ccData = nd.classCap;
373 ccFields = fieldnames(ccData);
374 for cci = 1:length(ccFields)
375 cname = ccFields{cci};
376 if class_map.isKey(cname)
377 jc = class_map(cname);
379 cls = model.getClasses();
380 for ci = 1:length(cls)
381 if strcmp(cls{ci}.name, cname)
382 node.classCap(ci) = ccData.(cname);
391 if isfield(nd,
'dropRule') && isa(node,
'Station')
392 drData = nd.dropRule;
393 drFields = fieldnames(drData);
394 for dri = 1:length(drFields)
395 cname = drFields{dri};
396 if class_map.isKey(cname)
397 cls = model.getClasses();
398 for ci = 1:length(cls)
399 if strcmp(cls{ci}.name, cname)
400 node.dropRule(ci) = str_to_droprule(drData.(cname));
408 % Order-independent / pass-and-swap (OI/PAS) service. The writer emits
the
409 % total rate function mu(c) as a macrostate table keyed by
the per-
class
410 % counts, plus
the per-
class cutoffs beyond which mu saturates and, for
411 % PAS,
the swap graph. Only
the JAR read these back; without
this branch a
412 % PAS/OI queue reloaded with no service rate function at all.
413 if isfield(nd,
'oiServiceRate') && isa(node,
'Queue') && ...
414 (node.schedStrategy == SchedStrategy.PAS || node.schedStrategy == SchedStrategy.OI)
415 % OI queues keep a fixed zero swap graph (setSwapGraph rejects them).
416 if isfield(nd,
'swapGraph') && node.schedStrategy == SchedStrategy.PAS
417 node.setSwapGraph(json2mat(nd.swapGraph));
419 if isfield(nd,
'oiCutoffs')
420 oicut = round(
double(oi_cutoffs_vec(nd.oiCutoffs)));
424 node.setServiceRateFunction(oi_table_to_handle(nd.oiServiceRate, oicut));
427 % Load-dependent scaling
428 if isfield(nd, 'loadDependence') && isa(node, 'Queue')
429 ld = nd.loadDependence;
430 if isfield(ld, 'type') && strcmp(ld.type, 'loadDependent') && isfield(ld, 'scaling')
431 scaling = ld.scaling(:)';
432 node.setLoadDependence(scaling);
436 % Class-dependent scaling
beta_{i,r}(n): rebuild
the handle from
the
437 % materialized lattice table (see cd_scaling_table in linemodel_save).
438 if isfield(nd,
'classDependence') && isa(node,
'Station')
439 cdep = nd.classDependence;
440 if isfield(cdep,
'type') && strcmp(cdep.type,
'classDependent') ...
441 && isfield(cdep,
'scaling')
442 if isfield(cdep, 'cutoffs')
443 cdcut = round(
double(cdep.cutoffs(:)'));
447 cdHandle = cd_table_to_handle(cdep.scaling, cdcut);
448 if isfield(cdep, 'peak') && ~isempty(cdep.peak)
450 cdPeak =
double(cell2mat(cdep.peak));
452 cdPeak =
double(cdep.peak(:)');
455 % Legacy JSON without an explicit peak: derive it from
the
456 % handle over
the population lattice (cutoffs), matching
the
457 % user-facing Util = T*S/peak normalization.
458 cdPeak = cd_peak_scaling(cdHandle, cdcut, numel(cdcut));
460 node.setLimitedClassDependence(cdHandle, cdPeak);
464 % Join strategy and quorum
466 if isfield(nd, 'joinStrategy')
467 jsStr = nd.joinStrategy;
468 classes = model.getClasses();
469 for ci = 1:length(classes)
472 node.input.setStrategy(classes{ci}, JoinStrategy.STD);
473 case {
'PARTIAL',
'QUORUM',
'Quorum'}
474 node.input.setStrategy(classes{ci}, JoinStrategy.PARTIAL);
478 if isfield(nd,
'joinQuorum')
480 classes = model.getClasses();
481 for ci = 1:length(classes)
482 node.input.setRequired(classes{ci}, jq);
487 % Cache hit/miss
class mappings and popularity distributions.
488 % Accept both
the nested MATLAB schema (nd.cache.*) and
the flat
489 % canonical JAR/Python schema (nd.hitClass, nd.missClass, ...).
490 if isa(node, 'Cache')
491 if isfield(nd, 'cache')
495 if isfield(nd, 'hitClass'), cc.hitClass = nd.hitClass; end
496 if isfield(nd, 'missClass'), cc.missClass = nd.missClass; end
497 if isfield(nd, 'popularity'), cc.popularity = nd.popularity; end
498 if isfield(nd, 'accessGraph'), cc.accessGraph = nd.accessGraph; end
499 if isfield(nd, 'accessProb'), cc.accessProb = nd.accessProb; end
500 if isfield(nd, 'initialState'), cc.initialState = nd.initialState; end
502 % Keys
the writer emits both nested and flat: prefer
the nested copy,
503 % fall back to
the flat one, so that a file written by any of
the
504 % three bridges loads
the same way.
505 if ~isfield(cc, 'admissionProb') && isfield(nd, 'admissionProb')
506 cc.admissionProb = nd.admissionProb;
508 if ~isfield(cc, 'retrievalSystem') && isfield(nd, 'retrievalSystem')
509 cc.retrievalSystem = nd.retrievalSystem;
511 % q-LRU admission probability on a miss
512 if isfield(cc, 'admissionProb')
513 node.setAdmissionProb(cc.admissionProb);
515 % Retrieval system (delayed-hit cache): restore
the cache-internal
516 % bookkeeping only. The retrieval classes themselves, their routing
517 % and
the queue service are ordinary classes/routing/service entries
518 % elsewhere in
the file and have already been rebuilt; calling
519 % setRetrievalSystem here would instead synthesize a second set.
520 % Mirrors
the Python reader.
521 if isfield(cc, 'retrievalSystem') && ~isempty(cc.retrievalSystem)
522 rs = cc.retrievalSystem;
523 if isfield(rs, 'capacity')
524 node.retrievalSystemCapacity =
double(rs.capacity);
526 if isfield(rs, 'byClass')
527 bcNames = fieldnames(rs.byClass);
528 for bci = 1:length(bcNames)
529 inName = bcNames{bci};
530 if ~class_map.isKey(inName),
continue; end
531 jobin = class_map(inName);
532 entry = rs.byClass.(inName);
533 if isfield(entry,
'queues')
534 qNames = cellify_string_array(entry.queues);
536 for qi = 1:numel(qNames)
537 if node_map.isKey(qNames{qi})
538 qIdx(end+1) = node_map(qNames{qi}).index; %#ok<AGROW>
541 node.retrievalSystemQueueIndices(int32(jobin.index - 1)) = qIdx;
543 if isfield(entry,
'items')
544 % jsondecode prefixes
the 0-based item keys with
'x'
545 itNames = fieldnames(entry.items);
546 for iti = 1:length(itNames)
547 rcName = entry.items.(itNames{iti});
548 if ~class_map.isKey(rcName),
continue; end
549 item0 = str2double(strrep(itNames{iti},
'x',
''));
550 rcIdx = class_map(rcName).index;
551 node.setRetrievalClass(jobin, class_map(rcName), item0 + 1);
552 if ~any(node.retrievalClassIndices == rcIdx)
553 node.retrievalClassIndices(end+1) = rcIdx;
561 if isfield(cc, 'hitClass')
562 hcData = cc.hitClass;
563 hcFields = fieldnames(hcData);
564 for hci = 1:length(hcFields)
565 inName = hcFields{hci};
566 outName = hcData.(inName);
567 if class_map.isKey(inName) && class_map.isKey(outName)
568 node.setHitClass(class_map(inName), class_map(outName));
573 if isfield(cc, 'missClass')
574 mcData = cc.missClass;
575 mcFields = fieldnames(mcData);
576 for mci = 1:length(mcFields)
577 inName = mcFields{mci};
578 outName = mcData.(inName);
579 if class_map.isKey(inName) && class_map.isKey(outName)
580 node.setMissClass(class_map(inName), class_map(outName));
584 % Popularity distributions (setRead)
585 if isfield(cc,
'popularity')
586 popData = cc.popularity;
587 popFields = fieldnames(popData);
588 for pfi = 1:length(popFields)
589 cname = popFields{pfi};
590 if class_map.isKey(cname)
591 popDist = json2dist(popData.(cname));
592 if ~isempty(popDist) && ~isa(popDist,
'Disabled')
593 node.setRead(class_map(cname), popDist);
598 % Access-cost (list-move) structure: per-item graph shared by all
599 % classes, or full per-class accessProb matrices
600 if isfield(cc, 'accessGraph')
601 node.graph = json2matcell(cc.accessGraph);
602 elseif isfield(cc, 'accessProb')
603 apData = cc.accessProb;
605 % jsondecode collapsed
the uniform [class][item][r][c]
606 % nesting into a 4D numeric array; re-split per class
607 apData = arrayfun(@(k1) squeeze(apData(k1, :, :, :)), ...
608 1:size(apData, 1), 'UniformOutput', false);
611 rows = cellfun(@json2matcell, apData, 'UniformOutput', false);
612 Nap = max(cellfun(@numel, rows));
615 R(k1, 1:numel(rows{k1})) = rows{k1};
617 node.setAccessProb(R);
619 % Initial cache state [
class counts | contents | retrieval bitmap]
620 if isfield(cc, 'initialState')
621 node.setState(cc.initialState(:)');
624 % Heterogeneous server types
625 if isa(node, 'Queue') && isfield(nd, 'serverTypes')
626 stArr = nd.serverTypes;
627 if isstruct(stArr), stArr = num2cell(stArr); end
628 for si = 1:length(stArr)
630 stName = stData.name;
631 stCount = stData.count;
632 st = ServerType(stName, stCount);
634 if isfield(stData,
'compatibleClasses')
635 ccList = stData.compatibleClasses;
636 if ~iscell(ccList), ccList = {ccList}; end
637 for cci = 1:length(ccList)
638 if class_map.isKey(ccList{cci})
639 st.addCompatible(class_map(ccList{cci}));
643 node.addServerType(st);
644 % Per-
class service distributions
645 if isfield(stData, 'service')
646 svcData = stData.service;
647 svcFields = fieldnames(svcData);
648 for fi = 1:length(svcFields)
649 cname = svcFields{fi};
650 if class_map.isKey(cname)
651 jc = class_map(cname);
652 dist = json2dist(svcData.(cname));
654 node.setHeteroService(jc, st, dist);
661 if isfield(nd,
'heteroSchedPolicy')
662 policy = HeteroSchedPolicy.fromText(nd.heteroSchedPolicy);
663 node.setHeteroSchedPolicy(policy);
669% --- Restore Balking, Retrial, Patience ---
670if isfield(data, '
nodes')
672 if isstruct(ndsImp), ndsImp = num2cell(ndsImp); end
673 for i = 1:length(ndsImp)
675 if ~node_map.isKey(ndImp.name),
continue; end
676 node = node_map(ndImp.name);
677 % Initial-state prior. Assigned directly rather than through
678 % setStatePrior, whose length check
is against node.space:
the state
679 % space has not been generated at load time (it
is built by
the solver
's
680 % init), so the check could not pass and the prior would be rejected.
681 if isfield(ndImp, 'statePrior
') && isa(node, 'StatefulNode
')
682 node.statePrior = double(ndImp.statePrior(:));
684 if ~isa(node, 'Queue
'), continue; end
685 % Immediate feedback on self-loops, per class
686 if isfield(ndImp, 'immediateFeedback
') && ~isempty(ndImp.immediateFeedback)
687 ifData = ndImp.immediateFeedback;
688 ifNames = fieldnames(ifData);
689 for fi = 1:length(ifNames)
691 if class_map.isKey(cname) && ifData.(cname)
692 node.setImmediateFeedback(class_map(cname));
696 % Orbit impatience (abandonment from the retrial orbit), per class
697 if isfield(ndImp, 'orbitImpatience
') && ~isempty(ndImp.orbitImpatience)
698 orbData = ndImp.orbitImpatience;
699 orbNames = fieldnames(orbData);
700 for fi = 1:length(orbNames)
701 cname = orbNames{fi};
702 if ~class_map.isKey(cname), continue; end
703 orbDist = json2dist(orbData.(cname));
704 if ~isempty(orbDist) && ~isa(orbDist, 'Disabled
')
705 node.setOrbitImpatience(class_map(cname), orbDist);
709 % Batch rejection probability (retrial queues), per class
710 if isfield(ndImp, 'batchRejectProb
') && ~isempty(ndImp.batchRejectProb)
711 brpData = ndImp.batchRejectProb;
712 brpNames = fieldnames(brpData);
713 for fi = 1:length(brpNames)
714 cname = brpNames{fi};
715 if ~class_map.isKey(cname), continue; end
716 node.setBatchRejectProbability(class_map(cname), brpData.(cname));
720 if isfield(ndImp, 'balking
') && ~isempty(ndImp.balking)
721 balkData = ndImp.balking;
722 fnames = fieldnames(balkData);
723 for fi = 1:length(fnames)
724 className = fnames{fi};
725 if ~class_map.isKey(className), continue; end
726 jc = class_map(className);
727 bjc = balkData.(className);
730 case 'QUEUE_LENGTH
', strategy = BalkingStrategy.QUEUE_LENGTH;
731 case 'EXPECTED_WAIT
', strategy = BalkingStrategy.EXPECTED_WAIT;
732 case 'COMBINED
', strategy = BalkingStrategy.COMBINED;
736 thData = bjc.thresholds;
737 if isstruct(thData), thData = num2cell(thData); end
739 for ti = 1:length(thData)
741 maxJobs = td.maxJobs;
742 if maxJobs < 0, maxJobs = Inf; end
743 thresholds{end+1} = {td.minJobs, maxJobs, td.probability};
745 node.setBalking(jc, strategy, thresholds);
749 if isfield(ndImp, 'retrial
') && ~isempty(ndImp.retrial)
750 retData = ndImp.retrial;
751 fnames = fieldnames(retData);
752 for fi = 1:length(fnames)
753 className = fnames{fi};
754 if ~class_map.isKey(className), continue; end
755 jc = class_map(className);
756 rjc = retData.(className);
757 delayDist = json2dist(rjc.delay);
759 if isfield(rjc, 'maxAttempts
')
760 maxAttempts = rjc.maxAttempts;
762 node.setRetrial(jc, delayDist, maxAttempts);
766 if isfield(ndImp, 'patience
') && ~isempty(ndImp.patience)
767 patData = ndImp.patience;
768 fnames = fieldnames(patData);
769 for fi = 1:length(fnames)
770 className = fnames{fi};
771 if ~class_map.isKey(className), continue; end
772 jc = class_map(className);
773 pjc = patData.(className);
774 patDist = json2dist(pjc.distribution);
775 if isfield(pjc, 'impatienceType
')
776 impType = str_to_impatience(pjc.impatienceType);
778 impType = ImpatienceType.RENEGING;
780 node.setPatience(jc, impType, patDist);
786% --- Configure Transition modes ---
787if isfield(data, 'nodes')
790 nds3 = num2cell(nds3);
792 for i = 1:length(nds3)
794 if ~isfield(nd3, 'modes
'), continue; end
795 if ~strcmp(nd3.type, 'Transition
'), continue; end
796 tnode = node_map(nd3.name);
797 modesData = nd3.modes;
798 if isstruct(modesData)
799 modesData = num2cell(modesData);
801 for mi = 1:length(modesData)
804 if isfield(md, 'name
'), modeName = md.name; end
805 mode = tnode.addMode(modeName);
807 if isfield(md, 'distribution
') && ~isempty(md.distribution)
808 dist = json2dist(md.distribution);
810 tnode.setDistribution(mode, dist);
814 if isfield(md, 'timingStrategy
')
815 if strcmp(md.timingStrategy, 'IMMEDIATE
')
816 tnode.setTimingStrategy(mode, TimingStrategy.IMMEDIATE);
818 tnode.setTimingStrategy(mode, TimingStrategy.TIMED);
822 if isfield(md, 'numServers
')
823 nsVal = md.numServers;
824 if ischar(nsVal) || isstring(nsVal)
825 if strcmpi(nsVal, 'Infinity
'), nsVal = Inf; else, nsVal = str2double(nsVal); end
828 tnode.setNumberOfServers(mode, nsVal);
832 if isfield(md, 'firingPriority
')
833 tnode.setFiringPriorities(mode, md.firingPriority);
836 if isfield(md, 'firingWeight
')
837 tnode.setFiringWeights(mode, md.firingWeight);
839 % Enabling conditions
840 if isfield(md, 'enablingConditions
')
841 ecList = md.enablingConditions;
842 if isstruct(ecList), ecList = num2cell(ecList); end
843 for ei = 1:length(ecList)
845 if node_map.isKey(ec.node) && class_map.isKey(ec.class)
846 tnode.setEnablingConditions(mode, class_map(ec.class), node_map(ec.node), ec.count);
850 % Inhibiting conditions
851 if isfield(md, 'inhibitingConditions
')
852 icList = md.inhibitingConditions;
853 if isstruct(icList), icList = num2cell(icList); end
854 for ii = 1:length(icList)
856 if node_map.isKey(ic.node) && class_map.isKey(ic.class)
857 tnode.setInhibitingConditions(mode, class_map(ic.class), node_map(ic.node), ic.count);
862 if isfield(md, 'firingOutcomes
')
863 foList = md.firingOutcomes;
864 if isstruct(foList), foList = num2cell(foList); end
865 for fi = 1:length(foList)
867 if node_map.isKey(fo.node) && class_map.isKey(fo.class)
868 tnode.setFiringOutcome(mode, class_map(fo.class), node_map(fo.node), fo.count);
876% --- Restore initial state for Place nodes ---
877if isfield(data, 'nodes')
879 if isstruct(nds4), nds4 = num2cell(nds4); end
880 for i = 1:length(nds4)
882 if isfield(nd4, 'initialState
') && node_map.isKey(nd4.name)
883 nodeObj = node_map(nd4.name);
884 if isa(nodeObj, 'Place
')
885 stVal = nd4.initialState;
886 stVal = stVal(:)'; % Ensure row vector (jsondecode returns
column vectors)
887 nodeObj.setState(stVal);
893% --- Build routing ---
894if isfield(data,
'routing') && isfield(data.routing,
'type') && strcmp(data.routing.type,
'matrix')
895 P = model.initRoutingMatrix();
896 K = length(classesList);
897 M = length(nodeList);
899 % Build node/class index maps
900 nodeIdx = containers.Map();
902 nodeIdx(nodeList{i}.name) = i;
904 classIdx = containers.Map();
906 classIdx(classesList{i}.name) = i;
909 % Parse routing keys from raw JSON to preserve commas
910 routingEntries = parse_routing_keys(rawJson, class_map, node_map);
912 for e = 1:length(routingEntries)
913 re = routingEntries{e};
914 r = classIdx(re.className1);
915 s = classIdx(re.className2);
916 ii = nodeIdx(re.fromNode);
917 jj = nodeIdx(re.toNode);
918 P{r,s}(ii, jj) = re.prob;
924% --- Restore routing strategies ---
925if isfield(data,
'routingStrategies')
926 stratMap = containers.Map();
927 stratMap('RAND') = RoutingStrategy.RAND;
928 stratMap('RROBIN') = RoutingStrategy.RROBIN;
929 stratMap('WRROBIN') = RoutingStrategy.WRROBIN;
930 stratMap('JSQ') = RoutingStrategy.JSQ;
931 stratMap('KCHOICES') = RoutingStrategy.KCHOICES;
932 stratMap('FIRING') = RoutingStrategy.FIRING;
933 stratMap('RL') = RoutingStrategy.RL;
934 stratMap('DISABLED') = RoutingStrategy.DISABLED;
936 rsFields = fieldnames(data.routingStrategies);
937 for fi = 1:length(rsFields)
938 nodeName = rsFields{fi};
939 if node_map.isKey(nodeName)
940 nodeObj = node_map(nodeName);
941 classStrats = data.routingStrategies.(nodeName);
942 csFields = fieldnames(classStrats);
943 for ci = 1:length(csFields)
944 className = csFields{ci};
945 stratName = classStrats.(className);
946 if class_map.isKey(className) && stratMap.isKey(stratName)
947 % Skip RAND, PROB: already handled by routing matrix
948 % Skip WRROBIN: handled separately in routingWeights section
949 rs = stratMap(stratName);
950 if rs ~= RoutingStrategy.RAND && rs ~= RoutingStrategy.PROB && rs ~= RoutingStrategy.WRROBIN
951 nodeObj.setRouting(class_map(className), rs);
959% --- Restore routing weights (WRROBIN) ---
960if isfield(data,
'routingWeights')
961 rwFields = fieldnames(data.routingWeights);
962 for fi = 1:length(rwFields)
963 nodeName = rwFields{fi};
964 if node_map.isKey(nodeName)
965 nodeObj = node_map(nodeName);
966 classWeights = data.routingWeights.(nodeName);
967 cwFields = fieldnames(classWeights);
968 for ci = 1:length(cwFields)
969 className = cwFields{ci};
970 destWeights = classWeights.(className);
971 if class_map.isKey(className)
972 % Clear existing routing entries
for this class
973 % (link() may have set PROB entries that would accumulate)
974 classIdx = class_map(className).index;
977 nodeObj.output.outputStrategy{1, classIdx}{3} = {};
979 dwFields = fieldnames(destWeights);
980 for di = 1:length(dwFields)
981 destName = dwFields{di};
982 weight = destWeights.(destName);
983 if node_map.isKey(destName)
984 nodeObj.setRouting(class_map(className), RoutingStrategy.WRROBIN, node_map(destName), weight);
993% --- Restore setup / delay-off, polling type and switchover times ---
994% jsondecode yields a cell array whenever
the node objects carry different
995% field sets, so
the struct-array form
is normalized to cells here.
997if isstruct(ndsSo), ndsSo = num2cell(ndsSo); end
998for ni = 1:length(ndsSo)
1000 if ~node_map.isKey(nd.name)
1004 % Setup / delay-off. The writer emits
the two maps together, keyed by
1005 %
class name, because setDelayOff requires both distributions.
1006 if isfield(nd, 'setupTime') && ~isempty(nd.setupTime) && ...
1007 isfield(nd,
'delayOffTime') && ~isempty(nd.delayOffTime)
1008 nodeObj = node_map(nd.name);
1009 suNames = fieldnames(nd.setupTime);
1010 for fi = 1:length(suNames)
1011 cname = suNames{fi};
1012 if ~class_map.isKey(cname) || ~isfield(nd.delayOffTime, cname)
1015 suDist = json2dist(nd.setupTime.(cname));
1016 doffDist = json2dist(nd.delayOffTime.(cname));
1017 if ~isempty(suDist) && ~isempty(doffDist)
1018 nodeObj.setDelayOff(class_map(cname), suDist, doffDist);
1023 % Polling type, restored by name. This must precede
the switchover
1024 % restore below: setPollingType resets
the switchover of every
class to
1026 if isfield(nd,
'pollingType') && ~isempty(nd.pollingType)
1027 nodeObj = node_map(nd.name);
1028 ptId = PollingType.fromName(nd.pollingType);
1029 if ptId == PollingType.KLIMITED
1030 if isfield(nd, 'pollingPar') && ~isempty(nd.pollingPar)
1031 nodeObj.setPollingType(ptId, nd.pollingPar);
1033 nodeObj.setPollingType(ptId, 1);
1036 nodeObj.setPollingType(ptId);
1040 % Switchover times: entries without a "to" field carry
the per-class
1041 % polling form, entries with one
the (from,to) pair form.
1042 if isfield(nd, 'switchoverTimes') && ~isempty(nd.switchoverTimes)
1043 nodeObj = node_map(nd.name);
1044 soArr = nd.switchoverTimes;
1046 soArr = num2cell(soArr);
1048 for si = 1:length(soArr)
1050 if ~class_map.isKey(so.from)
1053 fromCls = class_map(so.from);
1054 dist = json2dist(so.distribution);
1058 if isfield(so,
'to') && ~isempty(so.to)
1059 if ~class_map.isKey(so.to)
1062 nodeObj.setSwitchover(fromCls, class_map(so.to), dist);
1064 nodeObj.setSwitchover(fromCls, dist);
1070% --- Restore finite capacity regions ---
1071if isfield(data, 'finiteCapacityRegions')
1072 fcrArr = data.finiteCapacityRegions;
1076 classes = model.getClasses();
1077 for ri = 1:length(fcrArr)
1080 % Support both old format (
"nodes" list) and
new format (
"stations" array).
1081 % jsondecode returns a JSON array of like-shaped objects as a STRUCT ARRAY
1082 % (not a cell), so a multi-station region must be iterated with
struct
1083 % indexing stArr(si). Wrapping it in a cell (
the scalar
case) would collapse
1084 % every member but
the first (sj.node on a
struct array yields only
the
1085 % first element), silently dropping all but one station from
the region.
1086 if isfield(rj,
'stations')
1087 stArr = rj.stations;
1089 for si = 1:length(stArr)
1090 nodeName = stArr{si}.node;
1091 if node_map.isKey(nodeName)
1092 regNodes{end+1} = node_map(nodeName); %#ok<AGROW>
1096 for si = 1:length(stArr)
1097 nodeName = stArr(si).node;
1098 if node_map.isKey(nodeName)
1099 regNodes{end+1} = node_map(nodeName); %#ok<AGROW>
1103 elseif isfield(rj,
'nodes')
1104 nodeNames = rj.
nodes;
1105 if ~iscell(nodeNames), nodeNames = {nodeNames}; end
1106 for ni = 1:length(nodeNames)
1107 if node_map.isKey(nodeNames{ni})
1108 regNodes{end+1} = node_map(nodeNames{ni}); %#ok<AGROW>
1112 maxJobs = FiniteCapacityRegion.UNBOUNDED;
1113 if isfield(rj,
'globalMaxJobs')
1114 maxJobs = rj.globalMaxJobs;
1116 if ~isempty(regNodes)
1118 region = model.addRegion(regNodes);
1119 if isfield(rj, 'name') && ~isempty(rj.name)
1120 region.setName(rj.name);
1122 if maxJobs ~= FiniteCapacityRegion.UNBOUNDED
1123 region.setGlobalMaxJobs(maxJobs);
1126 if isfield(rj, 'globalMaxMemory')
1127 region.globalMaxMemory = rj.globalMaxMemory;
1130 if isfield(rj, 'classMaxJobs')
1131 cmj = rj.classMaxJobs;
1132 cmjFields = fieldnames(cmj);
1133 for ci = 1:length(cmjFields)
1134 cname = cmjFields{ci};
1135 if class_map.isKey(cname)
1136 jc = class_map(cname);
1137 region.classMaxJobs(jc.index) = cmj.(cname);
1142 if isfield(rj,
'dropRule')
1143 drData = rj.dropRule;
1144 drFields = fieldnames(drData);
1145 for di = 1:length(drFields)
1146 cname = drFields{di};
1147 if class_map.isKey(cname)
1148 jc = class_map(cname);
1149 region.dropRule(jc.index) = str_to_droprule(drData.(cname));
1153 % Per-station classWeight and classSize from stations array
1154 if isfield(rj,
'stations')
1155 stArr2 = rj.stations;
1156 if ~iscell(stArr2), stArr2 = {stArr2}; end
1157 for si = 1:length(stArr2)
1159 if isfield(sj2,
'classWeight')
1160 cwData = sj2.classWeight;
1161 cwFields = fieldnames(cwData);
1162 for ci = 1:length(cwFields)
1163 cname = cwFields{ci};
1164 if class_map.isKey(cname)
1165 jc = class_map(cname);
1166 region.classWeight(jc.index) = cwData.(cname);
1170 if isfield(sj2,
'classSize')
1171 csData = sj2.classSize;
1172 csFields = fieldnames(csData);
1173 for ci = 1:length(csFields)
1174 cname = csFields{ci};
1175 if class_map.isKey(cname)
1176 jc = class_map(cname);
1177 region.classSize(jc.index) = csData.(cname);
1183 % Linear constraints A * x <= b
1184 if isfield(rj,
'constraintA') && isfield(rj,
'constraintB')
1185 Adata = rj.constraintA;
1186 bdata = rj.constraintB;
1188 nrows = length(Adata);
1189 K = length(region.classes);
1190 A = zeros(nrows, K);
1193 A(ar, 1:length(row)) = row(:)
';
1198 region.setConstraint(A, bdata(:));
1207if isfield(data, 'rewards
')
1208 rewardsArr = data.rewards;
1209 if isstruct(rewardsArr), rewardsArr = num2cell(rewardsArr); end
1210 for i = 1:length(rewardsArr)
1212 if ~isfield(rw, 'name
') || ~isfield(rw, 'type
')
1213 line_warning(mfilename, 'Ignoring a reward entry without a
"name" or
"type" field.
');
1218 if ~isfield(rw, 'node
') || isempty(rw.node) || ~node_map.isKey(rw.node)
1219 line_warning(mfilename, sprintf(['Reward
"%s" refers to node
"%s", which
is not defined in this
' ...
1220 'model;
the reward
is ignored.
'], rname, char(getfield_default(rw, 'node
', ''))));
1223 rnode = node_map(rw.node);
1225 if isfield(rw, 'class') && ~isempty(rw.class)
1226 if ~class_map.isKey(rw.class)
1227 line_warning(mfilename, sprintf(['Reward
"%s" refers to
class "%s
", which is not defined in ' ...
1228 'this model; the reward is ignored.'], rname, rw.class));
1231 rclass = class_map(rw.class);
1236 model.setReward(rname, Reward.queueLength(rnode));
1238 model.setReward(rname, Reward.queueLength(rnode, rclass));
1242 model.setReward(rname, Reward.utilization(rnode));
1244 model.setReward(rname, Reward.utilization(rnode, rclass));
1247 model.setReward(rname, Reward.blocking(rnode));
1249 line_warning(mfilename, sprintf(['Reward "%s
" has type "%s
", for which no reward template is ' ...
1250 'implemented; the reward is ignored.'], rname, rtype));
1257function v = getfield_default(s, fieldName, defaultValue)
1258% Return s.(fieldName) when present, otherwise defaultValue.
1259if isfield(s, fieldName)
1267function node = create_node(model, nd, name, ntype)
1268% Create a node from JSON data.
1271 node = Source(model, name);
1273 node = Sink(model, name);
1275 node = Delay(model, name);
1278 if isfield(nd, 'scheduling')
1279 schedStr = nd.scheduling;
1281 schedId = str_to_sched_id(schedStr);
1282 node = Queue(model, name, schedId);
1283 if isfield(nd, 'servers')
1284 ns = servers_from_json(nd.servers);
1285 if isinf(ns) || ns > 1
1286 node.setNumberOfServers(ns);
1289 if isfield(nd, 'buffer') && isfinite(nd.buffer)
1290 node.cap = nd.buffer;
1293 node = Fork(model, name);
1295 node = Join(model, name);
1297 node = Router(model, name);
1299 node = ClassSwitch(model, name);
1301 % Accept both the nested MATLAB schema (nd.cache.items/capacity/
1302 % replacement) and the flat canonical JAR/Python schema
1303 % (nd.numItems/itemLevelCap/replacementStrategy).
1305 if isfield(nd, 'cache')
1309 if isfield(cc, 'items'), nitems = cc.items;
1310 elseif isfield(nd, 'numItems'), nitems = nd.numItems; end
1312 if isfield(cc, 'capacity'), cap = cc.capacity;
1313 elseif isfield(nd, 'itemLevelCap'), cap = nd.itemLevelCap(:)'; end
1315 if isfield(cc, 'replacement'), replStr = cc.replacement;
1316 elseif isfield(nd, 'replacementStrategy'), replStr = nd.replacementStrategy; end
1317 replId = str_to_repl_id(replStr);
1318 node = Cache(model, name, nitems, cap, replId);
1320 % A queueing Place (QPN embedded queue) carries a scheduling strategy;
1321 % an ordinary Place has none. The strategy must be supplied to the
1322 % constructor, since installQueueServer (triggered by the first
1323 % setService) picks the server section from it.
1324 if isfield(nd, 'scheduling')
1325 node = Place(model, name, str_to_sched_id(nd.scheduling));
1327 node = Place(model, name);
1329 if isfield(nd, 'servers')
1330 node.numberOfServers = servers_from_json(nd.servers);
1332 if isfield(nd, 'buffer') && isfinite(nd.buffer)
1333 node.cap = nd.buffer;
1336 node = Transition(model, name);
1338 node = Queue(model, name, SchedStrategy.FCFS);
1343function ns = servers_from_json(v)
1344% Decode a server count. Inf crosses the wire as the string "Infinity
" (the form
1345% Place numServers already used); a numeric value is taken verbatim.
1346if ischar(v) || isstring(v)
1347 if strcmpi(v, 'Infinity')
1358% =========================================================================
1359% LayeredNetwork deserialization
1360% =========================================================================
1362function mult = mult_from_json(v)
1363% Decode a JSON multiplicity, mapping the infinite-multiplicity sentinel
1364% (Java's Integer.MAX_VALUE, as written by the JAR and by linemodel_save) back
1365% to Inf. Negative values are also treated as infinite: earlier versions of the
1366% Python writer emitted -1 for infinite-server hosts, so files in that format
1367% must keep loading rather than silently becoming single-server stations.
1370elseif v >= 2147483647 || v < 0
1377function model = json2layered(data)
1378% Reconstruct a LayeredNetwork from decoded JSON struct.
1381if isfield(data, 'name')
1382 modelName = data.name;
1384model = LayeredNetwork(modelName);
1386% --- Processors (Python schema: "processors
", JAR schema: "hosts
") ---
1387proc_map = containers.Map();
1388if isfield(data, 'processors')
1389 procs = data.processors;
1390elseif isfield(data, 'hosts')
1396 if isstruct(procs), procs = num2cell(procs); end
1397 for i = 1:length(procs)
1401 if isfield(pd, 'multiplicity'), mult = mult_from_json(pd.multiplicity); end
1403 if isfield(pd, 'scheduling'), schedStr = pd.scheduling; end
1404 schedId = str_to_sched_id(schedStr);
1406 if isfield(pd, 'quantum'), quantum = pd.quantum; end
1408 if isfield(pd, 'speedFactor'), sf = pd.speedFactor; end
1409 proc = Host(model, pname, mult, schedId, quantum, sf);
1410 if isfield(pd, 'replication') && pd.replication > 1
1411 proc.setReplication(pd.replication);
1413 proc_map(pname) = proc;
1418task_map = containers.Map();
1419if isfield(data, 'tasks')
1421 if isstruct(tsks), tsks = num2cell(tsks); end
1422 for i = 1:length(tsks)
1426 if isfield(td, 'multiplicity'), mult = mult_from_json(td.multiplicity); end
1428 if isfield(td, 'scheduling'), schedStr = td.scheduling; end
1429 schedId = str_to_sched_id(schedStr);
1431 if isfield(td, 'taskType'), taskType = td.taskType; end
1432 if strcmp(taskType, 'FunctionTask')
1433 task = FunctionTask(model, tname, mult, schedId);
1434 elseif strcmp(taskType, 'CacheTask')
1436 if isfield(td, 'totalItems'), totalItems = td.totalItems; end
1438 if isfield(td, 'cacheCapacity'), cacheCap = td.cacheCapacity; end
1440 if isfield(td, 'replacementStrategy'), rsStr = td.replacementStrategy; end
1441 rsMap = containers.Map({'RR','FIFO','SFIFO','LRU'}, ...
1442 {ReplacementStrategy.RR, ReplacementStrategy.FIFO, ...
1443 ReplacementStrategy.SFIFO, ReplacementStrategy.LRU});
1444 if rsMap.isKey(upper(rsStr))
1445 rs = rsMap(upper(rsStr));
1447 rs = ReplacementStrategy.FIFO;
1449 task = CacheTask(model, tname, totalItems, cacheCap, rs, mult, schedId);
1451 task = Task(model, tname, mult, schedId);
1453 % Assign to processor (Python schema: "processor
", JAR schema: "host
")
1455 if isfield(td, 'processor'), procRef = td.processor;
1456 elseif isfield(td, 'host'), procRef = td.host;
1458 if ~isempty(procRef) && proc_map.isKey(procRef)
1459 task.on(proc_map(procRef));
1461 % Think time (Python schema: "thinkTime
" as dist, JAR schema: "thinkTimeMean
"/"thinkTimeSCV
")
1462 if isfield(td, 'thinkTime')
1463 dist = json2dist(td.thinkTime);
1465 task.setThinkTime(dist);
1467 elseif isfield(td, 'thinkTimeMean') && td.thinkTimeMean > 0
1468 task.setThinkTime(Exp(1.0 / td.thinkTimeMean));
1471 if isfield(td, 'setupTime')
1472 dist = json2dist(td.setupTime);
1474 task.setSetupTime(dist);
1476 elseif isfield(td, 'setupTimeMean') && td.setupTimeMean > 1e-8
1477 task.setSetupTime(Exp(1.0 / td.setupTimeMean));
1480 if isfield(td, 'delayOffTime')
1481 dist = json2dist(td.delayOffTime);
1483 task.setDelayOffTime(dist);
1485 elseif isfield(td, 'delayOffTimeMean') && td.delayOffTimeMean > 1e-8
1486 task.setDelayOffTime(Exp(1.0 / td.delayOffTimeMean));
1489 if isfield(td, 'fanIn') && isstruct(td.fanIn)
1490 fnames = fieldnames(td.fanIn);
1491 for fi = 1:length(fnames)
1492 task.setFanIn(fnames{fi}, td.fanIn.(fnames{fi}));
1496 if isfield(td, 'fanOut') && isstruct(td.fanOut)
1497 fnames = fieldnames(td.fanOut);
1498 for fi = 1:length(fnames)
1499 task.setFanOut(fnames{fi}, td.fanOut.(fnames{fi}));
1503 if isfield(td, 'replication') && td.replication > 1
1504 task.setReplication(td.replication);
1506 task_map(tname) = task;
1511entry_map = containers.Map();
1512if isfield(data, 'entries')
1513 ents = data.entries;
1514 if isstruct(ents), ents = num2cell(ents); end
1515 for i = 1:length(ents)
1518 entryType = 'Entry';
1519 if isfield(ed, 'entryType'), entryType = ed.entryType; end
1520 if strcmp(entryType, 'ItemEntry')
1522 if isfield(ed, 'totalItems'), totalItems = ed.totalItems; end
1524 if isfield(ed, 'accessProb')
1527 accessProb = json2dist(ap);
1528 elseif isnumeric(ap)
1529 accessProb = DiscreteSampler(ap);
1532 if isempty(accessProb)
1533 % Default uniform distribution
1534 accessProb = DiscreteSampler(ones(1, totalItems) / totalItems);
1536 entry = ItemEntry(model, ename, totalItems, accessProb);
1538 entry = Entry(model, ename);
1540 if isfield(ed, 'task') && task_map.isKey(ed.task)
1541 entry.on(task_map(ed.task));
1543 % Entry arrival distribution
1544 if isfield(ed, 'arrival')
1545 dist = json2dist(ed.arrival);
1547 entry.setArrival(dist);
1550 entry_map(ename) = entry;
1555act_map = containers.Map();
1556if isfield(data, 'activities')
1557 acts = data.activities;
1558 if isstruct(acts), acts = num2cell(acts); end
1559 for i = 1:length(acts)
1564 hd = GlobalConstants.FineTol;
1565 if isfield(ad, 'hostDemand')
1566 hdDist = json2dist(ad.hostDemand);
1572 % Bound to entry (Python schema: "boundTo
", JAR schema: "boundToEntry
")
1574 if isfield(ad, 'boundTo')
1576 elseif isfield(ad, 'boundToEntry')
1577 bte = ad.boundToEntry;
1580 act = Activity(model, aname, hd, bte);
1583 if isfield(ad, 'task') && task_map.isKey(ad.task)
1584 act.on(task_map(ad.task));
1588 if isfield(ad, 'repliesTo') && entry_map.isKey(ad.repliesTo)
1589 act.repliesTo(entry_map(ad.repliesTo));
1592 % Synch calls (Python schema: "entry
", JAR schema: "dest
")
1593 if isfield(ad, 'synchCalls')
1594 scs = ad.synchCalls;
1595 if isstruct(scs), scs = num2cell(scs); end
1596 for j = 1:length(scs)
1598 if isfield(sc, 'entry'), ename = sc.entry;
1599 elseif isfield(sc, 'dest'), ename = sc.dest;
1603 if isfield(sc, 'mean'), meanCalls = sc.mean; end
1604 if entry_map.isKey(ename)
1605 act.synchCall(entry_map(ename), meanCalls);
1610 % Asynch calls (Python schema: "entry
", JAR schema: "dest
")
1611 if isfield(ad, 'asynchCalls')
1612 acs = ad.asynchCalls;
1613 if isstruct(acs), acs = num2cell(acs); end
1614 for j = 1:length(acs)
1616 if isfield(ac, 'entry'), ename = ac.entry;
1617 elseif isfield(ac, 'dest'), ename = ac.dest;
1621 if isfield(ac, 'mean'), meanCalls = ac.mean; end
1622 if entry_map.isKey(ename)
1623 act.asynchCall(entry_map(ename), meanCalls);
1628 act_map(aname) = act;
1632% --- Precedences (Python schema: "type
"/"activities
", JAR schema: "preActs
"/"postActs
"/"preType
"/"postType
") ---
1633if isfield(data, 'precedences')
1634 precs = data.precedences;
1635 if isstruct(precs), precs = num2cell(precs); end
1636 for i = 1:length(precs)
1638 if ~isfield(pd, 'task') || ~task_map.isKey(pd.task)
1641 task = task_map(pd.task);
1643 if isfield(pd, 'preActs') || isfield(pd, 'postActs')
1647 if isfield(pd, 'preActs')
1648 preNames = pd.preActs;
1649 if ischar(preNames), preNames = {preNames}; end
1651 if isfield(pd, 'postActs')
1652 postNames = pd.postActs;
1653 if ischar(postNames), postNames = {postNames}; end
1657 if isfield(pd, 'preType'), preType = pd.preType; end
1658 if isfield(pd, 'postType'), postType = pd.postType; end
1660 % Normalize JAR naming convention to Python convention
1662 case 'post-AND', postType = 'and-fork';
1663 case 'post-OR', postType = 'or-fork';
1664 case 'post-LOOP', postType = 'loop';
1667 case 'pre-AND', preType = 'and-join';
1668 case 'pre-OR', preType = 'or-join';
1671 % Extract postParams (JAR schema: probabilities/loopCount)
1673 if isfield(pd, 'postParams')
1674 postParams = pd.postParams;
1675 if iscell(postParams), postParams = cell2mat(postParams); end
1679 for ai = 1:length(preNames)
1680 if act_map.isKey(preNames{ai})
1681 preActs{end+1} = act_map(preNames{ai}); %#ok<AGROW>
1685 for ai = 1:length(postNames)
1686 if act_map.isKey(postNames{ai})
1687 postActs{end+1} = act_map(postNames{ai}); %#ok<AGROW>
1691 if strcmp(preType, 'pre') && strcmp(postType, 'post')
1692 if length(preActs) == 1 && length(postActs) == 1
1693 ap = ActivityPrecedence.Serial(preActs{1}, postActs{1});
1694 task.addPrecedence(ap);
1696 elseif strcmp(preType, 'pre') && strcmp(postType, 'and-fork')
1697 if ~isempty(preActs) && ~isempty(postActs)
1698 ap = ActivityPrecedence.AndFork(preActs{1}, postActs);
1699 task.addPrecedence(ap);
1701 elseif strcmp(preType, 'and-join') && strcmp(postType, 'post')
1702 if ~isempty(preActs) && ~isempty(postActs)
1703 ap = ActivityPrecedence.AndJoin(preActs, postActs{1});
1704 task.addPrecedence(ap);
1706 elseif strcmp(preType, 'pre') && strcmp(postType, 'or-fork')
1707 if ~isempty(preActs) && ~isempty(postActs)
1709 if isfield(pd, 'probabilities')
1710 probs = pd.probabilities;
1711 if isstruct(probs), probs = cell2mat(struct2cell(probs)); end
1713 if isempty(probs) && ~isempty(postParams)
1714 probs = postParams(:)';
1717 n = length(postActs);
1718 probs = ones(1, n) / n;
1720 ap = ActivityPrecedence.OrFork(preActs{1}, postActs, probs);
1721 task.addPrecedence(ap);
1723 elseif strcmp(preType, 'or-join') && strcmp(postType, 'post')
1724 if ~isempty(preActs) && ~isempty(postActs)
1725 ap = ActivityPrecedence.OrJoin(preActs, postActs{1});
1726 task.addPrecedence(ap);
1728 elseif strcmp(preType, 'pre') && strcmp(postType, 'loop')
1730 if isfield(pd, 'loopCount'), count = pd.loopCount; end
1731 if count == 1.0 && ~isempty(postParams)
1732 count = postParams(1);
1734 if ~isempty(preActs) && ~isempty(postActs)
1735 if length(postActs) > 1
1736 ap = ActivityPrecedence.Loop(preActs{1}, postActs(1:end-1), postActs{end}, count);
1738 ap = ActivityPrecedence.Loop(preActs{1}, postActs, count);
1740 task.addPrecedence(ap);
1742 elseif strcmp(preType, 'pre') && strcmp(postType, 'post-CACHE')
1743 if ~isempty(preActs) && ~isempty(postActs)
1744 ap = ActivityPrecedence.CacheAccess(preActs{1}, postActs);
1745 task.addPrecedence(ap);
1751 actNames = pd.activities;
1752 if ischar(actNames), actNames = {actNames}; end
1754 % Resolve activity names to objects
1756 for ai = 1:length(actNames)
1758 if act_map.isKey(an)
1759 actObjs{end+1} = act_map(an); %#ok<AGROW>
1762 % A Loop carries its trigger separately in 'preActivity', so its body
1763 % may be a single activity; every other type names all its operands in
1764 % 'activities' and so needs at least two. Requiring two here dropped a
1765 % single-body Loop that linemodel_save had written out faithfully.
1766 isLoopWithPre = strcmp(ptype, 'Loop') && isfield(pd, 'preActivity') ...
1767 && act_map.isKey(pd.preActivity) && ~isempty(actObjs);
1768 if length(actObjs) < 2 && ~isLoopWithPre
1774 ap = ActivityPrecedence.Serial(actObjs{:});
1775 task.addPrecedence(ap);
1777 ap = ActivityPrecedence.AndFork(actObjs{1}, actObjs(2:end));
1778 task.addPrecedence(ap);
1780 ap = ActivityPrecedence.AndJoin(actObjs(1:end-1), actObjs{end});
1781 task.addPrecedence(ap);
1784 if isfield(pd, 'probabilities')
1785 probs = pd.probabilities;
1786 if isstruct(probs), probs = cell2mat(struct2cell(probs)); end
1789 n = length(actObjs) - 1;
1790 probs = ones(1, n) / n;
1792 ap = ActivityPrecedence.OrFork(actObjs{1}, actObjs(2:end), probs);
1793 task.addPrecedence(ap);
1795 ap = ActivityPrecedence.OrJoin(actObjs(1:end-1), actObjs{end});
1796 task.addPrecedence(ap);
1799 if isfield(pd, 'loopCount'), count = pd.loopCount; end
1800 % Check for explicit preActivity field (new format)
1801 if isfield(pd, 'preActivity') && act_map.isKey(pd.preActivity)
1802 preAct = act_map(pd.preActivity);
1803 ap = ActivityPrecedence.Loop(preAct, actObjs, count);
1804 elseif length(actObjs) >= 3
1805 % Legacy format: first is pre, rest is body+end
1806 ap = ActivityPrecedence.Loop(actObjs{1}, actObjs(2:end-1), actObjs{end}, count);
1808 ap = ActivityPrecedence.Loop(actObjs{1}, actObjs(2:end), count);
1810 task.addPrecedence(ap);
1812 if length(actObjs) >= 2
1813 ap = ActivityPrecedence.CacheAccess(actObjs{1}, actObjs(2:end));
1814 task.addPrecedence(ap);
1823% =========================================================================
1824% Workflow deserialization
1825% =========================================================================
1827function model = json2workflow(data)
1828% Reconstruct a Workflow from decoded JSON struct.
1830modelName = 'workflow';
1831if isfield(data, 'name')
1832 modelName = data.name;
1834model = Workflow(modelName);
1837if isfield(data, 'activities')
1838 acts = data.activities;
1839 if isstruct(acts), acts = num2cell(acts); end
1840 for i = 1:length(acts)
1843 if isfield(ad, 'hostDemand') && ~isempty(ad.hostDemand)
1844 dist = json2dist(ad.hostDemand);
1845 model.addActivity(actName, dist);
1847 model.addActivity(actName, 1.0);
1852% --- Precedences ---
1853if isfield(data, 'precedences')
1854 precs = data.precedences;
1855 if isstruct(precs), precs = num2cell(precs); end
1856 for i = 1:length(precs)
1860 if isfield(pd, 'preActs')
1861 preActs = cellify_string_array(pd.preActs);
1867 if isfield(pd, 'postActs')
1868 postActs = cellify_string_array(pd.postActs);
1873 % preType / postType - convert JAR strings to numeric IDs
1874 preType = ActivityPrecedenceType.PRE_SEQ;
1875 if isfield(pd, 'preType')
1876 preType = str_to_prectype(pd.preType);
1878 postType = ActivityPrecedenceType.POST_SEQ;
1879 if isfield(pd, 'postType')
1880 postType = str_to_prectype(pd.postType);
1883 % preParams / postParams
1885 if isfield(pd, 'preParams') && ~isempty(pd.preParams)
1886 preParams = pd.preParams(:)';
1889 if isfield(pd, 'postParams') && ~isempty(pd.postParams)
1890 postParams = pd.postParams(:)';
1893 ap = ActivityPrecedence(preActs, postActs, preType, postType, preParams, postParams);
1894 model.addPrecedence(ap);
1900% =========================================================================
1901% Environment deserialization
1902% =========================================================================
1904function model = json2environment(data, rawJson)
1905% Reconstruct an Environment from decoded JSON struct.
1908if isfield(data, 'name')
1909 modelName = data.name;
1912if isfield(data, 'numStages')
1913 numStages = data.numStages;
1915model = Environment(modelName, numStages);
1917% --- Node failures ---
1918% "nodeFailures
" is the declarative form of the addNodeBreakdown/addNodeRepair
1919% macros. It has two roles. When the DOWN_<node> stages are NOT declared (a
1920% hand-written model that lists only the base stage), it expands the base model
1921% into the UP and DOWN_<node> stages and their transitions. When they ARE declared
1922% (the form written by linemodel_save, which carries the stages losslessly), it
1923% only restores the queue-length reset policies, which are function handles and
1924% cannot be represented by the expanded form.
1926if isfield(data, 'nodeFailures')
1927 nfArr = data.nodeFailures;
1928 if isstruct(nfArr), nfArr = num2cell(nfArr); end
1932if isfield(data, 'stages')
1933 stages = data.stages;
1934 if isstruct(stages), stages = num2cell(stages); end
1937declaredNames = cell(1, length(stages));
1938for i = 1:length(stages)
1939 if isfield(stages{i}, 'name')
1940 declaredNames{i} = stages{i}.name;
1942 declaredNames{i} = sprintf('Stage%d', i);
1946% Expand the macro form only when no DOWN stage is declared for any entry.
1947macroMode = ~isempty(nfArr);
1948for k = 1:length(nfArr)
1949 if ~isfield(nfArr{k}, 'node')
1950 line_error(mfilename, 'A "nodeFailures
" entry is missing the required "node
" field.');
1952 if any(strcmp(declaredNames, sprintf('DOWN_%s', nfArr{k}.node)))
1959 if length(stages) ~= 1
1960 line_error(mfilename, ['"nodeFailures
" expands the base model into the UP and DOWN_<node> stages, ' ...
1961 'so "stages
" must declare exactly one stage, holding the base (UP) model.']);
1963 if isfield(data, 'transitions') && ~isempty(data.transitions)
1964 line_error(mfilename, ['"nodeFailures
" implies the breakdown and repair transitions; "transitions
" ' ...
1965 'must not be declared alongside it.']);
1967 if ~isfield(stages{1}, 'model') || isempty(stages{1}.model)
1968 line_error(mfilename, '"nodeFailures
" requires the base stage to carry a "model
".');
1970 baseModel = json2network(stages{1}.model, rawJson);
1971 for k = 1:length(nfArr)
1973 [breakdownDist, repairDist, downServiceDist, resetB, resetR] = nodefailure_fields(nf);
1974 if isempty(repairDist)
1975 model.addNodeBreakdown(baseModel, nf.node, breakdownDist, downServiceDist, resetB);
1977 model.addNodeFailureRepair(baseModel, nf.node, breakdownDist, repairDist, downServiceDist, ...
1984 for i = 1:length(stages)
1986 stageName = declaredNames{i};
1987 stageNames{end+1} = stageName; %#ok<AGROW>
1990 if isfield(sd, 'type')
1991 stageType = sd.type;
1995 if isfield(sd, 'model') && ~isempty(sd.model)
1996 stageModel = json2network(sd.model, rawJson);
1999 if ~isempty(stageModel)
2000 model.addStage(stageName, stageType, stageModel);
2004 % --- Transitions ---
2005 if isfield(data, 'transitions')
2006 trans = data.transitions;
2007 if isstruct(trans), trans = num2cell(trans); end
2008 for i = 1:length(trans)
2010 fromIdx = td.from + 1; % Convert from 0-indexed (JAR) to 1-indexed (MATLAB)
2011 toIdx = td.to + 1; % Convert from 0-indexed (JAR) to 1-indexed (MATLAB)
2012 if isfield(td, 'distribution') && ~isempty(td.distribution)
2013 dist = json2dist(td.distribution);
2014 if ~isempty(dist) && ~isa(dist, 'Disabled')
2015 % Use stage names for MATLAB Environment API
2016 if fromIdx <= length(stageNames) && toIdx <= length(stageNames)
2017 model.addTransition(stageNames{fromIdx}, stageNames{toIdx}, dist);
2024 % Re-attach the node-failure descriptors and their reset policies to the
2025 % stages just built, so that the environment serializes back identically.
2026 for k = 1:length(nfArr)
2028 [breakdownDist, repairDist, downServiceDist, resetB, resetR] = nodefailure_fields(nf);
2029 model.registerNodeFailure(nf.node, breakdownDist, repairDist, downServiceDist, resetB, resetR);
2037function [breakdownDist, repairDist, downServiceDist, resetB, resetR] = nodefailure_fields(nf)
2038% Decode the distributions and reset policies of one "nodeFailures
" entry.
2039% Note that breakdownRate/repairRate carry full distributions, not scalar rates.
2040if ~isfield(nf, 'breakdownRate') || isempty(nf.breakdownRate)
2041 line_error(mfilename, sprintf('Node failure on "%s
" is missing the required "breakdownRate
" field.', nf.node));
2043if ~isfield(nf, 'downService') || isempty(nf.downService)
2044 line_error(mfilename, sprintf('Node failure on "%s
" is missing the required "downService
" field.', nf.node));
2046breakdownDist = json2dist(nf.breakdownRate);
2047downServiceDist = json2dist(nf.downService);
2049if isfield(nf, 'repairRate') && ~isempty(nf.repairRate)
2050 repairDist = json2dist(nf.repairRate);
2053if isfield(nf, 'breakdownResetPolicy') && ~isempty(nf.breakdownResetPolicy)
2054 resetB = nf.breakdownResetPolicy;
2057if isfield(nf, 'repairResetPolicy') && ~isempty(nf.repairResetPolicy)
2058 resetR = nf.repairResetPolicy;
2063% =========================================================================
2064% Distribution deserialization
2065% =========================================================================
2067function dist = json2dist(d)
2068% Convert a JSON distribution struct to a MATLAB Distribution object.
2078 dist = Disabled.getInstance();
2081 dist = Immediate.getInstance();
2084 % Nested object (the density is a Sirio expression string); "Inf
" carries
2085 % an unbounded latest firing time.
2086 ep = d.expolynomial;
2087 if ischar(ep.lft) || isstring(ep.lft)
2090 lft = double(ep.lft);
2092 dist = Expolynomial(ep.density, double(ep.eft), lft);
2097if isfield(d, 'params') && ~isempty(d.params)
2101 % 'lambda' is canonical; 'rate' is accepted as a read-side alias
2102 % (the manual's published LQN example uses it, and Python honours it).
2103 if isfield(p, 'lambda')
2105 elseif isfield(p, 'rate')
2108 line_error(mfilename, 'Exp distribution has neither "lambda
" nor "rate
".');
2113 dist = Det(p.value);
2116 dist = Erlang(p.lambda, p.k);
2119 % Two forms cross the wire: the 2-phase form (p of length 2, lambda
2120 % of length 2) and the n-phase vector form (p and lambda both of
2121 % length n). Reading only p(1),lambda(1),lambda(2) silently truncated
2122 % every n>2 HyperExp to its first two phases.
2126 dist = HyperExp(pv, lv(1), lv(2));
2127 elseif numel(pv) == 2 && numel(lv) == 2
2128 dist = HyperExp(pv(1), lv(1), lv(2));
2130 if numel(pv) ~= numel(lv)
2131 line_error(mfilename, sprintf(...
2132 'HyperExp has %d probabilities but %d rates.', numel(pv), numel(lv)));
2134 dist = HyperExp(pv, lv);
2138 dist = Gamma(p.alpha, p.beta);
2141 dist = Lognormal(p.mu, p.sigma);
2144 dist = Uniform(p.a, p.b);
2147 dist = Zipf(p.s, p.n);
2150 dist = Pareto(p.alpha, p.scale);
2153 % JSON: alpha = scale (getParam(1)), beta = shape (getParam(2))
2154 % Constructor: Weibull(shape, scale)
2155 dist = Weibull(p.beta, p.alpha);
2158 dist = Normal(p.mu, p.sigma);
2161 dist = Geometric(p.p);
2164 dist = Binomial(p.n, p.p);
2167 dist = Poisson(p.lambda);
2170 dist = Bernoulli(p.p);
2172 case 'DiscreteUniform'
2173 dist = DiscreteUniform(p.min, p.max);
2175 case 'DiscreteSampler'
2178 dist = DiscreteSampler(pv, xv);
2183 dist = Coxian(mu, phi);
2186 dist = MMPP2(p.lambda0, p.lambda1, p.sigma0, p.sigma1);
2189 dist = MMDP2(p.r0, p.r1, p.sigma0, p.sigma1);
2192 % Absent 'cyclic' means cyclic, matching the constructor default.
2193 if isfield(p, 'cyclic')
2194 cyc = logical(p.cyclic);
2198 dist = NHPP(p.breakpoints(:)', p.rates(:)', cyc);
2201 dist = ME(p.alpha(:)', json2mat(p.A));
2204 dist = RAP(json2mat(p.H0), json2mat(p.H1));
2207 dist = DMAP(json2mat(p.D0), json2mat(p.D1));
2210 % D = {D0, D1, ..., Dk}, Dk driving batches of size k
2211 dist = BMAP(json2matcell(p.D));
2214 % D = {D0, D11, ..., D1K}; the ctor rebuilds the aggregate D1 from
2215 % the K == length(D)-1 form
2216 Dcell = json2matcell(p.D);
2220 Kmarks = numel(Dcell) - 1;
2222 dist = MarkedMMPP(Dcell, Kmarks);
2225 dist = EmpiricalCDF(p.x(:), p.F(:));
2228 % Try to load from file first
2229 if isfield(p, 'fileName') && exist(p.fileName, 'file') == 2
2230 dist = Replayer(p.fileName);
2233 % Fallback to APH if available
2234 if isfield(d, 'ph') && ~isempty(d.ph)
2238 if ~isvector(alpha), alpha = alpha(:)'; end
2239 dist = PH(alpha, T);
2242 % Fallback to Exp with stored mean
2244 if isfield(p, 'mean'), m = p.mean; end
2245 dist = Exp(1.0 / m);
2250% Prior distribution (mixture of alternatives with prior probabilities)
2251if strcmp(dtype, 'Prior')
2252 if isfield(d, 'distributions') && isfield(d, 'probabilities')
2253 altJsons = d.distributions;
2254 probs = d.probabilities;
2255 if ~iscell(altJsons)
2256 % jsondecode may return struct array instead of cell
2257 altJsons = num2cell(altJsons);
2259 alts = cell(1, length(altJsons));
2260 for ai = 1:length(altJsons)
2261 alts{ai} = json2dist(altJsons{ai});
2264 dist = Prior(alts, probs);
2269% PH/APH representation
2270if isfield(d, 'ph') && ~isempty(d.ph)
2274 alpha = alpha(:)'; % Ensure row vector (jsondecode returns column vectors)
2275 if strcmp(dtype, 'APH')
2276 dist = APH(alpha, T);
2278 dist = PH(alpha, T);
2284if isfield(d, 'map') && ~isempty(d.map)
2292% Marked MAP representation: {D0, per-mark D1k}; the aggregate D1 is
2293% rebuilt by the MarkedMAP constructor (K == length(D)-1 form)
2294if isfield(d, 'mmap') && ~isempty(d.mmap)
2299 % jsondecode collapses equally-sized matrices into an ND array:
2300 % (K x n x n) with marks along the first dimension
2301 Kmarks = size(d1k, 1);
2302 Dcell = cell(1, 1 + Kmarks);
2305 Dcell{1+km} = squeeze(d1k(km, :, :));
2308 if ~iscell(d1k), d1k = num2cell(d1k); end
2309 Dcell = [{D0}, d1k(:)'];
2311 dist = MarkedMAP(Dcell, numel(Dcell)-1);
2316if isfield(d, 'fit') && ~isempty(d.fit)
2318 method = fit.method;
2324 dist = Exp(1.0 / m);
2328 dist = Exp(1.0 / m);
2331 case 'fitMeanAndSCV'
2336 dist = Erlang.fitMeanAndSCV(m, scv);
2338 dist = HyperExp.fitMeanAndSCV(m, scv);
2340 dist = Exp(1.0 / m);
2343 case 'fitMeanAndOrder'
2348 dist = Erlang.fitMeanAndOrder(m, order);
2350 dist = Exp(1.0 / m);
2356% Unrecognized type. The writer's fallback contract emits the real type name
2357% together with params.mean/params.scv, so rebuild an APH matching both moments
2358% and warn. Exp(1.0) discarded the mean as well as the SCV and was silent about
2360if isfield(d, 'params') && isfield(d.params, 'mean') && isfield(d.params, 'scv')
2361 line_warning(mfilename, sprintf(['Distribution type "%s
" is not supported on load; ' ...
2362 'reconstructing an APH fitted to its mean and SCV.\n'], dtype));
2363 dist = APH.fitMeanAndSCV(d.params.mean, d.params.scv);
2366if isfield(d, 'params') && isfield(d.params, 'mean')
2367 line_warning(mfilename, sprintf(['Distribution type "%s
" is not supported on load and ' ...
2368 'carries no SCV; reconstructing an Exp with its mean.\n'], dtype));
2369 dist = Exp(1.0 / d.params.mean);
2372line_error(mfilename, sprintf(['Distribution type "%s
" is not supported on load and carries ' ...
2373 'no moments to fit.'], dtype));
2377% =========================================================================
2378% Routing parser (handles comma keys in JSON)
2379% =========================================================================
2381function entries = parse_routing_keys(rawJson, class_map, node_map)
2382% Parse routing matrix from raw JSON text to handle keys with commas.
2383% Returns a cell array of structs with fields:
2384% className1, className2, fromNode, toNode, prob
2387% Build reverse mapping: jsondecode-sanitized name -> original node name
2388% jsondecode uses matlab.lang.makeValidName which replaces spaces etc.
2389nodeNames = node_map.keys();
2390sanitized_map = containers.Map();
2391for ni = 1:length(nodeNames)
2392 origName = nodeNames{ni};
2393 sanitized = matlab.lang.makeValidName(origName);
2394 sanitized_map(sanitized) = origName;
2397classNames = class_map.keys();
2399% For each pair of class names, try to find the corresponding key in the JSON
2400for ri = 1:length(classNames)
2401 for si = 1:length(classNames)
2402 cn1 = classNames{ri};
2403 cn2 = classNames{si};
2404 keyStr = ['"', cn1, ',
', cn2, '"'];
2406 % Find this key in the raw JSON
2407 pos = strfind(rawJson, keyStr);
2412 % For each occurrence, extract the nested from -> to -> prob structure
2413 for pidx = 1:length(pos)
2414 startPos = pos(pidx) + length(keyStr);
2415 % Skip whitespace and colon
2417 while idx <= length(rawJson) && (rawJson(idx) == ' ' || rawJson(idx) == ':' || rawJson(idx) == newline || rawJson(idx) == char(13) || rawJson(idx) == char(9))
2420 if idx > length(rawJson) || rawJson(idx) ~= '{'
2423 % Extract the JSON object using brace counting
2424 objStr = extract_json_object(rawJson, idx);
2428 % Parse the from -> to -> prob structure
2430 fromTo = jsondecode(objStr);
2431 fromNames = fieldnames(fromTo);
2432 for fi = 1:length(fromNames)
2433 fromField = fromNames{fi};
2434 toStruct = fromTo.(fromField);
2435 toNames = fieldnames(toStruct);
2436 % Resolve sanitized field names back to original node names
2437 if sanitized_map.isKey(fromField)
2438 fromName = sanitized_map(fromField);
2440 fromName = fromField;
2442 for ti = 1:length(toNames)
2443 toField = toNames{ti};
2444 prob = toStruct.(toField);
2445 if sanitized_map.isKey(toField)
2446 toName = sanitized_map(toField);
2450 % Verify names exist in the model
2451 if node_map.isKey(fromName) && node_map.isKey(toName)
2453 re.className1 = cn1;
2454 re.className2 = cn2;
2455 re.fromNode = fromName;
2458 entries{end+1} = re; %#ok<AGROW>
2463 % Skip if parsing fails
2471function objStr = extract_json_object(str, startIdx)
2472% Extract a JSON object string starting at startIdx (must be '{').
2473if str(startIdx) ~= '{'
2480for i = startIdx:length(str)
2491 inString = ~inString;
2500 objStr = str(startIdx:i);
2510% =========================================================================
2512% =========================================================================
2514function id = str_to_sched_id(str)
2515% Map a wire scheduling enum name to a SchedStrategy numeric ID.
2517% SchedStrategy.fromText case-folds and resolves the FCFSPRIO/HOL, LAS/FB and
2518% SET/SETF aliases, and errors on an unknown name. Do not reintroduce a
2519% hand-rolled whitelist here: the previous one covered 23 of 40 strategies,
2520% silently degraded the rest to FCFS, and lacked the PAS/OI cases that
2521% linemodel_save itself emits.
2527 % Legacy alias kept for files written before SIRO was named: JMT calls
2528 % the same discipline "RAND". Not a SchedStrategy.fromText case.
2529 id = SchedStrategy.SIRO;
2531 id = SchedStrategy.fromText(lower(char(str)));
2536function id = str_to_depdisc(str)
2537% Map a departure discipline name to a DepartureDiscipline numeric ID. Matched
2538% case-insensitively, as the JAR reader does.
2539switch lower(char(str))
2540 case 'normal
', id = DepartureDiscipline.NORMAL;
2541 case 'fifo
', id = DepartureDiscipline.FIFO;
2543 line_error(mfilename, sprintf('Unrecognized departure discipline
"%s".
', str));
2548function id = str_to_impatience(str)
2549% Map an impatience type name to an ImpatienceType numeric ID.
2550switch lower(char(str))
2551 case 'reneging
', id = ImpatienceType.RENEGING;
2552 case 'balking
', id = ImpatienceType.BALKING;
2553 case 'retrial
', id = ImpatienceType.RETRIAL;
2555 line_error(mfilename, sprintf('Unrecognized impatience type
"%s".
', str));
2560function id = str_to_repl_id(str)
2561% Map replacement strategy string to ReplacementStrategy numeric ID.
2563 case 'LRU
', id = ReplacementStrategy.LRU;
2564 case 'FIFO
', id = ReplacementStrategy.FIFO;
2565 case 'RR
', id = ReplacementStrategy.RR;
2566 case 'SFIFO
', id = ReplacementStrategy.SFIFO;
2567 case 'HLRU
', id = ReplacementStrategy.HLRU;
2568 case 'CLIMB
', id = ReplacementStrategy.CLIMB;
2569 case 'QLRU
', id = ReplacementStrategy.QLRU;
2571 line_error(mfilename, sprintf('Unrecognized replacement strategy
"%s".
', str));
2576function id = str_to_prectype(str)
2577% Map JAR precedence type string to MATLAB ActivityPrecedenceType numeric ID.
2579 case 'pre
', id = ActivityPrecedenceType.PRE_SEQ;
2580 case 'pre-AND
', id = ActivityPrecedenceType.PRE_AND;
2581 case 'pre-OR
', id = ActivityPrecedenceType.PRE_OR;
2582 case 'post
', id = ActivityPrecedenceType.POST_SEQ;
2583 case 'post-AND
', id = ActivityPrecedenceType.POST_AND;
2584 case 'post-OR
', id = ActivityPrecedenceType.POST_OR;
2585 case 'post-LOOP
', id = ActivityPrecedenceType.POST_LOOP;
2586 case 'post-CACHE
', id = ActivityPrecedenceType.POST_CACHE;
2587 otherwise, id = ActivityPrecedenceType.PRE_SEQ;
2592function id = str_to_droprule(str)
2593% Map drop rule string to DropStrategy numeric ID.
2595 case 'drop
', id = DropStrategy.DROP;
2596 case 'waitingQueue
', id = DropStrategy.WAITQ;
2597 case 'blockingAfterService
', id = DropStrategy.BAS;
2598 case 'retrial
', id = DropStrategy.RETRIAL;
2599 case 'retrialWithLimit
', id = DropStrategy.RETRIAL_WITH_LIMIT;
2600 otherwise, id = DropStrategy.WAITQ;
2605function m = json2mat(arr)
2606% Convert a JSON 2D array to a numeric matrix. jsondecode returns a numeric
2607% matrix for a rectangular array of numbers, but a cell array of row vectors
2608% when the rows differ in length or when the array was written through a cell
2611 m = cell2mat(cellfun(@(r) double(r(:)'), arr(:),
'UniformOutput', false));
2617function c = json2matcell(arr)
2618% Convert a JSON array of 2D matrices (which jsondecode returns as a cell
2619% array of matrices, or collapses into a 3D numeric array when all matrices
2620% have equal size) into a cell array of 2D matrices.
2622 c = cell(1, numel(arr));
2623 for ci = 1:numel(arr)
2625 if iscell(m) % array of row arrays (ragged rows)
2626 c{ci} = cell2mat(cellfun(@(r) r(:)
', m(:), 'UniformOutput
', false));
2631elseif ndims(arr) == 3
2632 c = cell(1, size(arr, 1));
2633 for ci = 1:size(arr, 1)
2634 c{ci} = squeeze(arr(ci, :, :));
2637 c = {arr}; % single matrix
2643function c = cellify_string_array(arr)
2644% Convert a JSON string array (which may be decoded as a char, cell, or
2645% struct array) into a cell array of character vectors.
2653 % jsondecode can return a struct array or char matrix for string arrays
2658function v = oi_cutoffs_vec(c)
2659% Decode the oiCutoffs array. The writer wraps it in a cell so that a
2660% single-class model still encodes as a JSON array rather than a bare scalar.
2662 v = cell2mat(c(:)');
2668function muFun = oi_table_to_handle(tblStruct, cutoffs)
2669% Rebuild an OI/PAS total-service-rate handle mu(c) from the materialized
2670% macrostate table written by OI_RATE_TABLE (linemodel_save). The table is keyed
2671% by the per-class counts, which is lossless because mu is order-independent;
2672% the handle therefore reduces the ordered microstate vector c (a list of class
2673% indices) to its class counts before looking up. Counts are clamped to CUTOFFS,
2674% so mu saturates beyond the tabulated range exactly as the table intends. As in
2675% CD_TABLE_TO_HANDLE, jsondecode mangles the JSON keys ("1,1") into valid MATLAB
2676% identifiers ("x1_1"), so the counts are parsed back out of the field names.
2677map = containers.Map('KeyType
', 'char
', 'ValueType
', 'double
');
2678fn = fieldnames(tblStruct);
2682 parts = strsplit(nm(2:end), '_
'); % drop the 'x
' prefix jsondecode prepends
2683 n = cellfun(@str2double, parts);
2687 map(cd_state_key(n)) = double(tblStruct.(nm));
2689muFun = @(c) oi_table_eval(c, map, cutoffs, K);
2692function rate = oi_table_eval(c, map, cutoffs, K)
2693c = round(double(c(:)'));
2697 if ci >= 1 && ci <= K
2698 cnt(ci) = cnt(ci) + 1;
2702 m = min(numel(cnt), numel(cutoffs));
2703 cnt(1:m) = min(cnt(1:m), cutoffs(1:m));
2706 rate = 0; %
the empty state
is omitted from
the table: an idle queue
2709k = cd_state_key(cnt);
2717function beta = cd_table_to_handle(tblStruct, cutoffs)
2718% Rebuild a
class-dependence handle beta(n) from
the materialized lattice table
2719% written by CD_SCALING_TABLE (linemodel_save). jsondecode mangles
the JSON keys
2720% (
"1,1") into valid MATLAB identifiers (
"x1_1"), so
the per-
class counts are
2721% parsed back out of
the field names rather than reconstructed from them. The
2722% population
is clamped to CUTOFFS, so beta saturates beyond
the tabulated range
2723% exactly as
the table intends.
2724map = containers.Map(
'KeyType',
'char',
'ValueType',
'any');
2725fn = fieldnames(tblStruct);
2728 parts = strsplit(nm(2:end),
'_'); % drop
the 'x' prefix jsondecode prepends
2729 n = cellfun(@str2double, parts);
2730 v = double(tblStruct.(nm));
2731 map(cd_state_key(n)) = v(:)
';
2733beta = @(ni) cd_table_eval(ni, map, cutoffs);
2736function k = cd_state_key(n)
2737k = strjoin(arrayfun(@(x) sprintf('%d
', x), n(:)',
'UniformOutput', false),
',');
2740function v = cd_table_eval(ni,
map, cutoffs)
2741n = round(
double(ni(:)
'));
2744 m = min(numel(n), numel(cutoffs));
2745 n(1:m) = min(n(1:m), cutoffs(1:m));
2751 v = 1; % a state absent from the table is neutral (no scaling)