PlantUML: render class cardinalities. Extend constraint checker API.

This commit is contained in:
Joeri Exelmans 2024-10-08 21:08:06 +02:00
parent c351649d23
commit e70eae2286
9 changed files with 252 additions and 142 deletions

View file

@ -36,13 +36,9 @@ def bootstrap_type(type_name: str, scd_root: UUID, model_root: UUID, integer_typ
return class_node
def bootstrap_constraint(class_node, type_name: str, python_type: str, scd_root: UUID, model_root: UUID, actioncode_type: UUID, state: State):
# set constraint
# chicken-and-egg problem: we cannot create an action-code constraint because the action-code MM doesn't exist yet
bottom = Bottom(state)
constraint_model = bottom.create_node()
ActionCode(constraint_model, state).create(f"isinstance(read_value(element),{python_type})")
ActionCode(constraint_model, state).create(f"isinstance(read_value(this),{python_type})")
constraint_node = bottom.create_node(str(constraint_model))
bottom.create_edge(model_root, constraint_node, f"{type_name}.constraint")
constraint_link = bottom.create_edge(class_node, constraint_node)
@ -52,24 +48,6 @@ def bootstrap_constraint(class_node, type_name: str, python_type: str, scd_root:
bottom.create_edge(constraint_node, scd_node, "Morphism")
bottom.create_edge(constraint_link, scd_link, "Morphism")
# def bootstrap_type_type(scd_root: UUID, model_root: UUID, integer_type: UUID, actioncode_type: UUID, state: State):
# def bootstrap_boolean_type(scd_root: UUID, model_root: UUID, integer_type: UUID, actioncode_type: UUID, state: State):
# def bootstrap_integer_type(scd_root: UUID, model_root: UUID, integer_type: UUID, actioncode_type: UUID, state: State):
# def bootstrap_float_type(scd_root: UUID, model_root: UUID, integer_type: UUID, actioncode_type: UUID, state: State):
# def bootstrap_string_type(scd_root: UUID, model_root: UUID, integer_type: UUID, actioncode_type: UUID, state: State):
# def bootstrap_actioncode_type(scd_root: UUID, model_root: UUID, integer_type: UUID, actioncode_type: UUID, state: State):
# # we store action code as Python string:
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
class_integer = bootstrap_type("Integer", scd_root, integer_type, integer_type, state)

View file

@ -23,10 +23,19 @@ def render_class_diagram(state, model, prefix_ids=""):
if slot != None:
is_abstract, _ = od.read_primitive_value(bottom, slot, model_od.type_model)
if is_abstract:
output += f"\nabstract class \"{name}\" as {make_id(class_node)}"
lower_card, upper_card = model_scd.get_class_cardinalities(class_node)
if lower_card == None and upper_card == None:
card_spec = ""
else:
output += f"\nclass \"{name}\" as {make_id(class_node)}"
card_spec = f"{0 if lower_card == None else lower_card}..{"*" if upper_card == None else upper_card}"
if is_abstract:
output += f"\nabstract class \"{name} {card_spec}\" as {make_id(class_node)}"
else:
output += f"\nclass \"{name} {card_spec}\" as {make_id(class_node)}"
# Render attributes
output += " {"

View file

@ -29,8 +29,11 @@ BOOL: "True" | "False"
CODE: /`[^`]*`/
INDENTED_CODE: /```[^`]*```/
object: [IDENTIFIER] ":" IDENTIFIER [link_spec] ["{" slot* "}"]
# name (optional) type
object: [IDENTIFIER] ":" IDENTIFIER [link_spec] ["{" slot* "}"]
link_spec: "(" IDENTIFIER "->" IDENTIFIER ")"
slot: IDENTIFIER "=" literal ";"
"""
@ -78,7 +81,7 @@ def parse_od(state, cs_text, mm):
space_count += 1
lines = token.split('\n')[1:-1]
for line in lines:
if line[0:space_count] != ' '*space_count:
if len(line) >= space_count and line[0:space_count] != ' '*space_count:
raise Exception("wrong indentation of INDENTED_CODE")
unindented_lines = [l[space_count:] for l in lines]
return _Code('\n'.join(unindented_lines))

View file

@ -1,16 +1,7 @@
from state.devstate import DevState
from bootstrap.scd import bootstrap_scd
from uuid import UUID
from services.scd import SCD
from framework.conformance import Conformance
from services.od import OD
from transformation.ramify import ramify
from transformation import rewriter
from services.bottom.V0 import Bottom
from services.primitives.integer_type import Integer
from pattern_matching import mvs_adapter
from pattern_matching.matcher import MatcherVF2
from renderer import plantuml
from concrete_syntax.plantuml import renderer as plantuml
def main():
state = DevState()

View file

@ -0,0 +1,139 @@
package "DSL Meta-Model" {
class "Bear" as 00000000_0000_0000_0000_00000000046d {
}
abstract class "Animal" as 00000000_0000_0000_0000_000000000474 {
}
class "Man" as 00000000_0000_0000_0000_000000000491 {
weight : Integer
}
00000000_0000_0000_0000_000000000474 <|-- 00000000_0000_0000_0000_000000000491
00000000_0000_0000_0000_000000000474 <|-- 00000000_0000_0000_0000_00000000046d
00000000_0000_0000_0000_000000000491 " " --> "1 .. *" 00000000_0000_0000_0000_000000000474 : afraidOf
}
package "Int Meta-Model" {
class "Integer" as 00000000_0000_0000_0000_000000000094 {
}
}
package "RAMified DSL Meta-Model" {
class "RAM_Bear" as 00000000_0000_0000_0000_0000000005bb {
}
class "RAM_Animal" as 00000000_0000_0000_0000_0000000005c5 {
}
class "RAM_Man" as 00000000_0000_0000_0000_0000000005cf {
RAM_weight : ActionCode
}
00000000_0000_0000_0000_0000000005c5 <|-- 00000000_0000_0000_0000_0000000005cf
00000000_0000_0000_0000_0000000005c5 <|-- 00000000_0000_0000_0000_0000000005bb
00000000_0000_0000_0000_0000000005cf " " --> "0 .. *" 00000000_0000_0000_0000_0000000005c5 : RAM_afraidOf
}
package "RAMified Int Meta-Model" {
class "RAM_Integer" as 00000000_0000_0000_0000_00000000064c {
}
}
00000000_0000_0000_0000_0000000005bb ..> 00000000_0000_0000_0000_00000000046d #line:green;text:green : RAMifies
00000000_0000_0000_0000_0000000005c5 ..> 00000000_0000_0000_0000_000000000474 #line:green;text:green : RAMifies
00000000_0000_0000_0000_0000000005cf ..> 00000000_0000_0000_0000_000000000491 #line:green;text:green : RAMifies
00000000_0000_0000_0000_0000000005cf::RAM_weight ..> 00000000_0000_0000_0000_000000000491::weight #line:green;text:green : RAMifies
00000000_0000_0000_0000_00000000064c ..> 00000000_0000_0000_0000_000000000094 #line:green;text:green : RAMifies
package "LHS" {
map "scaryAnimal : RAM_Animal" as 00000000_0000_0000_0000_00000000068a {
}
map "man : RAM_Man" as 00000000_0000_0000_0000_00000000066d {
RAM_weight => `v > 60`
}
00000000_0000_0000_0000_00000000066d -> 00000000_0000_0000_0000_00000000068a : :RAM_afraidOf
}
00000000_0000_0000_0000_00000000068a ..> 00000000_0000_0000_0000_0000000005c5 #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_00000000066d ..> 00000000_0000_0000_0000_0000000005cf #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_00000000066d::RAM_weight ..> 00000000_0000_0000_0000_0000000005cf::RAM_weight #line:blue;text:blue : instanceOf
package "RHS" {
map "man : RAM_Man" as 00000000_0000_0000_0000_000000000699 {
RAM_weight => `v + 5`
}
map "bill : RAM_Man" as 00000000_0000_0000_0000_0000000006b6 {
RAM_weight => `100`
}
00000000_0000_0000_0000_0000000006b6 -> 00000000_0000_0000_0000_000000000699 : :RAM_afraidOf
}
00000000_0000_0000_0000_000000000699 ..> 00000000_0000_0000_0000_0000000005cf #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_000000000699::RAM_weight ..> 00000000_0000_0000_0000_0000000005cf::RAM_weight #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_0000000006b6 ..> 00000000_0000_0000_0000_0000000005cf #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_0000000006b6::RAM_weight ..> 00000000_0000_0000_0000_0000000005cf::RAM_weight #line:blue;text:blue : instanceOf
package "Model (before rewrite)" {
map "bear2 : Bear" as 00000000_0000_0000_0000_000000000597 {
}
map "bear1 : Bear" as 00000000_0000_0000_0000_000000000590 {
}
map "george : Man" as 00000000_0000_0000_0000_000000000573 {
weight => 80
}
00000000_0000_0000_0000_000000000573 -> 00000000_0000_0000_0000_000000000590 : :afraidOf
00000000_0000_0000_0000_000000000573 -> 00000000_0000_0000_0000_000000000597 : :afraidOf
}
00000000_0000_0000_0000_000000000597 ..> 00000000_0000_0000_0000_00000000046d #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_000000000590 ..> 00000000_0000_0000_0000_00000000046d #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_000000000573 ..> 00000000_0000_0000_0000_000000000491 #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_000000000573::weight ..> 00000000_0000_0000_0000_000000000491::weight #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_00000000068a ..> 00000000_0000_0000_0000_000000000590 #line:red;line.dotted;text:red : matchedWith
00000000_0000_0000_0000_00000000066d ..> 00000000_0000_0000_0000_000000000573 #line:red;line.dotted;text:red : matchedWith
00000000_0000_0000_0000_00000000066d::RAM_weight ..> 00000000_0000_0000_0000_000000000573::weight #line:red;line.dotted;text:red : matchedWith
package "Model (after rewrite 0)" {
map "bear2 : Bear" as 00000000_0000_0000_0000_0000000006db {
}
map "george : Man" as 00000000_0000_0000_0000_0000000006e9 {
weight => 85
}
map "bill0 : Man" as 00000000_0000_0000_0000_000000000723 {
weight => 100
}
00000000_0000_0000_0000_000000000723 -> 00000000_0000_0000_0000_0000000006e9 : :afraidOf
00000000_0000_0000_0000_0000000006e9 -> 00000000_0000_0000_0000_0000000006db : :afraidOf
}
00000000_0000_0000_0000_000000000699 ..> 00000000_0000_0000_0000_0000000006e9 #line:red;line.dotted;text:red : matchedWith
00000000_0000_0000_0000_000000000699::RAM_weight ..> 00000000_0000_0000_0000_0000000006e9::weight #line:red;line.dotted;text:red : matchedWith
00000000_0000_0000_0000_0000000006b6 ..> 00000000_0000_0000_0000_000000000723 #line:red;line.dotted;text:red : matchedWith
00000000_0000_0000_0000_0000000006db ..> 00000000_0000_0000_0000_00000000046d #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_0000000006e9 ..> 00000000_0000_0000_0000_000000000491 #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_0000000006e9::weight ..> 00000000_0000_0000_0000_000000000491::weight #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_000000000723 ..> 00000000_0000_0000_0000_000000000491 #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_000000000723::weight ..> 00000000_0000_0000_0000_000000000491::weight #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_00000000068a ..> 00000000_0000_0000_0000_000000000597 #line:orange;line.dotted;text:orange : matchedWith
00000000_0000_0000_0000_00000000066d ..> 00000000_0000_0000_0000_000000000573 #line:orange;line.dotted;text:orange : matchedWith
00000000_0000_0000_0000_00000000066d::RAM_weight ..> 00000000_0000_0000_0000_000000000573::weight #line:orange;line.dotted;text:orange : matchedWith
package "Model (after rewrite 1)" {
map "bear1 : Bear" as 00000000_0000_0000_0000_000000000747 {
}
map "george : Man" as 00000000_0000_0000_0000_00000000074e {
weight => 85
}
map "bill0 : Man" as 00000000_0000_0000_0000_000000000788 {
weight => 100
}
00000000_0000_0000_0000_000000000788 -> 00000000_0000_0000_0000_00000000074e : :afraidOf
00000000_0000_0000_0000_00000000074e -> 00000000_0000_0000_0000_000000000747 : :afraidOf
}
00000000_0000_0000_0000_000000000699 ..> 00000000_0000_0000_0000_00000000074e #line:orange;line.dotted;text:orange : matchedWith
00000000_0000_0000_0000_000000000699::RAM_weight ..> 00000000_0000_0000_0000_00000000074e::weight #line:orange;line.dotted;text:orange : matchedWith
00000000_0000_0000_0000_0000000006b6 ..> 00000000_0000_0000_0000_000000000788 #line:orange;line.dotted;text:orange : matchedWith
00000000_0000_0000_0000_000000000747 ..> 00000000_0000_0000_0000_00000000046d #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_00000000074e ..> 00000000_0000_0000_0000_000000000491 #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_00000000074e::weight ..> 00000000_0000_0000_0000_000000000491::weight #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_000000000788 ..> 00000000_0000_0000_0000_000000000491 #line:blue;text:blue : instanceOf
00000000_0000_0000_0000_000000000788::weight ..> 00000000_0000_0000_0000_000000000491::weight #line:blue;text:blue : instanceOf

View file

@ -15,8 +15,6 @@ from services.primitives.integer_type import Integer
from concrete_syntax.plantuml import renderer as plantuml
from concrete_syntax.textual_od import parser, renderer
import sys
def create_integer_node(state, i: int):
node = state.create_node()
integer_t = Integer(node, state)
@ -27,119 +25,79 @@ def main():
state = DevState()
root = state.read_root() # id: 0
scd_mm_id = bootstrap_scd(state)
# Meta-meta-model: a class diagram that describes the language of class diagrams
scd_mmm_id = bootstrap_scd(state)
int_mm_id = UUID(state.read_value(state.read_dict(state.read_root(), "Integer")))
string_mm_id = UUID(state.read_value(state.read_dict(state.read_root(), "String")))
# conf = Conformance(state, scd_mm_id, scd_mm_id)
# conf = Conformance(state, scd_mmm_id, scd_mmm_id)
# print("Conformance SCD_MM -> SCD_MM?", conf.check_nominal(log=True))
# print("--------------------------------------")
# print(renderer.render_od(state, scd_mm_id, scd_mm_id, hide_names=True))
# print(renderer.render_od(state, scd_mmm_id, scd_mmm_id, hide_names=True))
# print("--------------------------------------")
def create_dsl_mm_api():
# Create DSL MM with SCD API
dsl_mm_id = state.create_node()
dsl_mm_scd = SCD(dsl_mm_id, state)
dsl_mm_scd.create_class("Animal", abstract=True)
dsl_mm_scd.create_class("Man", min_c=1, max_c=2)
dsl_mm_scd.create_inheritance("Man", "Animal")
dsl_mm_scd.create_model_ref("Integer", int_mm_id)
dsl_mm_scd.create_attribute_link("Man", "Integer", "weight", optional=False)
dsl_mm_scd.create_class("Bear")
dsl_mm_scd.create_inheritance("Bear", "Animal")
dsl_mm_scd.create_association("afraidOf", "Man", "Animal",
# Every Man afraid of at least one Animal:
src_min_c=0,
src_max_c=None,
tgt_min_c=1,
tgt_max_c=None,
)
dsl_mm_scd.add_constraint("Man", "read_value(element) < 100")
return dsl_mm_id
# Create DSL MM with parser
dsl_mm_cs = """
# Integer:ModelRef
Bear:Class
Animal:Class {
abstract = True;
}
Man:Class {
lower_cardinality = 1;
upper_cardinality = 2;
constraint = ```
get_value(get_slot(this, "weight")) > 20
```;
}
Man_weight:AttributeLink (Man -> Integer) {
name = "weight";
optional = False;
constraint = ```
# this is the same constraint as above, but this time, part of the attributelink itself (and thus shorter)
tgt = get_target(this)
tgt_type = get_type_name(tgt)
get_value(tgt) > 20
```;
}
afraidOf:Association (Man -> Animal) {
target_lower_cardinality = 1;
}
:Inheritance (Man -> Animal)
:Inheritance (Bear -> Animal)
def create_dsl_mm_parser():
# Create DSL MM with parser
dsl_mm_cs = """
# Integer:ModelRef
Bear:Class
Animal:Class {
abstract = True;
}
Man:Class {
lower_cardinality = 1;
upper_cardinality = 2;
constraint = `get_value(get_slot(element, "weight")) > 20`;
}
Man_weight:AttributeLink (Man -> Integer) {
name = "weight";
optional = False;
constraint = ```
# this is the same constraint as above, but this time, part of the attributelink itself (and thus shorter)
node = get_target(element)
get_value(node) > 20
```;
}
afraidOf:Association (Man -> Animal) {
target_lower_cardinality = 1;
}
:Inheritance (Man -> Animal)
:Inheritance (Bear -> Animal)
not_too_fat:GlobalConstraint {
constraint = ```
# total weight of all men low enough
total_weight = 0
for man_name, man_id in get_all_instances("Man"):
total_weight += get_value(get_slot(man_id, "weight"))
total_weight < 85
```;
}
"""
dsl_mm_id = parser.parse_od(state, dsl_mm_cs, mm=scd_mm_id)
return dsl_mm_id
not_too_fat:GlobalConstraint {
constraint = ```
# total weight of all men low enough
total_weight = 0
for man_name, man_id in get_all_instances("Man"):
total_weight += get_value(get_slot(man_id, "weight"))
total_weight < 85
```;
}
"""
dsl_mm_id = parser.parse_od(state, dsl_mm_cs, mm=scd_mmm_id)
def create_dsl_m_api():
# Create DSL M with OD API
dsl_m_id = state.create_node()
dsl_m_od = OD(dsl_mm_id, dsl_m_id, state)
dsl_m_od.create_object("george", "Man")
dsl_m_od.create_slot("weight", "george",
dsl_m_od.create_integer_value("george.weight", 80))
dsl_m_od.create_object("bear1", "Bear")
dsl_m_od.create_object("bear2", "Bear")
dsl_m_od.create_link("georgeAfraidOfBear1", "afraidOf", "george", "bear1")
dsl_m_od.create_link("georgeAfraidOfBear2", "afraidOf", "george", "bear2")
return dsl_m_id
def create_dsl_m_parser():
# Create DSL M with parser
dsl_m_cs = """
george:Man {
weight = 80;
}
bear1:Bear
bear2:Bear
:afraidOf (george -> bear1)
:afraidOf (george -> bear2)
"""
dsl_m_id = parser.parse_od(state, dsl_m_cs, mm=dsl_mm_id)
return dsl_m_id
# dsl_mm_id = create_dsl_mm_api()
dsl_mm_id = create_dsl_mm_parser()
# Create DSL M with parser
dsl_m_cs = """
george:Man {
weight = 80;
}
bear1:Bear
bear2:Bear
:afraidOf (george -> bear1)
:afraidOf (george -> bear2)
"""
dsl_m_id = parser.parse_od(state, dsl_m_cs, mm=dsl_mm_id)
# print("DSL MM:")
# print("--------------------------------------")
# print(renderer.render_od(state, dsl_mm_id, scd_mm_id, hide_names=True))
# print(renderer.render_od(state, dsl_mm_id, scd_mmm_id, hide_names=True))
# print("--------------------------------------")
conf = Conformance(state, dsl_mm_id, scd_mm_id)
conf = Conformance(state, dsl_mm_id, scd_mmm_id)
print("Conformance DSL_MM -> SCD_MM?", conf.check_nominal(log=True))
# dsl_m_id = create_dsl_m_api()
dsl_m_id = create_dsl_m_parser()
# print("DSL M:")
# print("--------------------------------------")
# print(renderer.render_od(state, dsl_m_id, dsl_mm_id, hide_names=True))

View file

@ -375,21 +375,33 @@ class Conformance:
'read_value': self.state.read_value,
'get_value': lambda el: od.read_primitive_value(self.bottom, el, self.type_model)[0],
'get_target': lambda el: self.bottom.read_edge_target(el),
'get_source': lambda el: self.bottom.read_edge_source(el),
'get_slot': od.OD(self.type_model, self.model, self.state).get_slot,
'get_all_instances': self.get_all_instances
'get_all_instances': self.get_all_instances,
'get_name': lambda el: [name for name in self.bottom.read_keys(self.model) if self.bottom.read_outgoing_elements(self.model, name)[0] == el][0],
'get_type_name': self.get_type_name,
'get_outgoing': self.get_outgoing,
'get_incoming': self.get_incoming,
}
# print("evaluating constraint ...", code)
loc = {**kwargs, **funcs}
loc = {**kwargs, }
result = exec_then_eval(
code,
{'__builtins__': {'isinstance': isinstance, 'print': print,
'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len}
'int': int, 'float': float, 'bool': bool, 'str': str, 'tuple': tuple, 'len': len, 'set': set, 'dict': dict},
**funcs
}, # globals
loc # locals
)
# print('result =', result)
return result
def get_type_name(self, element: UUID):
type_node = self.bottom.read_outgoing_elements(element, "Morphism")[0]
for type_name in self.bottom.read_keys(self.type_model):
if self.bottom.read_outgoing_elements(self.type_model, type_name)[0] == type_node:
return type_name
def get_all_instances(self, type_name: str, include_subtypes=True):
result = [e_name for e_name, t_name in self.type_mapping.items() if t_name == type_name]
if include_subtypes:
@ -399,6 +411,12 @@ class Conformance:
result_with_ids = [ (e_name, self.bottom.read_outgoing_elements(self.model, e_name)[0]) for e_name in result]
return result_with_ids
def get_outgoing(self, element: UUID, assoc_or_attr_name: str):
return od.find_outgoing_typed_by(self.bottom, src=element, type_node=self.bottom.read_outgoing_elements(self.type_model, assoc_or_attr_name)[0])
def get_incoming(self, element: UUID, assoc_or_attr_name: str):
return od.find_incoming_typed_by(self.bottom, tgt=element, type_node=self.bottom.read_outgoing_elements(self.type_model, assoc_or_attr_name)[0])
def check_constraints(self):
"""
Check whether all constraints defined for a model are respected
@ -426,7 +444,7 @@ class Conformance:
morphisms = self.bottom.read_incoming_elements(tm_element, "Morphism")
morphisms = [m for m in morphisms if m in self.model_names]
for m_element in morphisms:
result = self.evaluate_constraint(code, element=m_element, type_name=tm_name)
result = self.evaluate_constraint(code, this=m_element)
description = f"Local constraint of \"{tm_name}\" in \"{m_name}\""
check_result(result, description)
@ -555,7 +573,7 @@ class Conformance:
# eval constraints
code = self.read_attribute(attr_tm, "constraint")
if code != None:
attr_conforms = self.evaluate_constraint(code, element=attr)
attr_conforms = self.evaluate_constraint(code, this=attr)
if attr_conforms:
matched += 1
print(" attr_conforms -> matched:", matched)

View file

@ -317,6 +317,15 @@ def find_outgoing_typed_by(bottom, src: UUID, type_node: UUID):
break
return edges
def find_incoming_typed_by(bottom, tgt: UUID, type_node: UUID):
edges = []
for incoming_edge in bottom.read_incoming_edges(tgt):
for typedBy in bottom.read_outgoing_elements(incoming_edge, "Morphism"):
if typedBy == type_node:
edges.append(incoming_edge)
break
return edges
def navigate_modelref(bottom, node: UUID):
uuid = bottom.read_value(node)
return UUID(uuid)

View file

@ -355,6 +355,11 @@ class SCD:
name_to_attr[name] = edge
return name_to_attr
def get_class_cardinalities(self, class_node):
lower_card = od.find_cardinality(self.bottom, class_node, od.get_scd_mm_class_lowercard_node(self.bottom))
upper_card = od.find_cardinality(self.bottom, class_node, od.get_scd_mm_class_uppercard_node(self.bottom))
return lower_card, upper_card
def get_assoc_cardinalities(self, assoc_edge):
src_lower_card = od.find_cardinality(self.bottom, assoc_edge, od.get_scd_mm_assoc_src_lowercard_node(self.bottom))
src_upper_card = od.find_cardinality(self.bottom, assoc_edge, od.get_scd_mm_assoc_src_uppercard_node(self.bottom))