Skip to content

Commit

Permalink
feat: Include nested comps in interface context
Browse files Browse the repository at this point in the history
Solves (#93).

---------

Co-authored-by: ewuerger <[email protected]>
  • Loading branch information
huyenngn and ewuerger authored May 7, 2024
1 parent c9226d3 commit bf7a801
Show file tree
Hide file tree
Showing 9 changed files with 1,161 additions and 166 deletions.
1 change: 1 addition & 0 deletions capellambse_context_diagrams/_elkjs.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"hierarchyHandling": "INCLUDE_CHILDREN",
"layered.edgeLabels.sideSelection": "ALWAYS_DOWN",
"layered.nodePlacement.strategy": "BRANDES_KOEPF",
"layered.considerModelOrder.strategy": "NODES_AND_EDGES",
"spacing.labelNode": "0.0",
}
"""
Expand Down
159 changes: 110 additions & 49 deletions capellambse_context_diagrams/collectors/exchanges.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import typing as t

from capellambse.model import common
from capellambse.model.crosslayer import cs
from capellambse.model.modeltypes import DiagramType as DT

from .. import _elkjs, context
Expand Down Expand Up @@ -65,31 +66,60 @@ def get_functions_and_exchanges(
self, comp: common.GenericElement, interface: common.GenericElement
) -> tuple[
list[common.GenericElement],
list[common.GenericElement],
list[common.GenericElement],
dict[str, common.GenericElement],
dict[str, common.GenericElement],
]:
"""Return `Function`s, incoming and outgoing
`FunctionalExchange`s for given `Component` and `interface`.
"""
functions, outgoings, incomings = [], [], []
functions, incomings, outgoings = [], {}, {}
alloc_functions = self.get_alloc_functions(comp)
for fex in self.get_alloc_fex(interface):
source = self.get_source(fex)
if source in alloc_functions:
if fex not in outgoings:
outgoings.append(fex)
if fex.uuid not in outgoings:
outgoings[fex.uuid] = fex
if source not in functions:
functions.append(source)

target = self.get_target(fex)
if target in alloc_functions:
if fex not in incomings:
incomings.append(fex)
if fex.uuid not in incomings:
incomings[fex.uuid] = fex
if target not in functions:
functions.append(target)

return functions, incomings, outgoings

def collect_context(
self, comp: common.GenericElement, interface: common.GenericElement
) -> tuple[
dict[str, t.Any],
dict[str, common.GenericElement],
dict[str, common.GenericElement],
]:
functions, incomings, outgoings = self.get_functions_and_exchanges(
comp, interface
)
components = []
for cmp in comp.components:
fncs, _, _ = self.get_functions_and_exchanges(cmp, interface)
functions.extend(fncs)
if fncs:
c, incs, outs = self.collect_context(cmp, interface)
components.append(c)
incomings |= incs
outgoings |= outs
return (
{
"element": comp,
"functions": functions,
"components": components,
},
incomings,
outgoings,
)

def make_ports_and_update_children_size(
self,
data: _elkjs.ELKInputChild,
Expand All @@ -100,6 +130,9 @@ def make_ports_and_update_children_size(
for child in data["children"]:
inputs, outputs = [], []
obj = self.obj._model.by_uuid(child["id"])
if isinstance(obj, cs.Component):
self.make_ports_and_update_children_size(child, exchanges)
return
port_ids = {p.uuid for p in obj.inputs + obj.outputs}
for ex in exchanges:
source, target = ex["sources"][0], ex["targets"][0]
Expand Down Expand Up @@ -153,21 +186,29 @@ class InterfaceContextCollector(ExchangeCollector):
for building the interface context.
"""

left: common.GenericElement
"""Source or target Component of the interface."""
right: common.GenericElement
"""Source or target Component of the interface."""
outgoing_edges: list[common.GenericElement]
incoming_edges: list[common.GenericElement]
left: _elkjs.ELKInputChild | None
"""Left (source) Component Box of the interface."""
right: _elkjs.ELKInputChild | None
"""Right (target) Component Box of the interface."""
outgoing_edges: dict[str, common.GenericElement]
incoming_edges: dict[str, common.GenericElement]

def __init__(
self,
diagram: context.InterfaceContextDiagram,
data: _elkjs.ELKInputData,
params: dict[str, t.Any],
) -> None:
self.left = None
self.right = None
self.incoming_edges = {}
self.outgoing_edges = {}

super().__init__(diagram, data, params)

self.get_left_and_right()
if diagram.include_interface:
self.add_interface()

def get_left_and_right(self) -> None:
made_children: set[str] = set()
Expand All @@ -178,15 +219,19 @@ def get_capella_order(
alloc_functions = self.get_alloc_functions(comp)
return [fnc for fnc in alloc_functions if fnc in functions]

def make_boxes(
comp: common.GenericElement, functions: list[common.GenericElement]
) -> None:
def make_boxes(cntxt: dict[str, t.Any]) -> _elkjs.ELKInputChild | None:
comp = cntxt["element"]
functions = cntxt["functions"]
components = cntxt["components"]
if comp.uuid not in made_children:
children = [
makers.make_box(c)
for c in functions
if c in self.get_alloc_functions(comp)
makers.make_box(fnc)
for fnc in functions
if fnc in self.get_alloc_functions(comp)
]
for cmp in components:
if child := make_boxes(cmp):
children.append(child)
if children:
layout_options = makers.DEFAULT_LABEL_LAYOUT_OPTIONS
else:
Expand All @@ -196,52 +241,68 @@ def make_boxes(
comp, no_symbol=True, layout_options=layout_options
)
box["children"] = children
self.data["children"].append(box)
made_children.add(comp.uuid)
return box
return None

try:
comp = self.get_source(self.obj)
functions, incs, outs = self.get_functions_and_exchanges(
comp, self.obj
)
inc_port_ids = set(ex.target.uuid for ex in incs)
out_port_ids = set(ex.source.uuid for ex in outs)
left_context, incs, outs = self.collect_context(comp, self.obj)
inc_port_ids = set(ex.target.uuid for ex in incs.values())
out_port_ids = set(ex.source.uuid for ex in outs.values())
port_spread = len(out_port_ids) - len(inc_port_ids)

_comp = self.get_target(self.obj)
_functions, _, _ = self.get_functions_and_exchanges(
_comp, self.obj
)
_inc_port_ids = set(ex.target.uuid for ex in outs)
_out_port_ids = set(ex.source.uuid for ex in incs)
right_context, _, _ = self.collect_context(_comp, self.obj)
_inc_port_ids = set(ex.target.uuid for ex in outs.values())
_out_port_ids = set(ex.source.uuid for ex in incs.values())
_port_spread = len(_out_port_ids) - len(_inc_port_ids)

functions = get_capella_order(comp, functions)
_functions = get_capella_order(_comp, _functions)
left_context["functions"] = get_capella_order(
comp, left_context["functions"]
)
right_context["functions"] = get_capella_order(
_comp, right_context["functions"]
)
if port_spread >= _port_spread:
self.left = comp
self.right = _comp
self.outgoing_edges = outs
self.incoming_edges = incs
left_functions = functions
right_functions = _functions
self.outgoing_edges = outs
else:
self.left = _comp
self.right = comp
self.outgoing_edges = incs
self.incoming_edges = outs
left_functions = _functions
right_functions = functions

make_boxes(self.left, left_functions)
make_boxes(self.right, right_functions)
self.outgoing_edges = incs
left_context, right_context = right_context, left_context

if left_child := make_boxes(left_context):
self.data["children"].append(left_child)
self.left = left_child
if right_child := make_boxes(right_context):
self.data["children"].append(right_child)
self.right = right_child
except AttributeError:
pass

def add_interface(self) -> None:
ex_data = generic.ExchangeData(
self.obj,
self.data,
self.diagram.filters,
self.params,
is_hierarchical=False,
)
src, tgt = generic.exchange_data_collector(ex_data)
assert self.right is not None
if self.get_source(self.obj).uuid == self.right["id"]:
self.data["edges"][-1]["sources"] = [tgt.uuid]
self.data["edges"][-1]["targets"] = [src.uuid]

assert self.left is not None
self.left.setdefault("ports", []).append(makers.make_port(src.uuid))
self.right.setdefault("ports", []).append(makers.make_port(tgt.uuid))

def collect(self) -> None:
"""Return all allocated `FunctionalExchange`s in the context."""
"""Collect all allocated `FunctionalExchange`s in the context."""
try:
for ex in self.incoming_edges + self.outgoing_edges:
for ex in (self.incoming_edges | self.outgoing_edges).values():
ex_data = generic.ExchangeData(
ex,
self.data,
Expand All @@ -251,7 +312,7 @@ def collect(self) -> None:
)
src, tgt = generic.exchange_data_collector(ex_data)

if ex in self.incoming_edges:
if ex in self.incoming_edges.values():
self.data["edges"][-1]["sources"] = [tgt.uuid]
self.data["edges"][-1]["targets"] = [src.uuid]

Expand Down Expand Up @@ -300,7 +361,7 @@ def collect(self) -> None:
made_children.add(comp.uuid)

all_functions.extend(functions)
functional_exchanges.extend(inc + outs)
functional_exchanges.extend(inc | outs)

self.data["children"][0]["children"] = [
makers.make_box(c)
Expand Down
9 changes: 8 additions & 1 deletion capellambse_context_diagrams/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,14 @@ class InterfaceContextDiagram(ContextDiagram):
``ComponentExchange``s.
"""

def __init__(self, class_: str, obj: common.GenericElement, **kw) -> None:
def __init__(
self,
class_: str,
obj: common.GenericElement,
include_interface: bool = False,
**kw,
) -> None:
self.include_interface = include_interface
super().__init__(class_, obj, **kw, display_symbols_as_boxes=True)

@property
Expand Down
2 changes: 1 addition & 1 deletion docs/data_flow_view.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The diagram elements are collected from the
diag.as_svgdiagram.save(pretty=True)
```
<figure markdown>
<img src="../assets/images/DatFlow view of Eat food.svg">
<img src="../assets/images/DataFlow view of Eat food.svg">
<figcaption>[OAIB] DataFlow View Diagram of Eat food</figcaption>
</figure>

Expand Down
52 changes: 29 additions & 23 deletions docs/gen_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import logging
import pathlib
import typing as t

import mkdocs_gen_files
from capellambse import MelodyModel, diagram
Expand All @@ -16,19 +17,23 @@
dest = pathlib.Path("assets") / "images"
model_path = pathlib.Path(__file__).parent.parent / "tests" / "data"
model = MelodyModel(path=model_path, entrypoint="ContextDiagram.aird")
general_context_diagram_uuids = {
"Environment": "e37510b9-3166-4f80-a919-dfaac9b696c7",
"Eat": "8bcb11e6-443b-4b92-bec2-ff1d87a224e7",
"Middle": "da08ddb6-92ba-4c3b-956a-017424dbfe85",
"Capability": "9390b7d5-598a-42db-bef8-23677e45ba06",
"Lost": "a5642060-c9cc-4d49-af09-defaa3024bae",
"Left": "f632888e-51bc-4c9f-8e81-73e9404de784",
"educate Wizards": "957c5799-1d4a-4ac0-b5de-33a65bf1519c",
"Weird guy": "098810d9-0325-4ae8-a111-82202c0d2016",
"Top secret": "5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6",
general_context_diagram_uuids: dict[str, tuple[str, dict[str, t.Any]]] = {
"Environment": ("e37510b9-3166-4f80-a919-dfaac9b696c7", {}),
"Eat": ("8bcb11e6-443b-4b92-bec2-ff1d87a224e7", {}),
"Middle": ("da08ddb6-92ba-4c3b-956a-017424dbfe85", {}),
"Capability": ("9390b7d5-598a-42db-bef8-23677e45ba06", {}),
"Lost": ("a5642060-c9cc-4d49-af09-defaa3024bae", {}),
"Left": ("f632888e-51bc-4c9f-8e81-73e9404de784", {}),
"educate Wizards": ("957c5799-1d4a-4ac0-b5de-33a65bf1519c", {}),
"Weird guy": ("098810d9-0325-4ae8-a111-82202c0d2016", {}),
"Top secret": ("5bf3f1e3-0f5e-4fec-81d5-c113d3a1b3a6", {}),
}
interface_context_diagram_uuids = {
"Left to right": "3ef23099-ce9a-4f7d-812f-935f47e7938d",
interface_context_diagram_uuids: dict[str, tuple[str, dict[str, t.Any]]] = {
"Left to right": ("3ef23099-ce9a-4f7d-812f-935f47e7938d", {}),
"Interface": (
"fbb7f735-3c1f-48de-9791-179d35ca7b98",
{"include_interface": True},
),
}
hierarchy_context = "16b4fcc5-548d-4721-b62a-d3d5b1c1d2eb"
diagram_uuids = general_context_diagram_uuids | interface_context_diagram_uuids
Expand All @@ -39,15 +44,16 @@


def generate_index_images() -> None:
for uuid in diagram_uuids.values():
for uuid, render_params in diagram_uuids.values():
diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram
with mkdocs_gen_files.open(f"{str(dest / diag.name)}.svg", "w") as fd:
print(diag.render("svg", transparent_background=False), file=fd)
render_params["transparent_background"] = False # type: ignore[index]
print(diag.render("svg", **render_params), file=fd) # type: ignore[arg-type]


def generate_no_symbol_images() -> None:
for name in ("Capability", "Middle"):
uuid = general_context_diagram_uuids[name]
uuid, _ = general_context_diagram_uuids[name]
diag: context.ContextDiagram = model.by_uuid(uuid).context_diagram
diag.display_symbols_as_boxes = True
diag.invalidate_cache()
Expand Down Expand Up @@ -157,23 +163,23 @@ def generate_data_flow_image() -> None:
generate_hierarchy_image()
generate_no_symbol_images()

wizard = general_context_diagram_uuids["educate Wizards"]
generate_no_edgelabel_image(wizard)
wizard_uuid = general_context_diagram_uuids["educate Wizards"][0]
generate_no_edgelabel_image(wizard_uuid)

lost = general_context_diagram_uuids["Lost"]
generate_filter_image(lost, filters.EX_ITEMS, "ex")
generate_filter_image(lost, filters.FEX_EX_ITEMS, "fex and ex")
generate_filter_image(lost, filters.FEX_OR_EX_ITEMS, "fex or ex")
lost_uuid = general_context_diagram_uuids["Lost"][0]
generate_filter_image(lost_uuid, filters.EX_ITEMS, "ex")
generate_filter_image(lost_uuid, filters.FEX_EX_ITEMS, "fex and ex")
generate_filter_image(lost_uuid, filters.FEX_OR_EX_ITEMS, "fex or ex")

generate_styling_image(
lost,
lost_uuid,
dict(
styling.BLUE_ACTOR_FNCS,
junction=lambda o, s: {"stroke": diagram.RGB(220, 20, 60)},
),
"red junction",
)
generate_styling_image(wizard, {}, "no_styles")
generate_styling_image(wizard_uuid, {}, "no_styles")
generate_class_tree_images()
generate_realization_view_images()
generate_data_flow_image()
Loading

0 comments on commit bf7a801

Please sign in to comment.