Skip to content

Commit e9ec90c

Browse files
committed
Diff Grok: LaTeX: Loop upd
Update export to md and latex of models with loops
1 parent e187bb5 commit e9ec90c

3 files changed

Lines changed: 161 additions & 7 deletions

File tree

‎.claude/settings.local.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"Bash(xargs wc:*)",
1717
"Bash(ls -la /c/Users/vmaka/Datagrok/diff-grok/src/latex-export/examples/*.ts)",
1818
"WebFetch(domain:datagrok.ai)",
19-
"Bash(npx tsx:*)"
19+
"Bash(npx tsx:*)",
20+
"Bash(node -e \":*)"
2021
]
2122
}
2223
}

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

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export function buildLatexDocument(
5050
parts.push(buildLatexAlign(model.expressions));
5151
}
5252

53+
const loopInfo = extractLoopInfo(model);
54+
if (loopInfo && loopInfo.updates.length > 0) {
55+
parts.push('\\subsection{Cyclic Update}');
56+
const lines = loopInfo.updates.map((u) => ` ${renderLoopUpdateLine(u)}`);
57+
parts.push(`Before each cycle:\n\\begin{align}\n${lines.join(' \\\\\n')}\n\\end{align}`);
58+
}
59+
5360
if (opts.includeInits && model.inits.length > 0) {
5461
parts.push(`\\subsection{${initsTitle(model)}}`);
5562
parts.push(buildLatexTable(model.inits));
@@ -104,6 +111,13 @@ export function buildMarkdownDocument(
104111
parts.push(buildMarkdownAlign(model.expressions));
105112
}
106113

114+
const loopInfo = extractLoopInfo(model);
115+
if (loopInfo && loopInfo.updates.length > 0) {
116+
parts.push('### Cyclic Update');
117+
const lines = loopInfo.updates.map((u) => ` ${renderLoopUpdateLine(u)}`);
118+
parts.push(`Before each cycle:\n\n$$\n\\begin{aligned}\n${lines.join(' \\\\\n')}\n\\end{aligned}\n$$`);
119+
}
120+
107121
if (opts.includeInits && model.inits.length > 0) {
108122
parts.push(`### ${initsTitle(model)}`);
109123
parts.push(buildMarkdownTable(model.inits));
@@ -133,7 +147,26 @@ function buildArgRangeLatex(model: ParsedModel): string {
133147
const start = model.argument.entries[0].value;
134148
const finish = model.argument.entries[1].value;
135149
const step = model.argument.entries[2].value;
136-
return `${arg} \\in \\left[${start},\\, ${finish}\\right], \\quad \\Delta ${arg} = ${step}`;
150+
151+
const loopInfo = extractLoopInfo(model);
152+
let effectiveFinish = finish;
153+
if (loopInfo) {
154+
const s = parseFloat(start);
155+
const f = parseFloat(finish);
156+
const n = parseFloat(loopInfo.count);
157+
if (!isNaN(s) && !isNaN(f) && !isNaN(n))
158+
effectiveFinish = String(s + n * (f - s));
159+
else
160+
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) {
165+
const duration = (!isNaN(parseFloat(start)) && !isNaN(parseFloat(finish))) ?
166+
String(parseFloat(finish) - parseFloat(start)) : `${finish} - ${start}`;
167+
result += `, \\quad ${arg}_{\\text{cycle}} = ${duration}`;
168+
}
169+
return result;
137170
}
138171

139172
function buildLatexArgRange(model: ParsedModel): string {
@@ -208,6 +241,32 @@ function compactExprItems(model: ParsedModel): string[] {
208241
});
209242
}
210243

244+
interface LoopUpdate { variable: string; value: string }
245+
246+
interface LoopInfo { count: string; updates: LoopUpdate[] }
247+
248+
function extractLoopInfo(model: ParsedModel): LoopInfo | undefined {
249+
if (model.loops.length === 0) return undefined;
250+
const loop = model.loops[0];
251+
let count = '';
252+
const updates: LoopUpdate[] = [];
253+
for (const entry of loop.entries) {
254+
if (entry.name === 'count')
255+
count = entry.value;
256+
else {
257+
const variable = entry.name.replace(/\s*\+$/, '');
258+
updates.push({variable, value: entry.value});
259+
}
260+
}
261+
return count ? {count, updates} : undefined;
262+
}
263+
264+
function renderLoopUpdateLine(u: LoopUpdate): string {
265+
const varLatex = identifierToLatex(u.variable);
266+
const valLatex = expressionToLatex(u.value);
267+
return `${varLatex} \\leftarrow ${varLatex} + ${valLatex}`;
268+
}
269+
211270
function buildLatexCompact(model: ParsedModel, opts: ConvertOptions): string {
212271
const parts: string[] = [];
213272

@@ -231,11 +290,17 @@ function buildLatexCompact(model: ParsedModel, opts: ConvertOptions): string {
231290
parts.push(`where\n${lines.join('\n')}`);
232291
}
233292

293+
const loopInfo = extractLoopInfo(model);
294+
if (loopInfo && loopInfo.updates.length > 0) {
295+
const lines = loopInfo.updates.map((u) => `\\[ ${renderLoopUpdateLine(u)} \\]`);
296+
parts.push(`before each cycle:\n${lines.join('\n')}`);
297+
}
298+
234299
if (opts.includeParameters && model.parameters.length > 0)
235-
parts.push(`${plural('Parameter', model.parameters.length)}:\n\n${buildLatexTable(model.parameters)}`);
300+
parts.push(`${plural('Parameter', model.parameters.length)}:\n${buildLatexList(model.parameters)}`);
236301

237302
if (opts.includeConstants && model.constants.length > 0)
238-
parts.push(`${plural('Constant', model.constants.length)}:\n\n${buildLatexTable(model.constants)}`);
303+
parts.push(`${plural('Constant', model.constants.length)}:\n${buildLatexList(model.constants)}`);
239304

240305
return parts.join('\n\n');
241306
}
@@ -263,15 +328,40 @@ function buildMarkdownCompact(model: ParsedModel, opts: ConvertOptions): string
263328
parts.push(`where\n${lines.join('\n')}`);
264329
}
265330

331+
const loopInfo = extractLoopInfo(model);
332+
if (loopInfo && loopInfo.updates.length > 0) {
333+
const lines = loopInfo.updates.map((u) => `$$${renderLoopUpdateLine(u)}$$`);
334+
parts.push(`before each cycle:\n${lines.join('\n')}`);
335+
}
336+
266337
if (opts.includeParameters && model.parameters.length > 0)
267-
parts.push(`${plural('Parameter', model.parameters.length)}:\n\n${buildMarkdownTable(model.parameters)}`);
338+
parts.push(`${plural('Parameter', model.parameters.length)}:\n${buildMarkdownList(model.parameters)}`);
268339

269340
if (opts.includeConstants && model.constants.length > 0)
270-
parts.push(`${plural('Constant', model.constants.length)}:\n\n${buildMarkdownTable(model.constants)}`);
341+
parts.push(`${plural('Constant', model.constants.length)}:\n${buildMarkdownList(model.constants)}`);
271342

272343
return parts.join('\n\n');
273344
}
274345

346+
function buildLatexList(entries: AnnotatedLine[]): string {
347+
const items = entries.map((e) => {
348+
const name = `$${identifierToLatex(e.name)}$`;
349+
const value = `$${e.value}$`;
350+
const units = e.units ? ` (${e.units})` : '';
351+
return ` \\item ${name} = ${value}${units}`;
352+
}).join('\n');
353+
return `\\begin{itemize}\n${items}\n\\end{itemize}`;
354+
}
355+
356+
function buildMarkdownList(entries: AnnotatedLine[]): string {
357+
return entries.map((e) => {
358+
const name = `$${identifierToLatex(e.name)}$`;
359+
const value = `$${e.value}$`;
360+
const units = e.units ? ` (${e.units})` : '';
361+
return `- ${name} = ${value}${units}`;
362+
}).join('\n');
363+
}
364+
275365
function buildLatexTable(entries: AnnotatedLine[]): string {
276366
const hasUnits = entries.some((e) => e.units);
277367
const cols = hasUnits ? 'lll' : 'll';

‎src/latex-export/generator/latex-generator.ts‎

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,12 @@ export function nodeToLatex(
6060
function renderBinary(node: ASTNode & { type: 'binary' }): string {
6161
const {op, left, right} = node;
6262

63-
// Division → \frac (no extra brackets needed)
63+
// Division → \frac; flatten into product of fractions when chain has multiple divisions
6464
if (op === '/') {
65+
const factors = flattenMulDivChain(node);
66+
const divisorCount = factors.filter((f) => f.isDivisor).length;
67+
if (divisorCount > 1)
68+
return renderProductOfFractions(factors);
6569
const l = nodeToLatex(left);
6670
const r = nodeToLatex(right);
6771
return operatorToLatex('/', l, r);
@@ -98,6 +102,65 @@ function getNodeOp(node: ASTNode): string | undefined {
98102
return undefined;
99103
}
100104

105+
/** Flatten a left-associative * / chain into factors with divisor flags. */
106+
function flattenMulDivChain(node: ASTNode & {type: 'binary'}):
107+
Array<{node: ASTNode; isDivisor: boolean}> {
108+
const factors: Array<{node: ASTNode; isDivisor: boolean}> = [];
109+
function collect(n: ASTNode): void {
110+
if (n.type === 'binary' && (n.op === '*' || n.op === '/')) {
111+
collect(n.left);
112+
factors.push({node: n.right, isDivisor: n.op === '/'});
113+
} else
114+
factors.push({node: n, isDivisor: false});
115+
}
116+
collect(node);
117+
return factors;
118+
}
119+
120+
/** Render a list of factors joined with \cdot, adding parens where needed. */
121+
function renderProductTerm(nodes: ASTNode[]): string {
122+
if (nodes.length === 0) return '1';
123+
if (nodes.length === 1) return nodeToLatex(nodes[0]);
124+
return nodes.map((n) => {
125+
const latex = nodeToLatex(n);
126+
if (n.type === 'binary' && (n.op === '+' || n.op === '-'))
127+
return `\\left(${latex}\\right)`;
128+
if (n.type === 'ternary')
129+
return `\\left(${latex}\\right)`;
130+
return latex;
131+
}).join(' \\cdot ');
132+
}
133+
134+
/** Render factors as a product of fractions. */
135+
function renderProductOfFractions(
136+
factors: Array<{node: ASTNode; isDivisor: boolean}>,
137+
): string {
138+
const groups: Array<{nums: ASTNode[]; dens: ASTNode[]}> = [];
139+
let cur: {nums: ASTNode[]; dens: ASTNode[]} = {nums: [], dens: []};
140+
141+
for (const f of factors) {
142+
if (f.isDivisor) {
143+
cur.dens.push(f.node);
144+
} else {
145+
if (cur.dens.length > 0) {
146+
groups.push(cur);
147+
cur = {nums: [], dens: []};
148+
}
149+
cur.nums.push(f.node);
150+
}
151+
}
152+
groups.push(cur);
153+
154+
const parts = groups.map((g) => {
155+
const num = renderProductTerm(g.nums);
156+
if (g.dens.length === 0) return num;
157+
const den = renderProductTerm(g.dens);
158+
return `\\frac{${num}}{${den}}`;
159+
});
160+
161+
return parts.join(' \\cdot ');
162+
}
163+
101164
/** Convert a derivative LHS to LaTeX (e.g. "dy/dt" → "\frac{dy}{dt}").
102165
* @param lhs derivative string, e.g. "d(X)/dt" or "dX/dt"
103166
* @returns LaTeX string

0 commit comments

Comments
 (0)