LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
linemodel_load.m
1function model = linemodel_load(filename)
2% LINEMODEL_LOAD Load a LINE model from JSON.
3%
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.
7%
9% filename - path to a .json file
10%
11% Returns:
12% model - Network, LayeredNetwork, Workflow, or Environment object
13%
14% Example:
15% model = linemodel_load('mm1.json');
16% solver = SolverMVA(model);
17% AvgTable = solver.getAvgTable();
18%
19% Copyright (c) 2012-2026, Imperial College London
20% All rights reserved.
21
22jsonText = fileread(filename);
23doc = jsondecode(jsonText);
24
25if ~isfield(doc, 'model')
26 error('linemodel_load:noModel', 'JSON file does not contain a "model" field.');
27end
28
29data = doc.model;
30mtype = data.type;
31
32switch mtype
33 case 'Network'
34 model = json2network(data, jsonText);
35 case 'LayeredNetwork'
36 model = json2layered(data);
37 case 'Workflow'
38 model = json2workflow(data);
39 case 'Environment'
40 model = json2environment(data, jsonText);
41 otherwise
42 error('linemodel_load:unknownType', 'Unsupported model type: %s', mtype);
43end
44end
45
46
47% =========================================================================
48% Network deserialization
49% =========================================================================
50
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.
54
55modelName = 'model';
56if isfield(data, 'name')
57 modelName = data.name;
58end
59model = Network(modelName);
60
61% --- Create nodes (before classes, since ClosedClass needs refstat) ---
62nodeList = {};
63if isfield(data, 'nodes')
64 nds = data.nodes;
65 if isstruct(nds)
66 nds = num2cell(nds);
67 end
68 for i = 1:length(nds)
69 nd = nds{i};
70 if isstruct(nd)
71 nd_name = nd.name;
72 nd_type = nd.type;
73 else
74 nd_name = nd('name');
75 nd_type = nd('type');
76 end
77 node = create_node(model, nd, nd_name, nd_type);
78 nodeList{end+1} = node; %#ok<AGROW>
79 end
80end
81node_map = containers.Map();
82for i = 1:length(nodeList)
83 node_map(nodeList{i}.name) = nodeList{i};
84end
85
86% --- Deferred node linking (Fork/Join, Fork tasksPerLink) ---
87if isfield(data, 'nodes')
88 nds2 = data.nodes;
89 if isstruct(nds2)
90 nds2 = num2cell(nds2);
91 end
92 for i = 1:length(nds2)
93 nd2 = nds2{i};
94 nd_name2 = nd2.name;
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);
100 end
101 end
102 % Fork: set tasksPerLink
103 if isfield(nd2, 'tasksPerLink') && isa(node2, 'Fork')
104 node2.setTasksPerLink(nd2.tasksPerLink);
105 end
106 end
107end
108
109% --- Create classes ---
110classesList = {};
111if isfield(data, 'classes')
112 cls = data.classes;
113 if isstruct(cls)
114 cls = num2cell(cls);
115 end
116 for i = 1:length(cls)
117 cd = cls{i};
118 cname = cd.name;
119 ctype = cd.type;
120 switch ctype
121 case 'Open'
122 prio = 0;
123 if isfield(cd, 'priority'), prio = cd.priority; end
124 jc = OpenClass(model, cname, prio);
125 case {'Closed', 'SelfLooping'}
126 pop = cd.population;
127 refNode = [];
128 if isfield(cd, 'refNode') && node_map.isKey(cd.refNode)
129 refNode = node_map(cd.refNode);
130 end
131 prio = 0;
132 if isfield(cd, 'priority'), prio = cd.priority; end
133 if isempty(refNode)
134 error('linemodel_load:noRefNode', ...
135 '%s class "%s" has no valid refNode.', ctype, cname);
136 end
137 if strcmp(ctype, 'SelfLooping')
138 jc = SelfLoopingClass(model, cname, pop, refNode, prio);
139 else
140 jc = ClosedClass(model, cname, pop, refNode, prio);
141 end
142 case 'Signal'
143 prio = 0;
144 if isfield(cd, 'priority'), prio = cd.priority; end
145 sigType = SignalType.NEGATIVE;
146 if isfield(cd, 'signalType')
147 sigType = SignalType.fromText(cd.signalType);
148 end
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')
156 refNode = [];
157 if isfield(cd, 'refNode') && node_map.isKey(cd.refNode)
158 refNode = node_map(cd.refNode);
159 end
160 if isempty(refNode)
161 error('linemodel_load:noRefNode', ...
162 'ClosedSignal "%s" has no valid refNode.', cname);
163 end
164 jc = ClosedSignal(model, cname, sigType, refNode, prio);
165 else
166 jc = OpenSignal(model, cname, sigType, prio);
167 end
168 % Removal distribution
169 if isfield(cd, 'removalDistribution')
170 remDist = json2dist(cd.removalDistribution);
171 if ~isempty(remDist)
172 jc.setRemovalDistribution(remDist);
173 end
174 end
175 % Removal policy
176 if isfield(cd, 'removalPolicy')
177 jc.setRemovalPolicy(RemovalPolicy.fromText(cd.removalPolicy));
178 end
179 otherwise
180 jc = OpenClass(model, cname);
181 end
182 if isfield(cd, 'deadline') && isfinite(cd.deadline)
183 jc.deadline = cd.deadline;
184 end
185 if isfield(cd, 'isReferenceClass') && cd.isReferenceClass
186 jc.setReferenceClass(true);
187 end
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);
195 else
196 jc.setPatience(patDist);
197 end
198 end
199 end
200 classesList{end+1} = jc; %#ok<AGROW>
201 end
202end
203class_map = containers.Map();
204for i = 1:length(classesList)
205 class_map(classesList{i}.name) = classesList{i};
206end
207
208% --- Resolve signal targetClass associations ---
209if isfield(data, 'classes')
210 cls2 = data.classes;
211 if isstruct(cls2), cls2 = num2cell(cls2); end
212 for i = 1:length(cls2)
213 cd2 = cls2{i};
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));
218 end
219 end
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));
225 end
226 end
227end
228
229% --- Set service/arrival distributions ---
230if isfield(data, 'nodes')
231 nds = data.nodes;
232 if isstruct(nds)
233 nds = num2cell(nds);
234 end
235 for i = 1:length(nds)
236 nd = nds{i};
237 nd_name = nd.name;
238 node = node_map(nd_name);
239
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
249 svc = nd.service;
250 svcFields = fieldnames(svc);
251 for f = 1:length(svcFields)
252 cname = svcFields{f};
253 distJson = svc.(cname);
254 if ~class_map.isKey(cname)
255 continue;
256 end
257 jc = class_map(cname);
258 dist = json2dist(distJson);
259 if ~isempty(dist)
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
269 weight = 1;
270 if isfield(nd, 'schedParams')
271 sp_tmp = nd.schedParams;
272 if isfield(sp_tmp, cname)
273 weight = sp_tmp.(cname);
274 end
275 end
276 node.setService(jc, dist, weight);
277 end
278 end
279 end
280 end
281
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)
289 continue;
290 end
291 bdist = json2dist(nd.arrivalBatch.(cname));
292 if ~isempty(bdist)
293 node.setArrivalBatch(class_map(cname), bdist);
294 end
295 end
296 end
297
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
304 markedList = {};
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>
309 end
310 end
311 if ~isempty(markedList)
312 firstDist = node.getArrivalProcess(markedList{1}.index);
313 if isa(firstDist, 'MarkedMAP')
314 node.setMarkedArrival(firstDist, markedList);
315 end
316 end
317 end
318
319 % ClassSwitch matrix (dict format: classSwitchMatrix)
320 if isfield(nd, 'classSwitchMatrix') && isa(node, 'ClassSwitch')
321 csm_data = nd.classSwitchMatrix;
322 classes = model.getClasses();
323 K = length(classes);
324 mat = zeros(K);
325 class_idx = containers.Map();
326 for ci = 1:K
327 class_idx(classes{ci}.name) = ci;
328 end
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);
341 end
342 end
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')
346 mat = nd.csMatrix;
347 if iscell(mat)
348 mat = cell2mat(mat);
349 end
350 node.server = node.server.updateClassSwitch(mat);
351 end
352
353 % DPS scheduling parameters (weights already passed via setService above)
354
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)));
365 end
366 end
367 end
368
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);
378 % Find class index
379 cls = model.getClasses();
380 for ci = 1:length(cls)
381 if strcmp(cls{ci}.name, cname)
382 node.classCap(ci) = ccData.(cname);
383 break;
384 end
385 end
386 end
387 end
388 end
389
390 % Drop rules
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));
401 break;
402 end
403 end
404 end
405 end
406 end
407
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));
418 end
419 if isfield(nd, 'oiCutoffs')
420 oicut = round(double(oi_cutoffs_vec(nd.oiCutoffs)));
421 else
422 oicut = [];
423 end
424 node.setServiceRateFunction(oi_table_to_handle(nd.oiServiceRate, oicut));
425 end
426
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);
433 end
434 end
435
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(:)'));
444 else
445 cdcut = [];
446 end
447 cdHandle = cd_table_to_handle(cdep.scaling, cdcut);
448 if isfield(cdep, 'peak') && ~isempty(cdep.peak)
449 if iscell(cdep.peak)
450 cdPeak = double(cell2mat(cdep.peak));
451 else
452 cdPeak = double(cdep.peak(:)');
453 end
454 else
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));
459 end
460 node.setLimitedClassDependence(cdHandle, cdPeak);
461 end
462 end
463
464 % Join strategy and quorum
465 if isa(node, 'Join')
466 if isfield(nd, 'joinStrategy')
467 jsStr = nd.joinStrategy;
468 classes = model.getClasses();
469 for ci = 1:length(classes)
470 switch jsStr
471 case 'STD'
472 node.input.setStrategy(classes{ci}, JoinStrategy.STD);
473 case {'PARTIAL', 'QUORUM', 'Quorum'}
474 node.input.setStrategy(classes{ci}, JoinStrategy.PARTIAL);
475 end
476 end
477 end
478 if isfield(nd, 'joinQuorum')
479 jq = nd.joinQuorum;
480 classes = model.getClasses();
481 for ci = 1:length(classes)
482 node.input.setRequired(classes{ci}, jq);
483 end
484 end
485 end
486
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')
492 cc = nd.cache;
493 else
494 cc = struct();
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
501 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;
507 end
508 if ~isfield(cc, 'retrievalSystem') && isfield(nd, 'retrievalSystem')
509 cc.retrievalSystem = nd.retrievalSystem;
510 end
511 % q-LRU admission probability on a miss
512 if isfield(cc, 'admissionProb')
513 node.setAdmissionProb(cc.admissionProb);
514 end
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);
525 end
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);
535 qIdx = [];
536 for qi = 1:numel(qNames)
537 if node_map.isKey(qNames{qi})
538 qIdx(end+1) = node_map(qNames{qi}).index; %#ok<AGROW>
539 end
540 end
541 node.retrievalSystemQueueIndices(int32(jobin.index - 1)) = qIdx;
542 end
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;
554 end
555 end
556 end
557 end
558 end
559 end
560 % Hit class mapping
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));
569 end
570 end
571 end
572 % Miss class mapping
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));
581 end
582 end
583 end
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);
594 end
595 end
596 end
597 end
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;
604 if ~iscell(apData)
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);
609 end
610 Kap = numel(apData);
611 rows = cellfun(@json2matcell, apData, 'UniformOutput', false);
612 Nap = max(cellfun(@numel, rows));
613 R = cell(Kap, Nap);
614 for k1 = 1:Kap
615 R(k1, 1:numel(rows{k1})) = rows{k1};
616 end
617 node.setAccessProb(R);
618 end
619 % Initial cache state [class counts | contents | retrieval bitmap]
620 if isfield(cc, 'initialState')
621 node.setState(cc.initialState(:)');
622 end
623 end
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)
629 stData = stArr{si};
630 stName = stData.name;
631 stCount = stData.count;
632 st = ServerType(stName, stCount);
633 % Compatible classes
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}));
640 end
641 end
642 end
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));
653 if ~isempty(dist)
654 node.setHeteroService(jc, st, dist);
655 end
656 end
657 end
658 end
659 end
660 % Scheduling policy
661 if isfield(nd, 'heteroSchedPolicy')
662 policy = HeteroSchedPolicy.fromText(nd.heteroSchedPolicy);
663 node.setHeteroSchedPolicy(policy);
664 end
665 end
666 end
667end
668
669% --- Restore Balking, Retrial, Patience ---
670if isfield(data, 'nodes')
671 ndsImp = data.nodes;
672 if isstruct(ndsImp), ndsImp = num2cell(ndsImp); end
673 for i = 1:length(ndsImp)
674 ndImp = ndsImp{i};
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(:));
683 end
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)
690 cname = ifNames{fi};
691 if class_map.isKey(cname) && ifData.(cname)
692 node.setImmediateFeedback(class_map(cname));
693 end
694 end
695 end
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);
706 end
707 end
708 end
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));
717 end
718 end
719 % Balking
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);
728 % Parse strategy
729 switch bjc.strategy
730 case 'QUEUE_LENGTH', strategy = BalkingStrategy.QUEUE_LENGTH;
731 case 'EXPECTED_WAIT', strategy = BalkingStrategy.EXPECTED_WAIT;
732 case 'COMBINED', strategy = BalkingStrategy.COMBINED;
733 otherwise, continue;
734 end
735 % Parse thresholds
736 thData = bjc.thresholds;
737 if isstruct(thData), thData = num2cell(thData); end
738 thresholds = {};
739 for ti = 1:length(thData)
740 td = thData{ti};
741 maxJobs = td.maxJobs;
742 if maxJobs < 0, maxJobs = Inf; end
743 thresholds{end+1} = {td.minJobs, maxJobs, td.probability};
744 end
745 node.setBalking(jc, strategy, thresholds);
746 end
747 end
748 % Retrial
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);
758 maxAttempts = -1;
759 if isfield(rjc, 'maxAttempts')
760 maxAttempts = rjc.maxAttempts;
761 end
762 node.setRetrial(jc, delayDist, maxAttempts);
763 end
764 end
765 % Patience
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);
777 else
778 impType = ImpatienceType.RENEGING;
779 end
780 node.setPatience(jc, impType, patDist);
781 end
782 end
783 end
784end
785
786% --- Configure Transition modes ---
787if isfield(data, 'nodes')
788 nds3 = data.nodes;
789 if isstruct(nds3)
790 nds3 = num2cell(nds3);
791 end
792 for i = 1:length(nds3)
793 nd3 = nds3{i};
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);
800 end
801 for mi = 1:length(modesData)
802 md = modesData{mi};
803 modeName = 'Mode';
804 if isfield(md, 'name'), modeName = md.name; end
805 mode = tnode.addMode(modeName);
806 % Distribution
807 if isfield(md, 'distribution') && ~isempty(md.distribution)
808 dist = json2dist(md.distribution);
809 if ~isempty(dist)
810 tnode.setDistribution(mode, dist);
811 end
812 end
813 % Timing strategy
814 if isfield(md, 'timingStrategy')
815 if strcmp(md.timingStrategy, 'IMMEDIATE')
816 tnode.setTimingStrategy(mode, TimingStrategy.IMMEDIATE);
817 else
818 tnode.setTimingStrategy(mode, TimingStrategy.TIMED);
819 end
820 end
821 % Number of servers
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
826 end
827 if nsVal > 1
828 tnode.setNumberOfServers(mode, nsVal);
829 end
830 end
831 % Firing priority
832 if isfield(md, 'firingPriority')
833 tnode.setFiringPriorities(mode, md.firingPriority);
834 end
835 % Firing weight
836 if isfield(md, 'firingWeight')
837 tnode.setFiringWeights(mode, md.firingWeight);
838 end
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)
844 ec = ecList{ei};
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);
847 end
848 end
849 end
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)
855 ic = icList{ii};
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);
858 end
859 end
860 end
861 % Firing outcomes
862 if isfield(md, 'firingOutcomes')
863 foList = md.firingOutcomes;
864 if isstruct(foList), foList = num2cell(foList); end
865 for fi = 1:length(foList)
866 fo = foList{fi};
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);
869 end
870 end
871 end
872 end
873 end
874end
875
876% --- Restore initial state for Place nodes ---
877if isfield(data, 'nodes')
878 nds4 = data.nodes;
879 if isstruct(nds4), nds4 = num2cell(nds4); end
880 for i = 1:length(nds4)
881 nd4 = nds4{i};
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);
888 end
889 end
890 end
891end
892
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);
898
899 % Build node/class index maps
900 nodeIdx = containers.Map();
901 for i = 1:M
902 nodeIdx(nodeList{i}.name) = i;
903 end
904 classIdx = containers.Map();
905 for i = 1:K
906 classIdx(classesList{i}.name) = i;
907 end
908
909 % Parse routing keys from raw JSON to preserve commas
910 routingEntries = parse_routing_keys(rawJson, class_map, node_map);
911
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;
919 end
920
921 model.link(P);
922end
923
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;
935
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);
952 end
953 end
954 end
955 end
956 end
957end
958
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;
975 if length(nodeObj.output.outputStrategy) >= classIdx && ...
976 length(nodeObj.output.outputStrategy{1, classIdx}) >= 3
977 nodeObj.output.outputStrategy{1, classIdx}{3} = {};
978 end
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);
985 end
986 end
987 end
988 end
989 end
990 end
991end
992
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.
996ndsSo = data.nodes;
997if isstruct(ndsSo), ndsSo = num2cell(ndsSo); end
998for ni = 1:length(ndsSo)
999 nd = ndsSo{ni};
1000 if ~node_map.isKey(nd.name)
1001 continue;
1002 end
1003
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)
1013 continue;
1014 end
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);
1019 end
1020 end
1021 end
1022
1023 % Polling type, restored by name. This must precede the switchover
1024 % restore below: setPollingType resets the switchover of every class to
1025 % Immediate.
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);
1032 else
1033 nodeObj.setPollingType(ptId, 1);
1034 end
1035 else
1036 nodeObj.setPollingType(ptId);
1037 end
1038 end
1039
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;
1045 if ~iscell(soArr)
1046 soArr = num2cell(soArr);
1047 end
1048 for si = 1:length(soArr)
1049 so = soArr{si};
1050 if ~class_map.isKey(so.from)
1051 continue;
1052 end
1053 fromCls = class_map(so.from);
1054 dist = json2dist(so.distribution);
1055 if isempty(dist)
1056 continue;
1057 end
1058 if isfield(so, 'to') && ~isempty(so.to)
1059 if ~class_map.isKey(so.to)
1060 continue;
1061 end
1062 nodeObj.setSwitchover(fromCls, class_map(so.to), dist);
1063 else
1064 nodeObj.setSwitchover(fromCls, dist);
1065 end
1066 end
1067 end
1068end
1069
1070% --- Restore finite capacity regions ---
1071if isfield(data, 'finiteCapacityRegions')
1072 fcrArr = data.finiteCapacityRegions;
1073 if ~iscell(fcrArr)
1074 fcrArr = {fcrArr};
1075 end
1076 classes = model.getClasses();
1077 for ri = 1:length(fcrArr)
1078 rj = fcrArr{ri};
1079 regNodes = {};
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;
1088 if iscell(stArr)
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>
1093 end
1094 end
1095 else
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>
1100 end
1101 end
1102 end
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>
1109 end
1110 end
1111 end
1112 maxJobs = FiniteCapacityRegion.UNBOUNDED;
1113 if isfield(rj, 'globalMaxJobs')
1114 maxJobs = rj.globalMaxJobs;
1115 end
1116 if ~isempty(regNodes)
1117 try
1118 region = model.addRegion(regNodes);
1119 if isfield(rj, 'name') && ~isempty(rj.name)
1120 region.setName(rj.name);
1121 end
1122 if maxJobs ~= FiniteCapacityRegion.UNBOUNDED
1123 region.setGlobalMaxJobs(maxJobs);
1124 end
1125 % globalMaxMemory
1126 if isfield(rj, 'globalMaxMemory')
1127 region.globalMaxMemory = rj.globalMaxMemory;
1128 end
1129 % classMaxJobs
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);
1138 end
1139 end
1140 end
1141 % dropRule
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));
1150 end
1151 end
1152 end
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)
1158 sj2 = stArr2{si};
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);
1167 end
1168 end
1169 end
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);
1178 end
1179 end
1180 end
1181 end
1182 end
1183 % Linear constraints A * x <= b
1184 if isfield(rj, 'constraintA') && isfield(rj, 'constraintB')
1185 Adata = rj.constraintA;
1186 bdata = rj.constraintB;
1187 if iscell(Adata)
1188 nrows = length(Adata);
1189 K = length(region.classes);
1190 A = zeros(nrows, K);
1191 for ar = 1:nrows
1192 row = Adata{ar};
1193 A(ar, 1:length(row)) = row(:)';
1194 end
1195 else
1196 A = Adata;
1197 end
1198 region.setConstraint(A, bdata(:));
1199 end
1200 catch
1201 end
1202 end
1203 end
1204end
1205
1206% --- Rewards ---
1207if isfield(data, 'rewards')
1208 rewardsArr = data.rewards;
1209 if isstruct(rewardsArr), rewardsArr = num2cell(rewardsArr); end
1210 for i = 1:length(rewardsArr)
1211 rw = rewardsArr{i};
1212 if ~isfield(rw, 'name') || ~isfield(rw, 'type')
1213 line_warning(mfilename, 'Ignoring a reward entry without a "name" or "type" field.');
1214 continue;
1215 end
1216 rname = rw.name;
1217 rtype = rw.type;
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', ''))));
1221 continue;
1222 end
1223 rnode = node_map(rw.node);
1224 rclass = [];
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));
1229 continue;
1230 end
1231 rclass = class_map(rw.class);
1232 end
1233 switch rtype
1234 case 'QLen'
1235 if isempty(rclass)
1236 model.setReward(rname, Reward.queueLength(rnode));
1237 else
1238 model.setReward(rname, Reward.queueLength(rnode, rclass));
1239 end
1240 case 'Util'
1241 if isempty(rclass)
1242 model.setReward(rname, Reward.utilization(rnode));
1243 else
1244 model.setReward(rname, Reward.utilization(rnode, rclass));
1245 end
1246 case 'Blocking'
1247 model.setReward(rname, Reward.blocking(rnode));
1248 otherwise
1249 line_warning(mfilename, sprintf(['Reward "%s" has type "%s", for which no reward template is ' ...
1250 'implemented; the reward is ignored.'], rname, rtype));
1251 end
1252 end
1253end
1254end
1255
1256
1257function v = getfield_default(s, fieldName, defaultValue)
1258% Return s.(fieldName) when present, otherwise defaultValue.
1259if isfield(s, fieldName)
1260 v = s.(fieldName);
1261else
1262 v = defaultValue;
1263end
1264end
1265
1266
1267function node = create_node(model, nd, name, ntype)
1268% Create a node from JSON data.
1269switch ntype
1270 case 'Source'
1271 node = Source(model, name);
1272 case 'Sink'
1273 node = Sink(model, name);
1274 case 'Delay'
1275 node = Delay(model, name);
1276 case 'Queue'
1277 schedStr = 'FCFS';
1278 if isfield(nd, 'scheduling')
1279 schedStr = nd.scheduling;
1280 end
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);
1287 end
1288 end
1289 if isfield(nd, 'buffer') && isfinite(nd.buffer)
1290 node.cap = nd.buffer;
1291 end
1292 case 'Fork'
1293 node = Fork(model, name);
1294 case 'Join'
1295 node = Join(model, name);
1296 case 'Router'
1297 node = Router(model, name);
1298 case 'ClassSwitch'
1299 node = ClassSwitch(model, name);
1300 case 'Cache'
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).
1304 cc = struct();
1305 if isfield(nd, 'cache')
1306 cc = nd.cache;
1307 end
1308 nitems = 10;
1309 if isfield(cc, 'items'), nitems = cc.items;
1310 elseif isfield(nd, 'numItems'), nitems = nd.numItems; end
1311 cap = 1;
1312 if isfield(cc, 'capacity'), cap = cc.capacity;
1313 elseif isfield(nd, 'itemLevelCap'), cap = nd.itemLevelCap(:)'; end
1314 replStr = 'LRU';
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);
1319 case 'Place'
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));
1326 else
1327 node = Place(model, name);
1328 end
1329 if isfield(nd, 'servers')
1330 node.numberOfServers = servers_from_json(nd.servers);
1331 end
1332 if isfield(nd, 'buffer') && isfinite(nd.buffer)
1333 node.cap = nd.buffer;
1334 end
1335 case 'Transition'
1336 node = Transition(model, name);
1337 otherwise
1338 node = Queue(model, name, SchedStrategy.FCFS);
1339end
1340end
1341
1342
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')
1348 ns = Inf;
1349 else
1350 ns = str2double(v);
1351 end
1352else
1353 ns = double(v);
1354end
1355end
1356
1357
1358% =========================================================================
1359% LayeredNetwork deserialization
1360% =========================================================================
1361
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.
1368if isempty(v)
1369 mult = 1;
1370elseif v >= 2147483647 || v < 0
1371 mult = Inf;
1372else
1373 mult = v;
1374end
1375end
1376
1377function model = json2layered(data)
1378% Reconstruct a LayeredNetwork from decoded JSON struct.
1379
1380modelName = 'model';
1381if isfield(data, 'name')
1382 modelName = data.name;
1383end
1384model = LayeredNetwork(modelName);
1385
1386% --- Processors (Python schema: "processors", JAR schema: "hosts") ---
1387proc_map = containers.Map();
1388if isfield(data, 'processors')
1389 procs = data.processors;
1390elseif isfield(data, 'hosts')
1391 procs = data.hosts;
1392else
1393 procs = [];
1394end
1395if ~isempty(procs)
1396 if isstruct(procs), procs = num2cell(procs); end
1397 for i = 1:length(procs)
1398 pd = procs{i};
1399 pname = pd.name;
1400 mult = 1;
1401 if isfield(pd, 'multiplicity'), mult = mult_from_json(pd.multiplicity); end
1402 schedStr = 'INF';
1403 if isfield(pd, 'scheduling'), schedStr = pd.scheduling; end
1404 schedId = str_to_sched_id(schedStr);
1405 quantum = 0.001;
1406 if isfield(pd, 'quantum'), quantum = pd.quantum; end
1407 sf = 1.0;
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);
1412 end
1413 proc_map(pname) = proc;
1414 end
1415end
1416
1417% --- Tasks ---
1418task_map = containers.Map();
1419if isfield(data, 'tasks')
1420 tsks = data.tasks;
1421 if isstruct(tsks), tsks = num2cell(tsks); end
1422 for i = 1:length(tsks)
1423 td = tsks{i};
1424 tname = td.name;
1425 mult = 1;
1426 if isfield(td, 'multiplicity'), mult = mult_from_json(td.multiplicity); end
1427 schedStr = 'INF';
1428 if isfield(td, 'scheduling'), schedStr = td.scheduling; end
1429 schedId = str_to_sched_id(schedStr);
1430 taskType = 'Task';
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')
1435 totalItems = 1;
1436 if isfield(td, 'totalItems'), totalItems = td.totalItems; end
1437 cacheCap = 1;
1438 if isfield(td, 'cacheCapacity'), cacheCap = td.cacheCapacity; end
1439 rsStr = 'FIFO';
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));
1446 else
1447 rs = ReplacementStrategy.FIFO;
1448 end
1449 task = CacheTask(model, tname, totalItems, cacheCap, rs, mult, schedId);
1450 else
1451 task = Task(model, tname, mult, schedId);
1452 end
1453 % Assign to processor (Python schema: "processor", JAR schema: "host")
1454 procRef = '';
1455 if isfield(td, 'processor'), procRef = td.processor;
1456 elseif isfield(td, 'host'), procRef = td.host;
1457 end
1458 if ~isempty(procRef) && proc_map.isKey(procRef)
1459 task.on(proc_map(procRef));
1460 end
1461 % Think time (Python schema: "thinkTime" as dist, JAR schema: "thinkTimeMean"/"thinkTimeSCV")
1462 if isfield(td, 'thinkTime')
1463 dist = json2dist(td.thinkTime);
1464 if ~isempty(dist)
1465 task.setThinkTime(dist);
1466 end
1467 elseif isfield(td, 'thinkTimeMean') && td.thinkTimeMean > 0
1468 task.setThinkTime(Exp(1.0 / td.thinkTimeMean));
1469 end
1470 % Setup time
1471 if isfield(td, 'setupTime')
1472 dist = json2dist(td.setupTime);
1473 if ~isempty(dist)
1474 task.setSetupTime(dist);
1475 end
1476 elseif isfield(td, 'setupTimeMean') && td.setupTimeMean > 1e-8
1477 task.setSetupTime(Exp(1.0 / td.setupTimeMean));
1478 end
1479 % Delay-off time
1480 if isfield(td, 'delayOffTime')
1481 dist = json2dist(td.delayOffTime);
1482 if ~isempty(dist)
1483 task.setDelayOffTime(dist);
1484 end
1485 elseif isfield(td, 'delayOffTimeMean') && td.delayOffTimeMean > 1e-8
1486 task.setDelayOffTime(Exp(1.0 / td.delayOffTimeMean));
1487 end
1488 % Fan in
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}));
1493 end
1494 end
1495 % Fan out
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}));
1500 end
1501 end
1502 % Replication
1503 if isfield(td, 'replication') && td.replication > 1
1504 task.setReplication(td.replication);
1505 end
1506 task_map(tname) = task;
1507 end
1508end
1509
1510% --- Entries ---
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)
1516 ed = ents{i};
1517 ename = ed.name;
1518 entryType = 'Entry';
1519 if isfield(ed, 'entryType'), entryType = ed.entryType; end
1520 if strcmp(entryType, 'ItemEntry')
1521 totalItems = 1;
1522 if isfield(ed, 'totalItems'), totalItems = ed.totalItems; end
1523 accessProb = [];
1524 if isfield(ed, 'accessProb')
1525 ap = ed.accessProb;
1526 if isstruct(ap)
1527 accessProb = json2dist(ap);
1528 elseif isnumeric(ap)
1529 accessProb = DiscreteSampler(ap);
1530 end
1531 end
1532 if isempty(accessProb)
1533 % Default uniform distribution
1534 accessProb = DiscreteSampler(ones(1, totalItems) / totalItems);
1535 end
1536 entry = ItemEntry(model, ename, totalItems, accessProb);
1537 else
1538 entry = Entry(model, ename);
1539 end
1540 if isfield(ed, 'task') && task_map.isKey(ed.task)
1541 entry.on(task_map(ed.task));
1542 end
1543 % Entry arrival distribution
1544 if isfield(ed, 'arrival')
1545 dist = json2dist(ed.arrival);
1546 if ~isempty(dist)
1547 entry.setArrival(dist);
1548 end
1549 end
1550 entry_map(ename) = entry;
1551 end
1552end
1553
1554% --- Activities ---
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)
1560 ad = acts{i};
1561 aname = ad.name;
1562
1563 % Host demand
1564 hd = GlobalConstants.FineTol;
1565 if isfield(ad, 'hostDemand')
1566 hdDist = json2dist(ad.hostDemand);
1567 if ~isempty(hdDist)
1568 hd = hdDist;
1569 end
1570 end
1571
1572 % Bound to entry (Python schema: "boundTo", JAR schema: "boundToEntry")
1573 bte = '';
1574 if isfield(ad, 'boundTo')
1575 bte = ad.boundTo;
1576 elseif isfield(ad, 'boundToEntry')
1577 bte = ad.boundToEntry;
1578 end
1579
1580 act = Activity(model, aname, hd, bte);
1581
1582 % Assign to task
1583 if isfield(ad, 'task') && task_map.isKey(ad.task)
1584 act.on(task_map(ad.task));
1585 end
1586
1587 % Replies to entry
1588 if isfield(ad, 'repliesTo') && entry_map.isKey(ad.repliesTo)
1589 act.repliesTo(entry_map(ad.repliesTo));
1590 end
1591
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)
1597 sc = scs{j};
1598 if isfield(sc, 'entry'), ename = sc.entry;
1599 elseif isfield(sc, 'dest'), ename = sc.dest;
1600 else, continue;
1601 end
1602 meanCalls = 1.0;
1603 if isfield(sc, 'mean'), meanCalls = sc.mean; end
1604 if entry_map.isKey(ename)
1605 act.synchCall(entry_map(ename), meanCalls);
1606 end
1607 end
1608 end
1609
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)
1615 ac = acs{j};
1616 if isfield(ac, 'entry'), ename = ac.entry;
1617 elseif isfield(ac, 'dest'), ename = ac.dest;
1618 else, continue;
1619 end
1620 meanCalls = 1.0;
1621 if isfield(ac, 'mean'), meanCalls = ac.mean; end
1622 if entry_map.isKey(ename)
1623 act.asynchCall(entry_map(ename), meanCalls);
1624 end
1625 end
1626 end
1627
1628 act_map(aname) = act;
1629 end
1630end
1631
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)
1637 pd = precs{i};
1638 if ~isfield(pd, 'task') || ~task_map.isKey(pd.task)
1639 continue;
1640 end
1641 task = task_map(pd.task);
1642
1643 if isfield(pd, 'preActs') || isfield(pd, 'postActs')
1644 % JAR schema
1645 preNames = {};
1646 postNames = {};
1647 if isfield(pd, 'preActs')
1648 preNames = pd.preActs;
1649 if ischar(preNames), preNames = {preNames}; end
1650 end
1651 if isfield(pd, 'postActs')
1652 postNames = pd.postActs;
1653 if ischar(postNames), postNames = {postNames}; end
1654 end
1655 preType = 'pre';
1656 postType = 'post';
1657 if isfield(pd, 'preType'), preType = pd.preType; end
1658 if isfield(pd, 'postType'), postType = pd.postType; end
1659
1660 % Normalize JAR naming convention to Python convention
1661 switch postType
1662 case 'post-AND', postType = 'and-fork';
1663 case 'post-OR', postType = 'or-fork';
1664 case 'post-LOOP', postType = 'loop';
1665 end
1666 switch preType
1667 case 'pre-AND', preType = 'and-join';
1668 case 'pre-OR', preType = 'or-join';
1669 end
1670
1671 % Extract postParams (JAR schema: probabilities/loopCount)
1672 postParams = [];
1673 if isfield(pd, 'postParams')
1674 postParams = pd.postParams;
1675 if iscell(postParams), postParams = cell2mat(postParams); end
1676 end
1677
1678 preActs = {};
1679 for ai = 1:length(preNames)
1680 if act_map.isKey(preNames{ai})
1681 preActs{end+1} = act_map(preNames{ai}); %#ok<AGROW>
1682 end
1683 end
1684 postActs = {};
1685 for ai = 1:length(postNames)
1686 if act_map.isKey(postNames{ai})
1687 postActs{end+1} = act_map(postNames{ai}); %#ok<AGROW>
1688 end
1689 end
1690
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);
1695 end
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);
1700 end
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);
1705 end
1706 elseif strcmp(preType, 'pre') && strcmp(postType, 'or-fork')
1707 if ~isempty(preActs) && ~isempty(postActs)
1708 probs = [];
1709 if isfield(pd, 'probabilities')
1710 probs = pd.probabilities;
1711 if isstruct(probs), probs = cell2mat(struct2cell(probs)); end
1712 end
1713 if isempty(probs) && ~isempty(postParams)
1714 probs = postParams(:)';
1715 end
1716 if isempty(probs)
1717 n = length(postActs);
1718 probs = ones(1, n) / n;
1719 end
1720 ap = ActivityPrecedence.OrFork(preActs{1}, postActs, probs);
1721 task.addPrecedence(ap);
1722 end
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);
1727 end
1728 elseif strcmp(preType, 'pre') && strcmp(postType, 'loop')
1729 count = 1.0;
1730 if isfield(pd, 'loopCount'), count = pd.loopCount; end
1731 if count == 1.0 && ~isempty(postParams)
1732 count = postParams(1);
1733 end
1734 if ~isempty(preActs) && ~isempty(postActs)
1735 if length(postActs) > 1
1736 ap = ActivityPrecedence.Loop(preActs{1}, postActs(1:end-1), postActs{end}, count);
1737 else
1738 ap = ActivityPrecedence.Loop(preActs{1}, postActs, count);
1739 end
1740 task.addPrecedence(ap);
1741 end
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);
1746 end
1747 end
1748 else
1749 % Python schema
1750 ptype = pd.type;
1751 actNames = pd.activities;
1752 if ischar(actNames), actNames = {actNames}; end
1753
1754 % Resolve activity names to objects
1755 actObjs = {};
1756 for ai = 1:length(actNames)
1757 an = actNames{ai};
1758 if act_map.isKey(an)
1759 actObjs{end+1} = act_map(an); %#ok<AGROW>
1760 end
1761 end
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
1769 continue;
1770 end
1771
1772 switch ptype
1773 case 'Serial'
1774 ap = ActivityPrecedence.Serial(actObjs{:});
1775 task.addPrecedence(ap);
1776 case 'AndFork'
1777 ap = ActivityPrecedence.AndFork(actObjs{1}, actObjs(2:end));
1778 task.addPrecedence(ap);
1779 case 'AndJoin'
1780 ap = ActivityPrecedence.AndJoin(actObjs(1:end-1), actObjs{end});
1781 task.addPrecedence(ap);
1782 case 'OrFork'
1783 probs = [];
1784 if isfield(pd, 'probabilities')
1785 probs = pd.probabilities;
1786 if isstruct(probs), probs = cell2mat(struct2cell(probs)); end
1787 end
1788 if isempty(probs)
1789 n = length(actObjs) - 1;
1790 probs = ones(1, n) / n;
1791 end
1792 ap = ActivityPrecedence.OrFork(actObjs{1}, actObjs(2:end), probs);
1793 task.addPrecedence(ap);
1794 case 'OrJoin'
1795 ap = ActivityPrecedence.OrJoin(actObjs(1:end-1), actObjs{end});
1796 task.addPrecedence(ap);
1797 case 'Loop'
1798 count = 1.0;
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);
1807 else
1808 ap = ActivityPrecedence.Loop(actObjs{1}, actObjs(2:end), count);
1809 end
1810 task.addPrecedence(ap);
1811 case 'CacheAccess'
1812 if length(actObjs) >= 2
1813 ap = ActivityPrecedence.CacheAccess(actObjs{1}, actObjs(2:end));
1814 task.addPrecedence(ap);
1815 end
1816 end
1817 end
1818 end
1819end
1820end
1821
1822
1823% =========================================================================
1824% Workflow deserialization
1825% =========================================================================
1826
1827function model = json2workflow(data)
1828% Reconstruct a Workflow from decoded JSON struct.
1829
1830modelName = 'workflow';
1831if isfield(data, 'name')
1832 modelName = data.name;
1833end
1834model = Workflow(modelName);
1835
1836% --- Activities ---
1837if isfield(data, 'activities')
1838 acts = data.activities;
1839 if isstruct(acts), acts = num2cell(acts); end
1840 for i = 1:length(acts)
1841 ad = acts{i};
1842 actName = ad.name;
1843 if isfield(ad, 'hostDemand') && ~isempty(ad.hostDemand)
1844 dist = json2dist(ad.hostDemand);
1845 model.addActivity(actName, dist);
1846 else
1847 model.addActivity(actName, 1.0);
1848 end
1849 end
1850end
1851
1852% --- Precedences ---
1853if isfield(data, 'precedences')
1854 precs = data.precedences;
1855 if isstruct(precs), precs = num2cell(precs); end
1856 for i = 1:length(precs)
1857 pd = precs{i};
1858
1859 % preActs
1860 if isfield(pd, 'preActs')
1861 preActs = cellify_string_array(pd.preActs);
1862 else
1863 preActs = {};
1864 end
1865
1866 % postActs
1867 if isfield(pd, 'postActs')
1868 postActs = cellify_string_array(pd.postActs);
1869 else
1870 postActs = {};
1871 end
1872
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);
1877 end
1878 postType = ActivityPrecedenceType.POST_SEQ;
1879 if isfield(pd, 'postType')
1880 postType = str_to_prectype(pd.postType);
1881 end
1882
1883 % preParams / postParams
1884 preParams = [];
1885 if isfield(pd, 'preParams') && ~isempty(pd.preParams)
1886 preParams = pd.preParams(:)';
1887 end
1888 postParams = [];
1889 if isfield(pd, 'postParams') && ~isempty(pd.postParams)
1890 postParams = pd.postParams(:)';
1891 end
1892
1893 ap = ActivityPrecedence(preActs, postActs, preType, postType, preParams, postParams);
1894 model.addPrecedence(ap);
1895 end
1896end
1897end
1898
1899
1900% =========================================================================
1901% Environment deserialization
1902% =========================================================================
1903
1904function model = json2environment(data, rawJson)
1905% Reconstruct an Environment from decoded JSON struct.
1906
1907modelName = 'env';
1908if isfield(data, 'name')
1909 modelName = data.name;
1910end
1911numStages = 0;
1912if isfield(data, 'numStages')
1913 numStages = data.numStages;
1914end
1915model = Environment(modelName, numStages);
1916
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.
1925nfArr = {};
1926if isfield(data, 'nodeFailures')
1927 nfArr = data.nodeFailures;
1928 if isstruct(nfArr), nfArr = num2cell(nfArr); end
1929end
1930
1931stages = {};
1932if isfield(data, 'stages')
1933 stages = data.stages;
1934 if isstruct(stages), stages = num2cell(stages); end
1935end
1936
1937declaredNames = cell(1, length(stages));
1938for i = 1:length(stages)
1939 if isfield(stages{i}, 'name')
1940 declaredNames{i} = stages{i}.name;
1941 else
1942 declaredNames{i} = sprintf('Stage%d', i);
1943 end
1944end
1945
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.');
1951 end
1952 if any(strcmp(declaredNames, sprintf('DOWN_%s', nfArr{k}.node)))
1953 macroMode = false;
1954 break;
1955 end
1956end
1957
1958if macroMode
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.']);
1962 end
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.']);
1966 end
1967 if ~isfield(stages{1}, 'model') || isempty(stages{1}.model)
1968 line_error(mfilename, '"nodeFailures" requires the base stage to carry a "model".');
1969 end
1970 baseModel = json2network(stages{1}.model, rawJson);
1971 for k = 1:length(nfArr)
1972 nf = nfArr{k};
1973 [breakdownDist, repairDist, downServiceDist, resetB, resetR] = nodefailure_fields(nf);
1974 if isempty(repairDist)
1975 model.addNodeBreakdown(baseModel, nf.node, breakdownDist, downServiceDist, resetB);
1976 else
1977 model.addNodeFailureRepair(baseModel, nf.node, breakdownDist, repairDist, downServiceDist, ...
1978 resetB, resetR);
1979 end
1980 end
1981else
1982 % --- Stages ---
1983 stageNames = {};
1984 for i = 1:length(stages)
1985 sd = stages{i};
1986 stageName = declaredNames{i};
1987 stageNames{end+1} = stageName; %#ok<AGROW>
1988
1989 stageType = '';
1990 if isfield(sd, 'type')
1991 stageType = sd.type;
1992 end
1993
1994 stageModel = [];
1995 if isfield(sd, 'model') && ~isempty(sd.model)
1996 stageModel = json2network(sd.model, rawJson);
1997 end
1998
1999 if ~isempty(stageModel)
2000 model.addStage(stageName, stageType, stageModel);
2001 end
2002 end
2003
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)
2009 td = trans{i};
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);
2018 end
2019 end
2020 end
2021 end
2022 end
2023
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)
2027 nf = nfArr{k};
2028 [breakdownDist, repairDist, downServiceDist, resetB, resetR] = nodefailure_fields(nf);
2029 model.registerNodeFailure(nf.node, breakdownDist, repairDist, downServiceDist, resetB, resetR);
2030 end
2031end
2032
2033model.init();
2034end
2035
2036
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));
2042end
2043if ~isfield(nf, 'downService') || isempty(nf.downService)
2044 line_error(mfilename, sprintf('Node failure on "%s" is missing the required "downService" field.', nf.node));
2045end
2046breakdownDist = json2dist(nf.breakdownRate);
2047downServiceDist = json2dist(nf.downService);
2048repairDist = [];
2049if isfield(nf, 'repairRate') && ~isempty(nf.repairRate)
2050 repairDist = json2dist(nf.repairRate);
2051end
2052resetB = 'keep';
2053if isfield(nf, 'breakdownResetPolicy') && ~isempty(nf.breakdownResetPolicy)
2054 resetB = nf.breakdownResetPolicy;
2055end
2056resetR = 'keep';
2057if isfield(nf, 'repairResetPolicy') && ~isempty(nf.repairResetPolicy)
2058 resetR = nf.repairResetPolicy;
2059end
2060end
2061
2062
2063% =========================================================================
2064% Distribution deserialization
2065% =========================================================================
2066
2067function dist = json2dist(d)
2068% Convert a JSON distribution struct to a MATLAB Distribution object.
2069if isempty(d)
2070 dist = [];
2071 return;
2072end
2073
2074dtype = d.type;
2075
2076switch dtype
2077 case 'Disabled'
2078 dist = Disabled.getInstance();
2079 return;
2080 case 'Immediate'
2081 dist = Immediate.getInstance();
2082 return;
2083 case 'Expolynomial'
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)
2088 lft = Inf;
2089 else
2090 lft = double(ep.lft);
2091 end
2092 dist = Expolynomial(ep.density, double(ep.eft), lft);
2093 return;
2094end
2095
2096% Direct params
2097if isfield(d, 'params') && ~isempty(d.params)
2098 p = d.params;
2099 switch dtype
2100 case 'Exp'
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')
2104 lam = p.lambda;
2105 elseif isfield(p, 'rate')
2106 lam = p.rate;
2107 else
2108 line_error(mfilename, 'Exp distribution has neither "lambda" nor "rate".');
2109 end
2110 dist = Exp(lam);
2111 return;
2112 case 'Det'
2113 dist = Det(p.value);
2114 return;
2115 case 'Erlang'
2116 dist = Erlang(p.lambda, p.k);
2117 return;
2118 case 'HyperExp'
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.
2123 pv = p.p(:)';
2124 lv = p.lambda(:)';
2125 if isscalar(pv)
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));
2129 else
2130 if numel(pv) ~= numel(lv)
2131 line_error(mfilename, sprintf(...
2132 'HyperExp has %d probabilities but %d rates.', numel(pv), numel(lv)));
2133 end
2134 dist = HyperExp(pv, lv);
2135 end
2136 return;
2137 case 'Gamma'
2138 dist = Gamma(p.alpha, p.beta);
2139 return;
2140 case 'Lognormal'
2141 dist = Lognormal(p.mu, p.sigma);
2142 return;
2143 case 'Uniform'
2144 dist = Uniform(p.a, p.b);
2145 return;
2146 case 'Zipf'
2147 dist = Zipf(p.s, p.n);
2148 return;
2149 case 'Pareto'
2150 dist = Pareto(p.alpha, p.scale);
2151 return;
2152 case 'Weibull'
2153 % JSON: alpha = scale (getParam(1)), beta = shape (getParam(2))
2154 % Constructor: Weibull(shape, scale)
2155 dist = Weibull(p.beta, p.alpha);
2156 return;
2157 case 'Normal'
2158 dist = Normal(p.mu, p.sigma);
2159 return;
2160 case 'Geometric'
2161 dist = Geometric(p.p);
2162 return;
2163 case 'Binomial'
2164 dist = Binomial(p.n, p.p);
2165 return;
2166 case 'Poisson'
2167 dist = Poisson(p.lambda);
2168 return;
2169 case 'Bernoulli'
2170 dist = Bernoulli(p.p);
2171 return;
2172 case 'DiscreteUniform'
2173 dist = DiscreteUniform(p.min, p.max);
2174 return;
2175 case 'DiscreteSampler'
2176 pv = p.p(:)';
2177 xv = p.x(:)';
2178 dist = DiscreteSampler(pv, xv);
2179 return;
2180 case 'Coxian'
2181 mu = p.mu(:)';
2182 phi = p.phi(:)';
2183 dist = Coxian(mu, phi);
2184 return;
2185 case 'MMPP2'
2186 dist = MMPP2(p.lambda0, p.lambda1, p.sigma0, p.sigma1);
2187 return;
2188 case 'MMDP2'
2189 dist = MMDP2(p.r0, p.r1, p.sigma0, p.sigma1);
2190 return;
2191 case 'NHPP'
2192 % Absent 'cyclic' means cyclic, matching the constructor default.
2193 if isfield(p, 'cyclic')
2194 cyc = logical(p.cyclic);
2195 else
2196 cyc = true;
2197 end
2198 dist = NHPP(p.breakpoints(:)', p.rates(:)', cyc);
2199 return;
2200 case 'ME'
2201 dist = ME(p.alpha(:)', json2mat(p.A));
2202 return;
2203 case 'RAP'
2204 dist = RAP(json2mat(p.H0), json2mat(p.H1));
2205 return;
2206 case 'DMAP'
2207 dist = DMAP(json2mat(p.D0), json2mat(p.D1));
2208 return;
2209 case 'BMAP'
2210 % D = {D0, D1, ..., Dk}, Dk driving batches of size k
2211 dist = BMAP(json2matcell(p.D));
2212 return;
2213 case 'MarkedMMPP'
2214 % D = {D0, D11, ..., D1K}; the ctor rebuilds the aggregate D1 from
2215 % the K == length(D)-1 form
2216 Dcell = json2matcell(p.D);
2217 if isfield(p, 'K')
2218 Kmarks = p.K;
2219 else
2220 Kmarks = numel(Dcell) - 1;
2221 end
2222 dist = MarkedMMPP(Dcell, Kmarks);
2223 return;
2224 case 'EmpiricalCDF'
2225 dist = EmpiricalCDF(p.x(:), p.F(:));
2226 return;
2227 case 'Replayer'
2228 % Try to load from file first
2229 if isfield(p, 'fileName') && exist(p.fileName, 'file') == 2
2230 dist = Replayer(p.fileName);
2231 return;
2232 end
2233 % Fallback to APH if available
2234 if isfield(d, 'ph') && ~isempty(d.ph)
2235 ph = d.ph;
2236 alpha = ph.alpha;
2237 T = ph.T;
2238 if ~isvector(alpha), alpha = alpha(:)'; end
2239 dist = PH(alpha, T);
2240 return;
2241 end
2242 % Fallback to Exp with stored mean
2243 m = 1.0;
2244 if isfield(p, 'mean'), m = p.mean; end
2245 dist = Exp(1.0 / m);
2246 return;
2247 end
2248end
2249
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);
2258 end
2259 alts = cell(1, length(altJsons));
2260 for ai = 1:length(altJsons)
2261 alts{ai} = json2dist(altJsons{ai});
2262 end
2263 probs = probs(:)';
2264 dist = Prior(alts, probs);
2265 return;
2266 end
2267end
2268
2269% PH/APH representation
2270if isfield(d, 'ph') && ~isempty(d.ph)
2271 ph = d.ph;
2272 alpha = ph.alpha;
2273 T = ph.T;
2274 alpha = alpha(:)'; % Ensure row vector (jsondecode returns column vectors)
2275 if strcmp(dtype, 'APH')
2276 dist = APH(alpha, T);
2277 else
2278 dist = PH(alpha, T);
2279 end
2280 return;
2281end
2282
2283% MAP representation
2284if isfield(d, 'map') && ~isempty(d.map)
2285 mapSpec = d.map;
2286 D0 = mapSpec.D0;
2287 D1 = mapSpec.D1;
2288 dist = MAP(D0, D1);
2289 return;
2290end
2291
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)
2295 mmapSpec = d.mmap;
2296 D0 = mmapSpec.D0;
2297 d1k = mmapSpec.D1k;
2298 if isnumeric(d1k)
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);
2303 Dcell{1} = D0;
2304 for km = 1:Kmarks
2305 Dcell{1+km} = squeeze(d1k(km, :, :));
2306 end
2307 else
2308 if ~iscell(d1k), d1k = num2cell(d1k); end
2309 Dcell = [{D0}, d1k(:)'];
2310 end
2311 dist = MarkedMAP(Dcell, numel(Dcell)-1);
2312 return;
2313end
2314
2315% Fit specification
2316if isfield(d, 'fit') && ~isempty(d.fit)
2317 fit = d.fit;
2318 method = fit.method;
2319 switch method
2320 case 'fitMean'
2321 m = fit.mean;
2322 switch dtype
2323 case 'Exp'
2324 dist = Exp(1.0 / m);
2325 case 'Det'
2326 dist = Det(m);
2327 otherwise
2328 dist = Exp(1.0 / m);
2329 end
2330 return;
2331 case 'fitMeanAndSCV'
2332 m = fit.mean;
2333 scv = fit.scv;
2334 switch dtype
2335 case 'Erlang'
2336 dist = Erlang.fitMeanAndSCV(m, scv);
2337 case 'HyperExp'
2338 dist = HyperExp.fitMeanAndSCV(m, scv);
2339 otherwise
2340 dist = Exp(1.0 / m);
2341 end
2342 return;
2343 case 'fitMeanAndOrder'
2344 m = fit.mean;
2345 order = fit.order;
2346 switch dtype
2347 case 'Erlang'
2348 dist = Erlang.fitMeanAndOrder(m, order);
2349 otherwise
2350 dist = Exp(1.0 / m);
2351 end
2352 return;
2353 end
2354end
2355
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
2359% doing so.
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);
2364 return;
2365end
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);
2370 return;
2371end
2372line_error(mfilename, sprintf(['Distribution type "%s" is not supported on load and carries ' ...
2373 'no moments to fit.'], dtype));
2374end
2375
2376
2377% =========================================================================
2378% Routing parser (handles comma keys in JSON)
2379% =========================================================================
2380
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
2385entries = {};
2386
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;
2395end
2396
2397classNames = class_map.keys();
2398
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, '"'];
2405
2406 % Find this key in the raw JSON
2407 pos = strfind(rawJson, keyStr);
2408 if isempty(pos)
2409 continue;
2410 end
2411
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
2416 idx = startPos;
2417 while idx <= length(rawJson) && (rawJson(idx) == ' ' || rawJson(idx) == ':' || rawJson(idx) == newline || rawJson(idx) == char(13) || rawJson(idx) == char(9))
2418 idx = idx + 1;
2419 end
2420 if idx > length(rawJson) || rawJson(idx) ~= '{'
2421 continue;
2422 end
2423 % Extract the JSON object using brace counting
2424 objStr = extract_json_object(rawJson, idx);
2425 if isempty(objStr)
2426 continue;
2427 end
2428 % Parse the from -> to -> prob structure
2429 try
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);
2439 else
2440 fromName = fromField;
2441 end
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);
2447 else
2448 toName = toField;
2449 end
2450 % Verify names exist in the model
2451 if node_map.isKey(fromName) && node_map.isKey(toName)
2452 re = struct();
2453 re.className1 = cn1;
2454 re.className2 = cn2;
2455 re.fromNode = fromName;
2456 re.toNode = toName;
2457 re.prob = prob;
2458 entries{end+1} = re; %#ok<AGROW>
2459 end
2460 end
2461 end
2462 catch
2463 % Skip if parsing fails
2464 end
2465 end
2466 end
2467end
2468end
2469
2470
2471function objStr = extract_json_object(str, startIdx)
2472% Extract a JSON object string starting at startIdx (must be '{').
2473if str(startIdx) ~= '{'
2474 objStr = '';
2475 return;
2476end
2477depth = 0;
2478inString = false;
2479escaped = false;
2480for i = startIdx:length(str)
2481 c = str(i);
2482 if escaped
2483 escaped = false;
2484 continue;
2485 end
2486 if c == '\'
2487 escaped = true;
2488 continue;
2489 end
2490 if c == '"'
2491 inString = ~inString;
2492 continue;
2493 end
2494 if ~inString
2495 if c == '{'
2496 depth = depth + 1;
2497 elseif c == '}'
2498 depth = depth - 1;
2499 if depth == 0
2500 objStr = str(startIdx:i);
2501 return;
2502 end
2503 end
2504 end
2505end
2506objStr = '';
2507end
2508
2509
2510% =========================================================================
2511% Helper functions
2512% =========================================================================
2513
2514function id = str_to_sched_id(str)
2515% Map a wire scheduling enum name to a SchedStrategy numeric ID.
2516%
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.
2522if iscell(str)
2523 str = str{1};
2524end
2525switch upper(str)
2526 case 'RAND'
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;
2530 otherwise
2531 id = SchedStrategy.fromText(lower(char(str)));
2532end
2533end
2534
2535
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;
2542 otherwise
2543 line_error(mfilename, sprintf('Unrecognized departure discipline "%s".', str));
2544end
2545end
2546
2547
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;
2554 otherwise
2555 line_error(mfilename, sprintf('Unrecognized impatience type "%s".', str));
2556end
2557end
2558
2559
2560function id = str_to_repl_id(str)
2561% Map replacement strategy string to ReplacementStrategy numeric ID.
2562switch upper(str)
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;
2570 otherwise
2571 line_error(mfilename, sprintf('Unrecognized replacement strategy "%s".', str));
2572end
2573end
2574
2575
2576function id = str_to_prectype(str)
2577% Map JAR precedence type string to MATLAB ActivityPrecedenceType numeric ID.
2578switch str
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;
2588end
2589end
2590
2591
2592function id = str_to_droprule(str)
2593% Map drop rule string to DropStrategy numeric ID.
2594switch str
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;
2601end
2602end
2603
2604
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
2609% wrapper.
2610if iscell(arr)
2611 m = cell2mat(cellfun(@(r) double(r(:)'), arr(:), 'UniformOutput', false));
2612else
2613 m = double(arr);
2614end
2615end
2616
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.
2621if iscell(arr)
2622 c = cell(1, numel(arr));
2623 for ci = 1:numel(arr)
2624 m = arr{ci};
2625 if iscell(m) % array of row arrays (ragged rows)
2626 c{ci} = cell2mat(cellfun(@(r) r(:)', m(:), 'UniformOutput', false));
2627 else
2628 c{ci} = m;
2629 end
2630 end
2631elseif ndims(arr) == 3
2632 c = cell(1, size(arr, 1));
2633 for ci = 1:size(arr, 1)
2634 c{ci} = squeeze(arr(ci, :, :));
2635 end
2636elseif ismatrix(arr)
2637 c = {arr}; % single matrix
2638else
2639 c = {};
2640end
2641end
2642
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.
2646if ischar(arr)
2647 c = {arr};
2648elseif isstring(arr)
2649 c = cellstr(arr);
2650elseif iscell(arr)
2651 c = arr;
2652else
2653 % jsondecode can return a struct array or char matrix for string arrays
2654 c = cellstr(arr);
2655end
2656end
2657
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.
2661if iscell(c)
2662 v = cell2mat(c(:)');
2663else
2664 v = double(c(:)');
2665end
2666end
2667
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);
2679K = numel(cutoffs);
2680for i = 1:numel(fn)
2681 nm = fn{i};
2682 parts = strsplit(nm(2:end), '_'); % drop the 'x' prefix jsondecode prepends
2683 n = cellfun(@str2double, parts);
2684 if K == 0
2685 K = numel(n);
2686 end
2687 map(cd_state_key(n)) = double(tblStruct.(nm));
2688end
2689muFun = @(c) oi_table_eval(c, map, cutoffs, K);
2690end
2691
2692function rate = oi_table_eval(c, map, cutoffs, K)
2693c = round(double(c(:)'));
2694cnt = zeros(1, K);
2695for i = 1:numel(c)
2696 ci = c(i);
2697 if ci >= 1 && ci <= K
2698 cnt(ci) = cnt(ci) + 1;
2699 end
2700end
2701if ~isempty(cutoffs)
2702 m = min(numel(cnt), numel(cutoffs));
2703 cnt(1:m) = min(cnt(1:m), cutoffs(1:m));
2704end
2705if sum(cnt) == 0
2706 rate = 0; % the empty state is omitted from the table: an idle queue
2707 return;
2708end
2709k = cd_state_key(cnt);
2710if isKey(map, k)
2711 rate = map(k);
2712else
2713 rate = 0;
2714end
2715end
2716
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);
2726for i = 1:numel(fn)
2727 nm = fn{i};
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(:)';
2732end
2733beta = @(ni) cd_table_eval(ni, map, cutoffs);
2734end
2735
2736function k = cd_state_key(n)
2737k = strjoin(arrayfun(@(x) sprintf('%d', x), n(:)', 'UniformOutput', false), ',');
2738end
2739
2740function v = cd_table_eval(ni, map, cutoffs)
2741n = round(double(ni(:)'));
2742n(n < 0) = 0;
2743if ~isempty(cutoffs)
2744 m = min(numel(n), numel(cutoffs));
2745 n(1:m) = min(n(1:m), cutoffs(1:m));
2746end
2747k = cd_state_key(n);
2748if isKey(map, k)
2749 v = map(k);
2750else
2751 v = 1; % a state absent from the table is neutral (no scaling)
2752end
2753end
Definition Station.m:287
Definition fjtag.m:157
Definition Station.m:245