patent2sysml / sysml_export.py
cmuchancel's picture
Agents author reviewed SJS before GradResearch and portable SysML translation
033aa66 verified
Raw History Blame Contribute Delete
11.7 kB
"""Translate SJS with pinned GradResearch code, then lower its custom profile.
The untouched @sjs profile stays in the research archive. The portable download
uses actual components, typed ports and flows; descriptive behavior remains docs.
"""
import json
from sjs import translator
FIELDS = {
'root': {'$schema', 'model_meta', 'subsystems', 'item_flows', 'requirements', 'behaviour', 'allocations'},
'model_meta': {'system_name', 'system_id', 'description', 'version', 'lifecycle_stage', 'domain', 'maturity', 'authors', 'date_modified', 'context'},
'subsystems': {'subsystem_id', 'subsystem_name', 'sysml_equivalent', 'functional_basis', 'description', 'domain', 'maturity', 'parts', 'ports', 'interfaces', 'notes'},
'ports': {'port_id', 'name', 'direction', 'flow_type', 'connector_type', 'notes'},
'parts': {'part_id', 'description', 'quantity', 'notes'},
'interfaces': {'interface_id', 'mating_subsystem', 'port_this', 'port_mate', 'flow_ref', 'interface_type', 'notes'},
'item_flows': {'flow_id', 'name', 'flow_type', 'notes'},
'requirements': {'req_id', 'category', 'priority', 'statement', 'rationale', 'satisfied_by', 'notes'},
'behaviour': {'actions', 'state_machines'},
'actions': {'action_id', 'name', 'owner', 'steps', 'notes'},
'steps': {'step_no', 'description'},
'state_machines': {'sm_id', 'name', 'owner', 'entry_state', 'states', 'notes'},
'states': {'name', 'description', 'entry_action', 'exit_action', 'transitions', 'notes'},
'transitions': {'name', 'trigger', 'guard', 'action', 'target'},
'allocations': {'allocation_id', 'type', 'from', 'to', 'rationale', 'notes'},
}
def validate_model(model):
errors = []
if not isinstance(model, dict):
return ['SJS must be an object.']
def fields(value, kind):
if not isinstance(value, dict):
raise ValueError(f'{kind} must contain objects.')
unknown = set(value) - FIELDS[kind]
if unknown:
errors.append(f'Unsupported {kind} fields: {sorted(unknown)}; omit rather than silently lose them.')
for key, child in value.items():
if key in FIELDS:
if key in {'model_meta', 'behaviour'}:
fields(child, key)
else:
if not isinstance(child, list):
raise ValueError(f'{key} must be an array.')
for entry in child:
fields(entry, key)
try:
fields(model, 'root')
if model.get('$schema') != 'sjs/1.2':
errors.append('Use $schema sjs/1.2.')
errors.extend(translator().validate(model))
if not model.get('model_meta', {}).get('date_modified'):
errors.append('model_meta.date_modified is required for a stable GradResearch round-trip.')
if not model.get('subsystems'):
errors.append('At least one supported subsystem is required.')
from quality import model_items
ids = set()
for path, item in model_items(model):
for key, value in item.items():
if key.endswith('_id'):
if not isinstance(value, str) or not value or value in ids:
errors.append(f'Unique nonempty ID required at {path}/{key}.')
else:
ids.add(value)
systems = {s['subsystem_id']: s for s in model['subsystems']}
flows = {f['flow_id']: f for f in model.get('item_flows', [])}
actions = {a['action_id']: a for a in model.get('behaviour', {}).get('actions', [])}
for subsystem in systems.values():
for key in ['subsystem_name', 'description', 'domain']:
if not isinstance(subsystem.get(key), str) or not subsystem[key].strip():
errors.append(f'{subsystem["subsystem_id"]} requires {key}.')
if subsystem.get('sysml_equivalent', 'part def') != 'part def':
errors.append('Use part def subsystems; the host creates their assembly usages.')
ports = {p['port_id']: p for p in subsystem.get('ports', [])}
for p in ports.values():
if p.get('direction') not in {'in', 'out', 'inout'} or not p.get('name') or not isinstance(p.get('flow_type'), str) or not p['flow_type']:
errors.append(f'Port {p["port_id"]} requires name, direction and flow_type.')
for part in subsystem.get('parts', []):
if type(part.get('quantity')) is not int or part['quantity'] <= 0:
errors.append('Part quantity must be a positive integer.')
for interface in subsystem.get('interfaces', []):
mate = systems.get(interface.get('mating_subsystem'), {})
remote = {p['port_id']: p for p in mate.get('ports', [])}
a, b = ports.get(interface.get('port_this')), remote.get(interface.get('port_mate'))
flow = flows.get(interface.get('flow_ref'))
if not a or not b or not flow:
errors.append(f'Interface {interface.get("interface_id")} has a missing subsystem, port or flow reference.')
elif a['flow_type'] != b['flow_type'] or a['flow_type'] != flow.get('flow_type'):
errors.append(f'Interface {interface["interface_id"]} connects incompatible flow types.')
elif (a['direction'], b['direction']) in {('in','in'), ('out','out')}:
errors.append(f'Interface {interface["interface_id"]} has incompatible port directions.')
for flow in flows.values():
if not flow.get('name') or not isinstance(flow.get('flow_type'), str) or not flow['flow_type']:
errors.append('Item flows require name and flow_type.')
for kind in ['actions', 'state_machines']:
for item in model.get('behaviour', {}).get(kind, []):
if item.get('owner') not in systems or not item.get('name'):
errors.append(f'{kind} requires a name and a valid subsystem owner.')
if kind == 'state_machines':
states = item.get('states', [])
names = [s.get('name') for s in states]
if not names or any(not isinstance(n, str) or not n for n in names) or len(set(names)) != len(names) or item.get('entry_state') not in names:
errors.append('State machine requires unique named states and an existing entry_state.')
for state in states:
if any(t.get('target') not in names for t in state.get('transitions', [])):
errors.append('State transition target is missing.')
for allocation in model.get('allocations', []):
if allocation.get('from') not in actions or allocation.get('to') not in systems:
errors.append('Allocation must reference an existing action and subsystem.')
for requirement in model.get('requirements', []):
if not requirement.get('statement') or not set(requirement.get('satisfied_by', [])) <= systems.keys():
errors.append('Requirement needs a statement and existing satisfied_by subsystem IDs.')
except (KeyError, TypeError, ValueError) as error:
errors.append(str(error))
return errors
def note(value):
return '/* ' + json.dumps(value, ensure_ascii=False).replace('*/', '* /') + ' */'
def translate(model):
errors = validate_model(model)
if errors:
raise ValueError('; '.join(errors))
native = translator()
profile = native.sysml_from_sjs(model)
restored = native.sjs_from_sysml(profile)
if restored != native.canonicalize(model):
raise ValueError('GradResearch SJS round-trip changed the model.')
return profile, portable_sysml(restored)
def to_sysml(model):
return translate(model)[1]
def portable_sysml(model):
systems = model['subsystems']
indices = {s['subsystem_id']: i for i, s in enumerate(systems, 1)}
types = sorted({p['flow_type'] for s in systems for p in s.get('ports', [])} |
{f['flow_type'] for f in model.get('item_flows', [])})
type_names = {t: f'FlowType{i}' for i, t in enumerate(types, 1)}
ports = {(s['subsystem_id'], p['port_id']): (j, p) for s in systems for j, p in enumerate(s.get('ports', []), 1)}
lines = ['package PatentModel {', ' doc ' + note(model['model_meta'])]
for t, name in type_names.items():
lines.extend([f' item def {name} {{', ' doc ' + note({'flow_type': t}), ' }'])
for i, s in enumerate(systems, 1):
lines.extend([f' part def Component{i} {{', ' doc ' + note(s)])
for j, p in enumerate(s.get('ports', []), 1):
lines.extend([f' port port{j} {{', ' doc ' + note(p),
f' {p["direction"]} item payload: {type_names[p["flow_type"]]};', ' }'])
for j, part in enumerate(s.get('parts', []), 1):
lines.extend([f' part member{j}[{part["quantity"]}] {{', ' doc ' + note(part), ' }'])
lines.append(' }')
actions = model.get('behaviour', {}).get('actions', [])
for i, a in enumerate(actions, 1):
lines.extend([f' action def Function{i} {{', ' doc ' + note(a), ' }'])
for i, sm in enumerate(model.get('behaviour', {}).get('state_machines', []), 1):
lines.extend([f' state def Behavior{i} {{', ' doc ' + note(sm)])
for j, state in enumerate(sm['states'], 1):
lines.extend([f' state state{j} {{', ' doc ' + note(state), ' }'])
lines.append(' }')
for i, r in enumerate(model.get('requirements', []), 1):
lines.extend([f' requirement def Requirement{i} {{', ' doc ' + note(r), ' }'])
lines.append(' part systemModel {')
for i, s in enumerate(systems, 1):
lines.append(f' part component{i}: Component{i};')
for i, a in enumerate(actions, 1):
lines.append(f' action function{i}: Function{i};')
lines.append(f' allocate function{i} to component{indices[a["owner"]]};')
for i, sm in enumerate(model.get('behaviour', {}).get('state_machines', []), 1):
lines.extend([f' state behavior{i}: Behavior{i};', ' // ' + note({'owner': sm['owner']})])
flow_lookup = {f['flow_id']: f for f in model.get('item_flows', [])}
count = 0
for s in systems:
for connection in s.get('interfaces', []):
count += 1
a, b = (s['subsystem_id'], connection['port_this']), (connection['mating_subsystem'], connection['port_mate'])
aj, ap = ports[a]; bj, bp = ports[b]
x, y = f'component{indices[a[0]]}.port{aj}', f'component{indices[b[0]]}.port{bj}'
lines.append(' // ' + note({'interface': connection, 'flow': flow_lookup[connection['flow_ref']]}))
if ap['direction'] == bp['direction'] == 'inout':
lines.append(f' connect {x} to {y};')
else:
if ap['direction'] == 'in' or bp['direction'] == 'out':
x, y = y, x
lines.append(f' flow transfer{count} from {x}.payload to {y}.payload;')
action_indices = {a['action_id']: i for i, a in enumerate(actions, 1)}
for allocation in model.get('allocations', []):
lines.extend([' // ' + note(allocation),
f' allocate function{action_indices[allocation["from"]]} to component{indices[allocation["to"]]};'])
lines.extend([' }', '}'])
return '\n'.join(lines) + '\n'