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:
# !pip install bigraph-schema --upgrade
# !pip install bigraph-viz --upgrade
# !pip install pint
!pip freeze | grep bigraph
Imports¶
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.
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:
core.type_registry.list()
['', '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.
float_schema = core.access('float')
float_schema
{'_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.
# 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.
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)
:
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)
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()
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")
plot_bigraph(example, out_dir='out', filename="foursquarebigraph")
Writing out/foursquarebigraph
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")
# 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.
bigraph_state = {
'node1': 1.0,
'node2': {
'node2.1': 2.0,
'node2.2': 2.1
}
}
plot_bigraph(bigraph_state)
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.
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)
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'}}}}
plot_bigraph(filled_instance, remove_process_place_edges=True, port_labels=False)
Update¶
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)
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)
# 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¶