@@ -57,6 +57,19 @@ export function buildLatexDocument(
5757 parts . push ( `Before each cycle:\n\\begin{align}\n${ lines . join ( ' \\\\\n' ) } \n\\end{align}` ) ;
5858 }
5959
60+ const stages = extractStageInfos ( model ) ;
61+ if ( stages . length > 0 ) {
62+ parts . push ( '\\subsection{Stage Transitions}' ) ;
63+ for ( const stage of stages ) {
64+ const dur = stage . duration ? ` (duration: $${ expressionToLatex ( stage . duration ) } $)` : '' ;
65+ parts . push ( `\\textbf{${ stage . stageName } }${ dur } ` ) ;
66+ if ( stage . transitions . length > 0 ) {
67+ const lines = stage . transitions . map ( ( t ) => ` ${ renderTransitionLine ( t ) } ` ) ;
68+ parts . push ( `Before this stage:\n\\begin{align}\n${ lines . join ( ' \\\\\n' ) } \n\\end{align}` ) ;
69+ }
70+ }
71+ }
72+
6073 if ( opts . includeInits && model . inits . length > 0 ) {
6174 parts . push ( `\\subsection{${ initsTitle ( model ) } }` ) ;
6275 parts . push ( buildLatexTable ( model . inits ) ) ;
@@ -118,6 +131,19 @@ export function buildMarkdownDocument(
118131 parts . push ( `Before each cycle:\n\n$$\n\\begin{aligned}\n${ lines . join ( ' \\\\\n' ) } \n\\end{aligned}\n$$` ) ;
119132 }
120133
134+ const stages = extractStageInfos ( model ) ;
135+ if ( stages . length > 0 ) {
136+ parts . push ( '### Stage Transitions' ) ;
137+ for ( const stage of stages ) {
138+ const dur = stage . duration ? ` (duration: $${ expressionToLatex ( stage . duration ) } $)` : '' ;
139+ parts . push ( `**${ stage . stageName } **${ dur } ` ) ;
140+ if ( stage . transitions . length > 0 ) {
141+ const lines = stage . transitions . map ( ( t ) => ` ${ renderTransitionLine ( t ) } ` ) ;
142+ parts . push ( `Before this stage:\n\n$$\n\\begin{aligned}\n${ lines . join ( ' \\\\\n' ) } \n\\end{aligned}\n$$` ) ;
143+ }
144+ }
145+ }
146+
121147 if ( opts . includeInits && model . inits . length > 0 ) {
122148 parts . push ( `### ${ initsTitle ( model ) } ` ) ;
123149 parts . push ( buildMarkdownTable ( model . inits ) ) ;
@@ -149,24 +175,34 @@ function buildArgRangeLatex(model: ParsedModel): string {
149175 const step = model . argument . entries [ 2 ] . value ;
150176
151177 const loopInfo = extractLoopInfo ( model ) ;
178+ const stages = extractStageInfos ( model ) ;
152179 let effectiveFinish = finish ;
180+ let suffix = '' ;
181+
153182 if ( loopInfo ) {
183+ // Cyclic model: total = start + count * (finish - start)
154184 const s = parseFloat ( start ) ;
155185 const f = parseFloat ( finish ) ;
156186 const n = parseFloat ( loopInfo . count ) ;
157187 if ( ! isNaN ( s ) && ! isNaN ( f ) && ! isNaN ( n ) )
158188 effectiveFinish = String ( s + n * ( f - s ) ) ;
159189 else
160190 effectiveFinish = `${ start } + ${ loopInfo . count } \\cdot (${ finish } - ${ start } )` ;
161- }
162-
163- let result = `${ arg } \\in \\left[${ start } ,\\, ${ effectiveFinish } \\right], \\quad \\Delta ${ arg } = ${ step } ` ;
164- if ( loopInfo ) {
165191 const duration = ( ! isNaN ( parseFloat ( start ) ) && ! isNaN ( parseFloat ( finish ) ) ) ?
166192 String ( parseFloat ( finish ) - parseFloat ( start ) ) : `${ finish } - ${ start } ` ;
167- result += `, \\quad ${ arg } _{\\text{cycle}} = ${ duration } ` ;
193+ suffix = `, \\quad ${ arg } _{\\text{cycle}} = ${ duration } ` ;
194+ } else if ( stages . length > 0 ) {
195+ // Multistage model: total = start + stage1_duration + stage2_duration + ...
196+ const durations = [ finish , ...stages . map ( ( s ) => s . duration ) . filter ( Boolean ) as string [ ] ] ;
197+ const totalExpr = `${ start } + ${ durations . join ( ' + ' ) } ` ;
198+ const total = tryEvalNumeric ( totalExpr , model ) ;
199+ if ( total !== undefined )
200+ effectiveFinish = String ( total ) ;
201+ else
202+ effectiveFinish = totalExpr ;
168203 }
169- return result ;
204+
205+ return `${ arg } \\in \\left[${ start } ,\\, ${ effectiveFinish } \\right], \\quad \\Delta ${ arg } = ${ step } ${ suffix } ` ;
170206}
171207
172208function buildLatexArgRange ( model : ParsedModel ) : string {
@@ -267,6 +303,65 @@ function renderLoopUpdateLine(u: LoopUpdate): string {
267303 return `${ varLatex } \\leftarrow ${ varLatex } + ${ valLatex } ` ;
268304}
269305
306+ interface StageTransition {
307+ variable : string ;
308+ value : string ;
309+ isIncrement : boolean ;
310+ }
311+
312+ interface StageInfo {
313+ stageName : string ;
314+ duration ?: string ;
315+ transitions : StageTransition [ ] ;
316+ }
317+
318+ /** Replace internal argument refs (_t0, _t1, _h) with actual values from #argument. */
319+ function resolveArgRefs ( expr : string , model : ParsedModel ) : string {
320+ const entries = model . argument . entries ;
321+ let result = expr ;
322+ if ( entries . length > 0 ) result = result . replace ( / \b _ t 0 \b / g, entries [ 0 ] . value ) ;
323+ if ( entries . length > 1 ) result = result . replace ( / \b _ t 1 \b / g, entries [ 1 ] . value ) ;
324+ if ( entries . length > 2 ) result = result . replace ( / \b _ h \b / g, entries [ 2 ] . value ) ;
325+ return result ;
326+ }
327+
328+ /** Try to evaluate a simple arithmetic expression numerically, resolving parameter/constant names. */
329+ function tryEvalNumeric ( expr : string , model : ParsedModel ) : number | undefined {
330+ let resolved = expr ;
331+ for ( const p of [ ...model . parameters , ...model . constants ] )
332+ resolved = resolved . replace ( new RegExp ( `\\b${ p . name } \\b` , 'g' ) , p . value ) ;
333+ const val = parseFloat ( resolved ) ;
334+ if ( ! isNaN ( val ) && / ^ [ \d . e E + \- \s * / ( ) ] + $ / . test ( resolved . trim ( ) ) ) {
335+ try { return Function ( `"use strict"; return (${ resolved } )` ) ( ) as number ; } catch { /* skip */ }
336+ }
337+ return undefined ;
338+ }
339+
340+ function extractStageInfos ( model : ParsedModel ) : StageInfo [ ] {
341+ return model . updates . map ( ( block ) => {
342+ let duration : string | undefined ;
343+ const transitions : StageTransition [ ] = [ ] ;
344+ for ( const entry of block . entries ) {
345+ if ( entry . name === 'duration' ) {
346+ duration = resolveArgRefs ( entry . value , model ) ;
347+ } else {
348+ const isIncrement = entry . name . endsWith ( '+' ) ;
349+ const variable = isIncrement ? entry . name . replace ( / \s * \+ $ / , '' ) : entry . name ;
350+ transitions . push ( { variable, value : entry . value , isIncrement} ) ;
351+ }
352+ }
353+ return { stageName : block . stageName , duration, transitions} ;
354+ } ) ;
355+ }
356+
357+ function renderTransitionLine ( t : StageTransition ) : string {
358+ const varLatex = identifierToLatex ( t . variable ) ;
359+ const valLatex = expressionToLatex ( t . value ) ;
360+ if ( t . isIncrement )
361+ return `${ varLatex } \\leftarrow ${ varLatex } + ${ valLatex } ` ;
362+ return `${ varLatex } \\leftarrow ${ valLatex } ` ;
363+ }
364+
270365function buildLatexCompact ( model : ParsedModel , opts : ConvertOptions ) : string {
271366 const parts : string [ ] = [ ] ;
272367
@@ -296,6 +391,15 @@ function buildLatexCompact(model: ParsedModel, opts: ConvertOptions): string {
296391 parts . push ( `before each cycle:\n${ lines . join ( '\n' ) } ` ) ;
297392 }
298393
394+ const stages = extractStageInfos ( model ) ;
395+ for ( const stage of stages ) {
396+ if ( stage . transitions . length > 0 ) {
397+ const dur = stage . duration ? ` ($${ expressionToLatex ( stage . duration ) } $)` : '' ;
398+ const lines = stage . transitions . map ( ( t ) => `\\[ ${ renderTransitionLine ( t ) } \\]` ) ;
399+ parts . push ( `before ${ stage . stageName } ${ dur } :\n${ lines . join ( '\n' ) } ` ) ;
400+ }
401+ }
402+
299403 if ( opts . includeParameters && model . parameters . length > 0 )
300404 parts . push ( `${ plural ( 'Parameter' , model . parameters . length ) } :\n${ buildLatexList ( model . parameters ) } ` ) ;
301405
@@ -334,6 +438,15 @@ function buildMarkdownCompact(model: ParsedModel, opts: ConvertOptions): string
334438 parts . push ( `before each cycle:\n${ lines . join ( '\n' ) } ` ) ;
335439 }
336440
441+ const stages = extractStageInfos ( model ) ;
442+ for ( const stage of stages ) {
443+ if ( stage . transitions . length > 0 ) {
444+ const dur = stage . duration ? ` ($${ expressionToLatex ( stage . duration ) } $)` : '' ;
445+ const lines = stage . transitions . map ( ( t ) => `$$${ renderTransitionLine ( t ) } $$` ) ;
446+ parts . push ( `before ${ stage . stageName } ${ dur } :\n${ lines . join ( '\n' ) } ` ) ;
447+ }
448+ }
449+
337450 if ( opts . includeParameters && model . parameters . length > 0 )
338451 parts . push ( `${ plural ( 'Parameter' , model . parameters . length ) } :\n${ buildMarkdownList ( model . parameters ) } ` ) ;
339452
0 commit comments