Conformance checker relies much more on ODAPI rather than duplicating its logic + Fix error in bootstrap of primitive types

This commit is contained in:
Joeri Exelmans 2024-11-05 16:54:00 +01:00
parent c92d5be284
commit a26ceef10f
5 changed files with 187 additions and 268 deletions

View file

@ -49,7 +49,7 @@ class CDAPI:
self.transitive_sub_types = { type_name: set(get_transitive_sub_types(type_name)) for type_name in self.direct_sub_types } 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 } self.transitive_super_types = { type_name: set(get_transitive_super_types(type_name)) for type_name in self.direct_super_types }
def get_type(self, type_name: str): def get(self, type_name: str):
return self.bottom.read_outgoing_elements(self.m, type_name)[0] return self.bottom.read_outgoing_elements(self.m, type_name)[0]
def is_direct_subtype(self, super_type_name: str, sub_type_name: str): def is_direct_subtype(self, super_type_name: str, sub_type_name: str):
@ -68,7 +68,9 @@ class CDAPI:
# # The edge connecting an object to the value of a slot must be named `{object_name}_{attr_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): def get_attr_link_name(self, class_name, attr_name):
return self.type_model_names[self.find_attribute_type(class_name, attr_name)] attr_type_link = self.find_attribute_type(class_name, attr_name)
if attr_type_link != None:
return self.type_model_names[attr_type_link]
# Attributes are inherited, so when we instantiate an attribute of a class, the AttributeLink may contain the name of the superclass # 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): def find_attribute_type(self, class_name: str, attr_name: str):

View file

@ -16,6 +16,9 @@ def build_name_mapping(state, m):
mapping[element] = name mapping[element] = name
return mapping return mapping
class NoSuchSlotException(Exception):
pass
# Object Diagram API # Object Diagram API
# Intended to replace the 'services.od.OD' class eventually # Intended to replace the 'services.od.OD' class eventually
class ODAPI: class ODAPI:
@ -25,7 +28,7 @@ class ODAPI:
self.m = m self.m = m
self.mm = mm self.mm = mm
self.od = od.OD(mm, m, state) self.od = od.OD(mm, m, state)
self.cd = cd.CDAPI(state, mm) self.cdapi = cd.CDAPI(state, mm)
self.create_boolean_value = self.od.create_boolean_value self.create_boolean_value = self.od.create_boolean_value
self.create_integer_value = self.od.create_integer_value self.create_integer_value = self.od.create_integer_value
@ -36,13 +39,14 @@ class ODAPI:
# Called after every change - makes querying faster but modifying slower # Called after every change - makes querying faster but modifying slower
def __recompute_mappings(self): def __recompute_mappings(self):
self.obj_to_name = {**build_name_mapping(self.state, self.m), **build_name_mapping(self.state, self.mm)} self.m_obj_to_name = build_name_mapping(self.state, self.m)
# self.obj_to_type = {} self.mm_obj_to_name = build_name_mapping(self.state, self.mm)
self.type_to_objs = { type_name : set() for type_name in self.bottom.read_keys(self.mm)} 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): for m_name in self.bottom.read_keys(self.m):
m_element, = self.bottom.read_outgoing_elements(self.m, m_name) m_element, = self.bottom.read_outgoing_elements(self.m, m_name)
tm_element = self.get_type(m_element) tm_element = self.get_type(m_element)
tm_name = self.obj_to_name[tm_element] if tm_element in self.mm_obj_to_name:
tm_name = self.mm_obj_to_name[tm_element]
# self.obj_to_type[m_name] = tm_name # self.obj_to_type[m_name] = tm_name
self.type_to_objs[tm_name].add(m_name) self.type_to_objs[tm_name].add(m_name)
@ -58,21 +62,44 @@ class ODAPI:
def get_slot(self, obj: UUID, attr_name: str): def get_slot(self, obj: UUID, attr_name: str):
slot = self.od.get_slot(obj, attr_name) slot = self.od.get_slot(obj, attr_name)
if slot == None: if slot == None:
raise Exception(f"Object '{self.obj_to_name[obj]}' has no slot '{attr_name}'") raise NoSuchSlotException(f"Object '{self.m_obj_to_name[obj]}' has no slot '{attr_name}'")
return slot return slot
def get_slot_link(self, obj: UUID, attr_name: str): def get_slot_link(self, obj: UUID, attr_name: str):
return self.od.get_slot_link(obj, attr_name) return self.od.get_slot_link(obj, attr_name)
def get_outgoing(self, obj: UUID, assoc_name: str): # Parameter 'include_subtypes': whether to include subtypes of the given association
return od.find_outgoing_typed_by(self.bottom, src=obj, type_node=self.bottom.read_outgoing_elements(self.mm, assoc_name)[0]) def get_outgoing(self, obj: UUID, assoc_name: str, include_subtypes=True):
outgoing = self.bottom.read_outgoing_edges(obj)
result = []
for o in outgoing:
try:
type_of_outgoing_link = self.get_type_name(o)
except:
continue # OK, not all edges are typed
if (include_subtypes and self.cdapi.is_subtype(super_type_name=assoc_name, sub_type_name=type_of_outgoing_link)
or not include_subtypes and type_of_outgoing_link == assoc_name):
result.append(o)
return result
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]) # Parameter 'include_subtypes': whether to include subtypes of the given association
def get_incoming(self, obj: UUID, assoc_name: str, include_subtypes=True):
incoming = self.bottom.read_incoming_edges(obj)
result = []
for i in incoming:
try:
type_of_incoming_link = self.get_type_name(i)
except:
continue # OK, not all edges are typed
if (include_subtypes and self.cdapi.is_subtype(super_type_name=assoc_name, sub_type_name=type_of_incoming_link)
or not include_subtypes and type_of_incoming_link == assoc_name):
result.append(i)
return result
def get_all_instances(self, type_name: str, include_subtypes=True): def get_all_instances(self, type_name: str, include_subtypes=True):
if include_subtypes: if include_subtypes:
all_types = self.cd.transitive_sub_types[type_name] all_types = self.cdapi.transitive_sub_types[type_name]
else: else:
all_types = set([type_name]) all_types = set([type_name])
obj_names = [obj_name for type_name in all_types for obj_name in self.type_to_objs[type_name]] obj_names = [obj_name for type_name in all_types for obj_name in self.type_to_objs[type_name]]
@ -89,7 +116,6 @@ class ODAPI:
[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.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] [name for name in self.bottom.read_keys(self.mm) if self.bottom.read_outgoing_elements(self.mm, name)[0] == obj]
)[0] )[0]
return self.obj_to_name[obj]
def get(self, name: str): def get(self, name: str):
return self.bottom.read_outgoing_elements(self.m, name)[0] return self.bottom.read_outgoing_elements(self.m, name)[0]
@ -98,8 +124,8 @@ class ODAPI:
return self.get_name(self.get_type(obj)) return self.get_name(self.get_type(obj))
def is_instance(obj: UUID, type_name: str, include_subtypes=True): def is_instance(obj: UUID, type_name: str, include_subtypes=True):
typ = self.cd.get_type(type_name) typ = self.cdapi.get_type(type_name)
types = set(typ) if not include_subtypes else self.cd.transitive_sub_types[type_name] types = set(typ) if not include_subtypes else self.cdapi.transitive_sub_types[type_name]
for type_of_obj in self.bottom.read_outgoing_elements(obj, "Morphism"): for type_of_obj in self.bottom.read_outgoing_elements(obj, "Morphism"):
if type_of_obj in types: if type_of_obj in types:
return True return True
@ -109,12 +135,20 @@ class ODAPI:
self.bottom.delete_element(obj) self.bottom.delete_element(obj)
self.__recompute_mappings() self.__recompute_mappings()
# Does the class of the object have the given attribute?
def has_slot(self, obj: UUID, attr_name: str): def has_slot(self, obj: UUID, attr_name: str):
class_name = self.get_name(self.get_type(obj)) class_name = self.get_name(self.get_type(obj))
return self.od.get_attr_link_name(class_name, attr_name) != None return self.od.get_attr_link_name(class_name, attr_name) != None
def get_slot_value(self, obj: UUID, attr_name: str): def get_slot_value(self, obj: UUID, attr_name: str):
return self.get_value(self.get_slot(obj, attr_name)) slot = self.get_slot(obj, attr_name)
return self.get_value(slot)
def get_slot_value_default(self, obj: UUID, attr_name: str, default: any):
try:
return self.get_slot_value(obj, attr_name)
except NoSuchSlotException:
return default
# create or update slot value # create or update slot value
def set_slot_value(self, obj: UUID, attr_name: str, new_value: any, is_code=False): def set_slot_value(self, obj: UUID, attr_name: str, new_value: any, is_code=False):
@ -130,7 +164,7 @@ class ODAPI:
self.bottom.delete_element(old_target) # this also deletes the slot-link self.bottom.delete_element(old_target) # this also deletes the slot-link
new_target = self.create_primitive_value(target_name, new_value, is_code) new_target = self.create_primitive_value(target_name, new_value, is_code)
slot_type = self.cd.find_attribute_type(self.get_type_name(obj), attr_name) slot_type = self.cdapi.find_attribute_type(self.get_type_name(obj), attr_name)
new_link = self.od._create_link(link_name, slot_type, obj, new_target) new_link = self.od._create_link(link_name, slot_type, obj, new_target)
self.__recompute_mappings() self.__recompute_mappings()

View file

@ -4,36 +4,35 @@ from services.primitives.integer_type import Integer
from services.primitives.actioncode_type import ActionCode from services.primitives.actioncode_type import ActionCode
def bootstrap_type(type_name: str, scd_root: UUID, model_root: UUID, integer_type: UUID, state: State): def bootstrap_type(type_name: str, scd_root: UUID, model_root: UUID, state: State):
bottom = Bottom(state) bottom = Bottom(state)
# create class # create class
class_node = bottom.create_node() # create class node type_class_node = bottom.create_node() # create class node
bottom.create_edge(model_root, class_node, type_name) # attach to model bottom.create_edge(model_root, type_class_node, type_name) # attach to model
scd_node, = bottom.read_outgoing_elements(scd_root, "Class") # retrieve type class_class_node, = bottom.read_outgoing_elements(scd_root, "Class") # retrieve type
bottom.create_edge(class_node, scd_node, "Morphism") # create morphism link bottom.create_edge(type_class_node, class_class_node, "Morphism") # create morphism link
scd_int_node, = bottom.read_outgoing_elements(scd_root, "Integer")
# set min_cardinality # set min_cardinality
min_c_model = bottom.create_node() min_c_model = bottom.create_node()
Integer(min_c_model, state).create(1) Integer(min_c_model, state).create(1)
min_c_node = bottom.create_node(str(min_c_model)) min_c_node = bottom.create_node(str(min_c_model))
bottom.create_edge(model_root, min_c_node, f"{type_name}.lower_cardinality") bottom.create_edge(model_root, min_c_node, f"{type_name}.lower_cardinality")
min_c_link = bottom.create_edge(class_node, min_c_node) min_c_link = bottom.create_edge(type_class_node, min_c_node)
bottom.create_edge(model_root, min_c_link, f"{type_name}_lower_cardinality") bottom.create_edge(model_root, min_c_link, f"{type_name}_lower_cardinality")
scd_node = integer_type
scd_link, = bottom.read_outgoing_elements(scd_root, "Class_lower_cardinality") scd_link, = bottom.read_outgoing_elements(scd_root, "Class_lower_cardinality")
bottom.create_edge(min_c_node, scd_node, "Morphism") bottom.create_edge(min_c_node, scd_int_node, "Morphism")
bottom.create_edge(min_c_link, scd_link, "Morphism") bottom.create_edge(min_c_link, scd_link, "Morphism")
# set max_cardinality # set max_cardinality
max_c_model = bottom.create_node() max_c_model = bottom.create_node()
Integer(max_c_model, state).create(1) Integer(max_c_model, state).create(1)
max_c_node = bottom.create_node(str(max_c_model)) max_c_node = bottom.create_node(str(max_c_model))
bottom.create_edge(model_root, max_c_node, f"{type_name}.upper_cardinality") bottom.create_edge(model_root, max_c_node, f"{type_name}.upper_cardinality")
max_c_link = bottom.create_edge(class_node, max_c_node) max_c_link = bottom.create_edge(type_class_node, max_c_node)
bottom.create_edge(model_root, max_c_link, f"{type_name}_upper_cardinality") bottom.create_edge(model_root, max_c_link, f"{type_name}_upper_cardinality")
scd_node = integer_type
scd_link, = bottom.read_outgoing_elements(scd_root, "Class_upper_cardinality") scd_link, = bottom.read_outgoing_elements(scd_root, "Class_upper_cardinality")
bottom.create_edge(max_c_node, scd_node, "Morphism") bottom.create_edge(max_c_node, scd_int_node, "Morphism")
bottom.create_edge(max_c_link, scd_link, "Morphism") bottom.create_edge(max_c_link, scd_link, "Morphism")
return class_node return type_class_node
def bootstrap_constraint(class_node, type_name: str, python_type: str, scd_root: UUID, model_root: UUID, actioncode_type: UUID, state: State): def bootstrap_constraint(class_node, type_name: str, python_type: str, scd_root: UUID, model_root: UUID, actioncode_type: UUID, state: State):
bottom = Bottom(state) bottom = Bottom(state)
@ -50,12 +49,12 @@ def bootstrap_constraint(class_node, type_name: str, python_type: str, scd_root:
def bootstrap_primitive_types(scd_root, state, integer_type, boolean_type, float_type, string_type, type_type, actioncode_type): def bootstrap_primitive_types(scd_root, state, integer_type, boolean_type, float_type, string_type, type_type, actioncode_type):
# Order is important: Integer must come first # Order is important: Integer must come first
class_integer = bootstrap_type("Integer", scd_root, integer_type, integer_type, state) class_integer = bootstrap_type("Integer", scd_root, integer_type, state)
class_type = bootstrap_type("Type", scd_root, type_type, integer_type, state) class_type = bootstrap_type("Type", scd_root, type_type, state)
class_boolean = bootstrap_type("Boolean", scd_root, boolean_type, integer_type, state) class_boolean = bootstrap_type("Boolean", scd_root, boolean_type, state)
class_float = bootstrap_type("Float", scd_root, float_type, integer_type, state) class_float = bootstrap_type("Float", scd_root, float_type, state)
class_string = bootstrap_type("String", scd_root, string_type, integer_type, state) class_string = bootstrap_type("String", scd_root, string_type, state)
class_actioncode = bootstrap_type("ActionCode", scd_root, actioncode_type, integer_type, state) class_actioncode = bootstrap_type("ActionCode", scd_root, actioncode_type, state)
# Can only create constraints after ActionCode type has been created: # Can only create constraints after ActionCode type has been created:
bootstrap_constraint(class_integer, "Integer", "int", scd_root, integer_type, actioncode_type, state) bootstrap_constraint(class_integer, "Integer", "int", scd_root, integer_type, actioncode_type, state)
@ -64,4 +63,3 @@ def bootstrap_primitive_types(scd_root, state, integer_type, boolean_type, float
bootstrap_constraint(class_float, "Float", "float", scd_root, float_type, actioncode_type, state) bootstrap_constraint(class_float, "Float", "float", scd_root, float_type, actioncode_type, state)
bootstrap_constraint(class_string, "String", "str", scd_root, string_type, actioncode_type, state) bootstrap_constraint(class_string, "String", "str", scd_root, string_type, actioncode_type, state)
bootstrap_constraint(class_actioncode, "ActionCode", "str", scd_root, actioncode_type, actioncode_type, state) bootstrap_constraint(class_actioncode, "ActionCode", "str", scd_root, actioncode_type, actioncode_type, state)

View file

@ -32,36 +32,33 @@ def render_conformance_check_result(error_list):
class Conformance: class Conformance:
# Parameter 'constraint_check_subtypes': whether to check local type-level constraints also on subtypes.
def __init__(self, state: State, model: UUID, type_model: UUID, constraint_check_subtypes=True): def __init__(self, state: State, model: UUID, type_model: UUID, constraint_check_subtypes=True):
self.state = state self.state = state
self.bottom = Bottom(state) self.bottom = Bottom(state)
type_model_id = state.read_dict(state.read_root(), "SCD")
self.scd_model = UUID(state.read_value(type_model_id))
self.model = model self.model = model
self.type_model = type_model self.type_model = type_model
self.constraint_check_subtypes = constraint_check_subtypes # for a class-level constraint, also check the constraint on the subtypes of that class? In other words, are constraints inherited. self.constraint_check_subtypes = constraint_check_subtypes
self.type_mapping: Dict[str, str] = {}
self.model_names = { # MCL
# map model elements to their names to prevent iterating too much type_model_id = state.read_dict(state.read_root(), "SCD")
self.bottom.read_outgoing_elements(self.model, e)[0]: e self.scd_model = UUID(state.read_value(type_model_id))
for e in self.bottom.read_keys(self.model)
} # Helpers
self.type_model_names = { self.cdapi = CDAPI(state, type_model)
# map type model elements to their names to prevent iterating too much self.odapi = ODAPI(state, model, type_model)
self.bottom.read_outgoing_elements(self.type_model, e)[0] self.type_odapi = ODAPI(state, type_model, self.scd_model)
: e for e in self.bottom.read_keys(self.type_model)
} # Pre-computed:
self.primitive_values: Dict[UUID, Any] = {}
self.abstract_types: List[str] = [] self.abstract_types: List[str] = []
self.multiplicities: Dict[str, Tuple] = {} self.multiplicities: Dict[str, Tuple] = {}
self.source_multiplicities: Dict[str, Tuple] = {} self.source_multiplicities: Dict[str, Tuple] = {}
self.target_multiplicities: Dict[str, Tuple] = {} self.target_multiplicities: Dict[str, Tuple] = {}
# ?
self.structures = {} self.structures = {}
self.matches = {}
self.candidates = {} self.candidates = {}
self.cdapi = CDAPI(state, type_model)
self.odapi = ODAPI(state, model, type_model)
def check_nominal(self, *, log=False): def check_nominal(self, *, log=False):
""" """
@ -103,120 +100,38 @@ class Conformance:
# print(e) # print(e)
# return False # return False
def read_attribute(self, element: UUID, attr_name: str):
"""
Read an attribute value attached to an element
Args:
element: UUID of the element
attr_name: name of the attribute to read
Returns:
The value of hte attribute, if no attribute with given name is found, returns None
"""
if element in self.type_model_names:
# type model element
element_name = self.type_model_names[element]
model = self.type_model
else:
# model element
element_name = self.model_names[element]
model = self.model
try:
attr_elem, = self.bottom.read_outgoing_elements(model, f"{element_name}.{attr_name}")
return self.primitive_values.get(attr_elem, self.bottom.read_value(UUID(self.bottom.read_value(attr_elem))))
except ValueError:
return None
def deref_primitive_values(self):
"""
Prefetch the values stored in referenced primitive type models
"""
ref_element, = self.bottom.read_outgoing_elements(self.scd_model, "ModelRef")
string_element, = self.bottom.read_outgoing_elements(self.scd_model, "String")
boolean_element, = self.bottom.read_outgoing_elements(self.scd_model, "Boolean")
integer_element, = self.bottom.read_outgoing_elements(self.scd_model, "Integer")
t_deref = []
t_refs = []
for tm_element, tm_name in self.type_model_names.items():
morphisms = self.bottom.read_outgoing_elements(tm_element, "Morphism")
if ref_element in morphisms:
t_refs.append(self.type_model_names[tm_element])
elif string_element in morphisms:
t_deref.append(tm_element)
elif boolean_element in morphisms:
t_deref.append(tm_element)
elif integer_element in morphisms:
t_deref.append(tm_element)
for elem in t_deref:
primitive_model = UUID(self.bottom.read_value(elem))
primitive_value_node, = self.bottom.read_outgoing_elements(primitive_model)
primitive_value = self.bottom.read_value(primitive_value_node)
self.primitive_values[elem] = primitive_value
for m_name, tm_name in self.type_mapping.items():
if tm_name in t_refs:
# dereference
m_element, = self.bottom.read_outgoing_elements(self.model, m_name)
primitive_model = UUID(self.bottom.read_value(m_element))
try:
primitive_value_node, = self.bottom.read_outgoing_elements(primitive_model)
primitive_value = self.bottom.read_value(primitive_value_node)
self.primitive_values[m_element] = primitive_value
except ValueError:
pass # multiple elements in model indicate that we're not dealing with a primitive
def precompute_multiplicities(self): def precompute_multiplicities(self):
""" """
Creates an internal representation of type multiplicities that is Creates an internal representation of type multiplicities that is
more easily queryable that the state graph more easily queryable that the state graph
""" """
for tm_element, tm_name in self.type_model_names.items(): for clss_name, clss in self.type_odapi.get_all_instances("Class"):
# class abstract flags and multiplicities abstract = self.type_odapi.get_slot_value_default(clss, "abstract", default=False)
abstract = self.read_attribute(tm_element, "abstract")
lc = self.read_attribute(tm_element, "lower_cardinality")
uc = self.read_attribute(tm_element, "upper_cardinality")
if abstract: if abstract:
self.abstract_types.append(tm_name) self.abstract_types.append(clss_name)
if lc or uc:
mult = (
lc if lc != None else float("-inf"),
uc if uc != None else float("inf")
)
self.multiplicities[tm_name] = mult
# multiplicities for associations
slc = self.read_attribute(tm_element, "source_lower_cardinality")
suc = self.read_attribute(tm_element, "source_upper_cardinality")
if slc or suc:
mult = (
# slc if slc != None else float("-inf"),
slc if slc != None else 0,
suc if suc != None else float("inf")
)
self.source_multiplicities[tm_name] = mult
tlc = self.read_attribute(tm_element, "target_lower_cardinality")
tuc = self.read_attribute(tm_element, "target_upper_cardinality")
if tlc or tuc:
mult = (
# tlc if tlc != None else float("-inf"),
tlc if tlc != None else 0,
tuc if tuc != None else float("inf")
)
self.target_multiplicities[tm_name] = mult
# optional for attribute links
opt = self.read_attribute(tm_element, "optional")
if opt != None:
self.source_multiplicities[tm_name] = (0, float('inf'))
self.target_multiplicities[tm_name] = (0 if opt else 1, 1)
def get_type(self, element: UUID): lc = self.type_odapi.get_slot_value_default(clss, "lower_cardinality", default=0)
""" uc = self.type_odapi.get_slot_value_default(clss, "upper_cardinality", default=float('inf'))
Retrieve the type of an element (wrt. current type model) if lc or uc:
""" self.multiplicities[clss_name] = (lc, uc)
morphisms = self.bottom.read_outgoing_elements(element, "Morphism")
tm_element, = [m for m in morphisms if m in self.type_model_names.keys()] for assoc_name, assoc in self.type_odapi.get_all_instances("Association"):
return tm_element # multiplicities for associations
slc = self.type_odapi.get_slot_value_default(assoc, "source_lower_cardinality", default=0)
suc = self.type_odapi.get_slot_value_default(assoc, "source_upper_cardinality", default=float('inf'))
if slc or suc:
self.source_multiplicities[assoc_name] = (slc, suc)
tlc = self.type_odapi.get_slot_value_default(assoc, "target_lower_cardinality", default=0)
tuc = self.type_odapi.get_slot_value_default(assoc, "target_upper_cardinality", default=float('inf'))
if tlc or tuc:
self.target_multiplicities[assoc_name] = (tlc, tuc)
for attr_name, attr in self.type_odapi.get_all_instances("AttributeLink"):
# optional for attribute links
opt = self.type_odapi.get_slot_value(attr, "optional")
if opt != None:
self.source_multiplicities[attr_name] = (0, float('inf'))
self.target_multiplicities[attr_name] = (0 if opt else 1, 1)
def check_typing(self): def check_typing(self):
""" """
@ -224,24 +139,15 @@ class Conformance:
link exists to some element of type_model link exists to some element of type_model
""" """
errors = [] errors = []
ref_element, = self.bottom.read_outgoing_elements(self.scd_model, "ModelRef")
model_names = self.bottom.read_keys(self.model) # Recursively do a conformance check for each ModelRef
for m_name in model_names: for ref_name, ref in self.type_odapi.get_all_instances("ModelRef"):
m_element, = self.bottom.read_outgoing_elements(self.model, m_name) sub_mm = UUID(self.bottom.read_value(ref))
try: for ref_inst_name, ref_inst in self.odapi.get_all_instances(ref_name):
tm_element = self.get_type(m_element) sub_m = UUID(self.bottom.read_value(ref_inst))
tm_name = self.type_model_names[tm_element] nested_errors = Conformance(self.state, sub_m, sub_mm).check_nominal()
self.type_mapping[m_name] = tm_name
if ref_element in self.bottom.read_outgoing_elements(tm_element, "Morphism"):
sub_m = UUID(self.bottom.read_value(m_element))
sub_tm = UUID(self.bottom.read_value(tm_element))
nested_errors = Conformance(self.state, sub_m, sub_tm).check_nominal()
errors += [f"In ModelRef ({m_name}):" + err for err in nested_errors] errors += [f"In ModelRef ({m_name}):" + err for err in nested_errors]
except ValueError as e:
import traceback
traceback.format_exc(e)
# no or too many morphism links found
errors.append(f"Incorrectly typed element: '{m_name}'")
return errors return errors
def check_link_typing(self): def check_link_typing(self):
@ -249,97 +155,76 @@ class Conformance:
for each link, check whether its source and target are of a valid type for each link, check whether its source and target are of a valid type
""" """
errors = [] errors = []
for m_name, tm_name in self.type_mapping.items(): for tm_name, tm_element in self.type_odapi.get_all_instances("Association") + self.type_odapi.get_all_instances("AttributeLink"):
m_element, = self.bottom.read_outgoing_elements(self.model, m_name) for m_name, m_element in self.odapi.get_all_instances(tm_name):
m_source = self.bottom.read_edge_source(m_element) m_source = self.bottom.read_edge_source(m_element)
m_target = self.bottom.read_edge_target(m_element) m_target = self.bottom.read_edge_target(m_element)
if m_source == None or m_target == None: if m_source == None or m_target == None:
# element is not a link # element is not a link
continue continue
tm_element, = self.bottom.read_outgoing_elements(self.type_model, tm_name) # tm_element, = self.bottom.read_outgoing_elements(self.type_model, tm_name)
tm_source = self.bottom.read_edge_source(tm_element) tm_source = self.bottom.read_edge_source(tm_element)
tm_target = self.bottom.read_edge_target(tm_element) tm_target = self.bottom.read_edge_target(tm_element)
# check if source is typed correctly # check if source is typed correctly
source_name = self.model_names[m_source] # source_name = self.odapi.m_obj_to_name[m_source]
source_type_actual = self.type_mapping[source_name] source_type_actual = self.odapi.get_type_name(m_source)
source_type_expected = self.type_model_names[tm_source] source_type_expected = self.odapi.mm_obj_to_name[tm_source]
if not self.cdapi.is_subtype(super_type_name=source_type_expected, sub_type_name=source_type_actual): if not self.cdapi.is_subtype(super_type_name=source_type_expected, sub_type_name=source_type_actual):
errors.append(f"Invalid source type '{source_type_actual}' for element '{m_name}'") errors.append(f"Invalid source type '{source_type_actual}' for link '{m_name}:{tm_name}'")
# check if target is typed correctly # check if target is typed correctly
target_name = self.model_names[m_target] # target_name = self.odapi.m_obj_to_name[m_target]
target_type_actual = self.type_mapping[target_name] target_type_actual = self.odapi.get_type_name(m_target)
target_type_expected = self.type_model_names[tm_target] target_type_expected = self.odapi.mm_obj_to_name[tm_target]
if not self.cdapi.is_subtype(super_type_name=source_type_expected, sub_type_name=source_type_actual): if not self.cdapi.is_subtype(super_type_name=source_type_expected, sub_type_name=source_type_actual):
errors.append(f"Invalid target type '{target_type_actual}' for element '{m_name}'") errors.append(f"Invalid target type '{target_type_actual}' for link '{m_name}:{tm_name}'")
return errors return errors
def check_multiplicities(self): def check_multiplicities(self):
""" """
Check whether multiplicities for all types are respected Check whether multiplicities for all types are respected
""" """
self.deref_primitive_values()
self.precompute_multiplicities() self.precompute_multiplicities()
errors = [] errors = []
for type_name in self.type_model_names.values(): for class_name, clss in self.type_odapi.get_all_instances("Class"):
# for type_name in self.odapi.mm_obj_to_name.values():
# abstract classes # abstract classes
if type_name in self.abstract_types: if class_name in self.abstract_types:
count = list(self.type_mapping.values()).count(type_name) count = len(self.odapi.get_all_instances(class_name, include_subtypes=False))
if count > 0: if count > 0:
errors.append(f"Invalid instantiation of abstract class: '{type_name}'") errors.append(f"Invalid instantiation of abstract class: '{class_name}'")
# class multiplicities # class multiplicities
if type_name in self.multiplicities: if class_name in self.multiplicities:
lc, uc = self.multiplicities[type_name] lc, uc = self.multiplicities[class_name]
count = 0 count = len(self.odapi.get_all_instances(class_name, include_subtypes=True))
for sub_type in self.cdapi.transitive_sub_types[type_name]:
count += list(self.type_mapping.values()).count(sub_type)
if count < lc or count > uc: if count < lc or count > uc:
errors.append(f"Cardinality of type exceeds valid multiplicity range: '{type_name}' ({count})") errors.append(f"Cardinality of type exceeds valid multiplicity range: '{class_name}' ({count})")
for assoc_name, assoc in self.type_odapi.get_all_instances("Association") + self.type_odapi.get_all_instances("AttributeLink"):
# association/attribute source multiplicities # association/attribute source multiplicities
if type_name in self.source_multiplicities: if assoc_name in self.source_multiplicities:
# type is an association # type is an association
type_obj, = self.bottom.read_outgoing_elements(self.type_model, type_name) assoc, = self.bottom.read_outgoing_elements(self.type_model, assoc_name)
tgt_type_obj = self.bottom.read_edge_target(type_obj) tgt_type_obj = self.bottom.read_edge_target(assoc)
tgt_type_name = self.type_model_names[tgt_type_obj] tgt_type_name = self.odapi.mm_obj_to_name[tgt_type_obj]
lc, uc = self.source_multiplicities[type_name] lc, uc = self.source_multiplicities[assoc_name]
for obj_name, obj_type_name in self.type_mapping.items(): for obj_name, obj in self.odapi.get_all_instances(tgt_type_name, include_subtypes=True):
if self.cdapi.is_subtype(super_type_name=tgt_type_name, sub_type_name=obj_type_name):
# obj's type has this incoming association -> now we will count the number of links typed by it # obj's type has this incoming association -> now we will count the number of links typed by it
count = 0 count = len(self.odapi.get_incoming(obj, assoc_name, include_subtypes=True))
obj, = self.bottom.read_outgoing_elements(self.model, obj_name)
incoming = self.bottom.read_incoming_edges(obj)
for i in incoming:
try:
type_of_incoming_link = self.type_mapping[self.model_names[i]]
if self.cdapi.is_subtype(super_type_name=type_name, sub_type_name=type_of_incoming_link):
count += 1
except KeyError:
pass # for elements not part of model, e.g. morphism links
if count < lc or count > uc: if count < lc or count > uc:
errors.append(f"Source cardinality of type '{type_name}' ({count}) out of bounds ({lc}..{uc}) in '{obj_name}'.") errors.append(f"Source cardinality of type '{assoc_name}' ({count}) out of bounds ({lc}..{uc}) in '{obj_name}'.")
# association/attribute target multiplicities # association/attribute target multiplicities
if type_name in self.target_multiplicities: if assoc_name in self.target_multiplicities:
# type is an association # type is an association
type_obj, = self.bottom.read_outgoing_elements(self.type_model, type_name) type_obj, = self.bottom.read_outgoing_elements(self.type_model, assoc_name)
src_type_obj = self.bottom.read_edge_source(type_obj) src_type_obj = self.bottom.read_edge_source(type_obj)
src_type_name = self.type_model_names[src_type_obj] src_type_name = self.odapi.mm_obj_to_name[src_type_obj]
lc, uc = self.target_multiplicities[type_name] lc, uc = self.target_multiplicities[assoc_name]
for obj_name, obj_type_name in self.type_mapping.items(): for obj_name, obj in self.odapi.get_all_instances(src_type_name, include_subtypes=True):
if self.cdapi.is_subtype(super_type_name=src_type_name, sub_type_name=obj_type_name):
# obj's type has this outgoing association -> now we will count the number of links typed by it # obj's type has this outgoing association -> now we will count the number of links typed by it
count = 0 count = len(self.odapi.get_outgoing(obj, assoc_name, include_subtypes=True))
obj, = self.bottom.read_outgoing_elements(self.model, obj_name)
outgoing = self.bottom.read_outgoing_edges(obj)
for o in outgoing:
try:
type_of_outgoing_link = self.type_mapping[self.model_names[o]]
if self.cdapi.is_subtype(super_type_name=type_name, sub_type_name=type_of_outgoing_link):
count += 1
except KeyError:
pass # for elements not part of model, e.g. morphism links
if count < lc or count > uc: if count < lc or count > uc:
errors.append(f"Target cardinality of type '{type_name}' ({count}) out of bounds ({lc}..{uc}) in '{obj_name}'.") errors.append(f"Target cardinality of type '{assoc_name}' ({count}) out of bounds ({lc}..{uc}) in '{obj_name}'.")
return errors return errors
def evaluate_constraint(self, code, **kwargs): def evaluate_constraint(self, code, **kwargs):
@ -447,7 +332,7 @@ class Conformance:
# collect types # collect types
class_element, = self.bottom.read_outgoing_elements(self.scd_model, "Class") class_element, = self.bottom.read_outgoing_elements(self.scd_model, "Class")
association_element, = self.bottom.read_outgoing_elements(self.scd_model, "Association") association_element, = self.bottom.read_outgoing_elements(self.scd_model, "Association")
for tm_element, tm_name in self.type_model_names.items(): for tm_element, tm_name in self.odapi.mm_obj_to_name.items():
# retrieve elements that tm_element is a morphism of # retrieve elements that tm_element is a morphism of
morphisms = self.bottom.read_outgoing_elements(tm_element, "Morphism") morphisms = self.bottom.read_outgoing_elements(tm_element, "Morphism")
morphism, = [m for m in morphisms if m in scd_elements] morphism, = [m for m in morphisms if m in scd_elements]
@ -457,7 +342,7 @@ class Conformance:
# collect type structures # collect type structures
# retrieve AttributeLink to check whether element is a morphism of AttributeLink # retrieve AttributeLink to check whether element is a morphism of AttributeLink
attr_link_element, = self.bottom.read_outgoing_elements(self.scd_model, "AttributeLink") attr_link_element, = self.bottom.read_outgoing_elements(self.scd_model, "AttributeLink")
for tm_element, tm_name in self.type_model_names.items(): for tm_element, tm_name in self.odapi.mm_obj_to_name.items():
# retrieve elements that tm_element is a morphism of # retrieve elements that tm_element is a morphism of
morphisms = self.bottom.read_outgoing_elements(tm_element, "Morphism") morphisms = self.bottom.read_outgoing_elements(tm_element, "Morphism")
morphism, = [m for m in morphisms if m in scd_elements] morphism, = [m for m in morphisms if m in scd_elements]
@ -465,8 +350,8 @@ class Conformance:
if attr_link_element == morphism: if attr_link_element == morphism:
# retrieve attributes of attribute link, i.e. 'name' and 'optional' # retrieve attributes of attribute link, i.e. 'name' and 'optional'
attrs = self.bottom.read_outgoing_elements(tm_element) attrs = self.bottom.read_outgoing_elements(tm_element)
name_model_node, = filter(lambda x: self.type_model_names.get(x, "").endswith(".name"), attrs) name_model_node, = filter(lambda x: self.odapi.m_obj_to_name.get(x, "").endswith(".name"), attrs)
opt_model_node, = filter(lambda x: self.type_model_names.get(x, "").endswith(".optional"), attrs) opt_model_node, = filter(lambda x: self.odapi.m_obj_to_name.get(x, "").endswith(".optional"), attrs)
# get attr name value # get attr name value
name_model = UUID(self.bottom.read_value(name_model_node)) name_model = UUID(self.bottom.read_value(name_model_node))
name_node, = self.bottom.read_outgoing_elements(name_model) name_node, = self.bottom.read_outgoing_elements(name_model)
@ -477,9 +362,9 @@ class Conformance:
opt = self.bottom.read_value(opt_node) opt = self.bottom.read_value(opt_node)
# get attr type name # get attr type name
source_type_node = self.bottom.read_edge_source(tm_element) source_type_node = self.bottom.read_edge_source(tm_element)
source_type_name = self.type_model_names[source_type_node] source_type_name = self.odapi.mm_obj_to_name[source_type_node]
target_type_node = self.bottom.read_edge_target(tm_element) target_type_node = self.bottom.read_edge_target(tm_element)
target_type_name = self.type_model_names[target_type_node] target_type_name = self.odapi.mm_obj_to_name[target_type_node]
# add attribute to the structure of its source type # add attribute to the structure of its source type
# attribute is stored as a (name, optional, type) triple # attribute is stored as a (name, optional, type) triple
self.structures.setdefault(source_type_name, set()).add((name, opt, target_type_name)) self.structures.setdefault(source_type_name, set()).add((name, opt, target_type_name))
@ -493,7 +378,7 @@ class Conformance:
# filter out abstract types, as they cannot be instantiated # filter out abstract types, as they cannot be instantiated
# retrieve Class_abstract to check whether element is a morphism of Class_abstract # retrieve Class_abstract to check whether element is a morphism of Class_abstract
class_abs_element, = self.bottom.read_outgoing_elements(self.scd_model, "Class_abstract") class_abs_element, = self.bottom.read_outgoing_elements(self.scd_model, "Class_abstract")
for tm_element, tm_name in self.type_model_names.items(): for tm_element, tm_name in self.odapi.mm_obj_to_name.items():
# retrieve elements that tm_element is a morphism of # retrieve elements that tm_element is a morphism of
morphisms = self.bottom.read_outgoing_elements(tm_element, "Morphism") morphisms = self.bottom.read_outgoing_elements(tm_element, "Morphism")
morphism, = [m for m in morphisms if m in scd_elements] morphism, = [m for m in morphisms if m in scd_elements]
@ -506,7 +391,7 @@ class Conformance:
is_abstract = self.bottom.read_value(abst_node) is_abstract = self.bottom.read_value(abst_node)
# retrieve type name # retrieve type name
source_node = self.bottom.read_edge_source(tm_element) source_node = self.bottom.read_edge_source(tm_element)
type_name = self.type_model_names[source_node] type_name = self.odapi.mm_obj_to_name[source_node]
if is_abstract: if is_abstract:
self.structures.pop(type_name) self.structures.pop(type_name)
@ -516,7 +401,7 @@ class Conformance:
""" """
ref_element, = self.bottom.read_outgoing_elements(self.scd_model, "ModelRef") ref_element, = self.bottom.read_outgoing_elements(self.scd_model, "ModelRef")
# matching # matching
for m_element, m_name in self.model_names.items(): for m_element, m_name in self.odapi.m_obj_to_name.items():
is_edge = self.bottom.read_edge_source(m_element) != None is_edge = self.bottom.read_edge_source(m_element) != None
print('element:', m_element, 'name:', m_name, 'is_edge', is_edge) print('element:', m_element, 'name:', m_name, 'is_edge', is_edge)
for type_name, structure in self.structures.items(): for type_name, structure in self.structures.items():
@ -567,23 +452,23 @@ class Conformance:
print(' add to candidates:', m_name, type_name) print(' add to candidates:', m_name, type_name)
self.candidates.setdefault(m_name, set()).add(type_name) self.candidates.setdefault(m_name, set()).add(type_name)
# filter out candidates for links based on source and target types # filter out candidates for links based on source and target types
for m_element, m_name in self.model_names.items(): for m_element, m_name in self.odapi.m_obj_to_name.items():
is_edge = self.bottom.read_edge_source(m_element) != None is_edge = self.bottom.read_edge_source(m_element) != None
if is_edge and m_name in self.candidates: if is_edge and m_name in self.candidates:
m_source = self.bottom.read_edge_source(m_element) m_source = self.bottom.read_edge_source(m_element)
m_target = self.bottom.read_edge_target(m_element) m_target = self.bottom.read_edge_target(m_element)
print(self.candidates) print(self.candidates)
source_candidates = self.candidates[self.model_names[m_source]] source_candidates = self.candidates[self.odapi.m_obj_to_name[m_source]]
target_candidates = self.candidates[self.model_names[m_target]] target_candidates = self.candidates[self.odapi.m_obj_to_name[m_target]]
remove = set() remove = set()
for candidate_name in self.candidates[m_name]: for candidate_name in self.candidates[m_name]:
candidate_element, = self.bottom.read_outgoing_elements(self.type_model, candidate_name) candidate_element, = self.bottom.read_outgoing_elements(self.type_model, candidate_name)
candidate_source = self.type_model_names[self.bottom.read_edge_source(candidate_element)] candidate_source = self.odapi.mm_obj_to_name[self.bottom.read_edge_source(candidate_element)]
if candidate_source not in source_candidates: if candidate_source not in source_candidates:
if len(source_candidates.intersection(set(self.odapi.transitive_sub_types[candidate_source]))) == 0: if len(source_candidates.intersection(set(self.odapi.transitive_sub_types[candidate_source]))) == 0:
# if len(source_candidates.intersection(set(self.sub_types[candidate_source]))) == 0: # if len(source_candidates.intersection(set(self.sub_types[candidate_source]))) == 0:
remove.add(candidate_name) remove.add(candidate_name)
candidate_target = self.type_model_names[self.bottom.read_edge_target(candidate_element)] candidate_target = self.odapi.mm_obj_to_name[self.bottom.read_edge_target(candidate_element)]
if candidate_target not in target_candidates: if candidate_target not in target_candidates:
if len(target_candidates.intersection(set(self.odapi.transitive_sub_types[candidate_target]))) == 0: if len(target_candidates.intersection(set(self.odapi.transitive_sub_types[candidate_target]))) == 0:
# if len(target_candidates.intersection(set(self.sub_types[candidate_target]))) == 0: # if len(target_candidates.intersection(set(self.sub_types[candidate_target]))) == 0:

View file

@ -372,7 +372,7 @@ def read_primitive_value(bottom, modelref: UUID, mm: UUID):
if not is_typed_by(bottom, typ, get_scd_mm_modelref_node(bottom)): if not is_typed_by(bottom, typ, get_scd_mm_modelref_node(bottom)):
raise Exception("Assertion failed: argument must be typed by ModelRef", typ) raise Exception("Assertion failed: argument must be typed by ModelRef", typ)
referred_model = UUID(bottom.read_value(modelref)) referred_model = UUID(bottom.read_value(modelref))
typ_name = get_object_name(bottom, mm, typ) typ_name = get_object_name(bottom, model=mm, object_node=typ)
if typ_name == "Integer": if typ_name == "Integer":
return Integer(referred_model, bottom.state).read(), typ_name return Integer(referred_model, bottom.state).read(), typ_name
elif typ_name == "String": elif typ_name == "String":