Hierarchical System#

The system module provides hierarchical namespace organization for domain objects. Systems can be nested arbitrarily deep with automatic parent-child relationship management, tag-based lookup, and dynamic reorganization.

Overview#

The System class is the culmination of all Jangada’s mixins, providing:

  • Hierarchical organization - Tree-structured subsystem relationships

  • Namespace access - Dict-style and attribute-style subsystem lookup

  • Dynamic reorganization - Automatic updates when tags change

  • Circular dependency prevention - Validates hierarchy constraints

  • Full persistence - Save/load entire hierarchies

  • Rich display - Beautiful terminal visualization

System(*args, **kwargs)

Hierarchical namespace container for organizing subsystems.

Quick Start#

Basic hierarchy:

from jangada.system import System

# Create systems
root = System(tag='root', name='Root System')
child = System(tag='child', name='Child System')

# Build hierarchy
root.add(child)

# Access subsystems
root['child']    # Dict-style
root.child       # Attribute-style

Dict-style registration:

system = System(tag='system')
system['sensors'] = System(name='Sensors')
system['controllers'] = System(name='Controllers')

# Tags are automatically set from keys
assert system['sensors'].tag == 'sensors'

Complex hierarchy:

system = System(tag='system')

# Build via dict-style
system['sensors'] = System(name='Sensors')
system['sensors']['temperature'] = System(name='Temp Sensor')
system['sensors']['pressure'] = System(name='Pressure Sensor')

# Navigate
temp = system.sensors.temperature
print(temp.name)  # 'Temp Sensor'

System Reference#

class jangada.system.System(*args, **kwargs)#

Bases: Persistable, Displayable, Identifiable, Taggable, Nameable, Describable

Hierarchical namespace container for organizing subsystems.

System provides a framework for building tree-structured hierarchies with automatic bidirectional parent-child relationship management. Subsystems can be accessed via tag using either dict-style (system['tag']) or attribute-style (system.tag) syntax.

Key features: - Automatic parent-child relationship management - Tag-based namespace access (dict and attribute style) - Dynamic tag reorganization via observers - Circular dependency prevention - Persistence of entire hierarchies - Rich terminal display integration

Attributes:
subsystemsdict[str, System]

Dictionary mapping tags to subsystem objects. Managed automatically.

supersystemSystem or None

Get parent system.

supersystem_chainlist[System]

Get list of all ancestors.

See also

Persistable

Serialization base class

Displayable

Terminal display base class

Identifiable

Unique identification mixin

Taggable

Tag-based identification mixin

Notes

Hierarchy Structure

Systems form a tree structure (not DAG). Each system has at most one parent. Circular dependencies are prevented automatically.

Tag Requirements

Subsystems must have tags to be registered. Tags must be valid Python identifiers and cannot be Python keywords.

Tag Observers

When a subsystem’s tag changes, the parent’s subsystems dict is automatically updated. This is achieved through the observer pattern.

Identity vs Content Equality
  • sys1 == sys2 uses Identifiable (ID-based equality)

  • sys1.equal(sys2) uses Persistable (content-based equality)

Persistence

Saving a system saves its entire subtree. Parent references are stored as IDs, allowing proper reconstruction on load.

Reserved Attributes

Avoid using subsystem tags that conflict with System’s methods and properties (e.g., ‘add’, ‘remove’, ‘subsystems’, ‘tag’, ‘id’).

Examples

Create simple hierarchy:

root = System(tag='root', name='Root System')
child = System(tag='child', name='Child System')

root.add(child)

# Access via dict or attribute
assert root['child'] is child
assert root.child is child

Dict-style registration:

root = System(tag='root')
root['sensor'] = System(name='Temperature Sensor')

# Tag is automatically set from key
assert root['sensor'].tag == 'sensor'

Build complex hierarchy:

system = System(tag='system')

# Using dict-style
system['sensors'] = System(name='Sensors')
system['sensors']['temperature'] = System(name='Temp Sensor')
system['sensors']['pressure'] = System(name='Pressure Sensor')

# Navigate
temp = system.sensors.temperature
assert temp.name == 'Temp Sensor'

Dynamic reorganization:

root = System(tag='root')
child = System(tag='old_tag')
root.add(child)

# Change tag - dict keys update automatically
child.tag = 'new_tag'
assert 'new_tag' in root
assert 'old_tag' not in root

Save and load hierarchies:

root = System(tag='root')
root['child'] = System(tag='child', name='Child')

root.save('hierarchy.sys')

loaded = System.load('hierarchy.sys')
assert loaded.child.name == 'Child'

Content comparison:

sys1 = System(tag='test', name='Name')
sys2 = System(tag='test', name='Name')

# Different IDs, so ID-based equality is False
assert sys1 != sys2

# But same content, so content equality is True
assert sys1.equal(sys2)
class ProxyDataset(dataset: Dataset)#

Bases: object

Lazy-loading wrapper for HDF5 datasets.

ProxyDataset provides array-like access to HDF5 datasets without loading the entire dataset into memory. It supports slicing, indexing, and modification operations, delegating to the underlying HDF5 dataset.

This class is automatically used when opening Persistable objects in context manager mode. It enables efficient access to large arrays that would be impractical to load fully.

Attributes:
_dataseth5py.Dataset

The underlying HDF5 dataset.

_attrsdict

Cached dataset attributes (metadata).

_dataset_type_namestr

Fully qualified name of the dataset type.

See also

Persistable

Parent class that creates ProxyDatasets

Notes

ProxyDataset does not implement the full NumPy array API. It provides:

Supported:
  • Slicing and indexing (__getitem__, __setitem__)

  • Shape, dtype, ndim, size, nbytes properties

  • Append operation

  • Automatic resizing on out-of-bounds assignment

Not supported:
  • Arithmetic operations (+, -, *, etc.)

  • Iteration (__iter__)

  • Length (__len__)

  • Universal functions (ufuncs)

For full array operations, load the data explicitly:

data_array = exp.data[:]  # Load entire dataset
result = data_array * 2   # Now can use numpy operations

Examples

Accessing data lazily:

with Experiment('data.hdf5', mode='r') as exp:
    # exp.data is a ProxyDataset
    print(exp.data.shape)  # (1000000,) - not loaded yet

    # Load only what you need
    chunk = exp.data[100:200]  # Loads only 100 elements

Modifying data:

with Experiment('data.hdf5', mode='r+') as exp:
    exp.data[50] = 99  # Modify single element
    exp.data[10:20] = new_values  # Modify slice

Appending data:

with Experiment('data.hdf5', mode='r+') as exp:
    exp.data.append(np.array([new, data]))
append(value: Any) None#

Append data to the end of the dataset.

Efficiently adds new data by resizing the dataset and writing only the new values. Much faster than loading, concatenating, and saving.

Parameters:
valueAny

Data to append. Must be compatible with the dataset type. For 1D datasets, can be a single value or array. For nD datasets, first dimension is extended.

Raises:
AssertionError

If the value’s metadata doesn’t match the dataset’s metadata.

Notes

The append operation: 1. Disassembles the value to array + metadata 2. Validates metadata matches existing dataset 3. Resizes dataset to accommodate new data 4. Writes only the new data (efficient)

For scalar datasets (ndim=0), append is not supported as they cannot be resized.

Examples

Append to time series:

with TimeSeries('data.hdf5', mode='r+') as ts:
    ts.data.append(np.array([new, values]))

Incremental data collection:

with Experiment('exp.hdf5', mode='r+') as exp:
    for measurement in new_measurements:
        exp.readings.append(measurement)
property attrs: dict#

Get dataset metadata attributes.

Returns:
dict

Copy of the dataset’s metadata attributes.

Notes

Returns a copy to prevent accidental modification. The __dataset_type__ attribute is excluded as it’s used internally.

Examples

>>> proxy.attrs
{'timezone': 'UTC', 'units': 'kelvin'}
property dtype: dtype#

Get the data type of the dataset.

Returns:
numpy.dtype

The dtype of the dataset elements.

Examples

>>> proxy.dtype
dtype('float64')
property nbytes: int#

Get the total bytes consumed by the dataset elements.

Returns:
int

Total bytes (size * dtype.itemsize).

Examples

>>> proxy.nbytes
80000  # 10000 elements * 8 bytes per float64
property ndim: int#

Get the number of dimensions.

Returns:
int

Number of dimensions.

Examples

>>> proxy.ndim
3
property shape: tuple[int]#

Get the shape of the dataset.

Returns:
tuple[int, …]

Dataset dimensions.

Examples

>>> proxy.shape
(1000, 100, 10)
property size: int#

Get the total number of elements in the dataset.

Returns:
int

Total number of elements (product of shape).

Examples

>>> proxy.shape
(100, 10)
>>> proxy.size
1000
add(*subsystems: System) None#

Add one or more subsystems.

Parameters:
*subsystemsSystem

Subsystems to add.

Raises:
ValueError

If subsystem has no tag, tag conflicts exist, or circular dependency would be created.

TypeError

If argument is not a System.

Notes

Equivalent to setting subsystem.supersystem = self for each subsystem.

Examples

>>> root = System(tag='root')
>>> child1 = System(tag='child1')
>>> child2 = System(tag='child2')
>>> root.add(child1, child2)
>>> len(root)
2
copy() Serializable#

Create an independent copy of this object.

Uses serialization with is_copy=True, so only copiable properties are included in the copy.

Returns:
Serializable

A new instance with the same copiable property values.

See also

__copy__

Implements copy.copy() support

Notes

The copy is created via serialization, so: - Only copiable properties are copied - Non-copiable properties use their defaults - Nested objects are also copied (deep copy) - All property initialization (parsers, observers, initializers) runs

Examples

>>> class MyClass(Serializable):
...     value = SerializableProperty(default=0, copiable=True)
...     cache = SerializableProperty(default=0, copiable=False)
...
>>> obj = MyClass(value=42, cache=100)
>>> copied = obj.copy()
>>> copied.value
42
>>> copied.cache
0
>>> copied is obj
False
description: str#

Free-form descriptive text.

Extended context, documentation, or explanatory information. Can be multiline and arbitrarily long. Intended for detailed information that doesn’t fit in a short name.

See also

Nameable.name

Short display name

Notes

Descriptions are for extended information - use name for short labels. No formatting is applied - if you need formatted text (markdown, HTML), store it as a string and format at display time.

Common use cases: detailed documentation, usage instructions, configuration notes, debugging context, help text in UIs.

Examples

Basic usage:

obj.description = "This sensor monitors ambient temperature."

Multiline text:

obj.description = '''
This is a detailed description.
It can span multiple lines.
Useful for documentation.
'''

Very long text:

obj.description = "Long documentation..." * 1000  # No limit

Normalization (same as name):

obj.description = "  Text  "
assert obj.description == "Text"

obj.description = ""
assert obj.description is None
static deserialize(data: Any) Any#

Recursively deserialize data to reconstruct objects.

Converts serialized dictionary structures back into Python objects. Handles nested Serializable objects, collections, and primitives.

Parameters:
dataAny

The serialized data to deserialize.

Returns:
Any

The reconstructed object: - dict with ‘__class__’ → Serializable instance - list → list (recursively deserialized) - dict without ‘__class__’ → dict (recursively deserialized) - Primitives → unchanged - None → None

Raises:
TypeError

If the data type is not supported for deserialization.

See also

serialize

Convert objects to serializable data

Notes

If a class referenced in ‘__class__’ is not registered (not imported), deserialize creates a generic Serializable subclass on-the-fly with the necessary properties. This allows reading data even when the original class definition is unavailable.

Deserialization triggers all property parsers, observers, and post-initializers as if the object were being constructed normally.

Examples

Deserialize a simple object:

>>> class MyClass(Serializable):
...     value = SerializableProperty(default=0)
...
>>> qualname = get_full_qualified_name(MyClass)
>>> data = {'__class__': qualname, 'value': 42}
>>> obj = Serializable.deserialize(data)
>>> obj.value
42

Roundtrip serialization:

>>> original = MyClass(value=99)
>>> data = Serializable.serialize(original)
>>> restored = Serializable.deserialize(data)
>>> restored.value
99
>>> original == restored
True

Collections are handled recursively:

>>> data = [1, 2, {'a': 3}]
>>> result = Serializable.deserialize(data)
>>> result
[1, 2, {'a': 3}]

Unknown classes create generic Serializable objects:

>>> data = {
...     '__class__': 'unknown.Module.UnknownClass',
...     'prop1': 42,
...     'prop2': "test"
... }
>>> obj = Serializable.deserialize(data)
>>> obj.prop1
42
display_settings: DisplaySettings#

Configuration for display formatting and styling.

Provides access to all visual customization options for this object’s terminal display. Each instance receives its own DisplaySettings object by default (via factory), allowing per-object customization without affecting other instances.

See also

DisplaySettings

The settings class with all options

format_as_form

Uses property_style setting

format_as_table

Uses table_* settings

Notes

Factory Behavior

The display_settings property uses a factory function to create a new DisplaySettings instance for each Displayable object. This ensures: - Each instance has independent settings by default - No unexpected sharing between objects - Settings can be customized per-object without side effects

Sharing Settings

To share settings across multiple objects, explicitly assign the same DisplaySettings instance:

shared_settings = DisplaySettings() obj1.display_settings = shared_settings obj2.display_settings = shared_settings

Now changes to shared_settings affect both objects.

Persistence

DisplaySettings is Persistable, so settings can be: - Saved to .disp files (themes) - Loaded from saved themes - Versioned and shared across projects - Used to maintain consistent styling

Immediate Effect

Changes to display_settings take effect immediately on the next render: - print(obj) uses current settings - obj.__rich__() uses current settings - obj.to_html() uses current settings

No need to “apply” or “refresh” - just change and use.

Available Settings

See DisplaySettings documentation for all available options: - Console: console_width - Panel: panel_border_style, panel_box, panel_title_align - Property labels: property_style - Tables: table_index_style, table_header_style, table_round_floats, table_spacing

Examples

Customize styling:

obj.display_settings.panel_border_style = 'green'
obj.display_settings.table_header_style = 'bold magenta'
obj.display_settings.console_width = 120

Access specific settings:

width = obj.display_settings.console_width
border = obj.display_settings.panel_border_style
spacing = obj.display_settings.table_spacing

Share settings across objects:

shared = DisplaySettings()
shared.panel_border_style = 'yellow'
shared.table_spacing = 8

obj1.display_settings = shared
obj2.display_settings = shared
# Both objects now share the same styling

Load saved theme:

theme = DisplaySettings.load('dark_theme.disp')
obj.display_settings = theme

Save current settings:

obj.display_settings.save('my_custom_theme.disp')

Temporary style changes:

# Modify for one display
obj.display_settings.console_width = 100
print(obj)

# Reset to defaults
obj.display_settings = DisplaySettings()
equal(system: System) bool#

Compare content equality with another system.

Uses Persistable’s equality which compares serialized content recursively, ignoring non-copiable properties like IDs.

Parameters:
systemSystem

System to compare with.

Returns:
bool

True if systems have identical content, False otherwise.

See also

Persistable.__eq__

Content comparison implementation

Identifiable.__eq__

ID comparison implementation

Notes

Distinction from ==: - sys1 == sys2 uses Identifiable (compares IDs) - sys1.equal(sys2) uses Persistable (compares content)

This is useful for comparing copies or checking if two systems have the same structure and data, regardless of their identities.

Examples

>>> sys1 = System(tag='test', name='Name')
>>> sys2 = System(tag='test', name='Name')
>>> sys1 == sys2  # ID-based equality
False
>>> sys1.equal(sys2)  # Content-based equality
True
extension: str = '.sys'#

File extension for saved System files.

format_as_form(data: dict[str, str] | Series) Table#

Format data as key-value form.

Creates two-column table with keys (left) and values (right).

Parameters:
datadict[str, str] or pandas.Series

Key-value pairs to display.

Returns:
Table

Rich Table in form layout.

Notes

Keys automatically get ‘:’ appended and use property_style. Values are left-aligned with no special styling.

Examples

>>> form = self.format_as_form({
...     'Name': 'Alice',
...     'Age': '25',
...     'City': 'NYC'
... })
format_as_table(frame: DataFrame, show_index: bool = True, format_index_as_property: bool = False, format_header_as_property: bool = False, align_header: str | dict[str, str] = 'center', align_column: str | dict[str, str] | None = None, max_rows: int = 31, **kwargs) Table#

Format DataFrame as Rich table.

Creates formatted table with automatic alignment, optional truncation, and extensive customization options.

Parameters:
framepandas.DataFrame

Data to display.

show_indexbool, optional

Include index column. Default True.

format_index_as_propertybool, optional

Use property_style for index. Default False.

format_header_as_propertybool, optional

Use property_style for headers. Default False.

align_headerstr or dict[str, str], optional

Header alignment. Default ‘center’.

align_columnstr, dict[str, str], or None, optional

Column alignment. None = auto-detect. Default None.

max_rowsint, optional

Max rows before truncation. Default 31.

**kwargs

Additional options: - header_style : str or None - index_style : str or None - round_floats : int, dict, or None

Returns:
Table

Formatted Rich Table.

Raises:
TypeError

If alignment or rounding parameters have invalid types.

Notes

Auto-alignment (align_column=None): numeric right, datetime/text left.

Truncation: Shows first n/2 rows, ‘…’, last n/2 rows when > max_rows.

Creates DataFrame copy for processing.

Examples

Basic usage:

>>> table = self.format_as_table(df)

Custom alignment:

>>> table = self.format_as_table(df, align_column='right')

Float rounding:

>>> table = self.format_as_table(df, round_floats=2)

Large dataset:

>>> table = self.format_as_table(df, max_rows=20)
classmethod get_instance(id_: str) Identifiable | None#

Retrieve an Identifiable instance by its ID.

Looks up an object in the global instance registry using its unique identifier. Returns the object if it exists and has not been garbage collected, otherwise returns None.

Parameters:
id_str

The 32-character hexadecimal UUID string to look up.

Returns:
Identifiable or None

The instance with the given ID, or None if no such instance exists or has been garbage collected.

See also

id

The unique identifier property

Notes

This method searches the global registry maintained by the Identifiable class. The registry uses weak references, so objects can be garbage collected even if they’re in the registry.

If an object has been garbage collected, its ID will no longer be in the registry and this method will return None.

The lookup is O(1) as the registry is implemented as a dictionary.

Examples

Basic lookup:

>>> obj = Identifiable()
>>> obj_id = obj.id
>>> retrieved = Identifiable.get_instance(obj_id)
>>> retrieved is obj
True

Lookup of non-existent ID:

>>> Identifiable.get_instance('00000000000000000000000000000000')
None

Object garbage collected:

>>> obj = Identifiable()
>>> obj_id = obj.id
>>> del obj  # Object can be garbage collected
>>> Identifiable.get_instance(obj_id)
None
id: str#

Globally unique identifier (UUID v4).

A 32-character hexadecimal UUID v4 string that uniquely identifies this instance. Automatically generated on first access if not explicitly provided. Write-once (cannot be changed after initialization) and non-copiable (each copy gets a new ID).

See also

Identifiable.get_instance

Retrieve instance by ID

Notes

The ID serves as the basis for __hash__() and __eq__(), enabling instances to be used in sets and as dictionary keys. Objects with the same ID are considered equal.

Examples

Automatic generation:

obj = Identifiable()
print(obj.id)  # '3a5f8e2c1b9d4f7a0b1c2d3e4f5a6b7c'

Explicit setting (during deserialization):

# ID is validated and normalized
obj.id = 'A1B2C3D4-E5F6-4789-ABCD-EF0123456789'

Immutability:

obj = Identifiable()
obj.id = 'different-id'  # Raises AttributeError

Lookup by ID:

obj_id = obj.id
retrieved = Identifiable.get_instance(obj_id)
assert retrieved is obj
classmethod load(path: Path | str) Persistable#

Load an object from an HDF5 file.

Parameters:
pathPath | str

File path to load from.

Returns:
Persistable

Reconstructed object.

Raises:
FileNotFoundError

If file does not exist.

See also

save

Save counterpart

Notes

The entire file is loaded into memory. For large files, consider using context manager mode for lazy loading:

with Experiment('large_file.hdf5', mode='r') as exp:
    # Access data on-demand
    chunk = exp.data[100:200]

Examples

>>> exp = Experiment.load('experiment.hdf5')
>>> print(exp.name)
'Test'
classmethod load_serialized_data(path: Path | str) Any#

Load serialized data dictionary from HDF5 file.

This is a low-level method that reads an HDF5 file and returns the data dictionary (which can be passed to Serializable.deserialize).

Parameters:
pathPath | str

File path to load from.

Returns:
Any

Deserialized data dictionary.

Raises:
FileNotFoundError

If the file does not exist.

See also

load

High-level load method

save_serialized_data

Save counterpart

Notes

This method is called internally by load(). Users typically use load() instead of calling this directly.

name: str#

Human-readable display name.

A string for labeling and display purposes. Less restrictive than tags - can contain spaces, special characters, and Unicode. Intended for user-facing contexts like UI labels, reports, and logs.

See also

Taggable.tag

Symbolic identifier for namespace access

Describable.description

Extended description text

Notes

Use names for display and human consumption. For programmatic identifiers that work in attribute access, use tags from Taggable.

Names are mutable and have no uniqueness constraints. Very long names may need truncation for display purposes.

Examples

Basic usage:

obj.name = "Temperature Sensor"
obj.name = "Sensor #1 (Main)"
obj.name = "Test-Case-A"

Unicode support:

obj.name = "Tëst Nämé"
obj.name = "测试名称"
obj.name = "📊 Data Dashboard"

Normalization:

obj.name = "  Name  "
assert obj.name == "Name"

obj.name = "   "
assert obj.name is None

obj.name = ""
assert obj.name is None

Type conversion:

obj.name = 123
assert obj.name == "123"
remove(subsystem: System | str) None#

Remove a subsystem.

Parameters:
subsystemSystem or str

Subsystem object or tag to remove.

Raises:
ValueError

If subsystem is not registered in this system.

Notes

Equivalent to setting subsystem.supersystem = None.

Examples

>>> root = System(tag='root')
>>> child = System(tag='child')
>>> root.add(child)
>>> root.remove(child)
>>> child in root
False
>>> root.add(child)
>>> root.remove('child')
>>> child in root
False
save(path: Path | str, overwrite: bool = True, use_default_extension: bool = True) None#

Save this object to an HDF5 file.

Parameters:
pathPath | str

File path to save to.

overwritebool, optional

If True, overwrite existing file. If False, raise FileExistsError if file exists. Default is True.

use_default_extensionbool, optional

If True, add the class’s default extension if not present. Default is True.

Raises:
FileExistsError

If file exists and overwrite=False.

See also

load

Load counterpart

Notes

The save process: 1. Resolves path (adds extension if needed) 2. Checks if file exists (if overwrite=False) 3. Serializes object to dictionary 4. Writes dictionary to HDF5 file

The entire object is serialized, so this may be slow for very large objects. For incremental updates, use context manager mode.

Examples

Basic save:

exp = Experiment(name="Test", temperature=300.0)
exp.save('experiment.hdf5')

Save without overwriting:

exp.save('experiment.hdf5', overwrite=False)

Save with custom extension:

exp.save('data.h5', use_default_extension=False)
save_serialized_data(path: Path | str, data: Any) None#

Save serialized data dictionary to HDF5 file.

This is a low-level method that writes a data dictionary (from Serializable.serialize) to an HDF5 file.

Parameters:
pathPath | str

File path to save to.

dataAny

Serialized data (typically from Serializable.serialize).

See also

save

High-level save method

load_serialized_data

Load counterpart

Notes

This method is called internally by save(). Users typically use save() instead of calling this directly.

Creates an HDF5 file with a ‘root’ group containing all the data.

static serialize(obj: Any, is_copy: bool = False) Any#

Recursively serialize an object to a dictionary structure.

Converts Python objects to a nested dictionary structure suitable for JSON, HDF5 attributes, or other storage formats. Handles Serializable objects, collections, primitives, and dataset types.

Parameters:
objAny

The object to serialize.

is_copybool, optional

If True, only serialize copiable properties. If False, serialize all properties. Default is False.

Returns:
Any

The serialized representation: - Serializable → dict with ‘__class__’ key - list/tuple/set → list (recursively serialized) - dict → dict (recursively serialized values) - Primitives → unchanged - None → None

Raises:
TypeError

If the object’s type is not registered as primitive, dataset, or Serializable.

See also

deserialize

Reconstruct objects from serialized data

copy

Create a copy using serialize with is_copy=True

Notes

The serialization format for Serializable objects is: ```python {

‘__class__’: ‘module.ClassName’, ‘property1’: value1, ‘property2’: value2, …

}#

Tuples and sets are converted to lists (Python’s JSON doesn’t distinguish these).

Examples

Serialize a simple object:

>>> class MyClass(Serializable):
...     value = SerializableProperty(default=0)
...
>>> obj = MyClass(value=42)
>>> data = Serializable.serialize(obj)
>>> '__class__' in data
True
>>> data['value']
42

Serialize with copy mode (only copiable properties):

>>> class MyClass2(Serializable):
...     important = SerializableProperty(default=0, copiable=True)
...     cache = SerializableProperty(default=0, copiable=False)
...
>>> obj = MyClass2(important=10, cache=20)
>>> data = Serializable.serialize(obj, is_copy=True)
>>> 'important' in data
True
>>> 'cache' in data
False

Collections are handled recursively:

>>> data = Serializable.serialize([1, "two", 3.0])
>>> data
[1, 'two', 3.0]
subsystems: dict[str, System]#

Dictionary mapping tags to subsystem objects.

Automatically managed - subsystems are added/removed via add(), remove(), dict-style assignment, or by setting supersystem property. Manual modification is discouraged as it bypasses validation.

See also

add

Add subsystems

remove

Remove subsystems

__setitem__

Dict-style subsystem registration

Notes

Automatic Management

This dict is managed automatically. When you add a subsystem via system.add(child) or system['tag'] = child, it is added to this dict. When tags change, dict keys update automatically via observers.

Direct Modification

Avoid directly modifying this dict (e.g., system.subsystems['tag'] = child) as it bypasses validation and observer setup. Use add(), remove(), or dict-style assignment (system['tag'] = child) instead.

Serialization

When a System is saved, the subsystems dict is serialized, preserving the entire hierarchy. Parent references are stored as IDs to avoid circular references.

Examples

Access subsystems:

for tag, subsystem in system.subsystems.items():
    print(f"{tag}: {subsystem.name}")

Check contents:

if 'sensor' in system.subsystems:
    sensor = system.subsystems['sensor']

Iterate:

for subsystem in system.subsystems.values():
    process(subsystem)
property supersystem: System | None#

Get parent system.

Returns:
System or None

Parent system, or None if this is a root system.

Examples

>>> root = System(tag='root')
>>> child = System(tag='child')
>>> root.add(child)
>>> child.supersystem is root
True
>>> root.supersystem is None
True
property supersystem_chain: list[System]#

Get list of all ancestors.

Returns list of all parent systems from immediate parent to root, in order.

Returns:
list[System]

List of ancestors, nearest first.

Notes

Walks the hierarchy upwards until reaching a root (supersystem is None). Used for circular dependency detection and hierarchy navigation.

Examples

>>> root = System(tag='root')
>>> level1 = System(tag='level1')
>>> level2 = System(tag='level2')
>>> root.add(level1)
>>> level1.add(level2)
>>> level2.supersystem_chain
[<System level1>, <System root>]
tag: str | None#

Symbolic identifier for namespace-style access.

A validated string that must be a valid Python identifier (but not a keyword). Intended for programmatic, attribute-style access in containers and namespaces, similar to how pandas DataFrames expose columns.

See also

Nameable.name

Human-readable display name

Notes

Tags are mutable (can be changed) to support dynamic reorganization of namespaces. Uniqueness is not enforced globally - containers should manage tag uniqueness within their scope.

Use tags for programmatic access, not display. For human-readable labels, use the name property from Nameable.

Examples

Valid tags:

obj.tag = "sensor_a"
obj.tag = "temp_sensor_01"
obj.tag = "_private"

Invalid tags:

obj.tag = "123invalid"      # Starts with digit
obj.tag = "invalid-tag"     # Contains hyphen
obj.tag = "invalid tag"     # Contains space
obj.tag = "if"              # Python keyword
obj.tag = ""                # Empty string

Namespace access pattern:

# Container provides attribute-style access by tag
system.sensor_a  # Returns object with tag='sensor_a'

Whitespace normalization:

obj.tag = "  my_tag  "
assert obj.tag == "my_tag"
to_html(width: int | None = None, **kwargs) str#

Export display as HTML.

Returns:
str

HTML with inline styles.

Examples

>>> html = obj.to_html()
>>> with open('output.html', 'w') as f:
...     f.write(html)
to_svg(width: int | None = None, **kwargs) str#

Export display as SVG.

Returns:
str

SVG vector graphics.

Examples

>>> svg = obj.to_svg()
>>> with open('output.svg', 'w') as f:
...     f.write(svg)

Core Concepts#

Hierarchy Structure#

Systems form a tree structure (not DAG). Each system has:

  • Zero or one parent (supersystem)

  • Zero or more children (subsystems)

  • No circular dependencies (automatically prevented)

Example hierarchy:

root
├── sensors
│   ├── temperature
│   └── pressure
└── controllers
    └── main

Tag-Based Access#

Subsystems are accessed by their tags using either dict-style or attribute-style syntax:

Dict-Style:

system['sensor_name']
system['sensor_name'] = System()

if 'sensor_name' in system:
    sensor = system['sensor_name']

Attribute-Style:

system.sensor_name
# Note: __setattr__ not yet implemented

if hasattr(system, 'sensor_name'):
    sensor = system.sensor_name

Which to use?

  • Dict-style: More explicit, works with any valid identifier

  • Attribute-style: Cleaner syntax for static structures

Bidirectional Relationships#

Parent-child relationships are bidirectional and automatically managed:

Setting parent automatically updates child list:

parent = System(tag='parent')
child = System(tag='child')

child.supersystem = parent
# Automatically: parent.subsystems['child'] = child

Setting child automatically updates parent:

parent = System(tag='parent')
child = System(tag='child')

parent.add(child)
# Automatically: child.supersystem = parent

Reparenting:

parent1 = System(tag='parent1')
parent2 = System(tag='parent2')
child = System(tag='child')

parent1.add(child)
parent2.add(child)  # Removes from parent1, adds to parent2

Dynamic Tag Reorganization#

When a subsystem’s tag changes, parent dicts update automatically via observers:

Example:

root = System(tag='root')
child = System(tag='old_tag')
root.add(child)

assert 'old_tag' in root

# Change tag
child.tag = 'new_tag'

# Dict keys updated automatically
assert 'new_tag' in root
assert 'old_tag' not in root

Usage Patterns#

Building Hierarchies#

Incremental construction:

system = System(tag='system')

sensors = System(tag='sensors')
system.add(sensors)

temp = System(tag='temperature')
sensors.add(temp)

Nested construction:

system = System(tag='system')
system['sensors'] = System()
system['sensors']['temperature'] = System()
system['sensors']['pressure'] = System()

Bulk addition:

system = System(tag='system')

components = [
    System(tag='comp1'),
    System(tag='comp2'),
    System(tag='comp3'),
]

system.add(*components)

Persistence#

Save hierarchy:

root = System(tag='root')
root['child'] = System(tag='child', name='Child')

root.save('hierarchy.sys')

Load hierarchy:

loaded = System.load('hierarchy.sys')

# Hierarchy reconstructed
assert loaded.child.name == 'Child'
assert loaded.child.supersystem is loaded

Content comparison:

sys1 = System(tag='test', name='Name')
sys2 = System(tag='test', name='Name')

# Different IDs
assert sys1 != sys2  # ID-based equality

# Same content
assert sys1.equal(sys2)  # Content-based equality

Display#

Terminal output:

system = System(tag='system', name='My System')
system['sensor'] = System(tag='sensor', name='Sensor')

print(system)  # Rich formatted panel

Customize display:

system.display_settings.panel_border_style = 'green'
system.display_settings.console_width = 120
print(system)

Export to HTML/SVG:

html = system.to_html()
svg = system.to_svg()

Advanced Topics#

Validation and Constraints#

Systems automatically validate:

Tag requirements:

child = System(tag=None)
parent.add(child)  # ValueError: must have tag

Circular dependencies:

sys_a = System(tag='a')
sys_b = System(tag='b')

sys_a.add(sys_b)
sys_b.add(sys_a)  # ValueError: circular dependency

Tag conflicts:

parent = System(tag='parent')
parent['sensor'] = System()
parent['sensor'] = System()  # OK: replaces

child1 = System(tag='sensor')
child2 = System(tag='sensor')
parent.add(child1)
parent.add(child2)  # ValueError: tag conflict

Observer Pattern#

System uses observers to maintain consistency when tags change:

How it works:

1. Subsystem added → Observer attached to tag property
2. Tag changes → Observer notified with old and new values
3. Observer updates parent's subsystems dict
4. Validates no conflicts, updates keys

Observer lifecycle:

parent = System(tag='parent')
child = System(tag='child')

parent.add(child)
# → Observer created and attached to child.tag

child.tag = 'renamed'
# → Observer fires, updates parent.subsystems

parent.remove(child)
# → Observer detached and cleaned up

Manual observer access (advanced):

# Check observers
observers = parent._tag_observers

# observers[child] contains the observer function

Identity vs Content Equality#

System supports two types of equality:

ID-Based (``==`` operator):

Uses Identifiable.__eq__

sys1 = System(tag='test')
sys2 = System(tag='test')

sys1 == sys2  # False (different IDs)

Content-Based (``equal()`` method):

Uses Persistable.__eq__

sys1 = System(tag='test', name='Name')
sys2 = System(tag='test', name='Name')

sys1.equal(sys2)  # True (same content)

Use cases:

  • ==: Check if same object (by ID)

  • equal(): Check if same structure/data (for copies)

Inheritance Hierarchy#

System inherits from five base classes:

System
├── Persistable      → Save/load to HDF5
├── Displayable      → Rich terminal display
├── Identifiable     → Unique ID, hash, equality
├── Taggable         → Symbolic tag for namespace
├── Nameable         → Human-readable name
└── Describable      → Free-form description

Each mixin provides orthogonal capabilities that System combines.

API Design Decisions#

Why Store Parent as ID?#

The _supersystem_id property stores the parent’s ID rather than a direct reference. This design:

Advantages:

  • Avoids circular references in serialization

  • Allows garbage collection (weak reference behavior)

  • Enables proper save/load of hierarchies

  • Works with Identifiable’s global registry

Trade-offs:

  • Requires ID lookup on every supersystem access

  • Parent must exist in Identifiable registry

  • Slightly more complex implementation

Why Mutable Tags?#

Tags are mutable (can be changed after creation). This design:

Advantages:

  • Dynamic reorganization without re-creating objects

  • Flexible namespace evolution

  • Observer pattern enables automatic updates

Trade-offs:

  • More complex implementation (observers required)

  • Potential for tag conflicts (validated automatically)

  • Users must be careful when changing tags

Tree vs DAG Structure#

System enforces a tree structure (single parent per system) rather than a DAG (multiple parents).

Why trees?:

  • Simpler mental model

  • Easier serialization

  • Clear ownership semantics

  • Prevents complex circular dependency scenarios

If you need DAG:

  • Use references instead of hierarchy

  • Store IDs in custom properties

  • Implement custom relationship management

Best Practices#

Naming Conventions#

Tags (machine identifiers):

  • Use lowercase with underscores: sensor_temp

  • Be descriptive but concise

  • Include type/category: ctrl_main, sensor_pressure

  • Avoid abbreviations unless standard

Names (human labels):

  • Use Title Case: "Temperature Sensor"

  • Include units where relevant: "Temp (°C)"

  • Be descriptive: "Primary Temperature Sensor"

Organization#

Logical grouping:

system = System(tag='system')
system['sensors'] = System()       # Group by type
system['controllers'] = System()
system['processors'] = System()

Hierarchical decomposition:

root = System(tag='root')
root['subsystem_a'] = System()
root['subsystem_a']['component_1'] = System()
root['subsystem_a']['component_2'] = System()

Keep hierarchies shallow: 3-5 levels is typically sufficient.

Error Handling#

Check before accessing:

if 'sensor' in system:
    sensor = system['sensor']
else:
    print("Sensor not found")

Handle KeyError/AttributeError:

try:
    sensor = system['nonexistent']
except KeyError:
    print("Subsystem not found")

try:
    sensor = system.nonexistent
except AttributeError:
    print("Subsystem not found")

Validate before adding:

if child.tag is None:
    child.tag = 'default_tag'

if child.tag not in parent:
    parent.add(child)

Memory Management#

Weak references: Identifiable uses weak references, so systems can be garbage collected when no longer referenced.

Break cycles: When deleting systems, explicitly remove from parent:

parent.remove(child)
del child

Large hierarchies: Be mindful of memory usage with deep/wide trees.

Troubleshooting#

Common Errors#

ValueError: Subsystems must have a tag:

# Problem
child = System(tag=None)
parent.add(child)

# Solution
child.tag = 'child_tag'
parent.add(child)

ValueError: circular dependency:

# Problem
sys_a.add(sys_b)
sys_b.add(sys_a)

# Solution: Don't create cycles
# Use references instead if bidirectional link needed

ValueError: tag already used:

# Problem
parent['sensor'] = System()
child = System(tag='sensor')
parent.add(child)

# Solution: Use different tag or replace
parent['sensor'] = child  # Replaces existing

KeyError: No subsystem registered:

# Problem
system['nonexistent']

# Solution: Check first
if 'nonexistent' in system:
    subsys = system['nonexistent']

Tag Observer Issues#

Tag conflicts during reorganization:

# If tag observer raises error
try:
    child.tag = 'conflicting_tag'
except ValueError as e:
    print(f"Tag change failed: {e}")
    # Tag remains unchanged

Observer not firing:

# Verify subsystem is registered
assert child in parent

# Change tag
child.tag = 'new_tag'

# Verify update
assert 'new_tag' in parent

Persistence Issues#

Hierarchy not reconstructed:

# Ensure entire tree is saved
root.save('hierarchy.sys')  # Saves root and all children

# Not this:
child.save('child.sys')  # Only saves child, loses parent

IDs change after load:

# IDs are non-copiable, so each load gets new IDs
original_id = system.id
system.save('system.sys')
loaded = System.load('system.sys')

assert loaded.id != original_id  # Expected behavior

Performance Considerations#

Hierarchy Depth#

  • O(1): Dict-style access (system['tag'])

  • O(n): Supersystem chain (system.supersystem_chain)

  • O(d): Navigation by path (d = depth)

For deep hierarchies, cache supersystem chain if accessed frequently.

Tag Changes#

Tag changes trigger observer which:

  1. Validates new tag (O(1) dict lookup)

  2. Updates parent dict (O(1) operations)

  3. No recursive updates

Impact is minimal for most use cases.

Serialization#

Saving a system serializes:

  • The system itself

  • All subsystems recursively

  • All SerializableProperty data

For large hierarchies:

  • Save can be slow (all subsystems written)

  • Load reconstructs entire tree

  • Consider splitting very large hierarchies

See Also#

External Resources#