From 29256e8141a8f6847d33631aedcf8d6afb49eee9 Mon Sep 17 00:00:00 2001 From: Julian Geiger Date: Tue, 21 Oct 2025 17:28:05 +0200 Subject: [PATCH] first version --- example_workflows/WHILE_LOOP_COMPARISON.md | 543 ++++++++++++++++++ .../while_loop_recursive/README.md | 460 +++++++++++++++ .../while_loop_recursive/fibonacci.json | 83 +++ .../recursive_accumulator.json | 59 ++ .../simple_recursive.json | 47 ++ .../while_loop_recursive/test_recursive.py | 218 +++++++ .../while_loop_recursive/workflow.py | 105 ++++ 7 files changed, 1515 insertions(+) create mode 100644 example_workflows/WHILE_LOOP_COMPARISON.md create mode 100644 example_workflows/while_loop_recursive/README.md create mode 100644 example_workflows/while_loop_recursive/fibonacci.json create mode 100644 example_workflows/while_loop_recursive/recursive_accumulator.json create mode 100644 example_workflows/while_loop_recursive/simple_recursive.json create mode 100644 example_workflows/while_loop_recursive/test_recursive.py create mode 100644 example_workflows/while_loop_recursive/workflow.py diff --git a/example_workflows/WHILE_LOOP_COMPARISON.md b/example_workflows/WHILE_LOOP_COMPARISON.md new file mode 100644 index 0000000..4823a3e --- /dev/null +++ b/example_workflows/WHILE_LOOP_COMPARISON.md @@ -0,0 +1,543 @@ +# While Loop Implementation Comparison + +This document compares two approaches for implementing while loops in Python Workflow Definition. + +## Approaches + +### 1. **While Node Approach** +**Location**: `example_workflows/while_loop/` +**Branch**: (separate branch with WhileNode implementation) + +Adds a new `PythonWorkflowDefinitionWhileNode` to the schema with support for: +- Function-based or expression-based conditions +- Function-based or nested workflow bodies +- Safe expression evaluation +- Explicit iteration limits + +### 2. **Recursive Approach** +**Location**: `example_workflows/while_loop_recursive/` +**Branch**: `main` (no schema changes) + +Uses native Python recursion within function nodes - no new node types needed. + +--- + +## Quick Comparison + +| Feature | While Node | Recursive | +|---------|-----------|-----------| +| **Schema Changes** | ❌ New node type | ✅ None required | +| **Max Iterations** | ✅ 1000+ | ❌ ~1000 (recursion limit) | +| **Memory Efficiency** | ✅ Constant | ❌ Stack grows | +| **Performance** | ✅ Native loop | ❌ Function call overhead | +| **Visualization** | ✅ Loop node visible | ❌ Just function node | +| **Backend Support** | ⚠️ Needs implementation | ✅ Works everywhere | +| **Debugging** | ✅ Iteration tracking | ❌ Deep call stacks | +| **Functional Style** | ⚠️ Hybrid | ✅ Pure functions | + +--- + +## Detailed Comparison + +### Schema & Implementation + +#### While Node Approach +```json +{ + "id": 2, + "type": "while", + "conditionExpression": "m < n", + "bodyFunction": "workflow.increment_m", + "maxIterations": 1000 +} +``` + +**Requires**: +- New `PythonWorkflowDefinitionWhileNode` class +- Expression evaluator (`expression_eval.py`) +- Updated discriminated union +- Backend execution support + +#### Recursive Approach +```json +{ + "id": 2, + "type": "function", + "value": "workflow.while_loop" +} +``` + +**Requires**: +- Nothing! Standard function node +- Recursive function implementation +- No backend changes + +--- + +### Code Examples + +#### Example: Count from 0 to 10 + +**While Node Approach**: + +*Workflow JSON* (`while_loop/simple_counter_expression.json`): +```json +{ + "nodes": [ + {"id": 0, "type": "input", "name": "n", "value": 10}, + {"id": 1, "type": "input", "name": "m", "value": 0}, + {"id": 2, "type": "while", + "conditionExpression": "m < n", + "bodyFunction": "workflow.increment_m"}, + {"id": 3, "type": "output", "name": "result"} + ] +} +``` + +*Python* (`while_loop/workflow.py`): +```python +def increment_m(n, m): + return {"n": n, "m": m + 1} +``` + +**Recursive Approach**: + +*Workflow JSON* (`while_loop_recursive/simple_recursive.json`): +```json +{ + "nodes": [ + {"id": 0, "type": "input", "name": "n", "value": 10}, + {"id": 1, "type": "input", "name": "m", "value": 0}, + {"id": 2, "type": "function", "value": "workflow.while_loop"}, + {"id": 3, "type": "output", "name": "result"} + ] +} +``` + +*Python* (`while_loop_recursive/workflow.py`): +```python +def while_loop(n, m): + if m >= n: + return m + return while_loop(n=n, m=m+1) +``` + +--- + +### Nested Workflows + +#### While Node Approach ✅ + +Supports complex multi-step loop bodies: + +```json +{ + "type": "while", + "conditionFunction": "check_converged", + "bodyWorkflow": { + "nodes": [ + {"id": 100, "type": "function", "value": "calculate_energy"}, + {"id": 101, "type": "function", "value": "calculate_forces"}, + {"id": 102, "type": "function", "value": "update_geometry"} + ], + "edges": [...] + } +} +``` + +#### Recursive Approach ⚠️ + +Must chain functions manually: + +```python +def optimization_step(structure, threshold): + energy = calculate_energy(structure) + forces = calculate_forces(structure, energy) + new_structure = update_geometry(structure, forces) + + if converged(energy, threshold): + return new_structure + + return optimization_step(new_structure, threshold) +``` + +--- + +### Condition Specification + +#### While Node Approach + +**Option 1: Function** +```json +"conditionFunction": "workflow.is_less_than" +``` + +**Option 2: Expression** (safe eval) +```json +"conditionExpression": "m < n and m % 2 == 0" +``` + +**Supported in expressions**: +- Comparison: `<`, `<=`, `>`, `>=`, `==`, `!=` +- Boolean: `and`, `or`, `not` +- Arithmetic: `+`, `-`, `*`, `/`, `%`, `**` +- Subscript: `data[0]`, `dict["key"]` + +**Blocked**: +- Function calls +- Imports +- Dunder attributes + +#### Recursive Approach + +Condition is Python code in the function: + +```python +def while_loop(n, m): + if m >= n: # Any Python expression + return m + return while_loop(n, m+1) +``` + +More flexible but no validation. + +--- + +### Safety & Limits + +#### While Node Approach + +**Built-in safety**: +```json +"maxIterations": 1000 // Explicit limit +``` + +- Configurable per workflow +- Prevents infinite loops +- Clear error messages +- Can be set high (10000+) safely + +#### Recursive Approach + +**Implicit Python limit**: +```python +import sys +sys.getrecursionlimit() # ~1000 +``` + +- Hard limit around 1000 +- RecursionError if exceeded +- Can increase (risky) +- Stack overflow danger + +--- + +### Execution Performance + +#### While Node Approach + +Backends can optimize to native loops: + +```python +# Backend can transform to: +result = initial_state +for _ in range(maxIterations): + if not condition(result): + break + result = body(result) +``` + +**Performance**: Near-native loop speed + +#### Recursive Approach + +Every iteration is a function call: + +```python +# Each call adds stack frame +while_loop(n, m) → while_loop(n, m+1) → while_loop(n, m+2) → ... +``` + +**Performance**: ~10-100x slower than loops + +--- + +### Visualization + +#### While Node Approach + +Graph shows clear loop structure: + +``` +┌──────────┐ +│ Input: n │───┐ +└──────────┘ │ + ├──► ┌──────────────┐ ┌────────┐ +┌──────────┐ │ │ While Loop │─────►│ Output │ +│ Input: m │───┘ │ (condition) │ └────────┘ +└──────────┘ │ (body) │ + │ max: 1000 │ + └──────────────┘ +``` + +Clear that there's a loop, condition, and body. + +#### Recursive Approach + +Graph shows single function: + +``` +┌──────────┐ +│ Input: n │───┐ +└──────────┘ │ + ├──► ┌──────────────┐ ┌────────┐ +┌──────────┐ │ │ Function │─────►│ Output │ +│ Input: m │───┘ │ while_loop │ └────────┘ +└──────────┘ └──────────────┘ +``` + +No indication of looping behavior from graph. + +--- + +### Backend Compatibility + +#### While Node Approach + +**Requires backend implementation**: + +- **Pure Python**: Execute condition + body in loop +- **AiiDA WorkGraph**: Translate to AiiDA while construct (if available) +- **Jobflow**: Map to Jobflow iteration pattern +- **ExecutorLib**: Parallel iteration support + +Each backend needs custom handling. + +#### Recursive Approach + +**Works immediately**: + +All backends already execute function nodes. Recursion happens inside Python - backends don't need to know. + +--- + +### Debugging + +#### While Node Approach + +Can provide rich debugging info: + +```python +# Workflow engine can track: +- Current iteration number +- State at each iteration +- Time per iteration +- Convergence history +``` + +Example error: +``` +WhileLoopError: Maximum iterations (1000) exceeded + Node ID: 2 + Last state: {"m": 1000, "n": 10000} + Suggestion: Increase maxIterations or check condition +``` + +#### Recursive Approach + +Standard Python stack trace: + +```python +RecursionError: maximum recursion depth exceeded + File "workflow.py", line 8, in while_loop + return while_loop(n=n, m=m+1) + File "workflow.py", line 8, in while_loop + return while_loop(n=n, m=m+1) + ... (996 more lines) +``` + +Harder to debug which iteration failed. + +--- + +## Use Case Recommendations + +### Use While Node When: + +✅ **Deep iterations** (> 100) +- Geometry optimization (100s of steps) +- Convergence algorithms +- Long-running simulations + +✅ **Visualization matters** +- Workflow needs to be human-readable +- Graph structure should show logic +- Documentation requires clear diagrams + +✅ **Production workflows** +- Unknown iteration counts +- Safety limits critical +- Performance matters + +✅ **Complex loop bodies** +- Multi-step iterations +- Nested workflows needed +- Parallel operations in body + +✅ **Expression conditions** +- Simple conditions like `error < threshold` +- Don't want separate function for condition +- Inline expressions clearer + +### Use Recursive Approach When: + +✅ **Shallow iterations** (< 50) +- Small search spaces +- Quick iterations +- Known small bounds + +✅ **No schema changes allowed** +- Working with existing infrastructure +- Can't modify workflow definition +- Backward compatibility required + +✅ **Functional style preferred** +- Pure functional programming +- Immutable data patterns +- No side effects + +✅ **Quick prototyping** +- Experimental workflows +- Rapid iteration +- Minimal setup + +✅ **Naturally recursive algorithms** +- Tree traversal +- Divide-and-conquer +- Algorithms already recursive + +--- + +## Migration Path + +### From Recursive → While Node + +Easy migration when you hit recursion limits: + +**Before** (recursive): +```python +def while_loop(n, m): + if m >= n: + return m + return while_loop(n=n, m=m+1) +``` + +**After** (while node): + +*Workflow*: +```json +{ + "type": "while", + "conditionExpression": "m < n", + "bodyFunction": "workflow.increment_m" +} +``` + +*Function*: +```python +def increment_m(n, m): + return {"n": n, "m": m + 1} +``` + +### From While Node → Recursive + +For simpler workflows: + +**Before** (while node + separate condition/body): +```json +{"type": "while", "conditionFunction": "...", "bodyFunction": "..."} +``` + +**After** (single recursive function): +```python +def combined_loop(...): + if : + return result + return combined_loop() +``` + +--- + +## Recommendation + +**For this project**: Implement **both approaches** on separate branches: + +1. **While Node branch**: Full-featured, production-ready + - Use for: complex workflows, deep iterations, visualization + - Requires: backend implementation work + +2. **Recursive branch** (main): Zero-overhead, immediate use + - Use for: simple workflows, prototyping, backward compatibility + - Requires: nothing! + +**Users can choose** based on their needs: +- Start with recursive (works now) +- Migrate to while node when needed (better performance, deep iterations) + +--- + +## Examples Summary + +### While Node Examples (`while_loop/`) +1. **simple_counter.json** - Function condition + function body +2. **simple_counter_expression.json** - Expression condition + function body +3. **nested_optimization.json** - Nested workflow body (geometry optimization) + +### Recursive Examples (`while_loop_recursive/`) +1. **simple_recursive.json** - Basic counter +2. **recursive_accumulator.json** - Accumulator pattern +3. **fibonacci.json** - Fibonacci sequence + +--- + +## Testing + +### While Node +```bash +cd example_workflows/while_loop +python test_while_node.py +``` + +Tests: +- Schema validation +- JSON loading +- Expression evaluation safety + +### Recursive +```bash +cd example_workflows/while_loop_recursive +python test_recursive.py +``` + +Tests: +- Workflow loading +- Function execution +- Recursion limits +- Schema compatibility + +--- + +## Conclusion + +Both approaches are valid: + +| Priority | Recommended Approach | +|----------|---------------------| +| **Quick start** | Recursive (no changes) | +| **Production** | While Node (robust) | +| **Performance** | While Node (faster) | +| **Simplicity** | Recursive (less code) | +| **Flexibility** | While Node (nested workflows) | +| **Compatibility** | Recursive (works everywhere) | + +**Ideal solution**: Support both, let users choose based on use case. diff --git a/example_workflows/while_loop_recursive/README.md b/example_workflows/while_loop_recursive/README.md new file mode 100644 index 0000000..9b4215e --- /dev/null +++ b/example_workflows/while_loop_recursive/README.md @@ -0,0 +1,460 @@ +# Recursive While Loop Implementation + +This directory demonstrates the **recursive approach** to implementing while loops in the Python Workflow Definition syntax. + +## Overview + +The recursive approach leverages Python's native recursion to implement while loops **without requiring any schema changes**. Instead of adding a new node type, recursive functions call themselves until a termination condition is met. + +## Key Concept + +```python +def while_(n, m): + if m >= n: # Base case: condition met + return m + m += 1 # Update state + return while_(n=n, m=m) # Recursive call +``` + +This translates to a workflow with a single `function` node - no special "while" node needed! + +## Files + +### Workflow Functions +- **workflow.py** - Recursive function implementations: + - `while_loop()` - Simple counter + - `while_loop_with_accumulator()` - Collecting results + - `fibonacci_recursive()` - Fibonacci sequence generator + - `converge_to_zero()` - Iterative convergence algorithm + +### Workflow Definitions +1. **simple_recursive.json** - Basic counter (m → n) +2. **recursive_accumulator.json** - Accumulator pattern example +3. **fibonacci.json** - Generate Fibonacci sequence + +### Testing +- **test_recursive.py** - Comprehensive test suite + +## Workflow Schema + +**No schema changes required!** Uses standard nodes: + +```json +{ + "version": "0.1.0", + "nodes": [ + {"id": 0, "type": "input", "name": "n", "value": 10}, + {"id": 1, "type": "input", "name": "m", "value": 0}, + {"id": 2, "type": "function", "value": "workflow.while_loop"}, + {"id": 3, "type": "output", "name": "result"} + ], + "edges": [ + {"source": 0, "target": 2, "targetPort": "n"}, + {"source": 1, "target": 2, "targetPort": "m"}, + {"source": 2, "target": 3} + ] +} +``` + +## Examples + +### Example 1: Simple Counter + +**Goal**: Count from 0 to 10 + +**Function** (`workflow.py`): +```python +def while_loop(n, m): + if m >= n: + return m + m = m + 1 + return while_loop(n=n, m=m) +``` + +**Execution**: +```python +while_loop(n=10, m=0) +# Calls: while_loop(10, 0) → while_loop(10, 1) → ... → while_loop(10, 10) → 10 +``` + +**Workflow**: `simple_recursive.json` + +### Example 2: Accumulator Pattern + +**Goal**: Collect all values from 0 to n-1 in a list + +**Function**: +```python +def while_loop_with_accumulator(n, m, accumulator): + if m >= n: + return {"m": m, "accumulator": accumulator} + + new_accumulator = accumulator + [m] + m = m + 1 + + return while_loop_with_accumulator(n=n, m=m, accumulator=new_accumulator) +``` + +**Execution**: +```python +while_loop_with_accumulator(n=5, m=0, accumulator=[]) +# Result: {"m": 5, "accumulator": [0, 1, 2, 3, 4]} +``` + +**Workflow**: `recursive_accumulator.json` + +### Example 3: Fibonacci Sequence + +**Goal**: Generate first N Fibonacci numbers + +**Function**: +```python +def fibonacci_recursive(n, current, a, b, results): + if current >= n: + return {"results": results, "count": current} + + next_fib = a + b + new_results = results + [b] + + return fibonacci_recursive( + n=n, current=current + 1, a=b, b=next_fib, results=new_results + ) +``` + +**Execution**: +```python +fibonacci_recursive(n=10, current=0, a=0, b=1, results=[]) +# Result: {"results": [1, 1, 2, 3, 5, 8, 13, 21, 34, 55], "count": 10} +``` + +**Workflow**: `fibonacci.json` + +### Example 4: Convergence Algorithm + +**Goal**: Reduce value until below threshold + +**Function**: +```python +def converge_to_zero(value, threshold, iterations): + if abs(value) <= threshold: + return {"value": value, "iterations": iterations, "converged": True} + + if iterations >= 1000: # Safety limit + return {"value": value, "iterations": iterations, "converged": False} + + new_value = value * 0.5 + return converge_to_zero(value=new_value, threshold=threshold, iterations=iterations + 1) +``` + +**Use case**: Simulates iterative scientific algorithms that converge to a solution. + +## How It Works + +### 1. Recursion Pattern + +Every recursive while loop follows this structure: + +```python +def recursive_loop(condition_vars, state_vars): + # Base case: check termination condition + if : + return + + # Recursive case: update state + + + # Recurse with updated state + return recursive_loop(condition_vars, updated_state_vars) +``` + +### 2. State Management + +State flows through function parameters and return values: + +**Input → Function → Recursive Call → ... → Output** + +Each iteration: +1. Receives current state as parameters +2. Checks termination condition +3. Updates state variables +4. Passes updated state to next call (itself) + +### 3. Workflow Execution + +The workflow engine executes the function node, which: +1. Receives initial inputs from input nodes +2. Executes the recursive function +3. Returns final result to output node + +**Key insight**: The recursion happens **inside** the function, not in the workflow graph! + +## Advantages ✅ + +### 1. **No Schema Changes** +- Uses existing `function` nodes +- Works with current workflow definition (v0.1.0) +- No new validation rules needed +- Backward compatible + +### 2. **Natural Functional Style** +- Pure functions with immutable data +- Easy to reason about +- Testable without workflow engine +- Follows functional programming paradigms + +### 3. **Explicit State** +- All state passed through parameters +- No hidden state or side effects +- Clear data flow +- Easy to debug + +### 4. **Backend Compatibility** +- Functions execute in any Python environment +- No special loop constructs needed +- Works with AiiDA, Jobflow, pure Python, etc. +- No backend-specific translation required + +### 5. **Type Safety** +- Standard Python type hints work +- IDEs provide autocomplete +- Static analysis tools work out-of-the-box + +## Disadvantages ❌ + +### 1. **Python Recursion Limit** +- Default limit: ~1000 recursive calls +- Can be increased but risky (stack overflow) +- Not suitable for deeply iterative algorithms + +**Example**: +```python +# This will hit recursion limit! +while_loop(n=2000, m=0) # RecursionError +``` + +**Workaround**: +```python +import sys +sys.setrecursionlimit(10000) # Dangerous! +``` + +### 2. **Stack Memory Usage** +- Each call consumes stack space +- Large iteration counts risk stack overflow +- Memory inefficient compared to loops + +### 3. **Performance Overhead** +- Function call overhead per iteration +- Slower than native loops +- Not suitable for tight inner loops + +### 4. **Visualization Challenges** +- Graph doesn't show the loop structure +- Appears as single function node +- Hard to understand iteration count from graph +- No cycle visualization + +### 5. **Limited Error Messages** +- RecursionError doesn't show which iteration failed +- Hard to debug deep recursion issues +- No built-in iteration tracking + +### 6. **No Early Termination** +- Can't implement `break` easily +- Must reach base case or hit recursion limit +- Difficult to handle exceptional conditions + +## Comparison with While Node Approach + +| Aspect | Recursive Approach | While Node Approach | +|--------|-------------------|---------------------| +| Schema changes | ✅ None | ❌ New node type needed | +| Max iterations | ❌ ~1000 (recursion limit) | ✅ Configurable (1000+ fine) | +| Memory usage | ❌ Stack grows with iterations | ✅ Constant memory | +| Performance | ❌ Function call overhead | ✅ Native loop performance | +| Visualization | ❌ No loop structure visible | ✅ Clear loop node in graph | +| Functional style | ✅ Pure functions | ⚠️ Hybrid (functions + graph structure) | +| Backend support | ✅ Works everywhere | ⚠️ Requires backend implementation | +| Debugging | ❌ Deep call stacks | ✅ Iteration tracking possible | +| Type safety | ✅ Standard Python types | ✅ Pydantic validation | +| Safety limits | ❌ Implicit (recursion limit) | ✅ Explicit `maxIterations` | + +## When to Use Recursive Approach + +### ✅ Good For: +- **Shallow iterations** (< 100 iterations) +- **Algorithms naturally recursive** (tree traversal, divide-and-conquer) +- **Functional workflow style** preferred +- **No schema modifications** allowed +- **Quick prototyping** without backend changes + +### ❌ Avoid For: +- **Deep iterations** (> 1000 iterations) +- **Performance-critical** tight loops +- **Large state** carried through iterations +- **Production workflows** with unknown iteration counts +- **Workflows needing visualization** of loop structure + +## Running the Examples + +### Direct Function Execution + +```bash +cd example_workflows/while_loop_recursive +python3 +``` + +```python +>>> import workflow +>>> workflow.while_loop(n=10, m=0) +10 + +>>> workflow.while_loop_with_accumulator(n=5, m=0, accumulator=[]) +{'m': 5, 'accumulator': [0, 1, 2, 3, 4]} + +>>> workflow.fibonacci_recursive(n=10, current=0, a=0, b=1, results=[]) +{'results': [1, 1, 2, 3, 5, 8, 13, 21, 34, 55], 'count': 10} +``` + +### Running Tests + +```bash +cd example_workflows/while_loop_recursive +python test_recursive.py +``` + +**Expected Output**: +- ✅ All workflow JSON files load successfully +- ✅ All recursive functions execute correctly +- ⚠️ Recursion depth test shows limitation at ~1000 iterations +- ✅ Schema validation passes (no changes needed) + +### Loading Workflows + +```python +from python_workflow_definition.models import PythonWorkflowDefinitionWorkflow + +wf = PythonWorkflowDefinitionWorkflow.load_json_file("simple_recursive.json") +print(wf) +``` + +## Implementation Notes + +### State Passing Pattern + +Always return updated state as dict to support multi-value state: + +```python +# ✅ Good: Return dict for multiple state vars +def loop_func(a, b, c): + if : + return {"a": a, "b": b, "c": c} + return loop_func(a=a+1, b=b*2, c=c-1) + +# ❌ Bad: Can't return multiple values easily +def loop_func(a, b): + if : + return a, b # How to route in workflow? + return loop_func(a+1, b*2) +``` + +### Safety Limits + +Always include maximum iteration guards: + +```python +def safe_loop(n, m, max_iter=1000): + if m >= n or m >= max_iter: # Safety check + return m + return safe_loop(n, m+1, max_iter) +``` + +### Immutable Updates + +Use immutable data structures to avoid state bugs: + +```python +# ✅ Good: Create new list +new_list = old_list + [item] + +# ❌ Bad: Mutate list (breaks functional purity) +old_list.append(item) +``` + +## Future Enhancements + +### Tail Call Optimization + +Python doesn't optimize tail calls, but workflow backends could: + +```python +# This is tail-recursive (could be optimized to loop) +def while_loop(n, m): + if m >= n: + return m + return while_loop(n, m+1) # Tail call +``` + +**Backend could transform to**: +```python +def while_loop_optimized(n, m): + while m < n: + m += 1 + return m +``` + +### Trampoline Pattern + +Implement explicit trampoline to avoid recursion limits: + +```python +def while_loop_trampoline(n, m): + while callable(result := _while_loop_step(n, m)): + result = result() + return result + +def _while_loop_step(n, m): + if m >= n: + return m + return lambda: _while_loop_step(n, m+1) +``` + +### Graph Cycle Detection + +Future workflow validators could: +1. Detect potential recursion (function calls itself) +2. Warn about deep recursion risks +3. Suggest while node approach for deep iterations + +## Conclusion + +The recursive approach offers a **zero-schema-change** solution for while loops, leveraging Python's native recursion. It's ideal for: +- Shallow iterations (< 100) +- Functional programming style +- Quick prototyping +- Naturally recursive algorithms + +However, for production workflows with deep iterations or performance requirements, the **while node approach** (see `example_workflows/while_loop/`) is recommended. + +## Related Examples + +- **While Node Approach**: `example_workflows/while_loop/` - Explicit while node with nested workflows +- **Arithmetic Examples**: `example_workflows/arithmetic/` - Basic function chaining +- **Quantum Espresso**: `example_workflows/quantum_espresso/` - Complex workflows with parallelism + +## Testing + +Run the test suite to validate: + +```bash +cd example_workflows/while_loop_recursive +python test_recursive.py +``` + +Tests cover: +- Workflow JSON validation +- Recursive function execution +- Recursion depth limits +- Schema compatibility + +All tests should pass except recursion depth test (demonstrates limitation). diff --git a/example_workflows/while_loop_recursive/fibonacci.json b/example_workflows/while_loop_recursive/fibonacci.json new file mode 100644 index 0000000..c576ecf --- /dev/null +++ b/example_workflows/while_loop_recursive/fibonacci.json @@ -0,0 +1,83 @@ +{ + "version": "0.1.0", + "nodes": [ + { + "id": 0, + "type": "input", + "name": "n", + "value": 10 + }, + { + "id": 1, + "type": "input", + "name": "current", + "value": 0 + }, + { + "id": 2, + "type": "input", + "name": "a", + "value": 0 + }, + { + "id": 3, + "type": "input", + "name": "b", + "value": 1 + }, + { + "id": 4, + "type": "input", + "name": "results", + "value": [] + }, + { + "id": 5, + "type": "function", + "value": "workflow.fibonacci_recursive" + }, + { + "id": 6, + "type": "output", + "name": "fibonacci_sequence" + } + ], + "edges": [ + { + "source": 0, + "sourcePort": null, + "target": 5, + "targetPort": "n" + }, + { + "source": 1, + "sourcePort": null, + "target": 5, + "targetPort": "current" + }, + { + "source": 2, + "sourcePort": null, + "target": 5, + "targetPort": "a" + }, + { + "source": 3, + "sourcePort": null, + "target": 5, + "targetPort": "b" + }, + { + "source": 4, + "sourcePort": null, + "target": 5, + "targetPort": "results" + }, + { + "source": 5, + "sourcePort": null, + "target": 6, + "targetPort": null + } + ] +} diff --git a/example_workflows/while_loop_recursive/recursive_accumulator.json b/example_workflows/while_loop_recursive/recursive_accumulator.json new file mode 100644 index 0000000..b50cd8a --- /dev/null +++ b/example_workflows/while_loop_recursive/recursive_accumulator.json @@ -0,0 +1,59 @@ +{ + "version": "0.1.0", + "nodes": [ + { + "id": 0, + "type": "input", + "name": "n", + "value": 5 + }, + { + "id": 1, + "type": "input", + "name": "m", + "value": 0 + }, + { + "id": 2, + "type": "input", + "name": "accumulator", + "value": [] + }, + { + "id": 3, + "type": "function", + "value": "workflow.while_loop_with_accumulator" + }, + { + "id": 4, + "type": "output", + "name": "final_state" + } + ], + "edges": [ + { + "source": 0, + "sourcePort": null, + "target": 3, + "targetPort": "n" + }, + { + "source": 1, + "sourcePort": null, + "target": 3, + "targetPort": "m" + }, + { + "source": 2, + "sourcePort": null, + "target": 3, + "targetPort": "accumulator" + }, + { + "source": 3, + "sourcePort": null, + "target": 4, + "targetPort": null + } + ] +} diff --git a/example_workflows/while_loop_recursive/simple_recursive.json b/example_workflows/while_loop_recursive/simple_recursive.json new file mode 100644 index 0000000..772beb7 --- /dev/null +++ b/example_workflows/while_loop_recursive/simple_recursive.json @@ -0,0 +1,47 @@ +{ + "version": "0.1.0", + "nodes": [ + { + "id": 0, + "type": "input", + "name": "n", + "value": 10 + }, + { + "id": 1, + "type": "input", + "name": "m", + "value": 0 + }, + { + "id": 2, + "type": "function", + "value": "workflow.while_loop" + }, + { + "id": 3, + "type": "output", + "name": "result" + } + ], + "edges": [ + { + "source": 0, + "sourcePort": null, + "target": 2, + "targetPort": "n" + }, + { + "source": 1, + "sourcePort": null, + "target": 2, + "targetPort": "m" + }, + { + "source": 2, + "sourcePort": null, + "target": 3, + "targetPort": null + } + ] +} diff --git a/example_workflows/while_loop_recursive/test_recursive.py b/example_workflows/while_loop_recursive/test_recursive.py new file mode 100644 index 0000000..63b1dbf --- /dev/null +++ b/example_workflows/while_loop_recursive/test_recursive.py @@ -0,0 +1,218 @@ +""" +Test script for recursive while loop approach. + +This tests: +1. Loading recursive workflow JSON files +2. Executing recursive functions directly +3. Validating that the workflow schema supports recursion +""" + +import sys +from pathlib import Path + +# Add the source directory to Python path +src_path = Path(__file__).parent.parent.parent / "python_workflow_definition" / "src" +sys.path.insert(0, str(src_path)) + +# Add current directory for workflow imports +sys.path.insert(0, str(Path(__file__).parent)) + +from python_workflow_definition.models import PythonWorkflowDefinitionWorkflow +import workflow + + +def test_workflow_loading(): + """Test loading recursive workflow JSON files.""" + print("=" * 60) + print("Testing Recursive Workflow JSON Loading") + print("=" * 60) + + # Test 1: Simple recursive workflow + print("\n1. Loading simple_recursive.json...") + try: + workflow_path = Path(__file__).parent / "simple_recursive.json" + wf = PythonWorkflowDefinitionWorkflow.load_json_file(workflow_path) + print(f" ✓ Loaded successfully") + print(f" - Nodes: {len(wf['nodes'])}") + print(f" - Edges: {len(wf['edges'])}") + func_node = [n for n in wf["nodes"] if n["type"] == "function"][0] + print(f" - Function: {func_node['value']}") + except Exception as e: + print(f" ✗ Failed: {e}") + + # Test 2: Recursive with accumulator + print("\n2. Loading recursive_accumulator.json...") + try: + workflow_path = Path(__file__).parent / "recursive_accumulator.json" + wf = PythonWorkflowDefinitionWorkflow.load_json_file(workflow_path) + print(f" ✓ Loaded successfully") + print(f" - Nodes: {len(wf['nodes'])}") + func_node = [n for n in wf["nodes"] if n["type"] == "function"][0] + print(f" - Function: {func_node['value']}") + except Exception as e: + print(f" ✗ Failed: {e}") + + # Test 3: Fibonacci + print("\n3. Loading fibonacci.json...") + try: + workflow_path = Path(__file__).parent / "fibonacci.json" + wf = PythonWorkflowDefinitionWorkflow.load_json_file(workflow_path) + print(f" ✓ Loaded successfully") + print(f" - Nodes: {len(wf['nodes'])}") + print(f" - Input nodes: {len([n for n in wf['nodes'] if n['type'] == 'input'])}") + except Exception as e: + print(f" ✗ Failed: {e}") + + +def test_recursive_functions(): + """Test executing recursive functions directly.""" + print("\n" + "=" * 60) + print("Testing Recursive Function Execution") + print("=" * 60) + + # Test 1: Simple while loop + print("\n1. Testing while_loop(n=10, m=0)...") + try: + result = workflow.while_loop(n=10, m=0) + expected = 10 + if result == expected: + print(f" ✓ Result: {result} (expected: {expected})") + else: + print(f" ✗ Result: {result} (expected: {expected})") + except Exception as e: + print(f" ✗ Failed: {e}") + + # Test 2: While loop with accumulator + print("\n2. Testing while_loop_with_accumulator(n=5, m=0, accumulator=[])...") + try: + result = workflow.while_loop_with_accumulator(n=5, m=0, accumulator=[]) + expected_m = 5 + expected_acc = [0, 1, 2, 3, 4] + if result["m"] == expected_m and result["accumulator"] == expected_acc: + print(f" ✓ Result: m={result['m']}, accumulator={result['accumulator']}") + else: + print(f" ✗ Result: {result}") + print(f" Expected: m={expected_m}, accumulator={expected_acc}") + except Exception as e: + print(f" ✗ Failed: {e}") + + # Test 3: Fibonacci sequence + print("\n3. Testing fibonacci_recursive(n=10, current=0, a=0, b=1, results=[])...") + try: + result = workflow.fibonacci_recursive(n=10, current=0, a=0, b=1, results=[]) + expected_count = 10 + expected_first_few = [1, 1, 2, 3, 5, 8] + if ( + result["count"] == expected_count + and result["results"][:6] == expected_first_few + ): + print(f" ✓ Generated {result['count']} Fibonacci numbers") + print(f" - First 6 numbers: {result['results'][:6]}") + else: + print(f" ✗ Result: {result}") + except Exception as e: + print(f" ✗ Failed: {e}") + + # Test 4: Convergence + print("\n4. Testing converge_to_zero(value=100, threshold=0.01, iterations=0)...") + try: + result = workflow.converge_to_zero(value=100, threshold=0.01, iterations=0) + if result["converged"] and abs(result["value"]) <= 0.01: + print(f" ✓ Converged after {result['iterations']} iterations") + print(f" - Final value: {result['value']}") + else: + print(f" ✗ Result: {result}") + except Exception as e: + print(f" ✗ Failed: {e}") + + +def test_recursion_depth(): + """Test that recursion handles reasonable iteration counts.""" + print("\n" + "=" * 60) + print("Testing Recursion Depth Handling") + print("=" * 60) + + # Test with moderate iteration count (should work) + print("\n1. Testing with n=100 (moderate depth)...") + try: + result = workflow.while_loop(n=100, m=0) + if result == 100: + print(f" ✓ Handled 100 iterations successfully") + else: + print(f" ✗ Unexpected result: {result}") + except RecursionError: + print(f" ✗ Hit recursion limit (this is a limitation of the recursive approach)") + except Exception as e: + print(f" ✗ Failed: {e}") + + # Test with large iteration count (may hit recursion limit) + print("\n2. Testing with n=1000 (deep recursion)...") + try: + result = workflow.while_loop(n=1000, m=0) + if result == 1000: + print(f" ✓ Handled 1000 iterations successfully") + else: + print(f" ✗ Unexpected result: {result}") + except RecursionError: + print(f" ⚠ Hit Python recursion limit (expected for deep recursion)") + print(f" This demonstrates a key limitation of the recursive approach.") + except Exception as e: + print(f" ✗ Failed: {e}") + + +def test_schema_compatibility(): + """Test that recursive workflows are compatible with current schema.""" + print("\n" + "=" * 60) + print("Testing Schema Compatibility") + print("=" * 60) + + print("\n1. Checking if workflows validate against schema...") + try: + # Load and validate each workflow + workflows = ["simple_recursive.json", "recursive_accumulator.json", "fibonacci.json"] + for wf_file in workflows: + workflow_path = Path(__file__).parent / wf_file + wf = PythonWorkflowDefinitionWorkflow.load_json_file(workflow_path) + print(f" ✓ {wf_file} validates successfully") + except Exception as e: + print(f" ✗ Validation failed: {e}") + + print("\n2. Checking node types...") + try: + workflow_path = Path(__file__).parent / "simple_recursive.json" + wf = PythonWorkflowDefinitionWorkflow.load_json_file(workflow_path) + node_types = {n["type"] for n in wf["nodes"]} + expected_types = {"input", "function", "output"} + if node_types == expected_types: + print(f" ✓ Uses standard node types: {node_types}") + print(f" ✓ No schema changes required for recursive approach!") + else: + print(f" ✗ Unexpected node types: {node_types}") + except Exception as e: + print(f" ✗ Failed: {e}") + + +def main(): + """Run all tests.""" + print("\n" + "=" * 60) + print("Recursive While Loop Implementation Tests") + print("=" * 60) + + test_workflow_loading() + test_recursive_functions() + test_recursion_depth() + test_schema_compatibility() + + print("\n" + "=" * 60) + print("All tests completed!") + print("=" * 60) + print("\nKey Findings:") + print("✓ No schema changes needed - uses existing function nodes") + print("✓ Natural functional programming style") + print("⚠ Python recursion depth limit (~1000 calls)") + print("⚠ Stack overflow risk for large iterations") + print("=" * 60 + "\n") + + +if __name__ == "__main__": + main() diff --git a/example_workflows/while_loop_recursive/workflow.py b/example_workflows/while_loop_recursive/workflow.py new file mode 100644 index 0000000..e832a50 --- /dev/null +++ b/example_workflows/while_loop_recursive/workflow.py @@ -0,0 +1,105 @@ +""" +Recursive while loop implementation. + +This demonstrates the recursive approach where a function calls itself +until a condition is met, creating a cycle in the workflow graph. +""" + + +def while_loop(n, m): + """ + Recursive while loop: increment m until m >= n. + + This function demonstrates the recursive pattern: + - If condition is met (m >= n), return the result + - Otherwise, increment m and recursively call itself + + Args: + n: Upper bound (target value) + m: Current value (counter) + + Returns: + int: Final value of m (should equal n) + """ + if m >= n: + return m + m = m + 1 + return while_loop(n=n, m=m) + + +def while_loop_with_accumulator(n, m, accumulator): + """ + Recursive while loop with accumulator pattern. + + This demonstrates collecting results across iterations. + + Args: + n: Upper bound + m: Current counter + accumulator: List collecting values + + Returns: + dict: Final state with counter and accumulated values + """ + if m >= n: + return {"m": m, "accumulator": accumulator} + + # Add current value to accumulator + new_accumulator = accumulator + [m] + m = m + 1 + + return while_loop_with_accumulator(n=n, m=m, accumulator=new_accumulator) + + +def fibonacci_recursive(n, current, a, b, results): + """ + Generate Fibonacci sequence recursively. + + Args: + n: Number of terms to generate + current: Current iteration index + a: Previous Fibonacci number + b: Current Fibonacci number + results: List of Fibonacci numbers generated so far + + Returns: + dict: Final results with Fibonacci sequence + """ + if current >= n: + return {"results": results, "count": current} + + # Calculate next Fibonacci number + next_fib = a + b + new_results = results + [b] + + return fibonacci_recursive( + n=n, current=current + 1, a=b, b=next_fib, results=new_results + ) + + +def converge_to_zero(value, threshold, iterations): + """ + Recursively reduce a value until it's below threshold. + + Simulates an iterative algorithm that converges. + + Args: + value: Current value + threshold: Convergence threshold + iterations: Number of iterations performed + + Returns: + dict: Final value and iteration count + """ + if abs(value) <= threshold: + return {"value": value, "iterations": iterations, "converged": True} + + if iterations >= 1000: # Safety limit + return {"value": value, "iterations": iterations, "converged": False} + + # Reduce value by 50% each iteration + new_value = value * 0.5 + + return converge_to_zero( + value=new_value, threshold=threshold, iterations=iterations + 1 + )