Skip to content

Commit f6002b3

Browse files
authored
Merge branch 'main' into sunxd/auto_marginalization
2 parents 6d2bc02 + 53fd7bb commit f6002b3

File tree

20 files changed

+357
-108
lines changed

20 files changed

+357
-108
lines changed

.JuliaFormatter.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ignore=["misc/**"]

DoodleBUGS/runtime/server.jl

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const CORS_HEADERS = [
1212
]
1313

1414
function cors_handler(handler)
15-
return function(req::HTTP.Request)
15+
return function (req::HTTP.Request)
1616
if HTTP.method(req) == "OPTIONS"
1717
return HTTP.Response(200, CORS_HEADERS)
1818
else
@@ -25,15 +25,19 @@ end
2525

2626
function health_check_handler(req::HTTP.Request)
2727
@info "Health check ping (backend reachable)"
28-
return HTTP.Response(200, ["Content-Type" => "application/json"], JSON3.write(Dict("status" => "ok")))
28+
return HTTP.Response(
29+
200,
30+
["Content-Type" => "application/json"],
31+
JSON3.write(Dict("status" => "ok")),
32+
)
2933
end
3034

3135

3236
function run_model_handler(req::HTTP.Request)
3337
logs = String[]
3438
log!(logs, "Received /api/run request")
3539
log!(logs, "Backend processing started.")
36-
40+
3741
tmp_dir = mktempdir()
3842
log!(logs, "Created temporary working directory at: $(tmp_dir)")
3943

@@ -217,12 +221,17 @@ function run_model_handler(req::HTTP.Request)
217221
cmd = `$(julia_executable) --project=$(project_dir) --threads=auto $(script_path)`
218222

219223
log!(logs, "Executing script in worker process...")
220-
timeout_s = try Int(get(settings, :timeout_s, 0)) catch; 0 end
224+
timeout_s = try
225+
Int(get(settings, :timeout_s, 0))
226+
catch
227+
;
228+
0
229+
end
221230
if timeout_s <= 0
222231
run(cmd)
223232
log!(logs, "Script execution finished.")
224233
else
225-
proc = run(cmd; wait=false)
234+
proc = run(cmd; wait = false)
226235
log!(logs, "Worker process started; enforcing timeout of $(timeout_s)s")
227236
deadline = time() + timeout_s
228237
while process_running(proc) && time() < deadline
@@ -258,21 +267,33 @@ function run_model_handler(req::HTTP.Request)
258267
Dict("name" => "payload.json", "content" => read(payload_path, String)),
259268
]
260269
if isfile(results_path)
261-
push!(files_arr, Dict("name" => "results.json", "content" => read(results_path, String)))
270+
push!(
271+
files_arr,
272+
Dict("name" => "results.json", "content" => read(results_path, String)),
273+
)
262274
end
263275

264276
# Diagnostics: log attachment sizes
265277
sizes = String[]
266278
for f in files_arr
267-
push!(sizes, string(f["name"], "=", sizeof(f["content"])) )
279+
push!(sizes, string(f["name"], "=", sizeof(f["content"])))
268280
end
269-
log!(logs, "Attaching $(length(files_arr)) files; sizes(bytes): $(join(sizes, ", "))")
281+
log!(
282+
logs,
283+
"Attaching $(length(files_arr)) files; sizes(bytes): $(join(sizes, ", "))",
284+
)
270285

271286
response_body = Dict(
272287
"success" => true,
273-
"results" => (haskey(results_content, :summary) ? results_content[:summary] : (haskey(results_content, :results) ? results_content[:results] : Any[])),
274-
"summary" => (haskey(results_content, :summary) ? results_content[:summary] : Any[]),
275-
"quantiles" => (haskey(results_content, :quantiles) ? results_content[:quantiles] : Any[]),
288+
"results" => (
289+
haskey(results_content, :summary) ? results_content[:summary] :
290+
(haskey(results_content, :results) ? results_content[:results] : Any[])
291+
),
292+
"summary" =>
293+
(haskey(results_content, :summary) ? results_content[:summary] : Any[]),
294+
"quantiles" => (
295+
haskey(results_content, :quantiles) ? results_content[:quantiles] : Any[]
296+
),
276297
"logs" => logs,
277298
"files" => files_arr,
278299
)
@@ -293,21 +314,34 @@ function run_model_handler(req::HTTP.Request)
293314
push!(files_arr, Dict("name" => "model.bugs", "content" => model_code))
294315
end
295316
if @isdefined(run_script_content)
296-
push!(files_arr, Dict("name" => "run_script.jl", "content" => run_script_content))
317+
push!(
318+
files_arr,
319+
Dict("name" => "run_script.jl", "content" => run_script_content),
320+
)
297321
end
298322
if @isdefined(payload_path) && isfile(payload_path)
299-
push!(files_arr, Dict("name" => "payload.json", "content" => read(payload_path, String)))
323+
push!(
324+
files_arr,
325+
Dict("name" => "payload.json", "content" => read(payload_path, String)),
326+
)
300327
end
301328
if @isdefined(results_path) && isfile(results_path)
302-
push!(files_arr, Dict("name" => "results.json", "content" => read(results_path, String)))
329+
push!(
330+
files_arr,
331+
Dict("name" => "results.json", "content" => read(results_path, String)),
332+
)
303333
end
304334
error_response = Dict(
305335
"success" => false,
306336
"error" => sprint(showerror, e),
307337
"logs" => logs,
308338
"files" => files_arr,
309339
)
310-
return HTTP.Response(500, ["Content-Type" => "application/json"], JSON3.write(error_response))
340+
return HTTP.Response(
341+
500,
342+
["Content-Type" => "application/json"],
343+
JSON3.write(error_response),
344+
)
311345
finally
312346
# Clean up temp directory in background with retries to avoid EBUSY on Windows
313347
@async safe_rmdir(tmp_dir)
@@ -327,11 +361,11 @@ end
327361
Remove directory tree with retries and backoff. Resilient to transient EBUSY on Windows.
328362
Intended to be called in a background task.
329363
"""
330-
function safe_rmdir(path::AbstractString; retries::Int=6, sleep_s::Float64=0.25)
331-
for _ in 1:retries
364+
function safe_rmdir(path::AbstractString; retries::Int = 6, sleep_s::Float64 = 0.25)
365+
for _ = 1:retries
332366
try
333367
GC.gc()
334-
rm(path; recursive=true, force=true)
368+
rm(path; recursive = true, force = true)
335369
return
336370
catch e
337371
msg = sprint(showerror, e)

DoodleBUGS/src/components/canvas/GraphCanvas.vue

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,6 @@ const { enableGridSnapping, disableGridSnapping, setGridSize } = useGridSnapping
2929
3030
const validNodeTypes: NodeType[] = ['stochastic', 'deterministic', 'constant', 'observed', 'plate'];
3131
32-
interface CompoundDropPayload {
33-
node: NodeSingular;
34-
newParent: NodeSingular | null;
35-
oldParent: NodeSingular | null;
36-
}
37-
3832
const formatElementsForCytoscape = (elements: GraphElement[], errors: Map<string, ValidationError[]>): ElementDefinition[] => {
3933
return elements.map(el => {
4034
if (el.type === 'node') {
@@ -131,15 +125,18 @@ onMounted(() => {
131125
emit('canvas-tap', evt);
132126
});
133127
134-
cy.on('compound-drop', (_evt: EventObject, data: CompoundDropPayload) => {
135-
const { node, newParent } = data;
136-
const newParentId = newParent ? newParent.id() : undefined;
137-
138-
emit('node-moved', {
139-
nodeId: node.id(),
140-
position: node.position(),
141-
parentId: newParentId
142-
});
128+
// Capture the final position of a node after any drag operation (including grid snapping).
129+
// This is the definitive event for updating node positions and saving the 'preset' layout.
130+
cy.on('free', 'node', (evt: EventObject) => {
131+
const node = evt.target as NodeSingular;
132+
const parentCollection = node.parent();
133+
const parentId = parentCollection.length > 0 ? parentCollection.first().id() : undefined;
134+
135+
emit('node-moved', {
136+
nodeId: node.id(),
137+
position: node.position(),
138+
parentId: parentId,
139+
});
143140
});
144141
145142
cy.on('tap', 'node, edge', (evt: EventObject) => {
@@ -238,7 +235,7 @@ watch([() => props.elements, () => props.validationErrors], ([newElements, newEr
238235
}
239236
240237
.cytoscape-container.mode-add-edge {
241-
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>') 12 12, crosshair;
238+
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24" fill="none" stroke="%23333" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="2" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>') 12 12, crosshair;
242239
}
243240
244241
/* Custom drag and drop styling */
@@ -259,3 +256,4 @@ watch([() => props.elements, () => props.validationErrors], ([newElements, newEr
259256
background-color: rgba(255, 0, 0, 0.1) !important;
260257
}
261258
</style>
259+

DoodleBUGS/src/components/canvas/GraphEditor.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const emit = defineEmits<{
2121
(e: 'element-selected', element: GraphElement | null): void;
2222
(e: 'update:currentMode', mode: string): void;
2323
(e: 'update:currentNodeType', type: NodeType): void;
24+
(e: 'layout-updated', layoutName: string): void;
2425
}>();
2526
2627
const { elements: graphElements, addElement, updateElement, deleteElement } = useGraphElements();
@@ -173,6 +174,7 @@ const handleNodeMoved = (payload: { nodeId: string, position: { x: number; y: nu
173174
parent: payload.parentId
174175
};
175176
updateElement(updatedNode);
177+
emit('layout-updated', 'preset');
176178
}
177179
};
178180

DoodleBUGS/src/components/layouts/MainLayout.vue

Lines changed: 58 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
<!-- src/components/layouts/MainLayout.vue -->
21
<script setup lang="ts">
32
import { ref, computed, onUnmounted, watch, onMounted, nextTick } from 'vue';
43
import type { StyleValue } from 'vue';
@@ -123,11 +122,13 @@ onMounted(async () => {
123122
124123
watch(
125124
() => graphStore.currentGraphId,
126-
(newId, oldId) => {
127-
if (newId && newId !== oldId) {
125+
(newId) => {
126+
if (newId) {
128127
nextTick(() => {
129128
setTimeout(() => {
130-
handleGraphLayout('dagre');
129+
const graphContent = graphStore.graphContents.get(newId);
130+
const layoutToApply = graphContent?.lastLayout || 'dagre';
131+
handleGraphLayout(layoutToApply);
131132
}, 100);
132133
});
133134
}
@@ -242,20 +243,50 @@ const handleDeleteElement = (elementId: string) => {
242243
deleteElement(elementId);
243244
};
244245
246+
const handleLayoutUpdated = (layoutName: string) => {
247+
if (graphStore.currentGraphId) {
248+
graphStore.updateGraphLayout(graphStore.currentGraphId, layoutName);
249+
}
250+
};
251+
245252
const handleGraphLayout = (layoutName: string) => {
246-
const cy = getCyInstance();
247-
if (!cy) return;
248-
/* eslint-disable @typescript-eslint/no-explicit-any */
249-
const layoutOptionsMap: Record<string, LayoutOptions> = {
250-
dagre: { name: 'dagre', animate: true, animationDuration: 500, fit: true, padding: 30 } as any,
251-
fcose: { name: 'fcose', animate: true, animationDuration: 500, fit: true, padding: 30, randomize: false, quality: 'proof' } as any,
252-
cola: { name: 'cola', animate: true, fit: true, padding: 30, refresh: 1, avoidOverlap: true, infinite: false, centerGraph: true, flow: { axis: 'y', minSeparation: 30 }, handleDisconnected: false, randomize: false } as any,
253-
klay: { name: 'klay', animate: true, animationDuration: 500, fit: true, padding: 30, klay: { direction: 'RIGHT', edgeRouting: 'SPLINES', nodePlacement: 'LINEAR_SEGMENTS' } } as any,
254-
preset: { name: 'preset' }
255-
};
256-
/* eslint-enable @typescript-eslint/no-explicit-any */
257-
const options = layoutOptionsMap[layoutName] || layoutOptionsMap.preset;
258-
cy.layout(options).run();
253+
const cy = getCyInstance();
254+
if (!cy) return;
255+
256+
const shouldUpdatePositions = layoutName !== 'preset';
257+
258+
/* eslint-disable @typescript-eslint/no-explicit-any */
259+
const layoutOptionsMap: Record<string, LayoutOptions> = {
260+
dagre: { name: 'dagre', animate: true, animationDuration: 500, fit: true, padding: 30 } as any,
261+
fcose: { name: 'fcose', animate: true, animationDuration: 500, fit: true, padding: 30, randomize: false, quality: 'proof' } as any,
262+
cola: { name: 'cola', animate: true, fit: true, padding: 30, refresh: 1, avoidOverlap: true, infinite: false, centerGraph: true, flow: { axis: 'y', minSeparation: 30 }, handleDisconnected: false, randomize: false } as any,
263+
klay: { name: 'klay', animate: true, animationDuration: 500, fit: true, padding: 30, klay: { direction: 'RIGHT', edgeRouting: 'SPLINES', nodePlacement: 'LINEAR_SEGMENTS' } } as any,
264+
preset: { name: 'preset' }
265+
};
266+
/* eslint-enable @typescript-eslint/no-explicit-any */
267+
const options = layoutOptionsMap[layoutName] || layoutOptionsMap.preset;
268+
const layout = cy.layout(options);
269+
270+
if (shouldUpdatePositions) {
271+
layout.one('layoutstop', () => {
272+
const updatedElements = graphStore.currentGraphElements.map(el => {
273+
if (el.type === 'node') {
274+
const cyNode = cy.getElementById(el.id);
275+
if (cyNode.length > 0) {
276+
const newPos = cyNode.position();
277+
return { ...el, position: { x: newPos.x, y: newPos.y } };
278+
}
279+
}
280+
return el;
281+
});
282+
if (graphStore.currentGraphId) {
283+
graphStore.updateGraphElements(graphStore.currentGraphId, updatedElements);
284+
}
285+
});
286+
}
287+
288+
layout.run();
289+
handleLayoutUpdated(layoutName);
259290
};
260291
261292
const handlePaletteSelection = (itemType: PaletteItemType) => {
@@ -524,14 +555,15 @@ const runModel = async () => {
524555
}
525556
});
526557
const frontendStandaloneFile: GeneratedFile = { name: 'standalone.jl', content: frontendStandaloneScript };
527-
const backendFiles = (result.files ?? [])
528-
.filter(file => file.name !== 'standalone.jl')
529-
.map(file => ({
530-
name: file.name,
531-
content: typeof (file as any).content === 'string'
532-
? (file as any).content
533-
: ((file as any).content == null ? '' : JSON.stringify((file as any).content)),
534-
}));
558+
const backendFiles = (result.files ?? []).filter(file => file.name !== 'standalone.jl').map(file => {
559+
const content = (file as GeneratedFile & { content: unknown }).content;
560+
return {
561+
name: file.name,
562+
content: typeof content === 'string'
563+
? content
564+
: (content == null ? '' : JSON.stringify(content)),
565+
};
566+
});
535567
// Put standalone first so users see a guaranteed non-empty file
536568
executionStore.generatedFiles = [frontendStandaloneFile, ...backendFiles];
537569
// Debug log: show file sizes to help diagnose empty content
@@ -569,37 +601,6 @@ const runModel = async () => {
569601
}
570602
};
571603
572-
// Convert a Julia NamedTuple-like string to a JSON-friendly object (best-effort)
573-
function juliaTupleToJsonObject(juliaString: string): Record<string, unknown> {
574-
try {
575-
// Very conservative: if it's already JSON, return parsed
576-
try { return JSON.parse(juliaString); } catch { /* ignore */ }
577-
// Minimal parser for patterns like: (a = 1, b = [1,2], c = [ [..]; [..] ])
578-
// We will transform into JSON by replacing Julia syntax tokens carefully.
579-
let s = juliaString.trim();
580-
if (!s || s === '()') return {};
581-
// Replace tuple parens with braces
582-
s = s.replace(/^\(/, '{').replace(/\)$/, '}');
583-
// Replace "key =" with ""key":"
584-
s = s.replace(/(\w+)\s*=\s*/g, '"$1": ');
585-
// Replace Julia matrix [a b; c d] with array of arrays [[a,b],[c,d]] (best-effort)
586-
s = s.replace(/\[\s*([\s\S]*?)\s*\]/g, (match, content) => {
587-
// If it contains semicolons, treat as rows
588-
if (content.includes(';')) {
589-
const rows = content.split(';').map((row: string) => `[${row.trim().replace(/\s+/g, ', ')}]`);
590-
return `[${rows.join(',')}]`;
591-
}
592-
// Otherwise keep commas
593-
return `[${content.replace(/\s+/g, ' ')}]`;
594-
});
595-
// Remove trailing commas if any
596-
s = s.replace(/,\s*}/g, '}');
597-
return JSON.parse(s);
598-
} catch {
599-
return {};
600-
}
601-
}
602-
603604
const jsonToJulia = (jsonString: string): string => {
604605
try {
605606
const obj = JSON.parse(jsonString);
@@ -711,7 +712,7 @@ const handleGenerateStandalone = () => {
711712
<GraphEditor :is-grid-enabled="isGridEnabled" :grid-size="gridSize" :current-mode="currentMode"
712713
:elements="elements" :current-node-type="currentNodeType" :validation-errors="validationErrors"
713714
@update:current-mode="currentMode = $event" @update:current-node-type="currentNodeType = $event"
714-
@element-selected="handleElementSelected" />
715+
@element-selected="handleElementSelected" @layout-updated="handleLayoutUpdated" />
715716
</main>
716717

717718
<div class="resizer resizer-right" @mousedown.prevent="startResizeRight"></div>

0 commit comments

Comments
 (0)