Add example of 'woods' operational semantics. Clearer error messages. Implement full OD-API. Small refactoring of Conformance class.

This commit is contained in:
Joeri Exelmans 2024-10-28 14:15:12 +01:00
parent f2dc299eab
commit cd26a401fe
7 changed files with 449 additions and 63 deletions

92
api/cd.py Normal file
View file

@ -0,0 +1,92 @@
from services.bottom.V0 import Bottom
from uuid import UUID
class CDAPI:
def __init__(self, state, m: UUID):
self.state = state
self.bottom = Bottom(state)
self.m = m
self.mm = UUID(state.read_value(state.read_dict(state.read_root(), "SCD")))
# pre-compute some things
# element -> name
self.type_model_names = {
self.bottom.read_outgoing_elements(self.m, e)[0]
: e for e in self.bottom.read_keys(self.m)
}
inh_type, = self.bottom.read_outgoing_elements(self.mm, "Inheritance")
inh_links = []
for tm_element, tm_name in self.type_model_names.items():
types = self.bottom.read_outgoing_elements(tm_element, "Morphism")
if inh_type in types:
inh_links.append(tm_element)
# for each inheritance link we add the parent and child to the sub types map
# name -> name
self.direct_sub_types = { type_name: set() for type_name in self.bottom.read_keys(self.m) } # empty initially
self.direct_super_types = { type_name: set() for type_name in self.bottom.read_keys(self.m) } # empty initially
for link in inh_links:
tm_source = self.bottom.read_edge_source(link)
tm_target = self.bottom.read_edge_target(link)
parent_name = self.type_model_names[tm_target]
child_name = self.type_model_names[tm_source]
self.direct_sub_types[parent_name].add(child_name)
self.direct_super_types[child_name].add(parent_name)
def get_transitive_sub_types(type_name: str):
# includes the type itself - reason: if we want to get all the instances of some type and its subtypes, we don't have to consider the type itself as an extra case
return [type_name, *(sub_type for child_name in self.direct_sub_types.get(type_name, set()) for sub_type in get_transitive_sub_types(child_name) )]
def get_transitive_super_types(type_name: str):
# includes the type itself - reason: if we want to check if something is an instance of a type, we check if its type or one of its super types is equal to the type we're looking for, without having to consider the instance's type itself as an extra case
return [type_name, *(super_type for parent_name in self.direct_super_types.get(type_name, set()) for super_type in get_transitive_super_types(parent_name))]
self.transitive_sub_types = { type_name: set(get_transitive_sub_types(type_name)) for type_name in self.direct_sub_types }
self.transitive_super_types = { type_name: set(get_transitive_super_types(type_name)) for type_name in self.direct_super_types }
def get_type(type_name: str):
return self.bottom.read_outgoing_elements(self.m, type_name)[0]
def is_direct_subtype(super_type_name: str, sub_type_name: str):
return sub_type_name in self.direct_sub_types[super_type]
def is_direct_supertype(sub_type_name: str, super_type_name: str):
return super_type_name in self.direct_super_types[sub_type_name]
def is_subtype(super_type_name: str, sub_type_name: str):
return sub_type_name in self.transitive_sub_types[super_type]
def is_supertype(sub_type_name: str, super_type_name: str):
return super_type_name in self.transitive_super_types[sub_type_name]
# # The edge connecting an object to the value of a slot must be named `{object_name}_{attr_name}`
# def get_attr_link_name(self, class_name, attr_name):
# assoc_name = f"{class_name}_{attr_name}"
# type_edges = self.bottom.read_outgoing_elements(self.m, assoc_name)
# if len(type_edges) == 1:
# return assoc_name
# else:
# # look for attribute in the super-types
# conf = Conformance(self.bottom.state, self.model, self.type_model)
# conf.precompute_sub_types() # only need to know about subtypes
# super_types = [s for s in conf.sub_types if class_name in conf.sub_types[s]]
# for s in super_types:
# assoc_name = f"{s}_{attr_name}"
# if len(self.bottom.read_outgoing_elements(self.type_model, assoc_name)) == 1:
# return assoc_name
# Attributes are inherited, so when we instantiate an attribute of a class, the AttributeLink may contain the name of the superclass
def find_attribute_type(self, class_name: str, attr_name: str):
assoc_name = f"{class_name}_{attr_name}"
type_edges = self.bottom.read_outgoing_elements(self.m, assoc_name)
if len(type_edges) == 1:
return type_edges[0]
else:
for supertype in self.direct_super_types[class_name]:
result = self.find_attribute_type(supertype, attr_name)
if result != None:
return result

148
api/od.py Normal file
View file

@ -0,0 +1,148 @@
from services import od
from api import cd
from services.bottom.V0 import Bottom
from uuid import UUID
from typing import Optional
NEXT_ID = 0
# Models map names to elements
# This builds the inverse mapping, so we can quickly lookup the name of an element
def build_name_mapping(state, m):
mapping = {}
bottom = Bottom(state)
for name in bottom.read_keys(m):
element, = bottom.read_outgoing_elements(m, name)
mapping[element] = name
return mapping
# Object Diagram API
# Intended to replace the 'services.od.OD' class eventually
class ODAPI:
def __init__(self, state, m: UUID, mm: UUID):
self.state = state
self.bottom = Bottom(state)
self.m = m
self.mm = mm
self.od = od.OD(mm, m, state)
self.cd = cd.CDAPI(state, mm)
self.create_boolean_value = self.od.create_boolean_value
self.create_integer_value = self.od.create_integer_value
self.create_string_value = self.od.create_string_value
self.create_actioncode_value = self.od.create_actioncode_value
self.__recompute_mappings()
# Called after every change - makes querying faster but modifying slower
def __recompute_mappings(self):
self.obj_to_name = {**build_name_mapping(self.state, self.m), **build_name_mapping(self.state, self.mm)}
# self.obj_to_type = {}
self.type_to_objs = { type_name : set() for type_name in self.bottom.read_keys(self.mm)}
for m_name in self.bottom.read_keys(self.m):
m_element, = self.bottom.read_outgoing_elements(self.m, m_name)
tm_element = self.get_type(m_element)
tm_name = self.obj_to_name[tm_element]
# self.obj_to_type[m_name] = tm_name
self.type_to_objs[tm_name].add(m_name)
def get_value(self, obj: UUID):
return od.read_primitive_value(self.bottom, obj, self.mm)[0]
def get_target(self, link: UUID):
return self.bottom.read_edge_target(link)
def get_source(self, link: UUID):
return self.bottom.read_edge_source(link)
def get_slot(self, obj: UUID, attr_name: str):
return self.od.get_slot(obj, attr_name)
def get_slot_link(self, obj: UUID, attr_name: str):
return self.od.get_slot_link(obj, attr_name)
def get_outgoing(self, obj: UUID, assoc_name: str):
return od.find_outgoing_typed_by(self.bottom, src=obj, type_node=self.bottom.read_outgoing_elements(self.mm, assoc_name)[0])
def get_incoming(self, obj: UUID, assoc_name: str):
return od.find_incoming_typed_by(self.bottom, tgt=obj, type_node=self.bottom.read_outgoing_elements(self.mm, assoc_name)[0])
def get_all_instances(self, type_name: str, include_subtypes=True):
obj_names = self.type_to_objs[type_name]
return [(obj_name, self.bottom.read_outgoing_elements(self.m, obj_name)[0]) for obj_name in obj_names]
def get_type(self, obj: UUID):
types = self.bottom.read_outgoing_elements(obj, "Morphism")
if len(types) != 1:
raise Exception(f"Expected obj to have 1 type, instead got {len(types)} types.")
return types[0]
def get_name(self, obj: UUID):
return (
[name for name in self.bottom.read_keys(self.m) if self.bottom.read_outgoing_elements(self.m, name)[0] == obj] +
[name for name in self.bottom.read_keys(self.mm) if self.bottom.read_outgoing_elements(self.mm, name)[0] == obj]
)[0]
return self.obj_to_name[obj]
def get(self, name: str):
return self.bottom.read_outgoing_elements(self.m, name)[0]
def get_type_name(self, obj: UUID):
return self.get_name(self.get_type(obj))
def is_instance(obj: UUID, type_name: str, include_subtypes=True):
typ = self.cd.get_type(type_name)
types = set(typ) if not include_subtypes else self.cd.transitive_subtypes[type_name]
for type_of_obj in self.bottom.read_outgoing_elements(obj, "Morphism"):
if type_of_obj in types:
return True
return False
def delete(self, obj: UUID):
self.bottom.delete_element(obj)
self.__recompute_mappings()
def get_slot_value(self, obj: UUID, attr_name: str):
return self.get_value(self.get_slot(obj, attr_name))
def set_slot_value(self, obj: UUID, attr_name: str, new_value: any):
obj_name = self.get_name(obj)
link_name = f"{obj_name}_{attr_name}"
target_name = f"{obj_name}.{attr_name}"
old_slot_link = self.get_slot_link(obj, attr_name)
if old_slot_link != None:
old_target = self.get_target(old_slot_link)
# if old_target != None:
self.bottom.delete_element(old_target) # this also deletes the slot-link
new_target = self.create_primitive_value(target_name, new_value)
slot_type = self.cd.find_attribute_type(self.get_type_name(obj), attr_name)
new_link = self.od._create_link(link_name, slot_type, obj, new_target)
self.__recompute_mappings()
def create_primitive_value(self, name: str, value: any, is_code=False):
if isinstance(value, bool):
tgt = self.create_boolean_value(name, value)
elif isinstance(value, int):
tgt = self.create_integer_value(name, value)
elif isinstance(value, str):
if is_code:
tgt = self.create_actioncode_value(name, value)
else:
tgt = self.create_string_value(name, value)
else:
raise Exception("Unimplemented type "+value)
self.__recompute_mappings()
return tgt
def create_link(self, link_name: Optional[str], assoc_name: str, src: UUID, tgt: UUID):
global NEXT_ID
typ, = self.bottom.read_outgoing_elements(self.mm, assoc_name)
if link_name == None:
link_name = f"__{assoc_name}{NEXT_ID}"
NEXT_ID += 1
link_id = self.od._create_link(link_name, typ, src, tgt)
self.__recompute_mappings()
return link_id