Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 48 additions & 11 deletions tensormap-backend/app/services/deep_learning.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import sys
import uuid as uuid_pkg
from collections import defaultdict, deque
from datetime import UTC
from typing import Any

Expand Down Expand Up @@ -403,19 +404,55 @@ def get_model_graph_service(db: Session, model_name: str, project_id: uuid_pkg.U


def _apply_auto_layout(graph: dict) -> None:
"""Assign default positions to nodes that lack one.
"""Assign deterministic DAG-aware positions to nodes lacking coordinates.

This is a simple vertical-stack layout used as a fallback so that the
ReactFlow canvas can render the graph without crashing. For a more
sophisticated layout (e.g. dagre), see:
https://reactflow.dev/docs/examples/layout/dagre/

TODO: Replace with a proper auto-layout algorithm (e.g. dagre) in a
follow-up issue.
Existing node positions are preserved. New positions are generated using a
lightweight layered layout derived from edge directions so ReactFlow can
render meaningful structure even for legacy graphs without saved positions.
Comment on lines +407 to +411
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docstring says nodes “lacking coordinates”, but the implementation only assigns positions when the "position" key is absent (it does not fill missing/partial x/y). Consider rewording to “lacking a position” (or expand logic to handle partial coordinates) so behavior and docs match.

Copilot uses AI. Check for mistakes.
"""
for i, node in enumerate(graph.get("nodes", [])):
if "position" not in node:
node["position"] = {"x": 100.0, "y": float(i * 200)}
nodes = graph.get("nodes", [])
if not nodes:
return

by_id = {str(node.get("id")): node for node in nodes if node.get("id") is not None}
adjacency: dict[str, set[str]] = defaultdict(set)
indegree: dict[str, int] = {node_id: 0 for node_id in by_id}

for edge in graph.get("edges", []):
source = str(edge.get("source")) if edge.get("source") is not None else None
target = str(edge.get("target")) if edge.get("target") is not None else None
if source in by_id and target in by_id and target not in adjacency[source]:
adjacency[source].add(target)
indegree[target] += 1

queue = deque(sorted(node_id for node_id, degree in indegree.items() if degree == 0))
layer: dict[str, int] = {node_id: 0 for node_id in queue}
topo: list[str] = []

while queue:
node_id = queue.popleft()
topo.append(node_id)
for nxt in sorted(adjacency[node_id]):
layer[nxt] = max(layer.get(nxt, 0), layer.get(node_id, 0) + 1)
indegree[nxt] -= 1
if indegree[nxt] == 0:
queue.append(nxt)

# Cycles or disconnected leftovers: place them into successive layers.
leftovers = [node_id for node_id in sorted(by_id) if node_id not in topo]
max_layer = max(layer.values(), default=0)
for idx, node_id in enumerate(leftovers, start=1):
layer[node_id] = max_layer + idx

rows_by_layer: dict[int, list[str]] = defaultdict(list)
for node_id in sorted(by_id):
rows_by_layer[layer.get(node_id, 0)].append(node_id)

for x_layer, node_ids in rows_by_layer.items():
for y_row, node_id in enumerate(node_ids):
node = by_id[node_id]
if "position" not in node:
node["position"] = {"x": float(250 * x_layer + 100), "y": float(140 * y_row + 100)}
Comment on lines +421 to +455
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new layered layout logic isn’t covered by assertions that edge direction affects X-layering or that cycles/disconnected graphs are handled deterministically (current tests only assert positions exist). Add a unit test that builds a small multi-layer DAG (and optionally a simple cycle) and asserts the relative x positions/layer ordering are as expected.

Copilot uses AI. Check for mistakes.


def _unflatten_model_configs(configs: list[ModelConfigs]) -> dict:
Expand Down
Loading