Validation

Lynx performs comprehensive validation before exporting diagrams to python-control. This ensures diagrams are well-formed and mathematically valid.

Validation Layers

Validation occurs in three layers:

  1. System Boundary Checks: At least one InputMarker and one OutputMarker

  2. Label Uniqueness: Warnings for duplicate block or connection labels

  3. Port Connectivity: All non-InputMarker input ports must be connected

Automatic Validation

Validation runs automatically when calling export methods:

# Validation happens here
sys = diagram.get_tf('r', 'y')  # Raises ValidationError if invalid
sys = diagram.get_ss('r', 'y')  # Raises ValidationError if invalid

You don’t need to manually call validation.

ValidationError

When validation fails, Lynx raises ValidationError with detailed context:

from lynx import ValidationError

try:
    sys = diagram.get_tf('r', 'y')
except ValidationError as e:
    print(f"Validation failed: {e}")
    print(f"Block ID: {e.block_id}")
    print(f"Port ID: {e.port_id}")

Error Attributes

  • message: Human-readable error description

  • block_id: ID of the block causing the error (if applicable)

  • port_id: ID of the port causing the error (if applicable)

Common Validation Errors

1. Missing System Boundaries

Error: “Diagram must have at least one InputMarker and one OutputMarker”

Cause: No IOMarker blocks, or all markers are the same type

Fix: Add at least one input and one output marker

# Before (invalid)
diagram.add_block('gain', 'K1', K=5.0)
diagram.add_block('transfer_function', 'plant', num=[1.0], den=[1.0, 1.0])

# After (valid)
diagram.add_block('io_marker', 'r', marker_type='input', label='r')
diagram.add_block('gain', 'K1', K=5.0)
diagram.add_block('transfer_function', 'plant', num=[1.0], den=[1.0, 1.0])
diagram.add_block('io_marker', 'y', marker_type='output', label='y')

2. Unconnected Input Port

Error: “Port ‘in’ of block ‘plant’ is not connected”

Cause: Block has an input port with no incoming connection

Fix: Connect the port or remove the block

# Before (invalid) - plant has no input connection
diagram.add_block('io_marker', 'r', marker_type='input', label='r')
diagram.add_block('transfer_function', 'plant', num=[1.0], den=[1.0, 1.0])
# Missing connection!

# After (valid)
diagram.add_block('io_marker', 'r', marker_type='input', label='r')
diagram.add_block('transfer_function', 'plant', num=[1.0], den=[1.0, 1.0])
diagram.add_connection('c1', 'r', 'out', 'plant', 'in')  # Connect input

3. Algebraic Loop

Error: “Algebraic loop detected in block ‘sum1’”

Cause: Feedback loop with no dynamics (no transfer function or integrator in the loop)

Fix: Add dynamics to the loop or remove the direct feedthrough

# Before (invalid) - pure gain feedback creates algebraic loop
diagram.add_block('sum', 'error', signs=['+', '-', '|'])
diagram.add_block('gain', 'K1', K=5.0)
diagram.add_block('gain', 'K2', K=2.0)
diagram.add_connection('c1', 'error', 'out', 'K1', 'in')
diagram.add_connection('c2', 'K1', 'out', 'K2', 'in')
diagram.add_connection('c3', 'K2', 'out', 'error', 'in2')  # Direct feedback - algebraic loop!

# After (valid) - add dynamics
diagram.add_block('sum', 'error', signs=['+', '-', '|'])
diagram.add_block('gain', 'K1', K=5.0)
diagram.add_block('transfer_function', 'plant',  # Add dynamics here
                  num=[1.0], den=[1.0, 1.0])
diagram.add_connection('c1', 'error', 'out', 'K1', 'in')
diagram.add_connection('c2', 'K1', 'out', 'plant', 'in')
diagram.add_connection('c3', 'plant', 'out', 'error', 'in2')  # Now valid

4. Duplicate Labels

Warning: “Duplicate block label ‘controller’ found”

Cause: Multiple blocks or connections with the same label

Fix: Use unique labels for all blocks and connections

# Before (warning)
diagram.add_block('gain', 'K1', K=5.0, label='controller')
diagram.add_block('gain', 'K2', K=2.0, label='controller')  # Duplicate!

# After (no warning)
diagram.add_block('gain', 'K1', K=5.0, label='controller_1')
diagram.add_block('gain', 'K2', K=2.0, label='controller_2')

Note: Duplicate labels issue warnings but don’t block export. However, signal references may be ambiguous.

Pre-Export Validation Workflow

Best practice for complex diagrams:

import lynx
from lynx import ValidationError

# Build diagram
diagram = lynx.Diagram()
# ... add blocks and connections ...

# Validate before analysis
try:
    # Attempt export (triggers validation)
    sys = diagram.get_tf('r', 'y')
    print("✓ Diagram is valid!")

    # Proceed with analysis
    import control as ct
    ct.bode_plot(sys)

except ValidationError as e:
    print(f"✗ Validation failed: {e}")
    print(f"  Block: {e.block_id}, Port: {e.port_id}")

    # Fix the issue...
    if e.port_id:
        print(f"  Check connections for block '{e.block_id}' port '{e.port_id}'")

SignalNotFoundError

Separate from ValidationError, this exception occurs when the specified signal references don’t exist:

from lynx import SignalNotFoundError

try:
    sys = diagram.get_tf('nonexistent', 'y')  # 'nonexistent' not found
except SignalNotFoundError as e:
    print(f"Signal not found: {e.signal_name}")
    print(f"Searched: {e.searched_locations}")

Error Attributes

  • signal_name: The signal reference that wasn’t found

  • searched_locations: List of places searched (IOMarker labels, connection labels, block outputs)

See Also