LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
linemodel_save.m
1function linemodel_save(model, filename)
2% LINEMODEL_SAVE Save a LINE model (Network or LayeredNetwork) 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 or LayeredNetwork 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);
29else
30 modelMap = network2json(model);
31end
32
33% Build the full document
34sb = {};
35sb{end+1} = '{';
36sb{end+1} = ' "format": "line-model",';
37sb{end+1} = ' "version": "1.0",';
38sb{end+1} = [' "model": ', encode_value(modelMap, 2)];
39sb{end+1} = '}';
40jsonStr = strjoin(sb, newline);
41
42fid = fopen(filename, 'w');
43if fid == -1
44 error('linemodel_save:fileOpen', 'Cannot open file: %s', filename);
45end
46cleanupObj = onCleanup(@() fclose(fid));
47fprintf(fid, '%s\n', jsonStr);
48end
49
50
51% =========================================================================
52% Network serialization
53% =========================================================================
54
55function result = network2json(model)
56% Convert a Network to a containers.Map (preserves key ordering/commas)
57result = containers.Map();
58result('type') = 'Network';
59result('name') = model.getName();
60
61nodes = model.getNodes();
62classes = model.getClasses();
63K = length(classes);
64M = length(nodes);
65
66% --- Nodes ---
67nodesJson = {};
68for i = 1:M
69 node = nodes{i};
70
71 % Skip implicit ClassSwitch nodes (auto-created by link())
72 if isa(node, 'ClassSwitch') && isprop(node, 'autoAdded') && node.autoAdded
73 continue;
74 end
75
76 nj = containers.Map();
77 nj('name') = node.name;
78 nj('type') = node_type_str(node);
79
80 % Scheduling
81 if isa(node, 'Delay')
82 nj('scheduling') = 'INF';
83 elseif isa(node, 'Queue')
84 sched = node.schedStrategy;
85 if ~isempty(sched)
86 nj('scheduling') = sched_id_to_str(sched);
87 end
88 end
89
90 % Servers
91 if isa(node, 'Queue') && ~isa(node, 'Delay')
92 ns = node.numberOfServers;
93 if isfinite(ns) && ns > 1
94 nj('servers') = ns;
95 end
96 end
97
98 % Buffer
99 if isa(node, 'Queue')
100 c = node.cap;
101 if ~isempty(c) && isfinite(c) && c > 0
102 nj('buffer') = c;
103 end
104 end
105
106 % Service / arrival distributions
107 svc = containers.Map();
108 for r = 1:K
109 jc = classes{r};
110 dist = [];
111 if isa(node, 'Source')
112 try
113 dist = node.getArrivalProcess(jc);
114 catch
115 dist = [];
116 end
117 elseif isa(node, 'Queue') || isa(node, 'Delay')
118 try
119 dist = node.getService(jc);
120 catch
121 dist = [];
122 end
123 end
124 if ~isempty(dist) && ~isa(dist, 'Disabled')
125 dj = dist2json(dist);
126 if ~isempty(dj)
127 svc(jc.name) = dj;
128 end
129 end
130 end
131 if svc.Count > 0
132 nj('service') = svc;
133 end
134
135 % ClassSwitch matrix
136 if isa(node, 'ClassSwitch')
137 csm = node.server.csMatrix;
138 if ~isempty(csm)
139 csDict = containers.Map();
140 for ri = 1:K
141 row = containers.Map();
142 for ci = 1:K
143 if ri <= size(csm,1) && ci <= size(csm,2) && csm(ri,ci) ~= 0
144 row(classes{ci}.name) = csm(ri,ci);
145 end
146 end
147 if row.Count > 0
148 csDict(classes{ri}.name) = row;
149 end
150 end
151 if csDict.Count > 0
152 nj('classSwitchMatrix') = csDict;
153 end
154 end
155 end
156
157 % Cache config
158 if isa(node, 'Cache')
159 cc = containers.Map();
160 cc('items') = node.items.nitems;
161 ilc = node.itemLevelCap;
162 if isscalar(ilc)
163 cc('capacity') = ilc;
164 else
165 cc('capacity') = ilc(:)';
166 end
167 cc('replacement') = repl_to_str(node.replacestrategy);
168
169 % Hit/miss class mappings
170 hc = full(node.server.hitClass);
171 mc = full(node.server.missClass);
172 if ~isempty(hc) && any(hc > 0)
173 hitMap = containers.Map();
174 for hi = 1:length(hc)
175 if hc(hi) > 0 && hi <= K && hc(hi) <= K
176 hitMap(classes{hi}.name) = classes{hc(hi)}.name;
177 end
178 end
179 if hitMap.Count > 0
180 cc('hitClass') = hitMap;
181 end
182 end
183 if ~isempty(mc) && any(mc > 0)
184 missMap = containers.Map();
185 for mi = 1:length(mc)
186 if mc(mi) > 0 && mi <= K && mc(mi) <= K
187 missMap(classes{mi}.name) = classes{mc(mi)}.name;
188 end
189 end
190 if missMap.Count > 0
191 cc('missClass') = missMap;
192 end
193 end
194
195 % Read popularity distributions (setRead)
196 if ~isempty(node.popularity)
197 popMap = containers.Map();
198 for pi = 1:size(node.popularity, 1)
199 for pj = 1:size(node.popularity, 2)
200 if pi <= size(node.popularity, 1) && pj <= size(node.popularity, 2) ...
201 && ~isempty(node.popularity{pi, pj})
202 popDist = node.popularity{pi, pj};
203 dj = dist2json(popDist);
204 if ~isempty(dj) && pj <= K
205 popMap(classes{pj}.name) = dj;
206 end
207 end
208 end
209 end
210 if popMap.Count > 0
211 cc('popularity') = popMap;
212 end
213 end
214
215 nj('cache') = cc;
216 end
217
218 % Fork tasksPerLink
219 if isa(node, 'Fork')
220 if ~isempty(node.output) && isprop(node.output, 'tasksPerLink') && node.output.tasksPerLink > 1
221 nj('tasksPerLink') = node.output.tasksPerLink;
222 end
223 end
224
225 % Join paired fork
226 if isa(node, 'Join')
227 if ~isempty(node.joinOf)
228 nj('forkNode') = node.joinOf.name;
229 end
230 end
231
232 % DPS scheduling parameters (weights per class)
233 if isa(node, 'Queue') && ~isa(node, 'Delay')
234 sched = node.schedStrategy;
235 if ~isempty(sched) && (sched == SchedStrategy.DPS || sched == SchedStrategy.GPS)
236 sp = containers.Map();
237 for r = 1:K
238 jc = classes{r};
239 try
240 w = node.schedStrategyPar(r);
241 if ~isempty(w) && isfinite(w) && w > 0
242 sp(jc.name) = w;
243 end
244 catch
245 end
246 end
247 if sp.Count > 0
248 nj('schedParams') = sp;
249 end
250 end
251 end
252
253 % Transition modes
254 if isa(node, 'Transition')
255 modesJson = {};
256 nModes = node.getNumberOfModes();
257 allNodes = model.getNodes();
258 for mi = 1:nModes
259 mj = containers.Map();
260 if mi <= length(node.modeNames) && ~isempty(node.modeNames{mi})
261 mj('name') = node.modeNames{mi};
262 else
263 mj('name') = sprintf('Mode%d', mi);
264 end
265 % Distribution
266 if mi <= length(node.distributions) && ~isempty(node.distributions{mi})
267 dj = dist2json(node.distributions{mi});
268 if ~isempty(dj)
269 mj('distribution') = dj;
270 end
271 end
272 % Timing strategy
273 if mi <= length(node.timingStrategies)
274 if node.timingStrategies(mi) == TimingStrategy.TIMED
275 mj('timingStrategy') = 'TIMED';
276 else
277 mj('timingStrategy') = 'IMMEDIATE';
278 end
279 end
280 % Number of servers
281 if mi <= length(node.numberOfServers) && node.numberOfServers(mi) > 1
282 mj('numServers') = node.numberOfServers(mi);
283 end
284 % Firing priority
285 if mi <= length(node.firingPriorities) && node.firingPriorities(mi) > 0
286 mj('firingPriority') = node.firingPriorities(mi);
287 end
288 % Firing weight
289 if mi <= length(node.firingWeights) && node.firingWeights(mi) ~= 1.0
290 mj('firingWeight') = node.firingWeights(mi);
291 end
292 % Enabling conditions
293 if mi <= length(node.enablingConditions)
294 ecMat = node.enablingConditions{mi};
295 ecList = {};
296 for ni = 1:size(ecMat, 1)
297 for ci = 1:size(ecMat, 2)
298 if ecMat(ni, ci) > 0
299 ec = containers.Map();
300 ec('node') = allNodes{ni}.name;
301 ec('class') = classes{ci}.name;
302 ec('count') = ecMat(ni, ci);
303 ecList{end+1} = ec; %#ok<AGROW>
304 end
305 end
306 end
307 if ~isempty(ecList)
308 mj('enablingConditions') = ecList;
309 end
310 end
311 % Inhibiting conditions
312 if mi <= length(node.inhibitingConditions)
313 icMat = node.inhibitingConditions{mi};
314 icList = {};
315 for ni = 1:size(icMat, 1)
316 for ci = 1:size(icMat, 2)
317 if isfinite(icMat(ni, ci))
318 ic = containers.Map();
319 ic('node') = allNodes{ni}.name;
320 ic('class') = classes{ci}.name;
321 ic('count') = icMat(ni, ci);
322 icList{end+1} = ic; %#ok<AGROW>
323 end
324 end
325 end
326 if ~isempty(icList)
327 mj('inhibitingConditions') = icList;
328 end
329 end
330 % Firing outcomes
331 if mi <= length(node.firingOutcomes)
332 foMat = node.firingOutcomes{mi};
333 foList = {};
334 for ni = 1:size(foMat, 1)
335 for ci = 1:size(foMat, 2)
336 if foMat(ni, ci) ~= 0
337 fo = containers.Map();
338 fo('node') = allNodes{ni}.name;
339 fo('class') = classes{ci}.name;
340 fo('count') = foMat(ni, ci);
341 foList{end+1} = fo; %#ok<AGROW>
342 end
343 end
344 end
345 if ~isempty(foList)
346 mj('firingOutcomes') = foList;
347 end
348 end
349 modesJson{end+1} = mj; %#ok<AGROW>
350 end
351 if ~isempty(modesJson)
352 nj('modes') = modesJson;
353 end
354 end
355
356 nodesJson{end+1} = nj; %#ok<AGROW>
357end
358result('nodes') = nodesJson;
359
360% --- Classes ---
361classesJson = {};
362for r = 1:K
363 jc = classes{r};
364 cj = containers.Map();
365 cj('name') = jc.name;
366 if isa(jc, 'OpenClass')
367 cj('type') = 'Open';
368 elseif isa(jc, 'ClosedClass')
369 cj('type') = 'Closed';
370 cj('population') = jc.population;
371 if ~isempty(jc.refstat) && isprop(jc.refstat, 'name')
372 cj('refNode') = jc.refstat.name;
373 end
374 else
375 cj('type') = 'Open';
376 end
377 if jc.priority ~= 0
378 cj('priority') = jc.priority;
379 end
380 classesJson{end+1} = cj; %#ok<AGROW>
381end
382result('classes') = classesJson;
383
384% --- Routing ---
385routingMap = containers.Map();
386try
387 sn = model.getStruct();
388 % Prefer rtorig (original P matrix before ClassSwitch expansion)
389 if ~isempty(sn) && isfield(sn, 'rtorig') && iscell(sn.rtorig) && ~isempty(sn.rtorig) && ~isempty(sn.rtorig{1,1})
390 P_orig = sn.rtorig;
391 M_orig = size(P_orig{1,1}, 1);
392 for r = 1:K
393 for s = 1:K
394 fromTo = containers.Map();
395 Prs = P_orig{r,s};
396 if issparse(Prs)
397 Prs = full(Prs);
398 end
399 for ii = 1:M_orig
400 for jj = 1:M_orig
401 val = Prs(ii, jj);
402 if val > 1e-14
403 ni = sn.nodenames{ii};
404 njn = sn.nodenames{jj};
405 if ~fromTo.isKey(ni)
406 fromTo(ni) = containers.Map();
407 end
408 dest = fromTo(ni);
409 dest(njn) = val;
410 fromTo(ni) = dest;
411 end
412 end
413 end
414 if fromTo.Count > 0
415 key = [classes{r}.name, ',', classes{s}.name];
416 routingMap(key) = fromTo;
417 end
418 end
419 end
420 elseif ~isempty(sn) && isfield(sn, 'rtnodes') && ~isempty(sn.rtnodes)
421 % Fallback to rtnodes if rtorig not available
422 rt = sn.rtnodes;
423 N = sn.nnodes;
424 for r = 1:K
425 for s = 1:K
426 fromTo = containers.Map();
427 for ii = 1:N
428 for jj = 1:N
429 val = rt((ii-1)*K+r, (jj-1)*K+s);
430 if val > 1e-14
431 ni = sn.nodenames{ii};
432 njn = sn.nodenames{jj};
433 if ~fromTo.isKey(ni)
434 fromTo(ni) = containers.Map();
435 end
436 dest = fromTo(ni);
437 dest(njn) = val;
438 fromTo(ni) = dest;
439 end
440 end
441 end
442 if fromTo.Count > 0
443 key = [classes{r}.name, ',', classes{s}.name];
444 routingMap(key) = fromTo;
445 end
446 end
447 end
448 end
449catch
450 % If struct not available, routing stays empty
451end
452
453routing = containers.Map();
454routing('type') = 'matrix';
455routing('matrix') = routingMap;
456result('routing') = routing;
457end
458
459
460% =========================================================================
461% LayeredNetwork serialization
462% =========================================================================
463
464function result = layered2json(model)
465result = containers.Map();
466result('type') = 'LayeredNetwork';
467result('name') = model.getName();
468
469% --- Processors ---
470procsJson = {};
471hosts = model.hosts;
472for i = 1:length(hosts)
473 h = hosts{i};
474 pj = containers.Map();
475 pj('name') = h.name;
476 mult = h.multiplicity;
477 if isfinite(mult) && mult > 1
478 pj('multiplicity') = mult;
479 end
480 schedStr = h.scheduling;
481 if ~isempty(schedStr) && ~strcmpi(schedStr, 'inf')
482 pj('scheduling') = upper(schedStr);
483 end
484 q = h.quantum;
485 if q > 0 && q ~= 0.001
486 pj('quantum') = q;
487 end
488 sf = h.speedFactor;
489 if sf ~= 1.0
490 pj('speedFactor') = sf;
491 end
492 repl = h.replication;
493 if repl > 1
494 pj('replication') = repl;
495 end
496 procsJson{end+1} = pj; %#ok<AGROW>
497end
498result('processors') = procsJson;
499
500% --- Tasks ---
501tasksJson = {};
502tasksList = model.tasks;
503for i = 1:length(tasksList)
504 t = tasksList{i};
505 tj = containers.Map();
506 tj('name') = t.name;
507 if ~isempty(t.parent)
508 tj('processor') = t.parent.name;
509 end
510 mult = t.multiplicity;
511 if isfinite(mult) && mult > 1
512 tj('multiplicity') = mult;
513 end
514 schedStr = t.scheduling;
515 if ~isempty(schedStr)
516 tj('scheduling') = upper(schedStr);
517 end
518 % Think time
519 ttMean = t.thinkTimeMean;
520 if ~isempty(ttMean) && ttMean > GlobalConstants.FineTol
521 if ~isempty(t.thinkTime) && isa(t.thinkTime, 'Distribution')
522 tj('thinkTime') = dist2json(t.thinkTime);
523 else
524 params = containers.Map();
525 params('lambda') = 1.0 / ttMean;
526 dj = containers.Map();
527 dj('type') = 'Exp';
528 dj('params') = params;
529 tj('thinkTime') = dj;
530 end
531 end
532 % Fan in
533 if ~isempty(t.fanInSource) && ischar(t.fanInSource) && ~isempty(t.fanInSource)
534 fi = containers.Map();
535 fi(t.fanInSource) = t.fanInValue;
536 tj('fanIn') = fi;
537 end
538 % Fan out
539 if ~isempty(t.fanOutDest)
540 fo = containers.Map();
541 for fi_idx = 1:length(t.fanOutDest)
542 fo(t.fanOutDest{fi_idx}) = t.fanOutValue(fi_idx);
543 end
544 tj('fanOut') = fo;
545 end
546 repl = t.replication;
547 if repl > 1
548 tj('replication') = repl;
549 end
550 tasksJson{end+1} = tj; %#ok<AGROW>
551end
552result('tasks') = tasksJson;
553
554% --- Entries ---
555entriesJson = {};
556entriesList = model.entries;
557for i = 1:length(entriesList)
558 e = entriesList{i};
559 ej = containers.Map();
560 ej('name') = e.name;
561 if ~isempty(e.parent)
562 ej('task') = e.parent.name;
563 end
564 entriesJson{end+1} = ej; %#ok<AGROW>
565end
566result('entries') = entriesJson;
567
568% --- Build reply map: activityName -> entryName ---
569replyMap = containers.Map();
570for i = 1:length(entriesList)
571 e = entriesList{i};
572 if ~isempty(e.replyActivity)
573 for j = 1:length(e.replyActivity)
574 replyMap(e.replyActivity{j}) = e.name;
575 end
576 end
577end
578
579% --- Activities ---
580actsJson = {};
581actsList = model.activities;
582for i = 1:length(actsList)
583 a = actsList{i};
584 aj = containers.Map();
585 aj('name') = a.name;
586 if ~isempty(a.parent)
587 if isa(a.parent, 'Task') || isa(a.parent, 'Entry')
588 aj('task') = a.parent.name;
589 elseif ischar(a.parent) || isstring(a.parent)
590 aj('task') = char(a.parent);
591 elseif ischar(a.parentName) && ~isempty(a.parentName)
592 aj('task') = a.parentName;
593 end
594 elseif ~isempty(a.parentName) && ischar(a.parentName)
595 aj('task') = a.parentName;
596 end
597 % Host demand
598 if ~isempty(a.hostDemand) && isa(a.hostDemand, 'Distribution')
599 if ~isa(a.hostDemand, 'Immediate')
600 aj('hostDemand') = dist2json(a.hostDemand);
601 end
602 elseif ~isempty(a.hostDemandMean) && a.hostDemandMean > GlobalConstants.FineTol
603 params = containers.Map();
604 params('lambda') = 1.0 / a.hostDemandMean;
605 dj = containers.Map();
606 dj('type') = 'Exp';
607 dj('params') = params;
608 aj('hostDemand') = dj;
609 end
610 % Bound to entry
611 if ~isempty(a.boundToEntry)
612 aj('boundTo') = a.boundToEntry;
613 end
614 % Replies to entry
615 if replyMap.isKey(a.name)
616 aj('repliesTo') = replyMap(a.name);
617 end
618 % Synch calls
619 if ~isempty(a.syncCallDests)
620 synchCalls = {};
621 for j = 1:length(a.syncCallDests)
622 sc = containers.Map();
623 sc('entry') = a.syncCallDests{j};
624 if j <= length(a.syncCallMeans) && a.syncCallMeans(j) ~= 1.0
625 sc('mean') = a.syncCallMeans(j);
626 end
627 synchCalls{end+1} = sc; %#ok<AGROW>
628 end
629 aj('synchCalls') = synchCalls;
630 end
631 % Asynch calls
632 if ~isempty(a.asyncCallDests)
633 asynchCalls = {};
634 for j = 1:length(a.asyncCallDests)
635 ac = containers.Map();
636 ac('entry') = a.asyncCallDests{j};
637 if j <= length(a.asyncCallMeans) && a.asyncCallMeans(j) ~= 1.0
638 ac('mean') = a.asyncCallMeans(j);
639 end
640 asynchCalls{end+1} = ac; %#ok<AGROW>
641 end
642 aj('asynchCalls') = asynchCalls;
643 end
644 actsJson{end+1} = aj; %#ok<AGROW>
645end
646result('activities') = actsJson;
647
648% --- Precedences ---
649precsJson = {};
650for i = 1:length(tasksList)
651 t = tasksList{i};
652 precs = t.precedences;
653 if isempty(precs), continue; end
654 for j = 1:length(precs)
655 p = precs(j);
656 pj = containers.Map();
657 pj('task') = t.name;
658
659 preType = p.preType;
660 postType = p.postType;
661
662 % Determine JSON precedence type and collect activity names
663 if preType == ActivityPrecedenceType.PRE_SEQ && postType == ActivityPrecedenceType.POST_SEQ
664 pj('type') = 'Serial';
665 pj('activities') = [p.preActs, p.postActs];
666 elseif preType == ActivityPrecedenceType.PRE_SEQ && postType == ActivityPrecedenceType.POST_AND
667 pj('type') = 'AndFork';
668 pj('activities') = [p.preActs, p.postActs];
669 elseif preType == ActivityPrecedenceType.PRE_AND && postType == ActivityPrecedenceType.POST_SEQ
670 pj('type') = 'AndJoin';
671 pj('activities') = [p.preActs, p.postActs];
672 elseif preType == ActivityPrecedenceType.PRE_SEQ && postType == ActivityPrecedenceType.POST_OR
673 pj('type') = 'OrFork';
674 pj('activities') = [p.preActs, p.postActs];
675 if ~isempty(p.postParams)
676 pj('probabilities') = p.postParams(:)';
677 end
678 elseif preType == ActivityPrecedenceType.PRE_OR && postType == ActivityPrecedenceType.POST_SEQ
679 pj('type') = 'OrJoin';
680 pj('activities') = [p.preActs, p.postActs];
681 elseif postType == ActivityPrecedenceType.POST_LOOP
682 pj('type') = 'Loop';
683 pj('activities') = [p.preActs, p.postActs];
684 if ~isempty(p.postParams)
685 pj('loopCount') = p.postParams(1);
686 end
687 else
688 continue;
689 end
690 precsJson{end+1} = pj; %#ok<AGROW>
691 end
692end
693if ~isempty(precsJson)
694 result('precedences') = precsJson;
695end
696end
697
698
699% =========================================================================
700% Distribution serialization
701% =========================================================================
702
703function d = dist2json(dist)
704% Convert a Distribution to a containers.Map for JSON output.
705if isempty(dist)
706 d = [];
707 return;
708end
709d = containers.Map();
710cn = builtin('class', dist);
711switch cn
712 case 'Disabled'
713 d('type') = 'Disabled';
714 case 'Immediate'
715 d('type') = 'Immediate';
716 case 'Exp'
717 d('type') = 'Exp';
718 params = containers.Map();
719 params('lambda') = dist.getParam(1).paramValue;
720 d('params') = params;
721 case 'Det'
722 d('type') = 'Det';
723 params = containers.Map();
724 params('value') = dist.getParam(1).paramValue;
725 d('params') = params;
726 case 'Erlang'
727 d('type') = 'Erlang';
728 params = containers.Map();
729 params('lambda') = dist.getParam(1).paramValue;
730 params('k') = dist.getParam(2).paramValue;
731 d('params') = params;
732 case 'HyperExp'
733 d('type') = 'HyperExp';
734 params = containers.Map();
735 p = dist.getParam(1).paramValue;
736 l1 = dist.getParam(2).paramValue;
737 l2 = dist.getParam(3).paramValue;
738 if isscalar(p)
739 params('p') = [p, 1-p];
740 params('lambda') = [l1, l2];
741 else
742 params('p') = p(:)';
743 params('lambda') = [l1, l2];
744 end
745 d('params') = params;
746 case 'Gamma'
747 d('type') = 'Gamma';
748 params = containers.Map();
749 params('alpha') = dist.getParam(1).paramValue;
750 params('beta') = dist.getParam(2).paramValue;
751 d('params') = params;
752 case 'Lognormal'
753 d('type') = 'Lognormal';
754 params = containers.Map();
755 params('mu') = dist.getParam(1).paramValue;
756 params('sigma') = dist.getParam(2).paramValue;
757 d('params') = params;
758 case 'Uniform'
759 d('type') = 'Uniform';
760 params = containers.Map();
761 params('a') = dist.getParam(1).paramValue;
762 params('b') = dist.getParam(2).paramValue;
763 d('params') = params;
764 case 'Zipf'
765 d('type') = 'Zipf';
766 params = containers.Map();
767 params('s') = dist.getParam(1).paramValue;
768 params('n') = dist.getParam(2).paramValue;
769 d('params') = params;
770 case 'Pareto'
771 d('type') = 'Pareto';
772 params = containers.Map();
773 params('alpha') = dist.getParam(1).paramValue;
774 params('scale') = dist.getParam(2).paramValue;
775 d('params') = params;
776 case {'PH', 'APH', 'Coxian', 'Cox2'}
777 d('type') = 'PH';
778 ph = containers.Map();
779 alpha = dist.getParam(1).paramValue;
780 T = dist.getParam(2).paramValue;
781 if isvector(alpha)
782 ph('alpha') = alpha(:)';
783 else
784 ph('alpha') = alpha;
785 end
786 ph('T') = T;
787 d('ph') = ph;
788 case 'MAP'
789 d('type') = 'MAP';
790 mapSpec = containers.Map();
791 mapSpec('D0') = dist.getParam(1).paramValue;
792 mapSpec('D1') = dist.getParam(2).paramValue;
793 d('map') = mapSpec;
794 case 'MMPP2'
795 % MMPP2 params are (lambda0, lambda1, sigma0, sigma1), not D0/D1.
796 % Use the process representation to get the actual D0/D1 matrices.
797 d('type') = 'MAP';
798 mapSpec = containers.Map();
799 proc = dist.getProcess();
800 mapSpec('D0') = full(proc{1});
801 mapSpec('D1') = full(proc{2});
802 d('map') = mapSpec;
803 case 'DiscreteSampler'
804 d('type') = 'DiscreteSampler';
805 params = containers.Map();
806 params('p') = dist.getParam(1).paramValue(:)';
807 params('x') = dist.getParam(2).paramValue(:)';
808 d('params') = params;
809 otherwise
810 % Fallback: fit from mean
811 try
812 m = dist.getMean();
813 d('type') = 'Exp';
814 fit = containers.Map();
815 fit('method') = 'fitMean';
816 fit('mean') = m;
817 d('fit') = fit;
818 catch
819 d = [];
820 end
821end
822end
823
824
825% =========================================================================
826% JSON encoding
827% =========================================================================
828
829function s = encode_value(val, indent)
830% Recursively encode a MATLAB value to JSON string.
831if nargin < 2, indent = 0; end
832pad = repmat(' ', 1, indent);
833pad2 = repmat(' ', 1, indent + 2);
834
835if isa(val, 'containers.Map')
836 ks = val.keys();
837 if isempty(ks)
838 s = '{}';
839 else
840 parts = cell(1, length(ks));
841 for i = 1:length(ks)
842 k = ks{i};
843 v = val(k);
844 parts{i} = sprintf('%s"%s": %s', pad2, json_escape(k), encode_value(v, indent + 2));
845 end
846 s = sprintf('{\n%s\n%s}', strjoin(parts, sprintf(',\n')), pad);
847 end
848elseif ischar(val) || isstring(val)
849 s = sprintf('"%s"', json_escape(char(val)));
850elseif islogical(val) && isscalar(val)
851 if val, s = 'true'; else, s = 'false'; end
852elseif isnumeric(val) && isscalar(val)
853 if isnan(val)
854 s = 'null';
855 elseif isinf(val)
856 if val > 0, s = '"Infinity"'; else, s = '"-Infinity"'; end
857 elseif val == floor(val) && abs(val) < 1e15
858 s = sprintf('%d', val);
859 else
860 s = sprintf('%.15g', val);
861 end
862elseif isnumeric(val) && isvector(val) && ~isscalar(val)
863 parts = cell(1, length(val));
864 for i = 1:length(val)
865 parts{i} = encode_value(val(i), 0);
866 end
867 s = ['[', strjoin(parts, ', '), ']'];
868elseif isnumeric(val) && ismatrix(val) && ~isvector(val)
869 rows = cell(1, size(val, 1));
870 for i = 1:size(val, 1)
871 rows{i} = encode_value(val(i,:), 0);
872 end
873 s = ['[', strjoin(rows, ', '), ']'];
874elseif iscell(val)
875 if isempty(val)
876 s = '[]';
877 else
878 parts = cell(1, length(val));
879 for i = 1:length(val)
880 parts{i} = sprintf('%s%s', pad2, encode_value(val{i}, indent + 2));
881 end
882 s = sprintf('[\n%s\n%s]', strjoin(parts, sprintf(',\n')), pad);
883 end
884elseif isstruct(val) && isscalar(val)
885 fnames = fieldnames(val);
886 if isempty(fnames)
887 s = '{}';
888 else
889 parts = cell(1, length(fnames));
890 for i = 1:length(fnames)
891 fn = fnames{i};
892 fv = val.(fn);
893 parts{i} = sprintf('%s"%s": %s', pad2, json_escape(fn), encode_value(fv, indent + 2));
894 end
895 s = sprintf('{\n%s\n%s}', strjoin(parts, sprintf(',\n')), pad);
896 end
897else
898 s = 'null';
899end
900end
901
902function s = json_escape(str)
903% Escape special characters for JSON strings.
904s = strrep(str, '\', '\\');
905s = strrep(s, '"', '\"');
906s = strrep(s, sprintf('\n'), '\n');
907s = strrep(s, sprintf('\r'), '\r');
908s = strrep(s, sprintf('\t'), '\t');
909end
910
911
912% =========================================================================
913% Helper functions
914% =========================================================================
915
916function s = node_type_str(node)
917% Get the JSON node type string for a node object.
918if isa(node, 'Source'), s = 'Source';
919elseif isa(node, 'Sink'), s = 'Sink';
920elseif isa(node, 'Delay'), s = 'Delay';
921elseif isa(node, 'Cache'), s = 'Cache';
922elseif isa(node, 'Place'), s = 'Place';
923elseif isa(node, 'Transition'), s = 'Transition';
924elseif isa(node, 'Queue'), s = 'Queue';
925elseif isa(node, 'Fork'), s = 'Fork';
926elseif isa(node, 'Join'), s = 'Join';
927elseif isa(node, 'Router'), s = 'Router';
928elseif isa(node, 'ClassSwitch'), s = 'ClassSwitch';
929else, s = 'Queue';
930end
931end
932
933function s = sched_id_to_str(id)
934% Map SchedStrategy numeric ID to schema-compatible string.
935if id == SchedStrategy.INF, s = 'INF';
936elseif id == SchedStrategy.FCFS, s = 'FCFS';
937elseif id == SchedStrategy.LCFS, s = 'LCFS';
938elseif id == SchedStrategy.LCFSPR, s = 'LCFSPR';
939elseif id == SchedStrategy.PS, s = 'PS';
940elseif id == SchedStrategy.DPS, s = 'DPS';
941elseif id == SchedStrategy.GPS, s = 'GPS';
942elseif id == SchedStrategy.SIRO, s = 'SIRO';
943elseif id == SchedStrategy.SJF, s = 'SJF';
944elseif id == SchedStrategy.LJF, s = 'LJF';
945elseif id == SchedStrategy.SEPT, s = 'SEPT';
946elseif id == SchedStrategy.LEPT, s = 'LEPT';
947elseif id == SchedStrategy.HOL, s = 'HOL';
948elseif id == SchedStrategy.FORK, s = 'FORK';
949elseif id == SchedStrategy.EXT, s = 'EXT';
950elseif id == SchedStrategy.REF, s = 'REF';
951elseif id == SchedStrategy.POLLING, s = 'POLLING';
952elseif id == SchedStrategy.PSPRIO, s = 'PSPRIO';
953elseif id == SchedStrategy.DPSPRIO, s = 'DPSPRIO';
954elseif id == SchedStrategy.GPSPRIO, s = 'GPSPRIO';
955elseif id == SchedStrategy.FCFSPRIO, s = 'FCFSPRIO';
956else, s = 'FCFS';
957end
958end
959
960function s = repl_to_str(id)
961% Map ReplacementStrategy numeric ID to schema string.
962if id == ReplacementStrategy.LRU, s = 'LRU';
963elseif id == ReplacementStrategy.FIFO, s = 'FIFO';
964elseif id == ReplacementStrategy.RR, s = 'RR';
965elseif id == ReplacementStrategy.SFIFO, s = 'SFIFO';
966else, s = 'LRU';
967end
968end
Definition mmt.m:93