MDAO¶
Requires the mdao extra (pip install "longeron[mdao]").
Project SysML v2 part trees and calcs onto OpenMDAO Problems (spike).
Mapping:
calc def->ExplicitComponentwhosecompute()calls the interpreter (calc_component()).part tree -> nested
Groupper part usage; each derived attribute (value expression referencing other features) becomes anExplicitComponent; free attributes (literal values) becomeIndepVarCompoutputs, so they can be design variables.DISCIPLINE GROUPING: when a derived attribute’s value invokes a calc def that lives in its own package (e.g.
DeepScout::Propulsion), the attribute’s component is housed in an OpenMDAOGroupnamed after that package (outputs promoted, so flat names keep working). The SysML package structure is the source of the grouping – organize the calc defs into discipline packages and the generated N2 shows the classic Aerodynamics / Propulsion / Structures / Performance blocks. Calcs owned by a namespace enclosing the part definition itself are shared context, not disciplines, and stay ungrouped (ProblemBuild.disciplinesrecords the mapping).attribute cross-references ->
connect()between promoted names (chassis.massconnects groupchassis’s promotedmass).assert constraint/ requirementrequirewith a comparison body -> a*_marginoutput (>= 0 iff the predicate holds), ready foradd_constraint.a
calc defannotated@ExternalAnalysis { component = "module:attr"; }declares the I/O contract of a higher-fidelity tool wrapped as an OpenMDAOExplicitComponent. When a derived attribute’s value is a direct invocation of such a calc,build_problem()can instantiate the referenced component instead of the interpreter-backed expression (fidelity={"CalcName": "external"}; bodiless annotated calcs bind externally by default), after validating the declared parameter names against the component’s actual inputs/outputs. The declared contract is the point: SysML owns the interface, the tool owns the physics, and both fidelities compose with the interpreter-backed components in oneProblem.
Object-valued I/O (the ratified docs/design/mdao-objects.md) rides
OpenMDAO’s stock discrete-variable machinery – no fork, no patched
internals:
ENTITY BINDING: a part/item member typed by a
variationdefinition becomes ONE discrete input carrying the configured M0 individual (longeron.m0.Individual– resolved attribute values, stableqname#indexidentity, definition backlink), instead of a scalar shred. The case being evaluated is anlongeron.m0. Interpretation(build_problem(..., interpretation=...)); without one, a model that has variation points lazily materializes the implicit anonymous interpretation (first declared variants – the same choice the interpreter makes), and scalar-only models behave exactly as before.bind_entity()rebinds a variation point to another individual (qname or instance, conformance-checked, pickle-checked);entity_cases()turns a trade study’s variation points intoom.ListGenerator-shaped DOE cases whose values are individuals. Homogeneous[n]multiplicities bind their (shared) per-unit individual; per-index heterogeneous selection is deferred with the trades phase-2 item.RESULT RECORDING:
record_case()returns a NEW immutable interpretation snapshot per case – the case’s individuals with the problem’s outputs written onto their slots (stable ids, JSON-cleanto_dict). The input interpretation stays pristine; in-placeInstance.set()remains available for interactive use but is outside the recorded lifecycle.case_values()feeds a snapshot straight into the scoreboard’svalues=seam.OBJECT FLOW: structured payloads (mesh dicts, cadquery recipes – never live kernel solids) move between components as discrete values, keyed by M0 individual id (
geometry.tag_parts). Serial OpenMDAO passes discretes BY REFERENCE: payloads are frozen by convention (producers emit fresh dicts, consumers never mutate). Under MPI every discrete crossing a rank boundary is pickled – picklability is asserted at bind time with an error naming the offender.FILE BOUNDARY:
FileArtifact(path + sha256 + media type) flows as a tiny discrete value while the bytes stay on disk –ExternalCodeComp-compatible, recorder-friendly (itsto_jsonis the losslessmake_serializablehook), and the hash is the caching identity.write_artifact()/file_artifact()build one;artifact_component()wraps a writer callable as a boundary component. The matching SysML convention ships asexamples/analysis_conventions.sysml(item def FileArtifact), so flows can be typed by it.ITEM-FLOW WIRING:
derive_flows()resolves a part’sflow of Payload from a.out to b.inusages against its action members and returns proposed OpenMDAO connections (endpoints validated, payload type checked against both ends);apply_flows()wires them. Propose + apply, never silent magic.
Requires the mdao extra: pip install "longeron[mdao]". OpenMDAO is
imported lazily so the module can be imported (for docs, dir()) without it.
- longeron.analysis.mdao.Fidelity¶
one calc’s evaluation route: its model body through the interpreter, or the wrapped higher-fidelity tool its
@ExternalAnalysisannotation declares (build_problem(fidelity={"CalcName": "external"}))alias of
Literal[‘model’, ‘external’]
- class longeron.analysis.mdao.FileArtifact(path, sha256, media_type='application/octet-stream')[source]¶
Bases:
objectA file crossing the analysis boundary: a path plus content identity.
Flows as a tiny discrete value while the bytes stay on disk. The hash is the caching identity (same recipe, same hash – skip the external run) and the recorder-bloat fix:
to_jsonis the hook OpenMDAO’smake_serializabletries first, so a recorded case reads back this record losslessly instead of megabytes of payload (or a silent class-name string). Consumers handpathtoExternalCodeComp’sexternal_input_filesor their own subprocess. The matching SysML convention is theitem def FileArtifactinexamples/analysis_conventions.sysml(attributespath/sha256/mediaType), so flows can be typed by it.
- class longeron.analysis.mdao.ProblemBuild(problem, independents=<factory>, derived=<factory>, constraints=<factory>, gaps=<factory>, externals=<factory>, disciplines=<factory>, entities=<factory>, interpretation=None, _interp=None, _target=None)[source]¶
Bases:
objectA built (not yet run) OpenMDAO problem plus its SysML bookkeeping.
- interpretation: Interpretation | None = None¶
the case being evaluated (
Noneuntilrecord_case()or a variation point asks – the design’s lazy implicit interpretation)
- longeron.analysis.mdao.add_optimization(build, objective, design_vars, maximize=False, constraints=None)[source]¶
Configure SLSQP over margin constraints; call before
setup().- Return type:
- longeron.analysis.mdao.apply_flows(target, flows)[source]¶
Wire derived flow triples as
connect()calls (before setup).targetis aProblemBuild, anom.Problem, or a group; components are expected under the action names the endpoints use (build.mesh -> rcs.meshconnects componentbuild’smeshoutput to componentrcs’smeshinput, discrete or continuous).- Return type:
- longeron.analysis.mdao.artifact_component(write, directory, *, payload='payload', artifact='artifact', media_type='application/octet-stream', payload_default=None)[source]¶
A boundary component: write the payload to disk, emit the artifact.
write(payload_value, directory)produces the file (STEP, STL, JSON, …) and returns its path; the component hashes it and emits aFileArtifactas theartifactdiscrete output. This is theExternalCodeComp-compatible pattern: downstream components (or the external code’sexternal_input_files) consume the path, the recorder sees ~200 bytes of JSON. Keydirectoryper case (e.g. by interpretation id + case counter) so concurrent DOE cases never collide.payload_defaulttypes the discrete input’s default (OpenMDAO checks connection compatibility byisinstanceon declared defaults; the default{}suits mesh/recipe dicts).- Return type:
- longeron.analysis.mdao.bind_entity(build, feature, entity)[source]¶
Rebind a variation point to an individual (the discrete case swap).
featureis a promoted entity input of the build (seeProblemBuild.entities);entityis either anInstance(typically anIndividualfromlongeron.m0.interpret()orentity_cases()) or a qualified name resolved through the model – a catalog part def ("ScoutParts::TMotorMn4006") or a variant usage ("P::MotorChoice::light", which keeps its inline:>>redefinitions). The individual’s definition is checked for conformance against the variation point’s base type, and the payload must pickle (MPI ranks, case recording). Call betweensetup()andrun_model(); homogeneous[n]members rebind their shared per-unit individual.- Return type:
- longeron.analysis.mdao.build_problem(model, part, requirements=(), setup=True, fidelity=None, interpretation=None)[source]¶
Build an OpenMDAO
Problemmirroring a part definition’s tree.fidelityselects, per@ExternalAnalysis-annotated calc def (keyed by name or qualified name), whether a directattribute x = Calc(...)value evaluates the calc’s first-order body through the interpreter ('model', the default when a body exists) or instantiates the annotated external component ('external'; the default – and only – choice when the calc declares no body). The two fidelities are drop-in replacements, so lo-fi/hi-fi swap studies are one keyword away.interpretationis the M0 case being evaluated (longeron.m0.interpret()): free scalars seed from its slots, and every variation-typed part/item member becomes a discrete input carrying its configured individual (seebind_entity()to swap cases,record_case()to freeze results). Without it, scalar-only models behave exactly as before; a model that has variation points materializes the implicit anonymous interpretation lazily (first declared variants, zero ceremony).- Return type:
- longeron.analysis.mdao.calc_component(interp, calc, out_name='result')[source]¶
Wrap a
calc defas anExplicitComponent(FD partials).- Return type:
- longeron.analysis.mdao.case_values(case)[source]¶
A scoreboard
values=dict from a recorded case snapshot.The snapshot’s root-level scalar (and boolean) slots, keyed by feature name – the same shape as
architecture_values(), soscoreboard(model, values=case_values(snapshot))scores a recorded case directly.
- longeron.analysis.mdao.derive_flows(model, part)[source]¶
Resolved (source, target, payload qname) triples from a part’s flows.
Each
flow of Payload from a.out to b.inusage is resolved against the part’s members through the resolver’s specialization walk (the same semantics asvalidate()’sdangling-flow/flow-payload-mismatchdiagnostics): every endpoint must resolve, the source’s final hop must be anout/inoutparameter, the target’s anin/inoutparameter, and – when both the payload typing and the target end’s typing are KNOWN – some pair of them must be related by specialization in either direction. Violations raiseAnalysisErrornaming the offending endpoint; unknown typing stays silent, exactly like the validator. Succession flows (control ordering, no payload) are skipped.The triples are proposals: pass them to
apply_flows()to wire an OpenMDAO problem whose component names mirror the action names. Nothing is connected implicitly.
- longeron.analysis.mdao.entity_cases(study, *points)[source]¶
DOE cases over a trade study’s catalog: one case per mix.
Walks the study’s variation points (all of them, or the named subset) and returns the full Cartesian product in
om.ListGeneratorshape – one[(point, individual), ...]list per case, where each value is the variant’s M0 individual (longeron.m0.interpret()of the variant usage, so inline:>>redefinitions are honored). Feed them to aDOEDriverafteradd_design_var()-ing each point:cases = mdao.entity_cases(study, "motors", "props") build.problem.model.add_design_var("motors") build.problem.model.add_design_var("props") build.problem.driver = om.DOEDriver(om.ListGenerator(cases))
The verify design’s covering arrays slot in later as another generator of the same currency: a population of interpretations.
- longeron.analysis.mdao.external_binding(calc)[source]¶
The
@ExternalAnalysiscomponent spec of a calc def, if any.The annotation’s
componentvalue must be a string literal of the form'package.module:attr'whereattris anom.ExplicitComponentsubclass or a zero-argument factory returning one. Matching is by metadata-definition name (ExternalAnalysis), the convention shipped with the DeepScout program (examples/deepscout).
- longeron.analysis.mdao.file_artifact(path, media_type='application/octet-stream')[source]¶
A
FileArtifactfor an existing file (contents hashed).- Return type:
- longeron.analysis.mdao.record_case(build, outputs=None)[source]¶
A new interpretation snapshot: the case’s individuals + the outputs.
Call after
run_model(). The build’s interpretation (created lazily when absent – the design’s implicit anonymous point) is copied, rebound entities are reflected (their positional individual ids stay stable across mixes), and every promoted output lands as an attribute value on the matching individual’s slot.outputsoverrides the default set (independents + derived attributes + constraint margins, read from the problem). The result is an immutable-by-convention snapshot: the input interpretation stays pristine, re-recording never overwrites evidence,to_dict()is JSON-clean, androllup()/case_values()work as usual. Values that fit no slot path are noted in the snapshot’sgaps.- Return type: