Skip to content

Commit f812723

Browse files
committed
Diff Grok: LaTeX: Multi stage models upd
Update export to md and latex of multi stage models
1 parent e9ec90c commit f812723

2 files changed

Lines changed: 182 additions & 6 deletions

File tree

src/latex-export/examples/test.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
## Acid Production
2+
3+
*Gluconic acid (GA) production by Aspergillus niger modeling*
4+
5+
### Equations
6+
7+
$$
8+
\begin{aligned}
9+
\frac{dX}{dt} &= \mathrm{rX} \\
10+
\frac{dS}{dt} &= -\gamma \cdot \mathrm{rX} - \lambda \cdot X \\
11+
\frac{dO}{dt} &= \mathrm{Kla} \cdot \left(\mathrm{Cod} - O\right) - \delta \cdot \mathrm{rX} - \phi \cdot X \\
12+
\frac{dP}{dt} &= \alpha \cdot \mathrm{rX} + \beta \cdot X
13+
\end{aligned}
14+
$$
15+
16+
$$t \in \left[0,\, 100\right], \quad \Delta t = 0.1$$
17+
18+
### Expressions
19+
20+
$$
21+
\begin{aligned}
22+
\mu &= \frac{\mathrm{muM} \cdot S}{\mathrm{Ks} + S} \cdot \frac{O}{\mathrm{Ko} + O} \\
23+
\mathrm{rX} &= \mu \cdot X
24+
\end{aligned}
25+
$$
26+
27+
### Stage Transitions
28+
29+
**2-nd stage** (duration: $\mathrm{overall} - 60$)
30+
31+
Before this stage:
32+
33+
$$
34+
\begin{aligned}
35+
S \leftarrow S + 70
36+
\end{aligned}
37+
$$
38+
39+
### Initial Conditions (t = 0)
40+
41+
| Name | Value | Units |
42+
| --- | --- | --- |
43+
| $X$ | $5$ | kg/m³ |
44+
| $S$ | $150$ | kg/m³ |
45+
| $O$ | $7$ | kg/m³ |
46+
| $P$ | $0$ | kg/m³ |
47+
48+
### Parameters
49+
50+
| Name | Value | Units |
51+
| --- | --- | --- |
52+
| $\mathrm{overall}$ | $100$ | h |
53+
| $\mathrm{muM}$ | $0.668$ | 1/h |
54+
| $\alpha$ | $2.92$ | |
55+
| $\beta$ | $0.131$ | 1/h |
56+
| $\gamma$ | $2.12$ | |
57+
| $\lambda$ | $0.232$ | 1/h |
58+
| $\delta$ | $0.278$ | |
59+
| $\phi$ | $4.87e-3$ | 1/h |
60+
| $\mathrm{Ks}$ | $1.309e2$ | g/L |
61+
| $\mathrm{Ko}$ | $3.63e-4$ | g/L |
62+
| $\mathrm{Kla}$ | $1.7e-2$ | 1/s |
63+
| $\mathrm{Cod}$ | $15$ | kg/m³ |

src/latex-export/generator/document-builder.ts

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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

172208
function 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_t0\b/g, entries[0].value);
323+
if (entries.length > 1) result = result.replace(/\b_t1\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.eE+\-\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+
270365
function 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

Comments
 (0)