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%
8% Parameters:
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'
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 'ClosedClass "%s" has no valid refNode.', cname);
136 end
137 jc = ClosedClass(model, cname, pop, refNode, prio);
138 case 'Signal'
139 prio = 0;
140 if isfield(cd, 'priority'), prio = cd.priority; end
141 sigType = SignalType.NEGATIVE;
142 if isfield(cd, 'signalType')
143 sigType = SignalType.fromText(cd.signalType);
144 end
145 openOrClosed = 'Open';
146 if isfield(cd, 'openOrClosed')
147 openOrClosed = cd.openOrClosed;
148 end
149 if strcmp(openOrClosed, 'Closed')
150 refNode = [];
151 if isfield(cd, 'refNode') && node_map.isKey(cd.refNode)
152 refNode = node_map(cd.refNode);
153 end
154 if isempty(refNode)
155 error('linemodel_load:noRefNode', ...
156 'ClosedSignal "%s" has no valid refNode.', cname);
157 end
158 jc = ClosedSignal(model, cname, sigType, refNode, prio);
159 else
160 jc = OpenSignal(model, cname, sigType, prio);
161 end
162 % Removal distribution
163 if isfield(cd, 'removalDistribution')
164 remDist = json2dist(cd.removalDistribution);
165 if ~isempty(remDist)
166 jc.setRemovalDistribution(remDist);
167 end
168 end
169 % Removal policy
170 if isfield(cd, 'removalPolicy')
171 jc.setRemovalPolicy(RemovalPolicy.fromText(cd.removalPolicy));
172 end
173 otherwise
174 jc = OpenClass(model, cname);
175 end
176 if isfield(cd, 'deadline') && isfinite(cd.deadline)
177 jc.deadline = cd.deadline;
178 end
179 classesList{end+1} = jc; %#ok<AGROW>
180 end
181end
182class_map = containers.Map();
183for i = 1:length(classesList)
184 class_map(classesList{i}.name) = classesList{i};
185end
186
187% --- Resolve signal targetClass associations ---
188if isfield(data, 'classes')
189 cls2 = data.classes;
190 if isstruct(cls2), cls2 = num2cell(cls2); end
191 for i = 1:length(cls2)
192 cd2 = cls2{i};
193 if isfield(cd2, 'type') && strcmp(cd2.type, 'Signal') && isfield(cd2, 'targetClass')
194 if class_map.isKey(cd2.name) && class_map.isKey(cd2.targetClass)
195 sigCls = class_map(cd2.name);
196 sigCls.forJobClass(class_map(cd2.targetClass));
197 end
198 end
199 end
200end
201
202% --- Set service/arrival distributions ---
203if isfield(data, 'nodes')
204 nds = data.nodes;
205 if isstruct(nds)
206 nds = num2cell(nds);
207 end
208 for i = 1:length(nds)
209 nd = nds{i};
210 nd_name = nd.name;
211 node = node_map(nd_name);
212
213 if isfield(nd, 'service') && ~isempty(nd.service)
214 svc = nd.service;
215 svcFields = fieldnames(svc);
216 for f = 1:length(svcFields)
217 cname = svcFields{f};
218 distJson = svc.(cname);
219 if ~class_map.isKey(cname)
220 continue;
221 end
222 jc = class_map(cname);
223 dist = json2dist(distJson);
224 if ~isempty(dist)
225 if isa(node, 'Source')
226 node.setArrival(jc, dist);
227 elseif isa(node, 'Queue') || isa(node, 'Delay')
228 % Pass DPS/GPS weight if available
229 weight = 1;
230 if isfield(nd, 'schedParams')
231 sp_tmp = nd.schedParams;
232 if isfield(sp_tmp, cname)
233 weight = sp_tmp.(cname);
234 end
235 end
236 node.setService(jc, dist, weight);
237 end
238 end
239 end
240 end
241
242 % ClassSwitch matrix (dict format: classSwitchMatrix)
243 if isfield(nd, 'classSwitchMatrix') && isa(node, 'ClassSwitch')
244 csm_data = nd.classSwitchMatrix;
245 classes = model.getClasses();
246 K = length(classes);
247 mat = zeros(K);
248 class_idx = containers.Map();
249 for ci = 1:K
250 class_idx(classes{ci}.name) = ci;
251 end
252 fromFields = fieldnames(csm_data);
253 for fi = 1:length(fromFields)
254 fromName = fromFields{fi};
255 if ~class_idx.isKey(fromName), continue; end
256 ri = class_idx(fromName);
257 toStruct = csm_data.(fromName);
258 toFields = fieldnames(toStruct);
259 for ti = 1:length(toFields)
260 toName = toFields{ti};
261 if ~class_idx.isKey(toName), continue; end
262 ci = class_idx(toName);
263 mat(ri, ci) = toStruct.(toName);
264 end
265 end
266 node.server = node.server.updateClassSwitch(mat);
267 % Legacy 2D array format: csMatrix (from older JAR saves)
268 elseif isfield(nd, 'csMatrix') && isa(node, 'ClassSwitch')
269 mat = nd.csMatrix;
270 if iscell(mat)
271 mat = cell2mat(mat);
272 end
273 node.server = node.server.updateClassSwitch(mat);
274 end
275
276 % DPS scheduling parameters (weights already passed via setService above)
277
278 % Per-class buffer capacity
279 if isfield(nd, 'classCap') && isa(node, 'Queue')
280 ccData = nd.classCap;
281 ccFields = fieldnames(ccData);
282 for cci = 1:length(ccFields)
283 cname = ccFields{cci};
284 if class_map.isKey(cname)
285 jc = class_map(cname);
286 % Find class index
287 cls = model.getClasses();
288 for ci = 1:length(cls)
289 if strcmp(cls{ci}.name, cname)
290 node.classCap(ci) = ccData.(cname);
291 break;
292 end
293 end
294 end
295 end
296 end
297
298 % Drop rules
299 if isfield(nd, 'dropRule') && isa(node, 'Queue')
300 drData = nd.dropRule;
301 drFields = fieldnames(drData);
302 for dri = 1:length(drFields)
303 cname = drFields{dri};
304 if class_map.isKey(cname)
305 cls = model.getClasses();
306 for ci = 1:length(cls)
307 if strcmp(cls{ci}.name, cname)
308 node.dropRule(ci) = str_to_droprule(drData.(cname));
309 break;
310 end
311 end
312 end
313 end
314 end
315
316 % Load-dependent scaling
317 if isfield(nd, 'loadDependence') && isa(node, 'Queue')
318 ld = nd.loadDependence;
319 if isfield(ld, 'type') && strcmp(ld.type, 'loadDependent') && isfield(ld, 'scaling')
320 scaling = ld.scaling(:)';
321 node.setLoadDependence(scaling);
322 end
323 end
324
325 % Join strategy and quorum
326 if isa(node, 'Join')
327 if isfield(nd, 'joinStrategy')
328 jsStr = nd.joinStrategy;
329 classes = model.getClasses();
330 for ci = 1:length(classes)
331 switch jsStr
332 case 'STD'
333 node.input.setStrategy(classes{ci}, JoinStrategy.STD);
334 case {'PARTIAL', 'QUORUM', 'Quorum'}
335 node.input.setStrategy(classes{ci}, JoinStrategy.PARTIAL);
336 end
337 end
338 end
339 if isfield(nd, 'joinQuorum')
340 jq = nd.joinQuorum;
341 classes = model.getClasses();
342 for ci = 1:length(classes)
343 node.input.setRequired(classes{ci}, jq);
344 end
345 end
346 end
347
348 % Cache hit/miss class mappings and popularity distributions
349 if isa(node, 'Cache') && isfield(nd, 'cache')
350 cc = nd.cache;
351 % Hit class mapping
352 if isfield(cc, 'hitClass')
353 hcData = cc.hitClass;
354 hcFields = fieldnames(hcData);
355 for hci = 1:length(hcFields)
356 inName = hcFields{hci};
357 outName = hcData.(inName);
358 if class_map.isKey(inName) && class_map.isKey(outName)
359 node.setHitClass(class_map(inName), class_map(outName));
360 end
361 end
362 end
363 % Miss class mapping
364 if isfield(cc, 'missClass')
365 mcData = cc.missClass;
366 mcFields = fieldnames(mcData);
367 for mci = 1:length(mcFields)
368 inName = mcFields{mci};
369 outName = mcData.(inName);
370 if class_map.isKey(inName) && class_map.isKey(outName)
371 node.setMissClass(class_map(inName), class_map(outName));
372 end
373 end
374 end
375 % Popularity distributions (setRead)
376 if isfield(cc, 'popularity')
377 popData = cc.popularity;
378 popFields = fieldnames(popData);
379 for pfi = 1:length(popFields)
380 cname = popFields{pfi};
381 if class_map.isKey(cname)
382 popDist = json2dist(popData.(cname));
383 if ~isempty(popDist) && ~isa(popDist, 'Disabled')
384 node.setRead(class_map(cname), popDist);
385 end
386 end
387 end
388 end
389 end
390 % Heterogeneous server types
391 if isa(node, 'Queue') && isfield(nd, 'serverTypes')
392 stArr = nd.serverTypes;
393 if isstruct(stArr), stArr = num2cell(stArr); end
394 for si = 1:length(stArr)
395 stData = stArr{si};
396 stName = stData.name;
397 stCount = stData.count;
398 st = ServerType(stName, stCount);
399 % Compatible classes
400 if isfield(stData, 'compatibleClasses')
401 ccList = stData.compatibleClasses;
402 if ~iscell(ccList), ccList = {ccList}; end
403 for cci = 1:length(ccList)
404 if class_map.isKey(ccList{cci})
405 st.addCompatible(class_map(ccList{cci}));
406 end
407 end
408 end
409 node.addServerType(st);
410 % Per-class service distributions
411 if isfield(stData, 'service')
412 svcData = stData.service;
413 svcFields = fieldnames(svcData);
414 for fi = 1:length(svcFields)
415 cname = svcFields{fi};
416 if class_map.isKey(cname)
417 jc = class_map(cname);
418 dist = json2dist(svcData.(cname));
419 if ~isempty(dist)
420 node.setHeteroService(jc, st, dist);
421 end
422 end
423 end
424 end
425 end
426 % Scheduling policy
427 if isfield(nd, 'heteroSchedPolicy')
428 policy = HeteroSchedPolicy.fromText(nd.heteroSchedPolicy);
429 node.setHeteroSchedPolicy(policy);
430 end
431 end
432 end
433end
434
435% --- Restore Balking, Retrial, Patience ---
436if isfield(data, 'nodes')
437 ndsImp = data.nodes;
438 if isstruct(ndsImp), ndsImp = num2cell(ndsImp); end
439 for i = 1:length(ndsImp)
440 ndImp = ndsImp{i};
441 if ~node_map.isKey(ndImp.name), continue; end
442 node = node_map(ndImp.name);
443 if ~isa(node, 'Queue'), continue; end
444 % Balking
445 if isfield(ndImp, 'balking') && ~isempty(ndImp.balking)
446 balkData = ndImp.balking;
447 fnames = fieldnames(balkData);
448 for fi = 1:length(fnames)
449 className = fnames{fi};
450 if ~class_map.isKey(className), continue; end
451 jc = class_map(className);
452 bjc = balkData.(className);
453 % Parse strategy
454 switch bjc.strategy
455 case 'QUEUE_LENGTH', strategy = BalkingStrategy.QUEUE_LENGTH;
456 case 'EXPECTED_WAIT', strategy = BalkingStrategy.EXPECTED_WAIT;
457 case 'COMBINED', strategy = BalkingStrategy.COMBINED;
458 otherwise, continue;
459 end
460 % Parse thresholds
461 thData = bjc.thresholds;
462 if isstruct(thData), thData = num2cell(thData); end
463 thresholds = {};
464 for ti = 1:length(thData)
465 td = thData{ti};
466 maxJobs = td.maxJobs;
467 if maxJobs < 0, maxJobs = Inf; end
468 thresholds{end+1} = {td.minJobs, maxJobs, td.probability};
469 end
470 node.setBalking(jc, strategy, thresholds);
471 end
472 end
473 % Retrial
474 if isfield(ndImp, 'retrial') && ~isempty(ndImp.retrial)
475 retData = ndImp.retrial;
476 fnames = fieldnames(retData);
477 for fi = 1:length(fnames)
478 className = fnames{fi};
479 if ~class_map.isKey(className), continue; end
480 jc = class_map(className);
481 rjc = retData.(className);
482 delayDist = json2dist(rjc.delay);
483 maxAttempts = -1;
484 if isfield(rjc, 'maxAttempts')
485 maxAttempts = rjc.maxAttempts;
486 end
487 node.setRetrial(jc, delayDist, maxAttempts);
488 end
489 end
490 % Patience
491 if isfield(ndImp, 'patience') && ~isempty(ndImp.patience)
492 patData = ndImp.patience;
493 fnames = fieldnames(patData);
494 for fi = 1:length(fnames)
495 className = fnames{fi};
496 if ~class_map.isKey(className), continue; end
497 jc = class_map(className);
498 pjc = patData.(className);
499 patDist = json2dist(pjc.distribution);
500 if isfield(pjc, 'impatienceType')
501 switch pjc.impatienceType
502 case 'reneging', impType = ImpatienceType.RENEGING;
503 case 'balking', impType = ImpatienceType.BALKING;
504 case 'retrial', impType = ImpatienceType.RETRIAL;
505 otherwise, impType = ImpatienceType.RENEGING;
506 end
507 else
508 impType = ImpatienceType.RENEGING;
509 end
510 node.setPatience(jc, impType, patDist);
511 end
512 end
513 end
514end
515
516% --- Configure Transition modes ---
517if isfield(data, 'nodes')
518 nds3 = data.nodes;
519 if isstruct(nds3)
520 nds3 = num2cell(nds3);
521 end
522 for i = 1:length(nds3)
523 nd3 = nds3{i};
524 if ~isfield(nd3, 'modes'), continue; end
525 if ~strcmp(nd3.type, 'Transition'), continue; end
526 tnode = node_map(nd3.name);
527 modesData = nd3.modes;
528 if isstruct(modesData)
529 modesData = num2cell(modesData);
530 end
531 for mi = 1:length(modesData)
532 md = modesData{mi};
533 modeName = 'Mode';
534 if isfield(md, 'name'), modeName = md.name; end
535 mode = tnode.addMode(modeName);
536 % Distribution
537 if isfield(md, 'distribution') && ~isempty(md.distribution)
538 dist = json2dist(md.distribution);
539 if ~isempty(dist)
540 tnode.setDistribution(mode, dist);
541 end
542 end
543 % Timing strategy
544 if isfield(md, 'timingStrategy')
545 if strcmp(md.timingStrategy, 'IMMEDIATE')
546 tnode.setTimingStrategy(mode, TimingStrategy.IMMEDIATE);
547 else
548 tnode.setTimingStrategy(mode, TimingStrategy.TIMED);
549 end
550 end
551 % Number of servers
552 if isfield(md, 'numServers')
553 nsVal = md.numServers;
554 if ischar(nsVal) || isstring(nsVal)
555 if strcmpi(nsVal, 'Infinity'), nsVal = Inf; else, nsVal = str2double(nsVal); end
556 end
557 if nsVal > 1
558 tnode.setNumberOfServers(mode, nsVal);
559 end
560 end
561 % Firing priority
562 if isfield(md, 'firingPriority')
563 tnode.setFiringPriorities(mode, md.firingPriority);
564 end
565 % Firing weight
566 if isfield(md, 'firingWeight')
567 tnode.setFiringWeights(mode, md.firingWeight);
568 end
569 % Enabling conditions
570 if isfield(md, 'enablingConditions')
571 ecList = md.enablingConditions;
572 if isstruct(ecList), ecList = num2cell(ecList); end
573 for ei = 1:length(ecList)
574 ec = ecList{ei};
575 if node_map.isKey(ec.node) && class_map.isKey(ec.class)
576 tnode.setEnablingConditions(mode, class_map(ec.class), node_map(ec.node), ec.count);
577 end
578 end
579 end
580 % Inhibiting conditions
581 if isfield(md, 'inhibitingConditions')
582 icList = md.inhibitingConditions;
583 if isstruct(icList), icList = num2cell(icList); end
584 for ii = 1:length(icList)
585 ic = icList{ii};
586 if node_map.isKey(ic.node) && class_map.isKey(ic.class)
587 tnode.setInhibitingConditions(mode, class_map(ic.class), node_map(ic.node), ic.count);
588 end
589 end
590 end
591 % Firing outcomes
592 if isfield(md, 'firingOutcomes')
593 foList = md.firingOutcomes;
594 if isstruct(foList), foList = num2cell(foList); end
595 for fi = 1:length(foList)
596 fo = foList{fi};
597 if node_map.isKey(fo.node) && class_map.isKey(fo.class)
598 tnode.setFiringOutcome(mode, class_map(fo.class), node_map(fo.node), fo.count);
599 end
600 end
601 end
602 end
603 end
604end
605
606% --- Restore initial state for Place nodes ---
607if isfield(data, 'nodes')
608 nds4 = data.nodes;
609 if isstruct(nds4), nds4 = num2cell(nds4); end
610 for i = 1:length(nds4)
611 nd4 = nds4{i};
612 if isfield(nd4, 'initialState') && node_map.isKey(nd4.name)
613 nodeObj = node_map(nd4.name);
614 if isa(nodeObj, 'Place')
615 stVal = nd4.initialState;
616 stVal = stVal(:)'; % Ensure row vector (jsondecode returns column vectors)
617 nodeObj.setState(stVal);
618 end
619 end
620 end
621end
622
623% --- Build routing ---
624if isfield(data, 'routing') && isfield(data.routing, 'type') && strcmp(data.routing.type, 'matrix')
625 P = model.initRoutingMatrix();
626 K = length(classesList);
627 M = length(nodeList);
628
629 % Build node/class index maps
630 nodeIdx = containers.Map();
631 for i = 1:M
632 nodeIdx(nodeList{i}.name) = i;
633 end
634 classIdx = containers.Map();
635 for i = 1:K
636 classIdx(classesList{i}.name) = i;
637 end
638
639 % Parse routing keys from raw JSON to preserve commas
640 routingEntries = parse_routing_keys(rawJson, class_map, node_map);
641
642 for e = 1:length(routingEntries)
643 re = routingEntries{e};
644 r = classIdx(re.className1);
645 s = classIdx(re.className2);
646 ii = nodeIdx(re.fromNode);
647 jj = nodeIdx(re.toNode);
648 P{r,s}(ii, jj) = re.prob;
649 end
650
651 model.link(P);
652end
653
654% --- Restore routing strategies ---
655if isfield(data, 'routingStrategies')
656 stratMap = containers.Map();
657 stratMap('RAND') = RoutingStrategy.RAND;
658 stratMap('RROBIN') = RoutingStrategy.RROBIN;
659 stratMap('WRROBIN') = RoutingStrategy.WRROBIN;
660 stratMap('JSQ') = RoutingStrategy.JSQ;
661 stratMap('KCHOICES') = RoutingStrategy.KCHOICES;
662 stratMap('FIRING') = RoutingStrategy.FIRING;
663 stratMap('RL') = RoutingStrategy.RL;
664 stratMap('DISABLED') = RoutingStrategy.DISABLED;
665
666 rsFields = fieldnames(data.routingStrategies);
667 for fi = 1:length(rsFields)
668 nodeName = rsFields{fi};
669 if node_map.isKey(nodeName)
670 nodeObj = node_map(nodeName);
671 classStrats = data.routingStrategies.(nodeName);
672 csFields = fieldnames(classStrats);
673 for ci = 1:length(csFields)
674 className = csFields{ci};
675 stratName = classStrats.(className);
676 if class_map.isKey(className) && stratMap.isKey(stratName)
677 % Skip RAND, PROB: already handled by routing matrix
678 % Skip WRROBIN: handled separately in routingWeights section
679 rs = stratMap(stratName);
680 if rs ~= RoutingStrategy.RAND && rs ~= RoutingStrategy.PROB && rs ~= RoutingStrategy.WRROBIN
681 nodeObj.setRouting(class_map(className), rs);
682 end
683 end
684 end
685 end
686 end
687end
688
689% --- Restore routing weights (WRROBIN) ---
690if isfield(data, 'routingWeights')
691 rwFields = fieldnames(data.routingWeights);
692 for fi = 1:length(rwFields)
693 nodeName = rwFields{fi};
694 if node_map.isKey(nodeName)
695 nodeObj = node_map(nodeName);
696 classWeights = data.routingWeights.(nodeName);
697 cwFields = fieldnames(classWeights);
698 for ci = 1:length(cwFields)
699 className = cwFields{ci};
700 destWeights = classWeights.(className);
701 if class_map.isKey(className)
702 % Clear existing routing entries for this class
703 % (link() may have set PROB entries that would accumulate)
704 classIdx = class_map(className).index;
705 if length(nodeObj.output.outputStrategy) >= classIdx && ...
706 length(nodeObj.output.outputStrategy{1, classIdx}) >= 3
707 nodeObj.output.outputStrategy{1, classIdx}{3} = {};
708 end
709 dwFields = fieldnames(destWeights);
710 for di = 1:length(dwFields)
711 destName = dwFields{di};
712 weight = destWeights.(destName);
713 if node_map.isKey(destName)
714 nodeObj.setRouting(class_map(className), RoutingStrategy.WRROBIN, node_map(destName), weight);
715 end
716 end
717 end
718 end
719 end
720 end
721end
722
723% --- Restore switchover times ---
724for ni = 1:length(data.nodes)
725 nd = data.nodes(ni);
726 if isfield(nd, 'switchoverTimes') && ~isempty(nd.switchoverTimes)
727 nodeObj = node_map(nd.name);
728 soArr = nd.switchoverTimes;
729 if ~iscell(soArr)
730 soArr = {soArr};
731 end
732 for si = 1:length(soArr)
733 so = soArr{si};
734 fromCls = class_map(so.from);
735 toCls = class_map(so.to);
736 dist = json2dist(so.distribution);
737 if ~isempty(dist)
738 nodeObj.setSwitchover(fromCls, toCls, dist);
739 end
740 end
741 end
742end
743
744% --- Restore finite capacity regions ---
745if isfield(data, 'finiteCapacityRegions')
746 fcrArr = data.finiteCapacityRegions;
747 if ~iscell(fcrArr)
748 fcrArr = {fcrArr};
749 end
750 classes = model.getClasses();
751 for ri = 1:length(fcrArr)
752 rj = fcrArr{ri};
753 regNodes = {};
754 % Support both old format ("nodes" list) and new format ("stations" array)
755 if isfield(rj, 'stations')
756 stArr = rj.stations;
757 if ~iscell(stArr), stArr = {stArr}; end
758 for si = 1:length(stArr)
759 sj = stArr{si};
760 nodeName = sj.node;
761 if node_map.isKey(nodeName)
762 regNodes{end+1} = node_map(nodeName); %#ok<AGROW>
763 end
764 end
765 elseif isfield(rj, 'nodes')
766 nodeNames = rj.nodes;
767 if ~iscell(nodeNames), nodeNames = {nodeNames}; end
768 for ni = 1:length(nodeNames)
769 if node_map.isKey(nodeNames{ni})
770 regNodes{end+1} = node_map(nodeNames{ni}); %#ok<AGROW>
771 end
772 end
773 end
774 maxJobs = FiniteCapacityRegion.UNBOUNDED;
775 if isfield(rj, 'globalMaxJobs')
776 maxJobs = rj.globalMaxJobs;
777 end
778 if ~isempty(regNodes)
779 try
780 region = model.addRegion(regNodes);
781 if isfield(rj, 'name') && ~isempty(rj.name)
782 region.setName(rj.name);
783 end
784 if maxJobs ~= FiniteCapacityRegion.UNBOUNDED
785 region.setGlobalMaxJobs(maxJobs);
786 end
787 % globalMaxMemory
788 if isfield(rj, 'globalMaxMemory')
789 region.globalMaxMemory = rj.globalMaxMemory;
790 end
791 % classMaxJobs
792 if isfield(rj, 'classMaxJobs')
793 cmj = rj.classMaxJobs;
794 cmjFields = fieldnames(cmj);
795 for ci = 1:length(cmjFields)
796 cname = cmjFields{ci};
797 if class_map.isKey(cname)
798 jc = class_map(cname);
799 region.classMaxJobs(jc.index) = cmj.(cname);
800 end
801 end
802 end
803 % dropRule
804 if isfield(rj, 'dropRule')
805 drData = rj.dropRule;
806 drFields = fieldnames(drData);
807 for di = 1:length(drFields)
808 cname = drFields{di};
809 if class_map.isKey(cname)
810 jc = class_map(cname);
811 region.dropRule(jc.index) = str_to_droprule(drData.(cname));
812 end
813 end
814 end
815 % Per-station classWeight and classSize from stations array
816 if isfield(rj, 'stations')
817 stArr2 = rj.stations;
818 if ~iscell(stArr2), stArr2 = {stArr2}; end
819 for si = 1:length(stArr2)
820 sj2 = stArr2{si};
821 if isfield(sj2, 'classWeight')
822 cwData = sj2.classWeight;
823 cwFields = fieldnames(cwData);
824 for ci = 1:length(cwFields)
825 cname = cwFields{ci};
826 if class_map.isKey(cname)
827 jc = class_map(cname);
828 region.classWeight(jc.index) = cwData.(cname);
829 end
830 end
831 end
832 if isfield(sj2, 'classSize')
833 csData = sj2.classSize;
834 csFields = fieldnames(csData);
835 for ci = 1:length(csFields)
836 cname = csFields{ci};
837 if class_map.isKey(cname)
838 jc = class_map(cname);
839 region.classSize(jc.index) = csData.(cname);
840 end
841 end
842 end
843 end
844 end
845 % Linear constraints A * x <= b
846 if isfield(rj, 'constraintA') && isfield(rj, 'constraintB')
847 Adata = rj.constraintA;
848 bdata = rj.constraintB;
849 if iscell(Adata)
850 nrows = length(Adata);
851 K = length(region.classes);
852 A = zeros(nrows, K);
853 for ar = 1:nrows
854 row = Adata{ar};
855 A(ar, 1:length(row)) = row(:)';
856 end
857 else
858 A = Adata;
859 end
860 region.setConstraint(A, bdata(:));
861 end
862 catch
863 end
864 end
865 end
866end
867end
868
869
870function node = create_node(model, nd, name, ntype)
871% Create a node from JSON data.
872switch ntype
873 case 'Source'
874 node = Source(model, name);
875 case 'Sink'
876 node = Sink(model, name);
877 case 'Delay'
878 node = Delay(model, name);
879 case 'Queue'
880 schedStr = 'FCFS';
881 if isfield(nd, 'scheduling')
882 schedStr = nd.scheduling;
883 end
884 schedId = str_to_sched_id(schedStr);
885 node = Queue(model, name, schedId);
886 if isfield(nd, 'servers') && nd.servers > 1
887 node.setNumberOfServers(nd.servers);
888 end
889 if isfield(nd, 'buffer') && isfinite(nd.buffer)
890 node.cap = nd.buffer;
891 end
892 case 'Fork'
893 node = Fork(model, name);
894 case 'Join'
895 node = Join(model, name);
896 case 'Router'
897 node = Router(model, name);
898 case 'ClassSwitch'
899 node = ClassSwitch(model, name);
900 case 'Cache'
901 cc = struct();
902 if isfield(nd, 'cache')
903 cc = nd.cache;
904 end
905 nitems = 10;
906 if isfield(cc, 'items'), nitems = cc.items; end
907 cap = 1;
908 if isfield(cc, 'capacity'), cap = cc.capacity; end
909 replStr = 'LRU';
910 if isfield(cc, 'replacement'), replStr = cc.replacement; end
911 replId = str_to_repl_id(replStr);
912 node = Cache(model, name, nitems, cap, replId);
913 case 'Place'
914 node = Place(model, name);
915 case 'Transition'
916 node = Transition(model, name);
917 otherwise
918 node = Queue(model, name, SchedStrategy.FCFS);
919end
920end
921
922
923% =========================================================================
924% LayeredNetwork deserialization
925% =========================================================================
926
927function model = json2layered(data)
928% Reconstruct a LayeredNetwork from decoded JSON struct.
929
930modelName = 'model';
931if isfield(data, 'name')
932 modelName = data.name;
933end
934model = LayeredNetwork(modelName);
935
936% --- Processors (Python schema: "processors", JAR schema: "hosts") ---
937proc_map = containers.Map();
938if isfield(data, 'processors')
939 procs = data.processors;
940elseif isfield(data, 'hosts')
941 procs = data.hosts;
942else
943 procs = [];
944end
945if ~isempty(procs)
946 if isstruct(procs), procs = num2cell(procs); end
947 for i = 1:length(procs)
948 pd = procs{i};
949 pname = pd.name;
950 mult = 1;
951 if isfield(pd, 'multiplicity'), mult = pd.multiplicity; end
952 schedStr = 'INF';
953 if isfield(pd, 'scheduling'), schedStr = pd.scheduling; end
954 schedId = str_to_sched_id(schedStr);
955 quantum = 0.001;
956 if isfield(pd, 'quantum'), quantum = pd.quantum; end
957 sf = 1.0;
958 if isfield(pd, 'speedFactor'), sf = pd.speedFactor; end
959 proc = Host(model, pname, mult, schedId, quantum, sf);
960 if isfield(pd, 'replication') && pd.replication > 1
961 proc.setReplication(pd.replication);
962 end
963 proc_map(pname) = proc;
964 end
965end
966
967% --- Tasks ---
968task_map = containers.Map();
969if isfield(data, 'tasks')
970 tsks = data.tasks;
971 if isstruct(tsks), tsks = num2cell(tsks); end
972 for i = 1:length(tsks)
973 td = tsks{i};
974 tname = td.name;
975 mult = 1;
976 if isfield(td, 'multiplicity'), mult = td.multiplicity; end
977 schedStr = 'INF';
978 if isfield(td, 'scheduling'), schedStr = td.scheduling; end
979 schedId = str_to_sched_id(schedStr);
980 taskType = 'Task';
981 if isfield(td, 'taskType'), taskType = td.taskType; end
982 if strcmp(taskType, 'FunctionTask')
983 task = FunctionTask(model, tname, mult, schedId);
984 elseif strcmp(taskType, 'CacheTask')
985 totalItems = 1;
986 if isfield(td, 'totalItems'), totalItems = td.totalItems; end
987 cacheCap = 1;
988 if isfield(td, 'cacheCapacity'), cacheCap = td.cacheCapacity; end
989 rsStr = 'FIFO';
990 if isfield(td, 'replacementStrategy'), rsStr = td.replacementStrategy; end
991 rsMap = containers.Map({'RR','FIFO','SFIFO','LRU'}, ...
992 {ReplacementStrategy.RR, ReplacementStrategy.FIFO, ...
993 ReplacementStrategy.SFIFO, ReplacementStrategy.LRU});
994 if rsMap.isKey(upper(rsStr))
995 rs = rsMap(upper(rsStr));
996 else
997 rs = ReplacementStrategy.FIFO;
998 end
999 task = CacheTask(model, tname, totalItems, cacheCap, rs, mult, schedId);
1000 else
1001 task = Task(model, tname, mult, schedId);
1002 end
1003 % Assign to processor (Python schema: "processor", JAR schema: "host")
1004 procRef = '';
1005 if isfield(td, 'processor'), procRef = td.processor;
1006 elseif isfield(td, 'host'), procRef = td.host;
1007 end
1008 if ~isempty(procRef) && proc_map.isKey(procRef)
1009 task.on(proc_map(procRef));
1010 end
1011 % Think time (Python schema: "thinkTime" as dist, JAR schema: "thinkTimeMean"/"thinkTimeSCV")
1012 if isfield(td, 'thinkTime')
1013 dist = json2dist(td.thinkTime);
1014 if ~isempty(dist)
1015 task.setThinkTime(dist);
1016 end
1017 elseif isfield(td, 'thinkTimeMean') && td.thinkTimeMean > 0
1018 task.setThinkTime(Exp(1.0 / td.thinkTimeMean));
1019 end
1020 % Setup time
1021 if isfield(td, 'setupTime')
1022 dist = json2dist(td.setupTime);
1023 if ~isempty(dist)
1024 task.setSetupTime(dist);
1025 end
1026 elseif isfield(td, 'setupTimeMean') && td.setupTimeMean > 1e-8
1027 task.setSetupTime(Exp(1.0 / td.setupTimeMean));
1028 end
1029 % Delay-off time
1030 if isfield(td, 'delayOffTime')
1031 dist = json2dist(td.delayOffTime);
1032 if ~isempty(dist)
1033 task.setDelayOffTime(dist);
1034 end
1035 elseif isfield(td, 'delayOffTimeMean') && td.delayOffTimeMean > 1e-8
1036 task.setDelayOffTime(Exp(1.0 / td.delayOffTimeMean));
1037 end
1038 % Fan in
1039 if isfield(td, 'fanIn') && isstruct(td.fanIn)
1040 fnames = fieldnames(td.fanIn);
1041 for fi = 1:length(fnames)
1042 task.setFanIn(fnames{fi}, td.fanIn.(fnames{fi}));
1043 end
1044 end
1045 % Fan out
1046 if isfield(td, 'fanOut') && isstruct(td.fanOut)
1047 fnames = fieldnames(td.fanOut);
1048 for fi = 1:length(fnames)
1049 task.setFanOut(fnames{fi}, td.fanOut.(fnames{fi}));
1050 end
1051 end
1052 % Replication
1053 if isfield(td, 'replication') && td.replication > 1
1054 task.setReplication(td.replication);
1055 end
1056 task_map(tname) = task;
1057 end
1058end
1059
1060% --- Entries ---
1061entry_map = containers.Map();
1062if isfield(data, 'entries')
1063 ents = data.entries;
1064 if isstruct(ents), ents = num2cell(ents); end
1065 for i = 1:length(ents)
1066 ed = ents{i};
1067 ename = ed.name;
1068 entryType = 'Entry';
1069 if isfield(ed, 'entryType'), entryType = ed.entryType; end
1070 if strcmp(entryType, 'ItemEntry')
1071 totalItems = 1;
1072 if isfield(ed, 'totalItems'), totalItems = ed.totalItems; end
1073 accessProb = [];
1074 if isfield(ed, 'accessProb')
1075 ap = ed.accessProb;
1076 if isstruct(ap)
1077 accessProb = json2dist(ap);
1078 elseif isnumeric(ap)
1079 accessProb = DiscreteSampler(ap);
1080 end
1081 end
1082 if isempty(accessProb)
1083 % Default uniform distribution
1084 accessProb = DiscreteSampler(ones(1, totalItems) / totalItems);
1085 end
1086 entry = ItemEntry(model, ename, totalItems, accessProb);
1087 else
1088 entry = Entry(model, ename);
1089 end
1090 if isfield(ed, 'task') && task_map.isKey(ed.task)
1091 entry.on(task_map(ed.task));
1092 end
1093 % Entry arrival distribution
1094 if isfield(ed, 'arrival')
1095 dist = json2dist(ed.arrival);
1096 if ~isempty(dist)
1097 entry.setArrival(dist);
1098 end
1099 end
1100 entry_map(ename) = entry;
1101 end
1102end
1103
1104% --- Activities ---
1105act_map = containers.Map();
1106if isfield(data, 'activities')
1107 acts = data.activities;
1108 if isstruct(acts), acts = num2cell(acts); end
1109 for i = 1:length(acts)
1110 ad = acts{i};
1111 aname = ad.name;
1112
1113 % Host demand
1114 hd = GlobalConstants.FineTol;
1115 if isfield(ad, 'hostDemand')
1116 hdDist = json2dist(ad.hostDemand);
1117 if ~isempty(hdDist)
1118 hd = hdDist;
1119 end
1120 end
1121
1122 % Bound to entry (Python schema: "boundTo", JAR schema: "boundToEntry")
1123 bte = '';
1124 if isfield(ad, 'boundTo')
1125 bte = ad.boundTo;
1126 elseif isfield(ad, 'boundToEntry')
1127 bte = ad.boundToEntry;
1128 end
1129
1130 act = Activity(model, aname, hd, bte);
1131
1132 % Assign to task
1133 if isfield(ad, 'task') && task_map.isKey(ad.task)
1134 act.on(task_map(ad.task));
1135 end
1136
1137 % Replies to entry
1138 if isfield(ad, 'repliesTo') && entry_map.isKey(ad.repliesTo)
1139 act.repliesTo(entry_map(ad.repliesTo));
1140 end
1141
1142 % Synch calls (Python schema: "entry", JAR schema: "dest")
1143 if isfield(ad, 'synchCalls')
1144 scs = ad.synchCalls;
1145 if isstruct(scs), scs = num2cell(scs); end
1146 for j = 1:length(scs)
1147 sc = scs{j};
1148 if isfield(sc, 'entry'), ename = sc.entry;
1149 elseif isfield(sc, 'dest'), ename = sc.dest;
1150 else, continue;
1151 end
1152 meanCalls = 1.0;
1153 if isfield(sc, 'mean'), meanCalls = sc.mean; end
1154 if entry_map.isKey(ename)
1155 act.synchCall(entry_map(ename), meanCalls);
1156 end
1157 end
1158 end
1159
1160 % Asynch calls (Python schema: "entry", JAR schema: "dest")
1161 if isfield(ad, 'asynchCalls')
1162 acs = ad.asynchCalls;
1163 if isstruct(acs), acs = num2cell(acs); end
1164 for j = 1:length(acs)
1165 ac = acs{j};
1166 if isfield(ac, 'entry'), ename = ac.entry;
1167 elseif isfield(ac, 'dest'), ename = ac.dest;
1168 else, continue;
1169 end
1170 meanCalls = 1.0;
1171 if isfield(ac, 'mean'), meanCalls = ac.mean; end
1172 if entry_map.isKey(ename)
1173 act.asynchCall(entry_map(ename), meanCalls);
1174 end
1175 end
1176 end
1177
1178 act_map(aname) = act;
1179 end
1180end
1181
1182% --- Precedences (Python schema: "type"/"activities", JAR schema: "preActs"/"postActs"/"preType"/"postType") ---
1183if isfield(data, 'precedences')
1184 precs = data.precedences;
1185 if isstruct(precs), precs = num2cell(precs); end
1186 for i = 1:length(precs)
1187 pd = precs{i};
1188 if ~isfield(pd, 'task') || ~task_map.isKey(pd.task)
1189 continue;
1190 end
1191 task = task_map(pd.task);
1192
1193 if isfield(pd, 'preActs') || isfield(pd, 'postActs')
1194 % JAR schema
1195 preNames = {};
1196 postNames = {};
1197 if isfield(pd, 'preActs')
1198 preNames = pd.preActs;
1199 if ischar(preNames), preNames = {preNames}; end
1200 end
1201 if isfield(pd, 'postActs')
1202 postNames = pd.postActs;
1203 if ischar(postNames), postNames = {postNames}; end
1204 end
1205 preType = 'pre';
1206 postType = 'post';
1207 if isfield(pd, 'preType'), preType = pd.preType; end
1208 if isfield(pd, 'postType'), postType = pd.postType; end
1209
1210 % Normalize JAR naming convention to Python convention
1211 switch postType
1212 case 'post-AND', postType = 'and-fork';
1213 case 'post-OR', postType = 'or-fork';
1214 case 'post-LOOP', postType = 'loop';
1215 end
1216 switch preType
1217 case 'pre-AND', preType = 'and-join';
1218 case 'pre-OR', preType = 'or-join';
1219 end
1220
1221 % Extract postParams (JAR schema: probabilities/loopCount)
1222 postParams = [];
1223 if isfield(pd, 'postParams')
1224 postParams = pd.postParams;
1225 if iscell(postParams), postParams = cell2mat(postParams); end
1226 end
1227
1228 preActs = {};
1229 for ai = 1:length(preNames)
1230 if act_map.isKey(preNames{ai})
1231 preActs{end+1} = act_map(preNames{ai}); %#ok<AGROW>
1232 end
1233 end
1234 postActs = {};
1235 for ai = 1:length(postNames)
1236 if act_map.isKey(postNames{ai})
1237 postActs{end+1} = act_map(postNames{ai}); %#ok<AGROW>
1238 end
1239 end
1240
1241 if strcmp(preType, 'pre') && strcmp(postType, 'post')
1242 if length(preActs) == 1 && length(postActs) == 1
1243 ap = ActivityPrecedence.Serial(preActs{1}, postActs{1});
1244 task.addPrecedence(ap);
1245 end
1246 elseif strcmp(preType, 'pre') && strcmp(postType, 'and-fork')
1247 if ~isempty(preActs) && ~isempty(postActs)
1248 ap = ActivityPrecedence.AndFork(preActs{1}, postActs);
1249 task.addPrecedence(ap);
1250 end
1251 elseif strcmp(preType, 'and-join') && strcmp(postType, 'post')
1252 if ~isempty(preActs) && ~isempty(postActs)
1253 ap = ActivityPrecedence.AndJoin(preActs, postActs{1});
1254 task.addPrecedence(ap);
1255 end
1256 elseif strcmp(preType, 'pre') && strcmp(postType, 'or-fork')
1257 if ~isempty(preActs) && ~isempty(postActs)
1258 probs = [];
1259 if isfield(pd, 'probabilities')
1260 probs = pd.probabilities;
1261 if isstruct(probs), probs = cell2mat(struct2cell(probs)); end
1262 end
1263 if isempty(probs) && ~isempty(postParams)
1264 probs = postParams(:)';
1265 end
1266 if isempty(probs)
1267 n = length(postActs);
1268 probs = ones(1, n) / n;
1269 end
1270 ap = ActivityPrecedence.OrFork(preActs{1}, postActs, probs);
1271 task.addPrecedence(ap);
1272 end
1273 elseif strcmp(preType, 'or-join') && strcmp(postType, 'post')
1274 if ~isempty(preActs) && ~isempty(postActs)
1275 ap = ActivityPrecedence.OrJoin(preActs, postActs{1});
1276 task.addPrecedence(ap);
1277 end
1278 elseif strcmp(preType, 'pre') && strcmp(postType, 'loop')
1279 count = 1.0;
1280 if isfield(pd, 'loopCount'), count = pd.loopCount; end
1281 if count == 1.0 && ~isempty(postParams)
1282 count = postParams(1);
1283 end
1284 if ~isempty(preActs) && ~isempty(postActs)
1285 if length(postActs) > 1
1286 ap = ActivityPrecedence.Loop(preActs{1}, postActs(1:end-1), postActs{end}, count);
1287 else
1288 ap = ActivityPrecedence.Loop(preActs{1}, postActs, count);
1289 end
1290 task.addPrecedence(ap);
1291 end
1292 elseif strcmp(preType, 'pre') && strcmp(postType, 'post-CACHE')
1293 if ~isempty(preActs) && ~isempty(postActs)
1294 ap = ActivityPrecedence.CacheAccess(preActs{1}, postActs);
1295 task.addPrecedence(ap);
1296 end
1297 end
1298 else
1299 % Python schema
1300 ptype = pd.type;
1301 actNames = pd.activities;
1302 if ischar(actNames), actNames = {actNames}; end
1303
1304 % Resolve activity names to objects
1305 actObjs = {};
1306 for ai = 1:length(actNames)
1307 an = actNames{ai};
1308 if act_map.isKey(an)
1309 actObjs{end+1} = act_map(an); %#ok<AGROW>
1310 end
1311 end
1312 if length(actObjs) < 2
1313 continue;
1314 end
1315
1316 switch ptype
1317 case 'Serial'
1318 ap = ActivityPrecedence.Serial(actObjs{:});
1319 task.addPrecedence(ap);
1320 case 'AndFork'
1321 ap = ActivityPrecedence.AndFork(actObjs{1}, actObjs(2:end));
1322 task.addPrecedence(ap);
1323 case 'AndJoin'
1324 ap = ActivityPrecedence.AndJoin(actObjs(1:end-1), actObjs{end});
1325 task.addPrecedence(ap);
1326 case 'OrFork'
1327 probs = [];
1328 if isfield(pd, 'probabilities')
1329 probs = pd.probabilities;
1330 if isstruct(probs), probs = cell2mat(struct2cell(probs)); end
1331 end
1332 if isempty(probs)
1333 n = length(actObjs) - 1;
1334 probs = ones(1, n) / n;
1335 end
1336 ap = ActivityPrecedence.OrFork(actObjs{1}, actObjs(2:end), probs);
1337 task.addPrecedence(ap);
1338 case 'OrJoin'
1339 ap = ActivityPrecedence.OrJoin(actObjs(1:end-1), actObjs{end});
1340 task.addPrecedence(ap);
1341 case 'Loop'
1342 count = 1.0;
1343 if isfield(pd, 'loopCount'), count = pd.loopCount; end
1344 % Check for explicit preActivity field (new format)
1345 if isfield(pd, 'preActivity') && act_map.isKey(pd.preActivity)
1346 preAct = act_map(pd.preActivity);
1347 ap = ActivityPrecedence.Loop(preAct, actObjs, count);
1348 elseif length(actObjs) >= 3
1349 % Legacy format: first is pre, rest is body+end
1350 ap = ActivityPrecedence.Loop(actObjs{1}, actObjs(2:end-1), actObjs{end}, count);
1351 else
1352 ap = ActivityPrecedence.Loop(actObjs{1}, actObjs(2:end), count);
1353 end
1354 task.addPrecedence(ap);
1355 case 'CacheAccess'
1356 if length(actObjs) >= 2
1357 ap = ActivityPrecedence.CacheAccess(actObjs{1}, actObjs(2:end));
1358 task.addPrecedence(ap);
1359 end
1360 end
1361 end
1362 end
1363end
1364end
1365
1366
1367% =========================================================================
1368% Workflow deserialization
1369% =========================================================================
1370
1371function model = json2workflow(data)
1372% Reconstruct a Workflow from decoded JSON struct.
1373
1374modelName = 'workflow';
1375if isfield(data, 'name')
1376 modelName = data.name;
1377end
1378model = Workflow(modelName);
1379
1380% --- Activities ---
1381if isfield(data, 'activities')
1382 acts = data.activities;
1383 if isstruct(acts), acts = num2cell(acts); end
1384 for i = 1:length(acts)
1385 ad = acts{i};
1386 actName = ad.name;
1387 if isfield(ad, 'hostDemand') && ~isempty(ad.hostDemand)
1388 dist = json2dist(ad.hostDemand);
1389 model.addActivity(actName, dist);
1390 else
1391 model.addActivity(actName, 1.0);
1392 end
1393 end
1394end
1395
1396% --- Precedences ---
1397if isfield(data, 'precedences')
1398 precs = data.precedences;
1399 if isstruct(precs), precs = num2cell(precs); end
1400 for i = 1:length(precs)
1401 pd = precs{i};
1402
1403 % preActs
1404 if isfield(pd, 'preActs')
1405 preActs = cellify_string_array(pd.preActs);
1406 else
1407 preActs = {};
1408 end
1409
1410 % postActs
1411 if isfield(pd, 'postActs')
1412 postActs = cellify_string_array(pd.postActs);
1413 else
1414 postActs = {};
1415 end
1416
1417 % preType / postType - convert JAR strings to numeric IDs
1418 preType = ActivityPrecedenceType.PRE_SEQ;
1419 if isfield(pd, 'preType')
1420 preType = str_to_prectype(pd.preType);
1421 end
1422 postType = ActivityPrecedenceType.POST_SEQ;
1423 if isfield(pd, 'postType')
1424 postType = str_to_prectype(pd.postType);
1425 end
1426
1427 % preParams / postParams
1428 preParams = [];
1429 if isfield(pd, 'preParams') && ~isempty(pd.preParams)
1430 preParams = pd.preParams(:)';
1431 end
1432 postParams = [];
1433 if isfield(pd, 'postParams') && ~isempty(pd.postParams)
1434 postParams = pd.postParams(:)';
1435 end
1436
1437 ap = ActivityPrecedence(preActs, postActs, preType, postType, preParams, postParams);
1438 model.addPrecedence(ap);
1439 end
1440end
1441end
1442
1443
1444% =========================================================================
1445% Environment deserialization
1446% =========================================================================
1447
1448function model = json2environment(data, rawJson)
1449% Reconstruct an Environment from decoded JSON struct.
1450
1451modelName = 'env';
1452if isfield(data, 'name')
1453 modelName = data.name;
1454end
1455numStages = 0;
1456if isfield(data, 'numStages')
1457 numStages = data.numStages;
1458end
1459model = Environment(modelName, numStages);
1460
1461% --- Stages ---
1462stageNames = {};
1463if isfield(data, 'stages')
1464 stages = data.stages;
1465 if isstruct(stages), stages = num2cell(stages); end
1466 for i = 1:length(stages)
1467 sd = stages{i};
1468 stageName = sprintf('Stage%d', i);
1469 if isfield(sd, 'name')
1470 stageName = sd.name;
1471 end
1472 stageNames{end+1} = stageName; %#ok<AGROW>
1473
1474 stageType = '';
1475 if isfield(sd, 'type')
1476 stageType = sd.type;
1477 end
1478
1479 stageModel = [];
1480 if isfield(sd, 'model') && ~isempty(sd.model)
1481 stageModel = json2network(sd.model, rawJson);
1482 end
1483
1484 if ~isempty(stageModel)
1485 model.addStage(stageName, stageType, stageModel);
1486 end
1487 end
1488end
1489
1490% --- Transitions ---
1491if isfield(data, 'transitions')
1492 trans = data.transitions;
1493 if isstruct(trans), trans = num2cell(trans); end
1494 for i = 1:length(trans)
1495 td = trans{i};
1496 fromIdx = td.from + 1; % Convert from 0-indexed (JAR) to 1-indexed (MATLAB)
1497 toIdx = td.to + 1; % Convert from 0-indexed (JAR) to 1-indexed (MATLAB)
1498 if isfield(td, 'distribution') && ~isempty(td.distribution)
1499 dist = json2dist(td.distribution);
1500 if ~isempty(dist) && ~isa(dist, 'Disabled')
1501 % Use stage names for MATLAB Environment API
1502 if fromIdx <= length(stageNames) && toIdx <= length(stageNames)
1503 model.addTransition(stageNames{fromIdx}, stageNames{toIdx}, dist);
1504 end
1505 end
1506 end
1507 end
1508end
1509
1510model.init();
1511end
1512
1513
1514% =========================================================================
1515% Distribution deserialization
1516% =========================================================================
1517
1518function dist = json2dist(d)
1519% Convert a JSON distribution struct to a MATLAB Distribution object.
1520if isempty(d)
1521 dist = [];
1522 return;
1523end
1524
1525dtype = d.type;
1526
1527switch dtype
1528 case 'Disabled'
1529 dist = Disabled.getInstance();
1530 return;
1531 case 'Immediate'
1532 dist = Immediate.getInstance();
1533 return;
1534end
1535
1536% Direct params
1537if isfield(d, 'params') && ~isempty(d.params)
1538 p = d.params;
1539 switch dtype
1540 case 'Exp'
1541 lam = p.lambda;
1542 dist = Exp(lam);
1543 return;
1544 case 'Det'
1545 dist = Det(p.value);
1546 return;
1547 case 'Erlang'
1548 dist = Erlang(p.lambda, p.k);
1549 return;
1550 case 'HyperExp'
1551 pv = p.p;
1552 lv = p.lambda;
1553 if isscalar(pv)
1554 dist = HyperExp(pv, lv(1), lv(2));
1555 else
1556 dist = HyperExp(pv(1), lv(1), lv(2));
1557 end
1558 return;
1559 case 'Gamma'
1560 dist = Gamma(p.alpha, p.beta);
1561 return;
1562 case 'Lognormal'
1563 dist = Lognormal(p.mu, p.sigma);
1564 return;
1565 case 'Uniform'
1566 dist = Uniform(p.a, p.b);
1567 return;
1568 case 'Zipf'
1569 dist = Zipf(p.s, p.n);
1570 return;
1571 case 'Pareto'
1572 dist = Pareto(p.alpha, p.scale);
1573 return;
1574 case 'Weibull'
1575 % JSON: alpha = scale (getParam(1)), beta = shape (getParam(2))
1576 % Constructor: Weibull(shape, scale)
1577 dist = Weibull(p.beta, p.alpha);
1578 return;
1579 case 'Normal'
1580 dist = Normal(p.mu, p.sigma);
1581 return;
1582 case 'Geometric'
1583 dist = Geometric(p.p);
1584 return;
1585 case 'Binomial'
1586 dist = Binomial(p.n, p.p);
1587 return;
1588 case 'Poisson'
1589 dist = Poisson(p.lambda);
1590 return;
1591 case 'Bernoulli'
1592 dist = Bernoulli(p.p);
1593 return;
1594 case 'DiscreteUniform'
1595 dist = DiscreteUniform(p.min, p.max);
1596 return;
1597 case 'DiscreteSampler'
1598 pv = p.p(:)';
1599 xv = p.x(:)';
1600 dist = DiscreteSampler(pv, xv);
1601 return;
1602 case 'Coxian'
1603 mu = p.mu(:)';
1604 phi = p.phi(:)';
1605 dist = Coxian(mu, phi);
1606 return;
1607 case 'MMPP2'
1608 dist = MMPP2(p.lambda0, p.lambda1, p.sigma0, p.sigma1);
1609 return;
1610 case 'Replayer'
1611 % Try to load from file first
1612 if isfield(p, 'fileName') && exist(p.fileName, 'file') == 2
1613 dist = Replayer(p.fileName);
1614 return;
1615 end
1616 % Fallback to APH if available
1617 if isfield(d, 'ph') && ~isempty(d.ph)
1618 ph = d.ph;
1619 alpha = ph.alpha;
1620 T = ph.T;
1621 if ~isvector(alpha), alpha = alpha(:)'; end
1622 dist = PH(alpha, T);
1623 return;
1624 end
1625 % Fallback to Exp with stored mean
1626 m = 1.0;
1627 if isfield(p, 'mean'), m = p.mean; end
1628 dist = Exp(1.0 / m);
1629 return;
1630 end
1631end
1632
1633% Prior distribution (mixture of alternatives with prior probabilities)
1634if strcmp(dtype, 'Prior')
1635 if isfield(d, 'distributions') && isfield(d, 'probabilities')
1636 altJsons = d.distributions;
1637 probs = d.probabilities;
1638 if ~iscell(altJsons)
1639 % jsondecode may return struct array instead of cell
1640 altJsons = num2cell(altJsons);
1641 end
1642 alts = cell(1, length(altJsons));
1643 for ai = 1:length(altJsons)
1644 alts{ai} = json2dist(altJsons{ai});
1645 end
1646 probs = probs(:)';
1647 dist = Prior(alts, probs);
1648 return;
1649 end
1650end
1651
1652% PH/APH representation
1653if isfield(d, 'ph') && ~isempty(d.ph)
1654 ph = d.ph;
1655 alpha = ph.alpha;
1656 T = ph.T;
1657 alpha = alpha(:)'; % Ensure row vector (jsondecode returns column vectors)
1658 if strcmp(dtype, 'APH')
1659 dist = APH(alpha, T);
1660 else
1661 dist = PH(alpha, T);
1662 end
1663 return;
1664end
1665
1666% MAP representation
1667if isfield(d, 'map') && ~isempty(d.map)
1668 mapSpec = d.map;
1669 D0 = mapSpec.D0;
1670 D1 = mapSpec.D1;
1671 dist = MAP(D0, D1);
1672 return;
1673end
1674
1675% Fit specification
1676if isfield(d, 'fit') && ~isempty(d.fit)
1677 fit = d.fit;
1678 method = fit.method;
1679 switch method
1680 case 'fitMean'
1681 m = fit.mean;
1682 switch dtype
1683 case 'Exp'
1684 dist = Exp(1.0 / m);
1685 case 'Det'
1686 dist = Det(m);
1687 otherwise
1688 dist = Exp(1.0 / m);
1689 end
1690 return;
1691 case 'fitMeanAndSCV'
1692 m = fit.mean;
1693 scv = fit.scv;
1694 switch dtype
1695 case 'Erlang'
1696 dist = Erlang.fitMeanAndSCV(m, scv);
1697 case 'HyperExp'
1698 dist = HyperExp.fitMeanAndSCV(m, scv);
1699 otherwise
1700 dist = Exp(1.0 / m);
1701 end
1702 return;
1703 case 'fitMeanAndOrder'
1704 m = fit.mean;
1705 order = fit.order;
1706 switch dtype
1707 case 'Erlang'
1708 dist = Erlang.fitMeanAndOrder(m, order);
1709 otherwise
1710 dist = Exp(1.0 / m);
1711 end
1712 return;
1713 end
1714end
1715
1716% Fallback
1717dist = Exp(1.0);
1718end
1719
1720
1721% =========================================================================
1722% Routing parser (handles comma keys in JSON)
1723% =========================================================================
1724
1725function entries = parse_routing_keys(rawJson, class_map, node_map)
1726% Parse routing matrix from raw JSON text to handle keys with commas.
1727% Returns a cell array of structs with fields:
1728% className1, className2, fromNode, toNode, prob
1729entries = {};
1730
1731% Build reverse mapping: jsondecode-sanitized name -> original node name
1732% jsondecode uses matlab.lang.makeValidName which replaces spaces etc.
1733nodeNames = node_map.keys();
1734sanitized_map = containers.Map();
1735for ni = 1:length(nodeNames)
1736 origName = nodeNames{ni};
1737 sanitized = matlab.lang.makeValidName(origName);
1738 sanitized_map(sanitized) = origName;
1739end
1740
1741classNames = class_map.keys();
1742
1743% For each pair of class names, try to find the corresponding key in the JSON
1744for ri = 1:length(classNames)
1745 for si = 1:length(classNames)
1746 cn1 = classNames{ri};
1747 cn2 = classNames{si};
1748 keyStr = ['"', cn1, ',', cn2, '"'];
1749
1750 % Find this key in the raw JSON
1751 pos = strfind(rawJson, keyStr);
1752 if isempty(pos)
1753 continue;
1754 end
1755
1756 % For each occurrence, extract the nested from -> to -> prob structure
1757 for pidx = 1:length(pos)
1758 startPos = pos(pidx) + length(keyStr);
1759 % Skip whitespace and colon
1760 idx = startPos;
1761 while idx <= length(rawJson) && (rawJson(idx) == ' ' || rawJson(idx) == ':' || rawJson(idx) == newline || rawJson(idx) == char(13) || rawJson(idx) == char(9))
1762 idx = idx + 1;
1763 end
1764 if idx > length(rawJson) || rawJson(idx) ~= '{'
1765 continue;
1766 end
1767 % Extract the JSON object using brace counting
1768 objStr = extract_json_object(rawJson, idx);
1769 if isempty(objStr)
1770 continue;
1771 end
1772 % Parse the from -> to -> prob structure
1773 try
1774 fromTo = jsondecode(objStr);
1775 fromNames = fieldnames(fromTo);
1776 for fi = 1:length(fromNames)
1777 fromField = fromNames{fi};
1778 toStruct = fromTo.(fromField);
1779 toNames = fieldnames(toStruct);
1780 % Resolve sanitized field names back to original node names
1781 if sanitized_map.isKey(fromField)
1782 fromName = sanitized_map(fromField);
1783 else
1784 fromName = fromField;
1785 end
1786 for ti = 1:length(toNames)
1787 toField = toNames{ti};
1788 prob = toStruct.(toField);
1789 if sanitized_map.isKey(toField)
1790 toName = sanitized_map(toField);
1791 else
1792 toName = toField;
1793 end
1794 % Verify names exist in the model
1795 if node_map.isKey(fromName) && node_map.isKey(toName)
1796 re = struct();
1797 re.className1 = cn1;
1798 re.className2 = cn2;
1799 re.fromNode = fromName;
1800 re.toNode = toName;
1801 re.prob = prob;
1802 entries{end+1} = re; %#ok<AGROW>
1803 end
1804 end
1805 end
1806 catch
1807 % Skip if parsing fails
1808 end
1809 end
1810 end
1811end
1812end
1813
1814
1815function objStr = extract_json_object(str, startIdx)
1816% Extract a JSON object string starting at startIdx (must be '{').
1817if str(startIdx) ~= '{'
1818 objStr = '';
1819 return;
1820end
1821depth = 0;
1822inString = false;
1823escaped = false;
1824for i = startIdx:length(str)
1825 c = str(i);
1826 if escaped
1827 escaped = false;
1828 continue;
1829 end
1830 if c == '\'
1831 escaped = true;
1832 continue;
1833 end
1834 if c == '"'
1835 inString = ~inString;
1836 continue;
1837 end
1838 if ~inString
1839 if c == '{'
1840 depth = depth + 1;
1841 elseif c == '}'
1842 depth = depth - 1;
1843 if depth == 0
1844 objStr = str(startIdx:i);
1845 return;
1846 end
1847 end
1848 end
1849end
1850objStr = '';
1851end
1852
1853
1854% =========================================================================
1855% Helper functions
1856% =========================================================================
1857
1858function id = str_to_sched_id(str)
1859% Map scheduling string to SchedStrategy numeric ID.
1860switch upper(str)
1861 case 'INF', id = SchedStrategy.INF;
1862 case 'FCFS', id = SchedStrategy.FCFS;
1863 case 'LCFS', id = SchedStrategy.LCFS;
1864 case 'LCFSPR', id = SchedStrategy.LCFSPR;
1865 case 'PS', id = SchedStrategy.PS;
1866 case 'DPS', id = SchedStrategy.DPS;
1867 case 'GPS', id = SchedStrategy.GPS;
1868 case 'SIRO', id = SchedStrategy.SIRO;
1869 case 'RAND', id = SchedStrategy.SIRO; % alias
1870 case 'SJF', id = SchedStrategy.SJF;
1871 case 'LJF', id = SchedStrategy.LJF;
1872 case 'SEPT', id = SchedStrategy.SEPT;
1873 case 'LEPT', id = SchedStrategy.LEPT;
1874 case 'HOL', id = SchedStrategy.HOL;
1875 case 'FCFSPRIO', id = SchedStrategy.FCFSPRIO;
1876 case 'FORK', id = SchedStrategy.FORK;
1877 case 'EXT', id = SchedStrategy.EXT;
1878 case 'REF', id = SchedStrategy.REF;
1879 case 'POLLING', id = SchedStrategy.POLLING;
1880 case 'PSPRIO', id = SchedStrategy.PSPRIO;
1881 case 'DPSPRIO', id = SchedStrategy.DPSPRIO;
1882 case 'GPSPRIO', id = SchedStrategy.GPSPRIO;
1883 otherwise, id = SchedStrategy.FCFS;
1884end
1885end
1886
1887
1888function id = str_to_repl_id(str)
1889% Map replacement strategy string to ReplacementStrategy numeric ID.
1890switch upper(str)
1891 case 'LRU', id = ReplacementStrategy.LRU;
1892 case 'FIFO', id = ReplacementStrategy.FIFO;
1893 case 'RR', id = ReplacementStrategy.RR;
1894 case 'SFIFO', id = ReplacementStrategy.SFIFO;
1895 otherwise, id = ReplacementStrategy.LRU;
1896end
1897end
1898
1899
1900function id = str_to_prectype(str)
1901% Map JAR precedence type string to MATLAB ActivityPrecedenceType numeric ID.
1902switch str
1903 case 'pre', id = ActivityPrecedenceType.PRE_SEQ;
1904 case 'pre-AND', id = ActivityPrecedenceType.PRE_AND;
1905 case 'pre-OR', id = ActivityPrecedenceType.PRE_OR;
1906 case 'post', id = ActivityPrecedenceType.POST_SEQ;
1907 case 'post-AND', id = ActivityPrecedenceType.POST_AND;
1908 case 'post-OR', id = ActivityPrecedenceType.POST_OR;
1909 case 'post-LOOP', id = ActivityPrecedenceType.POST_LOOP;
1910 case 'post-CACHE', id = ActivityPrecedenceType.POST_CACHE;
1911 otherwise, id = ActivityPrecedenceType.PRE_SEQ;
1912end
1913end
1914
1915
1916function id = str_to_droprule(str)
1917% Map drop rule string to DropStrategy numeric ID.
1918switch str
1919 case 'drop', id = DropStrategy.DROP;
1920 case 'waitingQueue', id = DropStrategy.WAITQ;
1921 case 'blockingAfterService', id = DropStrategy.BAS;
1922 case 'retrial', id = DropStrategy.RETRIAL;
1923 case 'retrialWithLimit', id = DropStrategy.RETRIAL_WITH_LIMIT;
1924 otherwise, id = DropStrategy.WAITQ;
1925end
1926end
1927
1928
1929function c = cellify_string_array(arr)
1930% Convert a JSON string array (which may be decoded as a char, cell, or
1931% struct array) into a cell array of character vectors.
1932if ischar(arr)
1933 c = {arr};
1934elseif isstring(arr)
1935 c = cellstr(arr);
1936elseif iscell(arr)
1937 c = arr;
1938else
1939 % jsondecode can return a struct array or char matrix for string arrays
1940 c = cellstr(arr);
1941end
1942end
Definition mmt.m:124