sysml2kit 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sysml2kit/__init__.py +14 -0
- sysml2kit/_version.py +24 -0
- sysml2kit/cli.py +26 -0
- sysml2kit/interchange/__init__.py +13 -0
- sysml2kit/interchange/reader.py +106 -0
- sysml2kit/interchange/typemap.py +62 -0
- sysml2kit/interchange/writer.py +73 -0
- sysml2kit/model/__init__.py +59 -0
- sysml2kit/model/analysis.py +17 -0
- sysml2kit/model/base.py +73 -0
- sysml2kit/model/builder.py +239 -0
- sysml2kit/model/container.py +220 -0
- sysml2kit/model/metadata.py +19 -0
- sysml2kit/model/relations.py +26 -0
- sysml2kit/model/requirements.py +23 -0
- sysml2kit/model/structure.py +60 -0
- sysml2kit/model/values.py +31 -0
- sysml2kit/py.typed +0 -0
- sysml2kit/units.py +60 -0
- sysml2kit-0.0.1.dist-info/METADATA +166 -0
- sysml2kit-0.0.1.dist-info/RECORD +25 -0
- sysml2kit-0.0.1.dist-info/WHEEL +4 -0
- sysml2kit-0.0.1.dist-info/entry_points.txt +2 -0
- sysml2kit-0.0.1.dist-info/licenses/LICENSE +202 -0
- sysml2kit-0.0.1.dist-info/licenses/NOTICE +10 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Fluent authoring helpers: the API humans and agents actually type.
|
|
2
|
+
|
|
3
|
+
Each helper constructs an element, registers it in the model under the given
|
|
4
|
+
owner, and returns it. The raw element classes stay the interchange-faithful
|
|
5
|
+
layer; nothing here adds state the classes lack.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from sysml2kit.model.analysis import AnalysisCaseDefinition, AnalysisCaseUsage
|
|
11
|
+
from sysml2kit.model.base import Element, Ref
|
|
12
|
+
from sysml2kit.model.container import Model
|
|
13
|
+
from sysml2kit.model.metadata import MetadataUsage
|
|
14
|
+
from sysml2kit.model.relations import (
|
|
15
|
+
AllocateRelationship,
|
|
16
|
+
DeriveRelationship,
|
|
17
|
+
SatisfyRelationship,
|
|
18
|
+
VerifyRelationship,
|
|
19
|
+
)
|
|
20
|
+
from sysml2kit.model.requirements import RequirementDefinition, RequirementUsage
|
|
21
|
+
from sysml2kit.model.structure import (
|
|
22
|
+
AttributeDefinition,
|
|
23
|
+
AttributeUsage,
|
|
24
|
+
ConnectionUsage,
|
|
25
|
+
Package,
|
|
26
|
+
PartDefinition,
|
|
27
|
+
PartUsage,
|
|
28
|
+
PortDefinition,
|
|
29
|
+
PortUsage,
|
|
30
|
+
)
|
|
31
|
+
from sysml2kit.model.values import AttributeValue
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def pkg(
|
|
35
|
+
model: Model, name: str, *, owner: Element | None = None, doc: str | None = None
|
|
36
|
+
) -> Package:
|
|
37
|
+
"""Create a package."""
|
|
38
|
+
return model.add(Package(declared_name=name, doc=doc), owner=owner) # type: ignore[return-value]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def part_def(
|
|
42
|
+
model: Model, name: str, *, owner: Element | None = None, doc: str | None = None
|
|
43
|
+
) -> PartDefinition:
|
|
44
|
+
"""Create a part definition."""
|
|
45
|
+
return model.add(PartDefinition(declared_name=name, doc=doc), owner=owner) # type: ignore[return-value]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def part(
|
|
49
|
+
model: Model,
|
|
50
|
+
name: str,
|
|
51
|
+
*,
|
|
52
|
+
owner: Element | None = None,
|
|
53
|
+
definition: Element | None = None,
|
|
54
|
+
multiplicity: str | None = None,
|
|
55
|
+
doc: str | None = None,
|
|
56
|
+
) -> PartUsage:
|
|
57
|
+
"""Create a part usage, optionally typed by a part definition."""
|
|
58
|
+
usage = PartUsage(
|
|
59
|
+
declared_name=name,
|
|
60
|
+
definition=Ref.to(definition) if definition else None,
|
|
61
|
+
multiplicity=multiplicity,
|
|
62
|
+
doc=doc,
|
|
63
|
+
)
|
|
64
|
+
return model.add(usage, owner=owner) # type: ignore[return-value]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def port_def(
|
|
68
|
+
model: Model, name: str, *, owner: Element | None = None, doc: str | None = None
|
|
69
|
+
) -> PortDefinition:
|
|
70
|
+
"""Create a port definition."""
|
|
71
|
+
return model.add(PortDefinition(declared_name=name, doc=doc), owner=owner) # type: ignore[return-value]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def port(
|
|
75
|
+
model: Model,
|
|
76
|
+
name: str,
|
|
77
|
+
*,
|
|
78
|
+
owner: Element | None = None,
|
|
79
|
+
definition: Element | None = None,
|
|
80
|
+
) -> PortUsage:
|
|
81
|
+
"""Create a port usage on a part."""
|
|
82
|
+
usage = PortUsage(
|
|
83
|
+
declared_name=name,
|
|
84
|
+
definition=Ref.to(definition) if definition else None,
|
|
85
|
+
)
|
|
86
|
+
return model.add(usage, owner=owner) # type: ignore[return-value]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def connect(
|
|
90
|
+
model: Model,
|
|
91
|
+
source: Element,
|
|
92
|
+
target: Element,
|
|
93
|
+
*,
|
|
94
|
+
owner: Element | None = None,
|
|
95
|
+
name: str | None = None,
|
|
96
|
+
) -> ConnectionUsage:
|
|
97
|
+
"""Create a connection between two ports (or parts)."""
|
|
98
|
+
usage = ConnectionUsage(declared_name=name, source=Ref.to(source), target=Ref.to(target))
|
|
99
|
+
return model.add(usage, owner=owner) # type: ignore[return-value]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def attr_def(
|
|
103
|
+
model: Model,
|
|
104
|
+
name: str,
|
|
105
|
+
*,
|
|
106
|
+
owner: Element | None = None,
|
|
107
|
+
unit: str | None = None,
|
|
108
|
+
doc: str | None = None,
|
|
109
|
+
) -> AttributeDefinition:
|
|
110
|
+
"""Create an attribute definition, optionally with a default unit."""
|
|
111
|
+
return model.add( # type: ignore[return-value]
|
|
112
|
+
AttributeDefinition(declared_name=name, unit=unit, doc=doc), owner=owner
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def attr(
|
|
117
|
+
model: Model,
|
|
118
|
+
name: str,
|
|
119
|
+
value: float | str | bool | None = None,
|
|
120
|
+
*,
|
|
121
|
+
owner: Element | None = None,
|
|
122
|
+
unit: str | None = None,
|
|
123
|
+
definition: Element | None = None,
|
|
124
|
+
source: str | None = None,
|
|
125
|
+
) -> AttributeUsage:
|
|
126
|
+
"""Create an attribute usage holding a value with optional unit and provenance."""
|
|
127
|
+
usage = AttributeUsage(
|
|
128
|
+
declared_name=name,
|
|
129
|
+
definition=Ref.to(definition) if definition else None,
|
|
130
|
+
value=AttributeValue(value=value, unit=unit, source=source) if value is not None else None,
|
|
131
|
+
)
|
|
132
|
+
return model.add(usage, owner=owner) # type: ignore[return-value]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def req_def(
|
|
136
|
+
model: Model, name: str, *, owner: Element | None = None, doc: str | None = None
|
|
137
|
+
) -> RequirementDefinition:
|
|
138
|
+
"""Create a requirement definition."""
|
|
139
|
+
return model.add(RequirementDefinition(declared_name=name, doc=doc), owner=owner) # type: ignore[return-value]
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def req(
|
|
143
|
+
model: Model,
|
|
144
|
+
short_name: str,
|
|
145
|
+
name: str,
|
|
146
|
+
*,
|
|
147
|
+
owner: Element | None = None,
|
|
148
|
+
text: str | None = None,
|
|
149
|
+
subject: Element | None = None,
|
|
150
|
+
definition: Element | None = None,
|
|
151
|
+
) -> RequirementUsage:
|
|
152
|
+
"""Create a requirement usage; ``short_name`` is the requirement id (e.g. REQ-001)."""
|
|
153
|
+
usage = RequirementUsage(
|
|
154
|
+
declared_short_name=short_name,
|
|
155
|
+
declared_name=name,
|
|
156
|
+
text=text,
|
|
157
|
+
subject=Ref.to(subject) if subject else None,
|
|
158
|
+
definition=Ref.to(definition) if definition else None,
|
|
159
|
+
)
|
|
160
|
+
return model.add(usage, owner=owner) # type: ignore[return-value]
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def analysis_def(
|
|
164
|
+
model: Model, name: str, *, owner: Element | None = None, doc: str | None = None
|
|
165
|
+
) -> AnalysisCaseDefinition:
|
|
166
|
+
"""Create an analysis case definition."""
|
|
167
|
+
return model.add(AnalysisCaseDefinition(declared_name=name, doc=doc), owner=owner) # type: ignore[return-value]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def analysis(
|
|
171
|
+
model: Model,
|
|
172
|
+
name: str,
|
|
173
|
+
*,
|
|
174
|
+
owner: Element | None = None,
|
|
175
|
+
subject: Element | None = None,
|
|
176
|
+
objective: str | None = None,
|
|
177
|
+
definition: Element | None = None,
|
|
178
|
+
) -> AnalysisCaseUsage:
|
|
179
|
+
"""Create an analysis case usage."""
|
|
180
|
+
usage = AnalysisCaseUsage(
|
|
181
|
+
declared_name=name,
|
|
182
|
+
subject=Ref.to(subject) if subject else None,
|
|
183
|
+
objective=objective,
|
|
184
|
+
definition=Ref.to(definition) if definition else None,
|
|
185
|
+
)
|
|
186
|
+
return model.add(usage, owner=owner) # type: ignore[return-value]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def metadata(
|
|
190
|
+
model: Model,
|
|
191
|
+
annotated: Element,
|
|
192
|
+
values: dict[str, str | float | int | bool],
|
|
193
|
+
*,
|
|
194
|
+
owner: Element | None = None,
|
|
195
|
+
name: str | None = None,
|
|
196
|
+
) -> MetadataUsage:
|
|
197
|
+
"""Attach a key-value metadata annotation to an element."""
|
|
198
|
+
usage = MetadataUsage(declared_name=name, annotated=Ref.to(annotated), values=dict(values))
|
|
199
|
+
return model.add(usage, owner=owner if owner is not None else annotated) # type: ignore[return-value]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _relate(
|
|
203
|
+
model: Model,
|
|
204
|
+
cls: type[SatisfyRelationship | VerifyRelationship | DeriveRelationship | AllocateRelationship],
|
|
205
|
+
source: Element,
|
|
206
|
+
target: Element,
|
|
207
|
+
owner: Element | None,
|
|
208
|
+
) -> Element:
|
|
209
|
+
rel = cls(source=Ref.to(source), target=Ref.to(target))
|
|
210
|
+
fallback = model.owner_of(source)
|
|
211
|
+
return model.add(rel, owner=owner if owner is not None else fallback)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def satisfy(
|
|
215
|
+
model: Model, *, source: Element, target: Element, owner: Element | None = None
|
|
216
|
+
) -> SatisfyRelationship:
|
|
217
|
+
"""Record that ``source`` (a design element) satisfies ``target`` (a requirement)."""
|
|
218
|
+
return _relate(model, SatisfyRelationship, source, target, owner) # type: ignore[return-value]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def verify(
|
|
222
|
+
model: Model, *, source: Element, target: Element, owner: Element | None = None
|
|
223
|
+
) -> VerifyRelationship:
|
|
224
|
+
"""Record that ``source`` (an analysis/test) verifies ``target`` (a requirement)."""
|
|
225
|
+
return _relate(model, VerifyRelationship, source, target, owner) # type: ignore[return-value]
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def derive(
|
|
229
|
+
model: Model, *, source: Element, target: Element, owner: Element | None = None
|
|
230
|
+
) -> DeriveRelationship:
|
|
231
|
+
"""Record that requirement ``source`` derives from requirement ``target``."""
|
|
232
|
+
return _relate(model, DeriveRelationship, source, target, owner) # type: ignore[return-value]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def allocate(
|
|
236
|
+
model: Model, *, source: Element, target: Element, owner: Element | None = None
|
|
237
|
+
) -> AllocateRelationship:
|
|
238
|
+
"""Record that ``source`` is allocated to ``target`` (a part)."""
|
|
239
|
+
return _relate(model, AllocateRelationship, source, target, owner) # type: ignore[return-value]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""The Model container: element registry, identity, and ownership.
|
|
2
|
+
|
|
3
|
+
Ownership lives here (owner/owned maps keyed by element id), not on the
|
|
4
|
+
elements, mirroring how the Systems Modeling API keeps owning-relationship
|
|
5
|
+
records separate from element payloads.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from collections.abc import Iterator, Sequence
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
from sysml2kit.model.base import Element, Ref, Relationship
|
|
15
|
+
|
|
16
|
+
#: Namespace for stable UUIDv5 ids derived from qualified names.
|
|
17
|
+
STABLE_ID_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_DNS, "sysml2kit")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Model:
|
|
21
|
+
"""A set of elements with identity, ownership, and lookup."""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self.elements: dict[UUID, Element] = {}
|
|
25
|
+
self.owner: dict[UUID, UUID] = {}
|
|
26
|
+
self.owned: dict[UUID, list[UUID]] = {}
|
|
27
|
+
self.roots: list[UUID] = []
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------- mutation
|
|
30
|
+
def add(self, element: Element, owner: Element | UUID | None = None) -> Element:
|
|
31
|
+
"""Register an element, optionally under an owner already in the model."""
|
|
32
|
+
eid = element.element_id
|
|
33
|
+
if eid in self.elements:
|
|
34
|
+
raise ValueError(f"duplicate element id {eid}")
|
|
35
|
+
self.elements[eid] = element
|
|
36
|
+
if owner is None:
|
|
37
|
+
self.roots.append(eid)
|
|
38
|
+
else:
|
|
39
|
+
oid = owner if isinstance(owner, UUID) else owner.element_id
|
|
40
|
+
if oid not in self.elements:
|
|
41
|
+
raise KeyError(f"owner {oid} is not in the model")
|
|
42
|
+
self.owner[eid] = oid
|
|
43
|
+
self.owned.setdefault(oid, []).append(eid)
|
|
44
|
+
return element
|
|
45
|
+
|
|
46
|
+
def remove(self, element: Element | UUID) -> None:
|
|
47
|
+
"""Remove an element and reparent nothing: its owned elements become roots."""
|
|
48
|
+
eid = element if isinstance(element, UUID) else element.element_id
|
|
49
|
+
if eid not in self.elements:
|
|
50
|
+
raise KeyError(f"element {eid} is not in the model")
|
|
51
|
+
for child in self.owned.pop(eid, []):
|
|
52
|
+
del self.owner[child]
|
|
53
|
+
self.roots.append(child)
|
|
54
|
+
oid = self.owner.pop(eid, None)
|
|
55
|
+
if oid is None:
|
|
56
|
+
self.roots.remove(eid)
|
|
57
|
+
else:
|
|
58
|
+
self.owned[oid].remove(eid)
|
|
59
|
+
del self.elements[eid]
|
|
60
|
+
|
|
61
|
+
# --------------------------------------------------------------- lookup
|
|
62
|
+
def resolve(self, ref: Ref | UUID) -> Element:
|
|
63
|
+
"""Return the element a ref (or id) points at."""
|
|
64
|
+
eid = ref.target if isinstance(ref, Ref) else ref
|
|
65
|
+
return self.elements[eid]
|
|
66
|
+
|
|
67
|
+
def owner_of(self, element: Element | UUID) -> Element | None:
|
|
68
|
+
"""Return the owning element, or None for a root."""
|
|
69
|
+
eid = element if isinstance(element, UUID) else element.element_id
|
|
70
|
+
oid = self.owner.get(eid)
|
|
71
|
+
return self.elements[oid] if oid is not None else None
|
|
72
|
+
|
|
73
|
+
def owned_by(self, element: Element | UUID) -> list[Element]:
|
|
74
|
+
"""Return the owned elements, in insertion order."""
|
|
75
|
+
eid = element if isinstance(element, UUID) else element.element_id
|
|
76
|
+
return [self.elements[cid] for cid in self.owned.get(eid, [])]
|
|
77
|
+
|
|
78
|
+
def qualified_name(self, element: Element | UUID) -> str:
|
|
79
|
+
"""Return the ``::``-joined name path from the root to this element.
|
|
80
|
+
|
|
81
|
+
Unnamed elements contribute their kind and a positional index, so the
|
|
82
|
+
path is always defined (and usable for stable-id hashing).
|
|
83
|
+
"""
|
|
84
|
+
eid = element if isinstance(element, UUID) else element.element_id
|
|
85
|
+
parts: list[str] = []
|
|
86
|
+
current: UUID | None = eid
|
|
87
|
+
while current is not None:
|
|
88
|
+
el = self.elements[current]
|
|
89
|
+
parts.append(el.declared_name or self._positional_name(current))
|
|
90
|
+
current = self.owner.get(current)
|
|
91
|
+
return "::".join(reversed(parts))
|
|
92
|
+
|
|
93
|
+
def _positional_name(self, eid: UUID) -> str:
|
|
94
|
+
el = self.elements[eid]
|
|
95
|
+
oid = self.owner.get(eid)
|
|
96
|
+
siblings = self.owned.get(oid, []) if oid is not None else self.roots
|
|
97
|
+
index = siblings.index(eid)
|
|
98
|
+
return f"{type(el).__name__}#{index}"
|
|
99
|
+
|
|
100
|
+
def find(
|
|
101
|
+
self,
|
|
102
|
+
*,
|
|
103
|
+
name: str | None = None,
|
|
104
|
+
kind: type[Element] | None = None,
|
|
105
|
+
) -> list[Element]:
|
|
106
|
+
"""Return elements matching a declared name and/or a class."""
|
|
107
|
+
out: list[Element] = []
|
|
108
|
+
for el in self.iter_elements(kind=kind):
|
|
109
|
+
if name is not None and el.declared_name != name:
|
|
110
|
+
continue
|
|
111
|
+
out.append(el)
|
|
112
|
+
return out
|
|
113
|
+
|
|
114
|
+
def find_by_qualified_name(self, qualified: str) -> Element | None:
|
|
115
|
+
"""Return the element with this exact qualified name, if any."""
|
|
116
|
+
for eid in self.elements:
|
|
117
|
+
if self.qualified_name(eid) == qualified:
|
|
118
|
+
return self.elements[eid]
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def iter_elements(self, *, kind: type[Element] | None = None) -> Iterator[Element]:
|
|
122
|
+
"""Iterate elements in ownership (depth-first) order."""
|
|
123
|
+
for eid in self._walk():
|
|
124
|
+
el = self.elements[eid]
|
|
125
|
+
if kind is None or isinstance(el, kind):
|
|
126
|
+
yield el
|
|
127
|
+
|
|
128
|
+
def _walk(self) -> Iterator[UUID]:
|
|
129
|
+
stack = list(reversed(self.roots))
|
|
130
|
+
while stack:
|
|
131
|
+
eid = stack.pop()
|
|
132
|
+
yield eid
|
|
133
|
+
stack.extend(reversed(self.owned.get(eid, [])))
|
|
134
|
+
|
|
135
|
+
def relationships(
|
|
136
|
+
self,
|
|
137
|
+
*,
|
|
138
|
+
kind: type[Relationship] | None = None,
|
|
139
|
+
source: Element | UUID | None = None,
|
|
140
|
+
target: Element | UUID | None = None,
|
|
141
|
+
) -> list[Relationship]:
|
|
142
|
+
"""Return relationships filtered by class and/or endpoint."""
|
|
143
|
+
sid = source.element_id if isinstance(source, Element) else source
|
|
144
|
+
tid = target.element_id if isinstance(target, Element) else target
|
|
145
|
+
out: list[Relationship] = []
|
|
146
|
+
for el in self.elements.values():
|
|
147
|
+
if not isinstance(el, Relationship):
|
|
148
|
+
continue
|
|
149
|
+
if kind is not None and not isinstance(el, kind):
|
|
150
|
+
continue
|
|
151
|
+
if sid is not None and el.source.target != sid:
|
|
152
|
+
continue
|
|
153
|
+
if tid is not None and el.target.target != tid:
|
|
154
|
+
continue
|
|
155
|
+
out.append(el)
|
|
156
|
+
return out
|
|
157
|
+
|
|
158
|
+
# ------------------------------------------------------------ integrity
|
|
159
|
+
def check_refs(self) -> list[tuple[UUID, str, UUID]]:
|
|
160
|
+
"""Return (element_id, field_name, missing_target) for dangling refs."""
|
|
161
|
+
dangling: list[tuple[UUID, str, UUID]] = []
|
|
162
|
+
for el in self.elements.values():
|
|
163
|
+
for field, ref in self._refs_of(el):
|
|
164
|
+
if ref.target not in self.elements:
|
|
165
|
+
dangling.append((el.element_id, field, ref.target))
|
|
166
|
+
return dangling
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _refs_of(element: Element) -> list[tuple[str, Ref]]:
|
|
170
|
+
refs: list[tuple[str, Ref]] = []
|
|
171
|
+
for field in type(element).model_fields:
|
|
172
|
+
value = getattr(element, field)
|
|
173
|
+
if isinstance(value, Ref):
|
|
174
|
+
refs.append((field, value))
|
|
175
|
+
elif isinstance(value, Sequence) and not isinstance(value, str | bytes):
|
|
176
|
+
refs.extend((field, item) for item in value if isinstance(item, Ref))
|
|
177
|
+
return refs
|
|
178
|
+
|
|
179
|
+
def assign_stable_ids(self) -> dict[UUID, UUID]:
|
|
180
|
+
"""Rewrite every element id as a UUIDv5 hash of its qualified name.
|
|
181
|
+
|
|
182
|
+
Returns the old-to-new id mapping. Refs, ownership maps, and roots are
|
|
183
|
+
remapped in place. Run this before committing generated interchange
|
|
184
|
+
files so regeneration produces stable diffs.
|
|
185
|
+
"""
|
|
186
|
+
mapping = {
|
|
187
|
+
eid: uuid.uuid5(STABLE_ID_NAMESPACE, self.qualified_name(eid)) for eid in self.elements
|
|
188
|
+
}
|
|
189
|
+
if len(set(mapping.values())) != len(mapping):
|
|
190
|
+
raise ValueError(
|
|
191
|
+
"duplicate qualified names; stable ids need unique name paths "
|
|
192
|
+
"(rename the clashing siblings, see validation rule S2K003)"
|
|
193
|
+
)
|
|
194
|
+
new_elements: dict[UUID, Element] = {}
|
|
195
|
+
for eid, el in self.elements.items():
|
|
196
|
+
updates: dict[str, object] = {"element_id": mapping[eid]}
|
|
197
|
+
for field, ref in self._refs_of(el):
|
|
198
|
+
value = getattr(el, field)
|
|
199
|
+
if isinstance(value, Ref):
|
|
200
|
+
updates[field] = Ref(target=mapping.get(ref.target, ref.target))
|
|
201
|
+
for field in type(el).model_fields:
|
|
202
|
+
value = getattr(el, field)
|
|
203
|
+
if (
|
|
204
|
+
isinstance(value, Sequence)
|
|
205
|
+
and not isinstance(value, str | bytes)
|
|
206
|
+
and any(isinstance(item, Ref) for item in value)
|
|
207
|
+
):
|
|
208
|
+
updates[field] = [
|
|
209
|
+
Ref(target=mapping.get(item.target, item.target))
|
|
210
|
+
if isinstance(item, Ref)
|
|
211
|
+
else item
|
|
212
|
+
for item in value
|
|
213
|
+
]
|
|
214
|
+
new_el = el.model_copy(update=updates)
|
|
215
|
+
new_elements[new_el.element_id] = new_el
|
|
216
|
+
self.elements = new_elements
|
|
217
|
+
self.owner = {mapping[k]: mapping[v] for k, v in self.owner.items()}
|
|
218
|
+
self.owned = {mapping[k]: [mapping[c] for c in v] for k, v in self.owned.items()}
|
|
219
|
+
self.roots = [mapping[r] for r in self.roots]
|
|
220
|
+
return mapping
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Metadata annotation elements."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from sysml2kit.model.base import Element, Ref
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MetadataDefinition(Element):
|
|
11
|
+
"""A reusable definition of a metadata annotation kind."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MetadataUsage(Element):
|
|
15
|
+
"""A metadata annotation on another element, as a key-value mapping."""
|
|
16
|
+
|
|
17
|
+
definition: Ref | None = None
|
|
18
|
+
annotated: Ref | None = None
|
|
19
|
+
values: dict[str, str | float | int | bool] = Field(default_factory=dict)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Reified traceability relationships.
|
|
2
|
+
|
|
3
|
+
The spec models some of these as membership forms; sysml2kit reifies each as
|
|
4
|
+
a first-class relationship with ``source``/``target`` refs (a documented
|
|
5
|
+
deviation, see SPEC.md).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from sysml2kit.model.base import Relationship
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SatisfyRelationship(Relationship):
|
|
14
|
+
"""``source`` (a part or design element) satisfies ``target`` (a requirement)."""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VerifyRelationship(Relationship):
|
|
18
|
+
"""``source`` (an analysis or test case) verifies ``target`` (a requirement)."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DeriveRelationship(Relationship):
|
|
22
|
+
"""``source`` (a requirement) is derived from ``target`` (a requirement)."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AllocateRelationship(Relationship):
|
|
26
|
+
"""``source`` (a function or requirement) is allocated to ``target`` (a part)."""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Requirement and constraint elements."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from sysml2kit.model.base import Element, Ref
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RequirementDefinition(Element):
|
|
9
|
+
"""A reusable definition of a requirement kind."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RequirementUsage(Element):
|
|
13
|
+
"""A requirement occurrence with an optional subject and statement text."""
|
|
14
|
+
|
|
15
|
+
definition: Ref | None = None
|
|
16
|
+
subject: Ref | None = None
|
|
17
|
+
text: str | None = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConstraintUsage(Element):
|
|
21
|
+
"""A constraint; the expression is an opaque string in v0.1."""
|
|
22
|
+
|
|
23
|
+
expression: str | None = None
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Structural elements: packages, parts, ports, interfaces, connections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
from sysml2kit.model.base import Element, Ref
|
|
8
|
+
from sysml2kit.model.values import AttributeValue
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Package(Element):
|
|
12
|
+
"""A namespace for other elements; maps to a top-level ``.sysml`` file."""
|
|
13
|
+
|
|
14
|
+
imports: list[str] = Field(default_factory=list)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PartDefinition(Element):
|
|
18
|
+
"""A reusable definition of a system component kind."""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PartUsage(Element):
|
|
22
|
+
"""A component occurrence, optionally typed by a :class:`PartDefinition`."""
|
|
23
|
+
|
|
24
|
+
definition: Ref | None = None
|
|
25
|
+
multiplicity: str | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PortDefinition(Element):
|
|
29
|
+
"""A reusable definition of an interaction point kind."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PortUsage(Element):
|
|
33
|
+
"""A port occurrence on a part, optionally typed by a :class:`PortDefinition`."""
|
|
34
|
+
|
|
35
|
+
definition: Ref | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InterfaceDefinition(Element):
|
|
39
|
+
"""A definition of how two ports connect."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ConnectionUsage(Element):
|
|
43
|
+
"""A connection between two port (or part) ends."""
|
|
44
|
+
|
|
45
|
+
definition: Ref | None = None
|
|
46
|
+
source: Ref | None = None
|
|
47
|
+
target: Ref | None = None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AttributeDefinition(Element):
|
|
51
|
+
"""A reusable definition of a value kind, e.g. a quantity with a unit."""
|
|
52
|
+
|
|
53
|
+
unit: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AttributeUsage(Element):
|
|
57
|
+
"""A value occurrence, optionally typed and optionally holding a value."""
|
|
58
|
+
|
|
59
|
+
definition: Ref | None = None
|
|
60
|
+
value: AttributeValue | None = None
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Attribute values with units and provenance.
|
|
2
|
+
|
|
3
|
+
Follows the ``Assumption`` pattern from spacedc-mdao: a number in a model
|
|
4
|
+
should say where it came from. Units are stored as text (what the textual
|
|
5
|
+
notation carries, e.g. ``"dBW"``); ``sysml2kit.units`` checks them against
|
|
6
|
+
pint during validation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Literal
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AttributeValue(BaseModel):
|
|
17
|
+
"""A literal value with optional unit text and provenance."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(frozen=True)
|
|
20
|
+
|
|
21
|
+
value: float | int | str | bool | None = None
|
|
22
|
+
unit: str | None = None
|
|
23
|
+
source: str | None = None
|
|
24
|
+
confidence: Literal["low", "medium", "high"] | None = None
|
|
25
|
+
|
|
26
|
+
def render(self) -> str:
|
|
27
|
+
"""Render as textual-notation value text, e.g. ``52.0 [dBW]``."""
|
|
28
|
+
body = f'"{self.value}"' if isinstance(self.value, str) else str(self.value)
|
|
29
|
+
if self.unit:
|
|
30
|
+
return f"{body} [{self.unit}]"
|
|
31
|
+
return body
|
sysml2kit/py.typed
ADDED
|
File without changes
|
sysml2kit/units.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Unit-string helpers backed by pint.
|
|
2
|
+
|
|
3
|
+
Models store units as text (round-trip fidelity with the textual notation);
|
|
4
|
+
these helpers check and convert them. A few decibel spellings common in
|
|
5
|
+
engineering practice are registered on top of pint's defaults.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
import pint
|
|
14
|
+
|
|
15
|
+
_EXTRA_DEFINITIONS = (
|
|
16
|
+
# Referenced-quantity decibels: dimensionally plain decibels; the reference
|
|
17
|
+
# (isotropic antenna, dipole, carrier, per-kelvin) is bookkeeping the model
|
|
18
|
+
# keeps in the unit text.
|
|
19
|
+
"dBi = decibel",
|
|
20
|
+
"dBd = decibel",
|
|
21
|
+
"dBc = decibel",
|
|
22
|
+
"dBK = decibel",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=1)
|
|
27
|
+
def registry() -> Any:
|
|
28
|
+
"""Return the shared pint unit registry (created on first use)."""
|
|
29
|
+
reg: Any = pint.UnitRegistry()
|
|
30
|
+
for definition in _EXTRA_DEFINITIONS:
|
|
31
|
+
reg.define(definition)
|
|
32
|
+
return reg
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_unit(text: str) -> Any:
|
|
36
|
+
"""Parse unit text into a pint unit; raises ValueError on unknown units."""
|
|
37
|
+
try:
|
|
38
|
+
return registry().Unit(text)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
raise ValueError(f"unparseable unit {text!r}: {exc}") from exc
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def is_valid_unit(text: str) -> bool:
|
|
44
|
+
"""Return whether pint can parse this unit text."""
|
|
45
|
+
try:
|
|
46
|
+
parse_unit(text)
|
|
47
|
+
except ValueError:
|
|
48
|
+
return False
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def convert(value: float, from_unit: str, to_unit: str) -> float:
|
|
53
|
+
"""Convert a value between two unit texts."""
|
|
54
|
+
quantity = registry().Quantity(value, parse_unit(from_unit))
|
|
55
|
+
return float(quantity.to(parse_unit(to_unit)).magnitude)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def check_dimensionality(unit_a: str, unit_b: str) -> bool:
|
|
59
|
+
"""Return whether two unit texts share a dimensionality."""
|
|
60
|
+
return bool(parse_unit(unit_a).dimensionality == parse_unit(unit_b).dimensionality)
|