This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Coral for Python is a workflow execution system that processes computational graphs defined in JSON format. The system supports four types of nodes:
- Primitive nodes: Hold constant values (int, float, string, bool, any)
- Function nodes: Execute Python functions with typed inputs and outputs
- Constructor nodes: Instantiate Python classes
- Method nodes: Call instance methods on objects created by constructor nodes
The project uses PhiFlow for physics simulations and numerical computing.
For the narrative — goals, architecture rationale, the two contracts with the DealiiX platform, and how to extend the project — see
docs/ONBOARDING.md(Italian:docs/ONBOARDING.it.md). ThisCLAUDE.mdis the mechanics reference;README.mdcovers setup and commands.
This is a uv project (pyproject.toml + uv.lock); requirements.in / requirements.txt are legacy.
# Create .venv and install all deps (incl. the dev group) from the lockfile
uv sync
# Then activate, or prefix commands with `uv run` (e.g. `uv run python main.py`)
source .venv/bin/activate # Linux/Mac# Add a runtime dependency (updates pyproject.toml + uv.lock, then syncs)
uv add <package-name>
# Add a dev-only dependency
uv add --dev <package-name>
# Re-resolve the lockfile and sync the environment
uv lock && uv syncmain.py is a coral-compatible CLI: a global -p/--plugin option (comma-separated modules; empty = all) plus
register / run subcommands. -p/--plugin must precede the subcommand.
# Run a workflow graph (default file network-from-fe.json, all modules)
python main.py run
python main.py run path/to/workflow.json
python main.py -p "math" run path/to/workflow.json
python main.py -p "math,string,phiflow" run path/to/workflow.json
# Generate the node registry (writes node_types.json into the cwd)
python main.py register
python main.py -p "math" register
python main.py register --output="custom-registry.json"The coral-py launcher wraps this for the DealiiX platform: it runs main.py inside the uv project while
preserving the caller's cwd (so register writes node_types.json there). Point the platform's coralBinaryPath
at coral-py and set coralPluginPath to the module list.
Default module behavior: When -p/--plugin is omitted, all available modules are loaded. Primitives are always included.
Available modules:
math- Mathematical operations and Calculator classstring- StringProcessor classphiflow- PhiFlow physics simulation wrappers (default)
# Run a PhiFlow simulation directly (not through workflow system)
python phi_flow/one_obstacle_absorb.py
python phi_flow/multiple_obstacles.py
# Check the generated .mp4 or .gif output file in phi_flow/ directory# Run all tests
pytest
# Run with coverage
pytest --cov=. --cov-report=html
open htmlcov/index.html
# Run specific test file
pytest tests/test_executor.py
pytest tests/test_integration.py
# Run specific test class or function
pytest tests/test_executor.py::TestPrimitiveNodeExecution
pytest tests/test_executor.py::TestPrimitiveNodeExecution::test_int_primitive
# Run tests by category (using markers)
pytest -m integration # Integration tests with JSON network files
pytest -m math # Math module tests
pytest -m phiflow # PhiFlow tests
pytest -m string # String module tests
# Verbose output with print statements
pytest -v # Verbose
pytest -vv # Extra verbose
pytest -s # Show print statementsThe system is organized into four main modules:
-
definitions/ - Modular package for callable functions and classes:
definitions/__init__.py: Exportsbuild_function_map(),build_class_map(), andPRIMITIVES_MAPdefinitions/primitives.py: Maps type strings to Python types (int, float, str, bool, any, none)definitions/math_ops.py: Mathematical operations (add,multiply,math.sqrt, etc.) andCalculatorclassdefinitions/string_ops.py:StringProcessorclass andprint_resultfunctiondefinitions/phiflow_defs.py: PhiFlow physics simulation wrappers (if available)
Module loading: Use the
-p/--pluginoption to control which definitions are loaded (comma-separated). Default when omitted: all modules.AVAILABLE_MODULES(indefinitions/__init__.py) lists them. -
registry.py - Registry generation and type conversion:
generate_registry(): Introspects functions and classes to create JSON schemapython_type_to_string(): Converts Python type hints to string representationssave_registry_to_file(): Saves registry to JSON file, acceptsmodulesparameter- Generates entries for primitives, functions, constructors, and methods
-
executor.py - Workflow execution engine:
WorkflowExecutor: Executes workflow JSON files by:- Loading specified modules via
modulesparameter (defaults to['phiflow']) - Building function and class maps dynamically based on loaded modules
- Performing topological sort (Kahn's algorithm) to determine execution order
- Evaluating primitive nodes (returning their typed
value) - Executing function nodes with inputs from connected edges
- Instantiating classes via constructor nodes
- Calling instance methods via method nodes
- Storing results for downstream nodes
- Loading specified modules via
-
main.py - Coral-compatible CLI entry point (argparse):
- Global
-p/--pluginoption names the definition modules to load (comma-separated; empty = all) registersubcommand →save_registry_to_file()(writesnode_types.jsoninto the cwd)runsubcommand →WorkflowExecutor(...).execute()- Wrapped by the
coral-pylauncher for the DealiiX platform
- Global
Network Files (e.g., network-from-fe.json):
Located at: workflow.nodes and workflow.edges
Nodes are lean: each carries only its type (plus value for primitives); the executor infers
the kind from type, so node_type/method_name are not part of the graph.
- Primitive:
{"type": "<type>", "value": <val>} - Function:
{"type": "<func_name>"} - Constructor:
{"type": "<ClassName>"} - Method:
{"type": "<ClassName>.<method_name>"}
Edge format:
{"source": "<source_id>", "target": "<target_id>", "source_output": <idx>, "target_input": <idx>}- CRITICAL:
target_inputdetermines parameter ordering for function/method calls
Registry Files (e.g., registry-py.json):
- Auto-generated schema describing all available node types, in the DealiiX platform format
- Keyed by each node's
type(primitives by type name, functions by name, constructors by class name, methods byClass.method) — the editor looks entries up asregistry[type] - Each entry has:
type: the node type string (equals the entry's key)arguments: Array withconnection_type("input"/"output"),type, andname(empty[]for primitives)inputs: List of input indicesoutputs: List of output indices (or[-1]for constructors/primitives)node_type: "primitive", "function", "constructor", or "method"
- Load: Workflow loaded from JSON (
workflow.nodesandworkflow.edges) - Sort: Topological sort determines execution order (detects cycles using Kahn's algorithm)
- Execute: Nodes executed in dependency order:
- Primitive nodes: Return typed value (type conversion via
PRIMITIVES_MAP) - Function nodes: Collect inputs from edges (sorted by
target_input), call function - Constructor nodes: Collect inputs, instantiate class from
CLASS_MAP - Method nodes: First input is instance, remaining inputs are parameters
- Primitive nodes: Return typed value (type conversion via
- Store: Results stored in
executor.resultsdictionary for downstream nodes
Each node's kind is inferred from its type by WorkflowExecutor._classify() (membership in
PRIMITIVES_MAP / FUNCTION_MAP / CLASS_MAP, plus the Class.method split), then executed:
Primitive nodes (type in PRIMITIVES_MAP):
- Extract
valueandtypefrom node definition - Convert value using
PRIMITIVES_MAP[type](handles string-to-type conversion from JSON) - Store result directly
Function nodes (type in FUNCTION_MAP):
- Look up function in
FUNCTION_MAPusingtype - Collect inputs from incoming edges sorted by
target_inputindex - Map inputs to function parameters positionally using
inspect.signature() - Execute function with kwargs, store result
Constructor nodes (type in CLASS_MAP):
- Look up class in
CLASS_MAPusingtypefield - Collect constructor inputs from edges (sorted by
target_input) - Map inputs to
__init__parameters (excludingself) - Instantiate class, store instance
Method nodes (type is Class.method with the class in CLASS_MAP):
- Parse the fully qualified
type(format:"ClassName.method_name") - First input (lowest
target_input) must be instance ofClassName - Remaining inputs are method parameters
- Use
getattr(instance, method_name)to get bound method - Execute method with parameters, store result
The phi_flow/ directory contains physics simulation examples using the PhiFlow library:
- Fluid dynamics simulations (smoke plumes, obstacles)
- Uses JIT compilation for performance
- Supports multiple backends (JAX, PyTorch, TensorFlow)
- Wrapper classes in definitions/phiflow_defs.py provide simplified API for workflow integration
- Edge ordering is critical: Function/method parameter order determined by
target_inputvalues on edges (sorted ascending) - Type system: Maps Python types (int, float, str, bool, None, Any) to string representations via
PRIMITIVES_MAP - No cycles: Workflow graphs must be acyclic (DAG) - executor will raise
ValueErrorif cycle detected - Naming conventions:
- Functions: Use simple names in
FUNCTION_MAP(e.g.,"add","math.sqrt") - Methods: Use fully qualified names (e.g.,
"Calculator.add_to_value") - Classes: Class name becomes the
typefield for constructors (e.g.,"Calculator")
- Functions: Use simple names in
- Type hint requirement: All functions/methods must have type hints for proper registry generation
- C extension limitation: C extension classes (like
datetime) only register constructors, not methods (due toinspect.isfunction()behavior)
The definitions system is modular. Add new functions and classes to the appropriate module file in definitions/.
To add a new function to the workflow system:
-
Choose or create a module file in
definitions/:- For math operations: Add to
definitions/math_ops.py - For string operations: Add to
definitions/string_ops.py - For PhiFlow wrappers: Add to
definitions/phiflow_defs.py - For new domains: Create a new file (e.g.,
definitions/numpy_ops.py)
- For math operations: Add to
-
Define the function with type hints:
# In definitions/math_ops.py
def my_function(param1: float, param2: str) -> int:
"""Function description"""
result = ...
print(f"my_function({param1}, {param2}) = {result}")
return result- Add to the module's
get_functions():
def get_functions() -> Dict[str, Any]:
"""Return math function definitions"""
return {
"add": add,
"multiply": multiply,
"my_function": my_function, # Add here
# ...
}- Regenerate registry with the appropriate module:
python main.py -p "math" register- The function is now available when the module is loaded
External library functions can be registered by creating wrapper functions with proper type hints.
Example - Adding a NumPy function:
# Create definitions/numpy_ops.py
import numpy as np
from typing import Any, Dict
def numpy_mean(values: list) -> float:
"""Calculate mean using NumPy"""
result = np.mean(values)
print(f"numpy.mean({values}) = {result}")
return float(result)
def get_functions() -> Dict[str, Any]:
return {
"numpy.mean": numpy_mean,
}
def get_classes() -> Dict[str, Any]:
return {}Then register the module in definitions/__init__.py — add the import and one entry to _MODULES
(build_function_map/build_class_map read from _MODULES, so no other edit is needed):
from . import math_ops, string_ops, phiflow_defs, primitives, numpy_ops
_MODULES = {
'math': math_ops,
'string': string_ops,
'phiflow': phiflow_defs,
'numpy': numpy_ops, # Add here
}Why wrappers are needed:
- Standard library functions often lack type hints
- Registry generation requires type annotations for proper schema creation
- Wrappers provide control over logging and error handling
- Type conversion may be needed (e.g., NumPy types to Python types)
Classes can be registered in any module's get_classes() function:
# In definitions/math_ops.py
class Calculator:
"""A simple calculator class"""
def __init__(self, initial_value: float = 0.0):
self.value = initial_value
def add_to_value(self, amount: float) -> float:
self.value += amount
print(f"Calculator.add_to_value({amount}) = {self.value}")
return self.value
def get_classes() -> Dict[str, Any]:
return {
"Calculator": Calculator,
}After updating the module, regenerate the registry:
python main.py -p "math" registerHow it works:
- Constructor nodes are generated from
__init__signatures - Method nodes are auto-generated for all public instance methods (non-underscore)
- Methods use fully qualified names:
ClassName.method_name - First input to method nodes is always the instance
Limitations:
- C extension classes (like
datetime) only register constructors, not methods - This is due to
inspect.isfunction()returning False for C extension methods - For full method support, create Python wrapper classes
The registry system requires explicit type hints:
- Use basic Python types:
int,float,str,bool - Use
Anyfromtypingfor flexible types (note: has issues withfunction-schemalibrary) - Return type
Noneindicates no output - Missing type hints default to
"any"in registry