LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
linemodel_save.m
1function linemodel_save(model, filename)
2% LINEMODEL_SAVE Save a LINE model to JSON.
3%
4% LINEMODEL_SAVE(MODEL, FILENAME) saves the model to the specified JSON
5% file, conforming to the line-model.schema.json specification.
6%
7% Parameters:
8% model - Network, LayeredNetwork, Workflow, or Environment object
9% filename - output file path (should end in .json)
10%
11% Example:
12% model = Network('M/M/1');
13% source = Source(model, 'Source');
14% queue = Queue(model, 'Queue', SchedStrategy.FCFS);
15% sink = Sink(model, 'Sink');
16% oclass = OpenClass(model, 'Class1');
17% source.setArrival(oclass, Exp(1.0));
18% queue.setService(oclass, Exp(2.0));
19% P = model.initRoutingMatrix();
20% P{1}(1,2) = 1; P{1}(2,3) = 1;
21% model.link(P);
22% linemodel_save(model, 'mm1.json');
23%
24% Copyright (c) 2012-2026, Imperial College London
25% All rights reserved.
26
27if isa(model, 'LayeredNetwork')
28 modelMap = layered2json(model);
29elseif isa(model, 'Workflow')
30 modelMap = workflow2json(model);
31elseif isa(model, 'Environment')
32 modelMap = environment2json(model);
33else
34 modelMap = network2json(model);
35end
36
37% Build the full document
38sb = {};
39sb{end+1} = '{';
40sb{end+1} = ' "format": "line-model",';
41sb{end+1} = ' "version": "1.0",';
42sb{end+1} = [' "model": ', encode_value(modelMap, 2)];
43sb{end+1} = '}';
44jsonStr = strjoin(sb, newline);
45
46fid = fopen(filename, 'w');
47if fid == -1
48 error('linemodel_save:fileOpen', 'Cannot open file: %s', filename);
49end
50cleanupObj = onCleanup(@() fclose(fid));
51fprintf(fid, '%s\n', jsonStr);
52end
53
54
55% =========================================================================
56% Network serialization
57% =========================================================================
58
59function result = network2json(model)
60% Convert a Network to a containers.Map (preserves key ordering/commas)
61result = containers.Map();
62result('type') = 'Network';
63result('name') = model.getName();
64
65nodes = model.getNodes();
66classes = model.getClasses();
67K = length(classes);
68M = length(nodes);
69
70% --- Nodes ---
71nodesJson = {};
72for i = 1:M
73 node = nodes{i};
74
75 % Skip implicit ClassSwitch nodes (auto-created by link())
76 if isa(node, 'ClassSwitch') && isprop(node, 'autoAdded') && node.autoAdded
77 continue;
78 end
79
80 nj = containers.Map();
81 nj('name') = node.name;
82 nj('type') = node_type_str(node);
83
84 % Scheduling
85 if isa(node, 'Delay')
86 nj('scheduling') = 'INF';
87 elseif isa(node, 'Queue')
88 sched = node.schedStrategy;
89 if ~isempty(sched)
90 nj('scheduling') = sched_id_to_str(sched);
91 end
92 end
93
94 % Servers
95 if isa(node, 'Queue') && ~isa(node, 'Delay')
96 ns = node.numberOfServers;
97 if isfinite(ns) && ns > 1
98 nj('servers') = ns;
99 end
100 end
101
102 % Buffer
103 if isa(node, 'Queue')
104 c = node.cap;
105 if ~isempty(c) && isfinite(c) && c > 0
106 nj('buffer') = c;
107 end
108 end
109
110 % Per-class buffer capacity
111 if isa(node, 'Queue') && ~isempty(node.classCap)
112 ccMap = containers.Map();
113 for r = 1:K
114 jc = classes{r};
115 if r <= length(node.classCap) && isfinite(node.classCap(r))
116 ccMap(jc.name) = node.classCap(r);
117 end
118 end
119 if ccMap.Count > 0
120 nj('classCap') = ccMap;
121 end
122 end
123
124 % Drop rules
125 if isa(node, 'Queue') && ~isempty(node.dropRule)
126 drMap = containers.Map();
127 for r = 1:K
128 jc = classes{r};
129 if r <= length(node.dropRule)
130 dr = node.dropRule(r);
131 drStr = droprule_to_str(dr);
132 if ~isempty(drStr)
133 drMap(jc.name) = drStr;
134 end
135 end
136 end
137 if drMap.Count > 0
138 nj('dropRule') = drMap;
139 end
140 end
141
142 % Load-dependent scaling
143 if isa(node, 'Station') && ~isempty(node.lldScaling)
144 ldMap = containers.Map();
145 ldMap('type') = 'loadDependent';
146 ldMap('scaling') = node.lldScaling(:)';
147 nj('loadDependence') = ldMap;
148 end
149
150 % Service / arrival distributions
151 svc = containers.Map();
152 for r = 1:K
153 jc = classes{r};
154 dist = [];
155 if isa(node, 'Source')
156 try
157 dist = node.getArrivalProcess(jc);
158 catch
159 dist = [];
160 end
161 elseif isa(node, 'Queue') || isa(node, 'Delay')
162 try
163 dist = node.getService(jc);
164 catch
165 dist = [];
166 end
167 end
168 if ~isempty(dist) && ~isa(dist, 'Disabled')
169 dj = dist2json(dist);
170 if ~isempty(dj)
171 svc(jc.name) = dj;
172 end
173 end
174 end
175 if svc.Count > 0
176 nj('service') = svc;
177 end
178
179 % ClassSwitch matrix
180 if isa(node, 'ClassSwitch')
181 csm = node.server.csMatrix;
182 if ~isempty(csm)
183 csDict = containers.Map();
184 for ri = 1:K
185 row = containers.Map();
186 for ci = 1:K
187 if ri <= size(csm,1) && ci <= size(csm,2) && csm(ri,ci) ~= 0
188 row(classes{ci}.name) = csm(ri,ci);
189 end
190 end
191 if row.Count > 0
192 csDict(classes{ri}.name) = row;
193 end
194 end
195 if csDict.Count > 0
196 nj('classSwitchMatrix') = csDict;
197 end
198 end
199 end
200
201 % Cache config
202 if isa(node, 'Cache')
203 cc = containers.Map();
204 cc('items') = node.items.nitems;
205 ilc = node.itemLevelCap;
206 if isscalar(ilc)
207 cc('capacity') = ilc;
208 else
209 cc('capacity') = ilc(:)';
210 end
211 cc('replacement') = repl_to_str(node.replacestrategy);
212
213 % Hit/miss class mappings
214 hc = full(node.server.hitClass);
215 mc = full(node.server.missClass);
216 if ~isempty(hc) && any(hc > 0)
217 hitMap = containers.Map();
218 for hi = 1:length(hc)
219 if hc(hi) > 0 && hi <= K && hc(hi) <= K
220 hitMap(classes{hi}.name) = classes{hc(hi)}.name;
221 end
222 end
223 if hitMap.Count > 0
224 cc('hitClass') = hitMap;
225 end
226 end
227 if ~isempty(mc) && any(mc > 0)
228 missMap = containers.Map();
229 for mi = 1:length(mc)
230 if mc(mi) > 0 && mi <= K && mc(mi) <= K
231 missMap(classes{mi}.name) = classes{mc(mi)}.name;
232 end
233 end
234 if missMap.Count > 0
235 cc('missClass') = missMap;
236 end
237 end
238
239 % Read popularity distributions (setRead)
240 if ~isempty(node.popularity)
241 popMap = containers.Map();
242 for pi = 1:size(node.popularity, 1)
243 for pj = 1:size(node.popularity, 2)
244 if pi <= size(node.popularity, 1) && pj <= size(node.popularity, 2) ...
245 && ~isempty(node.popularity{pi, pj})
246 popDist = node.popularity{pi, pj};
247 dj = dist2json(popDist);
248 if ~isempty(dj) && pj <= K
249 popMap(classes{pj}.name) = dj;
250 end
251 end
252 end
253 end
254 if popMap.Count > 0
255 cc('popularity') = popMap;
256 end
257 end
258
259 nj('cache') = cc;
260 end
261
262 % Fork tasksPerLink
263 if isa(node, 'Fork')
264 if ~isempty(node.output) && isprop(node.output, 'tasksPerLink') && node.output.tasksPerLink > 1
265 nj('tasksPerLink') = node.output.tasksPerLink;
266 end
267 end
268
269 % Join paired fork and join strategy
270 if isa(node, 'Join')
271 if ~isempty(node.joinOf)
272 nj('forkNode') = node.joinOf.name;
273 end
274 % Serialize per-class join strategy if non-default
275 if ~isempty(node.input) && isprop(node.input, 'joinStrategy') && ~isempty(node.input.joinStrategy)
276 for r = 1:K
277 jc = classes{r};
278 if r <= length(node.input.joinStrategy) && ~isempty(node.input.joinStrategy{r})
279 js = node.input.joinStrategy{r};
280 if js ~= JoinStrategy.STD
281 if js == JoinStrategy.PARTIAL
282 nj('joinStrategy') = 'PARTIAL';
283 end
284 end
285 end
286 end
287 end
288 if ~isempty(node.input) && isprop(node.input, 'joinRequired') && ~isempty(node.input.joinRequired)
289 for r = 1:K
290 jc = classes{r};
291 if r <= length(node.input.joinRequired) && ~isempty(node.input.joinRequired{r})
292 jq = node.input.joinRequired{r};
293 if jq > 0
294 nj('joinQuorum') = jq;
295 end
296 end
297 end
298 end
299 end
300
301 % DPS scheduling parameters (weights per class)
302 if isa(node, 'Queue') && ~isa(node, 'Delay')
303 sched = node.schedStrategy;
304 if ~isempty(sched) && (sched == SchedStrategy.DPS || sched == SchedStrategy.GPS)
305 sp = containers.Map();
306 for r = 1:K
307 jc = classes{r};
308 try
309 w = node.schedStrategyPar(r);
310 if ~isempty(w) && isfinite(w) && w > 0
311 sp(jc.name) = w;
312 end
313 catch
314 end
315 end
316 if sp.Count > 0
317 nj('schedParams') = sp;
318 end
319 end
320 end
321
322 % Transition modes
323 if isa(node, 'Transition')
324 modesJson = {};
325 nModes = node.getNumberOfModes();
326 allNodes = model.getNodes();
327 for mi = 1:nModes
328 mj = containers.Map();
329 if mi <= length(node.modeNames) && ~isempty(node.modeNames{mi})
330 mj('name') = node.modeNames{mi};
331 else
332 mj('name') = sprintf('Mode%d', mi);
333 end
334 % Distribution
335 if mi <= length(node.distributions) && ~isempty(node.distributions{mi})
336 dj = dist2json(node.distributions{mi});
337 if ~isempty(dj)
338 mj('distribution') = dj;
339 end
340 end
341 % Timing strategy
342 if mi <= length(node.timingStrategies)
343 if node.timingStrategies(mi) == TimingStrategy.TIMED
344 mj('timingStrategy') = 'TIMED';
345 else
346 mj('timingStrategy') = 'IMMEDIATE';
347 end
348 end
349 % Number of servers
350 if mi <= length(node.numberOfServers) && node.numberOfServers(mi) > 1
351 mj('numServers') = node.numberOfServers(mi);
352 end
353 % Firing priority
354 if mi <= length(node.firingPriorities) && node.firingPriorities(mi) > 0
355 mj('firingPriority') = node.firingPriorities(mi);
356 end
357 % Firing weight
358 if mi <= length(node.firingWeights) && node.firingWeights(mi) ~= 1.0
359 mj('firingWeight') = node.firingWeights(mi);
360 end
361 % Enabling conditions
362 if mi <= length(node.enablingConditions)
363 ecMat = node.enablingConditions{mi};
364 ecList = {};
365 for ni = 1:size(ecMat, 1)
366 for ci = 1:size(ecMat, 2)
367 if ecMat(ni, ci) > 0
368 ec = containers.Map();
369 ec('node') = allNodes{ni}.name;
370 ec('class') = classes{ci}.name;
371 ec('count') = ecMat(ni, ci);
372 ecList{end+1} = ec; %#ok<AGROW>
373 end
374 end
375 end
376 if ~isempty(ecList)
377 mj('enablingConditions') = ecList;
378 end
379 end
380 % Inhibiting conditions
381 if mi <= length(node.inhibitingConditions)
382 icMat = node.inhibitingConditions{mi};
383 icList = {};
384 for ni = 1:size(icMat, 1)
385 for ci = 1:size(icMat, 2)
386 if isfinite(icMat(ni, ci))
387 ic = containers.Map();
388 ic('node') = allNodes{ni}.name;
389 ic('class') = classes{ci}.name;
390 ic('count') = icMat(ni, ci);
391 icList{end+1} = ic; %#ok<AGROW>
392 end
393 end
394 end
395 if ~isempty(icList)
396 mj('inhibitingConditions') = icList;
397 end
398 end
399 % Firing outcomes
400 if mi <= length(node.firingOutcomes)
401 foMat = node.firingOutcomes{mi};
402 foList = {};
403 for ni = 1:size(foMat, 1)
404 for ci = 1:size(foMat, 2)
405 if foMat(ni, ci) ~= 0
406 fo = containers.Map();
407 fo('node') = allNodes{ni}.name;
408 fo('class') = classes{ci}.name;
409 fo('count') = foMat(ni, ci);
410 foList{end+1} = fo; %#ok<AGROW>
411 end
412 end
413 end
414 if ~isempty(foList)
415 mj('firingOutcomes') = foList;
416 end
417 end
418 modesJson{end+1} = mj; %#ok<AGROW>
419 end
420 if ~isempty(modesJson)
421 nj('modes') = modesJson;
422 end
423 end
424
425 % Initial state for Place nodes (token counts)
426 if isa(node, 'Place') && ~isempty(node.state)
427 nj('initialState') = node.state(:)';
428 end
429
430 nodesJson{end+1} = nj; %#ok<AGROW>
431end
432result('nodes') = nodesJson;
433
434% --- Classes ---
435classesJson = {};
436for r = 1:K
437 jc = classes{r};
438 cj = containers.Map();
439 cj('name') = jc.name;
440 if isa(jc, 'OpenSignal')
441 cj('type') = 'Signal';
442 cj('openOrClosed') = 'Open';
443 cj('signalType') = SignalType.toText(jc.signalType);
444 if ~isempty(jc.targetJobClass)
445 cj('targetClass') = jc.targetJobClass.name;
446 end
447 if ~isempty(jc.removalDistribution)
448 cj('removalDistribution') = dist2json(jc.removalDistribution);
449 end
450 if ~isempty(jc.removalPolicy) && jc.removalPolicy ~= RemovalPolicy.RANDOM
451 cj('removalPolicy') = RemovalPolicy.toText(jc.removalPolicy);
452 end
453 elseif isa(jc, 'ClosedSignal')
454 cj('type') = 'Signal';
455 cj('openOrClosed') = 'Closed';
456 cj('signalType') = SignalType.toText(jc.signalType);
457 if ~isempty(jc.refstat) && isprop(jc.refstat, 'name')
458 cj('refNode') = jc.refstat.name;
459 end
460 if ~isempty(jc.targetJobClass)
461 cj('targetClass') = jc.targetJobClass.name;
462 end
463 if ~isempty(jc.removalDistribution)
464 cj('removalDistribution') = dist2json(jc.removalDistribution);
465 end
466 if ~isempty(jc.removalPolicy) && jc.removalPolicy ~= RemovalPolicy.RANDOM
467 cj('removalPolicy') = RemovalPolicy.toText(jc.removalPolicy);
468 end
469 elseif isa(jc, 'OpenClass')
470 cj('type') = 'Open';
471 elseif isa(jc, 'ClosedClass')
472 cj('type') = 'Closed';
473 cj('population') = jc.population;
474 if ~isempty(jc.refstat) && isprop(jc.refstat, 'name')
475 cj('refNode') = jc.refstat.name;
476 end
477 else
478 cj('type') = 'Open';
479 end
480 if jc.priority ~= 0
481 cj('priority') = jc.priority;
482 end
483 if isprop(jc, 'deadline') && isfinite(jc.deadline)
484 cj('deadline') = jc.deadline;
485 end
486 classesJson{end+1} = cj; %#ok<AGROW>
487end
488result('classes') = classesJson;
489
490% --- Routing ---
491routingMap = containers.Map();
492try
493 sn = model.getStruct();
494 % Prefer rtorig (original P matrix before ClassSwitch expansion)
495 if ~isempty(sn) && isfield(sn, 'rtorig') && iscell(sn.rtorig) && ~isempty(sn.rtorig) && ~isempty(sn.rtorig{1,1})
496 P_orig = sn.rtorig;
497 M_orig = size(P_orig{1,1}, 1);
498 % Identify explicit ClassSwitch node indices (not auto-added)
499 nodes = model.getNodes();
500 explicit_cs = false(1, M_orig);
501 for ii = 1:min(M_orig, length(nodes))
502 if isa(nodes{ii}, 'ClassSwitch') && ~nodes{ii}.autoAdded
503 explicit_cs(ii) = true;
504 end
505 end
506 % For explicit CS sources, compute same-class routing:
507 % P_same(s,ii,jj) = sum_r P_orig{r,s}(ii,jj)
508 % This avoids saving cross-class entries that would cause
509 % double-switching on load.
510 cs_same = zeros(K, M_orig, M_orig);
511 for ii = 1:M_orig
512 if explicit_cs(ii)
513 for s = 1:K
514 for jj = 1:M_orig
515 total = 0;
516 for r = 1:K
517 Prs = P_orig{r,s};
518 if issparse(Prs); Prs = full(Prs); end
519 total = total + Prs(ii, jj);
520 end
521 cs_same(s, ii, jj) = total;
522 end
523 end
524 end
525 end
526 for r = 1:K
527 for s = 1:K
528 fromTo = containers.Map();
529 Prs = P_orig{r,s};
530 if issparse(Prs)
531 Prs = full(Prs);
532 end
533 for ii = 1:M_orig
534 if explicit_cs(ii)
535 % For explicit CS, use same-class routing only
536 if r == s
537 for jj = 1:M_orig
538 val = cs_same(s, ii, jj);
539 if val > 1e-14
540 ni = sn.nodenames{ii};
541 njn = sn.nodenames{jj};
542 if ~fromTo.isKey(ni)
543 fromTo(ni) = containers.Map();
544 end
545 dest = fromTo(ni);
546 dest(njn) = val;
547 fromTo(ni) = dest;
548 end
549 end
550 end
551 % Skip cross-class entries from explicit CS
552 continue;
553 end
554 for jj = 1:M_orig
555 val = Prs(ii, jj);
556 if val > 1e-14
557 ni = sn.nodenames{ii};
558 njn = sn.nodenames{jj};
559 if ~fromTo.isKey(ni)
560 fromTo(ni) = containers.Map();
561 end
562 dest = fromTo(ni);
563 dest(njn) = val;
564 fromTo(ni) = dest;
565 end
566 end
567 end
568 if fromTo.Count > 0
569 key = char(sprintf('%s,%s', classes{r}.name, classes{s}.name));
570 routingMap(key) = fromTo;
571 end
572 end
573 end
574 elseif ~isempty(sn) && isfield(sn, 'rtnodes') && ~isempty(sn.rtnodes)
575 % Fallback to rtnodes if rtorig not available
576 rt = sn.rtnodes;
577 N = sn.nnodes;
578 for r = 1:K
579 for s = 1:K
580 fromTo = containers.Map();
581 for ii = 1:N
582 for jj = 1:N
583 val = rt((ii-1)*K+r, (jj-1)*K+s);
584 if val > 1e-14
585 ni = sn.nodenames{ii};
586 njn = sn.nodenames{jj};
587 if ~fromTo.isKey(ni)
588 fromTo(ni) = containers.Map();
589 end
590 dest = fromTo(ni);
591 dest(njn) = val;
592 fromTo(ni) = dest;
593 end
594 end
595 end
596 if fromTo.Count > 0
597 key = char(sprintf('%s,%s', classes{r}.name, classes{s}.name));
598 routingMap(key) = fromTo;
599 end
600 end
601 end
602 end
603catch
604 % If struct not available, routing stays empty
605end
606
607routing = containers.Map();
608routing('type') = 'matrix';
609routing('matrix') = routingMap;
610result('routing') = routing;
611
612% --- Routing Strategies ---
613try
614 sn2 = model.getStruct();
615 if ~isempty(sn2) && isfield(sn2, 'routing') && ~isempty(sn2.routing)
616 routingStrategies = containers.Map();
617 stratNames = containers.Map('KeyType','int32','ValueType','char');
618 stratNames(int32(RoutingStrategy.RAND)) = 'RAND';
619 stratNames(int32(RoutingStrategy.RROBIN)) = 'RROBIN';
620 stratNames(int32(RoutingStrategy.WRROBIN)) = 'WRROBIN';
621 stratNames(int32(RoutingStrategy.JSQ)) = 'JSQ';
622 stratNames(int32(RoutingStrategy.KCHOICES)) = 'KCHOICES';
623 stratNames(int32(RoutingStrategy.FIRING)) = 'FIRING';
624 stratNames(int32(RoutingStrategy.RL)) = 'RL';
625 stratNames(int32(RoutingStrategy.DISABLED)) = 'DISABLED';
626 for i = 1:sn2.nnodes
627 nodeStrats = containers.Map();
628 for r = 1:K
629 routVal = int32(sn2.routing(i, r));
630 if routVal ~= int32(RoutingStrategy.PROB) && routVal ~= int32(RoutingStrategy.RAND) && stratNames.isKey(routVal)
631 nodeStrats(classes{r}.name) = stratNames(routVal);
632 end
633 end
634 if nodeStrats.Count > 0
635 routingStrategies(sn2.nodenames{i}) = nodeStrats;
636 end
637 end
638 if routingStrategies.Count > 0
639 result('routingStrategies') = routingStrategies;
640 end
641
642 % Save WRROBIN weights
643 routingWeights = containers.Map();
644 for i = 1:sn2.nnodes
645 stationIdx = sn2.nodeToStation(i);
646 if stationIdx < 1; continue; end
647 nodeObj2 = nodes{stationIdx};
648 nodeClassWeights = containers.Map();
649 for r = 1:K
650 if int32(sn2.routing(i, r)) == int32(RoutingStrategy.WRROBIN)
651 os = nodeObj2.output.outputStrategy;
652 if size(os,2) >= r
653 osEntry = os{1, r};
654 % osEntry = {className, stratName, forwardLinks}
655 if length(osEntry) >= 3
656 fwdLinks = osEntry{3};
657 destWeights = containers.Map();
658 for fi = 1:length(fwdLinks)
659 link = fwdLinks{fi};
660 % link = {destNode, weight}
661 if iscell(link) && length(link) >= 2 && isa(link{1}, 'Node')
662 destWeights(link{1}.name) = link{2};
663 end
664 end
665 if destWeights.Count > 0
666 nodeClassWeights(classes{r}.name) = destWeights;
667 end
668 end
669 end
670 end
671 end
672 if nodeClassWeights.Count > 0
673 routingWeights(sn2.nodenames{i}) = nodeClassWeights;
674 end
675 end
676 if routingWeights.Count > 0
677 result('routingWeights') = routingWeights;
678 end
679 end
680catch
681end
682
683% --- Switchover Times ---
684try
685 nodesCellTmp = result('nodes');
686 for i = 1:M
687 nodeObj = nodes{i};
688 if isa(nodeObj, 'Queue')
689 soTimes = {};
690 if isprop(nodeObj, 'switchoverTimes') && ~isempty(nodeObj.switchoverTimes)
691 for r = 1:K
692 for s = 1:K
693 if r ~= s
694 dist = [];
695 try
696 dist = nodeObj.getSwitchover(classes{r}, classes{s});
697 catch
698 end
699 if ~isempty(dist) && ~isa(dist, 'Disabled')
700 so = containers.Map();
701 so('from') = classes{r}.name;
702 so('to') = classes{s}.name;
703 so('distribution') = dist2json(dist);
704 soTimes{end+1} = so;
705 end
706 end
707 end
708 end
709 end
710 if ~isempty(soTimes)
711 for nj_idx = 1:length(nodesCellTmp)
712 nj = nodesCellTmp{nj_idx};
713 if strcmp(nj('name'), nodeObj.name)
714 nj('switchoverTimes') = soTimes;
715 nodesCellTmp{nj_idx} = nj;
716 break;
717 end
718 end
719 end
720 end
721 end
722 result('nodes') = nodesCellTmp;
723catch
724end
725
726% --- Heterogeneous Server Types ---
727try
728 nodesCellTmp = result('nodes');
729 for i = 1:M
730 nodeObj = nodes{i};
731 if isa(nodeObj, 'Queue') && nodeObj.isHeterogeneous()
732 stArr = {};
733 for ti = 1:length(nodeObj.serverTypes)
734 st = nodeObj.serverTypes{ti};
735 stj = containers.Map();
736 stj('name') = st.name;
737 stj('count') = st.numOfServers;
738 % Compatible classes
739 ccNames = {};
740 for cci = 1:length(st.compatibleClasses)
741 ccNames{end+1} = st.compatibleClasses{cci}.name; %#ok<AGROW>
742 end
743 if ~isempty(ccNames)
744 stj('compatibleClasses') = ccNames;
745 end
746 % Per-class service distributions
747 svcMap = containers.Map();
748 for r = 1:K
749 jc = classes{r};
750 dist = nodeObj.getHeteroService(jc, st);
751 if ~isempty(dist) && ~isa(dist, 'Disabled')
752 svcMap(jc.name) = dist2json(dist);
753 end
754 end
755 if svcMap.Count > 0
756 stj('service') = svcMap;
757 end
758 stArr{end+1} = stj; %#ok<AGROW>
759 end
760 if ~isempty(stArr)
761 for nj_idx = 1:length(nodesCellTmp)
762 nj = nodesCellTmp{nj_idx};
763 if strcmp(nj('name'), nodeObj.name)
764 nj('serverTypes') = stArr;
765 % Scheduling policy
766 policy = nodeObj.getHeteroSchedPolicy();
767 if ~isempty(policy) && policy ~= HeteroSchedPolicy.ORDER
768 nj('heteroSchedPolicy') = HeteroSchedPolicy.toText(policy);
769 end
770 nodesCellTmp{nj_idx} = nj;
771 break;
772 end
773 end
774 end
775 end
776 end
777 result('nodes') = nodesCellTmp;
778catch
779end
780
781% --- Balking, Retrial, Patience ---
782try
783 nodesCellTmp = result('nodes');
784 for nj_idx = 1:length(nodesCellTmp)
785 nj = nodesCellTmp{nj_idx};
786 nodeName = nj('name');
787 nodeObj = node_map(nodeName);
788 if ~isa(nodeObj, 'Queue'), continue; end
789 % Balking
790 balkJson = containers.Map();
791 for r = 1:K
792 jc = classes{r};
793 if nodeObj.hasBalking(jc)
794 [strategy, thresholds] = nodeObj.getBalking(jc);
795 bjc = containers.Map();
796 switch strategy
797 case BalkingStrategy.QUEUE_LENGTH, bjc('strategy') = 'QUEUE_LENGTH';
798 case BalkingStrategy.EXPECTED_WAIT, bjc('strategy') = 'EXPECTED_WAIT';
799 case BalkingStrategy.COMBINED, bjc('strategy') = 'COMBINED';
800 end
801 thArr = {};
802 for ti = 1:length(thresholds)
803 th = thresholds{ti};
804 tjson = containers.Map();
805 tjson('minJobs') = th{1};
806 if isinf(th{2})
807 tjson('maxJobs') = -1;
808 else
809 tjson('maxJobs') = th{2};
810 end
811 tjson('probability') = th{3};
812 thArr{end+1} = tjson;
813 end
814 bjc('thresholds') = thArr;
815 balkJson(jc.name) = bjc;
816 end
817 end
818 if balkJson.Count > 0
819 nj('balking') = balkJson;
820 end
821 % Retrial
822 retrialJson = containers.Map();
823 for r = 1:K
824 jc = classes{r};
825 if nodeObj.hasRetrial(jc)
826 [delayDist, maxAttempts] = nodeObj.getRetrial(jc);
827 rjc = containers.Map();
828 rjc('delay') = dist2json(delayDist);
829 rjc('maxAttempts') = maxAttempts;
830 retrialJson(jc.name) = rjc;
831 end
832 end
833 if retrialJson.Count > 0
834 nj('retrial') = retrialJson;
835 end
836 % Patience
837 patienceJson = containers.Map();
838 for r = 1:K
839 jc = classes{r};
840 patDist = nodeObj.getPatience(jc);
841 if ~isempty(patDist) && ~isa(patDist, 'Disabled')
842 pjc = containers.Map();
843 pjc('distribution') = dist2json(patDist);
844 impType = nodeObj.getImpatienceType(jc);
845 if ~isempty(impType)
846 pjc('impatienceType') = ImpatienceType.toText(impType);
847 end
848 patienceJson(jc.name) = pjc;
849 end
850 end
851 if patienceJson.Count > 0
852 nj('patience') = patienceJson;
853 end
854 nodesCellTmp{nj_idx} = nj;
855 end
856 result('nodes') = nodesCellTmp;
857catch
858end
859
860% --- Finite Capacity Regions ---
861try
862 regions = model.regions;
863 if ~isempty(regions)
864 fcrArray = {};
865 for ri = 1:length(regions)
866 reg = regions{ri};
867 rj = containers.Map();
868 rj('name') = reg.name;
869 % Stations with per-class details
870 stationsJson = {};
871 for ni = 1:length(reg.nodes)
872 sj = containers.Map();
873 sj('node') = reg.nodes{ni}.name;
874 % Per-class classCap
875 if isprop(reg, 'classMaxJobs') && ~isempty(reg.classMaxJobs)
876 ccMap = containers.Map();
877 for r = 1:K
878 jc = classes{r};
879 if r <= length(reg.classMaxJobs) && isfinite(reg.classMaxJobs(r))
880 ccMap(jc.name) = reg.classMaxJobs(r);
881 end
882 end
883 if ccMap.Count > 0
884 sj('classCap') = ccMap;
885 end
886 end
887 % Per-class classWeight
888 if isprop(reg, 'classWeight') && ~isempty(reg.classWeight)
889 cwMap = containers.Map();
890 for r = 1:K
891 jc = classes{r};
892 if r <= length(reg.classWeight) && reg.classWeight(r) ~= 1
893 cwMap(jc.name) = reg.classWeight(r);
894 end
895 end
896 if cwMap.Count > 0
897 sj('classWeight') = cwMap;
898 end
899 end
900 % Per-class classSize
901 if isprop(reg, 'classSize') && ~isempty(reg.classSize)
902 csMap = containers.Map();
903 for r = 1:K
904 jc = classes{r};
905 if r <= length(reg.classSize) && reg.classSize(r) ~= 1
906 csMap(jc.name) = reg.classSize(r);
907 end
908 end
909 if csMap.Count > 0
910 sj('classSize') = csMap;
911 end
912 end
913 stationsJson{end+1} = sj; %#ok<AGROW>
914 end
915 rj('stations') = stationsJson;
916 if isprop(reg, 'globalMaxJobs') && isfinite(reg.globalMaxJobs)
917 rj('globalMaxJobs') = reg.globalMaxJobs;
918 end
919 if isprop(reg, 'globalMaxMemory') && isfinite(reg.globalMaxMemory)
920 rj('globalMaxMemory') = reg.globalMaxMemory;
921 end
922 % Per-class classMaxJobs at region level
923 if isprop(reg, 'classMaxJobs') && ~isempty(reg.classMaxJobs)
924 cmjMap = containers.Map();
925 for r = 1:K
926 jc = classes{r};
927 if r <= length(reg.classMaxJobs) && isfinite(reg.classMaxJobs(r))
928 cmjMap(jc.name) = reg.classMaxJobs(r);
929 end
930 end
931 if cmjMap.Count > 0
932 rj('classMaxJobs') = cmjMap;
933 end
934 end
935 % Drop rule
936 if isprop(reg, 'dropRule') && ~isempty(reg.dropRule)
937 drMap = containers.Map();
938 for r = 1:K
939 jc = classes{r};
940 if r <= length(reg.dropRule)
941 drStr = droprule_to_str(reg.dropRule(r));
942 if ~isempty(drStr)
943 drMap(jc.name) = drStr;
944 end
945 end
946 end
947 if drMap.Count > 0
948 rj('dropRule') = drMap;
949 end
950 end
951 fcrArray{end+1} = rj; %#ok<AGROW>
952 end
953 if ~isempty(fcrArray)
954 result('finiteCapacityRegions') = fcrArray;
955 end
956 end
957catch
958end
959end
960
961
962% =========================================================================
963% LayeredNetwork serialization
964% =========================================================================
965
966function result = layered2json(model)
967result = containers.Map();
968result('type') = 'LayeredNetwork';
969result('name') = model.getName();
970
971% --- Processors ---
972procsJson = {};
973hosts = model.hosts;
974for i = 1:length(hosts)
975 h = hosts{i};
976 pj = containers.Map();
977 pj('name') = h.name;
978 mult = h.multiplicity;
979 if isfinite(mult) && mult > 1
980 pj('multiplicity') = mult;
981 end
982 schedStr = h.scheduling;
983 if ~isempty(schedStr) && ~strcmpi(schedStr, 'inf')
984 pj('scheduling') = upper(schedStr);
985 end
986 q = h.quantum;
987 if q > 0 && q ~= 0.001
988 pj('quantum') = q;
989 end
990 sf = h.speedFactor;
991 if sf ~= 1.0
992 pj('speedFactor') = sf;
993 end
994 repl = h.replication;
995 if repl > 1
996 pj('replication') = repl;
997 end
998 procsJson{end+1} = pj; %#ok<AGROW>
999end
1000result('processors') = procsJson;
1001
1002% --- Tasks ---
1003tasksJson = {};
1004tasksList = model.tasks;
1005for i = 1:length(tasksList)
1006 t = tasksList{i};
1007 tj = containers.Map();
1008 tj('name') = t.name;
1009 if ~isempty(t.parent)
1010 tj('processor') = t.parent.name;
1011 end
1012 mult = t.multiplicity;
1013 if isfinite(mult) && mult > 1
1014 tj('multiplicity') = mult;
1015 end
1016 schedStr = t.scheduling;
1017 if ~isempty(schedStr)
1018 tj('scheduling') = upper(schedStr);
1019 end
1020 % Think time
1021 ttMean = t.thinkTimeMean;
1022 if ~isempty(ttMean) && ttMean > GlobalConstants.FineTol
1023 if ~isempty(t.thinkTime) && isa(t.thinkTime, 'Distribution')
1024 tj('thinkTime') = dist2json(t.thinkTime);
1025 else
1026 params = containers.Map();
1027 params('lambda') = 1.0 / ttMean;
1028 dj = containers.Map();
1029 dj('type') = 'Exp';
1030 dj('params') = params;
1031 tj('thinkTime') = dj;
1032 end
1033 end
1034 % Fan in
1035 if ~isempty(t.fanInSource) && ischar(t.fanInSource) && ~isempty(t.fanInSource)
1036 fi = containers.Map();
1037 fi(t.fanInSource) = t.fanInValue;
1038 tj('fanIn') = fi;
1039 end
1040 % Fan out
1041 if ~isempty(t.fanOutDest)
1042 fo = containers.Map();
1043 for fi_idx = 1:length(t.fanOutDest)
1044 fo(t.fanOutDest{fi_idx}) = t.fanOutValue(fi_idx);
1045 end
1046 tj('fanOut') = fo;
1047 end
1048 repl = t.replication;
1049 if repl > 1
1050 tj('replication') = repl;
1051 end
1052 % FunctionTask detection
1053 if isa(t, 'FunctionTask')
1054 tj('taskType') = 'FunctionTask';
1055 end
1056 % Setup time / delay-off time (on any Task)
1057 if ~isempty(t.setupTime) && isa(t.setupTime, 'Distribution')
1058 stMean = t.setupTimeMean;
1059 if stMean > GlobalConstants.FineTol
1060 tj('setupTime') = dist2json(t.setupTime);
1061 end
1062 end
1063 if ~isempty(t.delayOffTime) && isa(t.delayOffTime, 'Distribution')
1064 dotMean = t.delayOffTimeMean;
1065 if dotMean > GlobalConstants.FineTol
1066 tj('delayOffTime') = dist2json(t.delayOffTime);
1067 end
1068 end
1069 % CacheTask detection
1070 if isa(t, 'CacheTask')
1071 tj('taskType') = 'CacheTask';
1072 tj('totalItems') = t.items;
1073 tj('cacheCapacity') = t.itemLevelCap;
1074 rs = t.replacestrategy;
1075 rsNameMap = containers.Map({ReplacementStrategy.RR, ReplacementStrategy.FIFO, ...
1076 ReplacementStrategy.SFIFO, ReplacementStrategy.LRU}, ...
1077 {'RR', 'FIFO', 'SFIFO', 'LRU'});
1078 if rsNameMap.isKey(rs)
1079 tj('replacementStrategy') = rsNameMap(rs);
1080 else
1081 tj('replacementStrategy') = 'FIFO';
1082 end
1083 end
1084 tasksJson{end+1} = tj; %#ok<AGROW>
1085end
1086result('tasks') = tasksJson;
1087
1088% --- Entries ---
1089entriesJson = {};
1090entriesList = model.entries;
1091for i = 1:length(entriesList)
1092 e = entriesList{i};
1093 ej = containers.Map();
1094 ej('name') = e.name;
1095 if ~isempty(e.parent)
1096 ej('task') = e.parent.name;
1097 end
1098 % Entry arrival distribution
1099 if ~isempty(e.arrival) && isa(e.arrival, 'Distribution')
1100 ej('arrival') = dist2json(e.arrival);
1101 end
1102 % ItemEntry detection
1103 if isa(e, 'ItemEntry')
1104 ej('entryType') = 'ItemEntry';
1105 ej('totalItems') = e.cardinality;
1106 if ~isempty(e.popularity)
1107 if isa(e.popularity, 'Distribution')
1108 ej('accessProb') = dist2json(e.popularity);
1109 end
1110 end
1111 end
1112 entriesJson{end+1} = ej; %#ok<AGROW>
1113end
1114result('entries') = entriesJson;
1115
1116% --- Build reply map: activityName -> entryName ---
1117replyMap = containers.Map();
1118for i = 1:length(entriesList)
1119 e = entriesList{i};
1120 if ~isempty(e.replyActivity)
1121 for j = 1:length(e.replyActivity)
1122 replyMap(e.replyActivity{j}) = e.name;
1123 end
1124 end
1125end
1126
1127% --- Activities ---
1128actsJson = {};
1129actsList = model.activities;
1130for i = 1:length(actsList)
1131 a = actsList{i};
1132 aj = containers.Map();
1133 aj('name') = a.name;
1134 if ~isempty(a.parent)
1135 if isa(a.parent, 'Task') || isa(a.parent, 'Entry')
1136 aj('task') = a.parent.name;
1137 elseif ischar(a.parent) || isstring(a.parent)
1138 aj('task') = char(a.parent);
1139 elseif ischar(a.parentName) && ~isempty(a.parentName)
1140 aj('task') = a.parentName;
1141 end
1142 elseif ~isempty(a.parentName) && ischar(a.parentName)
1143 aj('task') = a.parentName;
1144 end
1145 % Host demand
1146 if ~isempty(a.hostDemand) && isa(a.hostDemand, 'Distribution')
1147 if ~isa(a.hostDemand, 'Immediate')
1148 aj('hostDemand') = dist2json(a.hostDemand);
1149 end
1150 elseif ~isempty(a.hostDemandMean) && a.hostDemandMean > GlobalConstants.FineTol
1151 params = containers.Map();
1152 params('lambda') = 1.0 / a.hostDemandMean;
1153 dj = containers.Map();
1154 dj('type') = 'Exp';
1155 dj('params') = params;
1156 aj('hostDemand') = dj;
1157 end
1158 % Bound to entry
1159 if ~isempty(a.boundToEntry)
1160 aj('boundTo') = a.boundToEntry;
1161 end
1162 % Replies to entry
1163 if replyMap.isKey(a.name)
1164 aj('repliesTo') = replyMap(a.name);
1165 end
1166 % Synch calls
1167 if ~isempty(a.syncCallDests)
1168 synchCalls = {};
1169 for j = 1:length(a.syncCallDests)
1170 sc = containers.Map();
1171 sc('entry') = a.syncCallDests{j};
1172 if j <= length(a.syncCallMeans) && a.syncCallMeans(j) ~= 1.0
1173 sc('mean') = a.syncCallMeans(j);
1174 end
1175 synchCalls{end+1} = sc; %#ok<AGROW>
1176 end
1177 aj('synchCalls') = synchCalls;
1178 end
1179 % Asynch calls
1180 if ~isempty(a.asyncCallDests)
1181 asynchCalls = {};
1182 for j = 1:length(a.asyncCallDests)
1183 ac = containers.Map();
1184 ac('entry') = a.asyncCallDests{j};
1185 if j <= length(a.asyncCallMeans) && a.asyncCallMeans(j) ~= 1.0
1186 ac('mean') = a.asyncCallMeans(j);
1187 end
1188 asynchCalls{end+1} = ac; %#ok<AGROW>
1189 end
1190 aj('asynchCalls') = asynchCalls;
1191 end
1192 actsJson{end+1} = aj; %#ok<AGROW>
1193end
1194result('activities') = actsJson;
1195
1196% --- Precedences ---
1197precsJson = {};
1198for i = 1:length(tasksList)
1199 t = tasksList{i};
1200 precs = t.precedences;
1201 if isempty(precs), continue; end
1202 for j = 1:length(precs)
1203 p = precs(j);
1204 pj = containers.Map();
1205 pj('task') = t.name;
1206
1207 preType = p.preType;
1208 postType = p.postType;
1209
1210 % Determine JSON precedence type and collect activity names
1211 if preType == ActivityPrecedenceType.PRE_SEQ && postType == ActivityPrecedenceType.POST_SEQ
1212 pj('type') = 'Serial';
1213 pj('activities') = [p.preActs, p.postActs];
1214 elseif preType == ActivityPrecedenceType.PRE_SEQ && postType == ActivityPrecedenceType.POST_AND
1215 pj('type') = 'AndFork';
1216 pj('activities') = [p.preActs, p.postActs];
1217 elseif preType == ActivityPrecedenceType.PRE_AND && postType == ActivityPrecedenceType.POST_SEQ
1218 pj('type') = 'AndJoin';
1219 pj('activities') = [p.preActs, p.postActs];
1220 elseif preType == ActivityPrecedenceType.PRE_SEQ && postType == ActivityPrecedenceType.POST_OR
1221 pj('type') = 'OrFork';
1222 pj('activities') = [p.preActs, p.postActs];
1223 if ~isempty(p.postParams)
1224 pj('probabilities') = p.postParams(:)';
1225 end
1226 elseif preType == ActivityPrecedenceType.PRE_OR && postType == ActivityPrecedenceType.POST_SEQ
1227 pj('type') = 'OrJoin';
1228 pj('activities') = [p.preActs, p.postActs];
1229 elseif postType == ActivityPrecedenceType.POST_LOOP
1230 pj('type') = 'Loop';
1231 % For Loop, preActs is the trigger, postActs is the loop body
1232 pj('activities') = p.postActs;
1233 if ~isempty(p.preActs)
1234 pj('preActivity') = p.preActs{1};
1235 end
1236 if ~isempty(p.postParams)
1237 pj('loopCount') = p.postParams(1);
1238 end
1239 elseif preType == ActivityPrecedenceType.PRE_SEQ && postType == ActivityPrecedenceType.POST_CACHE
1240 pj('type') = 'CacheAccess';
1241 pj('activities') = [p.preActs, p.postActs];
1242 else
1243 continue;
1244 end
1245 precsJson{end+1} = pj; %#ok<AGROW>
1246 end
1247end
1248if ~isempty(precsJson)
1249 result('precedences') = precsJson;
1250end
1251end
1252
1253
1254% =========================================================================
1255% Workflow serialization
1256% =========================================================================
1257
1258function result = workflow2json(model)
1259% Convert a Workflow to a containers.Map for JSON output.
1260result = containers.Map();
1261result('type') = 'Workflow';
1262result('name') = model.getName();
1263
1264% --- Activities ---
1265actsJson = {};
1266acts = model.activities;
1267for i = 1:length(acts)
1268 act = acts{i};
1269 aj = containers.Map();
1270 aj('name') = act.name;
1271 if ~isempty(act.hostDemand) && isa(act.hostDemand, 'Distribution')
1272 dj = dist2json(act.hostDemand);
1273 if ~isempty(dj)
1274 aj('hostDemand') = dj;
1275 end
1276 end
1277 actsJson{end+1} = aj; %#ok<AGROW>
1278end
1279result('activities') = actsJson;
1280
1281% --- Precedences ---
1282precsJson = {};
1283precs = model.precedences;
1284for i = 1:length(precs)
1285 p = precs(i);
1286 pj = containers.Map();
1287
1288 % preActs
1289 preActsJson = {};
1290 for a = 1:length(p.preActs)
1291 preActsJson{end+1} = p.preActs{a}; %#ok<AGROW>
1292 end
1293 pj('preActs') = preActsJson;
1294
1295 % postActs
1296 postActsJson = {};
1297 for a = 1:length(p.postActs)
1298 postActsJson{end+1} = p.postActs{a}; %#ok<AGROW>
1299 end
1300 pj('postActs') = postActsJson;
1301
1302 % preType / postType - convert numeric IDs to JAR-compatible strings
1303 pj('preType') = prectype_to_str(p.preType);
1304 pj('postType') = prectype_to_str(p.postType);
1305
1306 % preParams
1307 if ~isempty(p.preParams)
1308 pj('preParams') = p.preParams(:)';
1309 end
1310
1311 % postParams
1312 if ~isempty(p.postParams)
1313 pj('postParams') = p.postParams(:)';
1314 end
1315
1316 precsJson{end+1} = pj; %#ok<AGROW>
1317end
1318result('precedences') = precsJson;
1319end
1320
1321
1322% =========================================================================
1323% Environment serialization
1324% =========================================================================
1325
1326function result = environment2json(model)
1327% Convert an Environment to a containers.Map for JSON output.
1328result = containers.Map();
1329result('type') = 'Environment';
1330result('name') = model.getName();
1331
1332E = height(model.envGraph.Nodes);
1333result('numStages') = E;
1334
1335% --- Stages ---
1336stagesJson = {};
1337for e = 1:E
1338 sj = containers.Map();
1339 sj('name') = model.envGraph.Nodes.Name{e};
1340 % Serialize the stage's Network model
1341 if e <= length(model.ensemble) && ~isempty(model.ensemble{e})
1342 sj('model') = network2json(model.ensemble{e});
1343 end
1344 stagesJson{end+1} = sj; %#ok<AGROW>
1345end
1346result('stages') = stagesJson;
1347
1348% --- Transitions ---
1349transJson = {};
1350for e = 1:E
1351 for h = 1:E
1352 if ~isempty(model.env) && e <= size(model.env, 1) && h <= size(model.env, 2) ...
1353 && ~isempty(model.env{e,h}) && ~isa(model.env{e,h}, 'Disabled')
1354 tj = containers.Map();
1355 tj('from') = e - 1; % Convert to 0-indexed for JAR compatibility
1356 tj('to') = h - 1; % Convert to 0-indexed for JAR compatibility
1357 dj = dist2json(model.env{e,h});
1358 if ~isempty(dj)
1359 tj('distribution') = dj;
1360 transJson{end+1} = tj; %#ok<AGROW>
1361 end
1362 end
1363 end
1364end
1365result('transitions') = transJson;
1366end
1367
1368
1369% =========================================================================
1370% Distribution serialization
1371% =========================================================================
1372
1373function d = dist2json(dist)
1374% Convert a Distribution to a containers.Map for JSON output.
1375if isempty(dist)
1376 d = [];
1377 return;
1378end
1379d = containers.Map();
1380cn = builtin('class', dist);
1381switch cn
1382 case 'Disabled'
1383 d('type') = 'Disabled';
1384 case 'Immediate'
1385 d('type') = 'Immediate';
1386 case 'Exp'
1387 d('type') = 'Exp';
1388 params = containers.Map();
1389 params('lambda') = dist.getParam(1).paramValue;
1390 d('params') = params;
1391 case 'Det'
1392 d('type') = 'Det';
1393 params = containers.Map();
1394 params('value') = dist.getParam(1).paramValue;
1395 d('params') = params;
1396 case 'Erlang'
1397 d('type') = 'Erlang';
1398 params = containers.Map();
1399 params('lambda') = dist.getParam(1).paramValue;
1400 params('k') = dist.getParam(2).paramValue;
1401 d('params') = params;
1402 case 'HyperExp'
1403 d('type') = 'HyperExp';
1404 params = containers.Map();
1405 p = dist.getParam(1).paramValue;
1406 l1 = dist.getParam(2).paramValue;
1407 l2 = dist.getParam(3).paramValue;
1408 if isscalar(p)
1409 params('p') = [p, 1-p];
1410 params('lambda') = [l1, l2];
1411 else
1412 params('p') = p(:)';
1413 params('lambda') = [l1, l2];
1414 end
1415 d('params') = params;
1416 case 'Gamma'
1417 d('type') = 'Gamma';
1418 params = containers.Map();
1419 params('alpha') = dist.getParam(1).paramValue;
1420 params('beta') = dist.getParam(2).paramValue;
1421 d('params') = params;
1422 case 'Lognormal'
1423 d('type') = 'Lognormal';
1424 params = containers.Map();
1425 params('mu') = dist.getParam(1).paramValue;
1426 params('sigma') = dist.getParam(2).paramValue;
1427 d('params') = params;
1428 case 'Uniform'
1429 d('type') = 'Uniform';
1430 params = containers.Map();
1431 params('a') = dist.getParam(1).paramValue;
1432 params('b') = dist.getParam(2).paramValue;
1433 d('params') = params;
1434 case 'Zipf'
1435 d('type') = 'Zipf';
1436 params = containers.Map();
1437 params('s') = dist.getParam(3).paramValue;
1438 params('n') = dist.getParam(4).paramValue;
1439 d('params') = params;
1440 case 'Pareto'
1441 d('type') = 'Pareto';
1442 params = containers.Map();
1443 params('alpha') = dist.getParam(1).paramValue;
1444 params('scale') = dist.getParam(2).paramValue;
1445 d('params') = params;
1446 case 'Weibull'
1447 d('type') = 'Weibull';
1448 params = containers.Map();
1449 params('alpha') = dist.getParam(1).paramValue;
1450 params('beta') = dist.getParam(2).paramValue;
1451 d('params') = params;
1452 case 'Normal'
1453 d('type') = 'Normal';
1454 params = containers.Map();
1455 params('mu') = dist.getParam(1).paramValue;
1456 params('sigma') = dist.getParam(2).paramValue;
1457 d('params') = params;
1458 case 'Geometric'
1459 d('type') = 'Geometric';
1460 params = containers.Map();
1461 params('p') = dist.getParam(1).paramValue;
1462 d('params') = params;
1463 case 'Binomial'
1464 d('type') = 'Binomial';
1465 params = containers.Map();
1466 params('n') = dist.getParam(1).paramValue;
1467 params('p') = dist.getParam(2).paramValue;
1468 d('params') = params;
1469 case 'Poisson'
1470 d('type') = 'Poisson';
1471 params = containers.Map();
1472 params('lambda') = dist.getParam(1).paramValue;
1473 d('params') = params;
1474 case 'Bernoulli'
1475 d('type') = 'Bernoulli';
1476 params = containers.Map();
1477 params('p') = dist.getParam(1).paramValue;
1478 d('params') = params;
1479 case 'DiscreteUniform'
1480 d('type') = 'DiscreteUniform';
1481 params = containers.Map();
1482 params('min') = dist.getParam(1).paramValue;
1483 params('max') = dist.getParam(2).paramValue;
1484 d('params') = params;
1485 case {'Coxian', 'Cox2'}
1486 d('type') = 'Coxian';
1487 params = containers.Map();
1488 params('mu') = dist.getMu()';
1489 params('phi') = dist.getPhi()';
1490 d('params') = params;
1491 case {'PH', 'APH'}
1492 d('type') = 'PH';
1493 ph = containers.Map();
1494 alpha = dist.getInitProb();
1495 T = dist.getSubgenerator();
1496 if isvector(alpha)
1497 ph('alpha') = alpha(:)';
1498 else
1499 ph('alpha') = alpha;
1500 end
1501 ph('T') = T;
1502 d('ph') = ph;
1503 case 'MAP'
1504 d('type') = 'MAP';
1505 mapSpec = containers.Map();
1506 mapSpec('D0') = dist.getParam(1).paramValue;
1507 mapSpec('D1') = dist.getParam(2).paramValue;
1508 d('map') = mapSpec;
1509 case 'MMPP2'
1510 d('type') = 'MMPP2';
1511 params = containers.Map();
1512 params('lambda0') = dist.getParam(1).paramValue;
1513 params('lambda1') = dist.getParam(2).paramValue;
1514 params('sigma0') = dist.getParam(3).paramValue;
1515 params('sigma1') = dist.getParam(4).paramValue;
1516 d('params') = params;
1517 case 'DiscreteSampler'
1518 d('type') = 'DiscreteSampler';
1519 params = containers.Map();
1520 params('p') = dist.getParam(1).paramValue(:)';
1521 params('x') = dist.getParam(2).paramValue(:)';
1522 d('params') = params;
1523 case 'Replayer'
1524 d('type') = 'Replayer';
1525 params = containers.Map();
1526 params('fileName') = dist.getParam(1).paramValue;
1527 try
1528 params('mean') = dist.getMean();
1529 catch
1530 end
1531 d('params') = params;
1532 % Save APH fit as fallback
1533 try
1534 aphDist = dist.fitAPH();
1535 if ~isempty(aphDist) && isa(aphDist, 'Distribution')
1536 ph = containers.Map();
1537 alpha = aphDist.getParam(1).paramValue;
1538 T = aphDist.getParam(2).paramValue;
1539 if isvector(alpha)
1540 ph('alpha') = alpha(:)';
1541 else
1542 ph('alpha') = alpha;
1543 end
1544 ph('T') = T;
1545 d('ph') = ph;
1546 end
1547 catch
1548 end
1549 case 'Prior'
1550 d('type') = 'Prior';
1551 alts = {};
1552 for ai = 1:dist.getNumAlternatives()
1553 altDist = dist.getAlternative(ai);
1554 altJson = dist2json(altDist);
1555 if ~isempty(altJson)
1556 alts{end+1} = altJson; %#ok<AGROW>
1557 end
1558 end
1559 d('distributions') = alts;
1560 d('probabilities') = dist.probabilities(:)';
1561 otherwise
1562 % Fallback: fit from mean
1563 try
1564 m = dist.getMean();
1565 d('type') = 'Exp';
1566 fit = containers.Map();
1567 fit('method') = 'fitMean';
1568 fit('mean') = m;
1569 d('fit') = fit;
1570 catch
1571 d = [];
1572 end
1573end
1574end
1575
1576
1577% =========================================================================
1578% JSON encoding
1579% =========================================================================
1580
1581function s = encode_value(val, indent)
1582% Recursively encode a MATLAB value to JSON string.
1583if nargin < 2, indent = 0; end
1584pad = repmat(' ', 1, indent);
1585pad2 = repmat(' ', 1, indent + 2);
1586
1587if isa(val, 'containers.Map')
1588 ks = val.keys();
1589 if isempty(ks)
1590 s = '{}';
1591 else
1592 parts = cell(1, length(ks));
1593 for i = 1:length(ks)
1594 k = ks{i};
1595 v = val(k);
1596 parts{i} = sprintf('%s"%s": %s', pad2, json_escape(k), encode_value(v, indent + 2));
1597 end
1598 s = sprintf('{\n%s\n%s}', strjoin(parts, sprintf(',\n')), pad);
1599 end
1600elseif ischar(val) || isstring(val)
1601 s = sprintf('"%s"', json_escape(char(val)));
1602elseif islogical(val) && isscalar(val)
1603 if val, s = 'true'; else, s = 'false'; end
1604elseif isnumeric(val) && isscalar(val)
1605 if isnan(val)
1606 s = 'null';
1607 elseif isinf(val)
1608 if val > 0, s = '"Infinity"'; else, s = '"-Infinity"'; end
1609 elseif val == floor(val) && abs(val) < 1e15
1610 s = sprintf('%d', val);
1611 else
1612 s = sprintf('%.15g', val);
1613 end
1614elseif isnumeric(val) && isvector(val) && ~isscalar(val)
1615 parts = cell(1, length(val));
1616 for i = 1:length(val)
1617 parts{i} = encode_value(val(i), 0);
1618 end
1619 s = ['[', strjoin(parts, ', '), ']'];
1620elseif isnumeric(val) && ismatrix(val) && ~isvector(val)
1621 rows = cell(1, size(val, 1));
1622 for i = 1:size(val, 1)
1623 rows{i} = encode_value(val(i,:), 0);
1624 end
1625 s = ['[', strjoin(rows, ', '), ']'];
1626elseif iscell(val)
1627 if isempty(val)
1628 s = '[]';
1629 else
1630 parts = cell(1, length(val));
1631 for i = 1:length(val)
1632 parts{i} = sprintf('%s%s', pad2, encode_value(val{i}, indent + 2));
1633 end
1634 s = sprintf('[\n%s\n%s]', strjoin(parts, sprintf(',\n')), pad);
1635 end
1636elseif isstruct(val) && isscalar(val)
1637 fnames = fieldnames(val);
1638 if isempty(fnames)
1639 s = '{}';
1640 else
1641 parts = cell(1, length(fnames));
1642 for i = 1:length(fnames)
1643 fn = fnames{i};
1644 fv = val.(fn);
1645 parts{i} = sprintf('%s"%s": %s', pad2, json_escape(fn), encode_value(fv, indent + 2));
1646 end
1647 s = sprintf('{\n%s\n%s}', strjoin(parts, sprintf(',\n')), pad);
1648 end
1649else
1650 s = 'null';
1651end
1652end
1653
1654function s = json_escape(str)
1655% Escape special characters for JSON strings.
1656s = strrep(str, '\', '\\');
1657s = strrep(s, '"', '\"');
1658s = strrep(s, sprintf('\n'), '\n');
1659s = strrep(s, sprintf('\r'), '\r');
1660s = strrep(s, sprintf('\t'), '\t');
1661end
1662
1663
1664% =========================================================================
1665% Helper functions
1666% =========================================================================
1667
1668function s = node_type_str(node)
1669% Get the JSON node type string for a node object.
1670if isa(node, 'Source'), s = 'Source';
1671elseif isa(node, 'Sink'), s = 'Sink';
1672elseif isa(node, 'Delay'), s = 'Delay';
1673elseif isa(node, 'Cache'), s = 'Cache';
1674elseif isa(node, 'Place'), s = 'Place';
1675elseif isa(node, 'Transition'), s = 'Transition';
1676elseif isa(node, 'Queue'), s = 'Queue';
1677elseif isa(node, 'Fork'), s = 'Fork';
1678elseif isa(node, 'Join'), s = 'Join';
1679elseif isa(node, 'Router'), s = 'Router';
1680elseif isa(node, 'ClassSwitch'), s = 'ClassSwitch';
1681else, s = 'Queue';
1682end
1683end
1684
1685function s = sched_id_to_str(id)
1686% Map SchedStrategy numeric ID to schema-compatible string.
1687if id == SchedStrategy.INF, s = 'INF';
1688elseif id == SchedStrategy.FCFS, s = 'FCFS';
1689elseif id == SchedStrategy.LCFS, s = 'LCFS';
1690elseif id == SchedStrategy.LCFSPR, s = 'LCFSPR';
1691elseif id == SchedStrategy.PS, s = 'PS';
1692elseif id == SchedStrategy.DPS, s = 'DPS';
1693elseif id == SchedStrategy.GPS, s = 'GPS';
1694elseif id == SchedStrategy.SIRO, s = 'SIRO';
1695elseif id == SchedStrategy.SJF, s = 'SJF';
1696elseif id == SchedStrategy.LJF, s = 'LJF';
1697elseif id == SchedStrategy.SEPT, s = 'SEPT';
1698elseif id == SchedStrategy.LEPT, s = 'LEPT';
1699elseif id == SchedStrategy.HOL, s = 'HOL';
1700elseif id == SchedStrategy.FORK, s = 'FORK';
1701elseif id == SchedStrategy.EXT, s = 'EXT';
1702elseif id == SchedStrategy.REF, s = 'REF';
1703elseif id == SchedStrategy.POLLING, s = 'POLLING';
1704elseif id == SchedStrategy.PSPRIO, s = 'PSPRIO';
1705elseif id == SchedStrategy.DPSPRIO, s = 'DPSPRIO';
1706elseif id == SchedStrategy.GPSPRIO, s = 'GPSPRIO';
1707elseif id == SchedStrategy.FCFSPRIO, s = 'FCFSPRIO';
1708else, s = 'FCFS';
1709end
1710end
1711
1712function s = repl_to_str(id)
1713% Map ReplacementStrategy numeric ID to schema string.
1714if id == ReplacementStrategy.LRU, s = 'LRU';
1715elseif id == ReplacementStrategy.FIFO, s = 'FIFO';
1716elseif id == ReplacementStrategy.RR, s = 'RR';
1717elseif id == ReplacementStrategy.SFIFO, s = 'SFIFO';
1718else, s = 'LRU';
1719end
1720end
1721
1722function s = droprule_to_str(id)
1723% Map DropStrategy numeric ID to schema-compatible string.
1724if id == DropStrategy.DROP, s = 'drop';
1725elseif id == DropStrategy.WAITQ, s = 'waitingQueue';
1726elseif id == DropStrategy.BAS, s = 'blockingAfterService';
1727elseif id == DropStrategy.RETRIAL, s = 'retrial';
1728elseif id == DropStrategy.RETRIAL_WITH_LIMIT, s = 'retrialWithLimit';
1729else, s = '';
1730end
1731end
1732
1733function s = prectype_to_str(id)
1734% Map ActivityPrecedenceType numeric ID to JAR-compatible string.
1735if id == ActivityPrecedenceType.PRE_SEQ, s = 'pre';
1736elseif id == ActivityPrecedenceType.PRE_AND, s = 'pre-AND';
1737elseif id == ActivityPrecedenceType.PRE_OR, s = 'pre-OR';
1738elseif id == ActivityPrecedenceType.POST_SEQ, s = 'post';
1739elseif id == ActivityPrecedenceType.POST_AND, s = 'post-AND';
1740elseif id == ActivityPrecedenceType.POST_OR, s = 'post-OR';
1741elseif id == ActivityPrecedenceType.POST_LOOP, s = 'post-LOOP';
1742elseif id == ActivityPrecedenceType.POST_CACHE, s = 'post-CACHE';
1743else, s = 'pre';
1744end
1745end
Definition mmt.m:124