Skip to content

Commit 5437851

Browse files
authored
feat: add loop ID mapping to trace nodes and update UI labels (#1098)
* Add loop index * feat: add loop ID mapping to trace nodes and update UI labels * lint * doc lint
1 parent 5accec3 commit 5437851

File tree

5 files changed

+53
-18
lines changed

5 files changed

+53
-18
lines changed

rdagent/core/proposal.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,29 @@ class Trace(Generic[ASpecificScen, ASpecificKB]):
127127

128128
def __init__(self, scen: ASpecificScen, knowledge_base: ASpecificKB | None = None) -> None:
129129
self.scen: ASpecificScen = scen
130+
131+
# BEGIN: graph structure -------------------------
130132
self.hist: list[Trace.NodeType] = (
131133
[]
132134
) # List of tuples containing experiments and their feedback, organized over time.
133135
self.dag_parent: list[tuple[int, ...]] = [] # List of tuples representing parent indices in the DAG structure.
134-
# (,) represents no parent; (1,) presents one parent; (1, 2) represents two parents.
136+
# Definition:
137+
# - (,) represents no parent (root node in one tree);
138+
# - (1,) presents one parent;
139+
# - (1, 2) represents two parents (Multiple parent is not implemented yet).
140+
# Syntax sugar for the parent relationship:
141+
# - Only for selection:
142+
# - (-1,) indicates that select the last record node as parent.
143+
144+
# NOTE: the sequence of hist and dag_parent is organized by the order to record the experiment.
145+
# So it may be different from the order of the loop_id.
146+
# So we need an extra mapping to map the enqueue id back to the loop id.
147+
self.idx2loop_id: dict[int, int] = {}
148+
149+
# Design discussion:
150+
# - If we unifiy the loop_id and the enqueue id, we will have less recognition burden.
151+
# - If we use different id for loop and enqueue, we don't have to handle the placeholder logic.
152+
# END: graph structure -------------------------
135153

136154
# TODO: self.hist is 2-tuple now, remove hypothesis from it, change old code for this later.
137155
self.knowledge_base: ASpecificKB | None = knowledge_base
@@ -227,9 +245,13 @@ def get_selection(self, trace: Trace) -> tuple[int, ...] | None:
227245
checkpoint_idx represents the place where we want to create a new node.
228246
the return value should be the idx of target node (the parent of the new generating node).
229247
- `(-1, )` represents starting from the latest trial in the trace - default value
248+
249+
- NOTE: we don't encourage to use this option; It is confusing when we have multiple traces.
250+
230251
- `(idx, )` represents starting from the `idx`-th trial in the trace.
231252
- `None` represents starting from scratch (start a new trace)
232253
254+
233255
- More advanced selection strategies in `select.py`
234256
"""
235257

rdagent/log/ui/utils.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -606,13 +606,22 @@ def trace_figure(trace: Trace):
606606
for i in range(len(trace.dag_parent)):
607607
levels[i] = len(trace.get_parents(i))
608608

609+
def get_display_name(idx: int):
610+
"""
611+
Convert to index in the queue (enque id) to loop_idx for easier understanding.
612+
"""
613+
if hasattr(trace, "idx2loop_id"):
614+
# FIXME: only keep me after it is stable. Just for compatibility.
615+
return f"L{trace.idx2loop_id[idx]}"
616+
return f"L{idx}"
617+
609618
# Add nodes and edges
610619
edges = []
611620
for i, parents in enumerate(trace.dag_parent):
612621
for parent in parents:
613-
edges.append((f"L{parent}", f"L{i}"))
622+
edges.append((get_display_name(parent), get_display_name(i)))
614623
if len(parents) == 0:
615-
G.add_node(f"L{i}")
624+
G.add_node(get_display_name(i))
616625
G.add_edges_from(edges)
617626

618627
# Check if G is a path (a single line)
@@ -643,7 +652,7 @@ def trace_figure(trace: Trace):
643652
# Group nodes by number of ancestors, fewer ancestors are higher up
644653
layer_nodes = {}
645654
for idx, lvl in levels.items():
646-
layer_nodes.setdefault(lvl, []).append(f"L{idx}")
655+
layer_nodes.setdefault(lvl, []).append(get_display_name(idx))
647656

648657
# Layout by level: y axis is -lvl, x axis is evenly distributed
649658
pos = {}

rdagent/scenarios/data_science/loop.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,8 @@ def record(self, prev_out: dict[str, Any]):
200200

201201
exp: DSExperiment = None
202202

203+
cur_loop_id = prev_out[self.LOOP_IDX_KEY]
204+
203205
e = prev_out.get(self.EXCEPTION_KEY, None)
204206
if e is None:
205207
exp = prev_out["running"]
@@ -210,18 +212,20 @@ def record(self, prev_out: dict[str, Any]):
210212
# set the local selection to the trace as global selection, then set the DAG parent for the trace
211213
if exp.local_selection is not None:
212214
self.trace.set_current_selection(exp.local_selection)
213-
self.trace.sync_dag_parent_and_hist((exp, prev_out["feedback"]))
215+
self.trace.sync_dag_parent_and_hist((exp, prev_out["feedback"]), cur_loop_id)
214216
else:
215217
exp: DSExperiment = prev_out["direct_exp_gen"] if isinstance(e, CoderError) else prev_out["coding"]
216218

217219
# set the local selection to the trace as global selection, then set the DAG parent for the trace
218220
if exp.local_selection is not None:
219221
self.trace.set_current_selection(exp.local_selection)
222+
220223
self.trace.sync_dag_parent_and_hist(
221224
(
222225
exp,
223226
ExperimentFeedback.from_exception(e),
224-
)
227+
),
228+
cur_loop_id,
225229
)
226230

227231
if self.trace.sota_experiment() is None:

rdagent/scenarios/data_science/proposal/exp_gen/base.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,10 @@ def __str__(self) -> str:
4949

5050
class DSTrace(Trace[DataScienceScen, KnowledgeBase]):
5151
def __init__(self, scen: DataScienceScen, knowledge_base: KnowledgeBase | None = None) -> None:
52-
self.scen: DataScienceScen = scen
53-
self.hist: list[tuple[DSExperiment, ExperimentFeedback]] = []
54-
"""
55-
The dag_parent is a list of tuples, each tuple is the parent index of the current node.
56-
The first element of the tuple is the parent index, the rest are the parent indexes of the parent (not implemented yet).
57-
If the current node is the root node without parent, the tuple is empty.
58-
"""
59-
self.dag_parent: list[tuple[int, ...]] = [] # List of tuples representing parent indices in the DAG structure.
60-
# () represents no parent; (1,) presents one parent; (1, 2) represents two parents.
52+
super().__init__(scen, knowledge_base)
6153

62-
self.knowledge_base = knowledge_base
63-
self.current_selection: tuple[int, ...] = (-1,)
54+
# NOTE: this line is just for linting.
55+
self.hist: list[tuple[DSExperiment, ExperimentFeedback] | None] = []
6456

6557
self.sota_exp_to_submit: DSExperiment | None = None # grab the global best exp to submit
6658

@@ -95,6 +87,7 @@ def get_leaves(self) -> list[int, ...]:
9587
def sync_dag_parent_and_hist(
9688
self,
9789
exp_and_fb: tuple[Experiment, ExperimentFeedback],
90+
cur_loop_id: int,
9891
) -> None:
9992
"""
10093
Adding corresponding parent index to the dag_parent when the hist is going to be changed.
@@ -114,6 +107,7 @@ def sync_dag_parent_and_hist(
114107

115108
self.dag_parent.append((current_node_idx,))
116109
self.hist.append(exp_and_fb)
110+
self.idx2loop_id[len(self.hist) - 1] = cur_loop_id
117111

118112
def retrieve_search_list(
119113
self,

rdagent/utils/workflow/loop.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ class LoopBase:
9797
] = () # you can define a list of error that will withdraw current loop
9898

9999
EXCEPTION_KEY = "_EXCEPTION"
100+
LOOP_IDX_KEY = "_LOOP_IDX"
100101
SENTINEL = -1
101102

102103
_pbar: tqdm # progress bar instance
@@ -113,7 +114,9 @@ def __init__(self) -> None:
113114
self.step_idx: defaultdict[int, int] = defaultdict(int) # dict from loop index to next step index
114115
self.queue: asyncio.Queue[Any] = asyncio.Queue()
115116

116-
# Store step results for all loops in a nested dictionary: loop_prev_out[loop_index][step_name]
117+
# Store step results for all loops in a nested dictionary, following information will be stored:
118+
# - loop_prev_out[loop_index][step_name]: the output of the step function
119+
# - loop_prev_out[loop_index][<special keys like LOOP_IDX_KEY or EXCEPTION_KEY>]: the special keys
117120
self.loop_prev_out: dict[int, dict[str, Any]] = defaultdict(dict)
118121
self.loop_trace = defaultdict(list[LoopTrace]) # the key is the number of loop
119122
self.session_folder = Path(LOG_SETTINGS.trace_path) / "__session__"
@@ -213,6 +216,9 @@ async def _run_step(self, li: int, force_subproc: bool = False) -> None:
213216

214217
next_step_idx = si + 1
215218
step_forward = True
219+
# NOTE: each step are aware are of current loop index
220+
# It is very important to set it before calling the step function!
221+
self.loop_prev_out[li][self.LOOP_IDX_KEY] = li
216222
try:
217223
# Call function with current loop's output, await if coroutine or use ProcessPoolExecutor for sync if required
218224
if force_subproc:

0 commit comments

Comments
 (0)