Bigraph Schema¶

This tutorial explores the bigraph schema library. Bigraph schema enables different simulators, models, and data to communicate with each other through standardized type definitions and wiring diagrams. By providing a robust type system and serializable data format, this makes composite simulators shareable, extendable, and easily distributed across various computer architectures.

This notebook uses the bigraph-schema library for validation and processing of the schema, and the bigraph-viz library for plotting library for visualizing the resulting simulation configuration.

This notebook is focused on type definition and access. For an introduction on Bigraphs and Process Bigraphs see this notebook

Installation¶

To start, let's install the necessary libraries and display their versions:

In [1]:
# !pip install bigraph-schema --upgrade
# !pip install bigraph-viz --upgrade
# !pip install pint
!pip freeze | grep bigraph

Imports¶

In [2]:
from bigraph_schema import TypeSystem
from bigraph_viz import plot_bigraph, pf, pp
from bigraph_viz.dict_utils import schema_keys

Type System¶

A type system comprises a set of rules and constraints that govern the organization, interaction, and manipulation of bigraph nodes, facilitating efficient and error-free compositional modeling by guaranteeing compatibility and seamless communication between various components within a simulation.

The main class for working with bigraph_schema is TypeSystem, which handles the different type schemas and provides methods for their manipulation. We will use TypeSystem.validate_schema to check our schemas, TypeSystem.access to fill in missing details to prepare the schema for simulation, and TypeSystem.type_registry to access different schemas and register new ones.

Schemas serve as a formalized framework for defining and organizing types, providing explicit specifications for data representation, validation, and transformation. In a schema, types are characterized by specific schema_keys associated with type definitions, encompassing default values, serialization/deserialization methods, and additional metadata. The schema_keys encompass the following attributes. We will see specific instances of these below.

In [3]:
core = TypeSystem()

Type Registry¶

A type registry is used to store and manage schema definitions related to various simulation modules, streamlining the process of accessing, extending, and reusing type information across different components.

The names of types available in the given registry can be printed:

In [4]:
core.type_registry.list()
Out[4]:
['',
 'temperature',
 'length^4*mass/current*time^3',
 'mass/length*time^2',
 'luminosity',
 'length^0_5*mass^0_5',
 'length^3*mass/current^2*time^4',
 'length^2/time',
 'length*time/mass',
 'current*time/substance',
 'length^2*mass/current^2*time^2',
 'length^2',
 'length^2*mass/current*time^2',
 'current*length*time',
 'length^3/mass*time^2',
 'length^4*mass/time^3',
 'printing_unit/length',
 'length',
 'mass/temperature^4*time^3',
 'map',
 'schema',
 'boolean',
 'length^2*mass/time^2',
 'length/time^2',
 'union',
 'current*length^2',
 'length^2*mass/current^2*time^3',
 'length^2*mass/time^3',
 'mass/time^2',
 'mass/length*time',
 'current^2*time^3/length^2*mass',
 'list',
 'current',
 'length*temperature',
 'mass/time^3',
 'tree',
 'length/mass',
 'current*time^2/length^2*mass',
 'length^3',
 'substance/time',
 'mass/length^3',
 'wires',
 'length*mass/current^2*time^2',
 'int',
 'substance/length^3',
 'array',
 'any',
 'edge',
 'mass',
 '/length',
 'length^0_5*mass^0_5/time',
 '/printing_unit',
 'length*mass/time^2',
 'current*time',
 'maybe',
 'mass/length',
 'mass^0_5/length^1_5',
 'number',
 'length^2*mass/temperature*time^2',
 'luminosity/length^2',
 'length^2*mass/substance*temperature*time^2',
 'length^1_5*mass^0_5/time',
 'tuple',
 'time^2/length',
 'length/time',
 'length^1_5*mass^0_5/time^2',
 'substance',
 'length^2/time^2',
 'printing_unit',
 'current^2*time^4/length^2*mass',
 '/substance',
 'mass/current*time^2',
 'time',
 'length^2*mass/time',
 'float',
 'current^2*time^4/length^3*mass',
 'length*mass/current*time^3',
 '/temperature*time',
 'string',
 'current*time/mass',
 '/time',
 'time/length',
 'current*length^2*time',
 'length^2*mass/current*time^3',
 'length^3/time',
 'mass^0_5/length^0_5*time']

Accessing a type¶

Type schemas can be accessed from the registry by their name. Here the float types shows that it uses an accumulate apply method, a to_string serialize method, that its default value is 0.0, and that it inherits from (its super is) the number type.

In [5]:
float_schema = core.access('float')
float_schema
Out[5]:
{'_type': 'float',
 '_apply': 'bigraph_schema.type_system.accumulate',
 '_check': 'bigraph_schema.type_system.check_float',
 '_serialize': 'bigraph_schema.type_system.to_string',
 '_description': '64-bit floating point precision number',
 '_default': '0.0',
 '_deserialize': 'bigraph_schema.type_system.deserialize_float',
 '_divide': 'bigraph_schema.type_system.divide_float',
 '_super': ['number']}

Accessing schema methods¶

The apply, serialize, deserialize, divide methods of a type are accessed by the type system through their respective registries.

In [6]:
# here showing the method specified by the `float` type
apply_method = core.type_registry.apply_registry.access(float_schema['_apply'])
serialize_method = core.type_registry.serialize_registry.access(float_schema['_serialize'])
deserialize_method = core.type_registry.deserialize_registry.access(float_schema['_deserialize'])
divide_method = core.type_registry.divide_registry.access(float_schema['_divide'])
default_value = float_schema['_default']
deserialized_default_value = deserialize_method(default_value)

print(f'default: {default_value}')
print(f'after apply: {apply_method(deserialized_default_value, 1.0)}')
default: 0.0
after apply: 1.0

Use access to expand a composite type¶

Nested types have types within types. Here showing an edge type, which has wires that have their own types.

In [7]:
pp(core.access('edge'))
{ '_apply': 'bigraph_schema.type_system.apply_edge',
  '_check': 'bigraph_schema.type_system.check_edge',
  '_default': {'inputs': {}, 'outputs': {}},
  '_description': 'hyperedges in the bigraph, with ports as a type parameter',
  '_deserialize': 'bigraph_schema.type_system.deserialize_edge',
  '_divide': 'bigraph_schema.type_system.divide_edge',
  '_serialize': 'bigraph_schema.type_system.serialize_edge',
  '_type': 'edge',
  '_type_parameters': ['inputs', 'outputs'],
  'inputs': { '_apply': 'bigraph_schema.registry.apply_tree',
              '_bindings': { 'leaf': { '_apply': 'bigraph_schema.type_system.apply_list',
                                       '_bindings': {'element': 'string'},
                                       '_check': 'bigraph_schema.type_system.check_list',
                                       '_default': [],
                                       '_description': 'general list type (or '
                                                       'sublists)',
                                       '_deserialize': 'bigraph_schema.type_system.deserialize_list',
                                       '_divide': 'bigraph_schema.type_system.divide_list',
                                       '_element': { '_apply': 'bigraph_schema.type_system.replace',
                                                     '_check': 'bigraph_schema.type_system.check_string',
                                                     '_default': '',
                                                     '_description': '64-bit '
                                                                     'integer',
                                                     '_deserialize': 'bigraph_schema.type_system.deserialize_string',
                                                     '_serialize': 'bigraph_schema.type_system.serialize_string',
                                                     '_type': 'string'},
                                       '_serialize': 'bigraph_schema.type_system.serialize_list',
                                       '_type': 'list',
                                       '_type_parameters': ['element']}},
              '_check': 'bigraph_schema.type_system.check_tree',
              '_default': {},
              '_description': 'mapping from str to some type in a potentially '
                              'nested form',
              '_deserialize': 'bigraph_schema.type_system.deserialize_tree',
              '_divide': 'bigraph_schema.type_system.divide_tree',
              '_leaf': { '_apply': 'bigraph_schema.type_system.apply_list',
                         '_bindings': {'element': 'string'},
                         '_check': 'bigraph_schema.type_system.check_list',
                         '_default': [],
                         '_description': 'general list type (or sublists)',
                         '_deserialize': 'bigraph_schema.type_system.deserialize_list',
                         '_divide': 'bigraph_schema.type_system.divide_list',
                         '_element': { '_apply': 'bigraph_schema.type_system.replace',
                                       '_check': 'bigraph_schema.type_system.check_string',
                                       '_default': '',
                                       '_description': '64-bit integer',
                                       '_deserialize': 'bigraph_schema.type_system.deserialize_string',
                                       '_serialize': 'bigraph_schema.type_system.serialize_string',
                                       '_type': 'string'},
                         '_serialize': 'bigraph_schema.type_system.serialize_list',
                         '_type': 'list',
                         '_type_parameters': ['element']},
              '_serialize': 'bigraph_schema.type_system.serialize_tree',
              '_type': 'tree',
              '_type_parameters': ['leaf']},
  'outputs': { '_apply': 'bigraph_schema.registry.apply_tree',
               '_bindings': { 'leaf': { '_apply': 'bigraph_schema.type_system.apply_list',
                                        '_bindings': {'element': 'string'},
                                        '_check': 'bigraph_schema.type_system.check_list',
                                        '_default': [],
                                        '_description': 'general list type (or '
                                                        'sublists)',
                                        '_deserialize': 'bigraph_schema.type_system.deserialize_list',
                                        '_divide': 'bigraph_schema.type_system.divide_list',
                                        '_element': { '_apply': 'bigraph_schema.type_system.replace',
                                                      '_check': 'bigraph_schema.type_system.check_string',
                                                      '_default': '',
                                                      '_description': '64-bit '
                                                                      'integer',
                                                      '_deserialize': 'bigraph_schema.type_system.deserialize_string',
                                                      '_serialize': 'bigraph_schema.type_system.serialize_string',
                                                      '_type': 'string'},
                                        '_serialize': 'bigraph_schema.type_system.serialize_list',
                                        '_type': 'list',
                                        '_type_parameters': ['element']}},
               '_check': 'bigraph_schema.type_system.check_tree',
               '_default': {},
               '_description': 'mapping from str to some type in a potentially '
                               'nested form',
               '_deserialize': 'bigraph_schema.type_system.deserialize_tree',
               '_divide': 'bigraph_schema.type_system.divide_tree',
               '_leaf': { '_apply': 'bigraph_schema.type_system.apply_list',
                          '_bindings': {'element': 'string'},
                          '_check': 'bigraph_schema.type_system.check_list',
                          '_default': [],
                          '_description': 'general list type (or sublists)',
                          '_deserialize': 'bigraph_schema.type_system.deserialize_list',
                          '_divide': 'bigraph_schema.type_system.divide_list',
                          '_element': { '_apply': 'bigraph_schema.type_system.replace',
                                        '_check': 'bigraph_schema.type_system.check_string',
                                        '_default': '',
                                        '_description': '64-bit integer',
                                        '_deserialize': 'bigraph_schema.type_system.deserialize_string',
                                        '_serialize': 'bigraph_schema.type_system.serialize_string',
                                        '_type': 'string'},
                          '_serialize': 'bigraph_schema.type_system.serialize_list',
                          '_type': 'list',
                          '_type_parameters': ['element']},
               '_serialize': 'bigraph_schema.type_system.serialize_tree',
               '_type': 'tree',
               '_type_parameters': ['leaf']}}

Register a new type schema¶

You can add new types to the type registry using type_registry.register(type_name:str, type_schema:dict):

In [8]:
def apply_foursquare(current, update, bindings, core):
    if isinstance(current, bool) or isinstance(update, bool):
        return update
    else:
        for key, value in update.items():
            current[key] = apply_foursquare(
                current[key],
                value,
                bindings,
                core)

        return current

foursquare_schema = {
    '_apply': apply_foursquare,
    '00': 'boolean~foursquare',
    '01': 'boolean~foursquare',
    '10': 'boolean~foursquare',
    '11': 'boolean~foursquare'}

core.register(
    'foursquare',
    foursquare_schema, force=True)
In [9]:
import os
import matplotlib.pyplot as plt
import numpy as np

def render(data, arr, x, y, size):
    if isinstance(data, dict):
        half_size = size // 2
        if '00' in data:
            render(data['00'], arr, x, y + half_size, half_size)
        if '01' in data:
            render(data['01'], arr, x + half_size, y + half_size, half_size)
        if '10' in data:
            render(data['10'], arr, x, y, half_size)
        if '11' in data:
            render(data['11'], arr, x + half_size, y, half_size)
    else:
        arr[y:y+size, x:x+size] = 0 if data else 1  # Change to '0 if data else 1' to make True values black

def find_depth(data):
    if isinstance(data, dict):
        return 1 + max(find_depth(data[key]) for key in data)
    return 0

def plot_foursquare(data, out_dir='out', filename=None):
    depth = find_depth(data)
    size = 2 ** depth
    arr = np.ones((size, size))
    render(data, arr, 0, 0, size)
    plt.imshow(arr, cmap='gray', interpolation='nearest')
    plt.axis('off')
    if filename:
        os.makedirs(out_dir, exist_ok=True)
        fig_path = os.path.join(out_dir, filename)
        plt.savefig(fig_path, format='png', dpi=300)
    plt.show()
In [26]:
foursquare_schema = {
    '_apply': apply_foursquare,
    '00': 'boolean~foursquare',
    '01': 'boolean~foursquare',
    '10': 'boolean~foursquare',
    '11': 'boolean~foursquare'}

core.register(
    'foursquare',
    foursquare_schema)

example = {
    '00': True,
    '01': False,
    '10': False,
    '11': {
        '00': True,
        '01': False,
        '10': False,
        '11': {
            '00': True,
            '01': False,
            '10': False,
            '11': {
                '00': True,
                '01': False,
                '10': False,
                '11': {
                    '00': True,
                    '01': False,
                    '10': False,
                    '11': {
                        '00': True,
                        '01': False,
                        '10': False,
                        '11': False}}}}}}

assert core.check(
    'foursquare',
    example)

# plot
plot_foursquare(example, filename="example.png")
No description has been provided for this image
In [27]:
plot_bigraph(example, out_dir='out', filename="foursquarebigraph")
Writing out/foursquarebigraph
Out[27]:
No description has been provided for this image
In [40]:
update = {
    '00': False,
    '10': {
        '01': True,
        '10': {
            '01': True,
            '10': { 
                '01': True,
                '10': {
                    '01': True,
                    '10': {
                        '01': True,
                    }}}}}}

result = core.apply(
    'foursquare',
    example,
    update)

plot_foursquare(result, filename="example2.png")
No description has been provided for this image
In [14]:
# plot_bigraph(example)

Instance¶

An instances is the particular set of values that populate a bigraph structure, adhering to a given schema. An instance encapsulates the current conditions and predicts future evolution based on the governing equations or rules. As the system evolves over time, the state transitions through various bigraphical configurations.

In [15]:
bigraph_state = {
    'node1': 1.0,
    'node2': {
        'node2.1': 2.0,
        'node2.2': 2.1
    }
}
plot_bigraph(bigraph_state)
Out[15]:
No description has been provided for this image

Fill state¶

One of the key advantages of a type system is its ability to improve automation in compositional modeling. The fill function exemplifies this by automatically populating missing schema elements. Given a schema and an incomplete instance, the fill function intelligently identifies and fills the missing schema components, ensuring that the instance is compliant with the schema definition.

In [16]:
dual_edge_schema = {
    'edge1': {
        '_type': 'edge',
        '_ports': {
            'port1': 'float',
            'port2': 'int',
        },
    },
    'edge2': {
        '_type': 'edge',
        '_ports': {
            'port1': 'float',
            'port2': 'int',
        },
    },
}    

core.type_registry.register('dual_edge', dual_edge_schema, force=True)

schema = {
    'store1': 'dual_edge',
    'edge3': {
        '_type': 'edge',
        '_ports': {
            'port1': 'dual_edge'
        }
    }
}

instance = {
    'store1': {
        'edge1': {
            'wires': {
                'port1': 'store1.1',
                'port2': 'store1.2',
            }
        },
        'edge2': {
            'wires': {
                'port1': 'store1.1',
                'port2': 'store1.2',
            }
        }
    },
    'edge3': {
        'wires': {
            'port1': 'store1',
        }
    },
}
filled_instance = core.fill(schema, instance)
filled_schema = core.type_registry.access(schema)
In [17]:
print(f'schema:\n {pf(schema)}\n')
print(f'instance:\n {pf(instance)}\n')
# print(f'filled schema:\n {pf(filled_schema)}\n')
print(f'filled instance:\n {pf(filled_instance)}\n')
schema:
 { 'edge3': {'_ports': {'port1': 'dual_edge'}, '_type': 'edge'},
  'store1': 'dual_edge'}

instance:
 { 'edge3': {'inputs': {}, 'outputs': {}, 'wires': {'port1': 'store1'}},
  'store1': { 'edge1': { 'inputs': {},
                         'outputs': {},
                         'wires': {'port1': 'store1.1', 'port2': 'store1.2'}},
              'edge2': { 'inputs': {},
                         'outputs': {},
                         'wires': {'port1': 'store1.1', 'port2': 'store1.2'}}}}

filled instance:
 { 'edge3': {'inputs': {}, 'outputs': {}, 'wires': {'port1': 'store1'}},
  'store1': { 'edge1': { 'inputs': {},
                         'outputs': {},
                         'wires': {'port1': 'store1.1', 'port2': 'store1.2'}},
              'edge2': { 'inputs': {},
                         'outputs': {},
                         'wires': {'port1': 'store1.1', 'port2': 'store1.2'}}}}

In [18]:
plot_bigraph(filled_instance, remove_process_place_edges=True, port_labels=False)
Out[18]:
No description has been provided for this image

Update¶

In [19]:
place_schema = {
    '_type': 'tree[float]',
}

place_state1 = {
    'node1': 1.0,
    'node2': {
        'node2.1': 2.0,
        'node2.2': 2.1
    }
}
plot_bigraph(place_state1, show_values=True)
Out[19]:
No description has been provided for this image
In [21]:
update = {
    'node1': 5.1,
    'node2': {
        'node2.1': -5.0,
        # 'node2.2': -2.0
    }
}

place_state2 = core.apply(
    place_schema,
    place_state1,
    update
)
plot_bigraph(place_state2, show_values=True)
Out[21]:
No description has been provided for this image
In [1]:
# update = {
#     '_remove': ['node1'],
#     'node2': {
#         '_remove': ['node2.2'],
#         'node2.3': 0.5,
#         'node2.4': {'node2.2.1': -2.0}
#     }
# }
# update = {
#     '_remove': [
#         ['node1'], 
#         ['node2', 'node2.2'],
#     ],
# }

# place_state3 = core.apply(
#     place_schema,
#     place_state2,
#     update
# )
# plot_bigraph(place_state2, show_values=True)

View/Project through wires¶

In [ ]:
 

Query¶

Find a schema within a composite. For example find a mitochondria within a cell

Adapters¶

Show how adapters can automatically compose into a schema to provide translation between types.