Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
40d92e7
feat: scaffold cudf executor skeleton
lmeyerov Nov 19, 2025
fc10902
feat: wire same-path plan into cudf executor
lmeyerov Nov 19, 2025
d10a654
feat: add gfql where metadata and planner
lmeyerov Nov 19, 2025
ba32a3b
feat: implement cudf executor forward pass
lmeyerov Nov 19, 2025
7b37b4d
test: add cudf forward parity cases
lmeyerov Nov 19, 2025
3a6a427
docs: copy issue 837 plan into impl folder
lmeyerov Nov 19, 2025
7f3b022
chore: remove tracked cudf executor plan
lmeyerov Nov 20, 2025
59a6286
feat: add oracle fallback for cudf same-path executor
lmeyerov Nov 20, 2025
d921059
chore: gate cudf same-path executor and add strict-mode test
lmeyerov Nov 20, 2025
38b5106
chore: document cuDF same-path fallback gating
lmeyerov Nov 20, 2025
6e81c14
feat: add same-path pruning for cudf executor
lmeyerov Nov 20, 2025
83ffc75
feat: route cudf chains with WHERE to same-path executor
lmeyerov Nov 20, 2025
118d0b8
feat: enforce same-path summaries in cudf executor
lmeyerov Nov 20, 2025
a60bfe3
fix(gfql): preserve edge filters in cudf same-path
lmeyerov Nov 22, 2025
2e751fc
chore(gfql): fix same-path typing and mypy config
lmeyerov Nov 22, 2025
aadc80e
chore(gfql): clean chain typing imports
lmeyerov Nov 22, 2025
3b6849b
chore(gfql): silence dtype comparisons for mypy 3.8
lmeyerov Nov 22, 2025
8d8aab3
test(gfql): cover same-path cycles, branches, edge filters, cudf
lmeyerov Nov 23, 2025
5cd82b4
test(gfql): compress same-path topology coverage
lmeyerov Nov 23, 2025
e082b2d
chore(gfql): tighten inequality mask
lmeyerov Nov 23, 2025
8ff6fd6
test(gfql): add dispatch same-path dict case
lmeyerov Nov 23, 2025
8417c13
test(gfql): add chain/list dispatch same-path parity
lmeyerov Nov 23, 2025
ec3bcc2
fix(gfql): import same_path_types from gfql
lmeyerov Dec 24, 2025
534febb
fix(gfql): add package init and clean mypy config
lmeyerov Dec 24, 2025
3825e38
fix(gfql): add ref package init
lmeyerov Dec 24, 2025
124d50b
fix: align same-path hop slicing with oracle
lmeyerov Dec 24, 2025
a631817
test(gfql): add 8 feature composition tests for hop ranges + WHERE
lmeyerov Dec 26, 2025
36a49be
fix(gfql): support WHERE clauses for multi-hop edges in same-path exe…
lmeyerov Dec 26, 2025
104d87f
refactor(gfql): rename CuDFSamePathExecutor to DFSamePathExecutor
lmeyerov Dec 26, 2025
0446696
fix(gfql): comprehensive WHERE + multi-hop bug fixes and test amplifi…
lmeyerov Dec 27, 2025
b4cab76
refactor(gfql): vectorize df_executor for GPU compatibility
lmeyerov Dec 28, 2025
fd395f4
test(gfql): add df_executor profiling script
lmeyerov Dec 28, 2025
9fde773
test(gfql): add cProfile analysis and extended profiling
lmeyerov Dec 28, 2025
e765324
fix(gfql): multiple bug fixes for native vectorized path
lmeyerov Dec 28, 2025
94e0dab
fix(gfql): resolve flake8 lint errors (F841, W504)
lmeyerov Dec 28, 2025
940a60d
docs(plan): add Session 9 summary for CI fixes and verification update
lmeyerov Dec 28, 2025
ef8d25c
chore: remove plan.md from repo
lmeyerov Dec 28, 2025
118ef08
fix(gfql): resolve mypy type errors
lmeyerov Dec 28, 2025
c4e897c
fix(gfql): correct mypy ignore codes for iterrows
lmeyerov Dec 28, 2025
329fa3c
fix(gfql): use pd.Index for column assignment to satisfy py38 mypy
lmeyerov Dec 28, 2025
62f953e
fix(executor): keep all edges in valid multi-hop paths, not just term…
lmeyerov Dec 29, 2025
60b4926
test(executor): add predicate type tests with data type coverage
lmeyerov Dec 29, 2025
035d409
fix(hop): use edge endpoints for min_hops goal nodes, not lossy node …
lmeyerov Dec 30, 2025
9bac15f
test(executor): add TestMultiplePathLengths + fix oracle min_hops bug
lmeyerov Dec 30, 2025
cac14e0
test(executor): add Yannakakis principle and hop labeling tests
lmeyerov Dec 30, 2025
259b514
test(executor): add dual-engine testing (pandas + cudf)
lmeyerov Dec 30, 2025
63c8cad
fix(gfql): handle undirected edges in WHERE clause filtering
lmeyerov Dec 31, 2025
b33ba46
feat(gfql): oracle supports source/destination_node_match filters
lmeyerov Dec 31, 2025
6ac0b50
refactor(gfql): clean up df_executor comments and add dimension cover…
lmeyerov Jan 1, 2026
8e69335
refactor(tests): split df_executor tests into 4 focused modules
lmeyerov Jan 2, 2026
db3ceaf
fix(gfql): use safe_concat instead of non-existent compute.concat
lmeyerov Jan 2, 2026
400730a
fix(tests): inline helper functions to fix import errors in CI
lmeyerov Jan 2, 2026
7cf9cc4
fix(enumerator): correct hop labeling for paths outside min_hops range
lmeyerov Jan 2, 2026
065cb50
test(enumerator): add regression test for hop label shortest path bug
lmeyerov Jan 2, 2026
555e619
test(enumerator): add edge and reverse hop label regression tests
lmeyerov Jan 2, 2026
36ec66d
chore: add /plan.md to .gitignore and remove from PR
lmeyerov Jan 2, 2026
1630673
test(TDD): add failing tests for pandas Yannakakis entrypoint
lmeyerov Jan 2, 2026
ca69678
fix(executor): route pandas+WHERE to Yannakakis, use _run_native for all
lmeyerov Jan 2, 2026
96ff684
refactor(tests): extract shared helpers to conftest.py
lmeyerov Jan 2, 2026
a5fe760
refactor: move same_path_*.py to graphistry/compute/gfql/
lmeyerov Jan 2, 2026
50ad013
perf(hop): avoid GPU->CPU transfers in backtracking
lmeyerov Jan 2, 2026
0709b9a
fix(executor): oracle fallback for unfiltered start + multihop + WHERE
lmeyerov Jan 2, 2026
3283751
Revert "fix(executor): oracle fallback for unfiltered start + multiho…
lmeyerov Jan 2, 2026
91e84d1
refactor(executor): rename _run_oracle to _run_test_only_oracle
lmeyerov Jan 2, 2026
737b383
fix(executor): handle unfiltered start + multihop in native path
lmeyerov Jan 2, 2026
9fab0dc
test(executor): amplify coverage for unfiltered start + direction var…
lmeyerov Jan 2, 2026
69e8bce
refactor(executor): extract BFS helpers to reduce duplication
lmeyerov Jan 3, 2026
7936561
perf(chain): skip edge re-run for single-hop chains
lmeyerov Jan 3, 2026
30c74d0
test(executor): add feature parity tests for df_executor vs chain
lmeyerov Jan 3, 2026
8d38896
perf(chain): skip hop() in backward pass for simple single-hop edges
lmeyerov Jan 4, 2026
e702d1e
test(chain): add 38 tests for backward pass and combine_steps optimiz…
lmeyerov Jan 4, 2026
a2f29cf
fix(chain): correct output_min_hops/output_max_hops semantics to matc…
lmeyerov Jan 8, 2026
e4ff031
fix(chain): correct multi-hop detection using _is_simple_single_hop
lmeyerov Jan 8, 2026
5f9fbb3
fix(chain): add to_fixed_point check to _is_simple_single_hop
lmeyerov Jan 8, 2026
0a04a7b
refactor(chain): remove unnecessary getattr for to_fixed_point
lmeyerov Jan 9, 2026
3b5a3b8
fix(chain): validate where field type before cast in from_json
lmeyerov Jan 9, 2026
7602e7d
fix(gfql): validate WHERE clause payload structure in parse_where_json
lmeyerov Jan 9, 2026
29505c6
refactor(gfql): consolidate WHERE validation in parse_where_json
lmeyerov Jan 9, 2026
13ae177
refactor(ast): move is_simple_single_hop to ASTEdge method
lmeyerov Jan 9, 2026
2413637
refactor(chain): extract _filter_edges_by_endpoint helper and golf down
lmeyerov Jan 9, 2026
fa40c19
refactor(chain): further code golf - reduce diff by 46%
lmeyerov Jan 9, 2026
81b8a25
refactor(chain): split WHERE/df_executor to stacked PR
lmeyerov Jan 9, 2026
95ace8a
style(chain): fix flake8 lint errors
lmeyerov Jan 9, 2026
37933a6
fix(chain): add type narrowing for mypy
lmeyerov Jan 9, 2026
19640a7
fix(tests): update test_ref_enumerator import to use enumerator's col…
lmeyerov Jan 9, 2026
cf59089
docs(changelog): update for PR 885 chain optimizations
lmeyerov Jan 9, 2026
da328bc
docs(changelog): add bugfix entries for GFQL/chain/hop fixes
lmeyerov Jan 9, 2026
a3215b7
docs(changelog): remove entries already on master, keep only PR 885 a…
lmeyerov Jan 9, 2026
0c442ac
docs(changelog): restore master entries and add PR 885 additions
lmeyerov Jan 9, 2026
c14d079
docs(changelog): move where-related fix to PR 886
lmeyerov Jan 9, 2026
c407c08
docs(changelog): remove cuDF same-path entries from released section …
lmeyerov Jan 9, 2026
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ docs/source/demos
# Legacy - kept for backward compatibility
AI_PROGRESS/
/PLAN.md
/plan.md
plans/
tmp/
test_env/
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- **GFQL / reference**: Extended the pandas reference enumerator and parity tests to cover hop ranges, labeling, and slicing so GFQL correctness checks include the new traversal shapes.
- **Docs / GFQL**: Documented the external `tck-gfql` conformance harness and local run instructions in GFQL docs.

### Performance
- **GFQL / chain**: Optimized backward pass for simple single-hop edges by skipping full `hop()` call and using vectorized merge filtering instead (~50% faster on small graphs). Added `is_simple_single_hop()` method on `ASTEdge` for optimization eligibility checks.

### Fixed
- **Compute / hop**: Exact-hop traversals now prune branches that do not reach `min_hops`, avoid reapplying min-hop pruning in reverse passes, keep seeds in wavefront outputs, and reuse forward wavefronts when recomputing labels so edge/node hop labels stay aligned (fixes 3-hop branch inclusion issues and mislabeled slices).
- **GFQL / chain**: Fixed `output_min_hops`/`output_max_hops` semantics to correctly slice output nodes/edges matching oracle behavior.
- **GFQL / chain**: Fixed multi-hop detection in `_is_simple_single_hop` to check `to_fixed_point` flag and correctly identify optimization-eligible edges.
- **GFQL / enumerator**: Fixed hop labeling for paths outside `min_hops` range to use shortest path distance instead of enumeration order.
- **Compute / hop**: Fixed `min_hops` goal node calculation to use edge endpoints instead of lossy node merge, ensuring correct branch pruning.

### Tests
- **GFQL / hop**: Expanded `test_compute_hops.py` and GFQL parity suites to assert branch pruning, bounded outputs, label collision handling, and forward/reverse slice behavior.
- **Reference enumerator**: Added oracle parity tests for hop ranges and output slices to guard GFQL integrations.
- **GFQL / chain**: Added 78 tests for backward pass and combine_steps optimizations covering edge cases, direction semantics, hop labels, and multi-step chains.

### Infra
- **Tooling**: `bin/flake8.sh` / `bin/mypy.sh` now require installed tools (no auto-install), honor `FLAKE8_CMD` / `MYPY_CMD` and optional `MYPY_EXTRA_ARGS`; `bin/lint.sh` / `bin/typecheck.sh` resolve via uvx → python -m → bare.
Expand Down
21 changes: 21 additions & 0 deletions graphistry/compute/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,27 @@ def __init__(
def __repr__(self) -> str:
return f'ASTEdge(direction={self.direction}, edge_match={self.edge_match}, hops={self.hops}, min_hops={self.min_hops}, max_hops={self.max_hops}, output_min_hops={self.output_min_hops}, output_max_hops={self.output_max_hops}, label_node_hops={self.label_node_hops}, label_edge_hops={self.label_edge_hops}, label_seeds={self.label_seeds}, to_fixed_point={self.to_fixed_point}, source_node_match={self.source_node_match}, destination_node_match={self.destination_node_match}, name={self._name}, source_node_query={self.source_node_query}, destination_node_query={self.destination_node_query}, edge_query={self.edge_query})'

def is_simple_single_hop(self) -> bool:
"""Check if edge is single-hop without hop labels (safe to skip backward hop call)."""
hop_min = self.min_hops if self.min_hops is not None else (
self.hops if isinstance(self.hops, int) else 1
)
hop_max = self.max_hops if self.max_hops is not None else (
self.hops if isinstance(self.hops, int) else hop_min
)
if hop_min != 1 or hop_max != 1:
return False
# No fixed-point (unbounded) traversal
if self.to_fixed_point:
return False
# No hop labels that require traversal to compute
if self.label_node_hops or self.label_edge_hops or self.label_seeds:
return False
# No output slicing
if self.output_min_hops is not None or self.output_max_hops is not None:
return False
return True

def _validate_fields(self) -> None:
"""Validate edge fields."""
# Validate hops
Expand Down
214 changes: 154 additions & 60 deletions graphistry/compute/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,24 @@
logger = setup_logger(__name__)


def _filter_edges_by_endpoint(edges_df, nodes_df, node_id: str, edge_col: str):
"""Filter edges to those with edge_col values in nodes_df[node_id]."""
if nodes_df is None or not node_id or not edge_col or edge_col not in edges_df.columns:
return edges_df
ids = nodes_df[[node_id]].drop_duplicates().rename(columns={node_id: edge_col})
return edges_df.merge(ids, on=edge_col, how='inner')


###############################################################################


class Chain(ASTSerializable):

def __init__(self, chain: List[ASTObject], validate: bool = True) -> None:
def __init__(
self,
chain: List[ASTObject],
validate: bool = True,
) -> None:
self.chain = chain
if validate:
# Fail fast on invalid chains; matches documented automatic validation behavior
Expand Down Expand Up @@ -120,7 +132,10 @@ def from_json(cls, d: Dict[str, JSONVal], validate: bool = True) -> 'Chain':
f"Chain field must be a list, got {type(d['chain']).__name__}"
)

out = cls([ASTObject_from_json(op, validate=validate) for op in d['chain']], validate=validate)
out = cls(
[ASTObject_from_json(op, validate=validate) for op in d['chain']],
validate=validate,
)
return out

def to_json(self, validate=True) -> Dict[str, JSONVal]:
Expand Down Expand Up @@ -174,42 +189,58 @@ def combine_steps(

logger.debug('combine_steps ops pre: %s', [op for (op, _) in steps])
if kind == 'edges':
logger.debug('EDGES << recompute forwards given reduced set')
node_id = getattr(g, '_node')
src_col = getattr(g, '_source')
dst_col = getattr(g, '_destination')
full_nodes = getattr(g, '_nodes', None)

# For edges, we need to re-run forward ops but use the PREVIOUS forward step's nodes
# as prev_node_wavefront (not the current reverse step's nodes which include
# nodes reached during reverse traversal).
new_steps = []
for idx, (op, g_step) in enumerate(steps):
# Get prev_node_wavefront from the previous forward step (label_steps), not reverse result
if label_steps is not None and idx > 0:
prev_fwd_step = label_steps[idx - 1][1]
prev_wavefront_source = prev_fwd_step._nodes
else:
prev_wavefront_source = g_step._nodes

prev_node_wavefront = (
safe_merge(
full_nodes,
prev_wavefront_source[[node_id]],
on=node_id,
how='inner',
engine=engine,
) if full_nodes is not None and node_id is not None and prev_wavefront_source is not None else prev_wavefront_source
)
# Check if any edge op is multi-hop - if so, fall back to original re-run approach
# Multi-hop edges span multiple nodes, so simple endpoint filtering doesn't work
has_multihop = any(
isinstance(op, ASTEdge) and not op.is_simple_single_hop()
for op, _ in steps
)

if has_multihop:
# Multi-hop: re-run forward ops (can't use simple endpoint filtering)
logger.debug('EDGES << recompute forwards given reduced set (multihop)')
new_steps = []
for idx, (op, g_step) in enumerate(steps):
prev_src = label_steps[idx - 1][1]._nodes if label_steps and idx > 0 else g_step._nodes
prev_wf = (safe_merge(full_nodes, prev_src[[node_id]], on=node_id, how='inner', engine=engine)
if full_nodes is not None and node_id and prev_src is not None else prev_src)
new_steps.append((op, op(g=g.edges(g_step._edges), prev_node_wavefront=prev_wf, target_wave_front=None, engine=engine)))
steps = new_steps
else:
# Optimization: filter by valid endpoints instead of re-running op
logger.debug('EDGES << filter by valid endpoints (optimized)')
new_steps = []
for idx, (op, g_step) in enumerate(steps):
edges_df = g_step._edges
if edges_df is None or len(edges_df) == 0:
new_steps.append((op, g_step))
continue

prev_nodes = label_steps[idx - 1][1]._nodes if label_steps and idx > 0 else g._nodes
next_nodes = label_steps[idx + 1][1]._nodes if label_steps and idx + 1 < len(label_steps) else None
direction = getattr(op, 'direction', 'forward') if isinstance(op, ASTEdge) else 'forward'

if direction == 'undirected' and prev_nodes is not None and next_nodes is not None and node_id:
prev_ids = prev_nodes[[node_id]].drop_duplicates()
next_ids = next_nodes[[node_id]].drop_duplicates()
# Either direction: (src in prev, dst in next) OR (dst in prev, src in next)
fwd = edges_df.merge(prev_ids.rename(columns={node_id: src_col}), on=src_col, how='inner') \
.merge(next_ids.rename(columns={node_id: dst_col}), on=dst_col, how='inner')
rev = edges_df.merge(prev_ids.rename(columns={node_id: dst_col}), on=dst_col, how='inner') \
.merge(next_ids.rename(columns={node_id: src_col}), on=src_col, how='inner')
edges_df = df_concat(engine)([fwd, rev]).drop_duplicates()
else:
prev_col, next_col = (dst_col, src_col) if direction == 'reverse' else (src_col, dst_col)
edges_df = _filter_edges_by_endpoint(edges_df, prev_nodes, node_id, prev_col)
edges_df = _filter_edges_by_endpoint(edges_df, next_nodes, node_id, next_col)

new_steps.append((
op, # forward op
op(
g=g.edges(g_step._edges), # transition via any found edge
prev_node_wavefront=prev_node_wavefront,
target_wave_front=None,
engine=engine
)
))
steps = new_steps
new_steps.append((op, g_step.edges(edges_df)))
steps = new_steps

logger.debug('-----------[ combine %s ---------------]', kind)

Expand All @@ -220,29 +251,24 @@ def combine_steps(
def apply_output_slice(op: ASTObject, op_label: ASTObject, df):
if not isinstance(op_label, ASTEdge):
return df
out_min = getattr(op, 'output_min_hops', None)
out_max = getattr(op, 'output_max_hops', None)
# Fall back to forward op (with labels) when reverse op drops slice info
if out_min is None and out_max is None:
out_min = getattr(op_label, 'output_min_hops', None)
out_max = getattr(op_label, 'output_max_hops', None)
out_min = getattr(op, 'output_min_hops', None) or getattr(op_label, 'output_min_hops', None)
out_max = getattr(op, 'output_max_hops', None) or getattr(op_label, 'output_max_hops', None)
if out_min is None and out_max is None:
return df
label_col = op_label.label_node_hops if kind == 'nodes' else op_label.label_edge_hops
if label_col is None:
# best-effort fallback to any hop-like column
hop_like = [c for c in df.columns if 'hop' in c]
if not hop_like:
return df
label_col = hop_like[0]
if label_col not in df.columns:
label_col = hop_like[0] if hop_like else None
if not label_col or label_col not in df.columns:
return df
mask = df[label_col].notna()
# Keep seeds (hop=0 or NA) and hops in range
is_seed = (df[label_col] == 0) | df[label_col].isna()
in_range = df[label_col].notna() & (df[label_col] > 0)
if out_min is not None:
mask = mask & (df[label_col] >= out_min)
in_range &= df[label_col] >= out_min
if out_max is not None:
mask = mask & (df[label_col] <= out_max)
return df[mask]
in_range &= df[label_col] <= out_max
return df[is_seed | in_range]

dfs_to_concat = []
extra_step_dfs = []
Expand Down Expand Up @@ -361,6 +387,31 @@ def apply_output_slice(op: ASTObject, op_label: ASTObject, df):
if allowed_ids is not None and id in out_df.columns:
out_df[op._name] = out_df[op._name] & out_df[id].isin(allowed_ids)

# Final output_min/max_hops filter for nodes with hop=NA
if kind == 'nodes':
hop_cols = [c for c in out_df.columns if 'hop' in c.lower()]
edge_ops = [op for op, _ in steps if isinstance(op, ASTEdge)]
has_output_min = any(getattr(op, 'output_min_hops', None) is not None for op in edge_ops)
has_output_max = any(getattr(op, 'output_max_hops', None) is not None for op in edge_ops)
if (has_output_min or has_output_max) and hop_cols:
hop_col = hop_cols[0]
has_na = out_df[hop_col].isna()
if has_output_min:
# output_min_hops: drop hop=NA nodes (re-added via edge endpoint coverage)
out_df = out_df[~has_na]
elif has_na.any():
# output_max_hops only: keep hop=NA nodes that have a True tag (seeds)
tag_cols = [c for c in out_df.columns if c not in [id, 'id'] + hop_cols]
has_tag = pd.Series(False, index=out_df.index)
for col in tag_cols:
try:
vals = out_df[col].fillna(False)
if vals.dtype == 'bool' or vals.dtype == 'object':
has_tag |= vals.astype(bool)
except (TypeError, ValueError):
pass
out_df = out_df[~has_na | has_tag]

# Use safe_merge for final merge with automatic engine type coercion
g_df = getattr(g, df_fld)
out_df = safe_merge(out_df, g_df, on=id, how='left', engine=engine)
Expand Down Expand Up @@ -935,23 +986,66 @@ def _chain_impl(self: Plottable, ops: Union[List[ASTObject], Chain], engine: Uni
engine=engine_concrete
)
assert prev_loop_step._nodes is not None
g_step_reverse = (
(op.reverse())(

# Edges: edges used in step (subset matching prev_node_wavefront will be returned)
# Nodes: nodes reached in step (subset matching prev_node_wavefront will be returned)
g=g_step,
# Fast path: for simple single-hop edges, skip the full hop() call
# and use vectorized merge filtering instead. This saves ~50% time on small graphs.
use_fast_backward = (
isinstance(op, ASTEdge)
and op.is_simple_single_hop()
and g_step._edges is not None
and len(g_step._edges) > 0
and g._node is not None
and g._source is not None
and g._destination is not None
)

# check for hits against fully valid targets
# ast will replace g.node() with this as its starting points
if use_fast_backward:
assert isinstance(op, ASTEdge) # type narrowing for mypy
edges_df = g_step._edges
node_id, src_col, dst_col = g._node, g._source, g._destination
assert node_id is not None and src_col is not None and dst_col is not None
is_undirected = op.direction == 'undirected'
prev_set = set(prev_wavefront_nodes[node_id]) if prev_wavefront_nodes is not None else None
target_set = set(target_wave_front_nodes[node_id]) if target_wave_front_nodes is not None else None

# Filter edges by wavefronts
if is_undirected:
if prev_set and target_set:
mask = ((edges_df[src_col].isin(prev_set) & edges_df[dst_col].isin(target_set))
| (edges_df[src_col].isin(target_set) & edges_df[dst_col].isin(prev_set)))
edges_df = edges_df[mask]
elif prev_set:
edges_df = edges_df[edges_df[src_col].isin(prev_set) | edges_df[dst_col].isin(prev_set)]
elif target_set:
edges_df = edges_df[edges_df[src_col].isin(target_set) | edges_df[dst_col].isin(target_set)]
else:
next_col, prev_col = (src_col, dst_col) if op.direction == 'reverse' else (dst_col, src_col)
edges_df = _filter_edges_by_endpoint(edges_df, prev_wavefront_nodes, node_id, next_col)
edges_df = _filter_edges_by_endpoint(edges_df, target_wave_front_nodes, node_id, prev_col)

# Get result nodes
if len(edges_df) > 0:
if is_undirected:
target_node_ids = df_concat(engine_concrete)([
edges_df[[src_col]].rename(columns={src_col: node_id}),
edges_df[[dst_col]].rename(columns={dst_col: node_id})
]).drop_duplicates()
else:
target_col = dst_col if op.direction == 'reverse' else src_col
target_node_ids = edges_df[[target_col]].rename(columns={target_col: node_id}).drop_duplicates()
nodes_df = safe_merge(g._nodes, target_node_ids, on=node_id, how='inner', engine=engine_concrete) if g._nodes is not None else target_node_ids
else:
nodes_df = g._nodes.iloc[:0] if g._nodes is not None else None

g_step_reverse = g_step.nodes(nodes_df).edges(edges_df)
else:
# Fall back to full hop() traversal for complex cases
g_step_reverse = op.reverse()(
g=g_step,
prev_node_wavefront=prev_wavefront_nodes,

# only allow transitions to these nodes (vs prev_node_wavefront)
target_wave_front=target_wave_front_nodes,

engine=engine_concrete
)
)
g_stack_reverse.append(g_step_reverse)

import logging
Expand Down
Loading
Loading