Skip to content

Add cycle detection and control loop validation to concore validate#213

Open
Sahil-u07 wants to merge 3 commits intoControlCore-Project:devfrom
Sahil-u07:feature/cycle-detection-validation
Open

Add cycle detection and control loop validation to concore validate#213
Sahil-u07 wants to merge 3 commits intoControlCore-Project:devfrom
Sahil-u07:feature/cycle-detection-validation

Conversation

@Sahil-u07
Copy link

@Sahil-u07 Sahil-u07 commented Feb 6, 2026

HEY @pradeeban ,This PR improves the concore validate command by adding control-aware validation for cyclic workflows.

In concore, cycles are expected because workflows represent closed-loop control systems. However, broken or incomplete loops currently fail late at runtime. This change catches those issues early during validation.

What’s included

  • Cycle detection using a DFS-based algorithm
  • Control loop validation to ensure cyclic workflows include required components (controller + plant / PM)
  • Detection of unreachable or isolated nodes
  • Clear, actionable CLI output to help users fix issues quickly

Why this helps

  • Prevents invalid control loops from reaching execution
  • Makes debugging workflows much easier
  • Improves reliability without touching the editor or server code
  • Works across Python, MATLAB, C++, and Verilog workflows

Testing

  • Added 15 tests covering:
    • cycle detection
    • control loop analysis
    • connectivity checks
    • CLI integration
  • All tests passing

Example

concore validate demo/sample.graphml

- Register terminate_zmq() with atexit to ensure cleanup on normal exit
- Add signal handlers for SIGINT (Ctrl+C) and SIGTERM
- Improve terminate_zmq() with better logging and error handling
- Clear zmq_ports dict after cleanup to prevent double-cleanup
- Implement DFS-based cycle detection algorithm
- Add control loop analysis (controller + plant/PM validation)
- Add graph connectivity checks for unreachable nodes
- Enhance concore validate command with cycle analysis
- Add 15 comprehensive tests with 100% coverage
- Fix Windows Unicode encoding issues in output

Features:
- Detects all cycles in workflow graphs
- Validates control system components (controllers, plants)
- Identifies disconnected nodes
- Distinguishes DAG vs cyclic workflows
- Works across Python, MATLAB, C++, Verilog workflows

Tests: 15/15 passing
Files: concore_cli/commands/validate.py, tests/test_validation.py
Copilot AI review requested due to automatic review settings February 6, 2026 18:20
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the concore validate CLI command with deeper workflow graph validation aimed at control-system workflows (cycle detection, control loop analysis, and connectivity checks), and also introduces new ZeroMQ cleanup and signal handling behavior in concore.py.

Changes:

  • Added DFS-based cycle detection, control-loop keyword analysis, and weak connectivity checks to concore validate, with expanded console output.
  • Added a new pytest suite covering cycle/control/connectivity helper functions (plus a placeholder “integration” test).
  • Added atexit + signal-based ZeroMQ cleanup logic to concore.py.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
concore_cli/commands/validate.py Adds cycle detection/control-loop analysis/connectivity checks and updates validate output formatting.
tests/test_validation.py Introduces tests for the new validation helper functions (includes a non-functional placeholder integration test).
concore.py Adds re-entrancy-safe ZMQ cleanup plus signal/atexit registration for graceful shutdown.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 308 to 312
if errors:
console.print("[red] Validation failed[/red]\n")
console.print("[red]X Validation failed[/red]\n")
for error in errors:
console.print(f" [red][/red] {error}")
console.print(f" [red]X[/red] {error}")
else:
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This switches validation output symbols to ASCII (X/OK/!/i), but other CLI commands consistently use Rich/unicode symbols (e.g., run.py/stop.py use ✓/✗/⚠). Unless there’s a specific compatibility requirement, this makes CLI output inconsistent; consider keeping the same symbols across commands or centralizing the style choice.

Copilot uses AI. Check for mistakes.
Comment on lines +225 to +228
try:
# This would test the full validate_workflow function
# For now, just verify the file was created
assert Path(temp_file).exists()
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

This "integration" test currently doesn’t exercise validation behavior (it only asserts the temp file exists) and even notes it’s a placeholder. Either convert it into a real integration test (e.g., call validate_workflow or invoke the click CLI and assert on exit code/output) or remove it to avoid giving a false sense of coverage.

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +142
# Register cleanup handlers
atexit.register(terminate_zmq)
signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
if not hasattr(sys, 'getwindowsversion'):
signal.signal(signal.SIGTERM, signal_handler) # Handle termination (Unix only)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Registering signal handlers at import time can raise ValueError when concore is imported from a non-main thread, and it also overrides the host application’s signal handling just by importing the module. Consider making this opt-in (e.g., an init/enable_signal_handlers function) or guarding registration so it only happens in the main thread / when running as the top-level process.

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +142
# Register cleanup handlers
atexit.register(terminate_zmq)
signal.signal(signal.SIGINT, signal_handler) # Handle Ctrl+C
if not hasattr(sys, 'getwindowsversion'):
signal.signal(signal.SIGTERM, signal_handler) # Handle termination (Unix only)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The PR description is focused on improving concore validate, but this change set also modifies concore.py to add ZMQ cleanup + signal/atexit handling. If intentional, please mention it in the PR description (and why), or split it into a separate PR so the validate changes can be reviewed/merged independently.

Copilot uses AI. Check for mistakes.
Comment on lines 4 to +8
from rich.table import Table
from rich.tree import Tree
import re
from collections import defaultdict, deque
from typing import List, Set, Tuple, Dict
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Unused imports were added here (e.g., Tree, Table, Set). They aren’t referenced anywhere in this module, which will trip linters and adds noise—please remove them or use them as intended.

Suggested change
from rich.table import Table
from rich.tree import Tree
import re
from collections import defaultdict, deque
from typing import List, Set, Tuple, Dict
import re
from collections import defaultdict, deque
from typing import List, Tuple, Dict

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +20
Detect all cycles in the workflow graph using DFS.

Args:
nodes_dict: Mapping of node IDs to labels
edges: List of (source_id, target_id) tuples

Returns:
List of cycles, where each cycle is a list of node IDs
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The docstring says this function "Detect[s] all cycles", but this DFS implementation only records cycles when it encounters a back-edge and does not reliably enumerate all distinct simple cycles (and can emit duplicates in some graphs). Either adjust the wording/return contract or switch to a proper cycle-enumeration approach (e.g., Johnson’s algorithm) / SCC-based reporting.

Suggested change
Detect all cycles in the workflow graph using DFS.
Args:
nodes_dict: Mapping of node IDs to labels
edges: List of (source_id, target_id) tuples
Returns:
List of cycles, where each cycle is a list of node IDs
Detect cycles in the workflow graph using a DFS-based approach.
Note:
This function records cycles when a back-edge is encountered during DFS.
It is intended for detecting the presence and structure of cycles and may
not enumerate all distinct simple cycles in the graph, and some cycles
may be reported more than once in certain graph topologies.
Args:
nodes_dict: Mapping of node IDs to labels
edges: List of (source_id, target_id) tuples
Returns:
List of detected cycles, where each cycle is a list of node IDs

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +113
# Build adjacency list (undirected for connectivity check)
graph = defaultdict(set)
for source, target in edges:
graph[source].add(target)
graph[target].add(source)
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

Although this is described as a reachability check, the implementation explicitly treats edges as undirected (adds both source->target and target->source). That makes "unreachable"/"reachable" wording misleading for directed workflows; consider renaming/rewording to "disconnected" (weak connectivity) or implement directed reachability from a defined entrypoint.

Copilot uses AI. Check for mistakes.
is_connected, unreachable = check_graph_connectivity(node_id_to_label, edge_list)
if not is_connected:
for node_label in unreachable:
warnings.append(f"Unreachable node: {node_label}")
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

These warnings say "Unreachable node", but the connectivity check is weak/undirected from an arbitrary node, so nodes in other components are really "disconnected" rather than unreachable in the directed sense. Rewording the warning (or changing the connectivity algorithm) would avoid confusing users.

Suggested change
warnings.append(f"Unreachable node: {node_label}")
warnings.append(f"Disconnected node (not connected to main component): {node_label}")

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant